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 1f49f12ecb..b18609a3a9 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 * [2979](https://github.com/zeta-chain/node/pull/2979) - add fungible keeper ability to lock/unlock ZRC20 tokens ### Refactor @@ -27,6 +28,7 @@ * [2826](https://github.com/zeta-chain/node/pull/2826) - remove unused code from emissions module and add new parameter for fixed block reward amount * [2890](https://github.com/zeta-chain/node/pull/2890) - refactor `MsgUpdateChainInfo` to accept a single chain, and add `MsgRemoveChainInfo` to remove a chain * [2899](https://github.com/zeta-chain/node/pull/2899) - remove btc deposit fee v1 and improve unit tests +* [2952](https://github.com/zeta-chain/node/pull/2952) - add error_message to cctx.status ### Tests 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 a8c0a2516e..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 @@ -58446,6 +58450,14 @@ definitions: $ref: '#/definitions/crosschainCctxStatus' status_message: type: string + description: |- + status_message carries information about the status transitions: + why they were triggered, old and new status. + error_message: + type: string + description: |- + error_message carries information about the error that caused the tx + to be PendingRevert, Reverted or Aborted. lastUpdate_timestamp: type: string format: int64 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 570e7b05eb..9484888cf3 100644 --- a/e2e/e2etests/test_eth_deposit_call.go +++ b/e2e/e2etests/test_eth_deposit_call.go @@ -87,6 +87,11 @@ func TestEtherDepositAndCall(r *runner.E2ERunner, args []string) { r.Logger.Info("Cross-chain call to reverter reverted") - // check the status message contains revert error hash in case of revert - require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo) + // Check the error carries the 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 e9155c9ddd..0a62b70ac6 100644 --- a/e2e/e2etests/test_solana_deposit_refund.go +++ b/e2e/e2etests/test_solana_deposit_refund.go @@ -31,6 +31,11 @@ func TestSolanaDepositAndCallRefund(r *runner.E2ERunner, args []string) { r.Logger.CCTX(*cctx, "solana_deposit_and_refund") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted) - // check the status message contains revert error hash in case of revert - require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo) + // Check the error carries the 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/crosschain/cross_chain_tx.proto b/proto/zetachain/zetacore/crosschain/cross_chain_tx.proto index 0ca1f56f7d..9b634a6378 100644 --- a/proto/zetachain/zetacore/crosschain/cross_chain_tx.proto +++ b/proto/zetachain/zetacore/crosschain/cross_chain_tx.proto @@ -17,7 +17,7 @@ enum CctxStatus { Reverted = 5; // inbound reverted. Aborted = 6; // inbound tx error or invalid paramters and cannot revert; just abort. - // But the amount can be refunded to zetachain using and admin proposal + // But the amount can be refunded to zetachain using and admin proposal } enum TxFinalizationStatus { @@ -94,7 +94,12 @@ message OutboundParams { message Status { CctxStatus status = 1; + // status_message carries information about the status transitions: + // why they were triggered, old and new status. string status_message = 2; + // error_message carries information about the error that caused the tx + // to be PendingRevert, Reverted or Aborted. + string error_message = 6; int64 lastUpdate_timestamp = 3; bool isAbortRefunded = 4; // when the CCTX was created. only populated on new transactions. 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/crosschain.go b/testutil/sample/crosschain.go index 8f09271e23..ff17525734 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -191,6 +191,7 @@ func Status(t *testing.T, index string) *types.Status { return &types.Status{ Status: types.CctxStatus(r.Intn(100)), StatusMessage: String(), + ErrorMessage: String(), CreatedTimestamp: createdAt, LastUpdateTimestamp: createdAt, } 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/crosschain/cross_chain_tx_pb.d.ts b/typescript/zetachain/zetacore/crosschain/cross_chain_tx_pb.d.ts index 1ffa04eaca..e5295ee133 100644 --- a/typescript/zetachain/zetacore/crosschain/cross_chain_tx_pb.d.ts +++ b/typescript/zetachain/zetacore/crosschain/cross_chain_tx_pb.d.ts @@ -355,10 +355,21 @@ export declare class Status extends Message { status: CctxStatus; /** + * status_message carries information about the status transitions: + * why they were triggered, old and new status. + * * @generated from field: string status_message = 2; */ statusMessage: string; + /** + * error_message carries information about the error that caused the tx + * to be PendingRevert, Reverted or Aborted. + * + * @generated from field: string error_message = 6; + */ + errorMessage: string; + /** * @generated from field: int64 lastUpdate_timestamp = 3; */ 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/cctx_gateway_observers.go b/x/crosschain/keeper/cctx_gateway_observers.go index 39a33f4edc..9576bcfe32 100644 --- a/x/crosschain/keeper/cctx_gateway_observers.go +++ b/x/crosschain/keeper/cctx_gateway_observers.go @@ -75,7 +75,7 @@ func (c CCTXGatewayObservers) InitiateOutbound( }() if err != nil { // do not commit anything here as the CCTX should be aborted - config.CCTX.SetAbort(err.Error()) + config.CCTX.SetAbort("internal error", err.Error()) return types.CctxStatus_Aborted, err } commit() diff --git a/x/crosschain/keeper/cctx_gateway_zevm.go b/x/crosschain/keeper/cctx_gateway_zevm.go index 81366d6af3..fc247ad7a4 100644 --- a/x/crosschain/keeper/cctx_gateway_zevm.go +++ b/x/crosschain/keeper/cctx_gateway_zevm.go @@ -28,7 +28,9 @@ func (c CCTXGatewayZEVM) InitiateOutbound( if err != nil && !isContractReverted { // exceptional case; internal error; should abort CCTX - config.CCTX.SetAbort(err.Error()) + config.CCTX.SetAbort( + "error during deposit that is not smart contract revert", + err.Error()) return types.CctxStatus_Aborted, err } diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go b/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go index 17c848613a..2f3acdd744 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go @@ -53,7 +53,9 @@ func (k Keeper) ValidateOutboundZEVM( cctx.InboundParams.Amount, ) if err != nil { - cctx.SetAbort(fmt.Sprintf("%s : %s", depositErr, err.Error())) + cctx.SetAbort( + "revert failed", + fmt.Sprintf("deposit error: %s, processing error: %s", depositErr.Error(), err.Error())) return types.CctxStatus_Aborted } @@ -122,7 +124,7 @@ func (k Keeper) processFailedOutboundObservers(ctx sdk.Context, cctx *types.Cros if cctx.InboundParams.CoinType == coin.CoinType_Cmd { // if the cctx is of coin type cmd or the sender chain is zeta chain, then we do not revert, the cctx is aborted cctx.GetCurrentOutboundParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - cctx.SetAbort("Outbound failed, cmd cctx reverted") + cctx.SetAbort("", "outbound failed for admin tx") } else if chains.IsZetaChain(cctx.InboundParams.SenderChainId, k.GetAuthorityKeeper().GetAdditionalChainList(ctx)) { switch cctx.InboundParams.CoinType { // Try revert if the coin-type is ZETA @@ -137,7 +139,7 @@ func (k Keeper) processFailedOutboundObservers(ctx sdk.Context, cctx *types.Cros default: { cctx.GetCurrentOutboundParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - cctx.SetAbort("Outbound failed for non-ZETA cctx") + cctx.SetAbort("", "outbound failed for non-ZETA cctx") } } } else { @@ -195,10 +197,10 @@ func (k Keeper) processFailedOutboundOnExternalChain( return err } // Not setting the finalization status here, the required changes have been made while creating the revert tx - cctx.SetPendingRevert(revertMsg) + cctx.SetPendingRevert("", revertMsg) case types.CctxStatus_PendingRevert: cctx.GetCurrentOutboundParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - cctx.SetAbort("Outbound failed: revert failed; abort TX") + cctx.SetAbort("aborted while processing failed outbound", "outbound and revert failed") } return nil } @@ -225,9 +227,9 @@ func (k Keeper) processSuccessfulOutbound( oldStatus := cctx.CctxStatus.Status switch oldStatus { case types.CctxStatus_PendingRevert: - cctx.SetReverted("Outbound succeeded, revert executed") + cctx.SetReverted("", "revert executed") case types.CctxStatus_PendingOutbound: - cctx.SetOutboundMined("Outbound succeeded, mined") + cctx.SetOutboundMined("") default: return } @@ -256,7 +258,7 @@ func (k Keeper) processFailedZETAOutboundOnZEVM(ctx sdk.Context, cctx *types.Cro } // Trying to revert the transaction this would get set to a finalized status in the same block as this does not need a TSS singing - cctx.SetPendingRevert("Outbound failed, trying revert") + cctx.SetPendingRevert("", "outbound failed") data, err := base64.StdEncoding.DecodeString(cctx.RelayedMessage) if err != nil { return fmt.Errorf("failed decoding relayed message: %s", err.Error()) @@ -290,7 +292,7 @@ func (k Keeper) processFailedZETAOutboundOnZEVM(ctx sdk.Context, cctx *types.Cro return fmt.Errorf("failed ZETARevertAndCallContract: %s", err.Error()) } - cctx.SetReverted("Outbound failed, revert executed") + cctx.SetReverted("", "outbound failed") if len(ctx.TxBytes()) > 0 { // add event for tendermint transaction hash format hash := tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()) @@ -336,7 +338,7 @@ func (k Keeper) processFailedOutboundV2(ctx sdk.Context, cctx *types.CrossChainT } // update status - cctx.SetPendingRevert("Outbound failed, trying revert") + cctx.SetPendingRevert("", "outbound failed") // process the revert on ZEVM if err := k.fungibleKeeper.ProcessV2RevertDeposit( @@ -354,7 +356,7 @@ func (k Keeper) processFailedOutboundV2(ctx sdk.Context, cctx *types.CrossChainT } // tx is reverted - cctx.SetReverted("Outbound failed, revert executed") + cctx.SetReverted("", "outbound failed") // add event for tendermint transaction hash format if len(ctx.TxBytes()) > 0 { @@ -367,7 +369,7 @@ func (k Keeper) processFailedOutboundV2(ctx sdk.Context, cctx *types.CrossChainT cctx.GetCurrentOutboundParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed case types.CctxStatus_PendingRevert: cctx.GetCurrentOutboundParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - cctx.SetAbort("Outbound failed: revert failed; abort TX") + cctx.SetAbort("aborted while processing failed outbound", "outbound and revert failed") } return nil } diff --git a/x/crosschain/keeper/cctx_test.go b/x/crosschain/keeper/cctx_test.go index 8a51b4cf2a..d1e3f6fafc 100644 --- a/x/crosschain/keeper/cctx_test.go +++ b/x/crosschain/keeper/cctx_test.go @@ -170,7 +170,6 @@ func TestCCTXs(t *testing.T) { require.True(t, found) require.Equal(t, s, send) } - }) } } 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/crosschain/keeper/initiate_outbound_test.go b/x/crosschain/keeper/initiate_outbound_test.go index 55ca3e36f8..204179bea5 100644 --- a/x/crosschain/keeper/initiate_outbound_test.go +++ b/x/crosschain/keeper/initiate_outbound_test.go @@ -76,7 +76,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.ErrorContains(t, err, "deposit error") require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) require.Equal(t, types.CctxStatus_Aborted, newStatus) - require.Equal(t, "deposit error", cctx.CctxStatus.StatusMessage) + require.Equal(t, "deposit error", cctx.CctxStatus.ErrorMessage) }) t.Run( @@ -111,7 +111,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.Equal(t, types.CctxStatus_Aborted, newStatus) require.Contains( t, - cctx.CctxStatus.StatusMessage, + cctx.CctxStatus.ErrorMessage, "chain not supported", ) }, @@ -151,7 +151,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.Equal(t, types.CctxStatus_Aborted, newStatus) require.Contains( t, - cctx.CctxStatus.StatusMessage, + cctx.CctxStatus.ErrorMessage, "GetRevertGasLimit: foreign coin not found for sender chain", ) }) @@ -194,7 +194,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.Equal(t, types.CctxStatus_Aborted, newStatus) require.Contains( t, - cctx.CctxStatus.StatusMessage, + cctx.CctxStatus.ErrorMessage, "chain not supported", ) }, @@ -239,7 +239,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.Equal(t, types.CctxStatus_Aborted, newStatus) require.Contains( t, - cctx.CctxStatus.StatusMessage, + cctx.CctxStatus.ErrorMessage, "chain not supported", ) }, @@ -284,7 +284,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.NoError(t, err) require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) require.Equal(t, types.CctxStatus_Aborted, newStatus) - require.Contains(t, cctx.CctxStatus.StatusMessage, "cannot find receiver chain nonce") + require.Contains(t, cctx.CctxStatus.ErrorMessage, "cannot find receiver chain nonce") }) t.Run("unable to process zevm deposit HandleEVMDeposit revert successfully", func(t *testing.T) { @@ -321,7 +321,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.NoError(t, err) require.Equal(t, types.CctxStatus_PendingRevert, cctx.CctxStatus.Status) require.Equal(t, types.CctxStatus_PendingRevert, newStatus) - require.Equal(t, errDeposit.Error(), cctx.CctxStatus.StatusMessage) + require.Equal(t, errDeposit.Error(), cctx.CctxStatus.ErrorMessage) require.Equal(t, updatedNonce, cctx.GetCurrentOutboundParam().TssNonce) }) @@ -361,7 +361,7 @@ func TestKeeper_InitiateOutboundZEVMDeposit(t *testing.T) { require.Equal(t, types.CctxStatus_Aborted, newStatus) require.Contains( t, - cctx.CctxStatus.StatusMessage, + cctx.CctxStatus.ErrorMessage, "cannot revert a revert tx", ) }, @@ -427,7 +427,7 @@ func TestKeeper_InitiateOutboundProcessCrosschainMsgPassing(t *testing.T) { require.ErrorIs(t, err, observertypes.ErrSupportedChains) require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) require.Equal(t, types.CctxStatus_Aborted, newStatus) - require.Equal(t, observertypes.ErrSupportedChains.Error(), cctx.CctxStatus.StatusMessage) + require.Equal(t, observertypes.ErrSupportedChains.Error(), cctx.CctxStatus.ErrorMessage) }) t.Run("unable to process crosschain msg passing UpdateNonce fails", func(t *testing.T) { @@ -456,7 +456,7 @@ func TestKeeper_InitiateOutboundProcessCrosschainMsgPassing(t *testing.T) { require.ErrorContains(t, err, "cannot find receiver chain nonce") require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) require.Equal(t, types.CctxStatus_Aborted, newStatus) - require.Contains(t, cctx.CctxStatus.StatusMessage, "cannot find receiver chain nonce") + require.Contains(t, cctx.CctxStatus.ErrorMessage, "cannot find receiver chain nonce") }) } diff --git a/x/crosschain/keeper/msg_server_migrate_tss_funds.go b/x/crosschain/keeper/msg_server_migrate_tss_funds.go index c285033f02..8ecc4c47ff 100644 --- a/x/crosschain/keeper/msg_server_migrate_tss_funds.go +++ b/x/crosschain/keeper/msg_server_migrate_tss_funds.go @@ -9,7 +9,6 @@ import ( tmbytes "github.com/cometbft/cometbft/libs/bytes" tmtypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/errors" authoritytypes "github.com/zeta-chain/node/x/authority/types" "github.com/zeta-chain/node/x/crosschain/types" @@ -26,7 +25,7 @@ func (k msgServer) MigrateTssFunds( // check if authorized err := k.GetAuthorityKeeper().CheckAuthorization(ctx, msg) if err != nil { - return nil, errors.Wrap(authoritytypes.ErrUnauthorized, err.Error()) + return nil, errorsmod.Wrap(authoritytypes.ErrUnauthorized, err.Error()) } if k.zetaObserverKeeper.IsInboundEnabled(ctx) { diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index e824a01707..5aa70be3bd 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -265,7 +265,7 @@ func TestKeeper_VoteInbound(t *testing.T) { }) } -func TestStatus_ChangeStatus(t *testing.T) { +func TestStatus_UpdateCctxStatus(t *testing.T) { tt := []struct { Name string Status types.Status @@ -302,7 +302,7 @@ func TestStatus_ChangeStatus(t *testing.T) { for _, test := range tt { test := test t.Run(test.Name, func(t *testing.T) { - test.Status.ChangeStatus(test.NonErrStatus, test.Msg) + test.Status.UpdateStatusAndErrorMessages(test.NonErrStatus, test.Msg, "") if test.IsErr { require.Equal(t, test.ErrStatus, test.Status.Status) } else { diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx.go b/x/crosschain/keeper/msg_server_vote_outbound_tx.go index 6682c2c705..4bb65a3c8f 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx.go @@ -185,7 +185,7 @@ SaveFailedOutbound saves a failed outbound transaction.It does the following thi */ func (k Keeper) SaveFailedOutbound(ctx sdk.Context, cctx *types.CrossChainTx, errMessage string, tssPubkey string) { - cctx.SetAbort(errMessage) + cctx.SetAbort("", errMessage) ctx.Logger().Error(errMessage) k.SaveOutbound(ctx, cctx, tssPubkey) } diff --git a/x/crosschain/migrations/v5/migrate.go b/x/crosschain/migrations/v5/migrate.go index aeeb22451f..0f21d8646b 100644 --- a/x/crosschain/migrations/v5/migrate.go +++ b/x/crosschain/migrations/v5/migrate.go @@ -137,6 +137,7 @@ func SetZetaAccounting( return nil } + func GetAbortedAmount(cctx types.CrossChainTx) sdkmath.Uint { if cctx.OutboundParams != nil && !cctx.GetCurrentOutboundParam().Amount.IsZero() { return cctx.GetCurrentOutboundParam().Amount diff --git a/x/crosschain/types/cctx.go b/x/crosschain/types/cctx.go index 0a235dffae..91004511d4 100644 --- a/x/crosschain/types/cctx.go +++ b/x/crosschain/types/cctx.go @@ -171,28 +171,28 @@ func (m *CrossChainTx) AddOutbound( } // SetAbort sets the CCTX status to Aborted with the given error message. -func (m CrossChainTx) SetAbort(message string) { - m.CctxStatus.ChangeStatus(CctxStatus_Aborted, message) +func (m CrossChainTx) SetAbort(statusMsg, errorMsg string) { + m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_Aborted, statusMsg, errorMsg) } // SetPendingRevert sets the CCTX status to PendingRevert with the given error message. -func (m CrossChainTx) SetPendingRevert(message string) { - m.CctxStatus.ChangeStatus(CctxStatus_PendingRevert, message) +func (m CrossChainTx) SetPendingRevert(statusMsg, errorMsg string) { + m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_PendingRevert, statusMsg, errorMsg) } // SetPendingOutbound sets the CCTX status to PendingOutbound with the given error message. -func (m CrossChainTx) SetPendingOutbound(message string) { - m.CctxStatus.ChangeStatus(CctxStatus_PendingOutbound, message) +func (m CrossChainTx) SetPendingOutbound(statusMsg string) { + m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_PendingOutbound, statusMsg, "") } // SetOutboundMined sets the CCTX status to OutboundMined with the given error message. -func (m CrossChainTx) SetOutboundMined(message string) { - m.CctxStatus.ChangeStatus(CctxStatus_OutboundMined, message) +func (m CrossChainTx) SetOutboundMined(statusMsg string) { + m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_OutboundMined, statusMsg, "") } // SetReverted sets the CCTX status to Reverted with the given error message. -func (m CrossChainTx) SetReverted(message string) { - m.CctxStatus.ChangeStatus(CctxStatus_Reverted, message) +func (m CrossChainTx) SetReverted(statusMsg, errorMsg string) { + m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_Reverted, statusMsg, errorMsg) } func (m CrossChainTx) GetCCTXIndexBytes() ([32]byte, error) { @@ -259,6 +259,7 @@ func NewCCTX(ctx sdk.Context, msg MsgVoteInbound, tssPubkey string) (CrossChainT status := &Status{ Status: CctxStatus_PendingInbound, StatusMessage: "", + ErrorMessage: "", CreatedTimestamp: ctx.BlockHeader().Time.Unix(), LastUpdateTimestamp: ctx.BlockHeader().Time.Unix(), IsAbortRefunded: false, diff --git a/x/crosschain/types/cctx_test.go b/x/crosschain/types/cctx_test.go index 00fbe2ac66..f49768f8a0 100644 --- a/x/crosschain/types/cctx_test.go +++ b/x/crosschain/types/cctx_test.go @@ -150,17 +150,20 @@ func Test_SetRevertOutboundValues(t *testing.T) { func TestCrossChainTx_SetAbort(t *testing.T) { cctx := sample.CrossChainTx(t, "test") - cctx.SetAbort("test") + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + cctx.SetAbort("test", "test") require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) - require.Equal(t, "test", "test") + require.Contains(t, cctx.CctxStatus.StatusMessage, "test") + require.Contains(t, cctx.CctxStatus.ErrorMessage, "test") } func TestCrossChainTx_SetPendingRevert(t *testing.T) { cctx := sample.CrossChainTx(t, "test") cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound - cctx.SetPendingRevert("test") + cctx.SetPendingRevert("test", "test") require.Equal(t, types.CctxStatus_PendingRevert, cctx.CctxStatus.Status) require.Contains(t, cctx.CctxStatus.StatusMessage, "test") + require.Contains(t, cctx.CctxStatus.ErrorMessage, "test") } func TestCrossChainTx_SetPendingOutbound(t *testing.T) { @@ -169,6 +172,7 @@ func TestCrossChainTx_SetPendingOutbound(t *testing.T) { cctx.SetPendingOutbound("test") require.Equal(t, types.CctxStatus_PendingOutbound, cctx.CctxStatus.Status) require.Contains(t, cctx.CctxStatus.StatusMessage, "test") + require.NotContains(t, cctx.CctxStatus.ErrorMessage, "test") } func TestCrossChainTx_SetOutboundMined(t *testing.T) { @@ -177,12 +181,14 @@ func TestCrossChainTx_SetOutboundMined(t *testing.T) { cctx.SetOutboundMined("test") require.Equal(t, types.CctxStatus_OutboundMined, cctx.CctxStatus.Status) require.Contains(t, cctx.CctxStatus.StatusMessage, "test") + require.NotContains(t, cctx.CctxStatus.ErrorMessage, "test") } func TestCrossChainTx_SetReverted(t *testing.T) { cctx := sample.CrossChainTx(t, "test") cctx.CctxStatus.Status = types.CctxStatus_PendingRevert - cctx.SetReverted("test") + cctx.SetReverted("test", "test") require.Equal(t, types.CctxStatus_Reverted, cctx.CctxStatus.Status) require.Contains(t, cctx.CctxStatus.StatusMessage, "test") + require.Contains(t, cctx.CctxStatus.ErrorMessage, "test") } diff --git a/x/crosschain/types/cross_chain_tx.pb.go b/x/crosschain/types/cross_chain_tx.pb.go index 842ba310ba..23923a9cbf 100644 --- a/x/crosschain/types/cross_chain_tx.pb.go +++ b/x/crosschain/types/cross_chain_tx.pb.go @@ -488,10 +488,15 @@ func (m *OutboundParams) GetCallOptions() *CallOptions { } type Status struct { - Status CctxStatus `protobuf:"varint,1,opt,name=status,proto3,enum=zetachain.zetacore.crosschain.CctxStatus" json:"status,omitempty"` - StatusMessage string `protobuf:"bytes,2,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` - LastUpdateTimestamp int64 `protobuf:"varint,3,opt,name=lastUpdate_timestamp,json=lastUpdateTimestamp,proto3" json:"lastUpdate_timestamp,omitempty"` - IsAbortRefunded bool `protobuf:"varint,4,opt,name=isAbortRefunded,proto3" json:"isAbortRefunded,omitempty"` + Status CctxStatus `protobuf:"varint,1,opt,name=status,proto3,enum=zetachain.zetacore.crosschain.CctxStatus" json:"status,omitempty"` + // status_message carries information about the status transitions: + // why they were triggered, old and new status. + StatusMessage string `protobuf:"bytes,2,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` + // error_message carries information about the error that caused the tx + // to be PendingRevert, Reverted or Aborted. + ErrorMessage string `protobuf:"bytes,6,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + LastUpdateTimestamp int64 `protobuf:"varint,3,opt,name=lastUpdate_timestamp,json=lastUpdateTimestamp,proto3" json:"lastUpdate_timestamp,omitempty"` + IsAbortRefunded bool `protobuf:"varint,4,opt,name=isAbortRefunded,proto3" json:"isAbortRefunded,omitempty"` // when the CCTX was created. only populated on new transactions. CreatedTimestamp int64 `protobuf:"varint,5,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` } @@ -543,6 +548,13 @@ func (m *Status) GetStatusMessage() string { return "" } +func (m *Status) GetErrorMessage() string { + if m != nil { + return m.ErrorMessage + } + return "" +} + func (m *Status) GetLastUpdateTimestamp() int64 { if m != nil { return m.LastUpdateTimestamp @@ -753,92 +765,93 @@ func init() { } var fileDescriptor_d4c1966807fb5cb2 = []byte{ - // 1348 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x4d, 0x6f, 0x1b, 0x37, - 0x13, 0xf6, 0xda, 0xb2, 0x2c, 0x8d, 0x3e, 0xbc, 0xa6, 0x15, 0x67, 0xe3, 0x17, 0x51, 0xfc, 0xaa, - 0x75, 0xa2, 0xb8, 0xb5, 0x84, 0x28, 0x40, 0x51, 0xf4, 0x66, 0x1b, 0x71, 0xe2, 0xb6, 0x89, 0x8d, - 0x8d, 0x63, 0x20, 0x39, 0x74, 0x4b, 0xed, 0xd2, 0x12, 0xe1, 0xd5, 0x52, 0x5d, 0x52, 0x86, 0x14, - 0xf4, 0xd6, 0x73, 0x81, 0xfe, 0x88, 0x1e, 0x7a, 0xec, 0xcf, 0xc8, 0x31, 0xc7, 0xa2, 0x87, 0x20, - 0x48, 0xfe, 0x41, 0xcf, 0x3d, 0x14, 0xfc, 0x92, 0xac, 0xc0, 0xb5, 0xd3, 0xb4, 0x27, 0x71, 0x66, - 0x38, 0xcf, 0xcc, 0x0e, 0xe7, 0x19, 0x52, 0xd0, 0x7a, 0x4e, 0x04, 0x0e, 0xbb, 0x98, 0x26, 0x4d, - 0xb5, 0x62, 0x29, 0x69, 0x86, 0x29, 0xe3, 0x5c, 0xeb, 0xd4, 0x32, 0x50, 0xeb, 0x40, 0x0c, 0x1b, - 0xfd, 0x94, 0x09, 0x86, 0xae, 0x8f, 0x7d, 0x1a, 0xd6, 0xa7, 0x31, 0xf1, 0x59, 0xad, 0x74, 0x58, - 0x87, 0xa9, 0x9d, 0x4d, 0xb9, 0xd2, 0x4e, 0xab, 0x37, 0xcf, 0x09, 0xd4, 0x3f, 0xe9, 0x34, 0x43, - 0x26, 0xc3, 0x30, 0x9a, 0xe8, 0x7d, 0xb5, 0x5f, 0x33, 0x50, 0xda, 0x4b, 0xda, 0x6c, 0x90, 0x44, - 0x07, 0x38, 0xc5, 0x3d, 0x8e, 0x56, 0x20, 0xcb, 0x49, 0x12, 0x91, 0xd4, 0x73, 0xd6, 0x9c, 0x7a, - 0xde, 0x37, 0x12, 0xba, 0x09, 0x8b, 0x7a, 0x65, 0xf2, 0xa3, 0x91, 0x37, 0xbb, 0xe6, 0xd4, 0xe7, - 0xfc, 0x92, 0x56, 0xef, 0x48, 0xed, 0x5e, 0x84, 0xfe, 0x07, 0x79, 0x31, 0x0c, 0x58, 0x4a, 0x3b, - 0x34, 0xf1, 0xe6, 0x14, 0x44, 0x4e, 0x0c, 0xf7, 0x95, 0x8c, 0xb6, 0x21, 0x2f, 0x83, 0x07, 0x62, - 0xd4, 0x27, 0x5e, 0x66, 0xcd, 0xa9, 0x97, 0x5b, 0xeb, 0x8d, 0x73, 0xbe, 0xaf, 0x7f, 0xd2, 0x69, - 0xa8, 0x2c, 0x77, 0x18, 0x4d, 0x0e, 0x47, 0x7d, 0xe2, 0xe7, 0x42, 0xb3, 0x42, 0x15, 0x98, 0xc7, - 0x9c, 0x13, 0xe1, 0xcd, 0x2b, 0x70, 0x2d, 0xa0, 0xfb, 0x90, 0xc5, 0x3d, 0x36, 0x48, 0x84, 0x97, - 0x95, 0xea, 0xed, 0xe6, 0x8b, 0x57, 0x37, 0x66, 0x7e, 0x7f, 0x75, 0xe3, 0x56, 0x87, 0x8a, 0xee, - 0xa0, 0xdd, 0x08, 0x59, 0xaf, 0x19, 0x32, 0xde, 0x63, 0xdc, 0xfc, 0x6c, 0xf2, 0xe8, 0xa4, 0x29, - 0xf3, 0xe0, 0x8d, 0x27, 0x34, 0x11, 0xbe, 0x71, 0x47, 0x1f, 0x41, 0x89, 0xb5, 0x39, 0x49, 0x4f, - 0x49, 0x14, 0x74, 0x31, 0xef, 0x7a, 0x0b, 0x2a, 0x4c, 0xd1, 0x2a, 0x1f, 0x60, 0xde, 0x45, 0x9f, - 0x83, 0x37, 0xde, 0x44, 0x86, 0x82, 0xa4, 0x09, 0x8e, 0x83, 0x2e, 0xa1, 0x9d, 0xae, 0xf0, 0x72, - 0x6b, 0x4e, 0x3d, 0xe3, 0xaf, 0x58, 0xfb, 0x3d, 0x63, 0x7e, 0xa0, 0xac, 0xe8, 0xff, 0x50, 0x6c, - 0xe3, 0x38, 0x66, 0x22, 0xa0, 0x49, 0x44, 0x86, 0x5e, 0x5e, 0xa1, 0x17, 0xb4, 0x6e, 0x4f, 0xaa, - 0x50, 0x0b, 0xae, 0x1c, 0xd3, 0x04, 0xc7, 0xf4, 0x39, 0x89, 0x02, 0x59, 0x12, 0x8b, 0x0c, 0x0a, - 0x79, 0x79, 0x6c, 0x7c, 0x46, 0x04, 0x36, 0xb0, 0x14, 0x56, 0xc4, 0x30, 0x30, 0x16, 0x2c, 0x28, - 0x4b, 0x02, 0x2e, 0xb0, 0x18, 0x70, 0xaf, 0xa0, 0xaa, 0x7c, 0xb7, 0x71, 0x61, 0x17, 0x35, 0x0e, - 0x87, 0xbb, 0x67, 0x7c, 0x1f, 0x2b, 0x57, 0xbf, 0x22, 0xce, 0xd1, 0xd6, 0xbe, 0x83, 0xb2, 0x0c, - 0xbc, 0x15, 0x86, 0xb2, 0x5e, 0x34, 0xe9, 0xa0, 0x00, 0x96, 0x71, 0x9b, 0xa5, 0xc2, 0xa6, 0x6b, - 0x0e, 0xc2, 0xf9, 0xb0, 0x83, 0x58, 0x32, 0x58, 0x2a, 0x88, 0x42, 0xaa, 0x1d, 0x41, 0x61, 0x07, - 0xc7, 0xf1, 0x7e, 0x5f, 0xa6, 0xc1, 0x65, 0x8b, 0x75, 0x30, 0x0f, 0x62, 0xda, 0xa3, 0x3a, 0x4a, - 0xc6, 0xcf, 0x75, 0x30, 0xff, 0x5a, 0xca, 0x68, 0x03, 0x96, 0x28, 0x0f, 0x70, 0xda, 0xa6, 0x22, - 0xc5, 0xe9, 0x28, 0x08, 0x71, 0x1c, 0xab, 0x4e, 0xcd, 0xf9, 0x8b, 0x94, 0x6f, 0x59, 0xbd, 0xc4, - 0xab, 0xbd, 0xce, 0x42, 0x79, 0x7f, 0x20, 0xce, 0xb6, 0xff, 0x2a, 0xe4, 0x52, 0x12, 0x12, 0x7a, - 0x3a, 0x26, 0xc0, 0x58, 0x46, 0xb7, 0xc1, 0xb5, 0x6b, 0x4d, 0x82, 0x3d, 0xcb, 0x81, 0x45, 0xab, - 0xb7, 0x2c, 0x98, 0x6a, 0xf4, 0xb9, 0x0f, 0x6b, 0xf4, 0x49, 0x4b, 0x67, 0xfe, 0x5d, 0x4b, 0x4b, - 0x4a, 0x72, 0x1e, 0x24, 0x2c, 0x09, 0x89, 0x62, 0x4d, 0xc6, 0xcf, 0x09, 0xce, 0x1f, 0x49, 0x79, - 0xba, 0x98, 0xd9, 0x77, 0x8a, 0x69, 0x8c, 0xfd, 0x94, 0x86, 0xc4, 0x10, 0x41, 0x1a, 0x0f, 0xa4, - 0x8c, 0xea, 0xe0, 0x1a, 0x23, 0x4b, 0xa9, 0x18, 0x05, 0xc7, 0x84, 0x78, 0x57, 0xd5, 0x9e, 0xb2, - 0xde, 0xa3, 0xd4, 0xbb, 0x84, 0x20, 0x04, 0x19, 0x45, 0xa5, 0x9c, 0xb2, 0xaa, 0xf5, 0xfb, 0x10, - 0xe1, 0x22, 0x96, 0xc1, 0x85, 0x2c, 0xbb, 0x06, 0x32, 0xcd, 0x60, 0xc0, 0x49, 0xe4, 0x55, 0xd4, - 0xce, 0x85, 0x0e, 0xe6, 0x4f, 0x38, 0x89, 0xd0, 0x37, 0xb0, 0x4c, 0x8e, 0x8f, 0x49, 0x28, 0xe8, - 0x29, 0x09, 0x26, 0x1f, 0x77, 0x45, 0x95, 0xb8, 0x61, 0x4a, 0x7c, 0xf3, 0x3d, 0x4a, 0xbc, 0x27, - 0x7b, 0x75, 0x0c, 0x75, 0xdf, 0x56, 0xa5, 0xf1, 0x2e, 0xbe, 0xae, 0xec, 0x8a, 0xca, 0x62, 0x6a, - 0xbf, 0x2e, 0xf1, 0x75, 0x00, 0x79, 0x38, 0xfd, 0x41, 0xfb, 0x84, 0x8c, 0x14, 0x5b, 0xf3, 0xbe, - 0x3c, 0xae, 0x03, 0xa5, 0xb8, 0x80, 0xd8, 0xc5, 0xff, 0x98, 0xd8, 0xe8, 0x21, 0x14, 0x25, 0x59, - 0x02, 0xa6, 0x69, 0xe6, 0x79, 0x6b, 0x4e, 0xbd, 0xd0, 0xda, 0xb8, 0x24, 0xc0, 0x19, 0x62, 0xfa, - 0x85, 0x70, 0x22, 0x7c, 0x99, 0xc9, 0x95, 0xdc, 0x4a, 0xed, 0x4f, 0x07, 0xb2, 0x06, 0x7f, 0x0b, - 0xb2, 0x26, 0x75, 0x47, 0xa5, 0x7e, 0xfb, 0x32, 0xe4, 0x50, 0x0c, 0x4d, 0xc2, 0xc6, 0x11, 0xad, - 0x43, 0x59, 0xaf, 0x82, 0x1e, 0xe1, 0x1c, 0x77, 0x88, 0xe2, 0x5f, 0xde, 0x2f, 0x69, 0xed, 0x43, - 0xad, 0x44, 0x77, 0xa0, 0x12, 0x63, 0x2e, 0x9e, 0xf4, 0x23, 0x2c, 0x48, 0x20, 0x68, 0x8f, 0x70, - 0x81, 0x7b, 0x7d, 0x45, 0xc4, 0x39, 0x7f, 0x79, 0x62, 0x3b, 0xb4, 0x26, 0x54, 0x07, 0x39, 0x1d, - 0xe4, 0xe4, 0xf1, 0xc9, 0xf1, 0x20, 0x89, 0x48, 0xa4, 0x58, 0xa7, 0x87, 0xc6, 0x59, 0x35, 0xfa, - 0x04, 0x96, 0xc2, 0x94, 0x60, 0x39, 0xed, 0x26, 0xc8, 0xf3, 0x0a, 0xd9, 0x35, 0x86, 0x31, 0x6c, - 0xed, 0x87, 0x59, 0x28, 0xf9, 0xe4, 0x94, 0xa4, 0xc2, 0x0e, 0xaf, 0x75, 0x28, 0xa7, 0x4a, 0x11, - 0xe0, 0x28, 0x4a, 0x09, 0xe7, 0x66, 0xcc, 0x94, 0xb4, 0x76, 0x4b, 0x2b, 0xd1, 0xc7, 0x50, 0xd6, - 0x87, 0x91, 0x04, 0xda, 0x60, 0x66, 0x98, 0x3a, 0xa2, 0xfd, 0x44, 0x63, 0xca, 0xcb, 0x4a, 0x4d, - 0xcb, 0x31, 0x96, 0xbe, 0x70, 0x8b, 0x4a, 0x69, 0xa1, 0x26, 0x11, 0x6d, 0xd1, 0xe4, 0x97, 0x15, - 0x6d, 0x44, 0x5b, 0xb4, 0xa7, 0x72, 0xba, 0xa9, 0x6d, 0x93, 0xae, 0x9d, 0xff, 0xb0, 0xc1, 0x63, - 0xe2, 0xd9, 0x1e, 0xaf, 0xfd, 0x38, 0x0f, 0xc5, 0x1d, 0x79, 0xb0, 0x6a, 0x3c, 0x1e, 0x0e, 0x91, - 0x07, 0x0b, 0xaa, 0x54, 0xcc, 0x0e, 0x59, 0x2b, 0xca, 0xdb, 0x5d, 0xcf, 0x03, 0x7d, 0xb0, 0x5a, - 0x40, 0xdf, 0x42, 0x5e, 0xdd, 0x2c, 0xc7, 0x84, 0x70, 0x93, 0xd4, 0xce, 0x3f, 0x4c, 0xea, 0x8f, - 0x57, 0x37, 0xdc, 0x11, 0xee, 0xc5, 0x5f, 0xd4, 0xc6, 0x48, 0x35, 0x3f, 0x27, 0xd7, 0xbb, 0x84, - 0x70, 0x74, 0x0b, 0x16, 0x53, 0x12, 0xe3, 0x11, 0x89, 0xc6, 0x55, 0xca, 0xea, 0x59, 0x66, 0xd4, - 0xb6, 0x4c, 0xbb, 0x50, 0x08, 0x43, 0x31, 0xb4, 0x2c, 0xcc, 0x29, 0x92, 0xac, 0x5f, 0xd2, 0xca, - 0xa6, 0x8d, 0x21, 0x1c, 0xb7, 0x34, 0x7a, 0x0c, 0x65, 0xaa, 0x1f, 0x5e, 0x41, 0x5f, 0x5d, 0x3d, - 0x6a, 0x02, 0x16, 0x5a, 0x9f, 0x5e, 0x02, 0x35, 0xf5, 0x5a, 0xf3, 0x4b, 0x74, 0xea, 0xf1, 0x76, - 0x04, 0x8b, 0xcc, 0xdc, 0x67, 0x16, 0x15, 0xd6, 0xe6, 0xea, 0x85, 0xd6, 0xe6, 0x25, 0xa8, 0xd3, - 0xb7, 0xa0, 0x5f, 0x66, 0xd3, 0xb7, 0x62, 0x0a, 0xd7, 0xd4, 0x7b, 0x31, 0x64, 0x71, 0x10, 0xb2, - 0x44, 0xa4, 0x38, 0x14, 0xc1, 0x29, 0x49, 0x39, 0x65, 0x89, 0x79, 0x61, 0x7c, 0x76, 0x49, 0x84, - 0x03, 0xe3, 0xbf, 0x63, 0xdc, 0x8f, 0xb4, 0xb7, 0x7f, 0xb5, 0x7f, 0xbe, 0x01, 0x3d, 0x1d, 0xb7, - 0xad, 0x1d, 0x48, 0xc5, 0xf7, 0x2a, 0xd0, 0x14, 0xdd, 0xb6, 0x33, 0xb2, 0x4d, 0x6c, 0xab, 0x1b, - 0xe5, 0xc6, 0xf7, 0x00, 0x93, 0xe1, 0x82, 0x10, 0x94, 0x0f, 0x48, 0x12, 0xd1, 0xa4, 0x63, 0x6a, - 0xeb, 0xce, 0xa0, 0x65, 0x58, 0x34, 0x3a, 0x5b, 0x19, 0xd7, 0x41, 0x4b, 0x50, 0xb2, 0xd2, 0x43, - 0x9a, 0x90, 0xc8, 0x9d, 0x93, 0x2a, 0xb3, 0x4f, 0x87, 0x75, 0x33, 0xa8, 0x08, 0x39, 0xbd, 0x26, - 0x91, 0x3b, 0x8f, 0x0a, 0xb0, 0xb0, 0xa5, 0xdf, 0x33, 0x6e, 0x76, 0x35, 0xf3, 0xcb, 0xcf, 0x55, - 0x67, 0xe3, 0x2b, 0xa8, 0x9c, 0x37, 0x95, 0x91, 0x0b, 0xc5, 0x47, 0x4c, 0xec, 0xda, 0xd7, 0x9d, - 0x3b, 0x83, 0x4a, 0x90, 0x9f, 0x88, 0x8e, 0x44, 0xbe, 0x37, 0x24, 0xe1, 0x40, 0x82, 0xcd, 0x1a, - 0xb0, 0x26, 0x5c, 0xfd, 0x9b, 0xca, 0xa2, 0x2c, 0xcc, 0x1e, 0xdd, 0x71, 0x67, 0xd4, 0x6f, 0xcb, - 0x75, 0xb4, 0xc3, 0xf6, 0xfd, 0x17, 0x6f, 0xaa, 0xce, 0xcb, 0x37, 0x55, 0xe7, 0xf5, 0x9b, 0xaa, - 0xf3, 0xd3, 0xdb, 0xea, 0xcc, 0xcb, 0xb7, 0xd5, 0x99, 0xdf, 0xde, 0x56, 0x67, 0x9e, 0x6d, 0x9e, - 0x61, 0x92, 0x2c, 0xec, 0xa6, 0xfe, 0xff, 0x90, 0xb0, 0x88, 0x34, 0x87, 0x67, 0xff, 0xa6, 0x28, - 0x52, 0xb5, 0xb3, 0xea, 0xe0, 0xee, 0xfe, 0x15, 0x00, 0x00, 0xff, 0xff, 0x94, 0x4b, 0x30, 0xb4, - 0xd4, 0x0c, 0x00, 0x00, + // 1361 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xcf, 0x6e, 0x1b, 0xb7, + 0x13, 0xf6, 0xda, 0xb2, 0x2c, 0x8d, 0xfe, 0x78, 0x4d, 0x2b, 0xce, 0xc6, 0x3f, 0x44, 0xf1, 0x4f, + 0xad, 0x13, 0xc5, 0xad, 0x25, 0x44, 0x01, 0x8a, 0xa2, 0x37, 0xdb, 0x88, 0x13, 0xb7, 0x4d, 0x6c, + 0x6c, 0x1c, 0x03, 0xc9, 0xa1, 0x5b, 0x6a, 0x97, 0x96, 0x08, 0xaf, 0x96, 0xea, 0x92, 0x32, 0xa4, + 0xa0, 0xb7, 0x9e, 0x0b, 0xf4, 0x15, 0x0a, 0xf4, 0xd0, 0x63, 0x1f, 0x23, 0xc7, 0x1c, 0x8b, 0x1e, + 0x82, 0x20, 0x79, 0x83, 0x3e, 0x41, 0xc1, 0x7f, 0x92, 0x15, 0xb8, 0x76, 0x9a, 0xf6, 0x24, 0xf2, + 0x1b, 0xf2, 0x9b, 0xd9, 0xe1, 0x7c, 0x43, 0x0a, 0x5a, 0xcf, 0x89, 0xc0, 0x61, 0x17, 0xd3, 0xa4, + 0xa9, 0x46, 0x2c, 0x25, 0xcd, 0x30, 0x65, 0x9c, 0x6b, 0x4c, 0x0d, 0x03, 0x35, 0x0e, 0xc4, 0xb0, + 0xd1, 0x4f, 0x99, 0x60, 0xe8, 0xfa, 0x78, 0x4f, 0xc3, 0xee, 0x69, 0x4c, 0xf6, 0xac, 0x56, 0x3a, + 0xac, 0xc3, 0xd4, 0xca, 0xa6, 0x1c, 0xe9, 0x4d, 0xab, 0x37, 0xcf, 0x71, 0xd4, 0x3f, 0xe9, 0x34, + 0x43, 0x26, 0xdd, 0x30, 0x9a, 0xe8, 0x75, 0xb5, 0xdf, 0x32, 0x50, 0xda, 0x4b, 0xda, 0x6c, 0x90, + 0x44, 0x07, 0x38, 0xc5, 0x3d, 0x8e, 0x56, 0x20, 0xcb, 0x49, 0x12, 0x91, 0xd4, 0x73, 0xd6, 0x9c, + 0x7a, 0xde, 0x37, 0x33, 0x74, 0x13, 0x16, 0xf5, 0xc8, 0xc4, 0x47, 0x23, 0x6f, 0x76, 0xcd, 0xa9, + 0xcf, 0xf9, 0x25, 0x0d, 0xef, 0x48, 0x74, 0x2f, 0x42, 0xff, 0x83, 0xbc, 0x18, 0x06, 0x2c, 0xa5, + 0x1d, 0x9a, 0x78, 0x73, 0x8a, 0x22, 0x27, 0x86, 0xfb, 0x6a, 0x8e, 0xb6, 0x21, 0x2f, 0x9d, 0x07, + 0x62, 0xd4, 0x27, 0x5e, 0x66, 0xcd, 0xa9, 0x97, 0x5b, 0xeb, 0x8d, 0x73, 0xbe, 0xaf, 0x7f, 0xd2, + 0x69, 0xa8, 0x28, 0x77, 0x18, 0x4d, 0x0e, 0x47, 0x7d, 0xe2, 0xe7, 0x42, 0x33, 0x42, 0x15, 0x98, + 0xc7, 0x9c, 0x13, 0xe1, 0xcd, 0x2b, 0x72, 0x3d, 0x41, 0xf7, 0x21, 0x8b, 0x7b, 0x6c, 0x90, 0x08, + 0x2f, 0x2b, 0xe1, 0xed, 0xe6, 0x8b, 0x57, 0x37, 0x66, 0xfe, 0x78, 0x75, 0xe3, 0x56, 0x87, 0x8a, + 0xee, 0xa0, 0xdd, 0x08, 0x59, 0xaf, 0x19, 0x32, 0xde, 0x63, 0xdc, 0xfc, 0x6c, 0xf2, 0xe8, 0xa4, + 0x29, 0xe3, 0xe0, 0x8d, 0x27, 0x34, 0x11, 0xbe, 0xd9, 0x8e, 0x3e, 0x82, 0x12, 0x6b, 0x73, 0x92, + 0x9e, 0x92, 0x28, 0xe8, 0x62, 0xde, 0xf5, 0x16, 0x94, 0x9b, 0xa2, 0x05, 0x1f, 0x60, 0xde, 0x45, + 0x9f, 0x83, 0x37, 0x5e, 0x44, 0x86, 0x82, 0xa4, 0x09, 0x8e, 0x83, 0x2e, 0xa1, 0x9d, 0xae, 0xf0, + 0x72, 0x6b, 0x4e, 0x3d, 0xe3, 0xaf, 0x58, 0xfb, 0x3d, 0x63, 0x7e, 0xa0, 0xac, 0xe8, 0xff, 0x50, + 0x6c, 0xe3, 0x38, 0x66, 0x22, 0xa0, 0x49, 0x44, 0x86, 0x5e, 0x5e, 0xb1, 0x17, 0x34, 0xb6, 0x27, + 0x21, 0xd4, 0x82, 0x2b, 0xc7, 0x34, 0xc1, 0x31, 0x7d, 0x4e, 0xa2, 0x40, 0xa6, 0xc4, 0x32, 0x83, + 0x62, 0x5e, 0x1e, 0x1b, 0x9f, 0x11, 0x81, 0x0d, 0x2d, 0x85, 0x15, 0x31, 0x0c, 0x8c, 0x05, 0x0b, + 0xca, 0x92, 0x80, 0x0b, 0x2c, 0x06, 0xdc, 0x2b, 0xa8, 0x2c, 0xdf, 0x6d, 0x5c, 0x58, 0x45, 0x8d, + 0xc3, 0xe1, 0xee, 0x99, 0xbd, 0x8f, 0xd5, 0x56, 0xbf, 0x22, 0xce, 0x41, 0x6b, 0xdf, 0x41, 0x59, + 0x3a, 0xde, 0x0a, 0x43, 0x99, 0x2f, 0x9a, 0x74, 0x50, 0x00, 0xcb, 0xb8, 0xcd, 0x52, 0x61, 0xc3, + 0x35, 0x07, 0xe1, 0x7c, 0xd8, 0x41, 0x2c, 0x19, 0x2e, 0xe5, 0x44, 0x31, 0xd5, 0x8e, 0xa0, 0xb0, + 0x83, 0xe3, 0x78, 0xbf, 0x2f, 0xc3, 0xe0, 0xb2, 0xc4, 0x3a, 0x98, 0x07, 0x31, 0xed, 0x51, 0xed, + 0x25, 0xe3, 0xe7, 0x3a, 0x98, 0x7f, 0x2d, 0xe7, 0x68, 0x03, 0x96, 0x28, 0x0f, 0x70, 0xda, 0xa6, + 0x22, 0xc5, 0xe9, 0x28, 0x08, 0x71, 0x1c, 0xab, 0x4a, 0xcd, 0xf9, 0x8b, 0x94, 0x6f, 0x59, 0x5c, + 0xf2, 0xd5, 0x5e, 0x67, 0xa1, 0xbc, 0x3f, 0x10, 0x67, 0xcb, 0x7f, 0x15, 0x72, 0x29, 0x09, 0x09, + 0x3d, 0x1d, 0x0b, 0x60, 0x3c, 0x47, 0xb7, 0xc1, 0xb5, 0x63, 0x2d, 0x82, 0x3d, 0xab, 0x81, 0x45, + 0x8b, 0x5b, 0x15, 0x4c, 0x15, 0xfa, 0xdc, 0x87, 0x15, 0xfa, 0xa4, 0xa4, 0x33, 0xff, 0xae, 0xa4, + 0xa5, 0x24, 0x39, 0x0f, 0x12, 0x96, 0x84, 0x44, 0xa9, 0x26, 0xe3, 0xe7, 0x04, 0xe7, 0x8f, 0xe4, + 0x7c, 0x3a, 0x99, 0xd9, 0x77, 0x92, 0x69, 0x8c, 0xfd, 0x94, 0x86, 0xc4, 0x08, 0x41, 0x1a, 0x0f, + 0xe4, 0x1c, 0xd5, 0xc1, 0x35, 0x46, 0x96, 0x52, 0x31, 0x0a, 0x8e, 0x09, 0xf1, 0xae, 0xaa, 0x35, + 0x65, 0xbd, 0x46, 0xc1, 0xbb, 0x84, 0x20, 0x04, 0x19, 0x25, 0xa5, 0x9c, 0xb2, 0xaa, 0xf1, 0xfb, + 0x08, 0xe1, 0x22, 0x95, 0xc1, 0x85, 0x2a, 0xbb, 0x06, 0x32, 0xcc, 0x60, 0xc0, 0x49, 0xe4, 0x55, + 0xd4, 0xca, 0x85, 0x0e, 0xe6, 0x4f, 0x38, 0x89, 0xd0, 0x37, 0xb0, 0x4c, 0x8e, 0x8f, 0x49, 0x28, + 0xe8, 0x29, 0x09, 0x26, 0x1f, 0x77, 0x45, 0xa5, 0xb8, 0x61, 0x52, 0x7c, 0xf3, 0x3d, 0x52, 0xbc, + 0x27, 0x6b, 0x75, 0x4c, 0x75, 0xdf, 0x66, 0xa5, 0xf1, 0x2e, 0xbf, 0xce, 0xec, 0x8a, 0x8a, 0x62, + 0x6a, 0xbd, 0x4e, 0xf1, 0x75, 0x00, 0x79, 0x38, 0xfd, 0x41, 0xfb, 0x84, 0x8c, 0x94, 0x5a, 0xf3, + 0xbe, 0x3c, 0xae, 0x03, 0x05, 0x5c, 0x20, 0xec, 0xe2, 0x7f, 0x2c, 0x6c, 0xf4, 0x10, 0x8a, 0x52, + 0x2c, 0x01, 0xd3, 0x32, 0xf3, 0xbc, 0x35, 0xa7, 0x5e, 0x68, 0x6d, 0x5c, 0xe2, 0xe0, 0x8c, 0x30, + 0xfd, 0x42, 0x38, 0x99, 0x7c, 0x99, 0xc9, 0x95, 0xdc, 0x4a, 0xed, 0xe7, 0x59, 0xc8, 0x1a, 0xfe, + 0x2d, 0xc8, 0x9a, 0xd0, 0x1d, 0x15, 0xfa, 0xed, 0xcb, 0x98, 0x43, 0x31, 0x34, 0x01, 0x9b, 0x8d, + 0x68, 0x1d, 0xca, 0x7a, 0x14, 0xf4, 0x08, 0xe7, 0xb8, 0x43, 0x94, 0xfe, 0xf2, 0x7e, 0x49, 0xa3, + 0x0f, 0x35, 0x28, 0x7b, 0x38, 0x49, 0x53, 0x96, 0x8e, 0x57, 0x65, 0x75, 0x0f, 0x57, 0xa0, 0x5d, + 0x74, 0x07, 0x2a, 0x31, 0xe6, 0xe2, 0x49, 0x3f, 0xc2, 0x82, 0x04, 0x82, 0xf6, 0x08, 0x17, 0xb8, + 0xd7, 0x57, 0x6a, 0x9d, 0xf3, 0x97, 0x27, 0xb6, 0x43, 0x6b, 0x42, 0x75, 0x90, 0x2d, 0x44, 0xb6, + 0x27, 0x9f, 0x1c, 0x0f, 0x92, 0x88, 0x44, 0x4a, 0x9a, 0xba, 0xb3, 0x9c, 0x85, 0xd1, 0x27, 0xb0, + 0x14, 0xa6, 0x04, 0xcb, 0x96, 0x38, 0x61, 0x9e, 0x57, 0xcc, 0xae, 0x31, 0x8c, 0x69, 0x6b, 0x3f, + 0xcc, 0x42, 0xc9, 0x27, 0xa7, 0x24, 0x15, 0xb6, 0xc3, 0xad, 0x43, 0x39, 0x55, 0x40, 0x80, 0xa3, + 0x28, 0x25, 0x9c, 0x9b, 0x5e, 0x54, 0xd2, 0xe8, 0x96, 0x06, 0xd1, 0xc7, 0x50, 0xd6, 0x27, 0x96, + 0x04, 0xda, 0x60, 0x1a, 0x9d, 0x3a, 0xc7, 0xfd, 0x44, 0x73, 0xca, 0x6c, 0xa8, 0x96, 0x3a, 0xe6, + 0xd2, 0xb7, 0x72, 0x51, 0x81, 0x96, 0x6a, 0xe2, 0xd1, 0xe6, 0x4c, 0x7e, 0x59, 0xd1, 0x7a, 0xb4, + 0x49, 0x7b, 0x2a, 0x5b, 0xa0, 0x5a, 0x36, 0x29, 0xed, 0xf9, 0x0f, 0xeb, 0x4e, 0xc6, 0x9f, 0x15, + 0x42, 0xed, 0xc7, 0x79, 0x28, 0xee, 0xc8, 0xd3, 0x57, 0x3d, 0xf4, 0x70, 0x88, 0x3c, 0x58, 0x50, + 0xa9, 0x62, 0xb6, 0x13, 0xdb, 0xa9, 0x7c, 0x02, 0xe8, 0xa6, 0xa1, 0x4f, 0x5f, 0x4f, 0xd0, 0xb7, + 0x90, 0x57, 0xd7, 0xcf, 0x31, 0x21, 0xdc, 0x04, 0xb5, 0xf3, 0x0f, 0x83, 0xfa, 0xf3, 0xd5, 0x0d, + 0x77, 0x84, 0x7b, 0xf1, 0x17, 0xb5, 0x31, 0x53, 0xcd, 0xcf, 0xc9, 0xf1, 0x2e, 0x21, 0x1c, 0xdd, + 0x82, 0xc5, 0x94, 0xc4, 0x78, 0x44, 0xa2, 0x77, 0x2a, 0xab, 0x6c, 0x60, 0x9b, 0xa6, 0x5d, 0x28, + 0x84, 0xa1, 0x18, 0x5a, 0xa9, 0xe6, 0x94, 0x92, 0xd6, 0x2f, 0xa9, 0x77, 0x53, 0xeb, 0x10, 0x8e, + 0xeb, 0x1e, 0x3d, 0x86, 0x32, 0xd5, 0xaf, 0xb3, 0xa0, 0xaf, 0xee, 0x27, 0xd5, 0x26, 0x0b, 0xad, + 0x4f, 0x2f, 0xa1, 0x9a, 0x7a, 0xd2, 0xf9, 0x25, 0x3a, 0xf5, 0xc2, 0x3b, 0x82, 0x45, 0x66, 0x2e, + 0x3d, 0xcb, 0x0a, 0x6b, 0x73, 0xf5, 0x42, 0x6b, 0xf3, 0x12, 0xd6, 0xe9, 0xab, 0xd2, 0x2f, 0xb3, + 0xe9, 0xab, 0x33, 0x85, 0x6b, 0xea, 0x51, 0x19, 0xb2, 0x38, 0x08, 0x59, 0x22, 0x52, 0x1c, 0x8a, + 0xe0, 0x94, 0xa4, 0x9c, 0xb2, 0xc4, 0x3c, 0x43, 0x3e, 0xbb, 0xc4, 0xc3, 0x81, 0xd9, 0xbf, 0x63, + 0xb6, 0x1f, 0xe9, 0xdd, 0xfe, 0xd5, 0xfe, 0xf9, 0x06, 0xf4, 0x74, 0x5c, 0xb6, 0xb6, 0x6b, 0x15, + 0xdf, 0x2b, 0x41, 0x53, 0x72, 0xdb, 0xce, 0xc8, 0x32, 0xb1, 0xa5, 0x6e, 0xc0, 0x8d, 0xef, 0x01, + 0x26, 0x1d, 0x08, 0x21, 0x28, 0x1f, 0x90, 0x24, 0xa2, 0x49, 0xc7, 0xe4, 0xd6, 0x9d, 0x41, 0xcb, + 0xb0, 0x68, 0x30, 0x9b, 0x19, 0xd7, 0x41, 0x4b, 0x50, 0xb2, 0xb3, 0x87, 0x34, 0x21, 0x91, 0x3b, + 0x27, 0x21, 0xb3, 0x4e, 0xbb, 0x75, 0x33, 0xa8, 0x08, 0x39, 0x3d, 0x26, 0x91, 0x3b, 0x8f, 0x0a, + 0xb0, 0xb0, 0xa5, 0x1f, 0x3d, 0x6e, 0x76, 0x35, 0xf3, 0xeb, 0x2f, 0x55, 0x67, 0xe3, 0x2b, 0xa8, + 0x9c, 0xd7, 0xba, 0x91, 0x0b, 0xc5, 0x47, 0x4c, 0xec, 0xda, 0x27, 0xa0, 0x3b, 0x83, 0x4a, 0x90, + 0x9f, 0x4c, 0x1d, 0xc9, 0x7c, 0x6f, 0x48, 0xc2, 0x81, 0x24, 0x9b, 0x35, 0x64, 0x4d, 0xb8, 0xfa, + 0x37, 0x99, 0x45, 0x59, 0x98, 0x3d, 0xba, 0xe3, 0xce, 0xa8, 0xdf, 0x96, 0xeb, 0xe8, 0x0d, 0xdb, + 0xf7, 0x5f, 0xbc, 0xa9, 0x3a, 0x2f, 0xdf, 0x54, 0x9d, 0xd7, 0x6f, 0xaa, 0xce, 0x4f, 0x6f, 0xab, + 0x33, 0x2f, 0xdf, 0x56, 0x67, 0x7e, 0x7f, 0x5b, 0x9d, 0x79, 0xb6, 0x79, 0x46, 0x49, 0x32, 0xb1, + 0x9b, 0xfa, 0x4f, 0x46, 0xc2, 0x22, 0xd2, 0x1c, 0x9e, 0xfd, 0x2f, 0xa3, 0x44, 0xd5, 0xce, 0xaa, + 0x83, 0xbb, 0xfb, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x84, 0x70, 0x56, 0x74, 0xf9, 0x0c, 0x00, + 0x00, } func (m *InboundParams) Marshal() (dAtA []byte, err error) { @@ -1172,6 +1185,13 @@ func (m *Status) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ErrorMessage) > 0 { + i -= len(m.ErrorMessage) + copy(dAtA[i:], m.ErrorMessage) + i = encodeVarintCrossChainTx(dAtA, i, uint64(len(m.ErrorMessage))) + i-- + dAtA[i] = 0x32 + } if m.CreatedTimestamp != 0 { i = encodeVarintCrossChainTx(dAtA, i, uint64(m.CreatedTimestamp)) i-- @@ -1548,6 +1568,10 @@ func (m *Status) Size() (n int) { if m.CreatedTimestamp != 0 { n += 1 + sovCrossChainTx(uint64(m.CreatedTimestamp)) } + l = len(m.ErrorMessage) + if l > 0 { + n += 1 + l + sovCrossChainTx(uint64(l)) + } return n } @@ -2773,6 +2797,38 @@ func (m *Status) Unmarshal(dAtA []byte) error { break } } + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ErrorMessage", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCrossChainTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthCrossChainTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthCrossChainTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ErrorMessage = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipCrossChainTx(dAtA[iNdEx:]) diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index d9fea4a77d..76d343dd3e 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -3,7 +3,7 @@ package types import ( "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" + "cosmossdk.io/math" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum/go-ethereum/common" ) @@ -29,8 +29,8 @@ const ( CCTXIndexLength = 66 ) -func GetProtocolFee() sdk.Uint { - return sdk.NewUint(ProtocolFee) +func GetProtocolFee() math.Uint { + return math.NewUint(ProtocolFee) } func KeyPrefix(p string) []byte { diff --git a/x/crosschain/types/rate_limiter_flags_test.go b/x/crosschain/types/rate_limiter_flags_test.go index 8531a34105..545ef708a7 100644 --- a/x/crosschain/types/rate_limiter_flags_test.go +++ b/x/crosschain/types/rate_limiter_flags_test.go @@ -6,7 +6,6 @@ import ( "testing" "cosmossdk.io/math" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -263,7 +262,7 @@ func TestConvertCctxValue(t *testing.T) { erc20AssetRates map[int64]map[string]types.AssetRate // output - expectedValue sdkmath.Int + expectedValue math.Int }{ { name: "should convert cctx ZETA value correctly", diff --git a/x/crosschain/types/status.go b/x/crosschain/types/status.go index cdaf5b0aae..00c08bf6f7 100644 --- a/x/crosschain/types/status.go +++ b/x/crosschain/types/status.go @@ -9,28 +9,45 @@ func (m *Status) AbortRefunded() { m.StatusMessage = "CCTX aborted and Refunded" } -// ChangeStatus changes the status of the cross chain transaction -// empty msg does not overwrite old status message -func (m *Status) ChangeStatus(newStatus CctxStatus, msg string) { - if len(msg) > 0 { - if m.StatusMessage != "" { - m.StatusMessage = fmt.Sprintf("%s : %s", m.StatusMessage, msg) - } else { - m.StatusMessage = msg - } +// UpdateStatusAndErrorMessages transitions the Status and Error messages. +func (m *Status) UpdateStatusAndErrorMessages(newStatus CctxStatus, statusMsg, errorMsg string) { + m.UpdateStatus(newStatus, statusMsg) + + if newStatus == CctxStatus_Aborted || newStatus == CctxStatus_Reverted || newStatus == CctxStatus_PendingRevert { + m.UpdateErrorMessage(errorMsg) } - if !m.ValidateTransition(newStatus) { +} + +// UpdateStatus updates the cctx status and cctx.status.status_message. +func (m *Status) UpdateStatus(newStatus CctxStatus, statusMsg string) { + if m.ValidateTransition(newStatus) { + m.StatusMessage = fmt.Sprintf("Status changed from %s to %s", m.Status.String(), newStatus.String()) + m.Status = newStatus + } else { m.StatusMessage = fmt.Sprintf( - "Failed to transition : OldStatus %s , NewStatus %s , MSG : %s :", + "Failed to transition status from %s to %s", m.Status.String(), newStatus.String(), - msg, ) + m.Status = CctxStatus_Aborted - return } - m.Status = newStatus -} //nolint:typecheck + + if statusMsg != "" { + m.StatusMessage += fmt.Sprintf(": %s", statusMsg) + } +} + +// UpdateErrorMessage updates cctx.status.error_message. +func (m *Status) UpdateErrorMessage(errorMsg string) { + errMsg := errorMsg + + if errMsg == "" { + errMsg = "unknown error" + } + + m.ErrorMessage = errMsg +} func (m *Status) ValidateTransition(newStatus CctxStatus) bool { stateTransitionMap := stateTransitionMap() diff --git a/x/crosschain/types/status_test.go b/x/crosschain/types/status_test.go index 88013a41a9..5bb272d522 100644 --- a/x/crosschain/types/status_test.go +++ b/x/crosschain/types/status_test.go @@ -140,31 +140,35 @@ func TestStatus_ChangeStatus(t *testing.T) { t.Run("should change status and msg if transition is valid", func(t *testing.T) { s := types.Status{Status: types.CctxStatus_PendingInbound} - s.ChangeStatus(types.CctxStatus_PendingOutbound, "msg") + s.UpdateStatus(types.CctxStatus_PendingOutbound, "msg") assert.Equal(t, s.Status, types.CctxStatus_PendingOutbound) - assert.Equal(t, s.StatusMessage, "msg") + assert.Equal(t, s.StatusMessage, "Status changed from PendingInbound to PendingOutbound: msg") }) t.Run("should change status if transition is valid", func(t *testing.T) { s := types.Status{Status: types.CctxStatus_PendingInbound} - s.ChangeStatus(types.CctxStatus_PendingOutbound, "") + s.UpdateStatus(types.CctxStatus_PendingOutbound, "") + fmt.Printf("%+v\n", s) assert.Equal(t, s.Status, types.CctxStatus_PendingOutbound) - assert.Equal(t, s.StatusMessage, "") + assert.Equal(t, s.StatusMessage, fmt.Sprintf( + "Status changed from %s to %s", + types.CctxStatus_PendingInbound.String(), + types.CctxStatus_PendingOutbound.String()), + ) }) t.Run("should change status to aborted and msg if transition is invalid", func(t *testing.T) { s := types.Status{Status: types.CctxStatus_PendingOutbound} - s.ChangeStatus(types.CctxStatus_PendingInbound, "msg") + s.UpdateStatus(types.CctxStatus_PendingInbound, "msg") assert.Equal(t, s.Status, types.CctxStatus_Aborted) assert.Equal( t, fmt.Sprintf( - "Failed to transition : OldStatus %s , NewStatus %s , MSG : %s :", + "Failed to transition status from %s to %s: msg", types.CctxStatus_PendingOutbound.String(), types.CctxStatus_PendingInbound.String(), - "msg", ), s.StatusMessage, ) 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 117b921168..6d05d97371 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -165,6 +165,10 @@ func (c Chain) IsSolana() bool { return chains.IsSolanaChain(c.ID(), c.registry.additionalChains) } +func (c Chain) IsTON() bool { + return chains.IsTONChain(c.ID(), c.registry.additionalChains) +} + // RelayerKeyPassword returns the relayer key password for the chain func (c Chain) RelayerKeyPassword() string { network := c.RawChain().Network diff --git a/zetaclient/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)