diff --git a/Makefile b/Makefile
index 36e414f012..7b01eafb41 100644
--- a/Makefile
+++ b/Makefile
@@ -296,15 +296,24 @@ start-v2-test: zetanode
###############################################################################
# build from source only if requested
+# NODE_VERSION and NODE_COMMIT must be set as old-runtime depends on lastest-runtime
ifdef UPGRADE_TEST_FROM_SOURCE
zetanode-upgrade: zetanode
@echo "Building zetanode-upgrade from source"
- $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime-source --build-arg OLD_VERSION='release/v20' .
+ $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime-source \
+ --build-arg OLD_VERSION='release/v20' \
+ --build-arg NODE_VERSION=$(NODE_VERSION) \
+ --build-arg NODE_COMMIT=$(NODE_COMMIT)
+ .
.PHONY: zetanode-upgrade
else
zetanode-upgrade: zetanode
@echo "Building zetanode-upgrade from binaries"
- $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v20.0.2' .
+ $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime \
+ --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v20.0.2' \
+ --build-arg NODE_VERSION=$(NODE_VERSION) \
+ --build-arg NODE_COMMIT=$(NODE_COMMIT) \
+ .
.PHONY: zetanode-upgrade
endif
diff --git a/changelog.md b/changelog.md
index 5de0dbd846..e25f46e129 100644
--- a/changelog.md
+++ b/changelog.md
@@ -17,6 +17,7 @@
* [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
* [2957](https://github.com/zeta-chain/node/pull/2957) - enable Bitcoin inscription support on testnet
+* [2896](https://github.com/zeta-chain/node/pull/2896) - add TON inbound observation
### Refactor
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/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go
index 4a7fbef39c..23a7f6b866 100644
--- a/cmd/zetae2e/local/local.go
+++ b/cmd/zetae2e/local/local.go
@@ -407,6 +407,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
tonTests := []string{
e2etests.TestTONDepositName,
+ e2etests.TestTONDepositAndCallName,
}
eg.Go(tonTestRoutine(conf, deployerRunner, verbose, tonTests...))
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/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml
index 461b8566cc..dfa9349794 100644
--- a/docs/openapi/openapi.swagger.yaml
+++ b/docs/openapi/openapi.swagger.yaml
@@ -56998,7 +56998,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
@@ -57014,6 +57016,7 @@ definitions:
- optimism
- base
- solana
+ - ton
default: eth
title: |-
Network represents the network of the chain
@@ -57048,6 +57051,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/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/e2etests.go b/e2e/e2etests/e2etests.go
index 004271c696..2d6eea998f 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
@@ -445,10 +446,18 @@ 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,
),
+ 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 e3ab963354..64f0920c2a 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/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -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_eth_deposit_call.go b/e2e/e2etests/test_eth_deposit_call.go
index 46805f9f81..9484888cf3 100644
--- a/e2e/e2etests/test_eth_deposit_call.go
+++ b/e2e/e2etests/test_eth_deposit_call.go
@@ -88,5 +88,10 @@ func TestEtherDepositAndCall(r *runner.E2ERunner, args []string) {
r.Logger.Info("Cross-chain call to reverter reverted")
// Check the error carries the revert executed.
- require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed")
+ // tolerate the error in both the ErrorMessage field and the StatusMessage field
+ if cctx.CctxStatus.ErrorMessage != "" {
+ require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed")
+ } else {
+ require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo)
+ }
}
diff --git a/e2e/e2etests/test_solana_deposit_refund.go b/e2e/e2etests/test_solana_deposit_refund.go
index 3176edee78..0a62b70ac6 100644
--- a/e2e/e2etests/test_solana_deposit_refund.go
+++ b/e2e/e2etests/test_solana_deposit_refund.go
@@ -32,5 +32,10 @@ func TestSolanaDepositAndCallRefund(r *runner.E2ERunner, args []string) {
utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted)
// Check the error carries the revert executed.
- require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed")
+ // tolerate the error in both the ErrorMessage field and the StatusMessage field
+ if cctx.CctxStatus.ErrorMessage != "" {
+ require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed")
+ } else {
+ require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo)
+ }
}
diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go
index a2e8df09d0..73860629df 100644
--- a/e2e/e2etests/test_ton_deposit.go
+++ b/e2e/e2etests/test_ton_deposit.go
@@ -1,42 +1,64 @@
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"
+ "github.com/zeta-chain/node/testutil/sample"
+ cctypes "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 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, 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 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
+ err = r.TONDeposit(sender, amount, recipient)
+
+ // ASSERT
require.NoError(r, err)
- // Check sender balance
- sb, err := sender.GetBalance(ctx)
+ // Wait for CCTX mining
+ filter := func(cctx *cctypes.CrossChainTx) bool {
+ return cctx.InboundParams.SenderChainId == chain.ChainId &&
+ cctx.InboundParams.Sender == sender.GetAddress().ToRaw()
+ }
+
+ cctx := r.WaitForSpecificCCTX(filter, time.Minute)
+
+ // Check CCTX
+ expectedDeposit := amount.Sub(depositFee)
+
+ require.Equal(r, sender.GetAddress().ToRaw(), cctx.InboundParams.Sender)
+ require.Equal(r, expectedDeposit.Uint64(), cctx.InboundParams.Amount.Uint64())
+
+ // Check receiver's balance
+ balance, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, recipient)
require.NoError(r, err)
- senderBalance := math.NewUint(sb)
+ r.Logger.Info("Recipient's zEVM TON balance after deposit: %d", balance.Uint64())
- // 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),
- )
+ require.Equal(r, expectedDeposit.Uint64(), balance.Uint64())
}
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..43e5dcc4e0
--- /dev/null
+++ b/e2e/e2etests/test_ton_deposit_and_call.go
@@ -0,0 +1,69 @@
+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"
+ testcontract "github.com/zeta-chain/node/testutil/contracts"
+ cctypes "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 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
+ err = r.TONDepositAndCall(sender, amount, contractAddr, callData)
+
+ // ASSERT
+ require.NoError(r, err)
+
+ // Wait for CCTX mining
+ filter := func(cctx *cctypes.CrossChainTx) bool {
+ return cctx.InboundParams.SenderChainId == chain.ChainId &&
+ cctx.InboundParams.Sender == sender.GetAddress().ToRaw()
+ }
+
+ r.WaitForSpecificCCTX(filter, time.Minute)
+
+ 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 receiver'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/runner.go b/e2e/runner/runner.go
index 57e5ab9a24..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.
@@ -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
@@ -230,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
@@ -275,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
@@ -359,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/runner/setup_ton.go b/e2e/runner/setup_ton.go
index 9b744401ad..62d89c3329 100644
--- a/e2e/runner/setup_ton.go
+++ b/e2e/runner/setup_ton.go
@@ -2,10 +2,16 @@ 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"
+ toncontracts "github.com/zeta-chain/node/pkg/contracts/ton"
+ observertypes "github.com/zeta-chain/node/x/observer/types"
)
// SetupTON setups TON deployer and deploys Gateway contract
@@ -42,7 +48,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)
@@ -55,7 +66,58 @@ func (r *E2ERunner) SetupTON() error {
}
r.TONDeployer = deployer
- r.TONGateway = gwAccount.ID
+ r.TONGateway = toncontracts.NewGateway(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/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go
index ffab5db811..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)
@@ -191,6 +192,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 +244,27 @@ 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() {
+ 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
+ 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/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/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/e2e/runner/zeta.go b/e2e/runner/zeta.go
index bdada4763c..1df7e676e3 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"
@@ -13,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"
@@ -96,12 +98,47 @@ 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
+}
+
+// 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 {
+ var (
+ 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, reqQuery)
+ require.NoError(r, err)
+
+ for i := range res.CrossChainTx {
+ tx := res.CrossChainTx[i]
+ if filter(tx) {
+ return r.WaitForMinedCCTXFromIndex(tx.Index)
+ }
+ }
+
+ 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
@@ -279,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 9ba7e5315a..a79c5af098 100644
--- a/e2e/txserver/zeta_tx_server.go
+++ b/e2e/txserver/zeta_tx_server.go
@@ -408,6 +408,7 @@ func (zts ZetaTxServer) DeploySystemContracts(
// returns the addresses of erc20 zrc20
func (zts ZetaTxServer) DeployZRC20s(
accountOperational, accountAdmin, erc20Addr string,
+ skipChain func(chainID int64) bool,
) (string, error) {
// retrieve account
accOperational, err := zts.clientCtx.Keyring.Key(accountOperational)
@@ -441,8 +442,31 @@ func (zts ZetaTxServer) DeployZRC20s(
deployerAddr = addrOperational.String()
}
+ 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)
+ }
+
+ 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 +479,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 +494,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 +509,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: %w", err)
}
// deploy erc20 zrc20
- res, err = zts.BroadcastTx(deployerAccount, fungibletypes.NewMsgDeployFungibleCoinZRC20(
+ erc20zrc20Addr, err := deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20(
deployerAddr,
erc20Addr,
chains.GoerliLocalnet.ChainId,
@@ -522,15 +540,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
}
diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go
index f5f6ee605d..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
@@ -93,6 +94,10 @@ func (chain Chain) IsBitcoinChain() bool {
return chain.Consensus == Consensus_bitcoin
}
+func (chain Chain) IsTONChain() bool {
+ return chain.Consensus == Consensus_catchain_consensus
+}
+
// 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
@@ -104,6 +109,14 @@ func DecodeAddressFromChainID(chainID int64, addr string, additionalChains []Cha
return []byte(addr), nil
case IsSolanaChain(chainID, additionalChains):
return []byte(addr), nil
+ case IsTONChain(chainID, additionalChains):
+ // 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)
}
@@ -132,6 +145,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
diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go
index 349651e7b9..4da81c7eee 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",
@@ -301,6 +301,19 @@ func TestDecodeAddressFromChainID(t *testing.T) {
addr: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw",
want: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"),
},
+ {
+ name: "TON",
+ chainID: chains.TONMainnet.ChainId,
+ 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/chains/chains.go b/pkg/chains/chains.go
index addd6a14df..70692ccc9c 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
*/
@@ -249,6 +261,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
*/
@@ -325,6 +348,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
*/
@@ -392,6 +426,9 @@ func DefaultChainsList() []Chain {
SolanaMainnet,
SolanaDevnet,
SolanaLocalnet,
+ TONMainnet,
+ TONTestnet,
+ TONLocalnet,
}
}
@@ -449,17 +486,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 90545bee1c..73c3521ffa 100644
--- a/pkg/chains/chains_test.go
+++ b/pkg/chains/chains_test.go
@@ -36,6 +36,7 @@ func TestChainListByNetworkType(t *testing.T) {
chains.OptimismMainnet,
chains.BaseMainnet,
chains.SolanaMainnet,
+ chains.TONMainnet,
},
},
{
@@ -54,6 +55,7 @@ func TestChainListByNetworkType(t *testing.T) {
chains.OptimismSepolia,
chains.BaseSepolia,
chains.SolanaDevnet,
+ chains.TONTestnet,
},
},
{
@@ -64,6 +66,7 @@ func TestChainListByNetworkType(t *testing.T) {
chains.BitcoinRegtest,
chains.GoerliLocalnet,
chains.SolanaLocalnet,
+ chains.TONLocalnet,
},
},
}
@@ -132,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 {
@@ -168,6 +176,9 @@ func TestDefaultChainList(t *testing.T) {
chains.SolanaMainnet,
chains.SolanaDevnet,
chains.SolanaLocalnet,
+ chains.TONMainnet,
+ chains.TONTestnet,
+ chains.TONLocalnet,
}, chains.DefaultChainsList())
}
@@ -202,6 +213,9 @@ func TestChainListByGateway(t *testing.T) {
chains.SolanaMainnet,
chains.SolanaDevnet,
chains.SolanaLocalnet,
+ chains.TONMainnet,
+ chains.TONTestnet,
+ chains.TONLocalnet,
},
},
{
@@ -246,6 +260,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/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go
new file mode 100644
index 0000000000..33ca2c7977
--- /dev/null
+++ b/pkg/contracts/ton/gateway.go
@@ -0,0 +1,258 @@
+// Package ton provider bindings for TON blockchain including Gateway contract wrapper.
+package ton
+
+import (
+ "cosmossdk.io/math"
+ "github.com/pkg/errors"
+ "github.com/tonkeeper/tongo/boc"
+ "github.com/tonkeeper/tongo/tlb"
+ "github.com/tonkeeper/tongo/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
+}
+
+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")
+)
+
+// 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
+}
+
+// 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
+ return nil, errors.Wrapf(ErrParse, "tx %s is not successful (exit code %d)", tx.Hash().Hex(), exitCode)
+ }
+
+ 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
+}
+
+// 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
+}
+
+// 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)
+ 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: GramsToUint(amount)}
+ case OpDeposit:
+ content, errContent = parseDeposit(tx, sender, body)
+ 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())
+ }
+
+ 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 := UnmarshalEVMAddress(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();
+
+ 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 {
+ 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: GramsToUint(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")
+ }
+
+ callData, err := UnmarshalSnakeCell(callDataCell)
+ if err != nil {
+ return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data")
+ }
+
+ return DepositAndCall{Deposit: deposit, CallData: callData}, 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")
+ }
+
+ var (
+ inMsg = tx.Msgs.InMsg.Value.Value
+ body = boc.Cell(inMsg.Body.Value)
+ )
+
+ return &body, nil
+}
diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go
new file mode 100644
index 0000000000..7d711ab89c
--- /dev/null
+++ b/pkg/contracts/ton/gateway_op.go
@@ -0,0 +1,115 @@
+package ton
+
+import (
+ "errors"
+
+ "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()
+
+ return b, writeDepositBody(b, d.Recipient)
+}
+
+// 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) {
+ b := boc.NewCell()
+
+ return b, writeDepositAndCallBody(b, d.Recipient, d.CallData)
+}
+
+func writeDepositBody(b *boc.Cell, recipient eth.Address) error {
+ return ErrCollect(
+ b.WriteUint(uint64(OpDeposit), sizeOpCode),
+ b.WriteUint(0, sizeQueryID),
+ 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
new file mode 100644
index 0000000000..5dd9c21340
--- /dev/null
+++ b/pkg/contracts/ton/gateway_send.go
@@ -0,0 +1,72 @@
+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
+}
+
+// see https://docs.ton.org/develop/smart-contracts/messages#message-modes
+const (
+ SendFlagSeparateFees = uint8(1)
+ 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)
+}
+
+// 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")
+ }
+
+ return s.Send(ctx, wallet.Message{
+ Amount: tlb.Coins(amount.Uint64()),
+ Address: gw.accountID,
+ Body: body,
+ Mode: sendMode,
+ })
+}
diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go
new file mode 100644
index 0000000000..dc680761ff
--- /dev/null
+++ b/pkg/contracts/ton/gateway_test.go
@@ -0,0 +1,313 @@
+package ton
+
+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"
+ "github.com/tonkeeper/tongo/tlb"
+ "github.com/tonkeeper/tongo/ton"
+)
+
+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
+ 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()))
+
+ // 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) {
+ // 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
+ 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)
+
+ // 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) {
+ // 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)
+
+ // 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) {
+ 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)
+
+ // 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 TestFiltering(t *testing.T) {
+ t.Run("Inbound", func(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ skip bool
+ error bool
+ }{
+ // Should be parsed and filtered
+ {"00-donation", false, false},
+ {"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, FilterInbounds)
+
+ 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")
+
+ // ASSERT
+ require.Equal(t, uint64(26023788000003), tx.Lt)
+ 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 := MarshalSnakeCell(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
+
+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, fixture) {
+ t.Helper()
+
+ var (
+ filename = fmt.Sprintf("testdata/%s.json", name)
+ b = readFixtureFile(t, filename)
+ )
+
+ // bag of cells
+ var fx fixture
+
+ require.NoError(t, json.Unmarshal(b, &fx))
+
+ cells, err := boc.DeserializeBocHex(fx.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{}))
+
+ t.Logf("Loaded fixture %s\n%s", filename, fx.Description)
+
+ 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/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-deposit.json b/pkg/contracts/ton/testdata/01-deposit.json
new file mode 100644
index 0000000000..d22170ddf0
--- /dev/null
+++ b/pkg/contracts/ton/testdata/01-deposit.json
@@ -0,0 +1,8 @@
+{
+ "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b",
+ "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04",
+ "description": "Sample deposit to gw contract. https://testnet.tonviewer.com/transaction/cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf",
+ "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf",
+ "logicalTime": 26023788000003,
+ "test": true
+}
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
diff --git a/pkg/contracts/ton/testdata/readme.md b/pkg/contracts/ton/testdata/readme.md
new file mode 100644
index 0000000000..8695f06982
--- /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 -testnet \
+ kQCZfYicgVrqwhxH-Grg44OD78PDRjBnWC9iY61IxaFIW77M \
+ 26023788000003 \
+ cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf | jq
+```
+
+Returns
+
+```json
+{
+ "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b",
+ "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04",
+ "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
new file mode 100644
index 0000000000..efe76ca3aa
--- /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 scraper.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), lt, 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.NewClientWithDefaultMainnet()
+ 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")
+}
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
+}
diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go
index 94fc9ab2fb..2a5a7edff1 100644
--- a/pkg/ticker/ticker.go
+++ b/pkg/ticker/ticker.go
@@ -34,16 +34,18 @@ 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
// runnerMu is a mutex to prevent double run
runnerMu sync.Mutex
@@ -51,25 +53,47 @@ 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
}
-// 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.
@@ -96,24 +120,33 @@ 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.signalChan = 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 {
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 errors.Wrap(err, "ticker task failed")
+ return fmt.Errorf("ticker task failed: %w", err)
}
- case <-t.signalChan:
+ case <-t.externalStopChan:
+ t.Stop()
return nil
}
}
@@ -139,11 +172,18 @@ func (t *Ticker) Stop() {
defer t.stateMu.Unlock()
// noop
- if t.stopped || t.signalChan == nil {
+ if t.stopped {
return
}
- close(t.signalChan)
+ t.ctxCancel()
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 4d890bf051..60d6c74dc8 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) {
@@ -148,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) {
@@ -173,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) {
@@ -197,4 +200,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/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/testutil/sample/sample_ton.go b/testutil/sample/sample_ton.go
new file mode 100644
index 0000000000..01ce8724c5
--- /dev/null
+++ b/testutil/sample/sample_ton.go
@@ -0,0 +1,272 @@
+package sample
+
+import (
+ "crypto/rand"
+ "reflect"
+ "testing"
+ "time"
+ "unsafe"
+
+ "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 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)
+
+ 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 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)
+
+ 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 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)
+
+ 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]]
+ }
+
+ tx := 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},
+ },
+ }
+
+ setTXHash(&tx.Transaction, Hash())
+
+ return tx
+}
+
+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
+}
+
+// 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/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())
+ })
+
+}
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,
}
/**
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/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 {
diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go
index 6cf8af9de6..f089f26815 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 if it's already 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.
@@ -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.
@@ -329,7 +334,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(),
@@ -338,6 +348,7 @@ func (ob *Observer) WithLogger(logger Logger) *Observer {
Headers: chainLogger.With().Str(logs.FieldModule, logs.ModNameHeaders).Logger(),
Compliance: logger.Compliance,
}
+
return ob
}
@@ -461,22 +472,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/base/observer_test.go b/zetaclient/chains/base/observer_test.go
index 0e772e31f9..0c53bea35c 100644
--- a/zetaclient/chains/base/observer_test.go
+++ b/zetaclient/chains/base/observer_test.go
@@ -175,15 +175,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 d31715db9a..fad3c87230 100644
--- a/zetaclient/chains/bitcoin/observer/inbound.go
+++ b/zetaclient/chains/bitcoin/observer/inbound.go
@@ -59,7 +59,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
@@ -89,7 +89,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
@@ -205,7 +205,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
@@ -224,7 +224,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 80781fdd33..f78d81af9c 100644
--- a/zetaclient/chains/bitcoin/observer/observer.go
+++ b/zetaclient/chains/bitcoin/observer/observer.go
@@ -162,25 +162,9 @@ 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 {
+ if ok := ob.Observer.Start(); !ok {
ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId)
return
}
@@ -219,12 +203,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
@@ -238,25 +222,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
@@ -316,7 +300,7 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error {
// 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
@@ -326,7 +310,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)
@@ -341,7 +325,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 3b25d8c0c1..16c8d24b81 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 e0fc3c651d..03688f4aa4 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..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"
@@ -39,23 +38,20 @@ 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)
}
- 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 {
@@ -74,7 +70,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 +87,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 +106,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 +187,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 +611,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..8823b13c47 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
}
@@ -190,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/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 ab58456de6..c89a77e4b8 100644
--- a/zetaclient/chains/interfaces/interfaces.go
+++ b/zetaclient/chains/interfaces/interfaces.go
@@ -39,15 +39,21 @@ const (
// ChainObserver is the interface for chain observer
type ChainObserver interface {
+ // Start starts the observer
Start(ctx context.Context)
+
+ // Stop stops the observer
Stop()
- VoteOutboundIfConfirmed(
- ctx context.Context,
- cctx *crosschaintypes.CrossChainTx,
- ) (bool, error)
+
+ // ChainParams returns observer chain params (might be out of date with zetacore)
+ ChainParams() observertypes.ChainParams
+
+ // SetChainParams sets observer chain params
SetChainParams(observertypes.ChainParams)
- GetChainParams() observertypes.ChainParams
- WatchInboundTracker(ctx context.Context) error
+
+ // VoteOutboundIfConfirmed checks outbound status and returns (continueKeySign, error)
+ // todo we should make this simpler.
+ 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..0548fcd6d3 100644
--- a/zetaclient/chains/solana/observer/observer.go
+++ b/zetaclient/chains/solana/observer/observer.go
@@ -96,25 +96,9 @@ 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 {
+ 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_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/config.go b/zetaclient/chains/ton/config.go
index ee7eaac701..731287756e 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,16 @@ func ConfigFromURL(ctx context.Context, url string) (*GlobalConfigurationFile, e
return config.ParseConfig(res.Body)
}
+
+func ConfigFromPath(path string) (*GlobalConfigurationFile, error) {
+ return config.ParseConfigFile(path)
+}
+
+// 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())
+ }
+
+ return ConfigFromPath(urlOrPath)
+}
diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go
new file mode 100644
index 0000000000..25b0efcf39
--- /dev/null
+++ b/zetaclient/chains/ton/liteapi/client.go
@@ -0,0 +1,230 @@
+package liteapi
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "strconv"
+ "strings"
+
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/pkg/errors"
+ "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
+// 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
+ blockCacheSize = 250
+)
+
+// New Client constructor.
+func New(client *liteapi.Client) *Client {
+ blockCache, _ := lru.New(blockCacheSize)
+
+ return &Client{Client: client, blockCache: blockCache}
+}
+
+// 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")
+ }
+
+ 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.
+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
+// 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.getLastTransactionHash(ctx, acc)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ var (
+ tx *ton.Transaction
+ scrolled int
+ )
+
+ 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
+}
+
+// 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.getLastTransactionHash(ctx, acc)
+ if err != nil {
+ return nil, err
+ }
+
+ var result []ton.Transaction
+
+ 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())
+ }
+
+ 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
+ }
+
+ // reverse the result to get the oldest tx first
+ slices.Reverse(result)
+
+ return result, nil
+}
+
+// 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")
+ }
+
+ if state.Account.Status() != tlb.AccountActive {
+ return 0, tlb.Bits256{}, errors.New("account is not active")
+ }
+
+ 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())
+}
+
+// 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 encoded 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..ed3c850dd8
--- /dev/null
+++ b/zetaclient/chains/ton/liteapi/client_live_test.go
@@ -0,0 +1,198 @@
+package liteapi
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "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/tlb"
+ "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 = New(mustCreateClient(t))
+ )
+
+ t.Run("GetFirstTransaction", func(t *testing.T) {
+ t.Run("Account doesn't exist", func(t *testing.T) {
+ // ARRANGE
+ accountID, err := ton.ParseAccountID("0:55798cb7b87168251a7c39f6806b8c202f6caa0f617a76f4070b3fdacfd056a2")
+ require.NoError(t, err)
+
+ // ACT
+ tx, scrolled, err := client.GetFirstTransaction(ctx, accountID)
+
+ // ASSERT
+ require.ErrorContains(t, err, "account is not active")
+ require.Zero(t, scrolled)
+ require.Nil(t, tx)
+ })
+
+ 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)
+
+ // 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)
+ })
+ })
+
+ t.Run("GetTransactionsUntil", func(t *testing.T) {
+ // ARRANGE
+ // Given sample account id (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.GetTransactionsSince(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)
+ })
+
+ 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 {
+ 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
+}
+
+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/liteapi/client_test.go b/zetaclient/chains/ton/liteapi/client_test.go
new file mode 100644
index 0000000000..a1148540be
--- /dev/null
+++ b/zetaclient/chains/ton/liteapi/client_test.go
@@ -0,0 +1,100 @@
+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))
+}
+
+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)
+ })
+ }
+}
diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go
new file mode 100644
index 0000000000..95f9a510d7
--- /dev/null
+++ b/zetaclient/chains/ton/observer/inbound.go
@@ -0,0 +1,260 @@
+package observer
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+
+ "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 (
+ // 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); 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"),
+ )
+}
+
+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")
+ }
+
+ // 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.GetTransactionsSince(ctx, ob.gateway.AccountID(), lt, hashBits)
+ if err != nil {
+ return errors.Wrap(err, "unable to get transactions")
+ }
+
+ 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))
+ }
+
+ for i := range txs {
+ tx := txs[i]
+
+ parsedTX, skip, err := ob.gateway.ParseAndFilter(tx, toncontracts.FilterInbounds)
+ if err != nil {
+ return errors.Wrap(err, "unable to parse and filter tx")
+ }
+
+ 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 {
+ 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)
+ }
+
+ 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)
+}
+
+// extractInboundData parses Gateway tx into deposit (TON sender, amount, memo)
+func extractInboundData(tx *toncontracts.Transaction) (string, math.Uint, []byte, error) {
+ 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
+ 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)
+ }
+}
+
+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()))
+ )
+
+ // TODO: use protocol contract v2 for deposit
+ // https://github.com/zeta-chain/node/issues/2967
+
+ msg := zetacore.GetInboundVoteMessage(
+ sender,
+ ob.Chain().ChainId,
+ sender,
+ sender,
+ ob.ZetacoreClient().Chain().ChainId,
+ amount,
+ hex.EncodeToString(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.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)
+
+ if err := ob.WriteLastTxScannedToDB(txHash); err != nil {
+ ob.Logger().Inbound.Error().
+ Err(err).
+ Fields(txLogFields(tx)).
+ Msgf("setLastScannedTX: unable to WriteLastTxScannedToDB")
+
+ return
+ }
+
+ ob.Logger().Inbound.Info().
+ 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.BlockID.String(),
+ }
+}
diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go
new file mode 100644
index 0000000000..281db44f79
--- /dev/null
+++ b/zetaclient/chains/ton/observer/inbound_test.go
@@ -0,0 +1,331 @@
+package observer
+
+import (
+ "encoding/hex"
+ "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("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
+ 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.OnGetTransactionsSince(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.
+ OnGetTransactionsSince(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.
+ OnGetTransactionsSince(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, hex.EncodeToString(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.
+ OnGetTransactionsSince(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())
+
+ 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))
+ 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.
+ OnGetTransactionsSince(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
new file mode 100644
index 0000000000..e20742116a
--- /dev/null
+++ b/zetaclient/chains/ton/observer/observer.go
@@ -0,0 +1,80 @@
+package observer
+
+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"
+)
+
+// Observer is a TON observer.
+type Observer struct {
+ *base.Observer
+
+ 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, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, 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)
+}
+
+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():
+ 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{
+ Observer: bo,
+ client: client,
+ gateway: 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)
+ 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))
+
+ // TODO: watchInboundTracker
+ // https://github.com/zeta-chain/node/issues/2935
+
+ // TODO: outbounds/withdrawals: (watchOutbound, watchGasPrice, watchRPCStatus)
+ // https://github.com/zeta-chain/node/issues/2807
+}
+
+func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossChainTx) (bool, error) {
+ return false, errors.New("not implemented")
+}
diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go
index e978a589b9..38c032eb4a 100644
--- a/zetaclient/chains/ton/observer/observer_test.go
+++ b/zetaclient/chains/ton/observer/observer_test.go
@@ -2,62 +2,171 @@ package observer
import (
"context"
- "encoding/json"
- "strings"
+ "strconv"
"testing"
+ "cosmossdk.io/math"
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/mock"
"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/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"
)
-// 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="}}}`
+type testSuite struct {
+ ctx context.Context
+ t *testing.T
-func TestObserver(t *testing.T) {
- t.Skip("skip test")
+ chain chains.Chain
+ chainParams *observertypes.ChainParams
- ctx := context.Background()
+ liteClient *mocks.LiteClient
- cfg, err := config.ParseConfig(strings.NewReader(configRaw))
- require.NoError(t, err)
+ 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()
- client, err := liteapi.NewClient(liteapi.WithConfigurationFile(*cfg))
+ 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)
- res, err := client.GetMasterchainInfo(ctx)
+ baseObserver, err := base.NewObserver(
+ chain,
+ *chainParams,
+ zetacore,
+ tss,
+ 1,
+ 1,
+ 60,
+ nil,
+ database,
+ logger,
+ )
+
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, "", " ")
+ 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).Maybe()
+
+ 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) OnGetTransactionsSince(
+ acc ton.AccountID,
+ lt uint64,
+ hash ton.Bits256,
+ txs []ton.Transaction,
+ err error,
+) *mock.Call {
+ return ts.liteClient.
+ On("GetTransactionsSince", mock.Anything, 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", mock.Anything, id, uint32(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)
- t.Log(string(b))
+ 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)
+ require.True(ts.t, ok, "unexpected cctx type")
+
+ ts.votesBag = append(ts.votesBag, cctx)
+ }
+ 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 ca0234c126..6f17153b52 100644
--- a/zetaclient/config/config_chain.go
+++ b/zetaclient/config/config_chain.go
@@ -23,6 +23,7 @@ func New(setDefaults bool) Config {
cfg.EVMChainConfigs = evmChainsConfigs()
cfg.BTCChainConfigs = btcChainsConfigs()
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 bf225b97f4..a60875b5e8 100644
--- a/zetaclient/config/types.go
+++ b/zetaclient/config/types.go
@@ -60,6 +60,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"`
@@ -97,6 +104,7 @@ type Config struct {
// Deprecated: the 'BitcoinConfig' will be removed once the 'BTCChainConfigs' is fully adopted
BitcoinConfig BTCConfig `json:"BitcoinConfig"`
SolanaConfig SolanaConfig `json:"SolanaConfig"`
+ TONConfig TONConfig `json:"TONConfig"`
// compliance config
ComplianceConfig ComplianceConfig `json:"ComplianceConfig"`
@@ -149,6 +157,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{}
+}
+
// StringMasked returns the string representation of the config with sensitive fields masked.
// Currently only the endpoints and bitcoin credentials are masked.
func (c Config) StringMasked() string {
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")
}
diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go
index d4e7e526b7..4f35953ce0 100644
--- a/zetaclient/context/chain.go
+++ b/zetaclient/context/chain.go
@@ -168,6 +168,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/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/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go
index 73f47d21cf..eaae3a8e6d 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.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = 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 08d625548f..34c94bf77d 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,9 +21,12 @@ 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"
+ "github.com/zeta-chain/node/zetaclient/logs"
"github.com/zeta-chain/node/zetaclient/metrics"
)
@@ -71,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++
}
@@ -80,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++
}
)
@@ -181,6 +186,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()).
@@ -238,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++
}
@@ -247,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++
}
@@ -394,6 +404,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.NewFromSource(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/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go
index dd0ef1eaab..698a520fcf 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"
@@ -228,7 +229,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 +448,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 +547,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 +619,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 {
@@ -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.
diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go
index 969dbbb393..2ab34b900e 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 {
diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go
new file mode 100644
index 0000000000..f11ccaf24c
--- /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, 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")
+ }
+
+ var r0 tlb.BlockInfo
+ var r1 error
+ 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, 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, uint32) error); ok {
+ r1 = rf(ctx, blockID, 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
+}
+
+// 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 GetTransactionsSince")
+ }
+
+ 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
+}
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)