From 9495518353004e4dae2ebb3b8a4042016e1c424e Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:40:45 +0300 Subject: [PATCH 01/36] Use chainParams from base observer --- zetaclient/chains/base/observer.go | 11 +++- zetaclient/chains/base/observer_test.go | 9 --- zetaclient/chains/bitcoin/observer/inbound.go | 12 ++-- .../chains/bitcoin/observer/observer.go | 34 +++-------- .../chains/bitcoin/observer/outbound.go | 4 +- .../chains/bitcoin/observer/rpc_status.go | 2 +- zetaclient/chains/evm/observer/inbound.go | 14 ++--- zetaclient/chains/evm/observer/observer.go | 26 ++------ .../chains/evm/observer/observer_gas.go | 8 +-- zetaclient/chains/evm/observer/outbound.go | 4 +- .../chains/evm/observer/outbound_test.go | 2 +- zetaclient/chains/evm/observer/rpc_status.go | 2 +- zetaclient/chains/interfaces/interfaces.go | 10 ++-- zetaclient/chains/solana/observer/inbound.go | 2 +- .../chains/solana/observer/inbound_tracker.go | 4 +- zetaclient/chains/solana/observer/observer.go | 16 ----- .../chains/solana/observer/observer_gas.go | 8 +-- zetaclient/chains/solana/observer/outbound.go | 4 +- .../chains/solana/observer/rpc_status.go | 2 +- zetaclient/chains/ton/observer/observer.go | 44 ++++++++++++++ .../chains/ton/observer/observer_test.go | 60 +------------------ zetaclient/orchestrator/orchestrator.go | 12 ++-- zetaclient/orchestrator/orchestrator_test.go | 6 +- zetaclient/testutils/mocks/chain_clients.go | 30 +++++----- 24 files changed, 130 insertions(+), 196 deletions(-) create mode 100644 zetaclient/chains/ton/observer/observer.go diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 020fd323f2..0a65ba2bc1 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -186,13 +186,18 @@ func (ob *Observer) WithChain(chain chains.Chain) *Observer { // ChainParams returns the chain params for the observer. func (ob *Observer) ChainParams() observertypes.ChainParams { + ob.mu.Lock() + defer ob.mu.Unlock() + return ob.chainParams } -// WithChainParams attaches a new chain params to the observer. -func (ob *Observer) WithChainParams(params observertypes.ChainParams) *Observer { +// SetChainParams attaches a new chain params to the observer. +func (ob *Observer) SetChainParams(params observertypes.ChainParams) { + ob.mu.Lock() + defer ob.mu.Unlock() + ob.chainParams = params - return ob } // ZetacoreClient returns the zetacore client for the observer. diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 01713f6c4c..bbf24ccd92 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -168,15 +168,6 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) - t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t, chain, defaultAlertLatency) - - // update chain params - newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) - ob = ob.WithChainParams(newChainParams) - require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) - }) - t.Run("should be able to update zetacore client", func(t *testing.T) { ob := createObserver(t, chain, defaultAlertLatency) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 5985d6f574..01205026fd 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -35,7 +35,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { return err } - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) + ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.ChainParams().InboundTicker) if err != nil { ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") return err @@ -65,7 +65,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") } } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) + ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) case <-ob.StopChannel(): ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return nil @@ -105,13 +105,13 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { ob.WithLastBlock(lastBlock) // skip if current height is too low - if lastBlock < ob.GetChainParams().ConfirmationCount { + if lastBlock < ob.ChainParams().ConfirmationCount { return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", currentBlock) } // skip if no new block is confirmed lastScanned := ob.LastBlockScanned() - if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { + if lastScanned >= lastBlock-ob.ChainParams().ConfirmationCount { return nil } @@ -192,7 +192,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { return err } - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInboundTracker", ob.GetChainParams().InboundTicker) + ticker, err := types.NewDynamicTicker("Bitcoin_WatchInboundTracker", ob.ChainParams().InboundTicker) if err != nil { ob.logger.Inbound.Err(err).Msg("error creating ticker") return err @@ -211,7 +211,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { Err(err). Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) + ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) case <-ob.StopChannel(): ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return nil diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 50de502b79..68e6f91022 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -181,22 +181,6 @@ func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { ob.btcClient = client } -// SetChainParams sets the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) SetChainParams(params observertypes.ChainParams) { - ob.Mu().Lock() - defer ob.Mu().Unlock() - ob.WithChainParams(params) -} - -// GetChainParams returns the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) GetChainParams() observertypes.ChainParams { - ob.Mu().Lock() - defer ob.Mu().Unlock() - return ob.ChainParams() -} - // Start starts the Go routine processes to observe the Bitcoin chain func (ob *Observer) Start(ctx context.Context) { if noop := ob.Observer.Start(); noop { @@ -238,12 +222,12 @@ func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { if amount.Cmp(big.NewInt(BigValueSats)) >= 0 { return BigValueConfirmationCount } - if BigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { + if BigValueConfirmationCount < ob.ChainParams().ConfirmationCount { return BigValueConfirmationCount } // #nosec G115 always in range - return int64(ob.GetChainParams().ConfirmationCount) + return int64(ob.ChainParams().ConfirmationCount) } // WatchGasPrice watches Bitcoin chain for gas rate and post to zetacore @@ -257,25 +241,25 @@ func (ob *Observer) WatchGasPrice(ctx context.Context) error { } // start gas price ticker - ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.GetChainParams().GasPriceTicker) + ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.ChainParams().GasPriceTicker) if err != nil { return errors.Wrapf(err, "NewDynamicTicker error") } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.ChainParams().GasPriceTicker) defer ticker.Stop() for { select { case <-ticker.C(): - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } err := ob.PostGasPrice(ctx) if err != nil { ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) + ticker.UpdateInterval(ob.ChainParams().GasPriceTicker, ob.logger.GasPrice) case <-ob.StopChannel(): ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return nil @@ -378,7 +362,7 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n // WatchUTXOs watches bitcoin chain for UTXOs owned by the TSS address // TODO(revamp): move ticker related functions to a specific file func (ob *Observer) WatchUTXOs(ctx context.Context) error { - ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOs", ob.GetChainParams().WatchUtxoTicker) + ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOs", ob.ChainParams().WatchUtxoTicker) if err != nil { ob.logger.UTXOs.Error().Err(err).Msg("error creating ticker") return err @@ -388,7 +372,7 @@ func (ob *Observer) WatchUTXOs(ctx context.Context) error { for { select { case <-ticker.C(): - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } err := ob.FetchUTXOs(ctx) @@ -403,7 +387,7 @@ func (ob *Observer) WatchUTXOs(ctx context.Context) error { ob.logger.UTXOs.Debug().Err(err).Msg("No wallet is loaded") } } - ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) + ticker.UpdateInterval(ob.ChainParams().WatchUtxoTicker, ob.logger.UTXOs) case <-ob.StopChannel(): ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) return nil diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 01b1d16609..13c0386253 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -32,7 +32,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { return errors.Wrap(err, "unable to get app from context") } - ticker, err := types.NewDynamicTicker("Bitcoin_WatchOutbound", ob.GetChainParams().OutboundTicker) + ticker, err := types.NewDynamicTicker("Bitcoin_WatchOutbound", ob.ChainParams().OutboundTicker) if err != nil { return errors.Wrap(err, "unable to create dynamic ticker") } @@ -106,7 +106,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) } } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) + ticker.UpdateInterval(ob.ChainParams().OutboundTicker, ob.logger.Outbound) case <-ob.StopChannel(): ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) return nil diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 008d49aa59..58ccdfc558 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -16,7 +16,7 @@ func (ob *Observer) watchRPCStatus(_ context.Context) error { for { select { case <-ticker.C: - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 0e1cb6b84d..0ad5ba6b6c 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -39,7 +39,7 @@ import ( // TODO(revamp): move ticker function to a separate file func (ob *Observer) WatchInbound(ctx context.Context) error { sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) - interval := ticker.SecondsFromUint64(ob.GetChainParams().InboundTicker) + interval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) task := func(ctx context.Context, t *ticker.Ticker) error { return ob.watchInboundOnce(ctx, t, sampledLogger) } @@ -74,7 +74,7 @@ func (ob *Observer) watchInboundOnce(ctx context.Context, t *ticker.Ticker, samp ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - newInterval := ticker.SecondsFromUint64(ob.GetChainParams().InboundTicker) + newInterval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) t.SetInterval(newInterval) return nil @@ -91,7 +91,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), - ob.GetChainParams().InboundTicker, + ob.ChainParams().InboundTicker, ) if err != nil { ob.Logger().Inbound.Err(err).Msg("error creating ticker") @@ -110,7 +110,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { if err != nil { ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.Logger().Inbound) case <-ob.StopChannel(): ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return nil @@ -191,10 +191,10 @@ func (ob *Observer) ObserveInbound(ctx context.Context, sampledLogger zerolog.Lo metrics.GetBlockByNumberPerChain.WithLabelValues(ob.Chain().Name).Inc() // skip if current height is too low - if blockNumber < ob.GetChainParams().ConfirmationCount { + if blockNumber < ob.ChainParams().ConfirmationCount { return fmt.Errorf("observeInbound: skipping observer, current block number %d is too low", blockNumber) } - confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount + confirmedBlockNum := blockNumber - ob.ChainParams().ConfirmationCount // skip if no new block is confirmed lastScanned := ob.LastBlockScanned() @@ -615,7 +615,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( // HasEnoughConfirmations checks if the given receipt has enough confirmations func (ob *Observer) HasEnoughConfirmations(receipt *ethtypes.Receipt, lastHeight uint64) bool { - confHeight := receipt.BlockNumber.Uint64() + ob.GetChainParams().ConfirmationCount + confHeight := receipt.BlockNumber.Uint64() + ob.ChainParams().ConfirmationCount return lastHeight >= confHeight } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 39de0eedea..68c0989672 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -116,39 +116,23 @@ func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { ob.evmJSONRPC = client } -// SetChainParams sets the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) SetChainParams(params observertypes.ChainParams) { - ob.Mu().Lock() - defer ob.Mu().Unlock() - ob.WithChainParams(params) -} - -// GetChainParams returns the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) GetChainParams() observertypes.ChainParams { - ob.Mu().Lock() - defer ob.Mu().Unlock() - return ob.ChainParams() -} - // GetConnectorContract returns the non-Eth connector address and binder func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) + addr := ethcommon.HexToAddress(ob.ChainParams().ConnectorContractAddress) contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) return addr, contract, err } // GetConnectorContractEth returns the Eth connector address and binder func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) + addr := ethcommon.HexToAddress(ob.ChainParams().ConnectorContractAddress) contract, err := FetchConnectorContractEth(addr, ob.evmClient) return addr, contract, err } // GetERC20CustodyContract returns ERC20Custody contract address and binder func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) + addr := ethcommon.HexToAddress(ob.ChainParams().Erc20CustodyContractAddress) contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } @@ -158,14 +142,14 @@ func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody. // this simplify the migration process v1 will be completely removed in the future // currently the ABI for withdraw is identical, therefore both contract instances can be used func (ob *Observer) GetERC20CustodyV2Contract() (ethcommon.Address, *erc20custodyv2.ERC20Custody, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) + addr := ethcommon.HexToAddress(ob.ChainParams().Erc20CustodyContractAddress) contract, err := erc20custodyv2.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } // GetGatewayContract returns the gateway contract address and binder func (ob *Observer) GetGatewayContract() (ethcommon.Address, *gatewayevm.GatewayEVM, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().GatewayAddress) + addr := ethcommon.HexToAddress(ob.ChainParams().GatewayAddress) contract, err := gatewayevm.NewGatewayEVM(addr, ob.evmClient) return addr, contract, err } diff --git a/zetaclient/chains/evm/observer/observer_gas.go b/zetaclient/chains/evm/observer/observer_gas.go index 754f0349c5..3ebca1d3a9 100644 --- a/zetaclient/chains/evm/observer/observer_gas.go +++ b/zetaclient/chains/evm/observer/observer_gas.go @@ -22,27 +22,27 @@ func (ob *Observer) WatchGasPrice(ctx context.Context) error { // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchGasPrice_%d", ob.Chain().ChainId), - ob.GetChainParams().GasPriceTicker, + ob.ChainParams().GasPriceTicker, ) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return err } ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.ChainParams().GasPriceTicker) defer ticker.Stop() for { select { case <-ticker.C(): - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } err = ob.PostGasPrice(ctx) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + ticker.UpdateInterval(ob.ChainParams().GasPriceTicker, ob.Logger().GasPrice) case <-ob.StopChannel(): ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") return nil diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 2534c47aab..0bab913592 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -44,7 +44,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { chainID := ob.Chain().ChainId ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), - ob.GetChainParams().OutboundTicker, + ob.ChainParams().OutboundTicker, ) if err != nil { ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") @@ -72,7 +72,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { Msgf("WatchOutbound: error ProcessOutboundTrackers for chain %d", chainID) } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) + ticker.UpdateInterval(ob.ChainParams().OutboundTicker, ob.Logger().Outbound) case <-ob.StopChannel(): ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") return nil diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 7b8e47f40f..5011e5660a 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -105,7 +105,7 @@ func Test_IsOutboundProcessed(t *testing.T) { ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail - chainParamsNew := ob.GetChainParams() + chainParamsNew := ob.ChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() ob.SetChainParams(chainParamsNew) continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) diff --git a/zetaclient/chains/evm/observer/rpc_status.go b/zetaclient/chains/evm/observer/rpc_status.go index c63e9e775a..68c7629523 100644 --- a/zetaclient/chains/evm/observer/rpc_status.go +++ b/zetaclient/chains/evm/observer/rpc_status.go @@ -17,7 +17,7 @@ func (ob *Observer) watchRPCStatus(ctx context.Context) error { for { select { case <-ticker.C: - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 36f3fcad5b..9c9cb3fbbb 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -41,13 +41,11 @@ const ( type ChainObserver interface { Start(ctx context.Context) Stop() - VoteOutboundIfConfirmed( - ctx context.Context, - cctx *crosschaintypes.CrossChainTx, - ) (bool, error) + + ChainParams() observertypes.ChainParams SetChainParams(observertypes.ChainParams) - GetChainParams() observertypes.ChainParams - WatchInboundTracker(ctx context.Context) error + + VoteOutboundIfConfirmed(ctx context.Context, cctx *crosschaintypes.CrossChainTx) (bool, error) } // ChainSigner is the interface to sign transactions for a chain diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index d9819bd53c..1441150ada 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -38,7 +38,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("Solana_WatchInbound_%d", ob.Chain().ChainId), - ob.GetChainParams().InboundTicker, + ob.ChainParams().InboundTicker, ) if err != nil { ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") diff --git a/zetaclient/chains/solana/observer/inbound_tracker.go b/zetaclient/chains/solana/observer/inbound_tracker.go index 70b6e9702f..19f8d26d04 100644 --- a/zetaclient/chains/solana/observer/inbound_tracker.go +++ b/zetaclient/chains/solana/observer/inbound_tracker.go @@ -21,7 +21,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("Solana_WatchInboundTracker_%d", ob.Chain().ChainId), - ob.GetChainParams().InboundTicker, + ob.ChainParams().InboundTicker, ) if err != nil { ob.Logger().Inbound.Err(err).Msg("error creating ticker") @@ -42,7 +42,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { Err(err). Msgf("WatchInboundTracker: error ProcessInboundTrackers for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.Logger().Inbound) case <-ob.StopChannel(): ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return nil diff --git a/zetaclient/chains/solana/observer/observer.go b/zetaclient/chains/solana/observer/observer.go index 634bdfd635..55a5938316 100644 --- a/zetaclient/chains/solana/observer/observer.go +++ b/zetaclient/chains/solana/observer/observer.go @@ -96,22 +96,6 @@ func (ob *Observer) WithSolClient(client interfaces.SolanaRPCClient) { ob.solClient = client } -// SetChainParams sets the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) SetChainParams(params observertypes.ChainParams) { - ob.Mu().Lock() - defer ob.Mu().Unlock() - ob.WithChainParams(params) -} - -// GetChainParams returns the chain params for the observer -// Note: chain params is accessed concurrently -func (ob *Observer) GetChainParams() observertypes.ChainParams { - ob.Mu().Lock() - defer ob.Mu().Unlock() - return ob.ChainParams() -} - // Start starts the Go routine processes to observe the Solana chain func (ob *Observer) Start(ctx context.Context) { if noop := ob.Observer.Start(); noop { diff --git a/zetaclient/chains/solana/observer/observer_gas.go b/zetaclient/chains/solana/observer/observer_gas.go index 80747e2efb..03291d6a54 100644 --- a/zetaclient/chains/solana/observer/observer_gas.go +++ b/zetaclient/chains/solana/observer/observer_gas.go @@ -42,26 +42,26 @@ func (ob *Observer) WatchGasPrice(ctx context.Context) error { // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("Solana_WatchGasPrice_%d", ob.Chain().ChainId), - ob.GetChainParams().GasPriceTicker, + ob.ChainParams().GasPriceTicker, ) if err != nil { return errors.Wrapf(err, "NewDynamicTicker error") } ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.ChainParams().GasPriceTicker) defer ticker.Stop() for { select { case <-ticker.C(): - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } err = ob.PostGasPrice(ctx) if err != nil { ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + ticker.UpdateInterval(ob.ChainParams().GasPriceTicker, ob.Logger().GasPrice) case <-ob.StopChannel(): ob.Logger().GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return nil diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index 7ff968ea93..e185b1a27d 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -36,7 +36,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { chainID := ob.Chain().ChainId ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("Solana_WatchOutbound_%d", chainID), - ob.GetChainParams().OutboundTicker, + ob.ChainParams().OutboundTicker, ) if err != nil { ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") @@ -63,7 +63,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { Msgf("WatchOutbound: error ProcessOutboundTrackers for chain %d", chainID) } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) + ticker.UpdateInterval(ob.ChainParams().OutboundTicker, ob.Logger().Outbound) case <-ob.StopChannel(): ob.Logger().Outbound.Info().Msgf("WatchOutbound: watcher stopped for chain %d", chainID) return nil diff --git a/zetaclient/chains/solana/observer/rpc_status.go b/zetaclient/chains/solana/observer/rpc_status.go index ff3d02f679..1b16492076 100644 --- a/zetaclient/chains/solana/observer/rpc_status.go +++ b/zetaclient/chains/solana/observer/rpc_status.go @@ -17,7 +17,7 @@ func (ob *Observer) watchRPCStatus(ctx context.Context) error { for { select { case <-ticker.C: - if !ob.GetChainParams().IsSupported { + if !ob.ChainParams().IsSupported { continue } diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go new file mode 100644 index 0000000000..d21312c31c --- /dev/null +++ b/zetaclient/chains/ton/observer/observer.go @@ -0,0 +1,44 @@ +package observer + +import ( + "context" + + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" +) + +type Observer struct { + base.Observer + + client *liteapi.Client + gatewayID ton.AccountID +} + +var _ interfaces.ChainObserver = (*Observer)(nil) + +func New(bo *base.Observer, client *liteapi.Client, gatewayID ton.AccountID) (*Observer, error) { + bo.LoadLastTxScanned() + + return &Observer{ + Observer: *bo, + gatewayID: gatewayID, + client: client, + }, nil +} + +func (ob *Observer) Start(ctx context.Context) { + // todo +} + +func (ob *Observer) Stop() { + // todo +} + +func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *types.CrossChainTx) (bool, error) { + // todo + return false, nil +} diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index e978a589b9..e4d0ce4e35 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -1,63 +1,7 @@ package observer -import ( - "context" - "encoding/json" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/tonkeeper/tongo/config" - "github.com/tonkeeper/tongo/liteapi" -) - -// todo tmp (will be resolved automatically) -// taken from ton:8000/lite-client.json -const configRaw = `{"@type":"config.global","dht":{"@type":"dht.config.global","k":3,"a":3,"static_nodes": -{"@type":"dht.nodes","nodes":[]}},"liteservers":[{"id":{"key":"+DjLFqH/N5jO1ZO8PYVYU6a6e7EnnsF0GWFsteE+qy8=","@type": -"pub.ed25519"},"port":4443,"ip":2130706433}],"validator":{"@type":"validator.config.global","zero_state": -{"workchain":-1,"shard":-9223372036854775808,"seqno":0,"root_hash":"rR8EFZNlyj3rfYlMyQC8gT0A6ghDrbKe4aMmodiNw6I=", -"file_hash":"fT2hXGv1OF7XDhraoAELrYz6wX3ue16QpSoWTiPrUAE="},"init_block":{"workchain":-1,"shard":-9223372036854775808, -"seqno":0,"root_hash":"rR8EFZNlyj3rfYlMyQC8gT0A6ghDrbKe4aMmodiNw6I=", -"file_hash":"fT2hXGv1OF7XDhraoAELrYz6wX3ue16QpSoWTiPrUAE="}}}` +import "testing" func TestObserver(t *testing.T) { - t.Skip("skip test") - - ctx := context.Background() - - cfg, err := config.ParseConfig(strings.NewReader(configRaw)) - require.NoError(t, err) - - client, err := liteapi.NewClient(liteapi.WithConfigurationFile(*cfg)) - require.NoError(t, err) - - res, err := client.GetMasterchainInfo(ctx) - require.NoError(t, err) - - // Outputs: - // { - // "Last": { - // "Workchain": 4294967295, - // "Shard": 9223372036854775808, - // "Seqno": 915, - // "RootHash": "2e9e312c5bd3b7b96d23ce1342ac76e5486012c9aac44781c2c25dbc55f5c8ad", - // "FileHash": "d3745319bfaeebb168d9db6bb5b4752b6b28ab9041735c81d4a02fc820040851" - // }, - // "StateRootHash": "02538fb9dc802004012285a90a7af9ba279706e2deea9ca635decd80e94a7045", - // "Init": { - // "Workchain": 4294967295, - // "RootHash": "ad1f04159365ca3deb7d894cc900bc813d00ea0843adb29ee1a326a1d88dc3a2", - // "FileHash": "7d3da15c6bf5385ed70e1adaa0010bad8cfac17dee7b5e90a52a164e23eb5001" - // } - // } - t.Logf("Masterchain info") - logJSON(t, res) -} - -func logJSON(t *testing.T, v any) { - b, err := json.MarshalIndent(v, "", " ") - require.NoError(t, err) - - t.Log(string(b)) + // todo } diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 6b2b673f4e..7594ffd763 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -228,7 +228,7 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in // update chain observer chain parameters var ( - curParams = observer.GetChainParams() + curParams = observer.ChainParams() freshParams = chain.Params() ) @@ -447,11 +447,11 @@ func (oc *Orchestrator) ScheduleCctxEVM( for _, v := range res { trackerMap[v.Nonce] = true } - outboundScheduleLookahead := observer.GetChainParams().OutboundScheduleLookahead + outboundScheduleLookahead := observer.ChainParams().OutboundScheduleLookahead // #nosec G115 always in range outboundScheduleLookback := uint64(float64(outboundScheduleLookahead) * evmOutboundLookbackFactor) // #nosec G115 positive - outboundScheduleInterval := uint64(observer.GetChainParams().OutboundScheduleInterval) + outboundScheduleInterval := uint64(observer.ChainParams().OutboundScheduleInterval) criticalInterval := uint64(10) // for critical pending outbound we reduce re-try interval nonCriticalInterval := outboundScheduleInterval * 2 // for non-critical pending outbound we increase re-try interval @@ -546,8 +546,8 @@ func (oc *Orchestrator) ScheduleCctxBTC( return } // #nosec G115 positive - interval := uint64(observer.GetChainParams().OutboundScheduleInterval) - lookahead := observer.GetChainParams().OutboundScheduleLookahead + interval := uint64(observer.ChainParams().OutboundScheduleInterval) + lookahead := observer.ChainParams().OutboundScheduleLookahead // schedule at most one keysign per ticker for idx, cctx := range cctxList { @@ -618,7 +618,7 @@ func (oc *Orchestrator) ScheduleCctxSolana( return } // #nosec G701 positive - interval := uint64(observer.GetChainParams().OutboundScheduleInterval) + interval := uint64(observer.ChainParams().OutboundScheduleInterval) // schedule keysign for each pending cctx for _, cctx := range cctxList { diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index b15b93bb54..739c299e00 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -196,7 +196,7 @@ func Test_GetUpdatedChainObserver(t *testing.T) { chainOb, err := orchestrator.resolveObserver(appContext, evmChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) - require.True(t, observertypes.ChainParamsEqual(*evmChainParamsNew, chainOb.GetChainParams())) + require.True(t, observertypes.ChainParamsEqual(*evmChainParamsNew, chainOb.ChainParams())) }) t.Run("btc chain observer should not be found", func(t *testing.T) { @@ -244,7 +244,7 @@ func Test_GetUpdatedChainObserver(t *testing.T) { chainOb, err := orchestrator.resolveObserver(appContext, btcChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) - require.True(t, observertypes.ChainParamsEqual(*btcChainParamsNew, chainOb.GetChainParams())) + require.True(t, observertypes.ChainParamsEqual(*btcChainParamsNew, chainOb.ChainParams())) }) t.Run("solana chain observer should not be found", func(t *testing.T) { orchestrator := mockOrchestrator( @@ -282,7 +282,7 @@ func Test_GetUpdatedChainObserver(t *testing.T) { chainOb, err := orchestrator.resolveObserver(appContext, solChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) - require.True(t, observertypes.ChainParamsEqual(*solChainParamsNew, chainOb.GetChainParams())) + require.True(t, observertypes.ChainParamsEqual(*solChainParamsNew, chainOb.ChainParams())) }) } diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index 94f636bf4e..aa5e36889b 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -15,12 +15,12 @@ var _ interfaces.ChainObserver = (*EVMObserver)(nil) // EVMObserver is a mock of evm chain observer for testing type EVMObserver struct { - ChainParams observertypes.ChainParams + chainParams observertypes.ChainParams } func NewEVMObserver(chainParams *observertypes.ChainParams) *EVMObserver { return &EVMObserver{ - ChainParams: *chainParams, + chainParams: *chainParams, } } @@ -35,11 +35,11 @@ func (ob *EVMObserver) VoteOutboundIfConfirmed( } func (ob *EVMObserver) SetChainParams(chainParams observertypes.ChainParams) { - ob.ChainParams = chainParams + ob.chainParams = chainParams } -func (ob *EVMObserver) GetChainParams() observertypes.ChainParams { - return ob.ChainParams +func (ob *EVMObserver) ChainParams() observertypes.ChainParams { + return ob.chainParams } func (ob *EVMObserver) GetTxID(_ uint64) string { @@ -57,12 +57,12 @@ var _ interfaces.ChainObserver = (*BTCObserver)(nil) // BTCObserver is a mock of btc chain observer for testing type BTCObserver struct { - ChainParams observertypes.ChainParams + chainParams observertypes.ChainParams } func NewBTCObserver(chainParams *observertypes.ChainParams) *BTCObserver { return &BTCObserver{ - ChainParams: *chainParams, + chainParams: *chainParams, } } @@ -78,11 +78,11 @@ func (ob *BTCObserver) VoteOutboundIfConfirmed( } func (ob *BTCObserver) SetChainParams(chainParams observertypes.ChainParams) { - ob.ChainParams = chainParams + ob.chainParams = chainParams } -func (ob *BTCObserver) GetChainParams() observertypes.ChainParams { - return ob.ChainParams +func (ob *BTCObserver) ChainParams() observertypes.ChainParams { + return ob.chainParams } func (ob *BTCObserver) GetTxID(_ uint64) string { @@ -98,12 +98,12 @@ var _ interfaces.ChainObserver = (*SolanaObserver)(nil) // SolanaObserver is a mock of solana chain observer for testing type SolanaObserver struct { - ChainParams observertypes.ChainParams + chainParams observertypes.ChainParams } func NewSolanaObserver(chainParams *observertypes.ChainParams) *SolanaObserver { return &SolanaObserver{ - ChainParams: *chainParams, + chainParams: *chainParams, } } @@ -119,11 +119,11 @@ func (ob *SolanaObserver) VoteOutboundIfConfirmed( } func (ob *SolanaObserver) SetChainParams(chainParams observertypes.ChainParams) { - ob.ChainParams = chainParams + ob.chainParams = chainParams } -func (ob *SolanaObserver) GetChainParams() observertypes.ChainParams { - return ob.ChainParams +func (ob *SolanaObserver) ChainParams() observertypes.ChainParams { + return ob.chainParams } func (ob *SolanaObserver) GetTxID(_ uint64) string { From 2824c27e828afae588a6c84d3b3802caaf349b55 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:54:58 +0300 Subject: [PATCH 02/36] Improve base observer Start() semantics --- zetaclient/chains/base/observer.go | 6 +++--- zetaclient/chains/bitcoin/observer/observer.go | 2 +- zetaclient/chains/evm/observer/observer.go | 2 +- zetaclient/chains/solana/observer/observer.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 0a65ba2bc1..e9e8e05ba2 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -135,19 +135,19 @@ func NewObserver( return &ob, nil } -// Start starts the observer. Returns true if the observer was already started (noop). +// Start starts the observer. Returns false started (noop). func (ob *Observer) Start() bool { ob.mu.Lock() defer ob.Mu().Unlock() // noop if ob.started { - return true + return false } ob.started = true - return false + return true } // Stop notifies all goroutines to stop and closes the database. diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 68e6f91022..6bcdc7fcf6 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -183,7 +183,7 @@ func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { // Start starts the Go routine processes to observe the Bitcoin chain func (ob *Observer) Start(ctx context.Context) { - if noop := ob.Observer.Start(); noop { + if ok := ob.Observer.Start(); !ok { ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) return } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 68c0989672..8823b13c47 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -174,7 +174,7 @@ func FetchZetaTokenContract( // Start all observation routines for the evm chain func (ob *Observer) Start(ctx context.Context) { - if noop := ob.Observer.Start(); noop { + if ok := ob.Observer.Start(); !ok { ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) return } diff --git a/zetaclient/chains/solana/observer/observer.go b/zetaclient/chains/solana/observer/observer.go index 55a5938316..0548fcd6d3 100644 --- a/zetaclient/chains/solana/observer/observer.go +++ b/zetaclient/chains/solana/observer/observer.go @@ -98,7 +98,7 @@ func (ob *Observer) WithSolClient(client interfaces.SolanaRPCClient) { // Start starts the Go routine processes to observe the Solana chain func (ob *Observer) Start(ctx context.Context) { - if noop := ob.Observer.Start(); noop { + if ok := ob.Observer.Start(); !ok { ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) return } From 3e3e386d9addb247c335a1547a8bb63a412ea6a5 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:25:38 +0300 Subject: [PATCH 03/36] Add pkg/ticker options --- pkg/ticker/ticker.go | 79 ++++++++++++++++------- pkg/ticker/ticker_test.go | 50 ++++++++++++++ zetaclient/chains/evm/observer/inbound.go | 18 ++---- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 566fc03f8b..0e12db2009 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -32,16 +32,19 @@ import ( "sync" "time" - "cosmossdk.io/errors" + "github.com/rs/zerolog" ) +// Task is a function that will be called by the Ticker +type Task func(ctx context.Context, t *Ticker) error + // Ticker represents a ticker that will run a function periodically. // It also invokes BEFORE ticker starts. type Ticker struct { - interval time.Duration - ticker *time.Ticker - task Task - signalChan chan struct{} + interval time.Duration + ticker *time.Ticker + task Task + internalStopChan chan struct{} // runnerMu is a mutex to prevent double run runnerMu sync.Mutex @@ -50,24 +53,45 @@ type Ticker struct { stateMu sync.Mutex stopped bool + + externalStopChan <-chan struct{} + logger zerolog.Logger } -// Task is a function that will be called by the Ticker -type Task func(ctx context.Context, t *Ticker) error +// Opt is a configuration option for the Ticker. +type Opt func(*Ticker) -// New creates a new Ticker. -func New(interval time.Duration, runner Task) *Ticker { - return &Ticker{interval: interval, task: runner} +// 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() + } } -// Run creates and runs a new Ticker. -func Run(ctx context.Context, interval time.Duration, task Task) error { - return New(interval, task).Run(ctx) +// WithStopChan sets the stop channel for the Ticker. +// Please note that stopChan is NOT signalChan. +// Stop channel is a trigger for invoking ticker.Stop(); +func WithStopChan(stopChan <-chan struct{}) Opt { + return func(cfg *Ticker) { cfg.externalStopChan = stopChan } } -// SecondsFromUint64 converts uint64 to time.Duration in seconds. -func SecondsFromUint64(d uint64) time.Duration { - return time.Duration(d) * time.Second +// New creates a new Ticker. +func New(interval time.Duration, task Task, opts ...Opt) *Ticker { + t := &Ticker{ + interval: interval, + task: task, + } + + for _, opt := range opts { + opt(t) + } + + return t +} + +// Run creates and runs a new Ticker. +func Run(ctx context.Context, interval time.Duration, task Task, opts ...Opt) error { + return New(interval, task, opts...).Run(ctx) } // Run runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. @@ -88,12 +112,13 @@ func (t *Ticker) Run(ctx context.Context) (err error) { // setup t.ticker = time.NewTicker(t.interval) - t.signalChan = make(chan struct{}) + t.internalStopChan = make(chan struct{}) t.stopped = false // initial run if err := t.task(ctx, t); err != nil { - return errors.Wrap(err, "ticker task failed") + t.Stop() + return fmt.Errorf("ticker task failed (initial run): %w", err) } for { @@ -102,9 +127,12 @@ func (t *Ticker) Run(ctx context.Context) (err error) { return ctx.Err() case <-t.ticker.C: if err := t.task(ctx, t); err != nil { - return errors.Wrap(err, "ticker task failed") + return fmt.Errorf("ticker task failed: %w", err) } - case <-t.signalChan: + case <-t.externalStopChan: + t.Stop() + return nil + case <-t.internalStopChan: return nil } } @@ -130,11 +158,18 @@ func (t *Ticker) Stop() { defer t.stateMu.Unlock() // noop - if t.stopped || t.signalChan == nil { + if t.stopped || t.internalStopChan == nil { return } - close(t.signalChan) + close(t.internalStopChan) t.stopped = true t.ticker.Stop() + + t.logger.Info().Msgf("Ticker stopped") +} + +// SecondsFromUint64 converts uint64 to time.Duration in seconds. +func SecondsFromUint64(d uint64) time.Duration { + return time.Duration(d) * time.Second } diff --git a/pkg/ticker/ticker_test.go b/pkg/ticker/ticker_test.go index 671091c71f..a86b0eae83 100644 --- a/pkg/ticker/ticker_test.go +++ b/pkg/ticker/ticker_test.go @@ -1,12 +1,15 @@ package ticker import ( + "bytes" "context" "fmt" "testing" "time" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTicker(t *testing.T) { @@ -170,4 +173,51 @@ func TestTicker(t *testing.T) { assert.ErrorIs(t, err, context.DeadlineExceeded) assert.Equal(t, 2, counter) }) + + t.Run("With stop channel", func(t *testing.T) { + // ARRANGE + var ( + tickerInterval = 100 * time.Millisecond + counter = 0 + + stopChan = make(chan struct{}) + sleepBeforeStop = 5*tickerInterval + (10 * time.Millisecond) + ) + + task := func(ctx context.Context, _ *Ticker) error { + t.Logf("Tick %d", counter) + counter++ + + return nil + } + + // ACT + go func() { + time.Sleep(sleepBeforeStop) + close(stopChan) + }() + + err := Run(context.Background(), tickerInterval, task, WithStopChan(stopChan)) + + // ASSERT + require.NoError(t, err) + require.Equal(t, 6, counter) // initial tick + 5 more ticks + }) + + t.Run("With logger", func(t *testing.T) { + // ARRANGE + out := &bytes.Buffer{} + logger := zerolog.New(out) + + // ACT + task := func(ctx context.Context, _ *Ticker) error { + return fmt.Errorf("hey") + } + + err := Run(context.Background(), time.Second, task, WithLogger(logger, "my-task")) + + // ARRANGE + require.ErrorContains(t, err, "hey") + require.Contains(t, out.String(), `{"level":"info","ticker_name":"my-task","message":"Ticker stopped"}`) + }) } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 0ad5ba6b6c..c662fc2d60 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zetaconnector.non-eth.sol" - "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" @@ -44,18 +43,15 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { return ob.watchInboundOnce(ctx, t, sampledLogger) } - t := ticker.New(interval, task) - - bg.Work(ctx, func(_ context.Context) error { - <-ob.StopChannel() - t.Stop() - ob.Logger().Inbound.Info().Msg("WatchInbound stopped") - return nil - }) - ob.Logger().Inbound.Info().Msgf("WatchInbound started") - return t.Run(ctx) + return ticker.Run( + ctx, + interval, + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().Inbound, "WatchInbound"), + ) } func (ob *Observer) watchInboundOnce(ctx context.Context, t *ticker.Ticker, sampledLogger zerolog.Logger) error { From 555d56084bb7e452cf040263e7b2b3384ef538d9 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:41:12 +0300 Subject: [PATCH 04/36] Add TON to pkg/chains & protos --- docs/openapi/openapi.swagger.yaml | 4 + pkg/chains/chain_test.go | 6 +- pkg/chains/chains.go | 48 +++++-- pkg/chains/chains.pb.go | 130 ++++++++++-------- pkg/chains/chains_test.go | 17 ++- .../zetacore/pkg/chains/chains.proto | 3 + .../zetacore/pkg/chains/chains_pb.d.ts | 17 +++ 7 files changed, 150 insertions(+), 75 deletions(-) diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 819122175f..182f2138e8 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -56995,7 +56995,9 @@ definitions: - bitcoin - op_stack - solana_consensus + - catchain_consensus default: ethereum + description: '- catchain_consensus: ton' title: |- Consensus represents the consensus algorithm used by the chain this can represent the consensus of a L1 @@ -57011,6 +57013,7 @@ definitions: - optimism - base - solana + - ton default: eth title: |- Network represents the network of the chain @@ -57045,6 +57048,7 @@ definitions: - no_vm - evm - svm + - tvm default: no_vm title: |- Vm represents the virtual machine type of the chain to support smart diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index d664e9c7fd..2b2dfd7e77 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -73,7 +73,7 @@ func TestChain_Validate(t *testing.T) { chain: chains.Chain{ ChainId: 42, Name: "foo", - Network: chains.Network_solana + 1, + Network: chains.Network_ton + 1, NetworkType: chains.NetworkType_testnet, Vm: chains.Vm_evm, Consensus: chains.Consensus_op_stack, @@ -101,7 +101,7 @@ func TestChain_Validate(t *testing.T) { Name: "foo", Network: chains.Network_base, NetworkType: chains.NetworkType_devnet, - Vm: chains.Vm_svm + 1, + Vm: chains.Vm_tvm + 1, Consensus: chains.Consensus_op_stack, IsExternal: true, }, @@ -115,7 +115,7 @@ func TestChain_Validate(t *testing.T) { Network: chains.Network_base, NetworkType: chains.NetworkType_devnet, Vm: chains.Vm_evm, - Consensus: chains.Consensus_solana_consensus + 1, + Consensus: chains.Consensus_catchain_consensus + 1, IsExternal: true, }, errStr: "invalid consensus", diff --git a/pkg/chains/chains.go b/pkg/chains/chains.go index da449af748..03b993f37e 100644 --- a/pkg/chains/chains.go +++ b/pkg/chains/chains.go @@ -113,6 +113,18 @@ var ( Name: "solana_mainnet", } + TONMainnet = Chain{ + // T[20] O[15] N[14] mainnet[0] :) + ChainId: 2015140, + Network: Network_ton, + NetworkType: NetworkType_mainnet, + Vm: Vm_tvm, + Consensus: Consensus_catchain_consensus, + IsExternal: true, + CctxGateway: CCTXGateway_observers, + Name: "ton_mainnet", + } + /** * Testnet chains */ @@ -225,6 +237,17 @@ var ( Name: "solana_devnet", } + TONTestnet = Chain{ + ChainId: 2015141, + Network: Network_ton, + NetworkType: NetworkType_testnet, + Vm: Vm_tvm, + Consensus: Consensus_catchain_consensus, + IsExternal: true, + CctxGateway: CCTXGateway_observers, + Name: "ton_testnet", + } + /** * Devnet chains */ @@ -301,6 +324,17 @@ var ( Name: "solana_localnet", } + TONLocalnet = Chain{ + ChainId: 2015142, + Network: Network_ton, + NetworkType: NetworkType_privnet, + Vm: Vm_tvm, + Consensus: Consensus_catchain_consensus, + IsExternal: true, + CctxGateway: CCTXGateway_observers, + Name: "ton_localnet", + } + /** * Deprecated chains */ @@ -366,6 +400,9 @@ func DefaultChainsList() []Chain { SolanaMainnet, SolanaDevnet, SolanaLocalnet, + TONMainnet, + TONTestnet, + TONLocalnet, } } @@ -423,17 +460,6 @@ func ChainListByGateway(gateway CCTXGateway, additionalChains []Chain) []Chain { return chainList } -// ChainListForHeaderSupport returns a list of chains that support headers -func ChainListForHeaderSupport(additionalChains []Chain) []Chain { - var chainList []Chain - for _, chain := range CombineDefaultChainsList(additionalChains) { - if chain.Consensus == Consensus_ethereum || chain.Consensus == Consensus_bitcoin { - chainList = append(chainList, chain) - } - } - return chainList -} - // ZetaChainFromCosmosChainID returns a ZetaChain chain object from a Cosmos chain ID func ZetaChainFromCosmosChainID(chainID string) (Chain, error) { ethChainID, err := CosmosToEthChainID(chainID) diff --git a/pkg/chains/chains.pb.go b/pkg/chains/chains.pb.go index 217ad55328..ad5ec2f77a 100644 --- a/pkg/chains/chains.pb.go +++ b/pkg/chains/chains.pb.go @@ -156,6 +156,7 @@ const ( Network_optimism Network = 5 Network_base Network = 6 Network_solana Network = 7 + Network_ton Network = 8 ) var Network_name = map[int32]string{ @@ -167,6 +168,7 @@ var Network_name = map[int32]string{ 5: "optimism", 6: "base", 7: "solana", + 8: "ton", } var Network_value = map[string]int32{ @@ -178,6 +180,7 @@ var Network_value = map[string]int32{ "optimism": 5, "base": 6, "solana": 7, + "ton": 8, } func (x Network) String() string { @@ -229,18 +232,21 @@ const ( Vm_no_vm Vm = 0 Vm_evm Vm = 1 Vm_svm Vm = 2 + Vm_tvm Vm = 3 ) var Vm_name = map[int32]string{ 0: "no_vm", 1: "evm", 2: "svm", + 3: "tvm", } var Vm_value = map[string]int32{ "no_vm": 0, "evm": 1, "svm": 2, + "tvm": 3, } func (x Vm) String() string { @@ -257,11 +263,12 @@ func (Vm) EnumDescriptor() ([]byte, []int) { type Consensus int32 const ( - Consensus_ethereum Consensus = 0 - Consensus_tendermint Consensus = 1 - Consensus_bitcoin Consensus = 2 - Consensus_op_stack Consensus = 3 - Consensus_solana_consensus Consensus = 4 + Consensus_ethereum Consensus = 0 + Consensus_tendermint Consensus = 1 + Consensus_bitcoin Consensus = 2 + Consensus_op_stack Consensus = 3 + Consensus_solana_consensus Consensus = 4 + Consensus_catchain_consensus Consensus = 5 ) var Consensus_name = map[int32]string{ @@ -270,14 +277,16 @@ var Consensus_name = map[int32]string{ 2: "bitcoin", 3: "op_stack", 4: "solana_consensus", + 5: "catchain_consensus", } var Consensus_value = map[string]int32{ - "ethereum": 0, - "tendermint": 1, - "bitcoin": 2, - "op_stack": 3, - "solana_consensus": 4, + "ethereum": 0, + "tendermint": 1, + "bitcoin": 2, + "op_stack": 3, + "solana_consensus": 4, + "catchain_consensus": 5, } func (x Consensus) String() string { @@ -455,56 +464,57 @@ func init() { } var fileDescriptor_236b85e7bff6130d = []byte{ - // 770 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0xcd, 0x8e, 0xe4, 0x34, - 0x10, 0xee, 0x24, 0xfd, 0x5b, 0x3d, 0x3f, 0x5e, 0xef, 0x00, 0x61, 0x25, 0x9a, 0x01, 0x09, 0x68, - 0x8d, 0xa0, 0x47, 0xc0, 0x91, 0x03, 0x68, 0x47, 0x2c, 0x42, 0x88, 0x3d, 0x84, 0xd5, 0x0a, 0x71, - 0x69, 0xdc, 0xee, 0x22, 0x6d, 0x75, 0x6c, 0x47, 0xb1, 0x3b, 0xbb, 0xcd, 0x53, 0xf0, 0x10, 0x1c, - 0x90, 0x78, 0x11, 0x8e, 0x7b, 0xe4, 0x88, 0x66, 0x1e, 0x04, 0x64, 0xc7, 0x49, 0x0f, 0x97, 0x9d, - 0x39, 0xc5, 0xfe, 0xf2, 0x7d, 0x55, 0x5f, 0x55, 0xd9, 0x86, 0x8b, 0x5f, 0xd1, 0x32, 0xbe, 0x61, - 0x42, 0x5d, 0xfa, 0x95, 0xae, 0xf0, 0xb2, 0xdc, 0xe6, 0x97, 0x1e, 0x32, 0xe1, 0xb3, 0x28, 0x2b, - 0x6d, 0x35, 0x7d, 0xa7, 0xe3, 0x2e, 0x5a, 0xee, 0xa2, 0xdc, 0xe6, 0x8b, 0x86, 0xf4, 0xe8, 0x2c, - 0xd7, 0xb9, 0xf6, 0xcc, 0x4b, 0xb7, 0x6a, 0x44, 0xef, 0xff, 0x9b, 0xc0, 0xe0, 0xca, 0x11, 0xe8, - 0xdb, 0x30, 0xf6, 0xcc, 0xa5, 0x58, 0xa7, 0xf1, 0x79, 0x34, 0x4f, 0xb2, 0x91, 0xdf, 0x7f, 0xbb, - 0xa6, 0xdf, 0x01, 0x34, 0xbf, 0x14, 0x93, 0x98, 0x46, 0xe7, 0xd1, 0xfc, 0xe4, 0xb3, 0xf9, 0xe2, - 0xb5, 0xe9, 0x16, 0x3e, 0xe8, 0x53, 0x26, 0xf1, 0x71, 0x9c, 0x46, 0xd9, 0x84, 0xb7, 0x5b, 0xfa, - 0x15, 0x8c, 0x14, 0xda, 0x17, 0xba, 0xda, 0xa6, 0x89, 0x8f, 0xf4, 0xe1, 0x1d, 0x91, 0x9e, 0x36, - 0xec, 0xac, 0x95, 0xd1, 0xef, 0xe1, 0x28, 0x2c, 0x97, 0x76, 0x5f, 0x62, 0xda, 0xf7, 0x61, 0x2e, - 0xee, 0x17, 0xe6, 0xd9, 0xbe, 0xc4, 0x6c, 0xaa, 0x0e, 0x1b, 0xfa, 0x29, 0xc4, 0xb5, 0x4c, 0x07, - 0x3e, 0xc8, 0x7b, 0x77, 0x04, 0x79, 0x2e, 0xb3, 0xb8, 0x96, 0xf4, 0x09, 0x4c, 0xb8, 0x56, 0x06, - 0x95, 0xd9, 0x99, 0x74, 0x78, 0xbf, 0x7e, 0xb4, 0xfc, 0xec, 0x20, 0xa5, 0xef, 0xc2, 0x54, 0x98, - 0x25, 0xbe, 0xb4, 0x58, 0x29, 0x56, 0xa4, 0xa3, 0xf3, 0x68, 0x3e, 0xce, 0x40, 0x98, 0xaf, 0x03, - 0xe2, 0x4a, 0xe5, 0xdc, 0xbe, 0x5c, 0xe6, 0xcc, 0xe2, 0x0b, 0xb6, 0x4f, 0xc7, 0xf7, 0x2a, 0xf5, - 0xea, 0xea, 0xd9, 0x8f, 0xdf, 0x34, 0x8a, 0x6c, 0xea, 0xf4, 0x61, 0x43, 0x29, 0xf4, 0xfd, 0x08, - 0x27, 0xe7, 0xd1, 0x7c, 0x92, 0xf9, 0xf5, 0xc5, 0x17, 0x70, 0x9c, 0x21, 0x47, 0x51, 0xe3, 0x0f, - 0x96, 0xd9, 0x9d, 0xa1, 0x53, 0x18, 0xf1, 0x0a, 0x99, 0xc5, 0x35, 0xe9, 0xb9, 0x8d, 0xd9, 0x71, - 0x8e, 0xc6, 0x90, 0x88, 0x02, 0x0c, 0x7f, 0x61, 0xa2, 0xc0, 0x35, 0x89, 0x1f, 0xf5, 0xff, 0xf8, - 0x7d, 0x16, 0x5d, 0xfc, 0x99, 0xc0, 0xa4, 0x9b, 0x34, 0x9d, 0xc0, 0x00, 0x65, 0x69, 0xf7, 0xa4, - 0x47, 0x4f, 0x61, 0x8a, 0x76, 0xb3, 0x94, 0x4c, 0x28, 0x85, 0x96, 0x44, 0x94, 0xc0, 0x91, 0xb3, - 0xda, 0x21, 0xb1, 0xa3, 0xac, 0x2c, 0xef, 0x80, 0x84, 0x3e, 0x84, 0xd3, 0x52, 0x17, 0xfb, 0x5c, - 0xab, 0x0e, 0xec, 0x7b, 0x96, 0x39, 0xb0, 0x06, 0x94, 0xc2, 0x49, 0xae, 0xb1, 0x2a, 0xc4, 0xd2, - 0xa2, 0xb1, 0x0e, 0x1b, 0x3a, 0x4c, 0xee, 0xe4, 0x8a, 0x1d, 0xb0, 0x51, 0x2b, 0x6c, 0x01, 0xe8, - 0x1c, 0xb4, 0xc8, 0xb4, 0x75, 0xd0, 0x02, 0x47, 0xce, 0x81, 0xc1, 0x52, 0x17, 0xe2, 0xc0, 0x3a, - 0x76, 0x60, 0x48, 0x58, 0x68, 0xce, 0x0a, 0x07, 0x9e, 0xb4, 0xd2, 0x0a, 0x73, 0x47, 0x24, 0xa7, - 0x2e, 0x3a, 0x93, 0x7a, 0xdf, 0xe9, 0x08, 0x3d, 0x03, 0xa2, 0x4b, 0x2b, 0xa4, 0x30, 0xb2, 0xb3, - 0xff, 0xe0, 0x7f, 0x68, 0xc8, 0x45, 0xa8, 0x53, 0xaf, 0x98, 0xc1, 0x8e, 0xf7, 0xb0, 0x43, 0x5a, - 0xce, 0x99, 0x2b, 0xd2, 0xe8, 0x82, 0xa9, 0x43, 0x0f, 0xdf, 0xa0, 0x0f, 0xe0, 0x38, 0x60, 0x6b, - 0xac, 0x1d, 0xf4, 0xa6, 0xaf, 0xa1, 0x81, 0x3a, 0xbb, 0x6f, 0x85, 0x69, 0x21, 0x8c, 0xc2, 0x2d, - 0xa0, 0x23, 0x48, 0xd0, 0x6e, 0x48, 0x8f, 0x8e, 0xa1, 0xef, 0xba, 0x42, 0x22, 0x07, 0xad, 0x2c, - 0x27, 0xb1, 0x9b, 0x79, 0x98, 0x03, 0x49, 0x3c, 0x6a, 0x38, 0xe9, 0xd3, 0x23, 0x18, 0xb7, 0xc6, - 0xc9, 0xc0, 0xc9, 0x9c, 0x3d, 0x32, 0x74, 0x87, 0xa2, 0xc9, 0x47, 0x46, 0x21, 0xcd, 0x13, 0x98, - 0xde, 0xba, 0x6c, 0x2e, 0x5c, 0x6b, 0xd8, 0x9f, 0xa7, 0xb6, 0x43, 0x91, 0x4f, 0x54, 0x89, 0xba, - 0x39, 0x0e, 0x00, 0xc3, 0x50, 0x43, 0x12, 0xe2, 0x7c, 0x04, 0xf1, 0x73, 0xe9, 0x0e, 0x95, 0xd2, - 0xcb, 0x5a, 0x92, 0x9e, 0x37, 0x5d, 0xcb, 0xc6, 0xaa, 0xa9, 0x65, 0x77, 0x0a, 0x7f, 0x86, 0x49, - 0x77, 0xbd, 0x9c, 0x4f, 0xb4, 0x1b, 0xac, 0x70, 0xe7, 0x24, 0x27, 0x00, 0x16, 0xd5, 0x1a, 0x2b, - 0x29, 0x54, 0x48, 0xb9, 0x12, 0x96, 0x6b, 0xa1, 0x48, 0xdc, 0x94, 0xb4, 0x34, 0x96, 0xf1, 0x2d, - 0x49, 0xdc, 0x64, 0x42, 0xe3, 0xba, 0x0b, 0x4a, 0xfa, 0x21, 0xc3, 0xc7, 0x30, 0xbd, 0x75, 0xa9, - 0x9a, 0xa6, 0x79, 0x4b, 0xc7, 0x30, 0xd1, 0x2b, 0x83, 0x55, 0x8d, 0x95, 0x21, 0x51, 0xc3, 0x7e, - 0xfc, 0xe5, 0x5f, 0xd7, 0xb3, 0xe8, 0xd5, 0xf5, 0x2c, 0xfa, 0xe7, 0x7a, 0x16, 0xfd, 0x76, 0x33, - 0xeb, 0xbd, 0xba, 0x99, 0xf5, 0xfe, 0xbe, 0x99, 0xf5, 0x7e, 0xfa, 0x20, 0x17, 0x76, 0xb3, 0x5b, - 0x2d, 0xb8, 0x96, 0xfe, 0x41, 0xff, 0xa4, 0x79, 0xdb, 0x95, 0x5e, 0xdf, 0x7e, 0xd7, 0x57, 0x43, - 0xff, 0x38, 0x7f, 0xfe, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x86, 0x0b, 0xc3, 0x03, 0xff, 0x05, - 0x00, 0x00, + // 789 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0xcd, 0x8e, 0x23, 0x35, + 0x10, 0x4e, 0x77, 0xe7, 0xb7, 0x32, 0x3f, 0x5e, 0xef, 0xb0, 0x34, 0x2b, 0x11, 0x06, 0x24, 0x50, + 0x34, 0x82, 0x0c, 0x3f, 0x47, 0x0e, 0xa0, 0x1d, 0xb1, 0x08, 0x21, 0xf6, 0xd0, 0xac, 0x56, 0x88, + 0x4b, 0xe4, 0x38, 0x45, 0xc7, 0x4a, 0xdb, 0x6e, 0xb5, 0x9d, 0xde, 0x09, 0x4f, 0xc1, 0x43, 0x70, + 0x40, 0xe2, 0x45, 0x38, 0xee, 0x91, 0x23, 0x9a, 0x79, 0x10, 0x90, 0xdd, 0xee, 0xce, 0x70, 0xd9, + 0x9d, 0x53, 0xec, 0x2f, 0xdf, 0x57, 0xf5, 0x55, 0x95, 0xed, 0x86, 0x8b, 0x5f, 0xd1, 0x32, 0xbe, + 0x61, 0x42, 0x5d, 0xfa, 0x95, 0xae, 0xf0, 0xb2, 0xdc, 0xe6, 0x97, 0x1e, 0x32, 0xe1, 0x67, 0x51, + 0x56, 0xda, 0x6a, 0xfa, 0x6e, 0xc7, 0x5d, 0xb4, 0xdc, 0x45, 0xb9, 0xcd, 0x17, 0x0d, 0xe9, 0xf1, + 0x59, 0xae, 0x73, 0xed, 0x99, 0x97, 0x6e, 0xd5, 0x88, 0x3e, 0xf8, 0x37, 0x81, 0xc1, 0x95, 0x23, + 0xd0, 0x77, 0x60, 0xec, 0x99, 0x4b, 0xb1, 0x4e, 0xe3, 0xf3, 0x68, 0x9e, 0x64, 0x23, 0xbf, 0xff, + 0x6e, 0x4d, 0xbf, 0x07, 0x68, 0xfe, 0x52, 0x4c, 0x62, 0x1a, 0x9d, 0x47, 0xf3, 0x93, 0xcf, 0xe7, + 0x8b, 0xd7, 0xa6, 0x5b, 0xf8, 0xa0, 0xcf, 0x98, 0xc4, 0x27, 0x71, 0x1a, 0x65, 0x13, 0xde, 0x6e, + 0xe9, 0xd7, 0x30, 0x52, 0x68, 0x5f, 0xea, 0x6a, 0x9b, 0x26, 0x3e, 0xd2, 0x47, 0x6f, 0x88, 0xf4, + 0xac, 0x61, 0x67, 0xad, 0x8c, 0xfe, 0x00, 0x47, 0x61, 0xb9, 0xb4, 0xfb, 0x12, 0xd3, 0xbe, 0x0f, + 0x73, 0x71, 0xbf, 0x30, 0xcf, 0xf7, 0x25, 0x66, 0x53, 0x75, 0xd8, 0xd0, 0xcf, 0x20, 0xae, 0x65, + 0x3a, 0xf0, 0x41, 0xde, 0x7f, 0x43, 0x90, 0x17, 0x32, 0x8b, 0x6b, 0x49, 0x9f, 0xc2, 0x84, 0x6b, + 0x65, 0x50, 0x99, 0x9d, 0x49, 0x87, 0xf7, 0xeb, 0x47, 0xcb, 0xcf, 0x0e, 0x52, 0xfa, 0x1e, 0x4c, + 0x85, 0x59, 0xe2, 0xb5, 0xc5, 0x4a, 0xb1, 0x22, 0x1d, 0x9d, 0x47, 0xf3, 0x71, 0x06, 0xc2, 0x7c, + 0x13, 0x10, 0x57, 0x2a, 0xe7, 0xf6, 0x7a, 0x99, 0x33, 0x8b, 0x2f, 0xd9, 0x3e, 0x1d, 0xdf, 0xab, + 0xd4, 0xab, 0xab, 0xe7, 0x3f, 0x7d, 0xdb, 0x28, 0xb2, 0xa9, 0xd3, 0x87, 0x0d, 0xa5, 0xd0, 0xf7, + 0x23, 0x9c, 0x9c, 0x47, 0xf3, 0x49, 0xe6, 0xd7, 0x17, 0x5f, 0xc2, 0x71, 0x86, 0x1c, 0x45, 0x8d, + 0x3f, 0x5a, 0x66, 0x77, 0x86, 0x4e, 0x61, 0xc4, 0x2b, 0x64, 0x16, 0xd7, 0xa4, 0xe7, 0x36, 0x66, + 0xc7, 0x39, 0x1a, 0x43, 0x22, 0x0a, 0x30, 0xfc, 0x85, 0x89, 0x02, 0xd7, 0x24, 0x7e, 0xdc, 0xff, + 0xe3, 0xf7, 0x59, 0x74, 0xf1, 0x67, 0x02, 0x93, 0x6e, 0xd2, 0x74, 0x02, 0x03, 0x94, 0xa5, 0xdd, + 0x93, 0x1e, 0x3d, 0x85, 0x29, 0xda, 0xcd, 0x52, 0x32, 0xa1, 0x14, 0x5a, 0x12, 0x51, 0x02, 0x47, + 0xce, 0x6a, 0x87, 0xc4, 0x8e, 0xb2, 0xb2, 0xbc, 0x03, 0x12, 0xfa, 0x10, 0x4e, 0x4b, 0x5d, 0xec, + 0x73, 0xad, 0x3a, 0xb0, 0xef, 0x59, 0xe6, 0xc0, 0x1a, 0x50, 0x0a, 0x27, 0xb9, 0xc6, 0xaa, 0x10, + 0x4b, 0x8b, 0xc6, 0x3a, 0x6c, 0xe8, 0x30, 0xb9, 0x93, 0x2b, 0x76, 0xc0, 0x46, 0xad, 0xb0, 0x05, + 0xa0, 0x73, 0xd0, 0x22, 0xd3, 0xd6, 0x41, 0x0b, 0x1c, 0x39, 0x07, 0x06, 0x4b, 0x5d, 0x88, 0x03, + 0xeb, 0xd8, 0x81, 0x21, 0x61, 0xa1, 0x39, 0x2b, 0x1c, 0x78, 0xd2, 0x4a, 0x2b, 0xcc, 0x1d, 0x91, + 0x9c, 0xba, 0xe8, 0x4c, 0xea, 0x7d, 0xa7, 0x23, 0xf4, 0x0c, 0x88, 0x2e, 0xad, 0x90, 0xc2, 0xc8, + 0xce, 0xfe, 0x83, 0xff, 0xa1, 0x21, 0x17, 0xa1, 0x4e, 0xbd, 0x62, 0x06, 0x3b, 0xde, 0xc3, 0x0e, + 0x69, 0x39, 0x67, 0xae, 0x48, 0xa3, 0x0b, 0xa6, 0x0e, 0x3d, 0x7c, 0x8b, 0x3e, 0x80, 0xe3, 0x80, + 0xad, 0xb1, 0x76, 0xd0, 0x23, 0x5f, 0x43, 0x03, 0x75, 0x76, 0xdf, 0x0e, 0xd3, 0x52, 0x30, 0x0a, + 0xb7, 0x80, 0x8e, 0x20, 0x41, 0xbb, 0x21, 0x3d, 0x3a, 0x86, 0xbe, 0xeb, 0x0a, 0x89, 0x1c, 0xb4, + 0xb2, 0x9c, 0xc4, 0x6e, 0xe6, 0x61, 0x0e, 0x24, 0xf1, 0xa8, 0xe1, 0xa4, 0x4f, 0x8f, 0x60, 0xdc, + 0x1a, 0x27, 0x03, 0x27, 0x73, 0xf6, 0xc8, 0xd0, 0x1d, 0x8a, 0x26, 0x1f, 0x19, 0x39, 0xb2, 0xd5, + 0x8a, 0x8c, 0x43, 0xbe, 0xa7, 0x30, 0xbd, 0x73, 0xeb, 0x5c, 0xdc, 0xd6, 0xb9, 0x3f, 0x58, 0x6d, + 0xab, 0x22, 0x9f, 0xb1, 0x12, 0x75, 0x73, 0x2e, 0x00, 0x86, 0xa1, 0x98, 0x24, 0xc4, 0xf9, 0x14, + 0xe2, 0x17, 0xd2, 0x9d, 0x2e, 0xa5, 0x97, 0xb5, 0x24, 0x3d, 0xef, 0xbe, 0x96, 0x8d, 0x67, 0x53, + 0x4b, 0x12, 0xfb, 0xcc, 0xb5, 0xec, 0x14, 0xd7, 0x30, 0xe9, 0x2e, 0x9c, 0x73, 0x8e, 0x76, 0x83, + 0x15, 0xee, 0x9c, 0xf6, 0x04, 0xc0, 0xa2, 0x5a, 0x63, 0x25, 0x85, 0x0a, 0xb9, 0x57, 0xc2, 0x72, + 0x2d, 0x14, 0x89, 0x9b, 0x22, 0x97, 0xc6, 0x32, 0xbe, 0x25, 0x89, 0x9b, 0x55, 0x68, 0x65, 0x77, + 0x65, 0x49, 0x9f, 0x3e, 0x02, 0xca, 0x99, 0x6d, 0x1e, 0xc4, 0x03, 0x3e, 0x08, 0x99, 0x3f, 0x86, + 0xe9, 0x9d, 0xeb, 0xd7, 0xb4, 0xd7, 0x7b, 0x3e, 0x86, 0x89, 0x5e, 0x19, 0xac, 0x6a, 0xac, 0x0c, + 0x89, 0x1a, 0xf6, 0x93, 0xaf, 0xfe, 0xba, 0x99, 0x45, 0xaf, 0x6e, 0x66, 0xd1, 0x3f, 0x37, 0xb3, + 0xe8, 0xb7, 0xdb, 0x59, 0xef, 0xd5, 0xed, 0xac, 0xf7, 0xf7, 0xed, 0xac, 0xf7, 0xf3, 0x87, 0xb9, + 0xb0, 0x9b, 0xdd, 0x6a, 0xc1, 0xb5, 0xf4, 0x4f, 0xff, 0x27, 0xcd, 0x57, 0x40, 0xe9, 0xf5, 0xdd, + 0x2f, 0xc0, 0x6a, 0xe8, 0x9f, 0xf1, 0x2f, 0xfe, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xb9, 0xaa, 0xd0, + 0x56, 0x29, 0x06, 0x00, 0x00, } func (m *Chain) Marshal() (dAtA []byte, err error) { diff --git a/pkg/chains/chains_test.go b/pkg/chains/chains_test.go index d6e1456434..aca3c6ff6d 100644 --- a/pkg/chains/chains_test.go +++ b/pkg/chains/chains_test.go @@ -11,7 +11,10 @@ import ( func TestChain_Name(t *testing.T) { t.Run("new Name field is compatible with ChainName enum", func(t *testing.T) { for _, chain := range chains.DefaultChainsList() { - require.EqualValues(t, chain.Name, chain.ChainName.String()) + chainName := chain.ChainName.String() + if chainName != "empty" { + require.EqualValues(t, chain.Name, chainName) + } } }) } @@ -34,6 +37,7 @@ func TestChainListByNetworkType(t *testing.T) { chains.OptimismMainnet, chains.BaseMainnet, chains.SolanaMainnet, + chains.TONMainnet, }, }, { @@ -50,6 +54,7 @@ func TestChainListByNetworkType(t *testing.T) { chains.OptimismSepolia, chains.BaseSepolia, chains.SolanaDevnet, + chains.TONTestnet, }, }, { @@ -60,6 +65,7 @@ func TestChainListByNetworkType(t *testing.T) { chains.BitcoinRegtest, chains.GoerliLocalnet, chains.SolanaLocalnet, + chains.TONLocalnet, }, }, } @@ -156,6 +162,9 @@ func TestDefaultChainList(t *testing.T) { chains.SolanaMainnet, chains.SolanaDevnet, chains.SolanaLocalnet, + chains.TONMainnet, + chains.TONTestnet, + chains.TONLocalnet, }, chains.DefaultChainsList()) } @@ -188,6 +197,9 @@ func TestChainListByGateway(t *testing.T) { chains.SolanaMainnet, chains.SolanaDevnet, chains.SolanaLocalnet, + chains.TONMainnet, + chains.TONTestnet, + chains.TONLocalnet, }, }, { @@ -230,6 +242,9 @@ func TestExternalChainList(t *testing.T) { chains.SolanaMainnet, chains.SolanaDevnet, chains.SolanaLocalnet, + chains.TONMainnet, + chains.TONTestnet, + chains.TONLocalnet, }, chains.ExternalChainList([]chains.Chain{})) } diff --git a/proto/zetachain/zetacore/pkg/chains/chains.proto b/proto/zetachain/zetacore/pkg/chains/chains.proto index 63b146cf42..dca1db9fa8 100644 --- a/proto/zetachain/zetacore/pkg/chains/chains.proto +++ b/proto/zetachain/zetacore/pkg/chains/chains.proto @@ -63,6 +63,7 @@ enum Network { optimism = 5; base = 6; solana = 7; + ton = 8; } // NetworkType represents the network type of the chain @@ -82,6 +83,7 @@ enum Vm { no_vm = 0; evm = 1; svm = 2; + tvm = 3; } // Consensus represents the consensus algorithm used by the chain @@ -94,6 +96,7 @@ enum Consensus { bitcoin = 2; op_stack = 3; solana_consensus = 4; + catchain_consensus = 5; // ton } // CCTXGateway describes for the chain the gateway used to handle CCTX outbounds diff --git a/typescript/zetachain/zetacore/pkg/chains/chains_pb.d.ts b/typescript/zetachain/zetacore/pkg/chains/chains_pb.d.ts index af161efd7b..9e68854962 100644 --- a/typescript/zetachain/zetacore/pkg/chains/chains_pb.d.ts +++ b/typescript/zetachain/zetacore/pkg/chains/chains_pb.d.ts @@ -197,6 +197,11 @@ export declare enum Network { * @generated from enum value: solana = 7; */ solana = 7, + + /** + * @generated from enum value: ton = 8; + */ + ton = 8, } /** @@ -248,6 +253,11 @@ export declare enum Vm { * @generated from enum value: svm = 2; */ svm = 2, + + /** + * @generated from enum value: tvm = 3; + */ + tvm = 3, } /** @@ -282,6 +292,13 @@ export declare enum Consensus { * @generated from enum value: solana_consensus = 4; */ solana_consensus = 4, + + /** + * ton + * + * @generated from enum value: catchain_consensus = 5; + */ + catchain_consensus = 5, } /** From 28b12985d4e58ba0f515e0366ca6a47c0aec5ad4 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:05:51 +0300 Subject: [PATCH 05/36] Add liteapi wrapper client; locate first tx --- zetaclient/chains/ton/liteapi/client.go | 90 +++++++++++++++++++ .../chains/ton/liteapi/client_live_test.go | 82 +++++++++++++++++ zetaclient/chains/ton/liteapi/client_test.go | 18 ++++ 3 files changed, 190 insertions(+) create mode 100644 zetaclient/chains/ton/liteapi/client.go create mode 100644 zetaclient/chains/ton/liteapi/client_live_test.go create mode 100644 zetaclient/chains/ton/liteapi/client_test.go diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go new file mode 100644 index 0000000000..0af19c5d72 --- /dev/null +++ b/zetaclient/chains/ton/liteapi/client.go @@ -0,0 +1,90 @@ +package liteapi + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +// Client extends tongo's liteapi.Client with some high-level tools +type Client struct { + *liteapi.Client +} + +// GetFirstTransaction scrolls through the transactions of the given account to find the first one. +// Note that it will fail in case of old transactions. Ideally, use archival node. +// Also returns the number of scrolled transactions for this account i.e. total transactions +func (c *Client) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*ton.Transaction, int, error) { + const pageSize = 100 + + state, err := c.GetAccountState(ctx, acc) + if err != nil { + return nil, 0, errors.Wrap(err, "unable to get account state") + } + + if state.Account.Status() != tlb.AccountActive { + return nil, 0, errors.New("account is not active") + } + + var tx *ton.Transaction + + // logical time and hash of the last transaction + lt, hash, scrolled := state.LastTransLt, state.LastTransHash, 0 + + for { + hashBits := ton.Bits256(hash) + + txs, err := c.GetTransactions(ctx, pageSize, acc, lt, hashBits) + if err != nil { + return nil, scrolled, errors.Wrapf(err, "unable to get transactions [lt %d, hash %s]", lt, hashBits.Hex()) + } + + if len(txs) == 0 { + break + } + + scrolled += len(txs) + + tx = &txs[len(txs)-1] + + // Not we take the latest item in the list (oldest tx in the page) + // and set it as the new last tx + lt, hash = tx.PrevTransLt, tx.PrevTransHash + } + + if tx == nil { + return nil, scrolled, fmt.Errorf("no transactions found [lt %d, hash %s]", lt, ton.Bits256(hash).Hex()) + } + + return tx, scrolled, nil +} + +func TransactionHashToString(lt uint64, hash ton.Bits256) string { + return fmt.Sprintf("%d:%s", lt, hash.Hex()) +} + +func TransactionHashFromString(hash string) (uint64, ton.Bits256, error) { + parts := strings.Split(hash, ":") + if len(parts) != 2 { + return 0, ton.Bits256{}, fmt.Errorf("invalid hash string format") + } + + lt, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return 0, ton.Bits256{}, fmt.Errorf("invalid logical time: %w", err) + } + + var hashBits ton.Bits256 + + if err = hashBits.FromHex(parts[1]); err != nil { + return 0, ton.Bits256{}, fmt.Errorf("invalid hash: %w", err) + } + + return lt, hashBits, nil +} diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go new file mode 100644 index 0000000000..2fb2e00385 --- /dev/null +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -0,0 +1,82 @@ +package liteapi + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/config" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/zetaclient/common" +) + +func TestClient(t *testing.T) { + if !common.LiveTestEnabled() { + t.Skip("Live tests are disabled") + } + + var ( + ctx = context.Background() + client = &Client{Client: mustCreateClient(t)} + ) + + t.Run("GetFirstTransaction", func(t *testing.T) { + // ARRANGE + // Given sample account id (a dev wallet) + // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions + accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") + require.NoError(t, err) + + // Given expected hash for the first tx + const expect = "b73df4853ca02a040df46f56635d6b8f49b554d5f556881ab389111bbfce4498" + + // as of 2024-09-18 + const expectedTransactions = 23 + + start := time.Now() + + // ACT + tx, scrolled, err := client.GetFirstTransaction(ctx, accountID) + + finish := time.Since(start) + + // ASSERT + require.NoError(t, err) + + assert.GreaterOrEqual(t, scrolled, expectedTransactions) + assert.Equal(t, expect, tx.Hash().Hex()) + + t.Logf("Time taken %s; transactions scanned: %d", finish.String(), scrolled) + }) +} + +func mustCreateClient(t *testing.T) *liteapi.Client { + client, err := liteapi.NewClient( + liteapi.WithConfigurationFile(mustFetchConfig(t)), + liteapi.WithDetectArchiveNodes(), + ) + + require.NoError(t, err) + + return client +} + +func mustFetchConfig(t *testing.T) config.GlobalConfigurationFile { + // archival light client for mainnet + const url = "https://api.tontech.io/ton/archive-mainnet.autoconf.json" + + res, err := http.Get(url) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + + conf, err := config.ParseConfig(res.Body) + require.NoError(t, err) + + return *conf +} diff --git a/zetaclient/chains/ton/liteapi/client_test.go b/zetaclient/chains/ton/liteapi/client_test.go new file mode 100644 index 0000000000..3ab56422a5 --- /dev/null +++ b/zetaclient/chains/ton/liteapi/client_test.go @@ -0,0 +1,18 @@ +package liteapi + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHashes(t *testing.T) { + const sample = `48644940000001:e02b8c7cec103e08175ade8106619a8908707623c31451df2a68497c7d23d15a` + + lt, hash, err := TransactionHashFromString(sample) + require.NoError(t, err) + + require.Equal(t, uint64(48644940000001), lt) + require.Equal(t, "e02b8c7cec103e08175ade8106619a8908707623c31451df2a68497c7d23d15a", hash.Hex()) + require.Equal(t, sample, TransactionHashToString(lt, hash)) +} From 7cd1c0671a31377ae9e268fb7b3d5ef6fde7ceaf Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:37:11 +0300 Subject: [PATCH 06/36] Add TON tx scraper --- pkg/contracts/ton/gateway_test.go | 54 +++++++++++ pkg/contracts/ton/testdata/01.json | 8 ++ pkg/contracts/ton/testdata/readme.md | 29 ++++++ pkg/contracts/ton/testdata/scraper.go | 131 ++++++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 pkg/contracts/ton/gateway_test.go create mode 100644 pkg/contracts/ton/testdata/01.json create mode 100644 pkg/contracts/ton/testdata/readme.md create mode 100644 pkg/contracts/ton/testdata/scraper.go diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go new file mode 100644 index 0000000000..5e05bd4ec7 --- /dev/null +++ b/pkg/contracts/ton/gateway_test.go @@ -0,0 +1,54 @@ +package ton + +import ( + "embed" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +func TestFixtures(t *testing.T) { + // ACT + tx := getFixtureTX(t, "01") + + // ASSERT + require.Equal(t, uint64(26023788000003), tx.Lt) + require.Equal(t, "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", tx.Hash().Hex()) +} + +//go:embed testdata +var fixtures embed.FS + +// testdata/$name.json tx +func getFixtureTX(t *testing.T, name string) ton.Transaction { + t.Helper() + + filename := fmt.Sprintf("testdata/%s.json", name) + + b, err := fixtures.ReadFile(filename) + require.NoError(t, err, filename) + + // bag of cells + var raw struct { + BOC string `json:"boc"` + } + + require.NoError(t, json.Unmarshal(b, &raw)) + + cells, err := boc.DeserializeBocHex(raw.BOC) + require.NoError(t, err) + require.Len(t, cells, 1) + + cell := cells[0] + + var tx ton.Transaction + + require.NoError(t, tx.UnmarshalTLB(cell, &tlb.Decoder{})) + + return tx +} diff --git a/pkg/contracts/ton/testdata/01.json b/pkg/contracts/ton/testdata/01.json new file mode 100644 index 0000000000..5c05bcc425 --- /dev/null +++ b/pkg/contracts/ton/testdata/01.json @@ -0,0 +1,8 @@ +{ + "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04", + "description": "A deposit to gw contract on testnet", + "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", + "logicalTime": 26023788000003, + "test": false +} diff --git a/pkg/contracts/ton/testdata/readme.md b/pkg/contracts/ton/testdata/readme.md new file mode 100644 index 0000000000..30a59a1639 --- /dev/null +++ b/pkg/contracts/ton/testdata/readme.md @@ -0,0 +1,29 @@ +# TON transaction scraper + +`scraper.go` represents a handy tool that allows to fetch transactions from TON blockchain +for further usage in test cases. + +`go run pkg/contracts/ton/testdata/scraper.go
[--testnet]` + +## Example usage + +```sh +go run pkg/contracts/ton/testdata/scraper.go \ + kQCZfYicgVrqwhxH-Grg44OD78PDRjBnWC9iY61IxaFIW77M \ + 26023788000003 \ + cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf | jq +``` + +Returns + +```json +{ + "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04", + "description": "todo", + "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", + "logicalTime": 26023788000003, + "test": false +} +``` + diff --git a/pkg/contracts/ton/testdata/scraper.go b/pkg/contracts/ton/testdata/scraper.go new file mode 100644 index 0000000000..c9c2705375 --- /dev/null +++ b/pkg/contracts/ton/testdata/scraper.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "strconv" + + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +func main() { + var testnet bool + + flag.BoolVar(&testnet, "testnet", false, "Use testnet network") + flag.Parse() + + if len(flag.Args()) != 3 { + log.Fatalf("Usage: go run scrape.go [--testnet]") + } + + // Parse account + acc, err := ton.ParseAccountID(flag.Arg(0)) + must(err, "Unable to parse account") + + // Parse LT + lt, err := strconv.ParseUint(flag.Arg(1), 10, 64) + must(err, "Unable to parse logical time") + + // Parse hash + var hash ton.Bits256 + + must(hash.FromHex(flag.Arg(2)), "Unable to parse hash") + + ctx, client := context.Background(), getClient(testnet) + + state, err := client.GetAccountState(ctx, acc) + must(err, "Unable to get account state") + + if state.Account.Status() != tlb.AccountActive { + fail("account %s is not active", acc.ToRaw()) + } + + txs, err := client.GetTransactions(ctx, 1, acc, lt, hash) + must(err, "Unable to get transactions") + + switch { + case len(txs) == 0: + fail("Not found") + case len(txs) > 1: + fail("invalid tx list length (got %d, want 1); lt %d, hash %s", len(txs), hash.Hex()) + } + + // Print the transaction + tx := txs[0] + + cell, err := transactionToCell(tx) + must(err, "unable to convert tx to cell") + + bocRaw, err := cell.MarshalJSON() + must(err, "unable to marshal cell to JSON") + + printAny(map[string]any{ + "test": testnet, + "account": acc.ToRaw(), + "description": "todo", + "logicalTime": lt, + "hash": hash.Hex(), + "boc": json.RawMessage(bocRaw), + }) +} + +func getClient(testnet bool) *liteapi.Client { + if testnet { + c, err := liteapi.NewClientWithDefaultTestnet() + must(err, "unable to create testnet lite client") + + return c + } + + c, err := liteapi.NewClientWithDefaultTestnet() + must(err, "unable to create mainnet lite client") + + return c +} + +func printAny(v any) { + b, err := json.MarshalIndent(v, "", " ") + must(err, "unable marshal data") + + fmt.Println(string(b)) +} + +func transactionToCell(tx ton.Transaction) (*boc.Cell, error) { + b, err := tx.SourceBoc() + if err != nil { + return nil, err + } + + cells, err := boc.DeserializeBoc(b) + if err != nil { + return nil, err + } + + if len(cells) != 1 { + return nil, fmt.Errorf("invalid cell count: %d", len(cells)) + } + + return cells[0], nil +} + +func must(err error, msg string) { + if err == nil { + return + } + + if msg == "" { + log.Fatalf("Error: %s", err.Error()) + } + + log.Fatalf("%s; error: %s", msg, err.Error()) +} + +func fail(msg string, args ...any) { + must(fmt.Errorf(msg, args...), "FAIL") +} From e0d806fa0216e6651f37a3cac766c15279175fdd Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:33:37 +0300 Subject: [PATCH 07/36] Add client.GetTransactionsUntil --- zetaclient/chains/ton/liteapi/client.go | 90 ++++++++-- .../chains/ton/liteapi/client_live_test.go | 71 ++++++++ zetaclient/chains/ton/observer/inbound.go | 162 ++++++++++++++++++ zetaclient/chains/ton/observer/observer.go | 23 ++- 4 files changed, 328 insertions(+), 18 deletions(-) create mode 100644 zetaclient/chains/ton/observer/inbound.go diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index 0af19c5d72..e895ff4298 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -17,25 +17,21 @@ type Client struct { *liteapi.Client } +const pageSize = 200 + // GetFirstTransaction scrolls through the transactions of the given account to find the first one. -// Note that it will fail in case of old transactions. Ideally, use archival node. -// Also returns the number of scrolled transactions for this account i.e. total transactions +// Note that it might fail w/o using an archival node. Also returns the number of +// scrolled transactions for this account i.e. total transactions func (c *Client) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*ton.Transaction, int, error) { - const pageSize = 100 - - state, err := c.GetAccountState(ctx, acc) + lt, hash, err := c.getLastTranHash(ctx, acc) if err != nil { - return nil, 0, errors.Wrap(err, "unable to get account state") - } - - if state.Account.Status() != tlb.AccountActive { - return nil, 0, errors.New("account is not active") + return nil, 0, err } - var tx *ton.Transaction - - // logical time and hash of the last transaction - lt, hash, scrolled := state.LastTransLt, state.LastTransHash, 0 + var ( + tx *ton.Transaction + scrolled int + ) for { hashBits := ton.Bits256(hash) @@ -65,6 +61,72 @@ func (c *Client) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*t return tx, scrolled, nil } +// GetTransactionsUntil returns all transactions in a range of (from,to] where from is "lt && hash". +// - oldestLT && oldestHash tx is EXCLUDED from the result. +// - ordered by DESC +func (c *Client) GetTransactionsUntil( + ctx context.Context, + acc ton.AccountID, + oldestLT uint64, + oldestHash ton.Bits256, +) ([]ton.Transaction, error) { + lt, hash, err := c.getLastTranHash(ctx, acc) + if err != nil { + return nil, err + } + + var result []ton.Transaction + + for { + hashBits := ton.Bits256(hash) + + txs, err := c.GetTransactions(ctx, pageSize, acc, lt, hashBits) + if err != nil { + return nil, errors.Wrapf(err, "unable to get transactions [lt %d, hash %s]", lt, hashBits.Hex()) + } + + if len(txs) == 0 { + break + } + + for i := range txs { + found := txs[i].Lt == oldestLT && txs[i].Hash() == tlb.Bits256(oldestHash) + if !found { + continue + } + + // early exit + result = append(result, txs[:i]...) + + return result, nil + } + + // otherwise, append all page results + result = append(result, txs...) + + // prepare pagination params for the next page + oldestIndex := len(txs) - 1 + + lt, hash = txs[oldestIndex].PrevTransLt, txs[oldestIndex].PrevTransHash + } + + return result, nil +} + +// getLastTranHash returns logical time and hash of the last transaction +func (c *Client) getLastTranHash(ctx context.Context, acc ton.AccountID) (uint64, tlb.Bits256, error) { + state, err := c.GetAccountState(ctx, acc) + if err != nil { + return 0, tlb.Bits256{}, errors.Wrap(err, "unable to get account state") + } + + if state.Account.Status() != tlb.AccountActive { + return 0, tlb.Bits256{}, errors.New("account is not active") + } + + return state.LastTransLt, state.LastTransHash, nil +} + func TransactionHashToString(lt uint64, hash ton.Bits256) string { return fmt.Sprintf("%d:%s", lt, hash.Hex()) } diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index 2fb2e00385..435da2787a 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -2,6 +2,8 @@ package liteapi import ( "context" + "encoding/json" + "fmt" "net/http" "testing" "time" @@ -10,6 +12,7 @@ import ( "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" "github.com/zeta-chain/node/zetaclient/common" ) @@ -52,6 +55,42 @@ func TestClient(t *testing.T) { t.Logf("Time taken %s; transactions scanned: %d", finish.String(), scrolled) }) + + t.Run("GetTransactionsUntil", func(t *testing.T) { + // ARRANGE + // Given sample account id (a dev wallet) + // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions + accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") + require.NoError(t, err) + + const getUntilLT = uint64(48645164000001) + const getUntilHash = `2e107215e634bbc3492bdf4b1466d59432623295072f59ab526d15737caa9531` + + // as of 2024-09-20 + const expectedTX = 3 + + var hash ton.Bits256 + require.NoError(t, hash.FromHex(getUntilHash)) + + start := time.Now() + + // ACT + // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions + txs, err := client.GetTransactionsUntil(ctx, accountID, getUntilLT, hash) + + finish := time.Since(start) + + // ASSERT + require.NoError(t, err) + + t.Logf("Time taken %s; transactions fetched: %d", finish.String(), len(txs)) + for _, tx := range txs { + printTx(t, tx) + } + + mustContainTX(t, txs, "a6672a0e80193c1f705ef1cf45a5883441b8252523b1d08f7656c80e400c74a8") + assert.GreaterOrEqual(t, len(txs), expectedTX) + }) } func mustCreateClient(t *testing.T) *liteapi.Client { @@ -80,3 +119,35 @@ func mustFetchConfig(t *testing.T) config.GlobalConfigurationFile { return *conf } + +func mustContainTX(t *testing.T, txs []ton.Transaction, hash string) { + var h ton.Bits256 + require.NoError(t, h.FromHex(hash)) + + for _, tx := range txs { + if tx.Hash() == tlb.Bits256(h) { + return + } + } + + t.Fatalf("transaction %q not found", hash) +} + +func printTx(t *testing.T, tx ton.Transaction) { + b, err := json.MarshalIndent(simplifyTx(tx), "", " ") + require.NoError(t, err) + + t.Logf("TX %s", string(b)) +} + +func simplifyTx(tx ton.Transaction) map[string]any { + return map[string]any{ + "block": fmt.Sprintf("shard: %d, seqno: %d", tx.BlockID.Shard, tx.BlockID.Seqno), + "hash": tx.Hash().Hex(), + "logicalTime": tx.Lt, + "unixTime": time.Unix(int64(tx.Transaction.Now), 0).UTC().String(), + "outMessagesCount": tx.OutMsgCnt, + // "inMessageInfo": tx.Msgs.InMsg.Value.Value.Info.IntMsgInfo, + // "outMessages": tx.Msgs.OutMsgs, + } +} diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go new file mode 100644 index 0000000000..e8d403d5e8 --- /dev/null +++ b/zetaclient/chains/ton/observer/inbound.go @@ -0,0 +1,162 @@ +package observer + +import ( + "context" + "fmt" + "slices" + + "cosmossdk.io/errors" + "github.com/rs/zerolog" + "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" + zctx "github.com/zeta-chain/node/zetaclient/context" +) + +const ( + // MaxTransactionsPerTick is the maximum number of transactions to process on a ticker + MaxTransactionsPerTick = 100 +) + +func (ob *Observer) watchInbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + var ( + chainID = ob.Chain().ChainId + initialInterval = ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) + sampledLogger = ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ) + + ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", chainID) + + task := func(ctx context.Context, t *ticker.Ticker) error { + if !app.IsInboundObservationEnabled() { + sampledLogger.Info().Msgf("WatchInbound: inbound observation is disabled for chain %d", chainID) + return nil + } + + if err := ob.observeInbound(ctx, app); err != nil { + ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") + } + + newInterval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) + t.SetInterval(newInterval) + + return nil + } + + return ticker.Run( + ctx, + initialInterval, + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().Inbound, "WatchInbound"), + ) +} + +// Flow: +// - [x] Ensure last scanned transaction is set +// - [x] Get all transaction between [lastScannedTx; now] +// - [ ] Filter only valid and inbound transactions +// - [ ] For each transaction (ordered by *ASC*) +// - [ ] Construct crosschain cosmos message +// - [ ] Vote +// - [ ] Save last scanned tx +func (ob *Observer) observeInbound(ctx context.Context, _ *zctx.AppContext) error { + if err := ob.ensureLastScannedTX(ctx); err != nil { + return errors.Wrap(err, "unable to ensure last scanned tx") + } + + lt, hashBits, err := liteapi.TransactionHashFromString(ob.LastTxScanned()) + if err != nil { + return errors.Wrapf(err, "unable to parse last scanned tx %q", ob.LastTxScanned()) + } + + txs, err := ob.client.GetTransactionsUntil(ctx, ob.gatewayID, lt, hashBits) + if err != nil { + return errors.Wrap(err, "unable to get transactions") + } + + // Process from oldest to latest (ASC) + slices.Reverse(txs) + + switch { + case len(txs) == 0: + // noop + return nil + case len(txs) > MaxTransactionsPerTick: + ob.Logger().Inbound.Info(). + Msgf("ObserveInbound: got %d transactions. Taking first %d", len(txs), MaxTransactionsPerTick) + + txs = txs[:MaxTransactionsPerTick] + default: + ob.Logger().Inbound.Info().Msgf("ObserveInbound: got %d transactions", len(txs)) + } + + // todo deploy sample GW to testnet + // todo send some TON and test + + // todo FilterInboundEvent + + for _, tx := range txs { + fmt.Println("TON TX", tx) + } + + /* + // loop signature from oldest to latest to filter inbound events + for i := len(signatures) - 1; i >= 0; i-- { + sig := signatures[i] + sigString := sig.Signature.String() + + // process successfully signature only + if sig.Err == nil { + txResult, err := ob.solClient.GetTransaction(ctx, sig.Signature, &rpc.GetTransactionOpts{}) + if err != nil { + // we have to re-scan this signature on next ticker + return errors.Wrapf(err, "error GetTransaction for chain %d sig %s", chainID, sigString) + } + + // filter inbound events and vote + err = ob.FilterInboundEventsAndVote(ctx, txResult) + if err != nil { + // we have to re-scan this signature on next ticker + return errors.Wrapf(err, "error FilterInboundEventAndVote for chain %d sig %s", chainID, sigString) + } + } + + // signature scanned; save last scanned signature to both memory and db, ignore db error + if err := ob.SaveLastTxScanned(sigString, sig.Slot); err != nil { + ob.Logger(). + Inbound.Error(). + Err(err). + Msgf("ObserveInbound: error saving last sig %s for chain %d", sigString, chainID) + } + ob.Logger(). + Inbound.Info(). + Msgf("ObserveInbound: last scanned sig is %s for chain %d in slot %d", sigString, chainID, sig.Slot) + */ + + return nil +} + +func (ob *Observer) ensureLastScannedTX(ctx context.Context) error { + // noop + if ob.LastTxScanned() != "" { + return nil + } + + tx, _, err := ob.client.GetFirstTransaction(ctx, ob.gatewayID) + if err != nil { + return err + } + + txHash := liteapi.TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) + + ob.WithLastTxScanned(txHash) + + return ob.WriteLastTxScannedToDB(txHash) +} diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index d21312c31c..03f019d187 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -3,12 +3,13 @@ package observer import ( "context" - "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" ) type Observer struct { @@ -31,11 +32,25 @@ func New(bo *base.Observer, client *liteapi.Client, gatewayID ton.AccountID) (*O } func (ob *Observer) Start(ctx context.Context) { - // todo -} + if ok := ob.Observer.Start(); !ok { + ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) + return + } + + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + + // Note that each `watch*` method has a ticker that will stop as soon as + // baseObserver.Stop() was called (ticker.WithStopChan) + + // watch for incoming txs and post votes to zetacore + bg.Work(ctx, ob.watchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) -func (ob *Observer) Stop() { // todo + // watchOutbound + // watchGasPrice + // watchInboundTracker + // watchOutbound + // watchRPCStatus } func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *types.CrossChainTx) (bool, error) { From 98a88133a6f73fcb5dfe795e6b7a05858b5e75ed Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:18:26 +0300 Subject: [PATCH 08/36] Gateway WIP --- pkg/contracts/ton/gateway.go | 298 ++++++++++++++++++ pkg/contracts/ton/gateway_test.go | 102 +++++- pkg/contracts/ton/gateway_tx.go | 51 +++ pkg/contracts/ton/testdata/00-donation.json | 8 + .../ton/testdata/{01.json => 01-deposit.json} | 4 +- pkg/contracts/ton/testdata/readme.md | 10 +- pkg/contracts/ton/testdata/scraper.go | 4 +- .../chains/ton/observer/observer_test.go | 7 - 8 files changed, 460 insertions(+), 24 deletions(-) create mode 100644 pkg/contracts/ton/gateway.go create mode 100644 pkg/contracts/ton/gateway_tx.go create mode 100644 pkg/contracts/ton/testdata/00-donation.json rename pkg/contracts/ton/testdata/{01.json => 01-deposit.json} (88%) delete mode 100644 zetaclient/chains/ton/observer/observer_test.go diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go new file mode 100644 index 0000000000..d41a37168f --- /dev/null +++ b/pkg/contracts/ton/gateway.go @@ -0,0 +1,298 @@ +package ton + +import ( + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +type Op uint32 + +// github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc +// Inbound operations +const ( + OpDonate Op = 100 + iota + OpDeposit + OpDepositAndCall +) + +// Outbound operations +const ( + OpWithdraw Op = 200 + iota + SetDepositsEnabled + UpdateTSS + UpdateCode +) + +type Gateway struct { + accountID ton.AccountID +} + +type Donation struct { + Sender ton.AccountID + Amount math.Uint +} + +type Deposit struct { + Sender ton.AccountID + Amount math.Uint + Recipient eth.Address +} + +type DepositAndCall struct { + Deposit + CallData []byte +} + +const ( + sizeOpCode = 32 + sizeQueryID = 64 +) + +var ( + ErrParse = errors.New("unable to parse tx") + ErrUnknownOp = errors.New("unknown op") + ErrCast = errors.New("unable to cast tx content") +) + +func NewGateway(accountID ton.AccountID) *Gateway { + return &Gateway{accountID} +} + +func (gw *Gateway) ParseTransaction(tx ton.Transaction) (*Transaction, error) { + if !tx.IsSuccess() { + return nil, errors.Wrapf(ErrParse, "tx %s is not successful", tx.Hash().Hex()) + } + + if tx.Msgs.InMsg.Exists { + inbound, err := gw.parseInbound(tx) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse inbound tx %s", tx.Hash().Hex()) + } + + return inbound, nil + } + + outbound, err := gw.parseOutbound(tx) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse outbound tx %s", tx.Hash().Hex()) + } + + return outbound, nil +} + +func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { + body, err := parseInternalMessageBody(tx) + if err != nil { + return nil, errors.Wrap(err, "unable to parse body") + } + + intMsgInfo := tx.Msgs.InMsg.Value.Value.Info.IntMsgInfo + if intMsgInfo == nil { + return nil, errors.Wrap(ErrParse, "no internal message info") + } + + sourceID, err := ton.AccountIDFromTlb(intMsgInfo.Src) + if err != nil { + return nil, errors.Wrap(err, "unable to parse source account") + } + + destinationID, err := ton.AccountIDFromTlb(intMsgInfo.Dest) + if err != nil { + return nil, errors.Wrap(err, "unable to parse destination account") + } + + if gw.accountID != *destinationID { + return nil, errors.Wrap(ErrParse, "destination account is not gateway") + } + + op, err := body.ReadUint(sizeOpCode) + if err != nil { + return nil, errors.Wrap(err, "unable to read op code") + } + + var ( + sender = *sourceID + opCode = Op(op) + + content any + errContent error + ) + + switch opCode { + case OpDonate: + amount := intMsgInfo.Value.Grams - tx.TotalFees.Grams + content = Donation{Sender: sender, Amount: gramToUint(amount)} + case OpDeposit: + content, errContent = parseDeposit(tx, sender, body) + case OpDepositAndCall: + content, errContent = parseDepositAndCall(tx, sender, body) + default: + return nil, errors.Wrapf(ErrUnknownOp, "op code %d", int64(op)) + } + + if errContent != nil { + return nil, errors.Wrapf(ErrParse, "unable to parse content for op code %d: %s", int64(op), errContent.Error()) + } + + return &Transaction{ + Transaction: tx, + Operation: opCode, + + content: content, + inbound: true, + }, nil +} + +func parseDeposit(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (Deposit, error) { + // skip query id + if err := body.Skip(sizeQueryID); err != nil { + return Deposit{}, err + } + + recipient, err := readEVMAddress(body) + if err != nil { + return Deposit{}, errors.Wrap(err, "unable to read recipient") + } + + dl, err := parseDepositLog(tx) + if err != nil { + return Deposit{}, errors.Wrap(err, "unable to parse deposit log") + } + + return Deposit{ + Sender: sender, + Amount: dl.Amount, + Recipient: recipient, + }, nil +} + +type depositLog struct { + Amount math.Uint +} + +func parseDepositLog(tx ton.Transaction) (depositLog, error) { + messages := tx.Msgs.OutMsgs.Values() + if len(messages) == 0 { + return depositLog{}, errors.Wrap(ErrParse, "no out messages") + } + + // stored as ref + // cell log = begin_cell() + // .store_uint(op::internal::deposit, size::op_code_size) + // .store_uint(0, size::query_id_size) + // .store_slice(sender) + // .store_coins(deposit_amount) + // .store_uint(evm_recipient, size::evm_address) + // .end_cell(); + + body, err := marshalCellRef(messages[0].Value.Body) + if err != nil { + return depositLog{}, errors.Wrap(err, "unable to read body cell") + } + + if err := body.Skip(sizeOpCode + sizeQueryID); err != nil { + return depositLog{}, errors.Wrap(err, "unable to skip bits") + } + + // skip msg address (ton sender) + if err := unmarshalTLB(&tlb.MsgAddress{}, body); err != nil { + return depositLog{}, errors.Wrap(err, "unable to read sender address") + } + + var deposited tlb.Grams + if err := unmarshalTLB(&deposited, body); err != nil { + return depositLog{}, errors.Wrap(err, "unable to read deposited amount") + } + + return depositLog{Amount: gramToUint(deposited)}, nil +} + +func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (DepositAndCall, error) { + deposit, err := parseDeposit(tx, sender, body) + if err != nil { + return DepositAndCall{}, err + } + + callDataCell, err := body.NextRef() + if err != nil { + return DepositAndCall{}, errors.Wrap(err, "unable to read call data cell") + } + + var sd tlb.SnakeData + if err = sd.UnmarshalTLB(callDataCell, &tlb.Decoder{}); err != nil { + return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") + } + + cd := boc.BitString(sd) + + return DepositAndCall{Deposit: deposit, CallData: cd.Buffer()}, nil +} + +func (gw *Gateway) parseOutbound(_ ton.Transaction) (*Transaction, error) { + return nil, errors.New("not implemented") +} + +func parseInternalMessageBody(tx ton.Transaction) (*boc.Cell, error) { + if !tx.Msgs.InMsg.Exists { + return nil, errors.Wrap(ErrParse, "tx should have an internal message") + } + + either := tx.Msgs.InMsg.Value.Value.Body + if either.IsRight { + return nil, errors.Wrap(ErrParse, "tx body should not be a Ref") + } + + body, err := marshalCell(&either.Value) + if err != nil { + return nil, errors.Wrap(err, "unable to read body cell") + } + + return body, nil +} + +func marshalCell(v tlb.MarshalerTLB) (*boc.Cell, error) { + cell := boc.NewCell() + + if err := v.MarshalTLB(cell, &tlb.Encoder{}); err != nil { + return nil, err + } + + return cell, nil +} + +func marshalCellRef(v tlb.MarshalerTLB) (*boc.Cell, error) { + c, err := marshalCell(v) + if err != nil { + return nil, err + } + + c, err = c.NextRef() + if err != nil { + return nil, errors.Wrap(err, "unable to create ref cell") + } + + return c, nil +} + +func unmarshalTLB(t tlb.UnmarshalerTLB, cell *boc.Cell) error { + return t.UnmarshalTLB(cell, &tlb.Decoder{}) +} + +func gramToUint(g tlb.Grams) math.Uint { + return math.NewUint(uint64(g)) +} + +func readEVMAddress(cell *boc.Cell) (eth.Address, error) { + const evmAddrBits = 20 * 8 + + s, err := cell.ReadBits(evmAddrBits) + if err != nil { + return eth.Address{}, err + } + + return eth.BytesToAddress(s.Buffer()), nil +} diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index 5e05bd4ec7..6e9ee2b3d1 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -6,15 +6,92 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" ) +func TestParsing(t *testing.T) { + t.Run("Donate", func(t *testing.T) { + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "00-donation") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + assert.Equal(t, int(OpDonate), int(parsedTX.Operation)) + assert.Equal(t, true, parsedTX.IsInbound()) + + const ( + expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" + expectedDonation = 1_499_432_947 // 1.49... TON + ) + + donation, err := parsedTX.Donation() + assert.NoError(t, err) + assert.Equal(t, expectedSender, donation.Sender.ToRaw()) + assert.Equal(t, expectedDonation, int(donation.Amount.Uint64())) + }) + + t.Run("Deposit", func(t *testing.T) { + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "01-deposit") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + // Check tx props + assert.Equal(t, int(OpDeposit), int(parsedTX.Operation)) + + // Check + deposit, err := parsedTX.Deposit() + assert.NoError(t, err) + + const ( + expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" + vitalikDotETH = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + expectedDeposit = 990_000_000 // 0.99 TON + ) + + assert.Equal(t, expectedSender, deposit.Sender.ToRaw()) + assert.Equal(t, expectedDeposit, int(deposit.Amount.Uint64())) + assert.Equal(t, vitalikDotETH, deposit.Recipient.Hex()) + + // Check that other casting fails + _, err = parsedTX.Donation() + assert.ErrorIs(t, err, ErrCast) + }) + + t.Run("Deposit and call", func(t *testing.T) { + // todo + }) + + t.Run("Irrelevant tx", func(t *testing.T) { + // todo + }) + + // todo +} + func TestFixtures(t *testing.T) { // ACT - tx := getFixtureTX(t, "01") + tx, _ := getFixtureTX(t, "01-deposit") // ASSERT require.Equal(t, uint64(26023788000003), tx.Lt) @@ -24,8 +101,17 @@ func TestFixtures(t *testing.T) { //go:embed testdata var fixtures embed.FS +type fixture struct { + Account string `json:"account"` + BOC string `json:"boc"` + Description string `json:"description"` + Hash string `json:"hash"` + LogicalTime uint64 `json:"logicalTime"` + Test bool `json:"test"` +} + // testdata/$name.json tx -func getFixtureTX(t *testing.T, name string) ton.Transaction { +func getFixtureTX(t *testing.T, name string) (ton.Transaction, fixture) { t.Helper() filename := fmt.Sprintf("testdata/%s.json", name) @@ -34,13 +120,11 @@ func getFixtureTX(t *testing.T, name string) ton.Transaction { require.NoError(t, err, filename) // bag of cells - var raw struct { - BOC string `json:"boc"` - } + var fx fixture - require.NoError(t, json.Unmarshal(b, &raw)) + require.NoError(t, json.Unmarshal(b, &fx)) - cells, err := boc.DeserializeBocHex(raw.BOC) + cells, err := boc.DeserializeBocHex(fx.BOC) require.NoError(t, err) require.Len(t, cells, 1) @@ -50,5 +134,7 @@ func getFixtureTX(t *testing.T, name string) ton.Transaction { require.NoError(t, tx.UnmarshalTLB(cell, &tlb.Decoder{})) - return tx + t.Logf("Loaded fixture %s\n%s", filename, fx.Description) + + return tx, fx } diff --git a/pkg/contracts/ton/gateway_tx.go b/pkg/contracts/ton/gateway_tx.go new file mode 100644 index 0000000000..75c12c8eff --- /dev/null +++ b/pkg/contracts/ton/gateway_tx.go @@ -0,0 +1,51 @@ +package ton + +import ( + "cosmossdk.io/errors" + "cosmossdk.io/math" + "github.com/tonkeeper/tongo/ton" +) + +// Transaction represents a Gateway transaction. +type Transaction struct { + ton.Transaction + Operation Op + + content any + inbound bool +} + +// IsInbound returns true if the transaction is inbound. +func (tx *Transaction) IsInbound() bool { + return tx.inbound +} + +// GasUsed returns the amount of gas used by the transaction. +func (tx *Transaction) GasUsed() math.Uint { + return math.NewUint(uint64(tx.TotalFees.Grams)) +} + +// Donation casts the transaction content to a Donation. +func (tx *Transaction) Donation() (Donation, error) { + return retrieveContent[Donation](tx) +} + +// Deposit casts the transaction content to a Deposit. +func (tx *Transaction) Deposit() (Deposit, error) { + return retrieveContent[Deposit](tx) +} + +// DepositAndCall casts the transaction content to a DepositAndCall. +func (tx *Transaction) DepositAndCall() (DepositAndCall, error) { + return retrieveContent[DepositAndCall](tx) +} + +func retrieveContent[T any](tx *Transaction) (T, error) { + typed, ok := tx.content.(T) + if !ok { + var tt T + return tt, errors.Wrapf(ErrCast, "not a %T (op %d)", tt, int(tx.Operation)) + } + + return typed, nil +} diff --git a/pkg/contracts/ton/testdata/00-donation.json b/pkg/contracts/ton/testdata/00-donation.json new file mode 100644 index 0000000000..867f096a90 --- /dev/null +++ b/pkg/contracts/ton/testdata/00-donation.json @@ -0,0 +1,8 @@ +{ + "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + "boc": "b5ee9c72010207010001a10003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017d46c458143cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf000017ab22a3b30366f17efd000146114e1a80102030101a00400827213c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0a7b38b0f8722a81351a279e9d229e4f5d92a5d91aed6c197ab4b5ef756b042cc021b04c0731749165a0bc01860db5611050600c968012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d165a0bc000608235a00002fa8d88b0284cde2fdfa00000032000000000000000040009e408c6c3d090000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bc00000000000000000000000012d452da449e50b8cf7dd27861f146122afe1b546bb8b70fc8216f0c614139f8e04", + "description": "Sample donation to gw contract. https://testnet.tonviewer.com/transaction/d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea", + "hash": "d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea", + "logicalTime": 26201117000003, + "test": true +} \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/01.json b/pkg/contracts/ton/testdata/01-deposit.json similarity index 88% rename from pkg/contracts/ton/testdata/01.json rename to pkg/contracts/ton/testdata/01-deposit.json index 5c05bcc425..d22170ddf0 100644 --- a/pkg/contracts/ton/testdata/01.json +++ b/pkg/contracts/ton/testdata/01-deposit.json @@ -1,8 +1,8 @@ { "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04", - "description": "A deposit to gw contract on testnet", + "description": "Sample deposit to gw contract. https://testnet.tonviewer.com/transaction/cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", "logicalTime": 26023788000003, - "test": false + "test": true } diff --git a/pkg/contracts/ton/testdata/readme.md b/pkg/contracts/ton/testdata/readme.md index 30a59a1639..af8a878904 100644 --- a/pkg/contracts/ton/testdata/readme.md +++ b/pkg/contracts/ton/testdata/readme.md @@ -8,10 +8,10 @@ for further usage in test cases. ## Example usage ```sh -go run pkg/contracts/ton/testdata/scraper.go \ - kQCZfYicgVrqwhxH-Grg44OD78PDRjBnWC9iY61IxaFIW77M \ - 26023788000003 \ - cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf | jq +go run pkg/contracts/ton/testdata/scraper.go -testnet \ + kQCZfYicgVrqwhxH-Grg44OD78PDRjBnWC9iY61IxaFIW77M \ + 26023788000003 \ + cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf | jq ``` Returns @@ -23,7 +23,7 @@ Returns "description": "todo", "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", "logicalTime": 26023788000003, - "test": false + "test": true } ``` diff --git a/pkg/contracts/ton/testdata/scraper.go b/pkg/contracts/ton/testdata/scraper.go index c9c2705375..e75f1bba23 100644 --- a/pkg/contracts/ton/testdata/scraper.go +++ b/pkg/contracts/ton/testdata/scraper.go @@ -20,8 +20,8 @@ func main() { flag.BoolVar(&testnet, "testnet", false, "Use testnet network") flag.Parse() - if len(flag.Args()) != 3 { - log.Fatalf("Usage: go run scrape.go [--testnet]") + if len(flag.Args()) < 3 { + log.Fatalf("Usage: go run scrape.go [-testnet] ") } // Parse account diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go deleted file mode 100644 index e4d0ce4e35..0000000000 --- a/zetaclient/chains/ton/observer/observer_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package observer - -import "testing" - -func TestObserver(t *testing.T) { - // todo -} From 861cb4a31ad74c43eeeb8bfe77a6413ecae041da Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:05:46 +0300 Subject: [PATCH 09/36] Implement Gateway inbound tx parsing --- pkg/contracts/ton/gateway.go | 12 ++- pkg/contracts/ton/gateway_test.go | 87 +++++++++++++++++-- .../ton/testdata/02-deposit-and-call.json | 8 ++ pkg/contracts/ton/testdata/03-failed-tx.json | 8 ++ .../ton/testdata/04-bounced-msg.json | 8 ++ pkg/contracts/ton/testdata/long-call-data.txt | 28 ++++++ 6 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 pkg/contracts/ton/testdata/02-deposit-and-call.json create mode 100644 pkg/contracts/ton/testdata/03-failed-tx.json create mode 100644 pkg/contracts/ton/testdata/04-bounced-msg.json create mode 100644 pkg/contracts/ton/testdata/long-call-data.txt diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index d41a37168f..f2d24d78aa 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -1,6 +1,8 @@ package ton import ( + "bytes" + "cosmossdk.io/math" eth "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" @@ -64,7 +66,8 @@ func NewGateway(accountID ton.AccountID) *Gateway { func (gw *Gateway) ParseTransaction(tx ton.Transaction) (*Transaction, error) { if !tx.IsSuccess() { - return nil, errors.Wrapf(ErrParse, "tx %s is not successful", tx.Hash().Hex()) + exitCode := tx.Description.TransOrd.ComputePh.TrPhaseComputeVm.Vm.ExitCode + return nil, errors.Wrapf(ErrParse, "tx %s is not successful (exit code %d)", tx.Hash().Hex(), exitCode) } if tx.Msgs.InMsg.Exists { @@ -223,13 +226,16 @@ func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cel } var sd tlb.SnakeData - if err = sd.UnmarshalTLB(callDataCell, &tlb.Decoder{}); err != nil { + if err = unmarshalTLB(&sd, callDataCell); err != nil { return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") } cd := boc.BitString(sd) - return DepositAndCall{Deposit: deposit, CallData: cd.Buffer()}, nil + // TLB operates with bits, so we (might) need to trim some "leftovers" (null chars) + callData := bytes.Trim(cd.Buffer(), "\x00") + + return DepositAndCall{Deposit: deposit, CallData: callData}, nil } func (gw *Gateway) parseOutbound(_ ton.Transaction) (*Transaction, error) { diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index 6e9ee2b3d1..fee14a8aed 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -59,7 +59,7 @@ func TestParsing(t *testing.T) { // Check tx props assert.Equal(t, int(OpDeposit), int(parsedTX.Operation)) - // Check + // Check deposit deposit, err := parsedTX.Deposit() assert.NoError(t, err) @@ -79,14 +79,74 @@ func TestParsing(t *testing.T) { }) t.Run("Deposit and call", func(t *testing.T) { - // todo + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "02-deposit-and-call") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + // Check tx props + assert.Equal(t, int(OpDepositAndCall), int(parsedTX.Operation)) + + // Check deposit and call + depositAndCall, err := parsedTX.DepositAndCall() + assert.NoError(t, err) + + const ( + expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" + vitalikDotETH = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + expectedDeposit = 490_000_000 // 0.49 TON + ) + + expectedCallData := readFixtureFile(t, "testdata/long-call-data.txt") + + assert.Equal(t, expectedSender, depositAndCall.Sender.ToRaw()) + assert.Equal(t, expectedDeposit, int(depositAndCall.Amount.Uint64())) + assert.Equal(t, vitalikDotETH, depositAndCall.Recipient.Hex()) + assert.Equal(t, expectedCallData, depositAndCall.CallData) }) t.Run("Irrelevant tx", func(t *testing.T) { - // todo - }) + t.Run("Failed tx", func(t *testing.T) { + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "03-failed-tx") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + _, err := gw.ParseTransaction(tx) + + assert.ErrorIs(t, err, ErrParse) - // todo + // 102 is 'unknown op' + // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/common/errors.fc + assert.ErrorContains(t, err, "is not successful (exit code 102)") + }) + + t.Run("not a deposit nor withdrawal", func(t *testing.T) { + // actually, it's a bounce of the previous tx + + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "04-bounced-msg") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + _, err := gw.ParseTransaction(tx) + assert.Error(t, err) + }) + }) } func TestFixtures(t *testing.T) { @@ -114,10 +174,10 @@ type fixture struct { func getFixtureTX(t *testing.T, name string) (ton.Transaction, fixture) { t.Helper() - filename := fmt.Sprintf("testdata/%s.json", name) - - b, err := fixtures.ReadFile(filename) - require.NoError(t, err, filename) + var ( + filename = fmt.Sprintf("testdata/%s.json", name) + b = readFixtureFile(t, filename) + ) // bag of cells var fx fixture @@ -138,3 +198,12 @@ func getFixtureTX(t *testing.T, name string) (ton.Transaction, fixture) { return tx, fx } + +func readFixtureFile(t *testing.T, filename string) []byte { + t.Helper() + + b, err := fixtures.ReadFile(filename) + require.NoError(t, err, filename) + + return b +} diff --git a/pkg/contracts/ton/testdata/02-deposit-and-call.json b/pkg/contracts/ton/testdata/02-deposit-and-call.json new file mode 100644 index 0000000000..60f824a828 --- /dev/null +++ b/pkg/contracts/ton/testdata/02-deposit-and-call.json @@ -0,0 +1,8 @@ +{ + "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + "boc": "b5ee9c7201021a01000a040003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017d4acf14a83d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea000017d46c45814366f1896b000347487d2080102030201e00405008272a7b38b0f8722a81351a279e9d229e4f5d92a5d91aed6c197ab4b5ef756b042ccdb7075fb2e3f1dc397aa3c93d8dd352cbe54af9fb033df63082875f03a70947102190480b409077359401866309211181901f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d077359400069397a000002fa959e29504cde312d60000003300000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c0080101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002fa959e29508cde312d6c007018b0000006600000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e83a699d01b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b0801fe57686174206973204c6f72656d20497073756d3f2028617070726f782032204b696c6f6279746573290a0a4c6f72656d20497073756d2069732073696d706c792064756d6d792074657874206f6620746865207072696e74696e6720616e64207479706573657474696e6720696e6475737472792e0a4c6f72656d204970730901fe756d20686173206265656e2074686520696e6475737472792773207374616e646172642064756d6d79207465787420657665722073696e6365207468652031353030732c207768656e20616e20756e6b6e6f776e207072696e74657220746f6f6b0a612067616c6c6579206f66207479706520616e6420736372616d626c650a01fe6420697420746f206d616b65206120747970652073706563696d656e20626f6f6b2e20497420686173207375727669766564206e6f74206f6e6c7920666976652063656e7475726965732c0a62757420616c736f20746865206c65617020696e746f20656c656374726f6e6963207479706573657474696e672c2072656d610b01fe696e696e6720657373656e7469616c6c7920756e6368616e6765642e0a0a49742077617320706f70756c61726973656420696e207468652031393630732077697468207468652072656c65617365206f66204c657472617365742073686565747320636f6e7461696e696e67204c6f72656d20497073756d207061737361670c01fe65732c0a616e64206d6f726520726563656e746c792077697468206465736b746f70207075626c697368696e6720736f667477617265206c696b6520416c64757320506167654d616b657220696e636c7564696e672076657273696f6e73206f66204c6f72656d20497073756d2e0a0a57687920646f2077652075736520690d01fe743f0a0a49742069732061206c6f6e672d65737461626c6973686564206661637420746861742061207265616465722077696c6c206265206469737472616374656420627920746865207265616461626c6520636f6e74656e74206f6620612070616765207768656e0a6c6f6f6b696e6720617420697473206c61796f75740e01fe2e2054686520706f696e74206f66207573696e67204c6f72656d20497073756d2069732074686174206974206861732061206d6f72652d6f722d6c657373206e6f726d616c20646973747269627574696f6e206f66206c6574746572732c0a6173206f70706f73656420746f207573696e672027436f6e74656e74206865720f01fe652c20636f6e74656e742068657265272c206d616b696e67206974206c6f6f6b206c696b65207265616461626c6520456e676c6973682e204d616e79206465736b746f70207075626c697368696e670a7061636b6167657320616e6420776562207061676520656469746f7273206e6f7720757365204c6f72656d204970731001fe756d2061732074686569722064656661756c74206d6f64656c20746578742c20616e6420612073656172636820666f7220276c6f72656d20697073756d270a77696c6c20756e636f766572206d616e7920776562207369746573207374696c6c20696e20746865697220696e66616e63792e20566172696f757320766572731101fe696f6e7320686176652065766f6c766564206f766572207468652079656172732c20736f6d6574696d65730a6279206163636964656e742c20736f6d6574696d6573206f6e20707572706f73652028696e6a65637465642068756d6f757220616e6420746865206c696b65292e0a0a576865726520646f657320697420636f1201fe6d652066726f6d3f0a0a436f6e747261727920746f20706f70756c61722062656c6965662c204c6f72656d20497073756d206973206e6f742073696d706c792072616e646f6d20746578742e2049742068617320726f6f747320696e2061207069656365206f6620636c6173736963616c0a4c6174696e206c6974657261741301fe7572652066726f6d2034352042432c206d616b696e67206974206f7665722032303030207965617273206f6c642e2052696368617264204d63436c696e746f636b2c2061204c6174696e2070726f666573736f720a61742048616d7064656e2d5379646e657920436f6c6c65676520696e2056697267696e69612c206c6f6f1401fe6b6564207570206f6e65206f6620746865206d6f7265206f627363757265204c6174696e20776f7264732c20636f6e73656374657475722c0a66726f6d2061204c6f72656d20497073756d20706173736167652c20616e6420676f696e67207468726f75676820746865206369746573206f662074686520776f726420696e1501fe20636c6173736963616c206c6974657261747572652c20646973636f76657265640a74686520756e646f75627461626c6520736f757263652e204c6f72656d20497073756d20636f6d65732066726f6d2073656374696f6e7320312e31302e333220616e6420312e31302e3333206f66202264652046696e6962757320426f1601fe6e6f72756d206574204d616c6f72756d220a285468652045787472656d6573206f6620476f6f6420616e64204576696c292062792043696365726f2c207772697474656e20696e2034352042432e205468697320626f6f6b2069732061207472656174697365206f6e20746865207468656f7279206f66206574686963732c17004a0a7665727920706f70756c617220647572696e67207468652052656e61697373616e63652e009e43f62c3d090000000000000000009b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc9b95b984dcadcc0000000000002000000000003fa4c79ea2219e757506c681b3ab938299662dccbcef3f334edcddee1cd13bade44920284", + "description": "Sample deposit-and-call to gw contract. https://testnet.tonviewer.com/transaction/3647f17cc28e4a70404a10c62ad6262fbf67aa72579acde449d66cc0d0fd7ca8", + "hash": "3647f17cc28e4a70404a10c62ad6262fbf67aa72579acde449d66cc0d0fd7ca8", + "logicalTime": 26202202000003, + "test": true +} \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/03-failed-tx.json b/pkg/contracts/ton/testdata/03-failed-tx.json new file mode 100644 index 0000000000..0fa772b7fe --- /dev/null +++ b/pkg/contracts/ton/testdata/03-failed-tx.json @@ -0,0 +1,8 @@ +{ + "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + "boc": "b5ee9c72010208010001e90003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017d5af81eb033647f17cc28e4a70404a10c62ad6262fbf67aa72579acde449d66cc0d0fd7ca8000017d4acf14a8366f1b2b00003461489a480102030201e00405008272db7075fb2e3f1dc397aa3c93d8dd352cbe54af9fb033df63082875f03a709471f41d1551f3ff76f22096fabf1806f354a5437a60a611df452f2a78e6dbd9adab01290482c7c9017d78401061061c0e0181046998208d6a0700cb68012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d017d784000608235a00002fab5f03d604cde365602432b6363796102bb7b9363210c00101df0600d3580132fb113902b5d584388ff0d5c1c70707df87868c60ceb05ec4c75a918b4290b700256531c67b13257d99a0ecbec7282c27792907dbec21ee93234996e5a9333943d0179e56800608235a00002fab5f03d608cde365607fffffffa432b6363796102bb7b9363210c0009e40a7cc0f424000000000cc0000002600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "description": "failed tx with body='Hello, World!'; https://testnet.tonviewer.com/transaction/653d37cfbff76585d336fb74a0eaa7fe6d1a2b3cae56d5e5f9609a821c9f1e45", + "hash": "653d37cfbff76585d336fb74a0eaa7fe6d1a2b3cae56d5e5f9609a821c9f1e45", + "logicalTime": 26206540000003, + "test": true +} diff --git a/pkg/contracts/ton/testdata/04-bounced-msg.json b/pkg/contracts/ton/testdata/04-bounced-msg.json new file mode 100644 index 0000000000..9887db3123 --- /dev/null +++ b/pkg/contracts/ton/testdata/04-bounced-msg.json @@ -0,0 +1,8 @@ +{ + "account": "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f", + "boc": "b5ee9c72010207010001a30003b579594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f000017d5af81eb05b4269279feefdc3ea4220465ec2bce20c6e7f0a8c09b51dac55b90aef3c342c6000017d5af81eb0166f1b2b0000146097f4080102030101a0040082727682d6ce21cbeaec70573e8d6ab7dc6357b875cbffc8c50608035fda3fe3bc378db05a075336855e0ae2db0067bf7de2341a87b9d555e968d64b216fe5491aba02150c090179e568186097f411050600d3580132fb113902b5d584388ff0d5c1c70707df87868c60ceb05ec4c75a918b4290b700256531c67b13257d99a0ecbec7282c27792907dbec21ee93234996e5a9333943d0179e56800608235a00002fab5f03d608cde365607fffffffa432b6363796102bb7b9363210c0009e40614c0f1da800000000000000001700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bc00000000000000000000000012d452da449e50b8cf7dd27861f146122afe1b546bb8b70fc8216f0c614139f8e04", + "description": "Sample bounced message. This address is not even a gw. https://testnet.tonviewer.com/transaction/b3c46f5faf8aee7348083e7adbbc9a60ab1c8e0eac09133d64e2c4eb831e607b", + "hash": "b3c46f5faf8aee7348083e7adbbc9a60ab1c8e0eac09133d64e2c4eb831e607b", + "logicalTime": 26206540000005, + "test": true +} diff --git a/pkg/contracts/ton/testdata/long-call-data.txt b/pkg/contracts/ton/testdata/long-call-data.txt new file mode 100644 index 0000000000..d66c4d103a --- /dev/null +++ b/pkg/contracts/ton/testdata/long-call-data.txt @@ -0,0 +1,28 @@ +What is Lorem Ipsum? (approx 2 Kilobytes) + +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took +a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, +but also the leap into electronic typesetting, remaining essentially unchanged. + +It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, +and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +Why do we use it? + +It is a long-established fact that a reader will be distracted by the readable content of a page when +looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, +as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing +packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' +will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes +by accident, sometimes on purpose (injected humour and the like). + +Where does it come from? + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical +Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor +at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, +from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered +the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" +(The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, +very popular during the Renaissance. \ No newline at end of file From a19a61b4c6d969ebe1f418c793bd716550360b32 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:54:06 +0300 Subject: [PATCH 10/36] Add Gateway tx filtration --- pkg/contracts/ton/gateway.go | 25 ++++++++++++++++ pkg/contracts/ton/gateway_test.go | 50 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index f2d24d78aa..1c331d17c8 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -87,6 +87,31 @@ func (gw *Gateway) ParseTransaction(tx ton.Transaction) (*Transaction, error) { return outbound, nil } +// ParseAndFilter parses transaction and applies filter to it. Returns (tx, skip?, error) +// If parse fails due to known error, skip is set to true +func (gw *Gateway) ParseAndFilter(tx ton.Transaction, filter func(*Transaction) bool) (*Transaction, bool, error) { + parsedTX, err := gw.ParseTransaction(tx) + switch { + case errors.Is(err, ErrParse): + return nil, true, nil + case errors.Is(err, ErrUnknownOp): + return nil, true, nil + case err != nil: + return nil, false, err + } + + if !filter(parsedTX) { + return nil, true, nil + } + + return parsedTX, false, nil +} + +// FilterDeposit filters transactions with deposit operations +func FilterDeposit(tx *Transaction) bool { + return tx.Operation == OpDeposit || tx.Operation == OpDepositAndCall +} + func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { body, err := parseInternalMessageBody(tx) if err != nil { diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index fee14a8aed..5820f59edc 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -149,6 +149,56 @@ func TestParsing(t *testing.T) { }) } +func TestFiltering(t *testing.T) { + t.Run("Inbound", func(t *testing.T) { + for _, tt := range []struct { + name string + skip bool + error bool + }{ + // donation is not a deposit :) + {"00-donation", true, false}, + + // Should be parsed and filtered + {"01-deposit", false, false}, + {"02-deposit-and-call", false, false}, + + // Should be skipped + {"03-failed-tx", true, false}, + {"04-bounced-msg", true, false}, + } { + t.Run(tt.name, func(t *testing.T) { + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, tt.name) + + // Given a gateway + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + parsedTX, skip, err := gw.ParseAndFilter(tx, FilterDeposit) + + if tt.error { + require.Error(t, err) + assert.False(t, skip) + assert.Nil(t, parsedTX) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.skip, skip) + + if tt.skip { + assert.Nil(t, parsedTX) + return + } + + assert.NotNil(t, parsedTX) + }) + } + }) +} + func TestFixtures(t *testing.T) { // ACT tx, _ := getFixtureTX(t, "01-deposit") From 5f14515b2668c123779514463b0d9046bc8783c1 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 25 Sep 2024 00:26:20 +0300 Subject: [PATCH 11/36] Add GetBlockHeader cache; Add masterchain seqno. Implement watchInbound --- pkg/contracts/ton/gateway.go | 32 ++- pkg/contracts/ton/gateway_test.go | 6 +- zetaclient/chains/ton/liteapi/client.go | 53 ++++- .../chains/ton/liteapi/client_live_test.go | 35 +++- zetaclient/chains/ton/observer/inbound.go | 186 +++++++++++++----- zetaclient/chains/ton/observer/observer.go | 24 ++- 6 files changed, 260 insertions(+), 76 deletions(-) diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index 1c331d17c8..847d967c9e 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -11,6 +11,7 @@ import ( "github.com/tonkeeper/tongo/ton" ) +// Op operation code type Op uint32 // github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc @@ -29,26 +30,46 @@ const ( UpdateCode ) +// Gateway wrapper around zeta gateway contract on TON type Gateway struct { accountID ton.AccountID } +// Donation represents a donation operation type Donation struct { Sender ton.AccountID Amount math.Uint } +// Deposit represents a deposit operation type Deposit struct { Sender ton.AccountID Amount math.Uint Recipient eth.Address } +// Memo casts deposit to memo bytes +func (d Deposit) Memo() []byte { + return d.Recipient.Bytes() +} + +// DepositAndCall represents a deposit and call operation type DepositAndCall struct { Deposit CallData []byte } +// Memo casts deposit and call to memo bytes +func (d DepositAndCall) Memo() []byte { + recipient := d.Recipient.Bytes() + out := make([]byte, 0, len(recipient)+len(d.CallData)) + + out = append(out, recipient...) + out = append(out, d.CallData...) + + return out +} + const ( sizeOpCode = 32 sizeQueryID = 64 @@ -64,6 +85,11 @@ func NewGateway(accountID ton.AccountID) *Gateway { return &Gateway{accountID} } +func (gw *Gateway) AccountID() ton.AccountID { + return gw.accountID +} + +// ParseTransaction parses transaction to Transaction func (gw *Gateway) ParseTransaction(tx ton.Transaction) (*Transaction, error) { if !tx.IsSuccess() { exitCode := tx.Description.TransOrd.ComputePh.TrPhaseComputeVm.Vm.ExitCode @@ -107,10 +133,8 @@ func (gw *Gateway) ParseAndFilter(tx ton.Transaction, filter func(*Transaction) return parsedTX, false, nil } -// FilterDeposit filters transactions with deposit operations -func FilterDeposit(tx *Transaction) bool { - return tx.Operation == OpDeposit || tx.Operation == OpDepositAndCall -} +// FilterInbounds filters transactions with deposit operations +func FilterInbounds(tx *Transaction) bool { return tx.IsInbound() } func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { body, err := parseInternalMessageBody(tx) diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index 5820f59edc..70d56e11c0 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -156,10 +156,8 @@ func TestFiltering(t *testing.T) { skip bool error bool }{ - // donation is not a deposit :) - {"00-donation", true, false}, - // Should be parsed and filtered + {"00-donation", false, false}, {"01-deposit", false, false}, {"02-deposit-and-call", false, false}, @@ -176,7 +174,7 @@ func TestFiltering(t *testing.T) { gw := NewGateway(ton.MustParseAccountID(fx.Account)) // ACT - parsedTX, skip, err := gw.ParseAndFilter(tx, FilterDeposit) + parsedTX, skip, err := gw.ParseAndFilter(tx, FilterInbounds) if tt.error { require.Error(t, err) diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index e895ff4298..bbe94147fd 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" @@ -13,11 +14,61 @@ import ( ) // Client extends tongo's liteapi.Client with some high-level tools +// Reference: https://github.com/ton-blockchain/ton/blob/master/tl/generate/scheme/tonlib_api.tl type Client struct { *liteapi.Client + blockCache *lru.Cache } -const pageSize = 200 +const ( + pageSize = 200 + blockCacheSize = 250 +) + +// New Client constructor. +func New(client *liteapi.Client) *Client { + blockCache, _ := lru.New(blockCacheSize) + + return &Client{Client: client, blockCache: blockCache} +} + +// GetBlockHeader returns block header by block ID. +// Uses LRU cache for network efficiency. +// I haven't found what mode means but `0` works fine. +func (c *Client) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) { + if c.blockCache == nil { + return tlb.BlockInfo{}, errors.New("block cache is not initialized") + } + + cached, ok := c.getBlockHeaderCache(blockID) + if ok { + return cached, nil + } + + header, err := c.Client.GetBlockHeader(ctx, blockID, mode) + if err != nil { + return tlb.BlockInfo{}, err + } + + c.setBlockHeaderCache(blockID, header) + + return header, nil +} + +func (c *Client) getBlockHeaderCache(blockID ton.BlockIDExt) (tlb.BlockInfo, bool) { + raw, ok := c.blockCache.Get(blockID.String()) + if !ok { + return tlb.BlockInfo{}, false + } + + header, ok := raw.(tlb.BlockInfo) + + return header, ok +} + +func (c *Client) setBlockHeaderCache(blockID ton.BlockIDExt, header tlb.BlockInfo) { + c.blockCache.Add(blockID.String(), header) +} // GetFirstTransaction scrolls through the transactions of the given account to find the first one. // Note that it might fail w/o using an archival node. Also returns the number of diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index 435da2787a..d090771ed9 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -19,12 +19,12 @@ import ( func TestClient(t *testing.T) { if !common.LiveTestEnabled() { - t.Skip("Live tests are disabled") + //t.Skip("Live tests are disabled") } var ( ctx = context.Background() - client = &Client{Client: mustCreateClient(t)} + client = New(mustCreateClient(t)) ) t.Run("GetFirstTransaction", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestClient(t *testing.T) { t.Run("GetTransactionsUntil", func(t *testing.T) { // ARRANGE - // Given sample account id (a dev wallet) + // Given sample account id (dev wallet) // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") require.NoError(t, err) @@ -91,6 +91,35 @@ func TestClient(t *testing.T) { mustContainTX(t, txs, "a6672a0e80193c1f705ef1cf45a5883441b8252523b1d08f7656c80e400c74a8") assert.GreaterOrEqual(t, len(txs), expectedTX) }) + + t.Run("GetBlockHeader", func(t *testing.T) { + // ARRANGE + // Given sample account id (dev wallet) + // https://tonscan.org/address/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr + accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") + require.NoError(t, err) + + const getUntilLT = uint64(48645164000001) + const getUntilHash = `2e107215e634bbc3492bdf4b1466d59432623295072f59ab526d15737caa9531` + + var hash ton.Bits256 + require.NoError(t, hash.FromHex(getUntilHash)) + + txs, err := client.GetTransactions(ctx, 1, accountID, getUntilLT, hash) + require.NoError(t, err) + require.Len(t, txs, 1) + + // Given a block + blockID := txs[0].BlockID + + // ACT + header, err := client.GetBlockHeader(ctx, blockID, 0) + + // ASSERT + require.NoError(t, err) + require.NotZero(t, header.MinRefMcSeqno) + require.Equal(t, header.MinRefMcSeqno, header.MasterRef.Master.SeqNo) + }) } func mustCreateClient(t *testing.T) *liteapi.Client { diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index e8d403d5e8..2d0950bd89 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -5,13 +5,17 @@ import ( "fmt" "slices" - "cosmossdk.io/errors" + "cosmossdk.io/math" + "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/coin" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/zetacore" ) const ( @@ -58,14 +62,6 @@ func (ob *Observer) watchInbound(ctx context.Context) error { ) } -// Flow: -// - [x] Ensure last scanned transaction is set -// - [x] Get all transaction between [lastScannedTx; now] -// - [ ] Filter only valid and inbound transactions -// - [ ] For each transaction (ordered by *ASC*) -// - [ ] Construct crosschain cosmos message -// - [ ] Vote -// - [ ] Save last scanned tx func (ob *Observer) observeInbound(ctx context.Context, _ *zctx.AppContext) error { if err := ob.ensureLastScannedTX(ctx); err != nil { return errors.Wrap(err, "unable to ensure last scanned tx") @@ -76,7 +72,7 @@ func (ob *Observer) observeInbound(ctx context.Context, _ *zctx.AppContext) erro return errors.Wrapf(err, "unable to parse last scanned tx %q", ob.LastTxScanned()) } - txs, err := ob.client.GetTransactionsUntil(ctx, ob.gatewayID, lt, hashBits) + txs, err := ob.client.GetTransactionsUntil(ctx, ob.gateway.AccountID(), lt, hashBits) if err != nil { return errors.Wrap(err, "unable to get transactions") } @@ -97,66 +93,154 @@ func (ob *Observer) observeInbound(ctx context.Context, _ *zctx.AppContext) erro ob.Logger().Inbound.Info().Msgf("ObserveInbound: got %d transactions", len(txs)) } - // todo deploy sample GW to testnet - // todo send some TON and test + for i := range txs { + tx := txs[i] - // todo FilterInboundEvent + parsedTX, skip, err := ob.gateway.ParseAndFilter(tx, toncontracts.FilterInbounds) + if err != nil { + return errors.Wrap(err, "unable to parse and filter tx") + } - for _, tx := range txs { - fmt.Println("TON TX", tx) - } + if skip { + ob.setLastScannedTX(&tx) + continue + } - /* - // loop signature from oldest to latest to filter inbound events - for i := len(signatures) - 1; i >= 0; i-- { - sig := signatures[i] - sigString := sig.Signature.String() - - // process successfully signature only - if sig.Err == nil { - txResult, err := ob.solClient.GetTransaction(ctx, sig.Signature, &rpc.GetTransactionOpts{}) - if err != nil { - // we have to re-scan this signature on next ticker - return errors.Wrapf(err, "error GetTransaction for chain %d sig %s", chainID, sigString) - } - - // filter inbound events and vote - err = ob.FilterInboundEventsAndVote(ctx, txResult) - if err != nil { - // we have to re-scan this signature on next ticker - return errors.Wrapf(err, "error FilterInboundEventAndVote for chain %d sig %s", chainID, sigString) - } - } - - // signature scanned; save last scanned signature to both memory and db, ignore db error - if err := ob.SaveLastTxScanned(sigString, sig.Slot); err != nil { - ob.Logger(). - Inbound.Error(). - Err(err). - Msgf("ObserveInbound: error saving last sig %s for chain %d", sigString, chainID) - } - ob.Logger(). - Inbound.Info(). - Msgf("ObserveInbound: last scanned sig is %s for chain %d in slot %d", sigString, chainID, sig.Slot) - */ + if _, err := ob.voteInbound(ctx, parsedTX); err != nil { + return errors.Wrapf(err, "unable to vote inbound (hash %s)", parsedTX.Hash().Hex()) + } + + ob.setLastScannedTX(&parsedTX.Transaction) + } return nil } +func (ob *Observer) voteInbound(ctx context.Context, tx *toncontracts.Transaction) (string, error) { + // noop + if tx.Operation == toncontracts.OpDonate { + ob.Logger().Inbound.Info(). + Uint64("tx.lt", tx.Lt). + Str("tx.hash", tx.Hash().Hex()). + Msg("Thank you rich folk for your donation!") + + return "", nil + } + + // todo add compliance check + // https://github.com/zeta-chain/node/issues/2916 + + blockHeader, err := ob.client.GetBlockHeader(ctx, tx.BlockID, 0) + if err != nil { + return "", errors.Wrapf(err, "unable to get block header %s", tx.BlockID.String()) + } + + sender, amount, memo, err := extractInboundData(tx) + if err != nil { + return "", err + } + + seqno := blockHeader.MinRefMcSeqno + + return ob.voteDeposit(ctx, tx, sender, amount, memo, seqno) +} + +func extractInboundData(tx *toncontracts.Transaction) (string, math.Uint, []byte, error) { + if tx.Operation == toncontracts.OpDeposit { + d, err := tx.Deposit() + if err != nil { + return "", math.NewUint(0), nil, err + } + + return d.Sender.ToRaw(), d.Amount, d.Memo(), nil + } + + if tx.Operation == toncontracts.OpDepositAndCall { + d, err := tx.DepositAndCall() + if err != nil { + return "", math.NewUint(0), nil, err + } + + return d.Sender.ToRaw(), d.Amount, d.Memo(), nil + } + + return "", math.NewUint(0), nil, fmt.Errorf("unknown operation %d", tx.Operation) +} + +func (ob *Observer) voteDeposit( + ctx context.Context, + tx *toncontracts.Transaction, + sender string, + amount math.Uint, + memo []byte, + seqno uint32, +) (string, error) { + const ( + eventIndex = 0 // not a smart contract call + coinType = coin.CoinType_Gas + asset = "" // empty for gas coin + gasLimit = 0 + retryGasLimit = zetacore.PostVoteInboundExecutionGasLimit + ) + + var ( + operatorAddress = ob.ZetacoreClient().GetKeys().GetOperatorAddress() + inboundHash = liteapi.TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) + ) + + msg := zetacore.GetInboundVoteMessage( + sender, + ob.Chain().ChainId, + sender, + sender, + ob.ZetacoreClient().Chain().ChainId, + amount, + string(memo), + inboundHash, + uint64(seqno), + gasLimit, + coinType, + asset, + operatorAddress.String(), + eventIndex, + ) + + return ob.PostVoteInbound(ctx, msg, retryGasLimit) +} + func (ob *Observer) ensureLastScannedTX(ctx context.Context) error { // noop if ob.LastTxScanned() != "" { return nil } - tx, _, err := ob.client.GetFirstTransaction(ctx, ob.gatewayID) + tx, _, err := ob.client.GetFirstTransaction(ctx, ob.gateway.AccountID()) if err != nil { return err } + ob.setLastScannedTX(tx) + + return nil +} + +func (ob *Observer) setLastScannedTX(tx *ton.Transaction) { txHash := liteapi.TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) ob.WithLastTxScanned(txHash) - return ob.WriteLastTxScannedToDB(txHash) + if err := ob.WriteLastTxScannedToDB(txHash); err != nil { + ob.Logger().Inbound.Error(). + Err(err). + Uint64("tx.lt", tx.Lt). + Str("tx.hash", tx.Hash().Hex()). + Msgf("ObserveInbound: unable to WriteLastTxScannedToDB") + + return + } + + ob.Logger().Inbound.Info(). + Uint64("tx.lt", tx.Lt). + Str("tx.hash", tx.Hash().Hex()). + Msgf("ObserveInbound: WriteLastTxScannedToDB") } diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index 03f019d187..5989293795 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -2,10 +2,10 @@ package observer import ( "context" - - "github.com/tonkeeper/tongo/ton" + "errors" "github.com/zeta-chain/node/pkg/bg" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -15,19 +15,19 @@ import ( type Observer struct { base.Observer - client *liteapi.Client - gatewayID ton.AccountID + client *liteapi.Client + gateway *toncontracts.Gateway } var _ interfaces.ChainObserver = (*Observer)(nil) -func New(bo *base.Observer, client *liteapi.Client, gatewayID ton.AccountID) (*Observer, error) { +func New(bo *base.Observer, client *liteapi.Client, gateway *toncontracts.Gateway) (*Observer, error) { bo.LoadLastTxScanned() return &Observer{ - Observer: *bo, - gatewayID: gatewayID, - client: client, + Observer: *bo, + client: client, + gateway: gateway, }, nil } @@ -46,14 +46,12 @@ func (ob *Observer) Start(ctx context.Context) { bg.Work(ctx, ob.watchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) // todo - // watchOutbound - // watchGasPrice // watchInboundTracker // watchOutbound + // watchGasPrice // watchRPCStatus } -func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *types.CrossChainTx) (bool, error) { - // todo - return false, nil +func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossChainTx) (bool, error) { + return false, errors.New("not implemented") } From bfa9af828a58ffcc52cb6350a4b1ceb3cf4d09e7 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:53:55 +0200 Subject: [PATCH 12/36] Improve ton contracts pkg --- e2e/runner/ton/accounts.go | 14 +--- pkg/contracts/ton/gateway.go | 97 +++++++++++----------------- pkg/contracts/ton/gateway_op.go | 103 ++++++++++++++++++++++++++++++ pkg/contracts/ton/gateway_test.go | 56 ++++++++++++++++ 4 files changed, 198 insertions(+), 72 deletions(-) create mode 100644 pkg/contracts/ton/gateway_op.go diff --git a/e2e/runner/ton/accounts.go b/e2e/runner/ton/accounts.go index f1a3fea659..3ffd8c0907 100644 --- a/e2e/runner/ton/accounts.go +++ b/e2e/runner/ton/accounts.go @@ -12,6 +12,8 @@ import ( "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/wallet" "golang.org/x/crypto/ed25519" + + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" ) const workchainID = 0 @@ -138,7 +140,7 @@ func buildGatewayData(tss eth.Address) (*boc.Cell, error) { cell = boc.NewCell() ) - err := errCollect( + err := toncontracts.ErrCollect( cell.WriteBit(true), // deposits_enabled zeroCoins.MarshalTLB(cell, enc), // total_locked zeroCoins.MarshalTLB(cell, enc), // fees @@ -153,16 +155,6 @@ func buildGatewayData(tss eth.Address) (*boc.Cell, error) { return cell, nil } -func errCollect(errs ...error) error { - for i, err := range errs { - if err != nil { - return errors.Wrapf(err, "error at index %d", i) - } - } - - return nil -} - // copied from tongo wallets_common.go func generateStateInit(code, data *boc.Cell) *tlb.StateInit { return &tlb.StateInit{ diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index 847d967c9e..bdd2c433e3 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -11,65 +11,11 @@ import ( "github.com/tonkeeper/tongo/ton" ) -// Op operation code -type Op uint32 - -// github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc -// Inbound operations -const ( - OpDonate Op = 100 + iota - OpDeposit - OpDepositAndCall -) - -// Outbound operations -const ( - OpWithdraw Op = 200 + iota - SetDepositsEnabled - UpdateTSS - UpdateCode -) - // Gateway wrapper around zeta gateway contract on TON type Gateway struct { accountID ton.AccountID } -// Donation represents a donation operation -type Donation struct { - Sender ton.AccountID - Amount math.Uint -} - -// Deposit represents a deposit operation -type Deposit struct { - Sender ton.AccountID - Amount math.Uint - Recipient eth.Address -} - -// Memo casts deposit to memo bytes -func (d Deposit) Memo() []byte { - return d.Recipient.Bytes() -} - -// DepositAndCall represents a deposit and call operation -type DepositAndCall struct { - Deposit - CallData []byte -} - -// Memo casts deposit and call to memo bytes -func (d DepositAndCall) Memo() []byte { - recipient := d.Recipient.Bytes() - out := make([]byte, 0, len(recipient)+len(d.CallData)) - - out = append(out, recipient...) - out = append(out, d.CallData...) - - return out -} - const ( sizeOpCode = 32 sizeQueryID = 64 @@ -274,16 +220,11 @@ func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cel return DepositAndCall{}, errors.Wrap(err, "unable to read call data cell") } - var sd tlb.SnakeData - if err = unmarshalTLB(&sd, callDataCell); err != nil { + callData, err := unmarshalSnakeCell(callDataCell) + if err != nil { return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") } - cd := boc.BitString(sd) - - // TLB operates with bits, so we (might) need to trim some "leftovers" (null chars) - callData := bytes.Trim(cd.Buffer(), "\x00") - return DepositAndCall{Deposit: deposit, CallData: callData}, nil } @@ -309,6 +250,16 @@ func parseInternalMessageBody(tx ton.Transaction) (*boc.Cell, error) { return body, nil } +func ErrCollect(errs ...error) error { + for i, err := range errs { + if err != nil { + return errors.Wrapf(err, "error at index %d", i) + } + } + + return nil +} + func marshalCell(v tlb.MarshalerTLB) (*boc.Cell, error) { cell := boc.NewCell() @@ -337,6 +288,30 @@ func unmarshalTLB(t tlb.UnmarshalerTLB, cell *boc.Cell) error { return t.UnmarshalTLB(cell, &tlb.Decoder{}) } +func unmarshalSnakeCell(cell *boc.Cell) ([]byte, error) { + var sd tlb.SnakeData + + if err := unmarshalTLB(&sd, cell); err != nil { + return nil, err + } + + cd := boc.BitString(sd) + + // TLB operates with bits, so we (might) need to trim some "leftovers" (null chars) + return bytes.Trim(cd.Buffer(), "\x00"), nil +} + +func marshalSnakeData(data []byte) (*boc.Cell, error) { + b := boc.NewCell() + + wrapped := tlb.Bytes(data) + if err := wrapped.MarshalTLB(b, &tlb.Encoder{}); err != nil { + return nil, err + } + + return b, nil +} + func gramToUint(g tlb.Grams) math.Uint { return math.NewUint(uint64(g)) } diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go new file mode 100644 index 0000000000..f476d49623 --- /dev/null +++ b/pkg/contracts/ton/gateway_op.go @@ -0,0 +1,103 @@ +package ton + +import ( + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/ton" +) + +// Op operation code +type Op uint32 + +// github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc +// Inbound operations +const ( + OpDonate Op = 100 + iota + OpDeposit + OpDepositAndCall +) + +// Outbound operations +const ( + OpWithdraw Op = 200 + iota + SetDepositsEnabled + UpdateTSS + UpdateCode +) + +// Donation represents a donation operation +type Donation struct { + Sender ton.AccountID + Amount math.Uint +} + +// AsBody casts struct as internal message body. +func (d Donation) AsBody() (*boc.Cell, error) { + b := boc.NewCell() + err := ErrCollect( + b.WriteUint(uint64(OpDonate), sizeOpCode), + b.WriteUint(0, sizeQueryID), + ) + + return b, err +} + +// Deposit represents a deposit operation +type Deposit struct { + Sender ton.AccountID + Amount math.Uint + Recipient eth.Address +} + +// Memo casts deposit to memo bytes +func (d Deposit) Memo() []byte { + return d.Recipient.Bytes() +} + +// AsBody casts struct as internal message body. +func (d Deposit) AsBody() (*boc.Cell, error) { + b := boc.NewCell() + err := ErrCollect( + b.WriteUint(uint64(OpDeposit), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(d.Recipient.Bytes()), + ) + + return b, err +} + +// DepositAndCall represents a deposit and call operation +type DepositAndCall struct { + Deposit + CallData []byte +} + +// Memo casts deposit to call to memo bytes +func (d DepositAndCall) Memo() []byte { + recipient := d.Recipient.Bytes() + out := make([]byte, 0, len(recipient)+len(d.CallData)) + + out = append(out, recipient...) + out = append(out, d.CallData...) + + return out +} + +// AsBody casts struct to internal message body. +func (d DepositAndCall) AsBody() (*boc.Cell, error) { + callDataCell, err := marshalSnakeData(d.CallData) + if err != nil { + return nil, err + } + + b := boc.NewCell() + err = ErrCollect( + b.WriteUint(uint64(OpDepositAndCall), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(d.Recipient.Bytes()), + b.AddRef(callDataCell), + ) + + return b, err +} diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index 70d56e11c0..6a300ae637 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -4,8 +4,10 @@ import ( "embed" "encoding/json" "fmt" + "strings" "testing" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/boc" @@ -14,6 +16,15 @@ import ( ) func TestParsing(t *testing.T) { + swapBodyAndParse := func(gw *Gateway, tx ton.Transaction, body *boc.Cell) *Transaction { + tx.Msgs.InMsg.Value.Value.Body.Value = tlb.Any(*body) + + parsed, err := gw.ParseTransaction(tx) + require.NoError(t, err) + + return parsed + } + t.Run("Donate", func(t *testing.T) { // ARRANGE // Given a tx @@ -40,6 +51,14 @@ func TestParsing(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedSender, donation.Sender.ToRaw()) assert.Equal(t, expectedDonation, int(donation.Amount.Uint64())) + + // Check that AsBody works + var ( + parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(donation.AsBody())) + donation2 = lo.Must(parsedTX2.Donation()) + ) + + assert.Equal(t, donation, donation2) }) t.Run("Deposit", func(t *testing.T) { @@ -76,6 +95,14 @@ func TestParsing(t *testing.T) { // Check that other casting fails _, err = parsedTX.Donation() assert.ErrorIs(t, err, ErrCast) + + // Check that AsBody works + var ( + parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(deposit.AsBody())) + deposit2 = lo.Must(parsedTX2.Deposit()) + ) + + assert.Equal(t, deposit, deposit2) }) t.Run("Deposit and call", func(t *testing.T) { @@ -111,6 +138,14 @@ func TestParsing(t *testing.T) { assert.Equal(t, expectedDeposit, int(depositAndCall.Amount.Uint64())) assert.Equal(t, vitalikDotETH, depositAndCall.Recipient.Hex()) assert.Equal(t, expectedCallData, depositAndCall.CallData) + + // Check that AsBody works + var ( + parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(depositAndCall.AsBody())) + depositAndCall2 = lo.Must(parsedTX2.DepositAndCall()) + ) + + assert.Equal(t, depositAndCall, depositAndCall2) }) t.Run("Irrelevant tx", func(t *testing.T) { @@ -206,6 +241,27 @@ func TestFixtures(t *testing.T) { require.Equal(t, "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", tx.Hash().Hex()) } +func TestSnakeData(t *testing.T) { + for _, tt := range []string{ + "Hello world", + "123", + strings.Repeat(`ZetaChain `, 300), + string(readFixtureFile(t, "testdata/long-call-data.txt")), + } { + a := []byte(tt) + + cell, err := marshalSnakeData(a) + require.NoError(t, err) + + b, err := unmarshalSnakeCell(cell) + require.NoError(t, err) + + t.Logf(string(b)) + + assert.Equal(t, a, b, tt) + } +} + //go:embed testdata var fixtures embed.FS From feda30f5c6ff27e7bb4d9d2949f7aea6691a3b39 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:21:38 +0200 Subject: [PATCH 13/36] Add IsTONChain() --- pkg/chains/chain.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index bfd7398625..19ac59830d 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -93,6 +93,10 @@ func (chain Chain) IsBitcoinChain() bool { return chain.Consensus == Consensus_bitcoin } +func (chain Chain) IsTONChain() bool { + return IsTONChain(chain.ChainId, []Chain{chain}) +} + // DecodeAddressFromChainID decode the address string to bytes // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade @@ -132,6 +136,11 @@ func IsSolanaChain(chainID int64, additionalChains []Chain) bool { return ChainIDInChainList(chainID, ChainListByNetwork(Network_solana, additionalChains)) } +// IsTONChain returns true is the chain is TON chain +func IsTONChain(chainID int64, additionalChains []Chain) bool { + return ChainIDInChainList(chainID, ChainListByNetwork(Network_ton, additionalChains)) +} + // IsEthereumChain returns true if the chain is an Ethereum chain // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade From 2be832f581f34da9b90a97a928e446ecc6de6abe Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:22:00 +0200 Subject: [PATCH 14/36] Refactor Gateway package --- pkg/contracts/ton/gateway.go | 115 ++++-------------------------- pkg/contracts/ton/gateway_op.go | 2 +- pkg/contracts/ton/gateway_test.go | 4 +- pkg/contracts/ton/tlb.go | 79 ++++++++++++++++++++ 4 files changed, 97 insertions(+), 103 deletions(-) create mode 100644 pkg/contracts/ton/tlb.go diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index bdd2c433e3..201e8abb69 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -1,10 +1,7 @@ package ton import ( - "bytes" - "cosmossdk.io/math" - eth "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" @@ -123,7 +120,7 @@ func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { switch opCode { case OpDonate: amount := intMsgInfo.Value.Grams - tx.TotalFees.Grams - content = Donation{Sender: sender, Amount: gramToUint(amount)} + content = Donation{Sender: sender, Amount: GramsToUint(amount)} case OpDeposit: content, errContent = parseDeposit(tx, sender, body) case OpDepositAndCall: @@ -151,7 +148,7 @@ func parseDeposit(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (Dep return Deposit{}, err } - recipient, err := readEVMAddress(body) + recipient, err := UnmarshalEVMAddress(body) if err != nil { return Deposit{}, errors.Wrap(err, "unable to read recipient") } @@ -187,26 +184,26 @@ func parseDepositLog(tx ton.Transaction) (depositLog, error) { // .store_uint(evm_recipient, size::evm_address) // .end_cell(); - body, err := marshalCellRef(messages[0].Value.Body) - if err != nil { - return depositLog{}, errors.Wrap(err, "unable to read body cell") - } + var ( + bodyValue = boc.Cell(messages[0].Value.Body.Value) + body = &bodyValue + ) if err := body.Skip(sizeOpCode + sizeQueryID); err != nil { return depositLog{}, errors.Wrap(err, "unable to skip bits") } // skip msg address (ton sender) - if err := unmarshalTLB(&tlb.MsgAddress{}, body); err != nil { + if err := UnmarshalTLB(&tlb.MsgAddress{}, body); err != nil { return depositLog{}, errors.Wrap(err, "unable to read sender address") } var deposited tlb.Grams - if err := unmarshalTLB(&deposited, body); err != nil { + if err := UnmarshalTLB(&deposited, body); err != nil { return depositLog{}, errors.Wrap(err, "unable to read deposited amount") } - return depositLog{Amount: gramToUint(deposited)}, nil + return depositLog{Amount: GramsToUint(deposited)}, nil } func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (DepositAndCall, error) { @@ -220,7 +217,7 @@ func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cel return DepositAndCall{}, errors.Wrap(err, "unable to read call data cell") } - callData, err := unmarshalSnakeCell(callDataCell) + callData, err := UnmarshalSnakeCell(callDataCell) if err != nil { return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") } @@ -237,92 +234,10 @@ func parseInternalMessageBody(tx ton.Transaction) (*boc.Cell, error) { return nil, errors.Wrap(ErrParse, "tx should have an internal message") } - either := tx.Msgs.InMsg.Value.Value.Body - if either.IsRight { - return nil, errors.Wrap(ErrParse, "tx body should not be a Ref") - } - - body, err := marshalCell(&either.Value) - if err != nil { - return nil, errors.Wrap(err, "unable to read body cell") - } - - return body, nil -} - -func ErrCollect(errs ...error) error { - for i, err := range errs { - if err != nil { - return errors.Wrapf(err, "error at index %d", i) - } - } - - return nil -} - -func marshalCell(v tlb.MarshalerTLB) (*boc.Cell, error) { - cell := boc.NewCell() - - if err := v.MarshalTLB(cell, &tlb.Encoder{}); err != nil { - return nil, err - } - - return cell, nil -} - -func marshalCellRef(v tlb.MarshalerTLB) (*boc.Cell, error) { - c, err := marshalCell(v) - if err != nil { - return nil, err - } - - c, err = c.NextRef() - if err != nil { - return nil, errors.Wrap(err, "unable to create ref cell") - } - - return c, nil -} - -func unmarshalTLB(t tlb.UnmarshalerTLB, cell *boc.Cell) error { - return t.UnmarshalTLB(cell, &tlb.Decoder{}) -} - -func unmarshalSnakeCell(cell *boc.Cell) ([]byte, error) { - var sd tlb.SnakeData - - if err := unmarshalTLB(&sd, cell); err != nil { - return nil, err - } - - cd := boc.BitString(sd) - - // TLB operates with bits, so we (might) need to trim some "leftovers" (null chars) - return bytes.Trim(cd.Buffer(), "\x00"), nil -} - -func marshalSnakeData(data []byte) (*boc.Cell, error) { - b := boc.NewCell() - - wrapped := tlb.Bytes(data) - if err := wrapped.MarshalTLB(b, &tlb.Encoder{}); err != nil { - return nil, err - } - - return b, nil -} - -func gramToUint(g tlb.Grams) math.Uint { - return math.NewUint(uint64(g)) -} - -func readEVMAddress(cell *boc.Cell) (eth.Address, error) { - const evmAddrBits = 20 * 8 - - s, err := cell.ReadBits(evmAddrBits) - if err != nil { - return eth.Address{}, err - } + var ( + inMsg = tx.Msgs.InMsg.Value.Value + body = boc.Cell(inMsg.Body.Value) + ) - return eth.BytesToAddress(s.Buffer()), nil + return &body, nil } diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go index f476d49623..962ebd7d53 100644 --- a/pkg/contracts/ton/gateway_op.go +++ b/pkg/contracts/ton/gateway_op.go @@ -86,7 +86,7 @@ func (d DepositAndCall) Memo() []byte { // AsBody casts struct to internal message body. func (d DepositAndCall) AsBody() (*boc.Cell, error) { - callDataCell, err := marshalSnakeData(d.CallData) + callDataCell, err := MarshalSnakeCell(d.CallData) if err != nil { return nil, err } diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index 6a300ae637..dc680761ff 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -250,10 +250,10 @@ func TestSnakeData(t *testing.T) { } { a := []byte(tt) - cell, err := marshalSnakeData(a) + cell, err := MarshalSnakeCell(a) require.NoError(t, err) - b, err := unmarshalSnakeCell(cell) + b, err := UnmarshalSnakeCell(cell) require.NoError(t, err) t.Logf(string(b)) diff --git a/pkg/contracts/ton/tlb.go b/pkg/contracts/ton/tlb.go new file mode 100644 index 0000000000..0ee3aea98a --- /dev/null +++ b/pkg/contracts/ton/tlb.go @@ -0,0 +1,79 @@ +package ton + +import ( + "bytes" + + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" +) + +// MarshalTLB encodes entity to BOC +func MarshalTLB(v tlb.MarshalerTLB) (*boc.Cell, error) { + cell := boc.NewCell() + + if err := v.MarshalTLB(cell, &tlb.Encoder{}); err != nil { + return nil, err + } + + return cell, nil +} + +// UnmarshalTLB decodes entity from BOC +func UnmarshalTLB(t tlb.UnmarshalerTLB, cell *boc.Cell) error { + return t.UnmarshalTLB(cell, &tlb.Decoder{}) +} + +// UnmarshalSnakeCell decodes TLB cell to []byte using snake-cell encoding +func UnmarshalSnakeCell(cell *boc.Cell) ([]byte, error) { + var sd tlb.SnakeData + + if err := UnmarshalTLB(&sd, cell); err != nil { + return nil, err + } + + cd := boc.BitString(sd) + + // TLB operates with bits, so we (might) need to trim some "leftovers" (null chars) + return bytes.Trim(cd.Buffer(), "\x00"), nil +} + +// MarshalSnakeCell encodes []byte to TLB using snake-cell encoding +func MarshalSnakeCell(data []byte) (*boc.Cell, error) { + b := boc.NewCell() + + wrapped := tlb.Bytes(data) + if err := wrapped.MarshalTLB(b, &tlb.Encoder{}); err != nil { + return nil, err + } + + return b, nil +} + +// UnmarshalEVMAddress decodes eth.Address from BOC +func UnmarshalEVMAddress(cell *boc.Cell) (eth.Address, error) { + const evmAddrBits = 20 * 8 + + s, err := cell.ReadBits(evmAddrBits) + if err != nil { + return eth.Address{}, err + } + + return eth.BytesToAddress(s.Buffer()), nil +} + +func GramsToUint(g tlb.Grams) math.Uint { + return math.NewUint(uint64(g)) +} + +func ErrCollect(errs ...error) error { + for i, err := range errs { + if err != nil { + return errors.Wrapf(err, "error at index %d", i) + } + } + + return nil +} From 0688c47feb0ba1a0e57de83d7f1773805a309de0 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:22:30 +0200 Subject: [PATCH 15/36] Implement samples for TON --- testutil/sample/sample_ton.go | 244 +++++++++++++++++++++++++++++ testutil/sample/sample_ton_test.go | 94 +++++++++++ 2 files changed, 338 insertions(+) create mode 100644 testutil/sample/sample_ton.go create mode 100644 testutil/sample/sample_ton_test.go diff --git a/testutil/sample/sample_ton.go b/testutil/sample/sample_ton.go new file mode 100644 index 0000000000..7d55b2a48a --- /dev/null +++ b/testutil/sample/sample_ton.go @@ -0,0 +1,244 @@ +package sample + +import ( + "crypto/rand" + "testing" + "time" + + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" +) + +const ( + tonWorkchainID = 0 + tonShardID = 123 + tonDepositFee = 10_000_000 // 0.01 TON + tonSampleGasUsage = 50_000_000 // 0.05 TON +) + +type TONTransactionProps struct { + Account ton.AccountID + GasUsed uint64 + BlockID ton.BlockIDExt + + // For simplicity let's have only one input + // and one output (both optional) + Input *tlb.Message + Output *tlb.Message +} + +type intMsgInfo struct { + IhrDisabled bool + Bounce bool + Bounced bool + Src tlb.MsgAddress + Dest tlb.MsgAddress + Value tlb.CurrencyCollection + IhrFee tlb.Grams + FwdFee tlb.Grams + CreatedLt uint64 + CreatedAt uint32 +} + +func TONDonateProps(t *testing.T, acc ton.AccountID, d toncontracts.Donation) TONTransactionProps { + body, err := d.AsBody() + require.NoError(t, err) + + deposited := tonSampleGasUsage + d.Amount.Uint64() + + return TONTransactionProps{ + Account: acc, + Input: &tlb.Message{ + Info: internalMessageInfo(&intMsgInfo{ + Bounce: true, + Src: d.Sender.ToMsgAddress(), + Dest: acc.ToMsgAddress(), + Value: tlb.CurrencyCollection{Grams: tlb.Grams(deposited)}, + }), + Body: tlb.EitherRef[tlb.Any]{Value: tlb.Any(*body)}, + }, + } +} + +func TONDepositProps(t *testing.T, acc ton.AccountID, d toncontracts.Deposit) TONTransactionProps { + body, err := d.AsBody() + require.NoError(t, err) + + logBody := depositLogMock(t, d.Sender, d.Amount.Uint64(), d.Recipient, nil) + + return TONTransactionProps{ + Account: acc, + Input: &tlb.Message{ + Info: internalMessageInfo(&intMsgInfo{ + Bounce: true, + Src: d.Sender.ToMsgAddress(), + Dest: acc.ToMsgAddress(), + Value: tlb.CurrencyCollection{Grams: fakeDepositAmount(d.Amount)}, + }), + Body: tlb.EitherRef[tlb.Any]{Value: tlb.Any(*body)}, + }, + Output: &tlb.Message{ + Body: tlb.EitherRef[tlb.Any]{IsRight: true, Value: tlb.Any(*logBody)}, + }, + } +} + +func TONDepositAndCallProps(t *testing.T, acc ton.AccountID, d toncontracts.DepositAndCall) TONTransactionProps { + body, err := d.AsBody() + require.NoError(t, err) + + logBody := depositLogMock(t, d.Sender, d.Amount.Uint64(), d.Recipient, d.CallData) + + return TONTransactionProps{ + Account: acc, + Input: &tlb.Message{ + Info: internalMessageInfo(&intMsgInfo{ + Bounce: true, + Src: d.Sender.ToMsgAddress(), + Dest: acc.ToMsgAddress(), + Value: tlb.CurrencyCollection{Grams: fakeDepositAmount(d.Amount)}, + }), + Body: tlb.EitherRef[tlb.Any]{Value: tlb.Any(*body)}, + }, + Output: &tlb.Message{ + Body: tlb.EitherRef[tlb.Any]{IsRight: true, Value: tlb.Any(*logBody)}, + }, + } +} + +// TONTransaction creates a sample TON transaction. +func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { + require.False(t, p.Account.IsZero(), "account address is empty") + require.False(t, p.Input == nil && p.Output == nil, "both input and output are empty") + + now := time.Now().UTC() + + if p.GasUsed == 0 { + p.GasUsed = tonSampleGasUsage + } + + if p.BlockID.BlockID.Seqno == 0 { + p.BlockID = tonBlockID(now) + } + + // Simulate logical time as `2 * now()` + lt := uint64(2 * now.Unix()) + + input := tlb.Maybe[tlb.Ref[tlb.Message]]{} + if p.Input != nil { + input.Exists = true + input.Value.Value = *p.Input + } + + var outputs tlb.HashmapE[tlb.Uint15, tlb.Ref[tlb.Message]] + if p.Output != nil { + outputs = tlb.NewHashmapE( + []tlb.Uint15{0}, + []tlb.Ref[tlb.Message]{{*p.Output}}, + ) + } + + type messages struct { + InMsg tlb.Maybe[tlb.Ref[tlb.Message]] + OutMsgs tlb.HashmapE[tlb.Uint15, tlb.Ref[tlb.Message]] + } + + return ton.Transaction{ + BlockID: p.BlockID, + Transaction: tlb.Transaction{ + AccountAddr: p.Account.Address, + Lt: lt, + Now: uint32(now.Unix()), + OutMsgCnt: tlb.Uint15(len(outputs.Keys())), + TotalFees: tlb.CurrencyCollection{Grams: tlb.Grams(p.GasUsed)}, + Msgs: messages{InMsg: input, OutMsgs: outputs}, + }, + } +} + +func GenerateTONAccountID() ton.AccountID { + var addr [32]byte + + //nolint:errcheck // test code + rand.Read(addr[:]) + + return *ton.NewAccountID(0, addr) +} + +func internalMessageInfo(info *intMsgInfo) tlb.CommonMsgInfo { + return tlb.CommonMsgInfo{ + SumType: "IntMsgInfo", + IntMsgInfo: (*struct { + IhrDisabled bool + Bounce bool + Bounced bool + Src tlb.MsgAddress + Dest tlb.MsgAddress + Value tlb.CurrencyCollection + IhrFee tlb.Grams + FwdFee tlb.Grams + CreatedLt uint64 + CreatedAt uint32 + })(info), + } +} + +func tonBlockID(now time.Time) ton.BlockIDExt { + // simulate shard seqno as unix timestamp + seqno := uint32(now.Unix()) + + return ton.BlockIDExt{ + BlockID: ton.BlockID{ + Workchain: tonWorkchainID, + Shard: tonShardID, + Seqno: seqno, + }, + } +} + +func fakeDepositAmount(v math.Uint) tlb.Grams { + return tlb.Grams(v.Uint64() + tonDepositFee) +} + +func depositLogMock( + t *testing.T, + sender ton.AccountID, + amount uint64, + recipient eth.Address, + callData []byte, +) *boc.Cell { + // cell log = begin_cell() + // .store_uint(op::internal::deposit_and_call, size::op_code_size) + // .store_uint(0, size::query_id_size) + // .store_slice(sender) + // .store_coins(deposit_amount) + // .store_uint(evm_recipient, size::evm_address) + // .store_ref(call_data) // only for DepositAndCall + // .end_cell(); + + b := boc.NewCell() + require.NoError(t, b.WriteUint(0, 32+64)) + + // skip + msgAddr := sender.ToMsgAddress() + require.NoError(t, tlb.Marshal(b, msgAddr)) + + coins := tlb.Grams(amount) + require.NoError(t, coins.MarshalTLB(b, nil)) + + require.NoError(t, b.WriteBytes(recipient.Bytes())) + + if callData != nil { + callDataCell, err := toncontracts.MarshalSnakeCell(callData) + require.NoError(t, err) + require.NoError(t, b.AddRef(callDataCell)) + } + + return b +} diff --git a/testutil/sample/sample_ton_test.go b/testutil/sample/sample_ton_test.go new file mode 100644 index 0000000000..73474db0d4 --- /dev/null +++ b/testutil/sample/sample_ton_test.go @@ -0,0 +1,94 @@ +package sample + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/ton" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" +) + +func TestTONSamples(t *testing.T) { + var ( + gatewayID = ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b") + gw = toncontracts.NewGateway(gatewayID) + ) + + t.Run("Donate", func(t *testing.T) { + // ARRANGE + d := toncontracts.Donation{ + Sender: GenerateTONAccountID(), + Amount: sdkmath.NewUint(100_000_000), + } + + tx := TONTransaction(t, TONDonateProps(t, gatewayID, d)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + d2, err := parsedTX.Donation() + require.NoError(t, err) + + require.Equal(t, int(d.Amount.Uint64()), int(d2.Amount.Uint64())) + require.Equal(t, d.Sender.ToRaw(), d2.Sender.ToRaw()) + }) + + t.Run("Deposit", func(t *testing.T) { + // ARRANGE + d := toncontracts.Deposit{ + Sender: GenerateTONAccountID(), + Amount: sdkmath.NewUint(200_000_000), + Recipient: EthAddress(), + } + + tx := TONTransaction(t, TONDepositProps(t, gatewayID, d)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + d2, err := parsedTX.Deposit() + require.NoError(t, err) + + require.Equal(t, int(d.Amount.Uint64()), int(d2.Amount.Uint64())) + require.Equal(t, d.Sender.ToRaw(), d2.Sender.ToRaw()) + require.Equal(t, d.Recipient.Hex(), d2.Recipient.Hex()) + require.Equal(t, d.Memo(), d2.Memo()) + }) + + t.Run("Deposit and call", func(t *testing.T) { + // ARRANGE + d := toncontracts.DepositAndCall{ + Deposit: toncontracts.Deposit{ + Sender: GenerateTONAccountID(), + Amount: sdkmath.NewUint(300_000_000), + Recipient: EthAddress(), + }, + CallData: []byte("Evidently, the most known and used kind of dictionaries in TON is hashmap."), + } + + tx := TONTransaction(t, TONDepositAndCallProps(t, gatewayID, d)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + + d2, err := parsedTX.DepositAndCall() + require.NoError(t, err) + + require.Equal(t, int(d.Amount.Uint64()), int(d2.Amount.Uint64())) + require.Equal(t, d.Sender.ToRaw(), d2.Sender.ToRaw()) + require.Equal(t, d.Recipient.Hex(), d2.Recipient.Hex()) + require.Equal(t, d.CallData, d2.CallData) + require.Equal(t, d.Memo(), d2.Memo()) + }) + +} From 1fd2cfefe6699ce8c00268a4fd4eac055a2c3600 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:23:08 +0200 Subject: [PATCH 16/36] Add liteClient mocks --- zetaclient/chains/ton/liteapi/client.go | 2 +- .../chains/ton/liteapi/client_live_test.go | 2 +- zetaclient/chains/ton/observer/inbound.go | 4 +- zetaclient/chains/ton/observer/observer.go | 26 +++- zetaclient/testutils/mocks/ton_liteclient.go | 127 ++++++++++++++++++ 5 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 zetaclient/testutils/mocks/ton_liteclient.go diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index bbe94147fd..03a659dd4f 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -13,7 +13,7 @@ import ( "github.com/tonkeeper/tongo/ton" ) -// Client extends tongo's liteapi.Client with some high-level tools +// Client extends liteapi.Client with some high-level tools // Reference: https://github.com/ton-blockchain/ton/blob/master/tl/generate/scheme/tonlib_api.tl type Client struct { *liteapi.Client diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index d090771ed9..40c37dce71 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -19,7 +19,7 @@ import ( func TestClient(t *testing.T) { if !common.LiveTestEnabled() { - //t.Skip("Live tests are disabled") + t.Skip("Live tests are disabled") } var ( diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 2d0950bd89..f2ab2af77c 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -43,7 +43,7 @@ func (ob *Observer) watchInbound(ctx context.Context) error { return nil } - if err := ob.observeInbound(ctx, app); err != nil { + if err := ob.observeInbound(ctx); err != nil { ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } @@ -62,7 +62,7 @@ func (ob *Observer) watchInbound(ctx context.Context) error { ) } -func (ob *Observer) observeInbound(ctx context.Context, _ *zctx.AppContext) error { +func (ob *Observer) observeInbound(ctx context.Context) error { if err := ob.ensureLastScannedTX(ctx); err != nil { return errors.Wrap(err, "unable to ensure last scanned tx") } diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index 5989293795..ee638ed61d 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -4,24 +4,44 @@ import ( "context" "errors" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/bg" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" ) type Observer struct { base.Observer - client *liteapi.Client + client LiteClient gateway *toncontracts.Gateway } +// LiteClient represents a TON client +// +//go:generate mockery --name LiteClient --filename ton_liteclient.go --case underscore --output ../../../testutils/mocks +type LiteClient interface { + GetBlockHeader(ctx context.Context, acc ton.BlockIDExt, mode int) (tlb.BlockInfo, error) + GetTransactionsUntil(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) + GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) +} + var _ interfaces.ChainObserver = (*Observer)(nil) -func New(bo *base.Observer, client *liteapi.Client, gateway *toncontracts.Gateway) (*Observer, error) { +func New(bo *base.Observer, client LiteClient, gateway *toncontracts.Gateway) (*Observer, error) { + switch { + case !bo.Chain().IsTONChain(): + return nil, errors.New("base observer chain is not TON") + case client == nil: + return nil, errors.New("liteapi client is nil") + case gateway == nil: + return nil, errors.New("gateway is nil") + } + bo.LoadLastTxScanned() return &Observer{ diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go new file mode 100644 index 0000000000..6074fc5949 --- /dev/null +++ b/zetaclient/testutils/mocks/ton_liteclient.go @@ -0,0 +1,127 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + tlb "github.com/tonkeeper/tongo/tlb" + + ton "github.com/tonkeeper/tongo/ton" +) + +// LiteClient is an autogenerated mock type for the LiteClient type +type LiteClient struct { + mock.Mock +} + +// GetBlockHeader provides a mock function with given fields: ctx, acc, mode +func (_m *LiteClient) GetBlockHeader(ctx context.Context, acc ton.BlockIDExt, mode int) (tlb.BlockInfo, error) { + ret := _m.Called(ctx, acc, mode) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeader") + } + + var r0 tlb.BlockInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, int) (tlb.BlockInfo, error)); ok { + return rf(ctx, acc, mode) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, int) tlb.BlockInfo); ok { + r0 = rf(ctx, acc, mode) + } else { + r0 = ret.Get(0).(tlb.BlockInfo) + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.BlockIDExt, int) error); ok { + r1 = rf(ctx, acc, mode) + } 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) + + if len(ret) == 0 { + panic("no return value specified for GetFirstTransaction") + } + + var r0 *ton.Transaction + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) (*ton.Transaction, int, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) *ton.Transaction); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ton.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID) int); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Get(1).(int) + } + + if rf, ok := ret.Get(2).(func(context.Context, ton.AccountID) error); ok { + r2 = rf(ctx, id) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetTransactionsUntil provides a mock function with given fields: ctx, acc, lt, bits +func (_m *LiteClient) GetTransactionsUntil(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) { + ret := _m.Called(ctx, acc, lt, bits) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionsUntil") + } + + var r0 []ton.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) ([]ton.Transaction, error)); ok { + return rf(ctx, acc, lt, bits) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) []ton.Transaction); ok { + r0 = rf(ctx, acc, lt, bits) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ton.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID, uint64, ton.Bits256) error); ok { + r1 = rf(ctx, acc, lt, bits) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewLiteClient creates a new instance of LiteClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLiteClient(t interface { + mock.TestingT + Cleanup(func()) +}) *LiteClient { + mock := &LiteClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 6619394c0e9a04df8f162526030324f17910ff52 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:23:28 +0200 Subject: [PATCH 17/36] Add unit tests for inbound TON observer --- testutil/sample/sample_ton.go | 30 +- .../chains/ton/observer/inbound_test.go | 317 ++++++++++++++++++ zetaclient/chains/ton/observer/observer.go | 14 +- .../chains/ton/observer/observer_test.go | 170 ++++++++++ 4 files changed, 524 insertions(+), 7 deletions(-) create mode 100644 zetaclient/chains/ton/observer/inbound_test.go create mode 100644 zetaclient/chains/ton/observer/observer_test.go diff --git a/testutil/sample/sample_ton.go b/testutil/sample/sample_ton.go index 7d55b2a48a..01ce8724c5 100644 --- a/testutil/sample/sample_ton.go +++ b/testutil/sample/sample_ton.go @@ -2,8 +2,10 @@ package sample import ( "crypto/rand" + "reflect" "testing" "time" + "unsafe" "cosmossdk.io/math" eth "github.com/ethereum/go-ethereum/common" @@ -46,6 +48,10 @@ type intMsgInfo struct { CreatedAt uint32 } +func TONDonation(t *testing.T, acc ton.AccountID, d toncontracts.Donation) ton.Transaction { + return TONTransaction(t, TONDonateProps(t, acc, d)) +} + func TONDonateProps(t *testing.T, acc ton.AccountID, d toncontracts.Donation) TONTransactionProps { body, err := d.AsBody() require.NoError(t, err) @@ -66,6 +72,10 @@ func TONDonateProps(t *testing.T, acc ton.AccountID, d toncontracts.Donation) TO } } +func TONDeposit(t *testing.T, acc ton.AccountID, d toncontracts.Deposit) ton.Transaction { + return TONTransaction(t, TONDepositProps(t, acc, d)) +} + func TONDepositProps(t *testing.T, acc ton.AccountID, d toncontracts.Deposit) TONTransactionProps { body, err := d.AsBody() require.NoError(t, err) @@ -89,6 +99,10 @@ func TONDepositProps(t *testing.T, acc ton.AccountID, d toncontracts.Deposit) TO } } +func TONDepositAndCall(t *testing.T, acc ton.AccountID, d toncontracts.DepositAndCall) ton.Transaction { + return TONTransaction(t, TONDepositAndCallProps(t, acc, d)) +} + func TONDepositAndCallProps(t *testing.T, acc ton.AccountID, d toncontracts.DepositAndCall) TONTransactionProps { body, err := d.AsBody() require.NoError(t, err) @@ -149,7 +163,7 @@ func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { OutMsgs tlb.HashmapE[tlb.Uint15, tlb.Ref[tlb.Message]] } - return ton.Transaction{ + tx := ton.Transaction{ BlockID: p.BlockID, Transaction: tlb.Transaction{ AccountAddr: p.Account.Address, @@ -160,6 +174,10 @@ func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { Msgs: messages{InMsg: input, OutMsgs: outputs}, }, } + + setTXHash(&tx.Transaction, Hash()) + + return tx } func GenerateTONAccountID() ton.AccountID { @@ -242,3 +260,13 @@ func depositLogMock( return b } + +// well, tlb.Transaction has unexported field `hash` that we need to set OUTSIDE tlb package. +// It's a hack, but it works for testing purposes. +func setTXHash(tx *tlb.Transaction, hash [32]byte) { + field := reflect.ValueOf(tx).Elem().FieldByName("hash") + ptr := unsafe.Pointer(field.UnsafeAddr()) + + arrPtr := (*[32]byte)(ptr) + *arrPtr = hash +} diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go new file mode 100644 index 0000000000..96f4b93d37 --- /dev/null +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -0,0 +1,317 @@ +package observer + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" +) + +func TestInbound(t *testing.T) { + gw := toncontracts.NewGateway( + ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b"), + ) + + t.Run("Ensure last scanned tx", func(t *testing.T) { + t.Run("Unable to get first tx", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + // Given mocked lite client call + ts.OnGetFirstTransaction(gw.AccountID(), nil, 0, errors.New("oops")).Once() + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.ErrorContains(t, err, "unable to ensure last scanned tx") + assert.Empty(t, ob.LastTxScanned()) + }) + + t.Run("All good", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given mocked lite client calls + firstTX := sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "1"), + }) + + ts.OnGetFirstTransaction(gw.AccountID(), &firstTX, 0, nil).Once() + ts.OnGetTransactionsUntil(gw.AccountID(), firstTX.Lt, txHash(firstTX), nil, nil).Once() + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // Check that last scanned tx is set and is valid + lastScanned, err := ob.ReadLastTxScannedFromDB() + assert.NoError(t, err) + assert.Equal(t, ob.LastTxScanned(), lastScanned) + + lt, hash, err := liteapi.TransactionHashFromString(lastScanned) + assert.NoError(t, err) + assert.Equal(t, firstTX.Lt, lt) + assert.Equal(t, firstTX.Hash().Hex(), hash.Hex()) + }) + }) + + t.Run("Donation", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + + // Given mocked lite client calls + donation := sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "12"), + }) + + txs := []ton.Transaction{donation} + + ts. + OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + Once() + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // nothing happened, but tx scanned + lt, hash, err := liteapi.TransactionHashFromString(ob.LastTxScanned()) + assert.NoError(t, err) + assert.Equal(t, donation.Lt, lt) + assert.Equal(t, donation.Hash().Hex(), hash.Hex()) + }) + + t.Run("Deposit", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + + // Given mocked lite client calls + deposit := toncontracts.Deposit{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "12"), + Recipient: sample.EthAddress(), + } + + depositTX := sample.TONDeposit(t, gw.AccountID(), deposit) + txs := []ton.Transaction{depositTX} + + ts. + OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + Once() + + ts.MockGetBlockHeader(depositTX.BlockID) + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // Check that cctx was sent to zetacore + require.Len(t, ts.votesBag, 1) + + // Check CCTX + cctx := ts.votesBag[0] + + assert.NotNil(t, cctx) + + assert.Equal(t, deposit.Sender.ToRaw(), cctx.Sender) + assert.Equal(t, ts.chain.ChainId, cctx.SenderChainId) + + assert.Equal(t, "", cctx.Asset) + assert.Equal(t, deposit.Amount.Uint64(), cctx.Amount.Uint64()) + assert.Equal(t, string(deposit.Recipient.Bytes()), cctx.Message) + + // Check hash & block height + expectedHash := liteapi.TransactionHashToString(depositTX.Lt, txHash(depositTX)) + assert.Equal(t, expectedHash, cctx.InboundHash) + + blockInfo, err := ts.liteClient.GetBlockHeader(ts.ctx, depositTX.BlockID, 0) + require.NoError(t, err) + + assert.Equal(t, uint64(blockInfo.MinRefMcSeqno), cctx.InboundBlockHeight) + }) + + t.Run("Deposit and call", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + + // Given mocked lite client calls + const callData = "hey there" + depositAndCall := toncontracts.DepositAndCall{ + Deposit: toncontracts.Deposit{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "4"), + Recipient: sample.EthAddress(), + }, + CallData: []byte(callData), + } + + depositAndCallTX := sample.TONDepositAndCall(t, gw.AccountID(), depositAndCall) + txs := []ton.Transaction{depositAndCallTX} + + ts. + OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + Once() + + ts.MockGetBlockHeader(depositAndCallTX.BlockID) + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // Check that cctx was sent to zetacore + require.Len(t, ts.votesBag, 1) + + // Check CCTX + cctx := ts.votesBag[0] + + assert.NotNil(t, cctx) + + assert.Equal(t, depositAndCall.Sender.ToRaw(), cctx.Sender) + assert.Equal(t, ts.chain.ChainId, cctx.SenderChainId) + + assert.Equal(t, "", cctx.Asset) + assert.Equal(t, depositAndCall.Amount.Uint64(), cctx.Amount.Uint64()) + assert.Equal(t, string(depositAndCall.Recipient.Bytes())+callData, cctx.Message) + + // Check hash & block height + expectedHash := liteapi.TransactionHashToString(depositAndCallTX.Lt, txHash(depositAndCallTX)) + assert.Equal(t, expectedHash, cctx.InboundHash) + + blockInfo, err := ts.liteClient.GetBlockHeader(ts.ctx, depositAndCallTX.BlockID, 0) + require.NoError(t, err) + + assert.Equal(t, uint64(blockInfo.MinRefMcSeqno), cctx.InboundBlockHeight) + }) + + t.Run("Multiple transactions", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + + // Given several transactions + txs := []ton.Transaction{ + // should be skipped + sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "1"), + }), + // should be voted + sample.TONDeposit(t, gw.AccountID(), toncontracts.Deposit{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "3"), + Recipient: sample.EthAddress(), + }), + // should be skipped (invalid inbound message) + sample.TONTransaction(t, sample.TONTransactionProps{ + Account: gw.AccountID(), + Input: &tlb.Message{}, + }), + // should be voted + sample.TONDeposit(t, gw.AccountID(), toncontracts.Deposit{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(t, "3"), + Recipient: sample.EthAddress(), + }), + // should be skipped (invalid inbound/outbound messages) + sample.TONTransaction(t, sample.TONTransactionProps{ + Account: gw.AccountID(), + Input: &tlb.Message{}, + Output: &tlb.Message{}, + }), + } + + ts. + OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + Once() + + for _, tx := range txs { + ts.MockGetBlockHeader(tx.BlockID) + } + + // ACT + // Observe inbounds once + err = ob.observeInbound(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // Check that cctx was sent to zetacore + assert.Equal(t, 2, len(ts.votesBag)) + + var ( + hash1 = liteapi.TransactionHashToString(txs[1].Lt, txHash(txs[1])) + hash2 = liteapi.TransactionHashToString(txs[3].Lt, txHash(txs[3])) + ) + + assert.Equal(t, hash1, ts.votesBag[0].InboundHash) + assert.Equal(t, hash2, ts.votesBag[1].InboundHash) + + // Check that last scanned tx points to the last tx in a list (even if it was skipped) + var ( + lastTX = txs[len(txs)-1] + lastScannedHash = ob.LastTxScanned() + ) + + lastLT, lastHash, err := liteapi.TransactionHashFromString(lastScannedHash) + assert.NoError(t, err) + assert.Equal(t, lastTX.Lt, lastLT) + assert.Equal(t, lastTX.Hash().Hex(), lastHash.Hex()) + }) +} + +func txHash(tx ton.Transaction) ton.Bits256 { + return ton.Bits256(tx.Hash()) +} diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index ee638ed61d..f6792e5b1b 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -15,7 +15,7 @@ import ( ) type Observer struct { - base.Observer + *base.Observer client LiteClient gateway *toncontracts.Gateway @@ -45,7 +45,7 @@ func New(bo *base.Observer, client LiteClient, gateway *toncontracts.Gateway) (* bo.LoadLastTxScanned() return &Observer{ - Observer: *bo, + Observer: bo, client: client, gateway: gateway, }, nil @@ -66,10 +66,12 @@ func (ob *Observer) Start(ctx context.Context) { bg.Work(ctx, ob.watchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) // todo - // watchInboundTracker - // watchOutbound - // watchGasPrice - // watchRPCStatus + // watchInboundTracker https://github.com/zeta-chain/node/issues/2935 + + // todo outbounds/withdrawals https://github.com/zeta-chain/node/issues/2807 + // watchOutbound + // watchGasPrice + // watchRPCStatus } func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossChainTx) (bool, error) { diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go new file mode 100644 index 0000000000..84e779f980 --- /dev/null +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -0,0 +1,170 @@ +package observer + +import ( + "context" + "strconv" + "testing" + + "cosmossdk.io/math" + "github.com/rs/zerolog" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/chains" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + cctxtypes "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" + "github.com/zeta-chain/node/zetaclient/db" + "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" +) + +type testSuite struct { + ctx context.Context + t *testing.T + + chain chains.Chain + chainParams *observertypes.ChainParams + + liteClient *mocks.LiteClient + + zetacore *mocks.ZetacoreClient + tss *mocks.TSS + database *db.DB + + baseObserver *base.Observer + + votesBag []*cctxtypes.MsgVoteInbound +} + +func newTestSuite(t *testing.T) *testSuite { + var ( + ctx = context.Background() + + chain = chains.TONTestnet + chainParams = sample.ChainParams(chain.ChainId) + + liteClient = mocks.NewLiteClient(t) + + tss = mocks.NewTSSAthens3() + zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) + + testLogger = zerolog.New(zerolog.NewTestWriter(t)) + logger = base.Logger{Std: testLogger, Compliance: testLogger} + ) + + database, err := db.NewFromSqliteInMemory(true) + require.NoError(t, err) + + baseObserver, err := base.NewObserver( + chain, + *chainParams, + zetacore, + tss, + 1, + 1, + 60, + nil, + database, + logger, + ) + + require.NoError(t, err) + + ts := &testSuite{ + ctx: ctx, + t: t, + + chain: chain, + chainParams: chainParams, + + liteClient: liteClient, + + zetacore: zetacore, + tss: tss, + database: database, + + baseObserver: baseObserver, + } + + // Setup mocks + ts.zetacore.On("Chain").Return(chain) + setupVotesBag(ts) + + return ts +} + +func (ts *testSuite) SetupLastScannedTX(gw ton.AccountID) ton.Transaction { + lastScannedTX := sample.TONDonation(ts.t, gw, toncontracts.Donation{ + Sender: sample.GenerateTONAccountID(), + Amount: tonCoins(ts.t, "1"), + }) + + txHash := liteapi.TransactionHashToString(lastScannedTX.Lt, ton.Bits256(lastScannedTX.Hash())) + + ts.baseObserver.WithLastTxScanned(txHash) + require.NoError(ts.t, ts.baseObserver.WriteLastTxScannedToDB(txHash)) + + return lastScannedTX +} + +func (ts *testSuite) OnGetFirstTransaction(acc ton.AccountID, tx *ton.Transaction, scanned int, err error) *mock.Call { + return ts.liteClient. + On("GetFirstTransaction", ts.ctx, acc). + Return(tx, scanned, err) +} + +func (ts *testSuite) OnGetTransactionsUntil( + acc ton.AccountID, + lt uint64, + hash ton.Bits256, + txs []ton.Transaction, + err error, +) *mock.Call { + return ts.liteClient. + On("GetTransactionsUntil", ts.ctx, acc, lt, hash). + Return(txs, err) +} + +func (ts *testSuite) MockGetBlockHeader(id ton.BlockIDExt) *mock.Call { + // let's pretend that block's masterchain ref has the same seqno + blockInfo := tlb.BlockInfo{ + BlockInfoPart: tlb.BlockInfoPart{MinRefMcSeqno: id.Seqno}, + } + + return ts.liteClient.On("GetBlockHeader", ts.ctx, id, 0).Return(blockInfo, nil) +} + +// parses string to TON +func tonCoins(t *testing.T, raw string) math.Uint { + t.Helper() + + const oneTON = 1_000_000_000 + + f, err := strconv.ParseFloat(raw, 64) + require.NoError(t, err) + + f *= oneTON + + return math.NewUint(uint64(f)) +} + +func setupVotesBag(ts *testSuite) { + catcher := func(args mock.Arguments) { + vote := args.Get(3) + cctx, ok := vote.(*cctxtypes.MsgVoteInbound) + if !ok { + panic("unexpected cctx type") + } + + ts.votesBag = append(ts.votesBag, cctx) + } + ts.zetacore. + On("PostVoteInbound", ts.ctx, mock.Anything, mock.Anything, mock.Anything). + Run(catcher). + Return("", "", nil) // zeta hash, ballot index, error +} From ccc1a9dafe7bec9643c0decc4051c4a9e3b9ae03 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:43:19 +0200 Subject: [PATCH 18/36] Localnet: add TON ZRC20 --- e2e/runner/runner.go | 2 + e2e/runner/setup_zeta.go | 18 +++++++++ e2e/txserver/zeta_tx_server.go | 71 +++++++++++++++++----------------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index e83245d01a..32fcb64b8e 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -127,6 +127,8 @@ type E2ERunner struct { BTCZRC20 *zrc20.ZRC20 SOLZRC20Addr ethcommon.Address SOLZRC20 *zrc20.ZRC20 + TONZRC20Addr ethcommon.Address + TONZRC20 *zrc20.ZRC20 UniswapV2FactoryAddr ethcommon.Address UniswapV2Factory *uniswapv2factory.UniswapV2Factory UniswapV2RouterAddr ethcommon.Address diff --git a/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go index 0001ccb065..adb3c10ee0 100644 --- a/e2e/runner/setup_zeta.go +++ b/e2e/runner/setup_zeta.go @@ -191,6 +191,7 @@ func (r *E2ERunner) SetZEVMZRC20s() { r.SetupETHZRC20() r.SetupBTCZRC20() r.SetupSOLZRC20() + r.SetupTONZRC20() } // SetupETHZRC20 sets up the ETH ZRC20 in the runner from the values queried from the chain @@ -242,6 +243,23 @@ func (r *E2ERunner) SetupSOLZRC20() { r.SOLZRC20 = SOLZRC20 } +// SetupTONZRC20 sets up the TON ZRC20 in the runner from the values queried from the chain +func (r *E2ERunner) SetupTONZRC20() { + TONZRC20Addr, err := r.SystemContract.GasCoinZRC20ByChainId( + &bind.CallOpts{}, + big.NewInt(chains.TONLocalnet.ChainId), + ) + require.NoError(r, err) + + r.TONZRC20Addr = TONZRC20Addr + r.Logger.Info("TON ZRC20 address: %s", TONZRC20Addr.Hex()) + + TONZRC20, err := zrc20.NewZRC20(TONZRC20Addr, r.ZEVMClient) + require.NoError(r, err) + + r.TONZRC20 = TONZRC20 +} + // EnableHeaderVerification enables the header verification for the given chain IDs func (r *E2ERunner) EnableHeaderVerification(chainIDList []int64) error { r.Logger.Print("⚙️ enabling verification flags for block headers") diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index b0e6cecce2..0137cb483b 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -406,9 +406,7 @@ func (zts ZetaTxServer) DeploySystemContracts( // DeployZRC20s deploys the ZRC20 contracts // returns the addresses of erc20 zrc20 -func (zts ZetaTxServer) DeployZRC20s( - accountOperational, accountAdmin, erc20Addr string, -) (string, error) { +func (zts ZetaTxServer) DeployZRC20s(accountOperational, accountAdmin, erc20Addr string) (string, error) { // retrieve account accOperational, err := zts.clientCtx.Keyring.Key(accountOperational) if err != nil { @@ -441,8 +439,26 @@ func (zts ZetaTxServer) DeployZRC20s( deployerAddr = addrOperational.String() } + deploy := func(msg *fungibletypes.MsgDeployFungibleCoinZRC20) (string, error) { + res, err := zts.BroadcastTx(deployerAccount, msg) + if err != nil { + return "", fmt.Errorf("failed to deploy eth zrc20: %w", err) + } + + addr, err := fetchZRC20FromDeployResponse(res) + if err != nil { + return "", fmt.Errorf("unable to fetch zrc20 from deploy response: %w", err) + } + + if err := zts.initializeLiquidityCap(addr); err != nil { + return "", fmt.Errorf("unable to initialize liquidity cap: %w", err) + } + + return addr, nil + } + // deploy eth zrc20 - res, err := zts.BroadcastTx(deployerAccount, fungibletypes.NewMsgDeployFungibleCoinZRC20( + _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, "", chains.GoerliLocalnet.ChainId, @@ -455,16 +471,9 @@ func (zts ZetaTxServer) DeployZRC20s( if err != nil { return "", fmt.Errorf("failed to deploy eth zrc20: %s", err.Error()) } - zrc20, err := fetchZRC20FromDeployResponse(res) - if err != nil { - return "", err - } - if err := zts.initializeLiquidityCap(zrc20); err != nil { - return "", err - } // deploy btc zrc20 - res, err = zts.BroadcastTx(deployerAccount, fungibletypes.NewMsgDeployFungibleCoinZRC20( + _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, "", chains.BitcoinRegtest.ChainId, @@ -477,16 +486,9 @@ func (zts ZetaTxServer) DeployZRC20s( if err != nil { return "", fmt.Errorf("failed to deploy btc zrc20: %s", err.Error()) } - zrc20, err = fetchZRC20FromDeployResponse(res) - if err != nil { - return "", err - } - if err := zts.initializeLiquidityCap(zrc20); err != nil { - return "", err - } // deploy sol zrc20 - res, err = zts.BroadcastTx(deployerAccount, fungibletypes.NewMsgDeployFungibleCoinZRC20( + _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, "", chains.SolanaLocalnet.ChainId, @@ -499,16 +501,24 @@ func (zts ZetaTxServer) DeployZRC20s( if err != nil { return "", fmt.Errorf("failed to deploy sol zrc20: %s", err.Error()) } - zrc20, err = fetchZRC20FromDeployResponse(res) + + // deploy ton zrc20 + _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + "", + chains.TONLocalnet.ChainId, + 9, + "TON", + "TON", + coin.CoinType_Gas, + 100_000, + )) if err != nil { - return "", err - } - if err := zts.initializeLiquidityCap(zrc20); err != nil { - return "", err + return "", fmt.Errorf("failed to deploy ton zrc20: %s", err.Error()) } // deploy erc20 zrc20 - res, err = zts.BroadcastTx(deployerAccount, fungibletypes.NewMsgDeployFungibleCoinZRC20( + erc20zrc20Addr, err := deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, erc20Addr, chains.GoerliLocalnet.ChainId, @@ -522,15 +532,6 @@ func (zts ZetaTxServer) DeployZRC20s( return "", fmt.Errorf("failed to deploy erc20 zrc20: %s", err.Error()) } - // fetch the erc20 zrc20 contract address and remove the quotes - erc20zrc20Addr, err := fetchZRC20FromDeployResponse(res) - if err != nil { - return "", err - } - if err := zts.initializeLiquidityCap(erc20zrc20Addr); err != nil { - return "", err - } - return erc20zrc20Addr, nil } From 9669adc4b4db5e1f78259c5c79ad0b6a701c77ab Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:43:21 +0200 Subject: [PATCH 19/36] Wire the TON observer into the orchestrator --- zetaclient/chains/ton/config.go | 13 ++++ zetaclient/chains/ton/liteapi/client.go | 20 +++++++ zetaclient/chains/ton/observer/observer.go | 5 +- .../chains/ton/observer/observer_test.go | 10 +++- zetaclient/config/config_chain.go | 10 +++- zetaclient/config/types.go | 16 +++++ zetaclient/context/chain.go | 4 ++ zetaclient/orchestrator/bootstap_test.go | 33 ++++++++--- zetaclient/orchestrator/bootstrap.go | 59 +++++++++++++++++++ zetaclient/testutils/mocks/ton_liteclient.go | 18 +++--- zetaclient/testutils/testrpc/rpc.go | 2 +- 11 files changed, 168 insertions(+), 22 deletions(-) diff --git a/zetaclient/chains/ton/config.go b/zetaclient/chains/ton/config.go index ee7eaac701..a8578936a3 100644 --- a/zetaclient/chains/ton/config.go +++ b/zetaclient/chains/ton/config.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "time" "github.com/tonkeeper/tongo/config" @@ -38,3 +39,15 @@ func ConfigFromURL(ctx context.Context, url string) (*GlobalConfigurationFile, e return config.ParseConfig(res.Body) } + +func ConfigFromPath(path string) (*GlobalConfigurationFile, error) { + return config.ParseConfigFile(path) +} + +func ConfigFromAny(ctx context.Context, urlOrPath string) (*GlobalConfigurationFile, error) { + if u, err := url.Parse(urlOrPath); err == nil { + return ConfigFromURL(ctx, u.String()) + } + + return ConfigFromPath(urlOrPath) +} diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index 03a659dd4f..ca79f462d9 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -11,6 +11,8 @@ import ( "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" + + zetaton "github.com/zeta-chain/node/zetaclient/chains/ton" ) // Client extends liteapi.Client with some high-level tools @@ -32,6 +34,24 @@ func New(client *liteapi.Client) *Client { return &Client{Client: client, blockCache: blockCache} } +// NewFromAny creates a new client from a URL or a file path. +func NewFromAny(ctx context.Context, urlOrPath string) (*Client, error) { + cfg, err := zetaton.ConfigFromAny(ctx, urlOrPath) + if err != nil { + return nil, errors.Wrap(err, "unable to get config") + } + + client, err := liteapi.NewClient( + liteapi.WithConfigurationFile(*cfg), + liteapi.WithDetectArchiveNodes(), + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create client") + } + + return New(client), nil +} + // GetBlockHeader returns block header by block ID. // Uses LRU cache for network efficiency. // I haven't found what mode means but `0` works fine. diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index f6792e5b1b..acdda42b46 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -14,6 +14,7 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) +// Observer is a TON observer. type Observer struct { *base.Observer @@ -25,13 +26,14 @@ type Observer struct { // //go:generate mockery --name LiteClient --filename ton_liteclient.go --case underscore --output ../../../testutils/mocks type LiteClient interface { - GetBlockHeader(ctx context.Context, acc ton.BlockIDExt, mode int) (tlb.BlockInfo, error) + GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) GetTransactionsUntil(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) } var _ interfaces.ChainObserver = (*Observer)(nil) +// New constructor for TON Observer. func New(bo *base.Observer, client LiteClient, gateway *toncontracts.Gateway) (*Observer, error) { switch { case !bo.Chain().IsTONChain(): @@ -51,6 +53,7 @@ func New(bo *base.Observer, client LiteClient, gateway *toncontracts.Gateway) (* }, nil } +// Start starts the observer. This method is NOT blocking. func (ob *Observer) Start(ctx context.Context) { if ok := ob.Observer.Start(); !ok { ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 84e779f980..3237833920 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -92,7 +92,8 @@ func newTestSuite(t *testing.T) *testSuite { } // Setup mocks - ts.zetacore.On("Chain").Return(chain) + ts.zetacore.On("Chain").Return(chain).Maybe() + setupVotesBag(ts) return ts @@ -126,7 +127,7 @@ func (ts *testSuite) OnGetTransactionsUntil( err error, ) *mock.Call { return ts.liteClient. - On("GetTransactionsUntil", ts.ctx, acc, lt, hash). + On("GetTransactionsUntil", mock.Anything, acc, lt, hash). Return(txs, err) } @@ -136,7 +137,9 @@ func (ts *testSuite) MockGetBlockHeader(id ton.BlockIDExt) *mock.Call { BlockInfoPart: tlb.BlockInfoPart{MinRefMcSeqno: id.Seqno}, } - return ts.liteClient.On("GetBlockHeader", ts.ctx, id, 0).Return(blockInfo, nil) + return ts.liteClient. + On("GetBlockHeader", mock.Anything, id, uint32(0)). + Return(blockInfo, nil) } // parses string to TON @@ -165,6 +168,7 @@ func setupVotesBag(ts *testSuite) { } ts.zetacore. On("PostVoteInbound", ts.ctx, mock.Anything, mock.Anything, mock.Anything). + Maybe(). Run(catcher). Return("", "", nil) // zeta hash, ballot index, error } diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 54d01baaf7..9bdfcc6bbb 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -21,8 +21,9 @@ func New(setDefaults bool) Config { if setDefaults { cfg.BitcoinConfig = bitcoinConfigRegnet() - cfg.SolanaConfig = solanaConfigLocalnet() cfg.EVMChainConfigs = evmChainsConfigs() + cfg.SolanaConfig = solanaConfigLocalnet() + cfg.TONConfig = tonConfigLocalnet() } return cfg @@ -47,6 +48,13 @@ func solanaConfigLocalnet() SolanaConfig { } } +func tonConfigLocalnet() TONConfig { + return TONConfig{ + LiteClientConfigURL: "http://ton:8000/lite-client.json", + RPCAlertLatency: 60, + } +} + // evmChainsConfigs contains EVM chain configs // it contains list of EVM chains with empty endpoint except for localnet func evmChainsConfigs() map[int64]EVMConfig { diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 9ef1a5d5a5..055714bfea 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -58,6 +58,13 @@ type SolanaConfig struct { RPCAlertLatency int64 } +// TONConfig is the config for TON chain +type TONConfig struct { + // Can be either URL of local file path + LiteClientConfigURL string `json:"liteClientConfigURL"` + RPCAlertLatency int64 `json:"rpcAlertLatency"` +} + // ComplianceConfig is the config for compliance type ComplianceConfig struct { LogPath string `json:"LogPath"` @@ -93,6 +100,7 @@ type Config struct { EVMChainConfigs map[int64]EVMConfig `json:"EVMChainConfigs"` BitcoinConfig BTCConfig `json:"BitcoinConfig"` SolanaConfig SolanaConfig `json:"SolanaConfig"` + TONConfig TONConfig `json:"TONConfig"` // compliance config ComplianceConfig ComplianceConfig `json:"ComplianceConfig"` @@ -137,6 +145,14 @@ func (c Config) GetSolanaConfig() (SolanaConfig, bool) { return c.SolanaConfig, c.SolanaConfig != (SolanaConfig{}) } +// GetTONConfig returns the TONConfig and a bool indicating if it's present. +func (c Config) GetTONConfig() (TONConfig, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.TONConfig, c.TONConfig != TONConfig{} +} + // String returns the string representation of the config func (c Config) String() string { s, err := json.MarshalIndent(c, "", "\t") diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 946dbae598..b23fce1a54 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -165,6 +165,10 @@ func (c Chain) IsSolana() bool { return chains.IsSolanaChain(c.ID(), c.registry.additionalChains) } +func (c Chain) IsTON() bool { + return chains.IsTONChain(c.ID(), c.registry.additionalChains) +} + // RelayerKeyPassword returns the relayer key password for the chain func (c Chain) RelayerKeyPassword() string { network := c.RawChain().Network diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 6153961183..6c263ff4f7 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -21,7 +21,11 @@ import ( "github.com/zeta-chain/node/zetaclient/testutils/testrpc" ) -const solanaGatewayAddress = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" +const ( + solanaGatewayAddress = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + tonGatewayAddress = "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b" + tonMainnet = "https://ton.org/global-config.json" +) func TestCreateSignerMap(t *testing.T) { var ( @@ -211,9 +215,12 @@ func TestCreateChainObserverMap(t *testing.T) { evmServer := testrpc.NewEVMServer(t) evmServer.SetBlockNumber(100) - // Given generic SOL RPC + // Given SOL config _, solConfig := testrpc.NewSolanaServer(t) + // Given TON config + tonConfig := config.TONConfig{LiteClientConfigURL: tonMainnet, RPCAlertLatency: 1} + // Given a zetaclient config with ETH, MATIC, and BTC chains cfg := config.New(false) @@ -229,6 +236,7 @@ func TestCreateChainObserverMap(t *testing.T) { cfg.BitcoinConfig = btcConfig cfg.SolanaConfig = solConfig + cfg.TONConfig = tonConfig // Given AppContext app := zctx.New(cfg, nil, log) @@ -239,6 +247,7 @@ func TestCreateChainObserverMap(t *testing.T) { mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, chains.BitcoinMainnet, + chains.TONMainnet, }) // ACT @@ -249,11 +258,12 @@ func TestCreateChainObserverMap(t *testing.T) { assert.NotEmpty(t, observers) // Okay, now we want to check that signers for EVM and BTC were created - assert.Equal(t, 2, len(observers)) + assert.Equal(t, 3, len(observers)) hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.BitcoinMainnet.ChainId) + hasObserver(t, observers, chains.TONMainnet.ChainId) - t.Run("Add polygon in the runtime", func(t *testing.T) { + t.Run("Add polygon and remove TON in the runtime", func(t *testing.T) { // ARRANGE mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, chains.BitcoinMainnet, chains.Polygon, @@ -265,7 +275,7 @@ func TestCreateChainObserverMap(t *testing.T) { // ASSERT assert.NoError(t, err) assert.Equal(t, 1, added) - assert.Equal(t, 0, removed) + assert.Equal(t, 1, removed) hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) @@ -400,6 +410,11 @@ func chainParams(supportedChains []chains.Chain) ([]chains.Chain, map[int64]*obs continue } + if chains.IsEVMChain(chainID, nil) { + params[chainID] = ptr.Ptr(mocks.MockChainParams(chainID, 100)) + continue + } + if chains.IsSolanaChain(chainID, nil) { p := mocks.MockChainParams(chainID, 100) p.GatewayAddress = solanaGatewayAddress @@ -407,10 +422,14 @@ func chainParams(supportedChains []chains.Chain) ([]chains.Chain, map[int64]*obs continue } - if chains.IsEVMChain(chainID, nil) { - params[chainID] = ptr.Ptr(mocks.MockChainParams(chainID, 100)) + if chains.IsTONChain(chainID, nil) { + p := mocks.MockChainParams(chainID, 100) + p.GatewayAddress = tonGatewayAddress + params[chainID] = &p continue } + + panic("unknown chain: " + chain.String()) } return supportedChains, params diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index f7be6ad504..8742368825 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -9,7 +9,9 @@ import ( solrpc "github.com/gagliardetto/solana-go/rpc" ethrpc2 "github.com/onrik/ethrpc" "github.com/pkg/errors" + "github.com/tonkeeper/tongo/ton" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/zetaclient/chains/base" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" @@ -19,6 +21,8 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/interfaces" solbserver "github.com/zeta-chain/node/zetaclient/chains/solana/observer" solanasigner "github.com/zeta-chain/node/zetaclient/chains/solana/signer" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" + tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/db" "github.com/zeta-chain/node/zetaclient/keys" @@ -181,6 +185,9 @@ func syncSignerMap( } addSigner(chainID, signer) + case chain.IsTON(): + logger.Std.Error().Err(err).Msgf("TON signer is not implemented yet for chain id %d", chainID) + continue default: logger.Std.Warn(). Int64("signer.chain_id", chain.ID()). @@ -394,6 +401,58 @@ func syncObserverMap( } addObserver(chainID, solObserver) + case chain.IsTON(): + cfg, found := app.Config().GetTONConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find chain params for TON chain %d", chainID) + continue + } + + database, err := db.NewFromSqlite(dbpath, chainName, true) + if err != nil { + logger.Std.Error().Err(err).Msgf("unable to open database for TON chain %d", chainID) + continue + } + + baseObserver, err := base.NewObserver( + *rawChain, + *params, + client, + tss, + base.DefaultBlockCacheSize, + base.DefaultHeaderCacheSize, + cfg.RPCAlertLatency, + ts, + database, + logger, + ) + + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to create base observer for TON chain %d", chainID) + continue + } + + tonClient, err := liteapi.NewFromAny(ctx, cfg.LiteClientConfigURL) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to create TON liteapi for chain %d", chainID) + continue + } + + gatewayID, err := ton.ParseAccountID(params.GatewayAddress) + if err != nil { + logger.Std.Error().Err(err). + Msgf("Unable to parse gateway address %q for chain %d", params.GatewayAddress, chainID) + continue + } + + gw := toncontracts.NewGateway(gatewayID) + tonObserver, err := tonobserver.New(baseObserver, tonClient, gw) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to create TON observer for chain %d", chainID) + continue + } + + addObserver(chainID, tonObserver) default: logger.Std.Warn(). Int64("observer.chain_id", chain.ID()). diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go index 6074fc5949..573d2d600e 100644 --- a/zetaclient/testutils/mocks/ton_liteclient.go +++ b/zetaclient/testutils/mocks/ton_liteclient.go @@ -17,9 +17,9 @@ type LiteClient struct { mock.Mock } -// GetBlockHeader provides a mock function with given fields: ctx, acc, mode -func (_m *LiteClient) GetBlockHeader(ctx context.Context, acc ton.BlockIDExt, mode int) (tlb.BlockInfo, error) { - ret := _m.Called(ctx, acc, mode) +// GetBlockHeader provides a mock function with given fields: ctx, blockID, mode +func (_m *LiteClient) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) { + ret := _m.Called(ctx, blockID, mode) if len(ret) == 0 { panic("no return value specified for GetBlockHeader") @@ -27,17 +27,17 @@ func (_m *LiteClient) GetBlockHeader(ctx context.Context, acc ton.BlockIDExt, mo var r0 tlb.BlockInfo var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, int) (tlb.BlockInfo, error)); ok { - return rf(ctx, acc, mode) + if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, uint32) (tlb.BlockInfo, error)); ok { + return rf(ctx, blockID, mode) } - if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, int) tlb.BlockInfo); ok { - r0 = rf(ctx, acc, mode) + if rf, ok := ret.Get(0).(func(context.Context, ton.BlockIDExt, uint32) tlb.BlockInfo); ok { + r0 = rf(ctx, blockID, mode) } else { r0 = ret.Get(0).(tlb.BlockInfo) } - if rf, ok := ret.Get(1).(func(context.Context, ton.BlockIDExt, int) error); ok { - r1 = rf(ctx, acc, mode) + if rf, ok := ret.Get(1).(func(context.Context, ton.BlockIDExt, uint32) error); ok { + r1 = rf(ctx, blockID, mode) } else { r1 = ret.Error(1) } diff --git a/zetaclient/testutils/testrpc/rpc.go b/zetaclient/testutils/testrpc/rpc.go index f444631813..12f368fb3f 100644 --- a/zetaclient/testutils/testrpc/rpc.go +++ b/zetaclient/testutils/testrpc/rpc.go @@ -59,7 +59,7 @@ func (s *Server) httpHandler(w http.ResponseWriter, r *http.Request) { // Decode request raw, err := io.ReadAll(r.Body) require.NoError(s.t, err) - require.NoError(s.t, json.Unmarshal(raw, &req), "unable to unmarshal request") + require.NoError(s.t, json.Unmarshal(raw, &req), "unable to unmarshal request for %s", s.name) // Process request res := s.rpcHandler(req) From 2c1f47c6e9ba4f27ec3c56302c38e8e9d83f47af Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:04:47 +0200 Subject: [PATCH 20/36] Add TON chain params of the fly! --- cmd/zetae2e/local/ton.go | 1 + e2e/runner/setup_ton.go | 65 ++++++++++++++++++++++++- zetaclient/chains/base/observer.go | 8 ++- zetaclient/logs/fields.go | 13 ++--- zetaclient/orchestrator/bootstrap.go | 11 +++-- zetaclient/orchestrator/orchestrator.go | 29 ++++------- 6 files changed, 95 insertions(+), 32 deletions(-) diff --git a/cmd/zetae2e/local/ton.go b/cmd/zetae2e/local/ton.go index 000c872b25..bbbbd991de 100644 --- a/cmd/zetae2e/local/ton.go +++ b/cmd/zetae2e/local/ton.go @@ -25,6 +25,7 @@ func tonTestRoutine( deployerRunner, conf.DefaultAccount, runner.NewLogger(verbose, color.FgCyan, "ton"), + runner.WithZetaTxServer(deployerRunner.ZetaTxServer), ) if err != nil { return errors.Wrap(err, "unable to init ton test runner") diff --git a/e2e/runner/setup_ton.go b/e2e/runner/setup_ton.go index 9b744401ad..19e91d2608 100644 --- a/e2e/runner/setup_ton.go +++ b/e2e/runner/setup_ton.go @@ -2,10 +2,15 @@ package runner import ( "fmt" + "time" "github.com/pkg/errors" "github.com/zeta-chain/node/e2e/runner/ton" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/constant" + observertypes "github.com/zeta-chain/node/x/observer/types" ) // SetupTON setups TON deployer and deploys Gateway contract @@ -42,7 +47,12 @@ func (r *E2ERunner) SetupTON() error { return errors.Wrapf(err, "unable to deploy TON gateway") } - r.Logger.Print("💎TON Gateway deployed %s (%s)", gwAccount.ID.ToRaw(), gwAccount.ID.ToHuman(false, true)) + r.Logger.Print( + "💎TON Gateway deployed %s (%s) with TSS address %s", + gwAccount.ID.ToRaw(), + gwAccount.ID.ToHuman(false, true), + r.TSSAddress.Hex(), + ) // 3. Check that the gateway indeed was deployed and has desired TON balance. gwBalance, err := deployer.GetBalanceOf(ctx, gwAccount.ID) @@ -57,5 +67,56 @@ func (r *E2ERunner) SetupTON() error { r.TONDeployer = deployer r.TONGateway = gwAccount.ID - return nil + return r.ensureTONChainParams(gwAccount) +} + +func (r *E2ERunner) ensureTONChainParams(gw *ton.AccountInit) error { + if r.ZetaTxServer == nil { + return errors.New("ZetaTxServer is not initialized") + } + + creator := r.ZetaTxServer.MustGetAccountAddressFromName(utils.OperationalPolicyName) + + chainID := chains.TONLocalnet.ChainId + + chainParams := &observertypes.ChainParams{ + ChainId: chainID, + ConfirmationCount: 1, + GasPriceTicker: 5, + InboundTicker: 5, + OutboundTicker: 5, + ZetaTokenContractAddress: constant.EVMZeroAddress, + ConnectorContractAddress: constant.EVMZeroAddress, + Erc20CustodyContractAddress: constant.EVMZeroAddress, + OutboundScheduleInterval: 2, + OutboundScheduleLookahead: 5, + BallotThreshold: observertypes.DefaultBallotThreshold, + MinObserverDelegation: observertypes.DefaultMinObserverDelegation, + IsSupported: true, + GatewayAddress: gw.ID.ToRaw(), + } + + msg := observertypes.NewMsgUpdateChainParams(creator, chainParams) + + if _, err := r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, msg); err != nil { + return errors.Wrap(err, "unable to broadcast TON chain params tx") + } + + r.Logger.Print("💎Voted for adding TON chain params (localnet). Waiting for confirmation") + + query := &observertypes.QueryGetChainParamsForChainRequest{ChainId: chainID} + + const duration = 2 * time.Second + + for i := 0; i < 10; i++ { + _, err := r.ObserverClient.GetChainParamsForChain(r.Ctx, query) + if err == nil { + r.Logger.Print("💎TON chain params are set") + return nil + } + + time.Sleep(duration) + } + + return errors.New("unable to set TON chain params") } diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index e9e8e05ba2..db5232fa30 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -313,7 +313,12 @@ func (ob *Observer) Logger() *ObserverLogger { // WithLogger attaches a new logger to the observer. func (ob *Observer) WithLogger(logger Logger) *Observer { - chainLogger := logger.Std.With().Int64(logs.FieldChain, ob.chain.ChainId).Logger() + chainLogger := logger.Std. + With(). + Int64(logs.FieldChain, ob.chain.ChainId). + Str(logs.FieldChainNetwork, ob.chain.Network.String()). + Logger() + ob.logger = ObserverLogger{ Chain: chainLogger, Inbound: chainLogger.With().Str(logs.FieldModule, logs.ModNameInbound).Logger(), @@ -322,6 +327,7 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { Headers: chainLogger.With().Str(logs.FieldModule, logs.ModNameHeaders).Logger(), Compliance: logger.Compliance, } + return ob } diff --git a/zetaclient/logs/fields.go b/zetaclient/logs/fields.go index 497690ffa4..78b95fc7e0 100644 --- a/zetaclient/logs/fields.go +++ b/zetaclient/logs/fields.go @@ -3,12 +3,13 @@ package logs // A group of predefined field keys and module names for zetaclient logs const ( // field keys - FieldModule = "module" - FieldMethod = "method" - FieldChain = "chain" - FieldNonce = "nonce" - FieldTx = "tx" - FieldCctx = "cctx" + FieldModule = "module" + FieldMethod = "method" + FieldChain = "chain" + FieldChainNetwork = "chain_network" + FieldNonce = "nonce" + FieldTx = "tx" + FieldCctx = "cctx" // module names ModNameInbound = "inbound" diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 8742368825..09fbebc45c 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -26,6 +26,7 @@ import ( zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/db" "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" ) @@ -75,7 +76,7 @@ func syncSignerMap( presentChainIDs = make([]int64, 0) onAfterAdd = func(chainID int64, _ interfaces.ChainSigner) { - logger.Std.Info().Msgf("Added signer for chain %d", chainID) + logger.Std.Info().Int64(logs.FieldChain, chainID).Msg("Added signer") added++ } @@ -84,7 +85,7 @@ func syncSignerMap( } onBeforeRemove = func(chainID int64, _ interfaces.ChainSigner) { - logger.Std.Info().Msgf("Removing signer for chain %d", chainID) + logger.Std.Info().Int64(logs.FieldChain, chainID).Msg("Removing signer") removed++ } ) @@ -245,7 +246,8 @@ func syncObserverMap( presentChainIDs = make([]int64, 0) - onAfterAdd = func(_ int64, ob interfaces.ChainObserver) { + onAfterAdd = func(chainID int64, ob interfaces.ChainObserver) { + logger.Std.Info().Int64(logs.FieldChain, chainID).Msg("Added observer") ob.Start(ctx) added++ } @@ -254,7 +256,8 @@ func syncObserverMap( mapSet[int64, interfaces.ChainObserver](observerMap, chainID, ob, onAfterAdd) } - onBeforeRemove = func(_ int64, ob interfaces.ChainObserver) { + onBeforeRemove = func(chainID int64, ob interfaces.ChainObserver) { + logger.Std.Info().Int64(logs.FieldChain, chainID).Msg("Removing observer") ob.Stop() removed++ } diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 7594ffd763..45156648e1 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -17,6 +17,7 @@ import ( "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/constant" zetamath "github.com/zeta-chain/node/pkg/math" + "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" @@ -666,28 +667,18 @@ func (oc *Orchestrator) ScheduleCctxSolana( // runObserverSignerSync runs a blocking ticker that observes chain changes from zetacore // and optionally (de)provisions respective observers and signers. func (oc *Orchestrator) runObserverSignerSync(ctx context.Context) error { - // sync observers and signers right away to speed up zetaclient startup - if err := oc.syncObserverSigner(ctx); err != nil { - oc.logger.Error().Err(err).Msg("runObserverSignerSync: syncObserverSigner failed for initial sync") - } - - // sync observer and signer every 10 blocks (approx. 1 minute) - const cadence = 10 * constant.ZetaBlockTime - - ticker := time.NewTicker(cadence) - defer ticker.Stop() + // every other block + const cadence = 2 * constant.ZetaBlockTime - for { - select { - case <-oc.stop: - oc.logger.Warn().Msg("runObserverSignerSync: stopped") - return nil - case <-ticker.C: - if err := oc.syncObserverSigner(ctx); err != nil { - oc.logger.Error().Err(err).Msg("runObserverSignerSync: syncObserverSigner failed") - } + task := func(ctx context.Context, _ *ticker.Ticker) error { + if err := oc.syncObserverSigner(ctx); err != nil { + oc.logger.Error().Err(err).Msg("syncObserverSigner failed") } + + return nil } + + return ticker.Run(ctx, cadence, task, ticker.WithLogger(oc.logger.Logger, "SyncObserverSigner")) } // syncs and provisions observers & signers. From b19333f86eb2450116ed73445b969112c510c19c Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:07:11 +0200 Subject: [PATCH 21/36] TON deposits E2E wip --- e2e/e2etests/e2etests.go | 2 +- e2e/e2etests/test_ton_deposit.go | 93 ++++++++++++++++++----- pkg/contracts/ton/gateway_op.go | 15 ++-- pkg/contracts/ton/gateway_send.go | 55 ++++++++++++++ zetaclient/chains/base/observer.go | 39 ++++++---- zetaclient/chains/ton/observer/inbound.go | 31 +++++--- 6 files changed, 189 insertions(+), 46 deletions(-) create mode 100644 pkg/contracts/ton/gateway_send.go diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index a33737eb41..1870f3a5a2 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -436,7 +436,7 @@ var AllE2ETests = []runner.E2ETest{ TestTONDepositName, "deposit TON into ZEVM", []runner.ArgDefinition{ - {Description: "amount in nano tons", DefaultValue: "900000000"}, // 0.9 TON + {Description: "amount in nano tons", DefaultValue: "1000000000"}, // 1.0 TON }, TestTONDeposit, ), diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index a2e8df09d0..bd32e2f654 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -1,42 +1,101 @@ package e2etests import ( + "time" + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/runner/ton" + "github.com/zeta-chain/node/pkg/chains" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" ) // TestTONDeposit (!) This boilerplate is a demonstration of E2E capabilities for TON integration // Actual Deposit test is not implemented yet. -func TestTONDeposit(r *runner.E2ERunner, _ []string) { - ctx, deployer := r.Ctx, r.TONDeployer +func TestTONDeposit(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + // Given TON Localnet chain + chain := chains.TONLocalnet // Given deployer - deployerBalance, err := deployer.GetBalance(ctx) - require.NoError(r, err, "failed to get deployer balance") - require.NotZero(r, deployerBalance, "deployer balance is zero") + ctx, deployer := r.Ctx, r.TONDeployer + + // Given amount + amount := math.NewUintFromBigInt(parseBigInt(r, args[0])) + + // Given TON Gateway + gw := toncontracts.NewGateway(r.TONGateway) // Given sample wallet with a balance of 50 TON sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) require.NoError(r, err) - // That was funded (again) but the faucet - _, err = deployer.Fund(ctx, sender.GetAddress(), ton.TONCoins(30)) + // Given sample EVM address + recipient := sample.EthAddress() + + // ACT + r.Logger.Print( + "Sending deposit of %s TON from %s to zEVM %s", + amount.String(), + sender.GetAddress().ToRaw(), + recipient.Hex(), + ) + + // we need to include this send mode due to how wallet V5 works + // https://github.com/tonkeeper/w5/blob/main/contracts/wallet_v5.fc#L82 + err = gw.SendDeposit(ctx, sender, amount, recipient, toncontracts.SendFlagIgnoreErrors) + + // ASSERT require.NoError(r, err) - // Check sender balance - sb, err := sender.GetBalance(ctx) + // Wait for CCTX mining + cctxs := catchPendingCCTX(r, chain.ChainId, time.Minute) + require.Len(r, cctxs, 1) + + cctx := cctxs[0] + + // Check cctx props + require.NotNil(r, cctx.InboundParams) + require.NotNil(r, cctx.InboundParams.Sender) + //require.NoError(r, cctx.InboundParams.) + //cctx + + // todo validate CCTX + + r.WaitForMinedCCTXFromIndex(cctx.Index) + + // Check sender's balance + balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, recipient) require.NoError(r, err) - senderBalance := math.NewUint(sb) + r.Logger.Print("recipient's zEVM TON balance after deposit: %d", balance) - // note that it's not exactly 80 TON, but 79.99... due to gas fees - // We'll tackle gas math later. - r.Logger.Print( - "Balance of sender (%s): %s", - sender.GetAddress().ToHuman(false, true), - ton.FormatCoins(senderBalance), - ) + // todo check balance equals to cctx.amount +} + +// use another method - this returns pending only pending OUTBOUNDS +func catchPendingCCTX(r *runner.E2ERunner, chainID int64, timeout time.Duration) []*crosschainTypes.CrossChainTx { + in := &crosschainTypes.QueryListPendingCctxRequest{ChainId: chainID} + + start := time.Now() + + for time.Since(start) < timeout { + res, err := r.CctxClient.ListPendingCctx(r.Ctx, in) + if err == nil && len(res.CrossChainTx) > 0 { + return res.CrossChainTx + } + + time.Sleep(time.Second) + } + + r.Logger.Error("Timeout waiting for pending CCTX for chain %d", chainID) + r.FailNow() + + return nil } diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go index 962ebd7d53..9b77120a65 100644 --- a/pkg/contracts/ton/gateway_op.go +++ b/pkg/contracts/ton/gateway_op.go @@ -58,13 +58,8 @@ func (d Deposit) Memo() []byte { // AsBody casts struct as internal message body. func (d Deposit) AsBody() (*boc.Cell, error) { b := boc.NewCell() - err := ErrCollect( - b.WriteUint(uint64(OpDeposit), sizeOpCode), - b.WriteUint(0, sizeQueryID), - b.WriteBytes(d.Recipient.Bytes()), - ) - return b, err + return b, writeDepositBody(b, d.Recipient) } // DepositAndCall represents a deposit and call operation @@ -101,3 +96,11 @@ func (d DepositAndCall) AsBody() (*boc.Cell, error) { return b, err } + +func writeDepositBody(b *boc.Cell, recipient eth.Address) error { + return ErrCollect( + b.WriteUint(uint64(OpDeposit), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(recipient.Bytes()), + ) +} diff --git a/pkg/contracts/ton/gateway_send.go b/pkg/contracts/ton/gateway_send.go new file mode 100644 index 0000000000..a6b30f2745 --- /dev/null +++ b/pkg/contracts/ton/gateway_send.go @@ -0,0 +1,55 @@ +package ton + +import ( + "context" + + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/wallet" +) + +// Sender TON tx sender. +type Sender interface { + Send(ctx context.Context, messages ...wallet.Sendable) error +} + +const ( + SendModeDefault = uint8(0) +) + +const ( + SendFlagIgnoreErrors = uint8(2) +) + +// SendDeposit sends a deposit operation to the gateway on behalf of the sender. +func (gw *Gateway) SendDeposit( + ctx context.Context, + s Sender, + amount math.Uint, + zevmRecipient eth.Address, + sendMode uint8, +) error { + body := boc.NewCell() + + if err := writeDepositBody(body, zevmRecipient); err != nil { + return errors.Wrap(err, "failed to write deposit body") + } + + return gw.send(ctx, s, amount, body, sendMode) +} + +func (gw *Gateway) send(ctx context.Context, s Sender, amount math.Uint, body *boc.Cell, sendMode uint8) error { + if body == nil { + return errors.New("body is nil") + } + + return s.Send(ctx, wallet.Message{ + Amount: tlb.Coins(amount.Uint64()), + Address: gw.accountID, + Body: body, + Mode: sendMode, + }) +} diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index db5232fa30..af8773dfdb 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -451,22 +451,35 @@ func (ob *Observer) PostVoteInbound( msg *crosschaintypes.MsgVoteInbound, retryGasLimit uint64, ) (string, error) { - txHash := msg.InboundHash - coinType := msg.CoinType - chainID := ob.Chain().ChainId - zetaHash, ballot, err := ob.ZetacoreClient(). - PostVoteInbound(ctx, zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) - if err != nil { - ob.logger.Inbound.Err(err). - Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) + const gasLimit = zetacore.PostVoteInboundGasLimit + + var ( + txHash = msg.InboundHash + coinType = msg.CoinType + chainID = ob.Chain().ChainId + ) + + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(ctx, gasLimit, retryGasLimit, msg) + + lf := map[string]any{ + "inbound.chain_id": chainID, + "inbound.coin_type": coinType.String(), + "inbound.external_tx_hash": txHash, + "inbound.ballot_index": ballot, + "inbound.zeta_tx_hash": zetaHash, + } + + switch { + case err != nil: + ob.logger.Inbound.Error().Err(err).Fields(lf).Msg("inbound detected: error posting vote") return "", err - } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) - } else { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) + case zetaHash == "": + ob.logger.Inbound.Info().Fields(lf).Msg("inbound detected: already voted on ballot") + default: + ob.logger.Inbound.Info().Fields(lf).Msgf("inbound detected: vote posted") } - return ballot, err + return ballot, nil } // AlertOnRPCLatency prints an alert if the RPC latency exceeds the threshold. diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index f2ab2af77c..7ab5afd43c 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -86,11 +86,11 @@ func (ob *Observer) observeInbound(ctx context.Context) error { return nil case len(txs) > MaxTransactionsPerTick: ob.Logger().Inbound.Info(). - Msgf("ObserveInbound: got %d transactions. Taking first %d", len(txs), MaxTransactionsPerTick) + Msgf("observeInbound: got %d transactions. Taking first %d", len(txs), MaxTransactionsPerTick) txs = txs[:MaxTransactionsPerTick] default: - ob.Logger().Inbound.Info().Msgf("ObserveInbound: got %d transactions", len(txs)) + ob.Logger().Inbound.Info().Msgf("observeInbound: got %d transactions", len(txs)) } for i := range txs { @@ -102,12 +102,19 @@ func (ob *Observer) observeInbound(ctx context.Context) error { } if skip { + ob.Logger().Inbound.Info().Fields(txLogFields(&tx)).Msg("observeInbound: skipping tx") ob.setLastScannedTX(&tx) + continue } if _, err := ob.voteInbound(ctx, parsedTX); err != nil { - return errors.Wrapf(err, "unable to vote inbound (hash %s)", parsedTX.Hash().Hex()) + ob.Logger().Inbound. + Error().Err(err). + Fields(txLogFields(&tx)). + Msg("observeInbound: unable to vote for tx") + + return errors.Wrapf(err, "unable to vote for inbound tx %s", tx.Hash().Hex()) } ob.setLastScannedTX(&parsedTX.Transaction) @@ -232,15 +239,21 @@ func (ob *Observer) setLastScannedTX(tx *ton.Transaction) { if err := ob.WriteLastTxScannedToDB(txHash); err != nil { ob.Logger().Inbound.Error(). Err(err). - Uint64("tx.lt", tx.Lt). - Str("tx.hash", tx.Hash().Hex()). - Msgf("ObserveInbound: unable to WriteLastTxScannedToDB") + Fields(txLogFields(tx)). + Msgf("setLastScannedTX: unable to WriteLastTxScannedToDB") return } ob.Logger().Inbound.Info(). - Uint64("tx.lt", tx.Lt). - Str("tx.hash", tx.Hash().Hex()). - Msgf("ObserveInbound: WriteLastTxScannedToDB") + Fields(txLogFields(tx)). + Msg("setLastScannedTX: WriteLastTxScannedToDB") +} + +func txLogFields(tx *ton.Transaction) map[string]any { + return map[string]any{ + "inbound.ton.lt": tx.Lt, + "inbound.ton.hash": tx.Hash().Hex(), + "inbound.ton.block_id": tx.BlockID.String(), + } } From a2acaf6e2d0509ddc7589c1571d77ad53a46c7d6 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:52:44 +0200 Subject: [PATCH 22/36] Fix bugs during cctx; --- pkg/chains/chain.go | 2 ++ pkg/chains/chain_test.go | 6 ++++++ x/crosschain/keeper/evm_deposit.go | 2 +- zetaclient/chains/ton/observer/inbound.go | 5 +++-- zetaclient/chains/ton/observer/inbound_test.go | 11 +++++++++-- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 19ac59830d..3cdb9eb161 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -108,6 +108,8 @@ func DecodeAddressFromChainID(chainID int64, addr string, additionalChains []Cha return []byte(addr), nil case IsSolanaChain(chainID, additionalChains): return []byte(addr), nil + case IsTONChain(chainID, additionalChains): + return []byte(addr), nil default: return nil, fmt.Errorf("chain (%d) not supported", chainID) } diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index 2b2dfd7e77..68c0f8d979 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -300,6 +300,12 @@ func TestDecodeAddressFromChainID(t *testing.T) { addr: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw", want: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"), }, + { + name: "TON", + chainID: chains.TONMainnet.ChainId, + addr: "0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a1", + want: []byte("0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a1"), + }, { name: "Non-supported chain", chainID: 9999, diff --git a/x/crosschain/keeper/evm_deposit.go b/x/crosschain/keeper/evm_deposit.go index c873e9b636..c672686c1a 100644 --- a/x/crosschain/keeper/evm_deposit.go +++ b/x/crosschain/keeper/evm_deposit.go @@ -96,7 +96,7 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo from, err := chains.DecodeAddressFromChainID(inboundSenderChainID, inboundSender, k.GetAuthorityKeeper().GetAdditionalChainList(ctx)) if err != nil { - return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %s", err.Error()) + return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %w", err) } evmTxResponse, contractCall, err := k.fungibleKeeper.ZRC20DepositAndCallContract( diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 7ab5afd43c..99b7083dac 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -2,6 +2,7 @@ package observer import ( "context" + "encoding/hex" "fmt" "slices" @@ -202,7 +203,7 @@ func (ob *Observer) voteDeposit( sender, ob.ZetacoreClient().Chain().ChainId, amount, - string(memo), + hex.EncodeToString(memo), inboundHash, uint64(seqno), gasLimit, @@ -254,6 +255,6 @@ func txLogFields(tx *ton.Transaction) map[string]any { return map[string]any{ "inbound.ton.lt": tx.Lt, "inbound.ton.hash": tx.Hash().Hex(), - "inbound.ton.block_id": tx.BlockID.String(), + "inbound.ton.block_id": tx.BlockID.BlockID.String(), } } diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go index 96f4b93d37..9aee03ddab 100644 --- a/zetaclient/chains/ton/observer/inbound_test.go +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -1,6 +1,7 @@ package observer import ( + "encoding/hex" "testing" "github.com/pkg/errors" @@ -157,7 +158,7 @@ func TestInbound(t *testing.T) { assert.Equal(t, "", cctx.Asset) assert.Equal(t, deposit.Amount.Uint64(), cctx.Amount.Uint64()) - assert.Equal(t, string(deposit.Recipient.Bytes()), cctx.Message) + assert.Equal(t, hex.EncodeToString(deposit.Recipient.Bytes()), cctx.Message) // Check hash & block height expectedHash := liteapi.TransactionHashToString(depositTX.Lt, txHash(depositTX)) @@ -219,7 +220,13 @@ func TestInbound(t *testing.T) { assert.Equal(t, "", cctx.Asset) assert.Equal(t, depositAndCall.Amount.Uint64(), cctx.Amount.Uint64()) - assert.Equal(t, string(depositAndCall.Recipient.Bytes())+callData, cctx.Message) + + expectedMessage := hex.EncodeToString(append( + depositAndCall.Recipient.Bytes(), + []byte(callData)..., + )) + + assert.Equal(t, expectedMessage, cctx.Message) // Check hash & block height expectedHash := liteapi.TransactionHashToString(depositAndCallTX.Lt, txHash(depositAndCallTX)) From 2642694600a3e458a5321f29745d5356d4959544 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:39:21 +0200 Subject: [PATCH 23/36] =?UTF-8?q?TON=20Deposits=20E2E=20=F0=9F=AB=A1?= =?UTF-8?q?=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/zetae2e/config/config.go | 1 + cmd/zetae2e/config/contracts.go | 11 +++++ e2e/config/config.go | 1 + e2e/e2etests/test_ton_deposit.go | 75 +++++++++++++++++++++---------- e2e/runner/runner.go | 6 +++ e2e/txserver/zeta_tx_server.go | 2 +- pkg/contracts/ton/gateway_send.go | 6 +-- 7 files changed, 73 insertions(+), 29 deletions(-) diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 8e87e63bef..0cea78af39 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -68,6 +68,7 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.ZEVM.ERC20ZRC20Addr = config.DoubleQuotedString(r.ERC20ZRC20Addr.Hex()) conf.Contracts.ZEVM.BTCZRC20Addr = config.DoubleQuotedString(r.BTCZRC20Addr.Hex()) conf.Contracts.ZEVM.SOLZRC20Addr = config.DoubleQuotedString(r.SOLZRC20Addr.Hex()) + conf.Contracts.ZEVM.TONZRC20Addr = config.DoubleQuotedString(r.TONZRC20Addr.Hex()) conf.Contracts.ZEVM.UniswapFactoryAddr = config.DoubleQuotedString(r.UniswapV2FactoryAddr.Hex()) conf.Contracts.ZEVM.UniswapRouterAddr = config.DoubleQuotedString(r.UniswapV2RouterAddr.Hex()) conf.Contracts.ZEVM.ConnectorZEVMAddr = config.DoubleQuotedString(r.ConnectorZEVMAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index d6cba953a5..9af3ccd812 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -135,6 +135,17 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { } } + if c := conf.Contracts.ZEVM.TONZRC20Addr; c != "" { + r.TONZRC20Addr, err = c.AsEVMAddress() + if err != nil { + return fmt.Errorf("invalid TONZRC20Addr: %w", err) + } + r.TONZRC20, err = zrc20.NewZRC20(r.TONZRC20Addr, r.ZEVMClient) + if err != nil { + return err + } + } + if c := conf.Contracts.ZEVM.UniswapFactoryAddr; c != "" { r.UniswapV2FactoryAddr, err = c.AsEVMAddress() if err != nil { diff --git a/e2e/config/config.go b/e2e/config/config.go index 089ea39b70..32a799c027 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -141,6 +141,7 @@ type ZEVM struct { ERC20ZRC20Addr DoubleQuotedString `yaml:"erc20_zrc20"` BTCZRC20Addr DoubleQuotedString `yaml:"btc_zrc20"` SOLZRC20Addr DoubleQuotedString `yaml:"sol_zrc20"` + TONZRC20Addr DoubleQuotedString `yaml:"ton_zrc20"` UniswapFactoryAddr DoubleQuotedString `yaml:"uniswap_factory"` UniswapRouterAddr DoubleQuotedString `yaml:"uniswap_router"` ConnectorZEVMAddr DoubleQuotedString `yaml:"connector_zevm"` diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index bd32e2f654..8c1cdd037e 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -1,6 +1,7 @@ package e2etests import ( + "errors" "time" "cosmossdk.io/math" @@ -15,6 +16,12 @@ import ( crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" ) +// we need to use this send mode due to how wallet V5 works +// +// https://github.com/tonkeeper/w5/blob/main/contracts/wallet_v5.fc#L82 +// https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook +const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors + // TestTONDeposit (!) This boilerplate is a demonstration of E2E capabilities for TON integration // Actual Deposit test is not implemented yet. func TestTONDeposit(r *runner.E2ERunner, args []string) { @@ -29,6 +36,10 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { // Given amount amount := math.NewUintFromBigInt(parseBigInt(r, args[0])) + // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc#L28 + // (will be optimized & dynamic in the future) + depositFee := math.NewUint(10_000_000) + // Given TON Gateway gw := toncontracts.NewGateway(r.TONGateway) @@ -47,26 +58,28 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { recipient.Hex(), ) - // we need to include this send mode due to how wallet V5 works - // https://github.com/tonkeeper/w5/blob/main/contracts/wallet_v5.fc#L82 - err = gw.SendDeposit(ctx, sender, amount, recipient, toncontracts.SendFlagIgnoreErrors) + err = gw.SendDeposit(ctx, sender, amount, recipient, tonDepositSendCode) // ASSERT require.NoError(r, err) // Wait for CCTX mining - cctxs := catchPendingCCTX(r, chain.ChainId, time.Minute) + filter := func(cctx *crosschainTypes.CrossChainTx) bool { + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() + } + + cctxs, err := waitForSpecificCCTX(r, filter, time.Minute) + require.NoError(r, err) require.Len(r, cctxs, 1) + // Check CCTX cctx := cctxs[0] - // Check cctx props - require.NotNil(r, cctx.InboundParams) - require.NotNil(r, cctx.InboundParams.Sender) - //require.NoError(r, cctx.InboundParams.) - //cctx + expectedDeposit := amount.Sub(depositFee) - // todo validate CCTX + require.Equal(r, sender.GetAddress().ToRaw(), cctx.InboundParams.Sender) + require.Equal(r, expectedDeposit.Uint64(), cctx.InboundParams.Amount.Uint64()) r.WaitForMinedCCTXFromIndex(cctx.Index) @@ -74,28 +87,42 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, recipient) require.NoError(r, err) - r.Logger.Print("recipient's zEVM TON balance after deposit: %d", balance) + r.Logger.Print("Recipient's zEVM TON balance after deposit: %d", balance.Uint64()) - // todo check balance equals to cctx.amount + require.Equal(r, expectedDeposit.Uint64(), balance.Uint64()) } -// use another method - this returns pending only pending OUTBOUNDS -func catchPendingCCTX(r *runner.E2ERunner, chainID int64, timeout time.Duration) []*crosschainTypes.CrossChainTx { - in := &crosschainTypes.QueryListPendingCctxRequest{ChainId: chainID} - - start := time.Now() +func waitForSpecificCCTX( + r *runner.E2ERunner, + filter func(*crosschainTypes.CrossChainTx) bool, + timeout time.Duration, +) ([]crosschainTypes.CrossChainTx, error) { + var ( + ctx = r.Ctx + start = time.Now() + query = &crosschainTypes.QueryAllCctxRequest{} + out []crosschainTypes.CrossChainTx + ) for time.Since(start) < timeout { - res, err := r.CctxClient.ListPendingCctx(r.Ctx, in) - if err == nil && len(res.CrossChainTx) > 0 { - return res.CrossChainTx + res, err := r.CctxClient.CctxAll(ctx, query) + if err != nil { + return nil, err + } + + for i := range res.CrossChainTx { + tx := res.CrossChainTx[i] + if filter(tx) { + out = append(out, *tx) + } + } + + if len(out) > 0 { + return out, nil } time.Sleep(time.Second) } - r.Logger.Error("Timeout waiting for pending CCTX for chain %d", chainID) - r.FailNow() - - return nil + return nil, errors.New("timeout waiting for CCTX") } diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 32fcb64b8e..7d67a1e42f 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -232,6 +232,7 @@ func (r *E2ERunner) CopyAddressesFrom(other *E2ERunner) (err error) { r.ETHZRC20Addr = other.ETHZRC20Addr r.BTCZRC20Addr = other.BTCZRC20Addr r.SOLZRC20Addr = other.SOLZRC20Addr + r.TONZRC20Addr = other.TONZRC20Addr r.UniswapV2FactoryAddr = other.UniswapV2FactoryAddr r.UniswapV2RouterAddr = other.UniswapV2RouterAddr r.ConnectorZEVMAddr = other.ConnectorZEVMAddr @@ -277,6 +278,10 @@ func (r *E2ERunner) CopyAddressesFrom(other *E2ERunner) (err error) { if err != nil { return err } + r.TONZRC20, err = zrc20.NewZRC20(r.TONZRC20Addr, r.ZEVMClient) + if err != nil { + return err + } r.UniswapV2Factory, err = uniswapv2factory.NewUniswapV2Factory(r.UniswapV2FactoryAddr, r.ZEVMClient) if err != nil { return err @@ -361,6 +366,7 @@ func (r *E2ERunner) PrintContractAddresses() { r.Logger.Print("ERC20ZRC20: %s", r.ERC20ZRC20Addr.Hex()) r.Logger.Print("BTCZRC20: %s", r.BTCZRC20Addr.Hex()) r.Logger.Print("SOLZRC20: %s", r.SOLZRC20Addr.Hex()) + r.Logger.Print("TONZRC20: %s", r.TONZRC20Addr.Hex()) r.Logger.Print("UniswapFactory: %s", r.UniswapV2FactoryAddr.Hex()) r.Logger.Print("UniswapRouter: %s", r.UniswapV2RouterAddr.Hex()) r.Logger.Print("ConnectorZEVM: %s", r.ConnectorZEVMAddr.Hex()) diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 0137cb483b..5e5044fbe3 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -514,7 +514,7 @@ func (zts ZetaTxServer) DeployZRC20s(accountOperational, accountAdmin, erc20Addr 100_000, )) if err != nil { - return "", fmt.Errorf("failed to deploy ton zrc20: %s", err.Error()) + return "", fmt.Errorf("failed to deploy ton zrc20: %w", err) } // deploy erc20 zrc20 diff --git a/pkg/contracts/ton/gateway_send.go b/pkg/contracts/ton/gateway_send.go index a6b30f2745..a9c14583dc 100644 --- a/pkg/contracts/ton/gateway_send.go +++ b/pkg/contracts/ton/gateway_send.go @@ -16,11 +16,9 @@ type Sender interface { Send(ctx context.Context, messages ...wallet.Sendable) error } +// see https://docs.ton.org/develop/smart-contracts/messages#message-modes const ( - SendModeDefault = uint8(0) -) - -const ( + SendFlagSeparateFees = uint8(1) SendFlagIgnoreErrors = uint8(2) ) From 0a3fbad4713c83027b867af2eb97f29bfc77175d Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:56:29 +0200 Subject: [PATCH 24/36] TON Deposit And Call E2E --- cmd/zetae2e/local/local.go | 1 + e2e/e2etests/e2etests.go | 11 ++- e2e/e2etests/helpers.go | 5 ++ e2e/e2etests/test_ton_deposit.go | 58 ++-------------- e2e/e2etests/test_ton_deposit_and_call.go | 84 +++++++++++++++++++++++ e2e/runner/zeta.go | 39 ++++++++++- pkg/contracts/ton/gateway_op.go | 33 +++++---- pkg/contracts/ton/gateway_send.go | 19 +++++ 8 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 e2e/e2etests/test_ton_deposit_and_call.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 7e31d4a407..954bade116 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -395,6 +395,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { tonTests := []string{ e2etests.TestTONDepositName, + e2etests.TestTONDepositAndCallName, } eg.Go(tonTestRoutine(conf, deployerRunner, verbose, tonTests...)) diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 1870f3a5a2..a41444c7de 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -65,7 +65,8 @@ const ( /** * TON tests */ - TestTONDepositName = "ton_deposit" + TestTONDepositName = "ton_deposit" + TestTONDepositAndCallName = "ton_deposit_and_call" /* Bitcoin tests @@ -440,6 +441,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestTONDeposit, ), + runner.NewE2ETest( + TestTONDepositAndCallName, + "deposit TON into ZEVM and call a contract", + []runner.ArgDefinition{ + {Description: "amount in nano tons", DefaultValue: "1000000000"}, // 1.0 TON + }, + TestTONDepositAndCall, + ), /* Bitcoin tests */ diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go index a27bf23799..3389cd0475 100644 --- a/e2e/e2etests/helpers.go +++ b/e2e/e2etests/helpers.go @@ -4,6 +4,7 @@ import ( "math/big" "strconv" + "cosmossdk.io/math" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" @@ -144,6 +145,10 @@ func parseBigInt(t require.TestingT, s string) *big.Int { return v } +func parseUint(t require.TestingT, s string) math.Uint { + return math.NewUintFromBigInt(parseBigInt(t, s)) +} + // bigIntFromFloat64 takes float64 (e.g. 0.001) that represents btc amount // and converts it to big.Int for downstream usage. func btcAmountFromFloat64(t require.TestingT, amount float64) *big.Int { diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index 8c1cdd037e..bc4b5c970d 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -1,7 +1,6 @@ package e2etests import ( - "errors" "time" "cosmossdk.io/math" @@ -22,19 +21,14 @@ import ( // https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors -// TestTONDeposit (!) This boilerplate is a demonstration of E2E capabilities for TON integration -// Actual Deposit test is not implemented yet. func TestTONDeposit(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) - // Given TON Localnet chain - chain := chains.TONLocalnet - // Given deployer - ctx, deployer := r.Ctx, r.TONDeployer + ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet // Given amount - amount := math.NewUintFromBigInt(parseBigInt(r, args[0])) + amount := parseUint(r, args[0]) // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc#L28 // (will be optimized & dynamic in the future) @@ -51,7 +45,7 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { recipient := sample.EthAddress() // ACT - r.Logger.Print( + r.Logger.Info( "Sending deposit of %s TON from %s to zEVM %s", amount.String(), sender.GetAddress().ToRaw(), @@ -69,60 +63,22 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { cctx.InboundParams.Sender == sender.GetAddress().ToRaw() } - cctxs, err := waitForSpecificCCTX(r, filter, time.Minute) - require.NoError(r, err) + cctxs := r.WaitForSpecificCCTX(filter, time.Minute) require.Len(r, cctxs, 1) - // Check CCTX - cctx := cctxs[0] + cctx := r.WaitForMinedCCTXFromIndex(cctxs[0].Index) + // Check CCTX expectedDeposit := amount.Sub(depositFee) require.Equal(r, sender.GetAddress().ToRaw(), cctx.InboundParams.Sender) require.Equal(r, expectedDeposit.Uint64(), cctx.InboundParams.Amount.Uint64()) - r.WaitForMinedCCTXFromIndex(cctx.Index) - // Check sender's balance balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, recipient) require.NoError(r, err) - r.Logger.Print("Recipient's zEVM TON balance after deposit: %d", balance.Uint64()) + r.Logger.Info("Recipient's zEVM TON balance after deposit: %d", balance.Uint64()) require.Equal(r, expectedDeposit.Uint64(), balance.Uint64()) } - -func waitForSpecificCCTX( - r *runner.E2ERunner, - filter func(*crosschainTypes.CrossChainTx) bool, - timeout time.Duration, -) ([]crosschainTypes.CrossChainTx, error) { - var ( - ctx = r.Ctx - start = time.Now() - query = &crosschainTypes.QueryAllCctxRequest{} - out []crosschainTypes.CrossChainTx - ) - - for time.Since(start) < timeout { - res, err := r.CctxClient.CctxAll(ctx, query) - if err != nil { - return nil, err - } - - for i := range res.CrossChainTx { - tx := res.CrossChainTx[i] - if filter(tx) { - out = append(out, *tx) - } - } - - if len(out) > 0 { - return out, nil - } - - time.Sleep(time.Second) - } - - return nil, errors.New("timeout waiting for CCTX") -} diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go new file mode 100644 index 0000000000..a612794e9a --- /dev/null +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -0,0 +1,84 @@ +package e2etests + +import ( + "time" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/runner/ton" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + testcontract "github.com/zeta-chain/node/testutil/contracts" + crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + // Given deployer + ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet + + // Given amount + amount := parseUint(r, args[0]) + + // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc#L28 + // (will be optimized & dynamic in the future) + depositFee := math.NewUint(10_000_000) + + // Given TON Gateway + gw := toncontracts.NewGateway(r.TONGateway) + + // Given sample wallet with a balance of 50 TON + sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) + require.NoError(r, err) + + // Given sample zEVM contract + contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + r.Logger.Info("Example zevm contract deployed at: %s", contractAddr.String()) + + // Given call data + callData := []byte("hello from TON!") + + // ACT + r.Logger.Info( + "Sending deposit of %s TON from %s to zEVM %s and calling contract with %q", + amount.String(), + sender.GetAddress().ToRaw(), + contractAddr.Hex(), + string(callData), + ) + + err = gw.SendDepositAndCall(ctx, sender, amount, contractAddr, callData, tonDepositSendCode) + + // ASSERT + require.NoError(r, err) + + // Wait for CCTX mining + filter := func(cctx *crosschainTypes.CrossChainTx) bool { + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() + } + + cctxs := r.WaitForSpecificCCTX(filter, time.Minute) + require.Len(r, cctxs, 1) + + r.WaitForMinedCCTXFromIndex(cctxs[0].Index) + + expectedDeposit := amount.Sub(depositFee) + + // check if example contract has been called, bar value should be set to amount + utils.MustHaveCalledExampleContract(r, contract, expectedDeposit.BigInt()) + + // Check sender's balance + balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) + require.NoError(r, err) + + r.Logger.Info("Contract's zEVM TON balance after deposit: %d", balance.Uint64()) + + require.Equal(r, expectedDeposit.Uint64(), balance.Uint64()) +} diff --git a/e2e/runner/zeta.go b/e2e/runner/zeta.go index cec59219cd..981a63ae33 100644 --- a/e2e/runner/zeta.go +++ b/e2e/runner/zeta.go @@ -67,12 +67,49 @@ func (r *E2ERunner) WaitForMinedCCTX(txHash ethcommon.Hash) { } // WaitForMinedCCTXFromIndex waits for a cctx to be mined from its index -func (r *E2ERunner) WaitForMinedCCTXFromIndex(index string) { +func (r *E2ERunner) WaitForMinedCCTXFromIndex(index string) *types.CrossChainTx { r.Lock() defer r.Unlock() cctx := utils.WaitCCTXMinedByIndex(r.Ctx, index, r.CctxClient, r.Logger, r.CctxTimeout) utils.RequireCCTXStatus(r, cctx, types.CctxStatus_OutboundMined) + + return cctx +} + +func (r *E2ERunner) WaitForSpecificCCTX( + filter func(*types.CrossChainTx) bool, + timeout time.Duration, +) []types.CrossChainTx { + var ( + ctx = r.Ctx + start = time.Now() + query = &types.QueryAllCctxRequest{} + out []types.CrossChainTx + ) + + for time.Since(start) < timeout { + res, err := r.CctxClient.CctxAll(ctx, query) + require.NoError(r, err) + + for i := range res.CrossChainTx { + tx := res.CrossChainTx[i] + if filter(tx) { + out = append(out, *tx) + } + } + + if len(out) > 0 { + return out + } + + time.Sleep(time.Second) + } + + r.Logger.Error("WaitForSpecificCCTX: No CCTX found. Timed out") + r.FailNow() + + return nil } // SendZetaOnEvm sends ZETA to an address on EVM diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go index 9b77120a65..7d711ab89c 100644 --- a/pkg/contracts/ton/gateway_op.go +++ b/pkg/contracts/ton/gateway_op.go @@ -1,6 +1,8 @@ package ton import ( + "errors" + "cosmossdk.io/math" eth "github.com/ethereum/go-ethereum/common" "github.com/tonkeeper/tongo/boc" @@ -81,20 +83,9 @@ func (d DepositAndCall) Memo() []byte { // AsBody casts struct to internal message body. func (d DepositAndCall) AsBody() (*boc.Cell, error) { - callDataCell, err := MarshalSnakeCell(d.CallData) - if err != nil { - return nil, err - } - b := boc.NewCell() - err = ErrCollect( - b.WriteUint(uint64(OpDepositAndCall), sizeOpCode), - b.WriteUint(0, sizeQueryID), - b.WriteBytes(d.Recipient.Bytes()), - b.AddRef(callDataCell), - ) - return b, err + return b, writeDepositAndCallBody(b, d.Recipient, d.CallData) } func writeDepositBody(b *boc.Cell, recipient eth.Address) error { @@ -104,3 +95,21 @@ func writeDepositBody(b *boc.Cell, recipient eth.Address) error { b.WriteBytes(recipient.Bytes()), ) } + +func writeDepositAndCallBody(b *boc.Cell, recipient eth.Address, callData []byte) error { + if len(callData) == 0 { + return errors.New("call data is empty") + } + + callDataCell, err := MarshalSnakeCell(callData) + if err != nil { + return err + } + + return ErrCollect( + b.WriteUint(uint64(OpDepositAndCall), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(recipient.Bytes()), + b.AddRef(callDataCell), + ) +} diff --git a/pkg/contracts/ton/gateway_send.go b/pkg/contracts/ton/gateway_send.go index a9c14583dc..5dd9c21340 100644 --- a/pkg/contracts/ton/gateway_send.go +++ b/pkg/contracts/ton/gateway_send.go @@ -39,6 +39,25 @@ func (gw *Gateway) SendDeposit( return gw.send(ctx, s, amount, body, sendMode) } +// SendDepositAndCall sends a deposit operation to the gateway on behalf of the sender +// with a callData to the recipient. +func (gw *Gateway) SendDepositAndCall( + ctx context.Context, + s Sender, + amount math.Uint, + zevmRecipient eth.Address, + callData []byte, + sendMode uint8, +) error { + body := boc.NewCell() + + if err := writeDepositAndCallBody(body, zevmRecipient, callData); err != nil { + return errors.Wrap(err, "failed to write depositAndCall body") + } + + return gw.send(ctx, s, amount, body, sendMode) +} + func (gw *Gateway) send(ctx context.Context, s Sender, amount math.Uint, body *boc.Cell, sendMode uint8) error { if body == nil { return errors.New("body is nil") From 6eda977d5132c0a3d2dfacd3271650d7164e732e Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:12:08 +0200 Subject: [PATCH 25/36] Merge fixes --- pkg/ticker/ticker_test.go | 4 ++-- zetaclient/config/config_chain.go | 7 +++++++ zetaclient/config/types_test.go | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/ticker/ticker_test.go b/pkg/ticker/ticker_test.go index 3d28dbd8a3..60d6c74dc8 100644 --- a/pkg/ticker/ticker_test.go +++ b/pkg/ticker/ticker_test.go @@ -151,7 +151,7 @@ func TestTicker(t *testing.T) { // ASSERT assert.ErrorContains(t, err, "panic during ticker run: oops") // assert that we get error with the correct line number - assert.ErrorContains(t, err, "ticker_test.go:142") + assert.ErrorContains(t, err, "ticker_test.go:145") }) t.Run("Nil panic", func(t *testing.T) { @@ -176,7 +176,7 @@ func TestTicker(t *testing.T) { "panic during ticker run: runtime error: invalid memory address or nil pointer dereference", ) // assert that we get error with the correct line number - assert.ErrorContains(t, err, "ticker_test.go:162") + assert.ErrorContains(t, err, "ticker_test.go:165") }) t.Run("Run as a single call", func(t *testing.T) { diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index d7af8eee4c..6f17153b52 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -88,3 +88,10 @@ func evmChainsConfigs() map[int64]EVMConfig { }, } } + +// btcChainsConfigs contains BTC chain configs +func btcChainsConfigs() map[int64]BTCConfig { + return map[int64]BTCConfig{ + chains.BitcoinRegtest.ChainId: bitcoinConfigRegnet(), + } +} diff --git a/zetaclient/config/types_test.go b/zetaclient/config/types_test.go index c57fd002e0..02f7eb5a6f 100644 --- a/zetaclient/config/types_test.go +++ b/zetaclient/config/types_test.go @@ -128,6 +128,8 @@ func Test_StringMasked(t *testing.T) { // create config with defaults cfg := config.New(true) + cfg.SolanaConfig.Endpoint += "?api-key=123" + // mask the config JSON string masked := cfg.StringMasked() require.NotEmpty(t, masked) @@ -137,5 +139,5 @@ func Test_StringMasked(t *testing.T) { require.Contains(t, masked, "BTCChainConfigs") // should not contain endpoint - require.NotContains(t, masked, "http") + require.NotContains(t, masked, "?api-key=123") } From 7b2f232438f1cfbaa1890198436b96065e2f7b35 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:26:14 +0200 Subject: [PATCH 26/36] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index dc83cb839b..f76255c14f 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,7 @@ * [2911](https://github.com/zeta-chain/node/pull/2911) - add chain static information for btc testnet4 * [2904](https://github.com/zeta-chain/node/pull/2904) - integrate authenticated calls smart contract functionality into protocol * [2919](https://github.com/zeta-chain/node/pull/2919) - add inbound sender to revert context +* [2896](https://github.com/zeta-chain/node/pull/2896) - add TON inbound observation ### Refactor From 142778b594ea3bbffd0972a4fe0ce4f06973bd96 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:35:23 +0200 Subject: [PATCH 27/36] gosec --- pkg/contracts/ton/gateway.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index 201e8abb69..4a85925b9e 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -126,10 +126,12 @@ func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { case OpDepositAndCall: content, errContent = parseDepositAndCall(tx, sender, body) default: + // #nosec G115 always in range return nil, errors.Wrapf(ErrUnknownOp, "op code %d", int64(op)) } if errContent != nil { + // #nosec G115 always in range return nil, errors.Wrapf(ErrParse, "unable to parse content for op code %d: %s", int64(op), errContent.Error()) } From 3f8f98958819c72911d6a78660c10490c14e72c6 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:32:17 +0200 Subject: [PATCH 28/36] Improve testing --- pkg/chains/chains_test.go | 5 ++ zetaclient/chains/interfaces/interfaces.go | 8 ++ .../chains/ton/liteapi/client_live_test.go | 52 ++++++++---- zetaclient/chains/ton/liteapi/client_test.go | 82 +++++++++++++++++++ 4 files changed, 129 insertions(+), 18 deletions(-) diff --git a/pkg/chains/chains_test.go b/pkg/chains/chains_test.go index 270a26b045..73c3521ffa 100644 --- a/pkg/chains/chains_test.go +++ b/pkg/chains/chains_test.go @@ -135,6 +135,11 @@ func TestChainListByNetwork(t *testing.T) { chains.Network_solana, []chains.Chain{chains.SolanaMainnet, chains.SolanaDevnet, chains.SolanaLocalnet}, }, + { + "TON", + chains.Network_ton, + []chains.Chain{chains.TONMainnet, chains.TONTestnet, chains.TONLocalnet}, + }, } for _, lt := range listTests { diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 0e1b938c64..c89a77e4b8 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -39,12 +39,20 @@ const ( // ChainObserver is the interface for chain observer type ChainObserver interface { + // Start starts the observer Start(ctx context.Context) + + // Stop stops the observer Stop() + // ChainParams returns observer chain params (might be out of date with zetacore) ChainParams() observertypes.ChainParams + + // SetChainParams sets observer chain params SetChainParams(observertypes.ChainParams) + // VoteOutboundIfConfirmed checks outbound status and returns (continueKeySign, error) + // todo we should make this simpler. VoteOutboundIfConfirmed(ctx context.Context, cctx *crosschaintypes.CrossChainTx) (bool, error) } diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index 40c37dce71..acf211736b 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -28,32 +28,48 @@ func TestClient(t *testing.T) { ) t.Run("GetFirstTransaction", func(t *testing.T) { - // ARRANGE - // Given sample account id (a dev wallet) - // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions - accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") - require.NoError(t, err) + t.Run("Account doesn't exist", func(t *testing.T) { + // ARRANGE + accountID, err := ton.ParseAccountID("0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a2") + require.NoError(t, err) - // Given expected hash for the first tx - const expect = "b73df4853ca02a040df46f56635d6b8f49b554d5f556881ab389111bbfce4498" + // ACT + tx, scrolled, err := client.GetFirstTransaction(ctx, accountID) - // as of 2024-09-18 - const expectedTransactions = 23 + // ASSERT + require.ErrorContains(t, err, "account is not active") + require.Zero(t, scrolled) + require.Nil(t, tx) + }) - start := time.Now() + t.Run("All good", func(t *testing.T) { + // ARRANGE + // Given sample account id (a dev wallet) + // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions + accountID, err := ton.ParseAccountID("UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr") + require.NoError(t, err) - // ACT - tx, scrolled, err := client.GetFirstTransaction(ctx, accountID) + // Given expected hash for the first tx + const expect = "b73df4853ca02a040df46f56635d6b8f49b554d5f556881ab389111bbfce4498" - finish := time.Since(start) + // as of 2024-09-18 + const expectedTransactions = 23 - // ASSERT - require.NoError(t, err) + start := time.Now() + + // ACT + tx, scrolled, err := client.GetFirstTransaction(ctx, accountID) + + finish := time.Since(start) + + // ASSERT + require.NoError(t, err) - assert.GreaterOrEqual(t, scrolled, expectedTransactions) - assert.Equal(t, expect, tx.Hash().Hex()) + assert.GreaterOrEqual(t, scrolled, expectedTransactions) + assert.Equal(t, expect, tx.Hash().Hex()) - t.Logf("Time taken %s; transactions scanned: %d", finish.String(), scrolled) + t.Logf("Time taken %s; transactions scanned: %d", finish.String(), scrolled) + }) }) t.Run("GetTransactionsUntil", func(t *testing.T) { diff --git a/zetaclient/chains/ton/liteapi/client_test.go b/zetaclient/chains/ton/liteapi/client_test.go index 3ab56422a5..a1148540be 100644 --- a/zetaclient/chains/ton/liteapi/client_test.go +++ b/zetaclient/chains/ton/liteapi/client_test.go @@ -16,3 +16,85 @@ func TestHashes(t *testing.T) { require.Equal(t, "e02b8c7cec103e08175ade8106619a8908707623c31451df2a68497c7d23d15a", hash.Hex()) require.Equal(t, sample, TransactionHashToString(lt, hash)) } + +func TestTransactionHashFromString(t *testing.T) { + for _, tt := range []struct { + name string + raw string + error bool + lt uint64 + hash string + }{ + { + name: "real example", + raw: "163000003:d0415f655644db6ee1260b1fa48e9f478e938823e8b293054fbae1f3511b77c5", + lt: 163000003, + hash: "d0415f655644db6ee1260b1fa48e9f478e938823e8b293054fbae1f3511b77c5", + }, + { + name: "zero lt", + raw: "0:0000000000000000000000000000000000000000000000000000000000000000", + lt: 0, + hash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "big lt", + raw: "999999999999:fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0", + lt: 999_999_999_999, + hash: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0", + }, + { + name: "missing colon", + raw: "123456abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef", + error: true, + }, + { + name: "missing logical time", + raw: ":abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef", + error: true, + }, + { + name: "hash length", + raw: "123456:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcde", + error: true, + }, + { + name: "non-numeric logical time", + raw: "notanumber:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef", + error: true, + }, + { + name: "non-hex hash", + raw: "123456:xyz123xyz123xyz123xyz123xyz123xyz123xyz123xyz123xyz123xyz123xyz123", + error: true, + }, + { + name: "empty string", + raw: "", + error: true, + }, + { + name: "Invalid - only logical time, no hash", + raw: "123456:", + error: true, + }, + { + name: "Invalid - too many parts (extra colon)", + raw: "123456:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef:extra", + error: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + lt, hash, err := TransactionHashFromString(tt.raw) + + if tt.error { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.lt, lt) + require.Equal(t, hash.Hex(), tt.hash) + }) + } +} From f5a4701dcbfec24408d727fc9cb64895ac25907f Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:55:58 +0200 Subject: [PATCH 29/36] Simplify ticker.Stop(). Leverage ctx.cancel() --- pkg/ticker/ticker.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index ff2bd7284c..2a5a7edff1 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -43,10 +43,9 @@ type Task func(ctx context.Context, t *Ticker) error // Ticker represents a ticker that will run a function periodically. // It also invokes BEFORE ticker starts. type Ticker struct { - interval time.Duration - ticker *time.Ticker - task Task - internalStopChan chan struct{} + interval time.Duration + ticker *time.Ticker + task Task // runnerMu is a mutex to prevent double run runnerMu sync.Mutex @@ -54,7 +53,8 @@ type Ticker struct { // stateMu is a mutex to prevent concurrent SetInterval calls stateMu sync.Mutex - stopped bool + stopped bool + ctxCancel context.CancelFunc externalStopChan <-chan struct{} logger zerolog.Logger @@ -120,8 +120,8 @@ func (t *Ticker) Run(ctx context.Context) (err error) { defer t.runnerMu.Unlock() // setup + ctx, t.ctxCancel = context.WithCancel(ctx) t.ticker = time.NewTicker(t.interval) - t.internalStopChan = make(chan struct{}) t.stopped = false // initial run @@ -133,16 +133,21 @@ func (t *Ticker) Run(ctx context.Context) (err error) { for { select { case <-ctx.Done(): + // if task is finished (i.e. last tick completed BEFORE ticker.Stop(), + // then we need to return nil) + if t.stopped { + return nil + } return ctx.Err() case <-t.ticker.C: + // If another goroutine calls ticker.Stop() while the current tick is running, + // Then it's okay to return ctx error if err := t.task(ctx, t); err != nil { return fmt.Errorf("ticker task failed: %w", err) } case <-t.externalStopChan: t.Stop() return nil - case <-t.internalStopChan: - return nil } } } @@ -167,11 +172,11 @@ func (t *Ticker) Stop() { defer t.stateMu.Unlock() // noop - if t.stopped || t.internalStopChan == nil { + if t.stopped { return } - close(t.internalStopChan) + t.ctxCancel() t.stopped = true t.ticker.Stop() From 2de851dc020b68814a952b1c60a533cc93593676 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:55:30 +0200 Subject: [PATCH 30/36] Simplify E2E --- e2e/e2etests/test_ton_deposit.go | 28 ++-------- e2e/e2etests/test_ton_deposit_and_call.go | 23 ++------- e2e/runner/runner.go | 4 +- e2e/runner/setup_ton.go | 3 +- e2e/runner/ton.go | 59 ++++++++++++++++++++++ e2e/runner/zeta.go | 21 ++++---- x/observer/keeper/msg_server_vote_blame.go | 6 +-- 7 files changed, 83 insertions(+), 61 deletions(-) create mode 100644 e2e/runner/ton.go diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index bc4b5c970d..b4c7a32168 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -10,17 +10,10 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/runner/ton" "github.com/zeta-chain/node/pkg/chains" - toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/testutil/sample" - crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" + cctypes "github.com/zeta-chain/node/x/crosschain/types" ) -// we need to use this send mode due to how wallet V5 works -// -// https://github.com/tonkeeper/w5/blob/main/contracts/wallet_v5.fc#L82 -// https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook -const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors - func TestTONDeposit(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -34,9 +27,6 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { // (will be optimized & dynamic in the future) depositFee := math.NewUint(10_000_000) - // Given TON Gateway - gw := toncontracts.NewGateway(r.TONGateway) - // Given sample wallet with a balance of 50 TON sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) require.NoError(r, err) @@ -45,28 +35,18 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { recipient := sample.EthAddress() // ACT - r.Logger.Info( - "Sending deposit of %s TON from %s to zEVM %s", - amount.String(), - sender.GetAddress().ToRaw(), - recipient.Hex(), - ) - - err = gw.SendDeposit(ctx, sender, amount, recipient, tonDepositSendCode) + err = r.TONDeposit(sender, amount, recipient) // ASSERT require.NoError(r, err) // Wait for CCTX mining - filter := func(cctx *crosschainTypes.CrossChainTx) bool { + filter := func(cctx *cctypes.CrossChainTx) bool { return cctx.InboundParams.SenderChainId == chain.ChainId && cctx.InboundParams.Sender == sender.GetAddress().ToRaw() } - cctxs := r.WaitForSpecificCCTX(filter, time.Minute) - require.Len(r, cctxs, 1) - - cctx := r.WaitForMinedCCTXFromIndex(cctxs[0].Index) + cctx := r.WaitForSpecificCCTX(filter, time.Minute) // Check CCTX expectedDeposit := amount.Sub(depositFee) diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go index a612794e9a..b385b19f49 100644 --- a/e2e/e2etests/test_ton_deposit_and_call.go +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -11,9 +11,8 @@ import ( "github.com/zeta-chain/node/e2e/runner/ton" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" - toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" testcontract "github.com/zeta-chain/node/testutil/contracts" - crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" + cctypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { @@ -29,9 +28,6 @@ func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { // (will be optimized & dynamic in the future) depositFee := math.NewUint(10_000_000) - // Given TON Gateway - gw := toncontracts.NewGateway(r.TONGateway) - // Given sample wallet with a balance of 50 TON sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) require.NoError(r, err) @@ -45,29 +41,18 @@ func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { callData := []byte("hello from TON!") // ACT - r.Logger.Info( - "Sending deposit of %s TON from %s to zEVM %s and calling contract with %q", - amount.String(), - sender.GetAddress().ToRaw(), - contractAddr.Hex(), - string(callData), - ) - - err = gw.SendDepositAndCall(ctx, sender, amount, contractAddr, callData, tonDepositSendCode) + err = r.TONDepositAndCall(sender, amount, contractAddr, callData) // ASSERT require.NoError(r, err) // Wait for CCTX mining - filter := func(cctx *crosschainTypes.CrossChainTx) bool { + filter := func(cctx *cctypes.CrossChainTx) bool { return cctx.InboundParams.SenderChainId == chain.ChainId && cctx.InboundParams.Sender == sender.GetAddress().ToRaw() } - cctxs := r.WaitForSpecificCCTX(filter, time.Minute) - require.Len(r, cctxs, 1) - - r.WaitForMinedCCTXFromIndex(cctxs[0].Index) + r.WaitForSpecificCCTX(filter, time.Minute) expectedDeposit := amount.Sub(depositFee) diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index ca3e633a55..03cafb6fc4 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -18,7 +18,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/tonkeeper/tongo/ton" "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/erc20custody.sol" zetaeth "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zeta.eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zetaconnector.eth.sol" @@ -40,6 +39,7 @@ import ( "github.com/zeta-chain/node/e2e/txserver" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/contracts/testdappv2" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" authoritytypes "github.com/zeta-chain/node/x/authority/types" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" @@ -73,7 +73,7 @@ type E2ERunner struct { BTCDeployerAddress *btcutil.AddressWitnessPubKeyHash SolanaDeployerAddress solana.PublicKey TONDeployer *tonrunner.Deployer - TONGateway ton.AccountID + TONGateway *toncontracts.Gateway // all clients. // a reference to this type is required to enable creating a new E2ERunner. diff --git a/e2e/runner/setup_ton.go b/e2e/runner/setup_ton.go index 19e91d2608..62d89c3329 100644 --- a/e2e/runner/setup_ton.go +++ b/e2e/runner/setup_ton.go @@ -10,6 +10,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -65,7 +66,7 @@ func (r *E2ERunner) SetupTON() error { } r.TONDeployer = deployer - r.TONGateway = gwAccount.ID + r.TONGateway = toncontracts.NewGateway(gwAccount.ID) return r.ensureTONChainParams(gwAccount) } diff --git a/e2e/runner/ton.go b/e2e/runner/ton.go new file mode 100644 index 0000000000..8746e25977 --- /dev/null +++ b/e2e/runner/ton.go @@ -0,0 +1,59 @@ +package runner + +import ( + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/wallet" + + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" +) + +// we need to use this send mode due to how wallet V5 works +// +// https://github.com/tonkeeper/w5/blob/main/contracts/wallet_v5.fc#L82 +// https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook +const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors + +// TONDeposit deposit TON to Gateway contract +func (r *E2ERunner) TONDeposit(sender *wallet.Wallet, amount math.Uint, zevmRecipient eth.Address) error { + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") + + require.NotNil(r, sender, "Sender wallet is nil") + require.False(r, amount.IsZero()) + require.NotEqual(r, (eth.Address{}).String(), zevmRecipient.String()) + + r.Logger.Info( + "Sending deposit of %s TON from %s to zEVM %s", + amount.String(), + sender.GetAddress().ToRaw(), + zevmRecipient.Hex(), + ) + + return r.TONGateway.SendDeposit(r.Ctx, sender, amount, zevmRecipient, tonDepositSendCode) +} + +// TONDepositAndCall deposit TON to Gateway contract with call data. +func (r *E2ERunner) TONDepositAndCall( + sender *wallet.Wallet, + amount math.Uint, + zevmRecipient eth.Address, + callData []byte, +) error { + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") + + require.NotNil(r, sender, "Sender wallet is nil") + require.False(r, amount.IsZero()) + require.NotEqual(r, (eth.Address{}).String(), zevmRecipient.String()) + require.NotEmpty(r, callData) + + r.Logger.Info( + "Sending deposit of %s TON from %s to zEVM %s and calling contract with %q", + amount.String(), + sender.GetAddress().ToRaw(), + zevmRecipient.Hex(), + string(callData), + ) + + return r.TONGateway.SendDepositAndCall(r.Ctx, sender, amount, zevmRecipient, callData, tonDepositSendCode) +} diff --git a/e2e/runner/zeta.go b/e2e/runner/zeta.go index 126f718ef3..5b977a9a31 100644 --- a/e2e/runner/zeta.go +++ b/e2e/runner/zeta.go @@ -6,6 +6,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + query "github.com/cosmos/cosmos-sdk/types/query" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" @@ -106,32 +107,30 @@ func (r *E2ERunner) WaitForMinedCCTXFromIndex(index string) *types.CrossChainTx return cctx } +// WaitForSpecificCCTX scans for cctx by filters and ensures it's mined func (r *E2ERunner) WaitForSpecificCCTX( filter func(*types.CrossChainTx) bool, timeout time.Duration, -) []types.CrossChainTx { +) *types.CrossChainTx { var ( - ctx = r.Ctx - start = time.Now() - query = &types.QueryAllCctxRequest{} - out []types.CrossChainTx + ctx = r.Ctx + start = time.Now() + reqQuery = &types.QueryAllCctxRequest{ + Pagination: &query.PageRequest{Reverse: true}, + } ) for time.Since(start) < timeout { - res, err := r.CctxClient.CctxAll(ctx, query) + res, err := r.CctxClient.CctxAll(ctx, reqQuery) require.NoError(r, err) for i := range res.CrossChainTx { tx := res.CrossChainTx[i] if filter(tx) { - out = append(out, *tx) + return r.WaitForMinedCCTXFromIndex(tx.Index) } } - if len(out) > 0 { - return out - } - time.Sleep(time.Second) } diff --git a/x/observer/keeper/msg_server_vote_blame.go b/x/observer/keeper/msg_server_vote_blame.go index 74a3654657..d320ff54db 100644 --- a/x/observer/keeper/msg_server_vote_blame.go +++ b/x/observer/keeper/msg_server_vote_blame.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - crosschainTypes "github.com/zeta-chain/node/x/crosschain/types" + cctypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/x/observer/types" ) @@ -21,9 +21,7 @@ func (k msgServer) VoteBlame( // GetChainFromChainID makes sure we are getting only supported chains , if a chain support has been turned on using gov proposal, this function returns nil observationChain, found := k.GetSupportedChainFromChainID(ctx, msg.ChainId) if !found { - return nil, sdkerrors.Wrapf( - crosschainTypes.ErrUnsupportedChain, - "%s, ChainID %d", voteBlameID, msg.ChainId) + return nil, sdkerrors.Wrapf(cctypes.ErrUnsupportedChain, "%s, ChainID %d", voteBlameID, msg.ChainId) } if ok := k.IsNonTombstonedObserver(ctx, msg.Creator); !ok { From d25a79a4d3e500a5050109736f00731626a46f69 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:32:46 +0200 Subject: [PATCH 31/36] Simplify liteapi semantics --- pkg/chains/chain.go | 2 +- zetaclient/chains/ton/config.go | 3 +- zetaclient/chains/ton/liteapi/client.go | 35 +++++++++++-------- .../chains/ton/liteapi/client_live_test.go | 2 +- zetaclient/chains/ton/observer/inbound.go | 7 ++-- .../chains/ton/observer/inbound_test.go | 10 +++--- zetaclient/chains/ton/observer/observer.go | 2 +- .../chains/ton/observer/observer_test.go | 4 +-- zetaclient/orchestrator/bootstrap.go | 2 +- zetaclient/testutils/mocks/ton_liteclient.go | 6 ++-- 10 files changed, 39 insertions(+), 34 deletions(-) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 819e9ba191..3cb8650714 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -94,7 +94,7 @@ func (chain Chain) IsBitcoinChain() bool { } func (chain Chain) IsTONChain() bool { - return IsTONChain(chain.ChainId, []Chain{chain}) + return chain.Consensus == Consensus_catchain_consensus } // DecodeAddressFromChainID decode the address string to bytes diff --git a/zetaclient/chains/ton/config.go b/zetaclient/chains/ton/config.go index a8578936a3..731287756e 100644 --- a/zetaclient/chains/ton/config.go +++ b/zetaclient/chains/ton/config.go @@ -44,7 +44,8 @@ func ConfigFromPath(path string) (*GlobalConfigurationFile, error) { return config.ParseConfigFile(path) } -func ConfigFromAny(ctx context.Context, urlOrPath string) (*GlobalConfigurationFile, error) { +// ConfigFromSource returns a parsed configuration file from a URL or a file path. +func ConfigFromSource(ctx context.Context, urlOrPath string) (*GlobalConfigurationFile, error) { if u, err := url.Parse(urlOrPath); err == nil { return ConfigFromURL(ctx, u.String()) } diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index ca79f462d9..25b0efcf39 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -3,6 +3,7 @@ package liteapi import ( "context" "fmt" + "slices" "strconv" "strings" @@ -34,9 +35,9 @@ func New(client *liteapi.Client) *Client { return &Client{Client: client, blockCache: blockCache} } -// NewFromAny creates a new client from a URL or a file path. -func NewFromAny(ctx context.Context, urlOrPath string) (*Client, error) { - cfg, err := zetaton.ConfigFromAny(ctx, urlOrPath) +// NewFromSource creates a new client from a URL or a file path. +func NewFromSource(ctx context.Context, urlOrPath string) (*Client, error) { + cfg, err := zetaton.ConfigFromSource(ctx, urlOrPath) if err != nil { return nil, errors.Wrap(err, "unable to get config") } @@ -94,7 +95,7 @@ func (c *Client) setBlockHeaderCache(blockID ton.BlockIDExt, header tlb.BlockInf // Note that it might fail w/o using an archival node. Also returns the number of // scrolled transactions for this account i.e. total transactions func (c *Client) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*ton.Transaction, int, error) { - lt, hash, err := c.getLastTranHash(ctx, acc) + lt, hash, err := c.getLastTransactionHash(ctx, acc) if err != nil { return nil, 0, err } @@ -132,16 +133,15 @@ func (c *Client) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*t return tx, scrolled, nil } -// GetTransactionsUntil returns all transactions in a range of (from,to] where from is "lt && hash". -// - oldestLT && oldestHash tx is EXCLUDED from the result. -// - ordered by DESC -func (c *Client) GetTransactionsUntil( +// GetTransactionsSince returns all account transactions since the given logicalTime and hash (exclusive). +// The result is ordered from oldest to newest. Used to detect new txs to observe. +func (c *Client) GetTransactionsSince( ctx context.Context, acc ton.AccountID, oldestLT uint64, oldestHash ton.Bits256, ) ([]ton.Transaction, error) { - lt, hash, err := c.getLastTranHash(ctx, acc) + lt, hash, err := c.getLastTransactionHash(ctx, acc) if err != nil { return nil, err } @@ -151,6 +151,8 @@ func (c *Client) GetTransactionsUntil( for { hashBits := ton.Bits256(hash) + // note that ton liteapi works in the reverse order. + // Here we go from the LATEST txs to the oldest at N txs per page txs, err := c.GetTransactions(ctx, pageSize, acc, lt, hashBits) if err != nil { return nil, errors.Wrapf(err, "unable to get transactions [lt %d, hash %s]", lt, hashBits.Hex()) @@ -181,11 +183,14 @@ func (c *Client) GetTransactionsUntil( lt, hash = txs[oldestIndex].PrevTransLt, txs[oldestIndex].PrevTransHash } + // reverse the result to get the oldest tx first + slices.Reverse(result) + return result, nil } -// getLastTranHash returns logical time and hash of the last transaction -func (c *Client) getLastTranHash(ctx context.Context, acc ton.AccountID) (uint64, tlb.Bits256, error) { +// getLastTransactionHash returns logical time and hash of the last transaction +func (c *Client) getLastTransactionHash(ctx context.Context, acc ton.AccountID) (uint64, tlb.Bits256, error) { state, err := c.GetAccountState(ctx, acc) if err != nil { return 0, tlb.Bits256{}, errors.Wrap(err, "unable to get account state") @@ -198,14 +203,16 @@ func (c *Client) getLastTranHash(ctx context.Context, acc ton.AccountID) (uint64 return state.LastTransLt, state.LastTransHash, nil } +// TransactionHashToString converts logicalTime and hash to string func TransactionHashToString(lt uint64, hash ton.Bits256) string { return fmt.Sprintf("%d:%s", lt, hash.Hex()) } -func TransactionHashFromString(hash string) (uint64, ton.Bits256, error) { - parts := strings.Split(hash, ":") +// TransactionHashFromString parses encoded string into logicalTime and hash +func TransactionHashFromString(encoded string) (uint64, ton.Bits256, error) { + parts := strings.Split(encoded, ":") if len(parts) != 2 { - return 0, ton.Bits256{}, fmt.Errorf("invalid hash string format") + return 0, ton.Bits256{}, fmt.Errorf("invalid encoded string format") } lt, err := strconv.ParseUint(parts[0], 10, 64) diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index acf211736b..ed3c850dd8 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -92,7 +92,7 @@ func TestClient(t *testing.T) { // ACT // https://tonviewer.com/UQCVlMcZ7EyV9maDsvscoLCd5KQfb7CHukyNJluWpMzlD0vr?section=transactions - txs, err := client.GetTransactionsUntil(ctx, accountID, getUntilLT, hash) + txs, err := client.GetTransactionsSince(ctx, accountID, getUntilLT, hash) finish := time.Since(start) diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 99b7083dac..4436b02369 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -4,7 +4,6 @@ import ( "context" "encoding/hex" "fmt" - "slices" "cosmossdk.io/math" "github.com/pkg/errors" @@ -68,19 +67,17 @@ func (ob *Observer) observeInbound(ctx context.Context) error { return errors.Wrap(err, "unable to ensure last scanned tx") } + // extract logicalTime and tx hash from last scanned tx lt, hashBits, err := liteapi.TransactionHashFromString(ob.LastTxScanned()) if err != nil { return errors.Wrapf(err, "unable to parse last scanned tx %q", ob.LastTxScanned()) } - txs, err := ob.client.GetTransactionsUntil(ctx, ob.gateway.AccountID(), lt, hashBits) + txs, err := ob.client.GetTransactionsSince(ctx, ob.gateway.AccountID(), lt, hashBits) if err != nil { return errors.Wrap(err, "unable to get transactions") } - // Process from oldest to latest (ASC) - slices.Reverse(txs) - switch { case len(txs) == 0: // noop diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go index 9aee03ddab..e6208756ac 100644 --- a/zetaclient/chains/ton/observer/inbound_test.go +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -51,7 +51,7 @@ func TestInbound(t *testing.T) { }) ts.OnGetFirstTransaction(gw.AccountID(), &firstTX, 0, nil).Once() - ts.OnGetTransactionsUntil(gw.AccountID(), firstTX.Lt, txHash(firstTX), nil, nil).Once() + ts.OnGetTransactionsSince(gw.AccountID(), firstTX.Lt, txHash(firstTX), nil, nil).Once() // Given observer ob, err := New(ts.baseObserver, ts.liteClient, gw) @@ -95,7 +95,7 @@ func TestInbound(t *testing.T) { txs := []ton.Transaction{donation} ts. - OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() // ACT @@ -133,7 +133,7 @@ func TestInbound(t *testing.T) { txs := []ton.Transaction{depositTX} ts. - OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() ts.MockGetBlockHeader(depositTX.BlockID) @@ -195,7 +195,7 @@ func TestInbound(t *testing.T) { txs := []ton.Transaction{depositAndCallTX} ts. - OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() ts.MockGetBlockHeader(depositAndCallTX.BlockID) @@ -281,7 +281,7 @@ func TestInbound(t *testing.T) { } ts. - OnGetTransactionsUntil(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() for _, tx := range txs { diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index acdda42b46..c5fd26aad3 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -27,7 +27,7 @@ type Observer struct { //go:generate mockery --name LiteClient --filename ton_liteclient.go --case underscore --output ../../../testutils/mocks type LiteClient interface { GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) - GetTransactionsUntil(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) + GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) } diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 3237833920..8bfcf2bfab 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -119,7 +119,7 @@ func (ts *testSuite) OnGetFirstTransaction(acc ton.AccountID, tx *ton.Transactio Return(tx, scanned, err) } -func (ts *testSuite) OnGetTransactionsUntil( +func (ts *testSuite) OnGetTransactionsSince( acc ton.AccountID, lt uint64, hash ton.Bits256, @@ -127,7 +127,7 @@ func (ts *testSuite) OnGetTransactionsUntil( err error, ) *mock.Call { return ts.liteClient. - On("GetTransactionsUntil", mock.Anything, acc, lt, hash). + On("GetTransactionsSince", mock.Anything, acc, lt, hash). Return(txs, err) } diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 497d5587d5..34c94bf77d 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -435,7 +435,7 @@ func syncObserverMap( continue } - tonClient, err := liteapi.NewFromAny(ctx, cfg.LiteClientConfigURL) + tonClient, err := liteapi.NewFromSource(ctx, cfg.LiteClientConfigURL) if err != nil { logger.Std.Error().Err(err).Msgf("Unable to create TON liteapi for chain %d", chainID) continue diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go index 573d2d600e..f11ccaf24c 100644 --- a/zetaclient/testutils/mocks/ton_liteclient.go +++ b/zetaclient/testutils/mocks/ton_liteclient.go @@ -82,12 +82,12 @@ func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) return r0, r1, r2 } -// GetTransactionsUntil provides a mock function with given fields: ctx, acc, lt, bits -func (_m *LiteClient) GetTransactionsUntil(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) { +// GetTransactionsSince provides a mock function with given fields: ctx, acc, lt, bits +func (_m *LiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) { ret := _m.Called(ctx, acc, lt, bits) if len(ret) == 0 { - panic("no return value specified for GetTransactionsUntil") + panic("no return value specified for GetTransactionsSince") } var r0 []ton.Transaction From b313ded4b5ec2c8631aea15331f76e484de55449 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:41:31 +0200 Subject: [PATCH 32/36] Fix comments --- zetaclient/chains/ton/observer/inbound.go | 8 ++++++-- zetaclient/chains/ton/observer/observer.go | 10 ++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 4436b02369..36f08af46e 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -132,8 +132,8 @@ func (ob *Observer) voteInbound(ctx context.Context, tx *toncontracts.Transactio return "", nil } - // todo add compliance check - // https://github.com/zeta-chain/node/issues/2916 + // TODO: Add compliance check + // https://github.com/zeta-chain/node/issues/2916 blockHeader, err := ob.client.GetBlockHeader(ctx, tx.BlockID, 0) if err != nil { @@ -150,6 +150,7 @@ func (ob *Observer) voteInbound(ctx context.Context, tx *toncontracts.Transactio return ob.voteDeposit(ctx, tx, sender, amount, memo, seqno) } +// extractInboundData parses Gateway tx into deposit (TON sender, amount, memo) func extractInboundData(tx *toncontracts.Transaction) (string, math.Uint, []byte, error) { if tx.Operation == toncontracts.OpDeposit { d, err := tx.Deposit() @@ -193,6 +194,9 @@ func (ob *Observer) voteDeposit( inboundHash = liteapi.TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) ) + // TODO: use protocol contract v2 for deposit + // https://github.com/zeta-chain/node/issues/2967 + msg := zetacore.GetInboundVoteMessage( sender, ob.Chain().ChainId, diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index c5fd26aad3..e20742116a 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -68,13 +68,11 @@ func (ob *Observer) Start(ctx context.Context) { // watch for incoming txs and post votes to zetacore bg.Work(ctx, ob.watchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) - // todo - // watchInboundTracker https://github.com/zeta-chain/node/issues/2935 + // TODO: watchInboundTracker + // https://github.com/zeta-chain/node/issues/2935 - // todo outbounds/withdrawals https://github.com/zeta-chain/node/issues/2807 - // watchOutbound - // watchGasPrice - // watchRPCStatus + // TODO: outbounds/withdrawals: (watchOutbound, watchGasPrice, watchRPCStatus) + // https://github.com/zeta-chain/node/issues/2807 } func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossChainTx) (bool, error) { From e0218c80455976e31869fc03123666e3f3629b46 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:43:57 +0200 Subject: [PATCH 33/36] Update zetaclient/chains/base/observer.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/base/observer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 83ecbc4b72..f089f26815 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -135,7 +135,7 @@ func NewObserver( return &ob, nil } -// Start starts the observer. Returns false started (noop). +// Start starts the observer. Returns false if it's already started (noop). func (ob *Observer) Start() bool { ob.mu.Lock() defer ob.Mu().Unlock() From cfd4728737839f31b68201a19e4888ba956ea718 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:23:27 +0200 Subject: [PATCH 34/36] Add gw explanatory comments; address PR comments --- e2e/e2etests/test_ton_deposit.go | 2 +- e2e/e2etests/test_ton_deposit_and_call.go | 2 +- pkg/contracts/ton/gateway.go | 15 ++++++++++++++- zetaclient/chains/ton/observer/inbound.go | 11 +++++------ zetaclient/chains/ton/observer/inbound_test.go | 7 +++++++ 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index b4c7a32168..73860629df 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -54,7 +54,7 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { require.Equal(r, sender.GetAddress().ToRaw(), cctx.InboundParams.Sender) require.Equal(r, expectedDeposit.Uint64(), cctx.InboundParams.Amount.Uint64()) - // Check sender's balance + // Check receiver's balance balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, recipient) require.NoError(r, err) diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go index b385b19f49..43e5dcc4e0 100644 --- a/e2e/e2etests/test_ton_deposit_and_call.go +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -59,7 +59,7 @@ func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { // check if example contract has been called, bar value should be set to amount utils.MustHaveCalledExampleContract(r, contract, expectedDeposit.BigInt()) - // Check sender's balance + // Check receiver's balance balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) require.NoError(r, err) diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index 4a85925b9e..33ca2c7977 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -1,3 +1,4 @@ +// Package ton provider bindings for TON blockchain including Gateway contract wrapper. package ton import ( @@ -8,7 +9,17 @@ import ( "github.com/tonkeeper/tongo/ton" ) -// Gateway wrapper around zeta gateway contract on TON +// Gateway represents bindings for Zeta Gateway contract on TON +// +// Gateway.ParseTransaction parses Gateway transaction. +// The parser reads tx body cell and decodes it based on Operation code (op) +// - inbound transactions: deposit, donate, depositAndCall +// - outbound transactions: not implemented yet +// - errors for all other transactions +// +// `Send*` methods work the same way by constructing (& signing) tx body cell that is expected by the contract +// +// @see https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc type Gateway struct { accountID ton.AccountID } @@ -24,10 +35,12 @@ var ( ErrCast = errors.New("unable to cast tx content") ) +// NewGateway Gateway constructor func NewGateway(accountID ton.AccountID) *Gateway { return &Gateway{accountID} } +// AccountID returns gateway address func (gw *Gateway) AccountID() ton.AccountID { return gw.accountID } diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 36f08af46e..95f9a510d7 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -152,25 +152,24 @@ func (ob *Observer) voteInbound(ctx context.Context, tx *toncontracts.Transactio // extractInboundData parses Gateway tx into deposit (TON sender, amount, memo) func extractInboundData(tx *toncontracts.Transaction) (string, math.Uint, []byte, error) { - if tx.Operation == toncontracts.OpDeposit { + switch tx.Operation { + case toncontracts.OpDeposit: d, err := tx.Deposit() if err != nil { return "", math.NewUint(0), nil, err } return d.Sender.ToRaw(), d.Amount, d.Memo(), nil - } - - if tx.Operation == toncontracts.OpDepositAndCall { + case toncontracts.OpDepositAndCall: d, err := tx.DepositAndCall() if err != nil { return "", math.NewUint(0), nil, err } return d.Sender.ToRaw(), d.Amount, d.Memo(), nil + default: + return "", math.NewUint(0), nil, fmt.Errorf("unknown operation %d", tx.Operation) } - - return "", math.NewUint(0), nil, fmt.Errorf("unknown operation %d", tx.Operation) } func (ob *Observer) voteDeposit( diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go index e6208756ac..281db44f79 100644 --- a/zetaclient/chains/ton/observer/inbound_test.go +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -19,6 +19,13 @@ func TestInbound(t *testing.T) { ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b"), ) + t.Run("No gateway provided", func(t *testing.T) { + ts := newTestSuite(t) + + _, err := New(ts.baseObserver, ts.liteClient, nil) + require.Error(t, err) + }) + t.Run("Ensure last scanned tx", func(t *testing.T) { t.Run("Unable to get first tx", func(t *testing.T) { // ARRANGE From c43ae26492099eb3997bcd1ea02f31a29ce0cb8d Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:52:48 +0200 Subject: [PATCH 35/36] Apply fixes for AI suggestions --- pkg/chains/chain.go | 9 ++++++++- pkg/chains/chain_test.go | 7 +++++++ pkg/contracts/ton/testdata/readme.md | 2 +- pkg/contracts/ton/testdata/scraper.go | 6 +++--- zetaclient/chains/ton/observer/observer_test.go | 4 +--- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 3cb8650714..665251eb39 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/tonkeeper/tongo/ton" ) // Validate checks whether the chain is valid @@ -109,7 +110,13 @@ func DecodeAddressFromChainID(chainID int64, addr string, additionalChains []Cha case IsSolanaChain(chainID, additionalChains): return []byte(addr), nil case IsTONChain(chainID, additionalChains): - return []byte(addr), nil + // e.g. `0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a1` + acc, err := ton.ParseAccountID(addr) + if err != nil { + return nil, fmt.Errorf("invalid TON address %q: %w", addr, err) + } + + return []byte(acc.ToRaw()), nil default: return nil, fmt.Errorf("chain (%d) not supported", chainID) } diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index c7cfc80d89..4da81c7eee 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -307,6 +307,13 @@ func TestDecodeAddressFromChainID(t *testing.T) { addr: "0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a1", want: []byte("0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a1"), }, + { + name: "TON", + chainID: chains.TONMainnet.ChainId, + // human friendly address should be always represented in raw format + addr: "EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt", + want: []byte("0:779dcc815138d9500e449c5291e7f12738c23d575b5310000f6a253bd607384e"), + }, { name: "Non-supported chain", chainID: 9999, diff --git a/pkg/contracts/ton/testdata/readme.md b/pkg/contracts/ton/testdata/readme.md index af8a878904..8695f06982 100644 --- a/pkg/contracts/ton/testdata/readme.md +++ b/pkg/contracts/ton/testdata/readme.md @@ -20,7 +20,7 @@ Returns { "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04", - "description": "todo", + "description": "Lorem Ipsum", "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", "logicalTime": 26023788000003, "test": true diff --git a/pkg/contracts/ton/testdata/scraper.go b/pkg/contracts/ton/testdata/scraper.go index e75f1bba23..efe76ca3aa 100644 --- a/pkg/contracts/ton/testdata/scraper.go +++ b/pkg/contracts/ton/testdata/scraper.go @@ -21,7 +21,7 @@ func main() { flag.Parse() if len(flag.Args()) < 3 { - log.Fatalf("Usage: go run scrape.go [-testnet] ") + log.Fatalf("Usage: go run scraper.go [-testnet] ") } // Parse account @@ -53,7 +53,7 @@ func main() { case len(txs) == 0: fail("Not found") case len(txs) > 1: - fail("invalid tx list length (got %d, want 1); lt %d, hash %s", len(txs), hash.Hex()) + fail("invalid tx list length (got %d, want 1); lt %d, hash %s", len(txs), lt, hash.Hex()) } // Print the transaction @@ -83,7 +83,7 @@ func getClient(testnet bool) *liteapi.Client { return c } - c, err := liteapi.NewClientWithDefaultTestnet() + c, err := liteapi.NewClientWithDefaultMainnet() must(err, "unable to create mainnet lite client") return c diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 8bfcf2bfab..38c032eb4a 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -160,9 +160,7 @@ func setupVotesBag(ts *testSuite) { catcher := func(args mock.Arguments) { vote := args.Get(3) cctx, ok := vote.(*cctxtypes.MsgVoteInbound) - if !ok { - panic("unexpected cctx type") - } + require.True(ts.t, ok, "unexpected cctx type") ts.votesBag = append(ts.votesBag, cctx) } From 126f96622b85cefa9028ec579de9db51aec5b6f4 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:03:33 +0200 Subject: [PATCH 36/36] Fix upgrade tests --- e2e/runner/setup_zeta.go | 13 +++++++++---- e2e/runner/zeta.go | 12 ++++++++++++ e2e/txserver/zeta_tx_server.go | 10 +++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go index 24c4918e13..1d1db47108 100644 --- a/e2e/runner/setup_zeta.go +++ b/e2e/runner/setup_zeta.go @@ -179,6 +179,7 @@ func (r *E2ERunner) SetZEVMZRC20s() { e2eutils.OperationalPolicyName, e2eutils.AdminPolicyName, r.ERC20Addr.Hex(), + r.skipChainOperations, ) require.NoError(r, err) @@ -245,10 +246,14 @@ func (r *E2ERunner) SetupSOLZRC20() { // SetupTONZRC20 sets up the TON ZRC20 in the runner from the values queried from the chain func (r *E2ERunner) SetupTONZRC20() { - TONZRC20Addr, err := r.SystemContract.GasCoinZRC20ByChainId( - &bind.CallOpts{}, - big.NewInt(chains.TONLocalnet.ChainId), - ) + chainID := chains.TONLocalnet.ChainId + + // noop + if r.skipChainOperations(chainID) { + return + } + + TONZRC20Addr, err := r.SystemContract.GasCoinZRC20ByChainId(&bind.CallOpts{}, big.NewInt(chainID)) require.NoError(r, err) r.TONZRC20Addr = TONZRC20Addr diff --git a/e2e/runner/zeta.go b/e2e/runner/zeta.go index 5b977a9a31..1df7e676e3 100644 --- a/e2e/runner/zeta.go +++ b/e2e/runner/zeta.go @@ -14,6 +14,7 @@ import ( connectorzevm "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/zevm/zetaconnectorzevm.sol" "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/retry" "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" @@ -315,3 +316,14 @@ func (r *E2ERunner) WithdrawERC20(amount *big.Int) *ethtypes.Transaction { return tx } + +// skipChainOperations checks if the chain operations should be skipped for E2E +func (r *E2ERunner) skipChainOperations(chainID int64) bool { + skip := r.IsRunningUpgrade() && chains.IsTONChain(chainID, nil) + + if skip { + r.Logger.Print("Skipping chain operations for chain %d", chainID) + } + + return skip +} diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 47f7cf37e6..a79c5af098 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -406,7 +406,10 @@ func (zts ZetaTxServer) DeploySystemContracts( // DeployZRC20s deploys the ZRC20 contracts // returns the addresses of erc20 zrc20 -func (zts ZetaTxServer) DeployZRC20s(accountOperational, accountAdmin, erc20Addr string) (string, error) { +func (zts ZetaTxServer) DeployZRC20s( + accountOperational, accountAdmin, erc20Addr string, + skipChain func(chainID int64) bool, +) (string, error) { // retrieve account accOperational, err := zts.clientCtx.Keyring.Key(accountOperational) if err != nil { @@ -440,6 +443,11 @@ func (zts ZetaTxServer) DeployZRC20s(accountOperational, accountAdmin, erc20Addr } deploy := func(msg *fungibletypes.MsgDeployFungibleCoinZRC20) (string, error) { + // noop + if skipChain(msg.ForeignChainId) { + return "", nil + } + res, err := zts.BroadcastTx(deployerAccount, msg) if err != nil { return "", fmt.Errorf("failed to deploy eth zrc20: %w", err)