From 7c0b8651b525ae7d062fc808aabf5136c0b21773 Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:18:18 -0600 Subject: [PATCH] feat: fast inbound confirmation for EVM and Bitcoin chain (#3551) * initiate fast confirmation for inbound * add changelog and switch EVM chain to FAST mode * increase default multiplier to 0.00025 * implement evm and btc chain fast inbound comfirmation; add E2E tests * make changelog explicit; go mod tidy; clean up uncessary logic * wrap common code into updateZRC20LiquidityCap; fix typo in bitcoin fast confirmation E2E * created a e2e util function WaitForZetaBlocks to wait for give number of Zeta blocks * ensure FAST confirmation mode works faster than SAFE mode in E2E; replace argument type string with common.Address * add description to RPC messages; clean up unnecessary lib; replace asset string with common.Address * rename method IsFungible as IsAsset * use default liquidity cap divisor unless custom number is provided; move divisor related file to chains pkg; renaming and comments * remove method GetInboundConfirmationMode to avoid opaque assumption * increase Bitcoin SAFE confirmation to fix E2E failure * add extra description for the liquidity cap divisor 4000 Co-authored-by: Lucas Bertrand --------- Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> Co-authored-by: Lucas Bertrand --- changelog.md | 1 + cmd/zetae2e/local/bitcoin.go | 10 +- cmd/zetae2e/local/evm.go | 1 + docs/openapi/openapi.swagger.yaml | 33 ++ e2e/e2etests/e2etests.go | 14 + e2e/e2etests/helpers.go | 4 - ...tcoin_deposit_and_call_revert_with_dust.go | 4 - e2e/e2etests/test_bitcoin_deposit_call.go | 4 - .../test_bitcoin_deposit_fast_confirmation.go | 112 ++++ e2e/e2etests/test_bitcoin_donation.go | 4 - e2e/e2etests/test_bitcoin_std_deposit.go | 4 - .../test_bitcoin_std_deposit_and_call.go | 4 - ...oin_std_memo_inscribed_deposit_and_call.go | 4 - e2e/e2etests/test_crosschain_swap.go | 4 - .../test_eth_deposit_fast_confirmation.go | 113 ++++ .../test_eth_deposit_liquidity_cap.go | 17 +- e2e/runner/bitcoin.go | 11 +- e2e/txserver/liquidity_cap.go | 45 ++ e2e/utils/zetacore.go | 35 ++ pkg/chains/fast_confirmation.go | 34 ++ pkg/chains/fast_confirmation_test.go | 45 ++ pkg/coin/coin.go | 6 + pkg/coin/coin_test.go | 22 + proto/zetachain/zetacore/fungible/query.proto | 20 + .../zetachain/zetacore/fungible/query_pb.d.ts | 59 ++ x/crosschain/types/message_vote_inbound.go | 22 + .../types/message_vote_inbound_test.go | 48 ++ x/fungible/keeper/foreign_coins_test.go | 5 +- x/fungible/keeper/grpc_query_foreign_coins.go | 18 + .../keeper/grpc_query_foreign_coins_test.go | 68 ++- x/fungible/types/query.pb.go | 560 +++++++++++++++--- x/fungible/types/query.pb.gw.go | 123 ++++ x/observer/types/chain_params.go | 6 + x/observer/types/chain_params_test.go | 17 + zetaclient/chains/base/confirmation.go | 53 +- zetaclient/chains/base/confirmation_test.go | 118 +++- zetaclient/chains/base/observer.go | 7 +- zetaclient/chains/bitcoin/observer/event.go | 18 +- .../chains/bitcoin/observer/event_test.go | 6 + zetaclient/chains/bitcoin/observer/inbound.go | 66 ++- zetaclient/chains/evm/observer/inbound.go | 34 +- zetaclient/chains/evm/observer/v2_inbound.go | 26 +- zetaclient/chains/interfaces/interfaces.go | 6 + zetaclient/logs/fields.go | 27 +- zetaclient/testutils/mocks/zetacore_client.go | 32 + zetaclient/zetacore/broadcast_test.go | 4 +- zetaclient/zetacore/client.go | 28 +- zetaclient/zetacore/client_fungible.go | 41 ++ zetaclient/zetacore/client_fungible_test.go | 79 +++ zetaclient/zetacore/client_test.go | 10 +- zetaclient/zetacore/tx_test.go | 16 +- 51 files changed, 1822 insertions(+), 226 deletions(-) create mode 100644 e2e/e2etests/test_bitcoin_deposit_fast_confirmation.go create mode 100644 e2e/e2etests/test_eth_deposit_fast_confirmation.go create mode 100644 e2e/txserver/liquidity_cap.go create mode 100644 pkg/chains/fast_confirmation.go create mode 100644 pkg/chains/fast_confirmation_test.go create mode 100644 zetaclient/zetacore/client_fungible.go create mode 100644 zetaclient/zetacore/client_fungible_test.go diff --git a/changelog.md b/changelog.md index f9c3c717b8..5fd7a3f8dd 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ * [3548](https://github.com/zeta-chain/node/pull/3548) - ensure cctx list is sorted by creation time * [3562](https://github.com/zeta-chain/node/pull/3562) - add Sui withdrawals * [3600](https://github.com/zeta-chain/node/pull/3600) - add dedicated zetaclient restricted addresses config. This file will be automatically reloaded when it changes without needing to restart zetaclient. +* [3551](https://github.com/zeta-chain/node/pull/3551) - support for EVM chain and Bitcoin chain inbound fast confirmation ### Refactor diff --git a/cmd/zetae2e/local/bitcoin.go b/cmd/zetae2e/local/bitcoin.go index c77a34267e..b99ff8c053 100644 --- a/cmd/zetae2e/local/bitcoin.go +++ b/cmd/zetae2e/local/bitcoin.go @@ -27,6 +27,7 @@ func startBitcoinTests( bitcoinDepositTests := []string{ e2etests.TestBitcoinDonationName, e2etests.TestBitcoinDepositName, + e2etests.TestBitcoinDepositFastConfirmationName, e2etests.TestBitcoinDepositAndCallName, e2etests.TestBitcoinDepositAndCallRevertName, e2etests.TestBitcoinStdMemoDepositName, @@ -141,7 +142,14 @@ func initBitcoinRunner( verbose, initNetwork, createWallet bool, ) *runner.E2ERunner { // initialize runner for bitcoin test - runner, err := initTestRunner(name, conf, deployerRunner, account, runner.NewLogger(verbose, printColor, name)) + runner, err := initTestRunner( + name, + conf, + deployerRunner, + account, + runner.NewLogger(verbose, printColor, name), + runner.WithZetaTxServer(deployerRunner.ZetaTxServer), + ) testutil.NoError(err) // setup TSS address and setup deployer wallet diff --git a/cmd/zetae2e/local/evm.go b/cmd/zetae2e/local/evm.go index 61dc8eafa9..44077b9f03 100644 --- a/cmd/zetae2e/local/evm.go +++ b/cmd/zetae2e/local/evm.go @@ -18,6 +18,7 @@ func startEVMTests(eg *errgroup.Group, conf config.Config, deployerRunner *runne eg.Go(evmTestRoutine(conf, "eth", conf.AdditionalAccounts.UserEther, color.FgHiGreen, deployerRunner, verbose, e2etests.TestETHDepositName, e2etests.TestETHDepositAndCallName, + e2etests.TestETHDepositFastConfirmationName, e2etests.TestETHWithdrawName, e2etests.TestETHWithdrawAndArbitraryCallName, e2etests.TestETHWithdrawAndCallName, diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index dd56e3b4e7..e364472e75 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -28188,6 +28188,31 @@ paths: type: boolean tags: - Query + /zeta-chain/fungible/foreign_coins/{chainId}/{asset}: + get: + summary: Queries a ForeignCoins by chain_id and asset. + operationId: ForeignCoinsFromAsset + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/google.rpc.Status' + parameters: + - name: chainId + in: path + required: true + type: string + format: int64 + - name: asset + in: path + required: true + type: string + tags: + - Query /zeta-chain/fungible/foreign_coins/{index}: get: summary: Queries a ForeignCoins by index. @@ -58246,6 +58271,14 @@ definitions: properties: codeHash: type: string + zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetResponse: + type: object + properties: + foreignCoins: + $ref: '#/definitions/zetachain.zetacore.fungible.ForeignCoins' + description: |- + QueryGetForeignCoinsFromAssetResponse defines the response type for the + ForeignCoinsFromAsset RPC method. zetachain.zetacore.fungible.QueryGetForeignCoinsResponse: type: object properties: diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 9d3c2c9733..0fd4e39939 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -13,6 +13,7 @@ const ( */ TestETHDepositName = "eth_deposit" TestETHDepositAndCallName = "eth_deposit_and_call" + TestETHDepositFastConfirmationName = "eth_deposit_fast_confirmation" TestETHDepositAndCallNoMessageName = "eth_deposit_and_call_no_message" TestETHDepositAndCallRevertName = "eth_deposit_and_call_revert" TestETHDepositAndCallRevertWithCallName = "eth_deposit_and_call_revert_with_call" @@ -98,6 +99,7 @@ const ( */ TestBitcoinDepositName = "bitcoin_deposit" TestBitcoinDepositAndCallName = "bitcoin_deposit_and_call" + TestBitcoinDepositFastConfirmationName = "bitcoin_deposit_fast_confirmation" TestBitcoinDepositAndCallRevertName = "bitcoin_deposit_and_call_revert" TestBitcoinDepositAndCallRevertWithDustName = "bitcoin_deposit_and_call_revert_with_dust" TestBitcoinDepositAndWithdrawWithDustName = "bitcoin_deposit_and_withdraw_with_dust" @@ -245,6 +247,12 @@ var AllE2ETests = []runner.E2ETest{ }, TestETHDepositAndCall, ), + runner.NewE2ETest( + TestETHDepositFastConfirmationName, + "deposit Ether into ZEVM using fast confirmation", + []runner.ArgDefinition{}, + TestETHDepositFastConfirmation, + ), runner.NewE2ETest( TestETHDepositAndCallNoMessageName, "deposit Ether into ZEVM and call a contract using no message content", @@ -739,6 +747,12 @@ var AllE2ETests = []runner.E2ETest{ }, TestBitcoinDeposit, ), + runner.NewE2ETest( + TestBitcoinDepositFastConfirmationName, + "deposit Bitcoin into ZEVM using fast confirmation", + []runner.ArgDefinition{}, + TestBitcoinDepositFastConfirmation, + ), runner.NewE2ETest( TestBitcoinDepositAndCallName, "deposit Bitcoin into ZEVM and call a contract", diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go index d1820491a8..774f564857 100644 --- a/e2e/e2etests/helpers.go +++ b/e2e/e2etests/helpers.go @@ -46,10 +46,6 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequireTxSuccessful(r, receipt) - // mine 10 blocks to confirm the withdrawal tx - _, err = r.GenerateToAddressIfLocalBitcoin(10, to) - require.NoError(r, err) - // get cctx and check status cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, receipt.TxHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go index bee2359da9..ceaa23b93b 100644 --- a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go @@ -15,10 +15,6 @@ import ( // TestBitcoinDepositAndCallRevertWithDust sends a Bitcoin deposit that reverts with a dust amount in the revert outbound. func TestBitcoinDepositAndCallRevertWithDust(r *runner.E2ERunner, args []string) { - // Given "Live" BTC network - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - require.Len(r, args, 0) // 0.002 BTC is consumed in a deposit and revert, the dust is set to 1000 satoshis in the protocol diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index b74f1061f0..b1e7c8fbc2 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -13,10 +13,6 @@ import ( ) func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { - // Given "Live" BTC network - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - // Given amount to send require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) diff --git a/e2e/e2etests/test_bitcoin_deposit_fast_confirmation.go b/e2e/e2etests/test_bitcoin_deposit_fast_confirmation.go new file mode 100644 index 0000000000..eba0208f2f --- /dev/null +++ b/e2e/e2etests/test_bitcoin_deposit_fast_confirmation.go @@ -0,0 +1,112 @@ +package e2etests + +import ( + "math/big" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/btcutil" + "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/utils" + "github.com/zeta-chain/node/pkg/chains" + mathpkg "github.com/zeta-chain/node/pkg/math" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// TestBitcoinDepositFastConfirmation tests the fast confirmation of Bitcoin deposits +func TestBitcoinDepositFastConfirmation(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0) + + // ARRANGE + // enable inbound fast confirmation by updating the chain params + chainID := r.GetBitcoinChainID() + reqQuery := &observertypes.QueryGetChainParamsForChainRequest{ChainId: chainID} + resOldChainParams, err := r.ObserverClient.GetChainParamsForChain(r.Ctx, reqQuery) + require.NoError(r, err) + + // define new confirmation params + chainParams := *resOldChainParams.ChainParams + chainParams.ConfirmationParams = &observertypes.ConfirmationParams{ + SafeInboundCount: 6, // approx 36 seconds, much longer than Fast confirmation time (6 second) + FastInboundCount: 1, + SafeOutboundCount: 1, + FastOutboundCount: 1, + } + err = r.ZetaTxServer.UpdateChainParams(&chainParams) + require.NoError(r, err, "failed to enable inbound fast confirmation") + + // it takes 1 Zeta block time for zetaclient to pick up the new chain params + // wait for 2 blocks to ensure the new chain params are effective + utils.WaitForZetaBlocks(r.Ctx, r, r.ZEVMClient, 2, 20*time.Second) + r.Logger.Info("enabled inbound fast confirmation") + + // query current BTC ZRC20 supply + supply, err := r.BTCZRC20.TotalSupply(&bind.CallOpts{}) + supplyUint := sdkmath.NewUintFromBigInt(supply) + require.NoError(r, err) + + // set ZRC20 liquidity cap to 150% of the current supply + // note: the percentage should not be too small as it may block other tests + liquidityCap, _ := mathpkg.IncreaseUintByPercent(supplyUint, 50) + require.True(r, liquidityCap.GT(sdkmath.ZeroUint())) + res, err := r.ZetaTxServer.SetZRC20LiquidityCap(r.BTCZRC20Addr, liquidityCap) + require.NoError(r, err) + r.Logger.Info("set liquidity cap to %s tx hash: %s", liquidityCap.String(), res.TxHash) + + // ACT-1 + // deposit with exactly fast amount cap, should be fast confirmed + fastAmountCap := chains.CalcInboundFastConfirmationAmountCap(chainID, liquidityCap) + fastAmountCapFloat := float64(fastAmountCap.Uint64()) / btcutil.SatoshiPerBitcoin + txHash := r.DepositBTCWithExactAmount(fastAmountCapFloat, nil) + r.Logger.Info("deposited exactly fast amount %d cap tx hash: %s", fastAmountCap, txHash) + + // ASSERT-1 + // wait for the cctx to be FAST confirmed + timeStart := time.Now() + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + require.Equal(r, crosschaintypes.ConfirmationMode_FAST, cctx.InboundParams.ConfirmationMode) + fastConfirmTime := time.Since(timeStart) + + r.Logger.Info("FAST confirmed deposit succeeded in %f seconds", fastConfirmTime.Seconds()) + + // ACT-2 + // deposit with amount more than fast amount cap + amountMoreThanCap := big.NewInt(0).Add(fastAmountCap.BigInt(), big.NewInt(1)) + amountMoreThanCapFloat := float64(amountMoreThanCap.Uint64()) / btcutil.SatoshiPerBitcoin + txHash = r.DepositBTCWithExactAmount(amountMoreThanCapFloat, nil) + r.Logger.Info("deposited more than fast amount cap %d tx hash: %s", amountMoreThanCap, txHash) + + // mine blocks at normal speed + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // ASSERT-2 + // wait for the cctx to be SAFE confirmed + timeStart = time.Now() + cctx = utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + require.Equal(r, crosschaintypes.ConfirmationMode_SAFE, cctx.InboundParams.ConfirmationMode) + safeConfirmTime := time.Since(timeStart) + + r.Logger.Info("SAFE confirmed deposit succeeded in %f seconds", safeConfirmTime.Seconds()) + + // ensure FAST confirmation is faster than SAFE confirmation + // using one BTC block time is good enough to check the difference + timeSaved := safeConfirmTime - fastConfirmTime + r.Logger.Info("FAST confirmation saved %f seconds", timeSaved.Seconds()) + require.True(r, timeSaved > runner.BTCRegnetBlockTime) + + // TEARDOWN + // restore old chain params + err = r.ZetaTxServer.UpdateChainParams(resOldChainParams.ChainParams) + require.NoError(r, err, "failed to restore chain params") + + // remove the liquidity cap + _, err = r.ZetaTxServer.RemoveZRC20LiquidityCap(r.BTCZRC20Addr) + require.NoError(r, err) +} diff --git a/e2e/e2etests/test_bitcoin_donation.go b/e2e/e2etests/test_bitcoin_donation.go index 38dbf33290..bbdfd8a74a 100644 --- a/e2e/e2etests/test_bitcoin_donation.go +++ b/e2e/e2etests/test_bitcoin_donation.go @@ -13,10 +13,6 @@ import ( ) func TestBitcoinDonation(r *runner.E2ERunner, args []string) { - // Given "Live" BTC network - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - // Given amount to send require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) diff --git a/e2e/e2etests/test_bitcoin_std_deposit.go b/e2e/e2etests/test_bitcoin_std_deposit.go index 4fb72b23bc..2db5833ecf 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit.go +++ b/e2e/e2etests/test_bitcoin_std_deposit.go @@ -14,10 +14,6 @@ import ( ) func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { - // start mining blocks if local bitcoin - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - // parse amount to deposit require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go index 1f99732500..e8eddf67a0 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go @@ -14,10 +14,6 @@ import ( ) func TestBitcoinStdMemoDepositAndCall(r *runner.E2ERunner, args []string) { - // start mining blocks if local bitcoin - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - // parse amount to deposit require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) diff --git a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go index baf83ee05a..5a0e997bfa 100644 --- a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go @@ -14,10 +14,6 @@ import ( ) func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []string) { - // Start mining blocks - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - // Given amount to send and fee rate require.Len(r, args, 2) amount := utils.ParseFloat(r, args[0]) diff --git a/e2e/e2etests/test_crosschain_swap.go b/e2e/e2etests/test_crosschain_swap.go index 183030a7f9..7f8f989ced 100644 --- a/e2e/e2etests/test_crosschain_swap.go +++ b/e2e/e2etests/test_crosschain_swap.go @@ -88,10 +88,6 @@ func TestCrosschainSwap(r *runner.E2ERunner, _ []string) { // check the cctx status utils.RequireCCTXStatus(r, cctx1, types.CctxStatus_OutboundMined) - // mine 10 blocks to confirm the outbound tx - _, err = r.GenerateToAddressIfLocalBitcoin(10, r.BTCDeployerAddress) - require.NoError(r, err) - // cctx1 index acts like the inboundHash for the second cctx (the one that withdraws BTC) cctx2 := utils.WaitCctxMinedByInboundHash(r.Ctx, cctx1.Index, r.CctxClient, r.Logger, r.CctxTimeout) diff --git a/e2e/e2etests/test_eth_deposit_fast_confirmation.go b/e2e/e2etests/test_eth_deposit_fast_confirmation.go new file mode 100644 index 0000000000..481db0f4c8 --- /dev/null +++ b/e2e/e2etests/test_eth_deposit_fast_confirmation.go @@ -0,0 +1,113 @@ +package e2etests + +import ( + "math/big" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" + mathpkg "github.com/zeta-chain/node/pkg/math" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// TestETHDepositFastConfirmation tests the fast confirmation of ETH deposits +func TestETHDepositFastConfirmation(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0) + + // ARRANGE + // query chainID + chainIDBig, err := r.EVMClient.ChainID(r.Ctx) + require.NoError(r, err) + chainID := chainIDBig.Int64() + + // enable inbound fast confirmation by updating the chain params + reqQuery := &observertypes.QueryGetChainParamsForChainRequest{ChainId: chainID} + resOldChainParams, err := r.ObserverClient.GetChainParamsForChain(r.Ctx, reqQuery) + require.NoError(r, err) + + chainParams := *resOldChainParams.ChainParams + chainParams.ConfirmationParams = &observertypes.ConfirmationParams{ + SafeInboundCount: 10, // approx 10 seconds, much longer than Fast confirmation time (1 second) + FastInboundCount: 1, + SafeOutboundCount: 1, + FastOutboundCount: 1, + } + err = r.ZetaTxServer.UpdateChainParams(&chainParams) + require.NoError(r, err, "failed to enable inbound fast confirmation") + + // it takes 1 Zeta block time for zetaclient to pick up the new chain params + // wait for 2 blocks to ensure the new chain params are effective + utils.WaitForZetaBlocks(r.Ctx, r, r.ZEVMClient, 2, 20*time.Second) + r.Logger.Info("enabled inbound fast confirmation") + + // query current ETH ZRC20 supply + supply, err := r.ETHZRC20.TotalSupply(&bind.CallOpts{}) + supplyUint := sdkmath.NewUintFromBigInt(supply) + require.NoError(r, err) + + // set ZRC20 liquidity cap to 150% of the current supply + // note: the percentage should not be too small as it may block other tests + liquidityCap, _ := mathpkg.IncreaseUintByPercent(supplyUint, 50) + require.True(r, liquidityCap.GT(sdkmath.ZeroUint())) + res, err := r.ZetaTxServer.SetZRC20LiquidityCap(r.ETHZRC20Addr, liquidityCap) + require.NoError(r, err) + r.Logger.Info("set liquidity cap to %s tx hash: %s", liquidityCap.String(), res.TxHash) + + // ACT-1 + // deposit with exactly fast amount cap, should be fast confirmed + fastAmountCap := chains.CalcInboundFastConfirmationAmountCap(chainID, liquidityCap) + tx := r.ETHDeposit( + r.EVMAddress(), + fastAmountCap.BigInt(), + gatewayevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, + ) + r.Logger.Info("deposited exactly fast amount %d cap tx hash: %s", fastAmountCap, tx.Hash().Hex()) + + // ASSERT-1 + // wait for the cctx to be FAST confirmed + timeStart := time.Now() + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + require.Equal(r, crosschaintypes.ConfirmationMode_FAST, cctx.InboundParams.ConfirmationMode) + fastConfirmTime := time.Since(timeStart) + + r.Logger.Info("FAST confirmed deposit succeeded in %f seconds", fastConfirmTime.Seconds()) + + // ACT-2 + // deposit with amount more than fast amount cap + amountMoreThanCap := big.NewInt(0).Add(fastAmountCap.BigInt(), big.NewInt(1)) + tx = r.ETHDeposit(r.EVMAddress(), amountMoreThanCap, gatewayevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}) + r.Logger.Info("deposited more than fast amount cap %d tx hash: %s", amountMoreThanCap, tx.Hash().Hex()) + + // ASSERT-2 + // wait for the cctx to be SAFE confirmed + timeStart = time.Now() + cctx = utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + require.Equal(r, crosschaintypes.ConfirmationMode_SAFE, cctx.InboundParams.ConfirmationMode) + safeConfirmTime := time.Since(timeStart) + + r.Logger.Info("SAFE confirmed deposit succeeded in %f seconds", safeConfirmTime.Seconds()) + + // ensure FAST confirmation is faster than SAFE confirmation + // using 3 seconds is good enough to check the difference on local goerli network + timeSaved := safeConfirmTime - fastConfirmTime + r.Logger.Info("FAST confirmation saved %f seconds", timeSaved.Seconds()) + require.True(r, timeSaved > 3*time.Second) + + // TEARDOWN + // restore old chain params + err = r.ZetaTxServer.UpdateChainParams(resOldChainParams.ChainParams) + require.NoError(r, err, "failed to restore chain params") + + // remove the liquidity cap + _, err = r.ZetaTxServer.RemoveZRC20LiquidityCap(r.ETHZRC20Addr) + require.NoError(r, err) +} diff --git a/e2e/e2etests/test_eth_deposit_liquidity_cap.go b/e2e/e2etests/test_eth_deposit_liquidity_cap.go index 169af6ac97..d36d3ba80e 100644 --- a/e2e/e2etests/test_eth_deposit_liquidity_cap.go +++ b/e2e/e2etests/test_eth_deposit_liquidity_cap.go @@ -10,7 +10,6 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/x/crosschain/types" - fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) // TestDepositEtherLiquidityCap tests depositing Ethers in a context where a liquidity cap is set @@ -24,12 +23,7 @@ func TestDepositEtherLiquidityCap(r *runner.E2ERunner, args []string) { liquidityCap := math.NewUintFromBigInt(supply).Add(liquidityCapArg) amountLessThanCap := liquidityCapArg.BigInt().Div(liquidityCapArg.BigInt(), big.NewInt(10)) // 1/10 of the cap amountMoreThanCap := liquidityCapArg.BigInt().Mul(liquidityCapArg.BigInt(), big.NewInt(10)) // 10 times the cap - msg := fungibletypes.NewMsgUpdateZRC20LiquidityCap( - r.ZetaTxServer.MustGetAccountAddressFromName(utils.OperationalPolicyName), - r.ETHZRC20Addr.Hex(), - liquidityCap, - ) - res, err := r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, msg) + res, err := r.ZetaTxServer.SetZRC20LiquidityCap(r.ETHZRC20Addr, liquidityCap) require.NoError(r, err) r.Logger.Info("set liquidity cap tx hash: %s", res.TxHash) @@ -68,15 +62,8 @@ func TestDepositEtherLiquidityCap(r *runner.E2ERunner, args []string) { r.Logger.Info("Deposit succeeded") r.Logger.Info("Removing the liquidity cap") - msg = fungibletypes.NewMsgUpdateZRC20LiquidityCap( - r.ZetaTxServer.MustGetAccountAddressFromName(utils.OperationalPolicyName), - r.ETHZRC20Addr.Hex(), - math.ZeroUint(), - ) - - res, err = r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, msg) + res, err = r.ZetaTxServer.RemoveZRC20LiquidityCap(r.ETHZRC20Addr) require.NoError(r, err) - r.Logger.Info("remove liquidity cap tx hash: %s", res.TxHash) initialBal, err = r.ETHZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 61eeb44d29..0dd1cfd0d7 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -28,6 +28,11 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" ) +const ( + // BTCRegnetBlockTime is the block time for the Bitcoin regnet + BTCRegnetBlockTime = 6 * time.Second +) + // ListDeployerUTXOs list the deployer's UTXOs func (r *E2ERunner) ListDeployerUTXOs() []btcjson.ListUnspentResult { // query UTXOs from node @@ -280,7 +285,9 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( txid, err := btcRPC.SendRawTransaction(r.Ctx, stx, true) require.NoError(r, err) r.Logger.Info("txid: %+v", txid) - _, err = r.GenerateToAddressIfLocalBitcoin(6, btcDeployerAddress) + + // mine 1 block to confirm the transaction + _, err = r.GenerateToAddressIfLocalBitcoin(1, btcDeployerAddress) require.NoError(r, err) gtx, err := btcRPC.GetTransaction(r.Ctx, txid) require.NoError(r, err) @@ -414,7 +421,7 @@ func (r *E2ERunner) MineBlocksIfLocalBitcoin() func() { _, err := r.GenerateToAddressIfLocalBitcoin(1, r.BTCDeployerAddress) require.NoError(r, err) - time.Sleep(6 * time.Second) + time.Sleep(BTCRegnetBlockTime) } } }() diff --git a/e2e/txserver/liquidity_cap.go b/e2e/txserver/liquidity_cap.go new file mode 100644 index 0000000000..8516507f6b --- /dev/null +++ b/e2e/txserver/liquidity_cap.go @@ -0,0 +1,45 @@ +package txserver + +import ( + "cosmossdk.io/errors" + "cosmossdk.io/math" + sdktypes "github.com/cosmos/cosmos-sdk/types" + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/node/e2e/utils" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// SetZRC20LiquidityCap sets the liquidity cap for given ZRC20 token +func (zts ZetaTxServer) SetZRC20LiquidityCap( + zrc20Addr ethcommon.Address, + liquidityCap math.Uint, +) (*sdktypes.TxResponse, error) { + return zts.updateZRC20LiquidityCap(zrc20Addr, liquidityCap) +} + +// RemoveZRC20LiquidityCap removes the liquidity cap for given ZRC20 token +func (zts ZetaTxServer) RemoveZRC20LiquidityCap(zrc20Addr ethcommon.Address) (*sdktypes.TxResponse, error) { + return zts.updateZRC20LiquidityCap(zrc20Addr, math.ZeroUint()) +} + +// updateZRC20LiquidityCap updates the liquidity cap for given ZRC20 token +func (zts ZetaTxServer) updateZRC20LiquidityCap( + zrc20Addr ethcommon.Address, + liquidityCap math.Uint, +) (*sdktypes.TxResponse, error) { + // create msg + msg := fungibletypes.NewMsgUpdateZRC20LiquidityCap( + zts.MustGetAccountAddressFromName(utils.OperationalPolicyName), + zrc20Addr.Hex(), + liquidityCap, + ) + + // broadcast tx + res, err := zts.BroadcastTx(utils.OperationalPolicyName, msg) + if err != nil { + return nil, errors.Wrapf(err, "unable to set ZRC20 liquidity cap for %s", zrc20Addr) + } + + return res, nil +} diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index 3446141fa8..c7ad8a8d45 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -5,11 +5,13 @@ import ( "time" rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/ethereum/go-ethereum/ethclient" "github.com/pkg/errors" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) @@ -332,3 +334,36 @@ func WaitForBlockHeight( return nil } + +// WaitForZetaBlocks waits for the given number of Zeta blocks +func WaitForZetaBlocks( + ctx context.Context, + t require.TestingT, + zevmClient *ethclient.Client, + waitBlocks uint64, + timeout time.Duration, +) { + oldHeight, err := zevmClient.BlockNumber(ctx) + require.NoError(t, err) + + // wait for given number of Zeta blocks + newHeight := oldHeight + startTime := time.Now() + checkInterval := constant.ZetaBlockTime / 2 + for { + time.Sleep(checkInterval) + require.False( + t, + time.Since(startTime) > timeout, + "timeout waiting for Zeta blocks, current height: %d", + newHeight, + ) + + // check how many blocks elapsed + newHeight, err = zevmClient.BlockNumber(ctx) + require.NoError(t, err) + if newHeight >= oldHeight+waitBlocks { + return + } + } +} diff --git a/pkg/chains/fast_confirmation.go b/pkg/chains/fast_confirmation.go new file mode 100644 index 0000000000..cbd58e4fd3 --- /dev/null +++ b/pkg/chains/fast_confirmation.go @@ -0,0 +1,34 @@ +package chains + +import ( + sdkmath "cosmossdk.io/math" +) + +const ( + // defaultInboundFastConfirmationLiquidityDivisor is the default ZRC20 liquidity cap divisor for inbound fast confirmation + // For example: given a liquidity cap of 1M, the fast confirmation cap is 1M / 4000 = 250. + // It represents 0.025% of the liquidity cap set for a token + defaultInboundFastConfirmationLiquidityDivisor = uint64(4000) +) + +var ( + // customInboundFastConfirmationLiquidityDivisorMap maps chainID to custom ZRC20 liquidity cap divisor for inbound fast confirmation. + // This map is used to override the default divisor for specific chains. + customInboundFastConfirmationLiquidityDivisorMap = map[int64]uint64{} +) + +// CalcInboundFastConfirmationAmountCap calculates the amount cap for inbound fast confirmation. +func CalcInboundFastConfirmationAmountCap(chainID int64, liquidityCap sdkmath.Uint) sdkmath.Uint { + divisor := getInboundFastConfirmationLiquidityDivisor(chainID) + return liquidityCap.QuoUint64(divisor) +} + +// getInboundFastConfirmationLiquidityDivisor returns the ZRC20 liquidity cap divisor for inbound fast confirmation. +// Default divisor will be used if there is no custom divisor for given chainID. +func getInboundFastConfirmationLiquidityDivisor(chainID int64) uint64 { + divisor, found := customInboundFastConfirmationLiquidityDivisorMap[chainID] + if found && divisor > 0 { + return divisor + } + return defaultInboundFastConfirmationLiquidityDivisor +} diff --git a/pkg/chains/fast_confirmation_test.go b/pkg/chains/fast_confirmation_test.go new file mode 100644 index 0000000000..f830fc0db0 --- /dev/null +++ b/pkg/chains/fast_confirmation_test.go @@ -0,0 +1,45 @@ +package chains_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" +) + +func Test_CalcInboundFastConfirmationAmountCap(t *testing.T) { + tests := []struct { + name string + chainID int64 + liquidityCap sdkmath.Uint + divisor sdkmath.LegacyDec + expected sdkmath.Uint + }{ + { + name: "1000000 / 4000", + chainID: 1, + liquidityCap: sdkmath.NewUintFromString("1000000"), + expected: sdkmath.NewUint(250), + }, + { + name: "700000 / 4000", + chainID: 1, + liquidityCap: sdkmath.NewUintFromString("700000"), + expected: sdkmath.NewUint(175), + }, + { + name: "70000 / 4000", + chainID: 1, + liquidityCap: sdkmath.NewUintFromString("70000"), + expected: sdkmath.NewUint(17), // truncate 17.5 to 17 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := chains.CalcInboundFastConfirmationAmountCap(tt.chainID, tt.liquidityCap) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/coin/coin.go b/pkg/coin/coin.go index aae573d55a..301ddde7b3 100644 --- a/pkg/coin/coin.go +++ b/pkg/coin/coin.go @@ -38,3 +38,9 @@ func GetAzetaDecFromAmountInZeta(zetaAmount string) (sdkmath.LegacyDec, error) { func (c CoinType) SupportsRefund() bool { return c == CoinType_ERC20 || c == CoinType_Gas || c == CoinType_Zeta } + +// IsAsset returns true if the coin type represents transport of asset. +// CoinType_Cmd and CoinType_NoAssetCall are not transport of asset. +func (c CoinType) IsAsset() bool { + return c == CoinType_ERC20 || c == CoinType_Gas || c == CoinType_Zeta +} diff --git a/pkg/coin/coin_test.go b/pkg/coin/coin_test.go index e08891503a..d33f405fe7 100644 --- a/pkg/coin/coin_test.go +++ b/pkg/coin/coin_test.go @@ -149,3 +149,25 @@ func TestCoinType_SupportsRefund(t *testing.T) { }) } } + +func TestCoinType_IsAsset(t *testing.T) { + tests := []struct { + name string + c coin.CoinType + want bool + }{ + {"Gas is asset", coin.CoinType_Gas, true}, + {"ERC20 is asset", coin.CoinType_ERC20, true}, + {"Zeta is asset", coin.CoinType_Zeta, true}, + {"Cmd is not asset", coin.CoinType_Cmd, false}, + {"CoinType_NoAssetCall is irrelevant and not asset", coin.CoinType_NoAssetCall, false}, + {"Unknown coin type is not asset", coin.CoinType(100), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.IsAsset(); got != tt.want { + t.Errorf("CoinType.IsAsset() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/proto/zetachain/zetacore/fungible/query.proto b/proto/zetachain/zetacore/fungible/query.proto index f35a44491c..353fcf0bfc 100644 --- a/proto/zetachain/zetacore/fungible/query.proto +++ b/proto/zetachain/zetacore/fungible/query.proto @@ -25,6 +25,13 @@ service Query { option (google.api.http).get = "/zeta-chain/fungible/foreign_coins"; } + // Queries a ForeignCoins by chain_id and asset. + rpc ForeignCoinsFromAsset(QueryGetForeignCoinsFromAssetRequest) + returns (QueryGetForeignCoinsFromAssetResponse) { + option (google.api.http).get = + "/zeta-chain/fungible/foreign_coins/{chain_id}/{asset}"; + } + // Queries SystemContract rpc SystemContract(QueryGetSystemContractRequest) returns (QueryGetSystemContractResponse) { @@ -73,6 +80,19 @@ message QueryAllForeignCoinsResponse { cosmos.base.query.v1beta1.PageResponse pagination = 2; } +// QueryGetForeignCoinsFromAssetRequest defines the request type for the +// ForeignCoinsFromAsset RPC method. +message QueryGetForeignCoinsFromAssetRequest { + int64 chain_id = 1; + string asset = 2; +} + +// QueryGetForeignCoinsFromAssetResponse defines the response type for the +// ForeignCoinsFromAsset RPC method. +message QueryGetForeignCoinsFromAssetResponse { + ForeignCoins foreignCoins = 1 [ (gogoproto.nullable) = false ]; +} + message QueryGetSystemContractRequest {} message QueryGetSystemContractResponse { diff --git a/typescript/zetachain/zetacore/fungible/query_pb.d.ts b/typescript/zetachain/zetacore/fungible/query_pb.d.ts index b149a8d06b..3d161b4a4e 100644 --- a/typescript/zetachain/zetacore/fungible/query_pb.d.ts +++ b/typescript/zetachain/zetacore/fungible/query_pb.d.ts @@ -110,6 +110,65 @@ export declare class QueryAllForeignCoinsResponse extends Message | undefined, b: QueryAllForeignCoinsResponse | PlainMessage | undefined): boolean; } +/** + * QueryGetForeignCoinsFromAssetRequest defines the request type for the + * ForeignCoinsFromAsset RPC method. + * + * @generated from message zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetRequest + */ +export declare class QueryGetForeignCoinsFromAssetRequest extends Message { + /** + * @generated from field: int64 chain_id = 1; + */ + chainId: bigint; + + /** + * @generated from field: string asset = 2; + */ + asset: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): QueryGetForeignCoinsFromAssetRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): QueryGetForeignCoinsFromAssetRequest; + + static fromJsonString(jsonString: string, options?: Partial): QueryGetForeignCoinsFromAssetRequest; + + static equals(a: QueryGetForeignCoinsFromAssetRequest | PlainMessage | undefined, b: QueryGetForeignCoinsFromAssetRequest | PlainMessage | undefined): boolean; +} + +/** + * QueryGetForeignCoinsFromAssetResponse defines the response type for the + * ForeignCoinsFromAsset RPC method. + * + * @generated from message zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetResponse + */ +export declare class QueryGetForeignCoinsFromAssetResponse extends Message { + /** + * @generated from field: zetachain.zetacore.fungible.ForeignCoins foreignCoins = 1; + */ + foreignCoins?: ForeignCoins; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): QueryGetForeignCoinsFromAssetResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): QueryGetForeignCoinsFromAssetResponse; + + static fromJsonString(jsonString: string, options?: Partial): QueryGetForeignCoinsFromAssetResponse; + + static equals(a: QueryGetForeignCoinsFromAssetResponse | PlainMessage | undefined, b: QueryGetForeignCoinsFromAssetResponse | PlainMessage | undefined): boolean; +} + /** * @generated from message zetachain.zetacore.fungible.QueryGetSystemContractRequest */ diff --git a/x/crosschain/types/message_vote_inbound.go b/x/crosschain/types/message_vote_inbound.go index 8b2d31b052..7f2df1b2fe 100644 --- a/x/crosschain/types/message_vote_inbound.go +++ b/x/crosschain/types/message_vote_inbound.go @@ -156,3 +156,25 @@ func (msg *MsgVoteInbound) Digest() string { hash := crypto.Keccak256Hash([]byte(m.String())) return hash.Hex() } + +// EligibleForFastConfirmation determines if the inbound msg is eligible for fast confirmation +func (msg *MsgVoteInbound) EligibleForFastConfirmation() bool { + // only asset CoinType is eligible for fast confirmation + if !msg.CoinType.IsAsset() { + return false + } + + switch msg.ProtocolContractVersion { + case ProtocolContractVersion_V1: + // msg using protocol contract version V1 is not eligible for fast confirmation because: + // 1. whether the receiver address is a contract or not is unknown + // 2. it can be a depositAndCall (Gas or ZRC20) with empty payload + // 3. it can be a message passing (CoinType_Zeta) calls 'onReceive' + return false + case ProtocolContractVersion_V2: + // in protocol contract version V2, simple deposit is distinguished from depositAndCall/NoAssetCall + return !msg.IsCrossChainCall + default: + return false + } +} diff --git a/x/crosschain/types/message_vote_inbound_test.go b/x/crosschain/types/message_vote_inbound_test.go index e6411f79d5..02d3ef9539 100644 --- a/x/crosschain/types/message_vote_inbound_test.go +++ b/x/crosschain/types/message_vote_inbound_test.go @@ -622,6 +622,54 @@ func TestMsgVoteInbound_Digest(t *testing.T) { require.NotEqual(t, hash, hash2, "confirmation mode should change hash") } +func TestMsgVoteInbound_EligibleForFastConfirmation(t *testing.T) { + tests := []struct { + name string + msg types.MsgVoteInbound + eligible bool + }{ + { + name: "eligible for fast confirmation", + msg: func() types.MsgVoteInbound { + msg := sample.InboundVote(coin.CoinType_Gas, 1, 7000) + msg.ProtocolContractVersion = types.ProtocolContractVersion_V2 + return msg + }(), + eligible: true, + }, + { + name: "not eligible for non-fungible coin type", + msg: sample.InboundVote(coin.CoinType_NoAssetCall, 1, 7000), + eligible: false, + }, + { + name: "not eligible for protocol contract version V1", + msg: func() types.MsgVoteInbound { + msg := sample.InboundVote(coin.CoinType_Gas, 1, 7000) + msg.ProtocolContractVersion = types.ProtocolContractVersion_V1 + return msg + }(), + eligible: false, + }, + { + name: "not eligible for unknown protocol contract version", + msg: func() types.MsgVoteInbound { + msg := sample.InboundVote(coin.CoinType_Gas, 1, 7000) + msg.ProtocolContractVersion = types.ProtocolContractVersion(999) + return msg + }(), + eligible: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eligible := tt.msg.EligibleForFastConfirmation() + require.Equal(t, tt.eligible, eligible) + }) + } +} + func TestMsgVoteInbound_GetSigners(t *testing.T) { signer := sample.AccAddress() tests := []struct { diff --git a/x/fungible/keeper/foreign_coins_test.go b/x/fungible/keeper/foreign_coins_test.go index 3ebc13f2e6..2b7befde53 100644 --- a/x/fungible/keeper/foreign_coins_test.go +++ b/x/fungible/keeper/foreign_coins_test.go @@ -16,10 +16,11 @@ import ( "github.com/zeta-chain/node/x/fungible/types" ) -func createNForeignCoins(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.ForeignCoins { +func createNForeignCoins(t *testing.T, keeper *keeper.Keeper, ctx sdk.Context, n int) []types.ForeignCoins { items := make([]types.ForeignCoins, n) for i := range items { - items[i].Zrc20ContractAddress = strconv.Itoa(i) + fCoin := sample.ForeignCoins(t, strconv.Itoa(i)) + items[i] = fCoin keeper.SetForeignCoins(ctx, items[i]) } diff --git a/x/fungible/keeper/grpc_query_foreign_coins.go b/x/fungible/keeper/grpc_query_foreign_coins.go index bcd73774f1..8d80fe882e 100644 --- a/x/fungible/keeper/grpc_query_foreign_coins.go +++ b/x/fungible/keeper/grpc_query_foreign_coins.go @@ -62,3 +62,21 @@ func (k Keeper) ForeignCoins( return &types.QueryGetForeignCoinsResponse{ForeignCoins: val}, nil } + +// ForeignCoinsFromAsset returns the foreign coin for a given asset and chain id +func (k Keeper) ForeignCoinsFromAsset( + c context.Context, + req *types.QueryGetForeignCoinsFromAssetRequest, +) (*types.QueryGetForeignCoinsFromAssetResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + ctx := sdk.UnwrapSDKContext(c) + + fCoin, found := k.GetForeignCoinFromAsset(ctx, req.Asset, req.ChainId) + if !found { + return nil, status.Error(codes.NotFound, "not found") + } + + return &types.QueryGetForeignCoinsFromAssetResponse{ForeignCoins: fCoin}, nil +} diff --git a/x/fungible/keeper/grpc_query_foreign_coins_test.go b/x/fungible/keeper/grpc_query_foreign_coins_test.go index 6fd36fc4fd..20f8277fb7 100644 --- a/x/fungible/keeper/grpc_query_foreign_coins_test.go +++ b/x/fungible/keeper/grpc_query_foreign_coins_test.go @@ -4,7 +4,6 @@ import ( "strconv" "testing" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -17,8 +16,7 @@ import ( func TestForeignCoinsQuerySingle(t *testing.T) { keeper, ctx, _, _ := keepertest.FungibleKeeper(t) - wctx := sdk.WrapSDKContext(ctx) - msgs := createNForeignCoins(keeper, ctx, 2) + msgs := createNForeignCoins(t, keeper, ctx, 2) for _, tc := range []struct { desc string request *types.QueryGetForeignCoinsRequest @@ -52,7 +50,7 @@ func TestForeignCoinsQuerySingle(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - response, err := keeper.ForeignCoins(wctx, tc.request) + response, err := keeper.ForeignCoins(ctx, tc.request) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { @@ -66,10 +64,60 @@ func TestForeignCoinsQuerySingle(t *testing.T) { } } +func TestForeignCoinsFromAsset(t *testing.T) { + keeper, ctx, _, _ := keepertest.FungibleKeeper(t) + msgs := createNForeignCoins(t, keeper, ctx, 2) + for _, tc := range []struct { + desc string + request *types.QueryGetForeignCoinsFromAssetRequest + response *types.QueryGetForeignCoinsFromAssetResponse + err error + }{ + { + desc: "First", + request: &types.QueryGetForeignCoinsFromAssetRequest{ + ChainId: msgs[0].ForeignChainId, + Asset: msgs[0].Asset, + }, + response: &types.QueryGetForeignCoinsFromAssetResponse{ForeignCoins: msgs[0]}, + }, + { + desc: "Second", + request: &types.QueryGetForeignCoinsFromAssetRequest{ + ChainId: msgs[1].ForeignChainId, + Asset: msgs[1].Asset, + }, + response: &types.QueryGetForeignCoinsFromAssetResponse{ForeignCoins: msgs[1]}, + }, + { + desc: "Not found", + request: &types.QueryGetForeignCoinsFromAssetRequest{ + ChainId: msgs[0].ForeignChainId + 1, + Asset: msgs[0].Asset, + }, + err: status.Error(codes.NotFound, "not found"), + }, + { + desc: "Invalid request", + request: nil, + err: status.Error(codes.InvalidArgument, "invalid request"), + }, + } { + t.Run(tc.desc, func(t *testing.T) { + response, err := keeper.ForeignCoinsFromAsset(ctx, tc.request) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + require.Equal(t, nullify.Fill(tc.response), nullify.Fill(response)) + } + }) + } +} + func TestForeignCoinsQueryPaginated(t *testing.T) { keeper, ctx, _, _ := keepertest.FungibleKeeper(t) - wctx := sdk.WrapSDKContext(ctx) - msgs := createNForeignCoins(keeper, ctx, 5) + msgs := createNForeignCoins(t, keeper, ctx, 5) request := func(next []byte, offset, limit uint64, total bool) *types.QueryAllForeignCoinsRequest { return &types.QueryAllForeignCoinsRequest{ @@ -84,7 +132,7 @@ func TestForeignCoinsQueryPaginated(t *testing.T) { t.Run("ByOffset", func(t *testing.T) { step := 2 for i := 0; i < len(msgs); i += step { - resp, err := keeper.ForeignCoinsAll(wctx, request(nil, uint64(i), uint64(step), false)) + resp, err := keeper.ForeignCoinsAll(ctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.ForeignCoins), step) require.Subset(t, @@ -97,7 +145,7 @@ func TestForeignCoinsQueryPaginated(t *testing.T) { step := 2 var next []byte for i := 0; i < len(msgs); i += step { - resp, err := keeper.ForeignCoinsAll(wctx, request(next, 0, uint64(step), false)) + resp, err := keeper.ForeignCoinsAll(ctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.ForeignCoins), step) require.Subset(t, @@ -108,7 +156,7 @@ func TestForeignCoinsQueryPaginated(t *testing.T) { } }) t.Run("Total", func(t *testing.T) { - resp, err := keeper.ForeignCoinsAll(wctx, request(nil, 0, 0, true)) + resp, err := keeper.ForeignCoinsAll(ctx, request(nil, 0, 0, true)) require.NoError(t, err) require.Equal(t, len(msgs), int(resp.Pagination.Total)) require.ElementsMatch(t, @@ -117,7 +165,7 @@ func TestForeignCoinsQueryPaginated(t *testing.T) { ) }) t.Run("InvalidRequest", func(t *testing.T) { - _, err := keeper.ForeignCoinsAll(wctx, nil) + _, err := keeper.ForeignCoinsAll(ctx, nil) require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request")) }) } diff --git a/x/fungible/types/query.pb.go b/x/fungible/types/query.pb.go index ed6c2924d7..7d5c6a7d8d 100644 --- a/x/fungible/types/query.pb.go +++ b/x/fungible/types/query.pb.go @@ -215,6 +215,106 @@ func (m *QueryAllForeignCoinsResponse) GetPagination() *query.PageResponse { return nil } +// QueryGetForeignCoinsFromAssetRequest defines the request type for the +// ForeignCoinsFromAsset RPC method. +type QueryGetForeignCoinsFromAssetRequest struct { + ChainId int64 `protobuf:"varint,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + Asset string `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"` +} + +func (m *QueryGetForeignCoinsFromAssetRequest) Reset() { *m = QueryGetForeignCoinsFromAssetRequest{} } +func (m *QueryGetForeignCoinsFromAssetRequest) String() string { return proto.CompactTextString(m) } +func (*QueryGetForeignCoinsFromAssetRequest) ProtoMessage() {} +func (*QueryGetForeignCoinsFromAssetRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_9cd9a7c9e94d3c90, []int{4} +} +func (m *QueryGetForeignCoinsFromAssetRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetForeignCoinsFromAssetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetForeignCoinsFromAssetRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetForeignCoinsFromAssetRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetForeignCoinsFromAssetRequest.Merge(m, src) +} +func (m *QueryGetForeignCoinsFromAssetRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryGetForeignCoinsFromAssetRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetForeignCoinsFromAssetRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetForeignCoinsFromAssetRequest proto.InternalMessageInfo + +func (m *QueryGetForeignCoinsFromAssetRequest) GetChainId() int64 { + if m != nil { + return m.ChainId + } + return 0 +} + +func (m *QueryGetForeignCoinsFromAssetRequest) GetAsset() string { + if m != nil { + return m.Asset + } + return "" +} + +// QueryGetForeignCoinsFromAssetResponse defines the response type for the +// ForeignCoinsFromAsset RPC method. +type QueryGetForeignCoinsFromAssetResponse struct { + ForeignCoins ForeignCoins `protobuf:"bytes,1,opt,name=foreignCoins,proto3" json:"foreignCoins"` +} + +func (m *QueryGetForeignCoinsFromAssetResponse) Reset() { *m = QueryGetForeignCoinsFromAssetResponse{} } +func (m *QueryGetForeignCoinsFromAssetResponse) String() string { return proto.CompactTextString(m) } +func (*QueryGetForeignCoinsFromAssetResponse) ProtoMessage() {} +func (*QueryGetForeignCoinsFromAssetResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_9cd9a7c9e94d3c90, []int{5} +} +func (m *QueryGetForeignCoinsFromAssetResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetForeignCoinsFromAssetResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetForeignCoinsFromAssetResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetForeignCoinsFromAssetResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetForeignCoinsFromAssetResponse.Merge(m, src) +} +func (m *QueryGetForeignCoinsFromAssetResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryGetForeignCoinsFromAssetResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetForeignCoinsFromAssetResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetForeignCoinsFromAssetResponse proto.InternalMessageInfo + +func (m *QueryGetForeignCoinsFromAssetResponse) GetForeignCoins() ForeignCoins { + if m != nil { + return m.ForeignCoins + } + return ForeignCoins{} +} + type QueryGetSystemContractRequest struct { } @@ -222,7 +322,7 @@ func (m *QueryGetSystemContractRequest) Reset() { *m = QueryGetSystemCon func (m *QueryGetSystemContractRequest) String() string { return proto.CompactTextString(m) } func (*QueryGetSystemContractRequest) ProtoMessage() {} func (*QueryGetSystemContractRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{4} + return fileDescriptor_9cd9a7c9e94d3c90, []int{6} } func (m *QueryGetSystemContractRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -259,7 +359,7 @@ func (m *QueryGetSystemContractResponse) Reset() { *m = QueryGetSystemCo func (m *QueryGetSystemContractResponse) String() string { return proto.CompactTextString(m) } func (*QueryGetSystemContractResponse) ProtoMessage() {} func (*QueryGetSystemContractResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{5} + return fileDescriptor_9cd9a7c9e94d3c90, []int{7} } func (m *QueryGetSystemContractResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -302,7 +402,7 @@ func (m *QueryGetGasStabilityPoolAddress) Reset() { *m = QueryGetGasStab func (m *QueryGetGasStabilityPoolAddress) String() string { return proto.CompactTextString(m) } func (*QueryGetGasStabilityPoolAddress) ProtoMessage() {} func (*QueryGetGasStabilityPoolAddress) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{6} + return fileDescriptor_9cd9a7c9e94d3c90, []int{8} } func (m *QueryGetGasStabilityPoolAddress) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -342,7 +442,7 @@ func (m *QueryGetGasStabilityPoolAddressResponse) Reset() { func (m *QueryGetGasStabilityPoolAddressResponse) String() string { return proto.CompactTextString(m) } func (*QueryGetGasStabilityPoolAddressResponse) ProtoMessage() {} func (*QueryGetGasStabilityPoolAddressResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{7} + return fileDescriptor_9cd9a7c9e94d3c90, []int{9} } func (m *QueryGetGasStabilityPoolAddressResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -393,7 +493,7 @@ func (m *QueryGetGasStabilityPoolBalance) Reset() { *m = QueryGetGasStab func (m *QueryGetGasStabilityPoolBalance) String() string { return proto.CompactTextString(m) } func (*QueryGetGasStabilityPoolBalance) ProtoMessage() {} func (*QueryGetGasStabilityPoolBalance) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{8} + return fileDescriptor_9cd9a7c9e94d3c90, []int{10} } func (m *QueryGetGasStabilityPoolBalance) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -439,7 +539,7 @@ func (m *QueryGetGasStabilityPoolBalanceResponse) Reset() { func (m *QueryGetGasStabilityPoolBalanceResponse) String() string { return proto.CompactTextString(m) } func (*QueryGetGasStabilityPoolBalanceResponse) ProtoMessage() {} func (*QueryGetGasStabilityPoolBalanceResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{9} + return fileDescriptor_9cd9a7c9e94d3c90, []int{11} } func (m *QueryGetGasStabilityPoolBalanceResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -482,7 +582,7 @@ func (m *QueryAllGasStabilityPoolBalance) Reset() { *m = QueryAllGasStab func (m *QueryAllGasStabilityPoolBalance) String() string { return proto.CompactTextString(m) } func (*QueryAllGasStabilityPoolBalance) ProtoMessage() {} func (*QueryAllGasStabilityPoolBalance) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{10} + return fileDescriptor_9cd9a7c9e94d3c90, []int{12} } func (m *QueryAllGasStabilityPoolBalance) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -521,7 +621,7 @@ func (m *QueryAllGasStabilityPoolBalanceResponse) Reset() { func (m *QueryAllGasStabilityPoolBalanceResponse) String() string { return proto.CompactTextString(m) } func (*QueryAllGasStabilityPoolBalanceResponse) ProtoMessage() {} func (*QueryAllGasStabilityPoolBalanceResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{11} + return fileDescriptor_9cd9a7c9e94d3c90, []int{13} } func (m *QueryAllGasStabilityPoolBalanceResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -570,7 +670,7 @@ func (m *QueryAllGasStabilityPoolBalanceResponse_Balance) String() string { } func (*QueryAllGasStabilityPoolBalanceResponse_Balance) ProtoMessage() {} func (*QueryAllGasStabilityPoolBalanceResponse_Balance) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{11, 0} + return fileDescriptor_9cd9a7c9e94d3c90, []int{13, 0} } func (m *QueryAllGasStabilityPoolBalanceResponse_Balance) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -621,7 +721,7 @@ func (m *QueryCodeHashRequest) Reset() { *m = QueryCodeHashRequest{} } func (m *QueryCodeHashRequest) String() string { return proto.CompactTextString(m) } func (*QueryCodeHashRequest) ProtoMessage() {} func (*QueryCodeHashRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{12} + return fileDescriptor_9cd9a7c9e94d3c90, []int{14} } func (m *QueryCodeHashRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -665,7 +765,7 @@ func (m *QueryCodeHashResponse) Reset() { *m = QueryCodeHashResponse{} } func (m *QueryCodeHashResponse) String() string { return proto.CompactTextString(m) } func (*QueryCodeHashResponse) ProtoMessage() {} func (*QueryCodeHashResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_9cd9a7c9e94d3c90, []int{13} + return fileDescriptor_9cd9a7c9e94d3c90, []int{15} } func (m *QueryCodeHashResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -706,6 +806,8 @@ func init() { proto.RegisterType((*QueryGetForeignCoinsResponse)(nil), "zetachain.zetacore.fungible.QueryGetForeignCoinsResponse") proto.RegisterType((*QueryAllForeignCoinsRequest)(nil), "zetachain.zetacore.fungible.QueryAllForeignCoinsRequest") proto.RegisterType((*QueryAllForeignCoinsResponse)(nil), "zetachain.zetacore.fungible.QueryAllForeignCoinsResponse") + proto.RegisterType((*QueryGetForeignCoinsFromAssetRequest)(nil), "zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetRequest") + proto.RegisterType((*QueryGetForeignCoinsFromAssetResponse)(nil), "zetachain.zetacore.fungible.QueryGetForeignCoinsFromAssetResponse") proto.RegisterType((*QueryGetSystemContractRequest)(nil), "zetachain.zetacore.fungible.QueryGetSystemContractRequest") proto.RegisterType((*QueryGetSystemContractResponse)(nil), "zetachain.zetacore.fungible.QueryGetSystemContractResponse") proto.RegisterType((*QueryGetGasStabilityPoolAddress)(nil), "zetachain.zetacore.fungible.QueryGetGasStabilityPoolAddress") @@ -724,63 +826,68 @@ func init() { } var fileDescriptor_9cd9a7c9e94d3c90 = []byte{ - // 889 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0xd1, 0x4e, 0xdb, 0x48, - 0x14, 0x8d, 0x61, 0xd9, 0x84, 0x81, 0x65, 0xa5, 0x11, 0x2b, 0x58, 0xc3, 0x26, 0xbb, 0x16, 0x0b, - 0x6c, 0x60, 0x3d, 0x24, 0xac, 0xb4, 0x2c, 0x1b, 0x55, 0x4d, 0xd2, 0x42, 0x2b, 0xf5, 0x81, 0x86, - 0xa7, 0xf6, 0x25, 0x9a, 0xd8, 0x83, 0x63, 0xc9, 0xf1, 0x84, 0x8c, 0x13, 0x91, 0x46, 0x48, 0x55, - 0xbf, 0xa0, 0x52, 0x3f, 0xa1, 0x3f, 0x50, 0xf5, 0xa5, 0x2f, 0xfd, 0x00, 0x1e, 0x91, 0x2a, 0x55, - 0xed, 0x4b, 0xd5, 0x86, 0x4a, 0xfd, 0x8c, 0x56, 0x19, 0x8f, 0x4d, 0x12, 0xec, 0x24, 0x0d, 0x4f, - 0xf1, 0x8c, 0xef, 0xb9, 0xf7, 0x9c, 0x7b, 0x67, 0x8e, 0x03, 0xd6, 0x1e, 0x11, 0x07, 0x6b, 0x65, - 0x6c, 0xda, 0x88, 0x3f, 0xd1, 0x1a, 0x41, 0x47, 0x75, 0xdb, 0x30, 0x4b, 0x16, 0x41, 0xc7, 0x75, - 0x52, 0x6b, 0xaa, 0xd5, 0x1a, 0x75, 0x28, 0x5c, 0xf2, 0x03, 0x55, 0x2f, 0x50, 0xf5, 0x02, 0xe5, - 0xa4, 0x46, 0x59, 0x85, 0x32, 0x54, 0xc2, 0x4c, 0xa0, 0x50, 0x23, 0x55, 0x22, 0x0e, 0x4e, 0xa1, - 0x2a, 0x36, 0x4c, 0x1b, 0x3b, 0x26, 0xb5, 0xdd, 0x44, 0x32, 0x1a, 0x54, 0xf1, 0x88, 0xd6, 0x88, - 0x69, 0xd8, 0x45, 0x8d, 0x9a, 0x36, 0x13, 0x80, 0xd4, 0x20, 0x00, 0x6b, 0x32, 0x87, 0x54, 0x8a, - 0x1a, 0xb5, 0x9d, 0x1a, 0xd6, 0x1c, 0x01, 0x99, 0x37, 0xa8, 0x41, 0xf9, 0x23, 0xea, 0x3c, 0x89, - 0xdd, 0x65, 0x83, 0x52, 0xc3, 0x22, 0x08, 0x57, 0x4d, 0x84, 0x6d, 0x9b, 0x3a, 0x9c, 0x96, 0x57, - 0x66, 0x41, 0x68, 0xa8, 0x30, 0x03, 0x35, 0x52, 0x9d, 0x1f, 0xf7, 0x85, 0xb2, 0x0d, 0x96, 0xee, - 0x77, 0x24, 0xed, 0x13, 0x67, 0xcf, 0xa5, 0x97, 0xef, 0xb0, 0x2b, 0x90, 0xe3, 0x3a, 0x61, 0x0e, - 0x9c, 0x07, 0x53, 0xa6, 0xad, 0x93, 0x93, 0x45, 0xe9, 0x77, 0x69, 0x7d, 0xba, 0xe0, 0x2e, 0x14, - 0x06, 0x96, 0x83, 0x41, 0xac, 0x4a, 0x6d, 0x46, 0xe0, 0x21, 0x98, 0x3d, 0xea, 0xda, 0xe7, 0xe0, - 0x99, 0xf4, 0x5f, 0xea, 0x80, 0x2e, 0xab, 0xdd, 0x89, 0x72, 0x3f, 0x9c, 0x7d, 0x48, 0x44, 0x0a, - 0x3d, 0x49, 0x14, 0x22, 0x98, 0x66, 0x2d, 0x2b, 0x88, 0xe9, 0x1e, 0x00, 0x97, 0xd3, 0x10, 0x15, - 0x57, 0x55, 0x57, 0xb6, 0xda, 0x19, 0x9d, 0xea, 0x0e, 0x5c, 0x8c, 0x4e, 0x3d, 0xc0, 0x06, 0x11, - 0xd8, 0x42, 0x17, 0x52, 0x79, 0x2d, 0x09, 0x71, 0x57, 0xea, 0x84, 0x8a, 0x9b, 0xbc, 0xb6, 0x38, - 0xb8, 0xdf, 0xc3, 0x7e, 0x82, 0xb3, 0x5f, 0x1b, 0xca, 0xde, 0x65, 0xd4, 0x43, 0x3f, 0x01, 0x7e, - 0xf3, 0x46, 0x73, 0xc8, 0x4f, 0x4f, 0x5e, 0x1c, 0x1e, 0xa1, 0x55, 0x69, 0x81, 0x78, 0x58, 0x80, - 0x10, 0xf8, 0x00, 0xcc, 0xf5, 0xbe, 0x11, 0xdd, 0xdc, 0x18, 0x28, 0xb1, 0x17, 0x22, 0x44, 0xf6, - 0x25, 0x52, 0xfe, 0x00, 0x09, 0xaf, 0xf8, 0x3e, 0x66, 0x87, 0x0e, 0x2e, 0x99, 0x96, 0xe9, 0x34, - 0x0f, 0x28, 0xb5, 0xb2, 0xba, 0x5e, 0x23, 0x8c, 0x29, 0xc7, 0x60, 0x6d, 0x48, 0x88, 0x4f, 0xf4, - 0x4f, 0x30, 0xe7, 0x76, 0xa8, 0x88, 0xdd, 0x37, 0xe2, 0x94, 0xfe, 0xe4, 0xee, 0x8a, 0x70, 0x98, - 0x00, 0x33, 0xa4, 0x51, 0xf1, 0x63, 0x26, 0x78, 0x0c, 0x20, 0x8d, 0x8a, 0x57, 0x32, 0x13, 0xce, - 0x2a, 0x87, 0x2d, 0x6c, 0x6b, 0x04, 0xfe, 0x0a, 0x62, 0x5c, 0x78, 0xd1, 0xd4, 0x79, 0x91, 0xc9, - 0x42, 0x94, 0xaf, 0xef, 0xea, 0x4a, 0x3e, 0x9c, 0xb0, 0x40, 0xfb, 0x84, 0x17, 0x41, 0xb4, 0xe4, - 0x6e, 0x09, 0x16, 0xde, 0xd2, 0x6f, 0x4c, 0xd6, 0xb2, 0x42, 0x92, 0x28, 0xef, 0x25, 0x51, 0x28, - 0x3c, 0xc6, 0x2f, 0x64, 0x83, 0x98, 0xc8, 0xec, 0x9d, 0xcf, 0x7b, 0x03, 0x87, 0x37, 0x62, 0x5e, - 0x55, 0xac, 0xc5, 0x74, 0xfd, 0x1a, 0xf2, 0x0d, 0x10, 0x1d, 0xde, 0xa9, 0x01, 0xf2, 0xb7, 0xc0, - 0x3c, 0xa7, 0x90, 0xa7, 0x3a, 0xb9, 0x83, 0x59, 0xd9, 0xbb, 0xd4, 0x8b, 0x20, 0xda, 0x3b, 0x5a, - 0x6f, 0xa9, 0xfc, 0x03, 0x7e, 0xe9, 0x43, 0x08, 0xe9, 0x4b, 0x60, 0x5a, 0xa3, 0x3a, 0x29, 0x96, - 0x31, 0x2b, 0x0b, 0x50, 0x4c, 0x13, 0x41, 0xe9, 0xaf, 0x00, 0x4c, 0x71, 0x18, 0x7c, 0x25, 0x81, - 0xd9, 0xee, 0x5b, 0x09, 0x77, 0x86, 0x37, 0x28, 0xd8, 0x23, 0xe5, 0xff, 0xc6, 0x40, 0xba, 0x64, - 0x95, 0xf4, 0x93, 0x37, 0x9f, 0x9f, 0x4d, 0x6c, 0xc2, 0x24, 0x37, 0xff, 0xbf, 0xdd, 0xef, 0x40, - 0xf0, 0xf7, 0x02, 0xb5, 0xb8, 0xf7, 0x9e, 0xc2, 0x97, 0x12, 0xf8, 0xb9, 0x3b, 0x59, 0xd6, 0xb2, - 0x46, 0x21, 0x1f, 0x6c, 0x9b, 0xa3, 0x90, 0x0f, 0x31, 0x42, 0x25, 0xc9, 0xc9, 0xaf, 0x40, 0x65, - 0x38, 0xf9, 0x4e, 0xbb, 0xfb, 0xbc, 0x00, 0xee, 0x8e, 0xd4, 0xb6, 0x40, 0x13, 0x93, 0xff, 0x1f, - 0x0b, 0x2b, 0x78, 0x6f, 0x72, 0xde, 0xab, 0x70, 0x25, 0x90, 0x77, 0xdf, 0x37, 0x17, 0xbe, 0x95, - 0xc0, 0x42, 0x88, 0x11, 0xc1, 0xcc, 0x48, 0x34, 0x42, 0xd0, 0xf2, 0xad, 0xeb, 0xa0, 0x7d, 0x35, - 0xff, 0x72, 0x35, 0x29, 0x88, 0x02, 0xd5, 0x18, 0x98, 0x15, 0x99, 0x07, 0x2f, 0x56, 0x29, 0xb5, - 0x3c, 0x1f, 0x84, 0x9f, 0x02, 0x84, 0x79, 0x97, 0x78, 0x3c, 0x61, 0x02, 0x3d, 0xa6, 0xb0, 0x3e, - 0xaf, 0x51, 0x72, 0x5c, 0x58, 0x06, 0xee, 0x8e, 0x2a, 0x4c, 0x98, 0x09, 0x6a, 0x79, 0xfe, 0x73, - 0x0a, 0xdb, 0x12, 0x90, 0x43, 0xea, 0x74, 0xae, 0x4d, 0xe6, 0x3a, 0xa6, 0x38, 0x8a, 0xcc, 0xe1, - 0x96, 0xaa, 0xdc, 0xe4, 0x32, 0x77, 0xe1, 0x4e, 0xb7, 0xcc, 0xab, 0x7f, 0x05, 0xc3, 0xf5, 0xc2, - 0xe7, 0x12, 0x88, 0x79, 0x36, 0x08, 0x53, 0xc3, 0x49, 0xf5, 0x99, 0xac, 0x9c, 0xfe, 0x1e, 0x88, - 0x60, 0xbd, 0xc5, 0x59, 0x27, 0xe1, 0x7a, 0xe0, 0x70, 0x7c, 0x03, 0x46, 0x2d, 0x71, 0xda, 0x4e, - 0xe5, 0xa9, 0xc7, 0x5f, 0x5e, 0x24, 0xa5, 0xdc, 0xed, 0xb3, 0x76, 0x5c, 0x3a, 0x6f, 0xc7, 0xa5, - 0x8f, 0xed, 0xb8, 0xf4, 0xf4, 0x22, 0x1e, 0x39, 0xbf, 0x88, 0x47, 0xde, 0x5d, 0xc4, 0x23, 0x0f, - 0x37, 0x0c, 0xd3, 0x29, 0xd7, 0x4b, 0xaa, 0x46, 0x2b, 0xdd, 0x49, 0x6d, 0xaa, 0x13, 0x74, 0x72, - 0x99, 0xdb, 0x69, 0x56, 0x09, 0x2b, 0xfd, 0xc8, 0xff, 0xbd, 0x6e, 0x7f, 0x0b, 0x00, 0x00, 0xff, - 0xff, 0x8f, 0x9d, 0x05, 0x74, 0xe2, 0x0b, 0x00, 0x00, + // 962 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0xdd, 0x6e, 0xdc, 0x44, + 0x14, 0x8e, 0x5b, 0x42, 0x92, 0xd3, 0x50, 0xa4, 0x51, 0xaa, 0x06, 0xa7, 0x6c, 0x60, 0x94, 0x36, + 0x65, 0x5b, 0x3c, 0xdd, 0x14, 0xd4, 0x12, 0x02, 0x62, 0x37, 0x90, 0x80, 0xc4, 0x45, 0xd9, 0x5c, + 0x20, 0xb8, 0x59, 0xcd, 0x7a, 0x27, 0x5e, 0x4b, 0x5e, 0xcf, 0x66, 0xc7, 0x89, 0x1a, 0x42, 0x24, + 0xc4, 0x13, 0x20, 0xf1, 0x08, 0xbc, 0x00, 0xe2, 0x86, 0x1b, 0x1e, 0xa0, 0x97, 0x95, 0x90, 0x10, + 0x5c, 0xf0, 0x97, 0x20, 0xf1, 0x1a, 0xc8, 0xe3, 0x33, 0xee, 0xee, 0xd6, 0x5e, 0xbb, 0x89, 0xb8, + 0x8a, 0xc7, 0x3e, 0xdf, 0x39, 0xdf, 0x37, 0x67, 0xe6, 0x3b, 0x1b, 0x58, 0xfd, 0x42, 0x44, 0xdc, + 0xed, 0x72, 0x3f, 0x64, 0xfa, 0x49, 0x0e, 0x04, 0xdb, 0xdd, 0x0f, 0x3d, 0xbf, 0x1d, 0x08, 0xb6, + 0xb7, 0x2f, 0x06, 0x87, 0x4e, 0x7f, 0x20, 0x23, 0x49, 0x96, 0xd2, 0x40, 0xc7, 0x04, 0x3a, 0x26, + 0xd0, 0xae, 0xba, 0x52, 0xf5, 0xa4, 0x62, 0x6d, 0xae, 0x10, 0xc5, 0x0e, 0x6a, 0x6d, 0x11, 0xf1, + 0x1a, 0xeb, 0x73, 0xcf, 0x0f, 0x79, 0xe4, 0xcb, 0x30, 0x49, 0x64, 0xb3, 0x49, 0x15, 0x77, 0xe5, + 0x40, 0xf8, 0x5e, 0xd8, 0x72, 0xa5, 0x1f, 0x2a, 0x04, 0xd4, 0x26, 0x01, 0xd4, 0xa1, 0x8a, 0x44, + 0xaf, 0xe5, 0xca, 0x30, 0x1a, 0x70, 0x37, 0x42, 0xc8, 0x82, 0x27, 0x3d, 0xa9, 0x1f, 0x59, 0xfc, + 0x84, 0x6f, 0xaf, 0x79, 0x52, 0x7a, 0x81, 0x60, 0xbc, 0xef, 0x33, 0x1e, 0x86, 0x32, 0xd2, 0xb4, + 0x4c, 0x99, 0xab, 0xa8, 0xa1, 0xa7, 0x3c, 0x76, 0x50, 0x8b, 0xff, 0x24, 0x1f, 0xe8, 0x5d, 0x58, + 0xfa, 0x24, 0x96, 0xb4, 0x2d, 0xa2, 0xad, 0x84, 0xde, 0x66, 0xcc, 0xae, 0x29, 0xf6, 0xf6, 0x85, + 0x8a, 0xc8, 0x02, 0x4c, 0xfb, 0x61, 0x47, 0x3c, 0x5c, 0xb4, 0x5e, 0xb1, 0x6e, 0xce, 0x35, 0x93, + 0x05, 0x55, 0x70, 0x2d, 0x1b, 0xa4, 0xfa, 0x32, 0x54, 0x82, 0xec, 0xc0, 0xfc, 0xee, 0xd0, 0x7b, + 0x0d, 0xbe, 0xb4, 0xf6, 0x9a, 0x33, 0x61, 0x97, 0x9d, 0xe1, 0x44, 0x8d, 0xe7, 0x1e, 0xfd, 0xb1, + 0x3c, 0xd5, 0x1c, 0x49, 0x42, 0x05, 0x32, 0xad, 0x07, 0x41, 0x16, 0xd3, 0x2d, 0x80, 0x27, 0xdd, + 0xc0, 0x8a, 0x37, 0x9c, 0x44, 0xb6, 0x13, 0xb7, 0xce, 0x49, 0x1a, 0x8e, 0xad, 0x73, 0x1e, 0x70, + 0x4f, 0x20, 0xb6, 0x39, 0x84, 0xa4, 0x3f, 0x59, 0x28, 0xee, 0xa9, 0x3a, 0xb9, 0xe2, 0x2e, 0x9e, + 0x5b, 0x1c, 0xd9, 0x1e, 0x61, 0x7f, 0x41, 0xb3, 0x5f, 0x2d, 0x64, 0x9f, 0x30, 0x1a, 0xa1, 0xff, + 0x29, 0xac, 0x64, 0xb5, 0x66, 0x6b, 0x20, 0x7b, 0x75, 0xa5, 0x44, 0x64, 0xb6, 0xeb, 0x25, 0x98, + 0xd5, 0x64, 0x5b, 0x7e, 0x47, 0x6f, 0xd6, 0xc5, 0xe6, 0x8c, 0x5e, 0x7f, 0xd4, 0x89, 0x7b, 0xce, + 0xe3, 0x50, 0x4d, 0x63, 0xae, 0x99, 0x2c, 0xe8, 0x97, 0x70, 0xbd, 0x20, 0xf1, 0xff, 0xd9, 0xfc, + 0x65, 0x78, 0xd9, 0x54, 0xdf, 0xd1, 0x97, 0x62, 0x13, 0xef, 0x04, 0xea, 0xa1, 0x47, 0x50, 0xc9, + 0x0b, 0x40, 0x5e, 0x9f, 0xc1, 0xe5, 0xd1, 0x2f, 0xc8, 0xec, 0xd6, 0x44, 0x66, 0xa3, 0x10, 0xe4, + 0x36, 0x96, 0x88, 0xbe, 0x0a, 0xcb, 0xa6, 0xf8, 0x36, 0x57, 0x3b, 0x11, 0x6f, 0xfb, 0x81, 0x1f, + 0x1d, 0x3e, 0x90, 0x32, 0xa8, 0x77, 0x3a, 0x03, 0xa1, 0x14, 0xdd, 0x83, 0xd5, 0x82, 0x90, 0x94, + 0xe8, 0x75, 0xb8, 0x9c, 0x34, 0xbe, 0xc5, 0x93, 0x2f, 0x78, 0xf9, 0x5e, 0x48, 0xde, 0x62, 0x38, + 0x59, 0x86, 0x4b, 0xe2, 0xa0, 0x97, 0xc6, 0x24, 0xcd, 0x02, 0x71, 0xd0, 0x33, 0x25, 0x37, 0xf2, + 0x59, 0x35, 0x78, 0xc0, 0x43, 0x57, 0x4c, 0x38, 0x05, 0x74, 0x33, 0x9f, 0x30, 0xa2, 0x53, 0xc2, + 0x8b, 0x30, 0xd3, 0x4e, 0x5e, 0x21, 0x0b, 0xb3, 0x4c, 0x37, 0xa6, 0x1e, 0x04, 0x39, 0x49, 0xe8, + 0x6f, 0x16, 0x16, 0xca, 0x8f, 0x49, 0x0b, 0x85, 0x30, 0x8b, 0x99, 0xcd, 0xb5, 0xfb, 0x78, 0x62, + 0xf3, 0x4a, 0xe6, 0x75, 0x70, 0x8d, 0xdd, 0x4d, 0x6b, 0xd8, 0xef, 0xc2, 0x4c, 0xf1, 0x4e, 0x4d, + 0x90, 0x7f, 0x07, 0x16, 0x34, 0x85, 0x4d, 0xd9, 0x11, 0x1f, 0x72, 0xd5, 0x35, 0x97, 0x6f, 0x11, + 0x66, 0x46, 0x5b, 0x6b, 0x96, 0xf4, 0x0d, 0xb8, 0x32, 0x86, 0x40, 0xe9, 0x4b, 0x30, 0xe7, 0xca, + 0x8e, 0x68, 0x75, 0xb9, 0xea, 0x22, 0x68, 0xd6, 0xc5, 0xa0, 0xb5, 0x3f, 0xe7, 0x61, 0x5a, 0xc3, + 0xc8, 0x8f, 0x16, 0xcc, 0x0f, 0x5f, 0x26, 0x72, 0xbf, 0x78, 0x83, 0xb2, 0xad, 0xdf, 0x7e, 0xeb, + 0x0c, 0xc8, 0x84, 0x2c, 0x5d, 0xfb, 0xfa, 0xe7, 0x7f, 0xbe, 0xbd, 0x70, 0x9b, 0x54, 0xf5, 0x4c, + 0x7b, 0x3d, 0x19, 0x6f, 0xd9, 0x63, 0x90, 0x1d, 0xe9, 0x91, 0x72, 0x4c, 0x7e, 0xb0, 0xe0, 0xc5, + 0xe1, 0x64, 0xf5, 0x20, 0x28, 0x43, 0x3e, 0x7b, 0x1a, 0x94, 0x21, 0x9f, 0xe3, 0xef, 0xb4, 0xaa, + 0xc9, 0xaf, 0x10, 0x5a, 0x4c, 0x9e, 0xfc, 0x6e, 0xc1, 0x95, 0x4c, 0x37, 0x24, 0xf5, 0x67, 0xde, + 0xbd, 0x71, 0x8b, 0xb6, 0x1b, 0xe7, 0x49, 0x81, 0x62, 0xde, 0xd1, 0x62, 0xee, 0x91, 0x37, 0xcb, + 0x74, 0xc2, 0x1c, 0xf0, 0x63, 0x76, 0xa4, 0x3d, 0xff, 0x38, 0x3e, 0x4e, 0x63, 0x5e, 0x47, 0xd6, + 0x4b, 0xb1, 0xca, 0x34, 0x69, 0xfb, 0xed, 0x33, 0x61, 0x51, 0xca, 0x6d, 0x2d, 0xe5, 0x06, 0x59, + 0xc9, 0x94, 0x32, 0xf6, 0x53, 0x89, 0xfc, 0x62, 0xc1, 0xd5, 0x1c, 0xa3, 0x25, 0x1b, 0xa5, 0x68, + 0xe4, 0xa0, 0xed, 0xf7, 0xcf, 0x83, 0x4e, 0xd5, 0xdc, 0xd3, 0x6a, 0x6a, 0x84, 0x65, 0xaa, 0xf1, + 0xb8, 0x6a, 0x29, 0x03, 0x6f, 0xf5, 0xa5, 0x0c, 0x8c, 0xcf, 0x93, 0xbf, 0x33, 0x84, 0x19, 0x93, + 0x3a, 0x9b, 0x30, 0x44, 0x9f, 0x51, 0xd8, 0x98, 0x97, 0xd2, 0x86, 0x16, 0xb6, 0x41, 0xd6, 0xcb, + 0x0a, 0x43, 0xb3, 0x1c, 0x3a, 0x7e, 0xe4, 0xc4, 0x02, 0x3b, 0xa7, 0x4e, 0x6c, 0x0b, 0x1b, 0xe7, + 0x31, 0xfd, 0x32, 0x32, 0x8b, 0x47, 0x06, 0x7d, 0x4f, 0xcb, 0x5c, 0x27, 0xf7, 0x87, 0x65, 0x3e, + 0xfd, 0x0b, 0x3e, 0x5f, 0x2f, 0xf9, 0xce, 0x82, 0x59, 0x63, 0xf3, 0xa4, 0x56, 0x4c, 0x6a, 0x6c, + 0x88, 0xd8, 0x6b, 0xcf, 0x02, 0x41, 0xd6, 0x77, 0x34, 0xeb, 0x2a, 0xb9, 0x99, 0xd9, 0x9c, 0x74, + 0xc0, 0xb0, 0x23, 0x3c, 0x6d, 0xc7, 0xf6, 0xf4, 0x57, 0xff, 0x7e, 0x5f, 0xb5, 0x1a, 0x1f, 0x3c, + 0x3a, 0xa9, 0x58, 0x8f, 0x4f, 0x2a, 0xd6, 0x5f, 0x27, 0x15, 0xeb, 0x9b, 0xd3, 0xca, 0xd4, 0xe3, + 0xd3, 0xca, 0xd4, 0xaf, 0xa7, 0x95, 0xa9, 0xcf, 0x6f, 0x79, 0x7e, 0xd4, 0xdd, 0x6f, 0x3b, 0xae, + 0xec, 0x0d, 0x27, 0x0d, 0x65, 0x47, 0xb0, 0x87, 0x4f, 0x72, 0x47, 0x87, 0x7d, 0xa1, 0xda, 0xcf, + 0xeb, 0x7f, 0x3a, 0xee, 0xfe, 0x17, 0x00, 0x00, 0xff, 0xff, 0x34, 0xa2, 0x67, 0x89, 0x99, 0x0d, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -799,6 +906,8 @@ type QueryClient interface { ForeignCoins(ctx context.Context, in *QueryGetForeignCoinsRequest, opts ...grpc.CallOption) (*QueryGetForeignCoinsResponse, error) // Queries a list of ForeignCoins items. ForeignCoinsAll(ctx context.Context, in *QueryAllForeignCoinsRequest, opts ...grpc.CallOption) (*QueryAllForeignCoinsResponse, error) + // Queries a ForeignCoins by chain_id and asset. + ForeignCoinsFromAsset(ctx context.Context, in *QueryGetForeignCoinsFromAssetRequest, opts ...grpc.CallOption) (*QueryGetForeignCoinsFromAssetResponse, error) // Queries SystemContract SystemContract(ctx context.Context, in *QueryGetSystemContractRequest, opts ...grpc.CallOption) (*QueryGetSystemContractResponse, error) // Queries the address of a gas stability pool on a given chain. @@ -837,6 +946,15 @@ func (c *queryClient) ForeignCoinsAll(ctx context.Context, in *QueryAllForeignCo return out, nil } +func (c *queryClient) ForeignCoinsFromAsset(ctx context.Context, in *QueryGetForeignCoinsFromAssetRequest, opts ...grpc.CallOption) (*QueryGetForeignCoinsFromAssetResponse, error) { + out := new(QueryGetForeignCoinsFromAssetResponse) + err := c.cc.Invoke(ctx, "/zetachain.zetacore.fungible.Query/ForeignCoinsFromAsset", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *queryClient) SystemContract(ctx context.Context, in *QueryGetSystemContractRequest, opts ...grpc.CallOption) (*QueryGetSystemContractResponse, error) { out := new(QueryGetSystemContractResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.fungible.Query/SystemContract", in, out, opts...) @@ -888,6 +1006,8 @@ type QueryServer interface { ForeignCoins(context.Context, *QueryGetForeignCoinsRequest) (*QueryGetForeignCoinsResponse, error) // Queries a list of ForeignCoins items. ForeignCoinsAll(context.Context, *QueryAllForeignCoinsRequest) (*QueryAllForeignCoinsResponse, error) + // Queries a ForeignCoins by chain_id and asset. + ForeignCoinsFromAsset(context.Context, *QueryGetForeignCoinsFromAssetRequest) (*QueryGetForeignCoinsFromAssetResponse, error) // Queries SystemContract SystemContract(context.Context, *QueryGetSystemContractRequest) (*QueryGetSystemContractResponse, error) // Queries the address of a gas stability pool on a given chain. @@ -910,6 +1030,9 @@ func (*UnimplementedQueryServer) ForeignCoins(ctx context.Context, req *QueryGet func (*UnimplementedQueryServer) ForeignCoinsAll(ctx context.Context, req *QueryAllForeignCoinsRequest) (*QueryAllForeignCoinsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ForeignCoinsAll not implemented") } +func (*UnimplementedQueryServer) ForeignCoinsFromAsset(ctx context.Context, req *QueryGetForeignCoinsFromAssetRequest) (*QueryGetForeignCoinsFromAssetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ForeignCoinsFromAsset not implemented") +} func (*UnimplementedQueryServer) SystemContract(ctx context.Context, req *QueryGetSystemContractRequest) (*QueryGetSystemContractResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SystemContract not implemented") } @@ -966,6 +1089,24 @@ func _Query_ForeignCoinsAll_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Query_ForeignCoinsFromAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryGetForeignCoinsFromAssetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).ForeignCoinsFromAsset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/zetachain.zetacore.fungible.Query/ForeignCoinsFromAsset", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).ForeignCoinsFromAsset(ctx, req.(*QueryGetForeignCoinsFromAssetRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Query_SystemContract_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryGetSystemContractRequest) if err := dec(in); err != nil { @@ -1068,6 +1209,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "ForeignCoinsAll", Handler: _Query_ForeignCoinsAll_Handler, }, + { + MethodName: "ForeignCoinsFromAsset", + Handler: _Query_ForeignCoinsFromAsset_Handler, + }, { MethodName: "SystemContract", Handler: _Query_SystemContract_Handler, @@ -1240,6 +1385,74 @@ func (m *QueryAllForeignCoinsResponse) MarshalToSizedBuffer(dAtA []byte) (int, e return len(dAtA) - i, nil } +func (m *QueryGetForeignCoinsFromAssetRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetForeignCoinsFromAssetRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetForeignCoinsFromAssetRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Asset) > 0 { + i -= len(m.Asset) + copy(dAtA[i:], m.Asset) + i = encodeVarintQuery(dAtA, i, uint64(len(m.Asset))) + i-- + dAtA[i] = 0x12 + } + if m.ChainId != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.ChainId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *QueryGetForeignCoinsFromAssetResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetForeignCoinsFromAssetResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetForeignCoinsFromAssetResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.ForeignCoins.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func (m *QueryGetSystemContractRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1636,6 +1849,33 @@ func (m *QueryAllForeignCoinsResponse) Size() (n int) { return n } +func (m *QueryGetForeignCoinsFromAssetRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ChainId != 0 { + n += 1 + sovQuery(uint64(m.ChainId)) + } + l = len(m.Asset) + if l > 0 { + n += 1 + l + sovQuery(uint64(l)) + } + return n +} + +func (m *QueryGetForeignCoinsFromAssetResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.ForeignCoins.Size() + n += 1 + l + sovQuery(uint64(l)) + return n +} + func (m *QueryGetSystemContractRequest) Size() (n int) { if m == nil { return 0 @@ -2150,6 +2390,190 @@ func (m *QueryAllForeignCoinsResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryGetForeignCoinsFromAssetRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetForeignCoinsFromAssetRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetForeignCoinsFromAssetRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + m.ChainId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ChainId |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Asset", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + 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 ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Asset = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryGetForeignCoinsFromAssetResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetForeignCoinsFromAssetResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetForeignCoinsFromAssetResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ForeignCoins", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ForeignCoins.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *QueryGetSystemContractRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/x/fungible/types/query.pb.gw.go b/x/fungible/types/query.pb.gw.go index f35a7e5b2f..69c27652b2 100644 --- a/x/fungible/types/query.pb.gw.go +++ b/x/fungible/types/query.pb.gw.go @@ -123,6 +123,82 @@ func local_request_Query_ForeignCoinsAll_0(ctx context.Context, marshaler runtim } +func request_Query_ForeignCoinsFromAsset_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryGetForeignCoinsFromAssetRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["chain_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "chain_id") + } + + protoReq.ChainId, err = runtime.Int64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "chain_id", err) + } + + val, ok = pathParams["asset"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset") + } + + protoReq.Asset, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset", err) + } + + msg, err := client.ForeignCoinsFromAsset(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_ForeignCoinsFromAsset_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryGetForeignCoinsFromAssetRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["chain_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "chain_id") + } + + protoReq.ChainId, err = runtime.Int64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "chain_id", err) + } + + val, ok = pathParams["asset"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset") + } + + protoReq.Asset, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset", err) + } + + msg, err := server.ForeignCoinsFromAsset(ctx, &protoReq) + return msg, metadata, err + +} + func request_Query_SystemContract_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq QueryGetSystemContractRequest var metadata runtime.ServerMetadata @@ -337,6 +413,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_ForeignCoinsFromAsset_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_ForeignCoinsFromAsset_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_ForeignCoinsFromAsset_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Query_SystemContract_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -533,6 +632,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_ForeignCoinsFromAsset_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_ForeignCoinsFromAsset_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_ForeignCoinsFromAsset_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Query_SystemContract_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -641,6 +760,8 @@ var ( pattern_Query_ForeignCoinsAll_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"zeta-chain", "fungible", "foreign_coins"}, "", runtime.AssumeColonVerbOpt(false))) + pattern_Query_ForeignCoinsFromAsset_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 1, 0, 4, 1, 5, 4}, []string{"zeta-chain", "fungible", "foreign_coins", "chain_id", "asset"}, "", runtime.AssumeColonVerbOpt(false))) + pattern_Query_SystemContract_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"zeta-chain", "fungible", "system_contract"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_GasStabilityPoolAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"zeta-chain", "fungible", "gas_stability_pool_address"}, "", runtime.AssumeColonVerbOpt(false))) @@ -657,6 +778,8 @@ var ( forward_Query_ForeignCoinsAll_0 = runtime.ForwardResponseMessage + forward_Query_ForeignCoinsFromAsset_0 = runtime.ForwardResponseMessage + forward_Query_SystemContract_0 = runtime.ForwardResponseMessage forward_Query_GasStabilityPoolAddress_0 = runtime.ForwardResponseMessage diff --git a/x/observer/types/chain_params.go b/x/observer/types/chain_params.go index 8bfb5c355c..fd3f7aeaa6 100644 --- a/x/observer/types/chain_params.go +++ b/x/observer/types/chain_params.go @@ -151,6 +151,12 @@ func (cp ChainParams) Validate() error { return nil } +// IsInboundFastConfirmationEnabled returns true if fast inbound confirmation is enabled. +func (cp ChainParams) IsInboundFastConfirmationEnabled() bool { + return cp.ConfirmationParams.FastInboundCount > 0 && + cp.ConfirmationParams.FastInboundCount < cp.ConfirmationParams.SafeInboundCount +} + // InboundConfirmationSafe returns the safe number of confirmation for inbound observation. func (cp ChainParams) InboundConfirmationSafe() uint64 { return cp.ConfirmationParams.SafeInboundCount diff --git a/x/observer/types/chain_params_test.go b/x/observer/types/chain_params_test.go index 8e30af2f07..50d3b9731c 100644 --- a/x/observer/types/chain_params_test.go +++ b/x/observer/types/chain_params_test.go @@ -326,6 +326,23 @@ func (s *UpdateChainParamsSuite) TestCoreContractAddresses() { require.Error(s.T(), cp.Validate()) } +func Test_IsInboundFastConfirmationEnabled(t *testing.T) { + cp := sample.ChainParams(1) + + // fast confirmation is enabled + cp.ConfirmationParams.SafeInboundCount = 2 + cp.ConfirmationParams.FastInboundCount = 1 + require.True(t, cp.IsInboundFastConfirmationEnabled()) + + // fast confirmation is disabled if fast count = 0 + cp.ConfirmationParams.FastInboundCount = 0 + require.False(t, cp.IsInboundFastConfirmationEnabled()) + + // fast confirmation is disabled if fast count == safe count + cp.ConfirmationParams.FastInboundCount = 2 + require.False(t, cp.IsInboundFastConfirmationEnabled()) +} + func Test_InboundConfirmationSafe(t *testing.T) { cp := sample.ChainParams(1) diff --git a/zetaclient/chains/base/confirmation.go b/zetaclient/chains/base/confirmation.go index 4aa0a06cff..fb214860fb 100644 --- a/zetaclient/chains/base/confirmation.go +++ b/zetaclient/chains/base/confirmation.go @@ -1,5 +1,15 @@ package base +import ( + "context" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/chains" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + // GetScanRangeInboundSafe calculates the block range to scan using inbound safe confirmation count. // It returns a range of blocks [from, end (exclusive)) that need to be scanned. func (ob *Observer) GetScanRangeInboundSafe(blockLimit uint64) (from uint64, end uint64) { @@ -50,14 +60,47 @@ func (ob *Observer) IsBlockConfirmedForOutboundFast(blockNumber uint64) bool { return isBlockConfirmed(blockNumber, confirmation, lastBlock) } +// IsInboundEligibleForFastConfirmation determines if given inbound vote message is eligible for fast confirmation. +func (ob *Observer) IsInboundEligibleForFastConfirmation( + ctx context.Context, + msg *crosschaintypes.MsgVoteInbound, +) (bool, error) { + // check if fast confirmation is enabled + if !ob.ChainParams().IsInboundFastConfirmationEnabled() { + return false, nil + } + + // check eligibility + if !msg.EligibleForFastConfirmation() { + return false, nil + } + + // query liquidity cap for asset + chainID := msg.SenderChainId + fCoins, err := ob.zetacoreClient.GetForeignCoinsFromAsset(ctx, chainID, ethcommon.HexToAddress(msg.Asset)) + if err != nil { + return false, errors.Wrapf(err, "unable to get foreign coins for asset %s on chain %d", msg.Asset, chainID) + } + + // ensure the deposit amount does not exceed amount cap + fastAmountCap := chains.CalcInboundFastConfirmationAmountCap(chainID, fCoins.LiquidityCap) + if msg.Amount.GT(fastAmountCap) { + return false, nil + } + + return true, nil +} + // calcUnscannedBlockRange calculates the unscanned block range [from, end (exclusive)) within given block limit. // // example 1: given lastBlock = 99, lastScanned = 90, confirmation = 10, then no unscanned block // example 2: given lastBlock = 100, lastScanned = 90, confirmation = 10, then 1 unscanned block (block 91) func calcUnscannedBlockRange(lastBlock, lastScanned, confirmation, blockLimit uint64) (from uint64, end uint64) { // got unscanned blocks or not? + // returning same values to indicate no unscanned block + nextBlock := lastScanned + 1 if lastBlock < lastScanned+confirmation { - return 0, 0 + return nextBlock, nextBlock } // calculate the highest confirmed block @@ -65,13 +108,9 @@ func calcUnscannedBlockRange(lastBlock, lastScanned, confirmation, blockLimit ui highestConfirmed := lastBlock - confirmation + 1 // calculate a range of unscanned blocks within block limit - from = lastScanned + 1 - end = from + blockLimit - // 'end' is exclusive, so ensure it is not greater than (highestConfirmed+1) - if end > highestConfirmed+1 { - end = highestConfirmed + 1 - } + from = nextBlock + end = min(from+blockLimit, highestConfirmed+1) return from, end } diff --git a/zetaclient/chains/base/confirmation_test.go b/zetaclient/chains/base/confirmation_test.go index 7668f704a8..83f5f729f7 100644 --- a/zetaclient/chains/base/confirmation_test.go +++ b/zetaclient/chains/base/confirmation_test.go @@ -1,10 +1,18 @@ package base_test import ( + "context" + "errors" "testing" + sdkmath "cosmossdk.io/math" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -27,7 +35,7 @@ func Test_GetScanRangeInboundSafe(t *testing.T) { confParams: observertypes.ConfirmationParams{ SafeInboundCount: 10, }, - expectedBlockRange: [2]uint64{0, 0}, // [0, 0) + expectedBlockRange: [2]uint64{91, 91}, // [91, 91), nothing to scan }, { name: "1 unscanned blocks", @@ -93,7 +101,7 @@ func Test_GetScanRangeInboundFast(t *testing.T) { SafeInboundCount: 10, FastInboundCount: 10, }, - expectedBlockRange: [2]uint64{0, 0}, // [0, 0) + expectedBlockRange: [2]uint64{91, 91}, // [91, 91), nothing to scan }, { name: "1 unscanned blocks", @@ -298,3 +306,109 @@ func Test_IsBlockConfirmedForOutboundFast(t *testing.T) { }) } } + +func Test_IsInboundEligibleForFastConfirmation(t *testing.T) { + chain := chains.Ethereum + liquidityCap := sdkmath.NewUint(100_000) + fastAmountCap := chains.CalcInboundFastConfirmationAmountCap(chain.ChainId, liquidityCap) + confParamsEnabled := observertypes.ConfirmationParams{ + SafeInboundCount: 2, + FastInboundCount: 1, + } + + tests := []struct { + name string + confParams observertypes.ConfirmationParams + msg *crosschaintypes.MsgVoteInbound + failForeignCoinsRPC bool + eligible bool + errMsg string + }{ + { + name: "eligible for fast confirmation", + confParams: confParamsEnabled, + msg: &crosschaintypes.MsgVoteInbound{ + SenderChainId: chain.ChainId, + Amount: sdkmath.NewUint(fastAmountCap.Uint64()), + CoinType: coin.CoinType_Gas, + Asset: "", + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V2, + }, + eligible: true, + }, + { + name: "not eligible if fast confirmation is disabled", + confParams: observertypes.ConfirmationParams{ + SafeInboundCount: 2, + FastInboundCount: 2, // equal to safe confirmation, effectively disabled + }, + msg: &crosschaintypes.MsgVoteInbound{ + SenderChainId: chains.SolanaMainnet.ChainId, // not set for Solana + }, + eligible: false, + }, + { + name: "not eligible if protocol contract version V1 is used", + confParams: confParamsEnabled, + msg: &crosschaintypes.MsgVoteInbound{ + SenderChainId: chain.ChainId, + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V1, // not eligible for V1 + }, + eligible: false, + }, + { + name: "return error if foreign coins query RPC fails", + confParams: confParamsEnabled, + msg: &crosschaintypes.MsgVoteInbound{ + SenderChainId: chain.ChainId, + CoinType: coin.CoinType_Gas, + Asset: "", + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V2, + }, + failForeignCoinsRPC: true, + eligible: false, + errMsg: "unable to get foreign coins", + }, + { + name: "not eligible if amount exceeds fast amount cap", + confParams: confParamsEnabled, + msg: &crosschaintypes.MsgVoteInbound{ + SenderChainId: chain.ChainId, + Amount: sdkmath.NewUint(fastAmountCap.Uint64() + 1), // +1 to exceed + CoinType: coin.CoinType_Gas, + Asset: "", + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V2, + }, + eligible: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // ARRANGE + ob := newTestSuite(t, chain, withConfirmationParams(tt.confParams)) + + // mock up the foreign coins RPC + assetAddress := ethcommon.HexToAddress(tt.msg.Asset) + if tt.failForeignCoinsRPC { + ob.zetacore.On("GetForeignCoinsFromAsset", mock.Anything, chain.ChainId, assetAddress). + Maybe(). + Return(fungibletypes.ForeignCoins{}, errors.New("rpc failed")) + } else { + ob.zetacore.On("GetForeignCoinsFromAsset", mock.Anything, chain.ChainId, assetAddress).Maybe().Return(fungibletypes.ForeignCoins{LiquidityCap: liquidityCap}, nil) + } + + // ACT + ctx := context.Background() + eligible, err := ob.IsInboundEligibleForFastConfirmation(ctx, tt.msg) + + // ASSERT + require.Equal(t, tt.eligible, eligible) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + }) + } +} diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index bdceb2e128..ff2180db7e 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -395,9 +395,10 @@ func (ob *Observer) PostVoteInbound( // prepare logger fields lf := map[string]any{ - logs.FieldMethod: "PostVoteInbound", - logs.FieldTx: txHash, - logs.FieldCoinType: coinType.String(), + logs.FieldMethod: "PostVoteInbound", + logs.FieldTx: txHash, + logs.FieldCoinType: coinType.String(), + logs.FieldConfirmationMode: msg.ConfirmationMode.String(), } // make sure the message is valid to avoid unnecessary retries diff --git a/zetaclient/chains/bitcoin/observer/event.go b/zetaclient/chains/bitcoin/observer/event.go index ce0489263e..0650b19536 100644 --- a/zetaclient/chains/bitcoin/observer/event.go +++ b/zetaclient/chains/bitcoin/observer/event.go @@ -183,6 +183,12 @@ func (ob *Observer) NewInboundVoteFromLegacyMemo( event *BTCInboundEvent, amountSats *big.Int, ) *crosschaintypes.MsgVoteInbound { + // determine confirmation mode + confirmationMode := crosschaintypes.ConfirmationMode_FAST + if ob.IsBlockConfirmedForInboundSafe(event.BlockNumber) { + confirmationMode = crosschaintypes.ConfirmationMode_SAFE + } + return crosschaintypes.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.FromAddress, @@ -201,14 +207,12 @@ func (ob *Observer) NewInboundVoteFromLegacyMemo( crosschaintypes.ProtocolContractVersion_V2, false, // no arbitrary call for deposit to ZetaChain event.Status, - crosschaintypes.ConfirmationMode_SAFE, + confirmationMode, crosschaintypes.WithCrossChainCall(len(event.MemoBytes) > 0), ) } // NewInboundVoteFromStdMemo creates a MsgVoteInbound message for inbound that uses standard memo -// TODO: upgrade to ProtocolContractVersion_V2 and enable more options -// https://github.com/zeta-chain/node/issues/2711 func (ob *Observer) NewInboundVoteFromStdMemo( event *BTCInboundEvent, amountSats *big.Int, @@ -223,6 +227,12 @@ func (ob *Observer) NewInboundVoteFromStdMemo( // check if the memo is a cross-chain call, or simple token deposit isCrosschainCall := event.MemoStd.OpCode == memo.OpCodeCall || event.MemoStd.OpCode == memo.OpCodeDepositAndCall + // determine confirmation mode + confirmationMode := crosschaintypes.ConfirmationMode_FAST + if ob.IsBlockConfirmedForInboundSafe(event.BlockNumber) { + confirmationMode = crosschaintypes.ConfirmationMode_SAFE + } + return crosschaintypes.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.FromAddress, @@ -241,7 +251,7 @@ func (ob *Observer) NewInboundVoteFromStdMemo( crosschaintypes.ProtocolContractVersion_V2, false, // no arbitrary call for deposit to ZetaChain event.Status, - crosschaintypes.ConfirmationMode_SAFE, + confirmationMode, crosschaintypes.WithRevertOptions(revertOptions), crosschaintypes.WithCrossChainCall(isCrosschainCall), ) diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index e73b0eef1a..a58b8839bc 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -367,6 +367,9 @@ func Test_NewInboundVoteFromLegacyMemo(t *testing.T) { // test amount amountSats := big.NewInt(1000) + // mock SAFE confirmed block + ob.WithLastBlock(event.BlockNumber + ob.ChainParams().InboundConfirmationSafe()) + // expected vote expectedVote := crosschaintypes.MsgVoteInbound{ Sender: event.FromAddress, @@ -422,6 +425,9 @@ func Test_NewInboundVoteFromStdMemo(t *testing.T) { // test amount amountSats := big.NewInt(1000) + // mock SAFE confirmed block + ob.WithLastBlock(event.BlockNumber + ob.ChainParams().InboundConfirmationSafe()) + // expected vote memoBytesExpected := event.MemoStd.Payload expectedVote := crosschaintypes.MsgVoteInbound{ diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 4ae043c92d..fb7c375274 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -28,27 +28,41 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { return err } - // get the block range to scan - startBlock, endBlock := ob.GetScanRangeInboundSafe(config.MaxBlocksPerScan) - if startBlock >= endBlock { - return nil - } + // scan SAFE confirmed blocks + startBlockSafe, endBlockSafe := ob.GetScanRangeInboundSafe(config.MaxBlocksPerScan) + if startBlockSafe < endBlockSafe { + // observe inbounds for the block range [startBlock, endBlock-1] + lastScannedNew, err := ob.observeInboundInBlockRange(ctx, startBlockSafe, endBlockSafe-1) + if err != nil { + logger.Error(). + Err(err). + Uint64("from", startBlockSafe). + Uint64("to", endBlockSafe-1). + Msg("error observing inbounds in block range") + } - // observe inbounds for the block range [startBlock, endBlock-1] - lastScannedNew, err := ob.observeInboundInBlockRange(ctx, startBlock, endBlock-1) - if err != nil { - logger.Error(). - Err(err). - Uint64("from", startBlock). - Uint64("to", endBlock-1). - Msg("error observing inbounds in block range") + // save last scanned block to both memory and db + if lastScannedNew > ob.LastBlockScanned() { + logger.Info(). + Uint64("from", startBlockSafe). + Uint64("to", lastScannedNew). + Msg("observed blocks for inbounds") + if err := ob.SaveLastBlockScanned(lastScannedNew); err != nil { + return errors.Wrapf(err, "unable to save last scanned Bitcoin block %d", lastScannedNew) + } + } } - // save last scanned block to both memory and db - if lastScannedNew > ob.LastBlockScanned() { - logger.Info().Uint64("from", startBlock).Uint64("to", lastScannedNew).Msg("observed blocks for inbounds") - if err := ob.SaveLastBlockScanned(lastScannedNew); err != nil { - return errors.Wrapf(err, "unable to save last scanned Bitcoin block %d", lastScannedNew) + // scan FAST confirmed blocks if available + _, endBlockFast := ob.GetScanRangeInboundFast(config.MaxBlocksPerScan) + if endBlockSafe < endBlockFast { + _, err := ob.observeInboundInBlockRange(ctx, endBlockSafe, endBlockFast-1) + if err != nil { + logger.Error(). + Err(err). + Uint64("from", endBlockSafe). + Uint64("to", endBlockFast-1). + Msg("error observing inbounds in block range (fast)") } } @@ -93,6 +107,21 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, for _, event := range events { msg := ob.GetInboundVoteFromBtcEvent(event) if msg != nil { + // skip early observed inbound that is not eligible for fast confirmation + if msg.ConfirmationMode == types.ConfirmationMode_FAST { + eligible, err := ob.IsInboundEligibleForFastConfirmation(ctx, msg) + if err != nil { + return blockNumber - 1, errors.Wrapf( + err, + "unable to determine inbound fast confirmation eligibility for tx %s", + event.TxHash, + ) + } + if !eligible { + continue + } + } + _, err = ob.PostVoteInbound(ctx, msg, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { // we have to re-scan this block next time @@ -135,7 +164,6 @@ func FilterAndParseIncomingTx( if event != nil { events = append(events, event) - logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) } } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index db4e3c475b..8dbe88f889 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -112,22 +112,28 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // https://github.com/zeta-chain/node/issues/3186 //return nil - // get the block range to scan - // Note: using separate scan range for each event incur more complexity (metrics, db, etc) and not worth it - startBlock, endBlock := ob.GetScanRangeInboundSafe(config.MaxBlocksPerScan) - if startBlock >= endBlock { - return nil + // scan SAFE confirmed blocks + startBlockSafe, endBlockSafe := ob.GetScanRangeInboundSafe(config.MaxBlocksPerScan) + if startBlockSafe < endBlockSafe { + // observe inbounds in block range [startBlock, endBlock-1] + lastScannedNew := ob.observeInboundInBlockRange(ctx, startBlockSafe, endBlockSafe-1) + + // save last scanned block to both memory and db + if lastScannedNew > ob.LastBlockScanned() { + logger.Debug(). + Uint64("from", startBlockSafe). + Uint64("to", lastScannedNew). + Msg("observed blocks for inbounds") + if err := ob.SaveLastBlockScanned(lastScannedNew); err != nil { + return errors.Wrapf(err, "unable to save last scanned block %d", lastScannedNew) + } + } } - // observe inbounds in block range [startBlock, endBlock-1] - lastScannedNew := ob.observeInboundInBlockRange(ctx, startBlock, endBlock-1) - - // save last scanned block to both memory and db - if lastScannedNew > ob.LastBlockScanned() { - logger.Debug().Uint64("from", startBlock).Uint64("to", lastScannedNew).Msg("observed blocks for inbounds") - if err := ob.SaveLastBlockScanned(lastScannedNew); err != nil { - return errors.Wrapf(err, "unable to save last scanned block %d", lastScannedNew) - } + // scan FAST confirmed blocks if available + _, endBlockFast := ob.GetScanRangeInboundFast(config.MaxBlocksPerScan) + if endBlockSafe < endBlockFast { + ob.observeInboundInBlockRange(ctx, endBlockSafe, endBlockFast-1) } return nil diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index 69cdc8fb83..28b5d2e565 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -90,10 +90,20 @@ func (ob *Observer) observeGatewayDeposit( msg := ob.newDepositInboundVote(event) - ob.Logger().Inbound.Info(). - Msgf("ObserveGateway: Deposit inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.Chain(). - ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, event.Sender.Hex(), event.Amount.String(), hex.EncodeToString(event.Payload)) + // skip early observed inbound that is not eligible for fast confirmation + if msg.ConfirmationMode == types.ConfirmationMode_FAST { + eligible, err := ob.IsInboundEligibleForFastConfirmation(ctx, &msg) + if err != nil { + return lastScanned - 1, errors.Wrapf( + err, + "unable to determine inbound fast confirmation eligibility for tx %s", + event.Raw.TxHash, + ) + } + if !eligible { + continue + } + } _, err = ob.PostVoteInbound(ctx, &msg, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { @@ -175,6 +185,12 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) isCrossChainCall = true } + // determine confirmation mode + confirmationMode := types.ConfirmationMode_FAST + if ob.IsBlockConfirmedForInboundSafe(event.Raw.BlockNumber) { + confirmationMode = types.ConfirmationMode_SAFE + } + return *types.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Sender.Hex(), @@ -193,7 +209,7 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) types.ProtocolContractVersion_V2, false, // currently not relevant since calls are not arbitrary types.InboundStatus_SUCCESS, - types.ConfirmationMode_SAFE, + confirmationMode, types.WithEVMRevertOptions(event.RevertOptions), types.WithCrossChainCall(isCrossChainCall), ) diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 5213997033..87f128a606 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -18,6 +18,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ethclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" keyinterfaces "github.com/zeta-chain/node/zetaclient/keys/interfaces" @@ -63,6 +64,11 @@ type ZetacoreClient interface { GetSupportedChains(ctx context.Context) ([]chains.Chain, error) GetAdditionalChains(ctx context.Context) ([]chains.Chain, error) GetChainParams(ctx context.Context) ([]*observertypes.ChainParams, error) + GetForeignCoinsFromAsset( + ctx context.Context, + chainID int64, + assetAddress ethcommon.Address, + ) (fungibletypes.ForeignCoins, error) GetKeyGen(ctx context.Context) (observertypes.Keygen, error) GetTSS(ctx context.Context) (observertypes.TSS, error) diff --git a/zetaclient/logs/fields.go b/zetaclient/logs/fields.go index 9c267030db..fc601375e8 100644 --- a/zetaclient/logs/fields.go +++ b/zetaclient/logs/fields.go @@ -3,19 +3,20 @@ package logs // A group of predefined field keys and module names for zetaclient logs const ( // field keys - FieldModule = "module" - FieldMethod = "method" - FieldChain = "chain" - FieldChainNetwork = "chain_network" - FieldNonce = "nonce" - FieldTracker = "tracker_id" - FieldTx = "tx" - FieldOutboundID = "outbound_id" - FieldBlock = "block" - FieldCctx = "cctx" - FieldZetaTx = "zeta_tx" - FieldBallot = "ballot" - FieldCoinType = "coin_type" + FieldModule = "module" + FieldMethod = "method" + FieldChain = "chain" + FieldChainNetwork = "chain_network" + FieldNonce = "nonce" + FieldTracker = "tracker_id" + FieldTx = "tx" + FieldOutboundID = "outbound_id" + FieldBlock = "block" + FieldCctx = "cctx" + FieldZetaTx = "zeta_tx" + FieldBallot = "ballot" + FieldCoinType = "coin_type" + FieldConfirmationMode = "confirmation_mode" // module names ModNameInbound = "inbound" diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index 79a3c273e3..bb14064651 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -8,8 +8,12 @@ import ( cometbfttypes "github.com/cometbft/cometbft/types" + common "github.com/ethereum/go-ethereum/common" + context "context" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + interfaces "github.com/zeta-chain/node/zetaclient/chains/interfaces" keysinterfaces "github.com/zeta-chain/node/zetaclient/keys/interfaces" @@ -252,6 +256,34 @@ func (_m *ZetacoreClient) GetCrosschainFlags(ctx context.Context) (observertypes return r0, r1 } +// GetForeignCoinsFromAsset provides a mock function with given fields: ctx, chainID, assetAddress +func (_m *ZetacoreClient) GetForeignCoinsFromAsset(ctx context.Context, chainID int64, assetAddress common.Address) (fungibletypes.ForeignCoins, error) { + ret := _m.Called(ctx, chainID, assetAddress) + + if len(ret) == 0 { + panic("no return value specified for GetForeignCoinsFromAsset") + } + + var r0 fungibletypes.ForeignCoins + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, common.Address) (fungibletypes.ForeignCoins, error)); ok { + return rf(ctx, chainID, assetAddress) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, common.Address) fungibletypes.ForeignCoins); ok { + r0 = rf(ctx, chainID, assetAddress) + } else { + r0 = ret.Get(0).(fungibletypes.ForeignCoins) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, common.Address) error); ok { + r1 = rf(ctx, chainID, assetAddress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetInboundTrackersForChain provides a mock function with given fields: ctx, chainID func (_m *ZetacoreClient) GetInboundTrackersForChain(ctx context.Context, chainID int64) ([]types.InboundTracker, error) { ret := _m.Called(ctx, chainID) diff --git a/zetaclient/zetacore/broadcast_test.go b/zetaclient/zetacore/broadcast_test.go index f91aad2110..973c61cf81 100644 --- a/zetaclient/zetacore/broadcast_test.go +++ b/zetaclient/zetacore/broadcast_test.go @@ -80,7 +80,7 @@ func TestBroadcast(t *testing.T) { t.Run("broadcast success", func(t *testing.T) { client := setupZetacoreClient(t, withObserverKeys(observerKeys), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), ) msg := crosschaintypes.NewMsgVoteGasPrice(address.String(), chains.Ethereum.ChainId, 10000, 1000, 1) @@ -94,7 +94,7 @@ func TestBroadcast(t *testing.T) { t.Run("broadcast failed", func(t *testing.T) { client := setupZetacoreClient(t, withObserverKeys(observerKeys), - withTendermint( + withCometBFT( mocks.NewSDKClientWithErr(t, errors.New("account sequence mismatch, expected 5 got 4"), 32), ), ) diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index 809726cdd1..d3b47ac45e 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -58,8 +58,8 @@ type Client struct { var unsecureGRPC = grpc.WithTransportCredentials(insecure.NewCredentials()) type constructOpts struct { - customTendermint bool - tendermintClient cometbftrpc.Client + customCometBFT bool + cometBFTClient cometbftrpc.Client customAccountRetriever bool accountRetriever cosmosclient.AccountRetriever @@ -67,15 +67,15 @@ type constructOpts struct { type Opt func(cfg *constructOpts) -// WithTendermintClient sets custom tendermint client -func WithTendermintClient(client cometbftrpc.Client) Opt { +// WithCometBFTClient sets custom CometBFT client +func WithCometBFTClient(client cometbftrpc.Client) Opt { return func(c *constructOpts) { - c.customTendermint = true - c.tendermintClient = client + c.customCometBFT = true + c.cometBFTClient = client } } -// WithCustomAccountRetriever sets custom tendermint client +// WithCustomAccountRetriever sets custom CometBFT client func WithCustomAccountRetriever(ac cosmosclient.AccountRetriever) Opt { return func(c *constructOpts) { c.customAccountRetriever = true @@ -108,7 +108,7 @@ func NewClient( ChainHost: cosmosREST(chainIP), SignerName: signerName, SignerPasswd: "password", - ChainRPC: tendermintRPC(chainIP), + ChainRPC: CometBFTRPC(chainIP), } encodingCfg := app.MakeEncodingConfig() @@ -130,11 +130,11 @@ func NewClient( return nil, errors.Wrap(err, "unable to build cosmos client context") } - cometBFTClientIface := constructOptions.tendermintClient + cometBFTClientIface := constructOptions.cometBFTClient // create a cometbft client if one was not provided in the constructOptions - if !constructOptions.customTendermint { - cometBFTURL := "http://" + tendermintRPC(chainIP) + if !constructOptions.customCometBFT { + cometBFTURL := "http://" + CometBFTRPC(chainIP) cometBFTClient, err := cometbfthttp.New(cometBFTURL, "/websocket") if err != nil { return nil, errors.Wrapf(err, "new cometbft client (%s)", cometBFTURL) @@ -196,8 +196,8 @@ func buildCosmosClientContext( // note that in rare cases, this might give FALSE positive // (google "golang nil interface comparison") - client = opts.tendermintClient - if !opts.customTendermint { + client = opts.cometBFTClient + if !opts.customCometBFT { remote := config.ChainRPC if !strings.HasPrefix(config.ChainHost, "http") { remote = fmt.Sprintf("tcp://%s", remote) @@ -299,6 +299,6 @@ func cosmosGRPC(host string) string { return fmt.Sprintf("%s:9090", host) } -func tendermintRPC(host string) string { +func CometBFTRPC(host string) string { return fmt.Sprintf("%s:26657", host) } diff --git a/zetaclient/zetacore/client_fungible.go b/zetaclient/zetacore/client_fungible.go new file mode 100644 index 0000000000..33a06a44b3 --- /dev/null +++ b/zetaclient/zetacore/client_fungible.go @@ -0,0 +1,41 @@ +package zetacore + +import ( + "context" + + "cosmossdk.io/errors" + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/node/pkg/crypto" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// GetForeignCoinsFromAsset returns the foreign coin for a given asset for a given chain ID +func (c *Client) GetForeignCoinsFromAsset( + ctx context.Context, + chainID int64, + assetAddress ethcommon.Address, +) (fungibletypes.ForeignCoins, error) { + // convert asset to checksum address or empty string (for Gas asset) + assetString := assetAddress.Hex() + if crypto.IsEmptyAddress(assetAddress) { + assetString = "" + } + + request := &fungibletypes.QueryGetForeignCoinsFromAssetRequest{ + ChainId: chainID, + Asset: assetString, + } + + resp, err := c.Fungible.ForeignCoinsFromAsset(ctx, request) + if err != nil { + return fungibletypes.ForeignCoins{}, errors.Wrapf( + err, + "unable to get foreign coins for asset %s on chain %d", + assetString, + chainID, + ) + } + + return resp.ForeignCoins, nil +} diff --git a/zetaclient/zetacore/client_fungible_test.go b/zetaclient/zetacore/client_fungible_test.go new file mode 100644 index 0000000000..e8ac7288cb --- /dev/null +++ b/zetaclient/zetacore/client_fungible_test.go @@ -0,0 +1,79 @@ +package zetacore + +import ( + "context" + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/testutil/sample" + + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" +) + +func Test_GetForeignCoinsFromAsset(t *testing.T) { + erc20Asset := sample.EthAddress() + + tests := []struct { + name string + chainID int64 + assetAddress ethcommon.Address + errMsg string + }{ + { + name: "get ERC20 foreign coins from asset", + chainID: 1, + assetAddress: erc20Asset, + }, + { + name: "get Gas foreign coins from zero-address asset", + chainID: 1, + assetAddress: ethcommon.Address{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // ARRANGE + // construct foreign coin + assetString := tt.assetAddress.Hex() + if crypto.IsEmptyAddress(tt.assetAddress) { + assetString = "" + } + fCoins := sample.ForeignCoins(t, "0x123") + fCoins.Asset = assetString + fCoins.ForeignChainId = tt.chainID + + // mock RPC server + method := "/zetachain.zetacore.fungible.Query/ForeignCoinsFromAsset" + mockRequest := fungibletypes.QueryGetForeignCoinsFromAssetRequest{ + ChainId: fCoins.ForeignChainId, + Asset: fCoins.Asset, + } + mockResponse := &fungibletypes.QueryGetForeignCoinsFromAssetResponse{ + ForeignCoins: fCoins, + } + setupMockServer(t, fungibletypes.RegisterQueryServer, method, mockRequest, mockResponse) + client := setupZetacoreClient( + t, + withDefaultObserverKeys(), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), + ) + + // ACT + ctx := context.Background() + resp, err := client.GetForeignCoinsFromAsset(ctx, tt.chainID, tt.assetAddress) + + // ASSERT + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + require.Equal(t, fungibletypes.ForeignCoins{}, resp) + return + } + require.NoError(t, err) + require.Equal(t, fCoins, resp) + }) + } +} diff --git a/zetaclient/zetacore/client_test.go b/zetaclient/zetacore/client_test.go index 86258c5ce1..f389c80d18 100644 --- a/zetaclient/zetacore/client_test.go +++ b/zetaclient/zetacore/client_test.go @@ -113,8 +113,8 @@ func withDefaultObserverKeys() clientTestOpt { return withObserverKeys(keys.NewKeysWithKeybase(keyRing, address, testSigner, "")) } -func withTendermint(client cometbftrpc.Client) clientTestOpt { - return func(cfg *clientTestConfig) { cfg.opts = append(cfg.opts, WithTendermintClient(client)) } +func withCometBFT(client cometbftrpc.Client) clientTestOpt { + return func(cfg *clientTestConfig) { cfg.opts = append(cfg.opts, WithCometBFTClient(client)) } } func withAccountRetriever(t *testing.T, accNum uint64, accSeq uint64) clientTestOpt { @@ -182,7 +182,7 @@ func TestZetacore_GetZetaHotKeyBalance(t *testing.T) { client := setupZetacoreClient( t, withDefaultObserverKeys(), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), ) // should be able to get balance of signer @@ -228,7 +228,7 @@ func TestZetacore_GetAllOutboundTrackerByChain(t *testing.T) { client := setupZetacoreClient( t, withDefaultObserverKeys(), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), ) resp, err := client.GetAllOutboundTrackerByChain(ctx, chain.ChainId, interfaces.Ascending) @@ -246,7 +246,7 @@ func TestZetacore_SubscribeNewBlocks(t *testing.T) { client := setupZetacoreClient( t, withDefaultObserverKeys(), - withTendermint(cometBFTClient), + withCometBFT(cometBFTClient), ) newBlockChan, err := client.NewBlockSubscriber(ctx) diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index 318264e3e1..188ffa3dce 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -99,7 +99,7 @@ func TestZetacore_PostGasPrice(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), withAccountRetriever(t, 100, 100), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), ) t.Run("post gas price success", func(t *testing.T) { @@ -146,7 +146,7 @@ func TestZetacore_AddOutboundTracker(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), withAccountRetriever(t, 100, 100), - withTendermint(tendermintMock), + withCometBFT(tendermintMock), ) t.Run("add tx hash success", func(t *testing.T) { @@ -173,7 +173,7 @@ func TestZetacore_SetTSS(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), withAccountRetriever(t, 100, 100), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), ) t.Run("set tss success", func(t *testing.T) { @@ -197,7 +197,7 @@ func TestZetacore_PostBlameData(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), withAccountRetriever(t, 100, 100), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), ) t.Run("post blame data success", func(t *testing.T) { @@ -234,7 +234,7 @@ func TestZetacore_PostVoteInbound(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), withAccountRetriever(t, 100, 100), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), ) t.Run("post inbound vote already voted", func(t *testing.T) { @@ -275,7 +275,7 @@ func TestZetacore_MonitorVoteInboundResult(t *testing.T) { address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) client := setupZetacoreClient(t, withObserverKeys(keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "")), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), ) t.Run("monitor inbound vote", func(t *testing.T) { @@ -312,7 +312,7 @@ func TestZetacore_PostVoteOutbound(t *testing.T) { client := setupZetacoreClient(t, withDefaultObserverKeys(), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0).SetBroadcastTxHash(sampleHash)), withAccountRetriever(t, accountNum, accountSeq), ) @@ -345,7 +345,7 @@ func TestZetacore_MonitorVoteOutboundResult(t *testing.T) { address := sdktypes.AccAddress(mocks.TestKeyringPair.PubKey().Address().Bytes()) client := setupZetacoreClient(t, withObserverKeys(keys.NewKeysWithKeybase(mocks.NewKeyring(), address, testSigner, "")), - withTendermint(mocks.NewSDKClientWithErr(t, nil, 0)), + withCometBFT(mocks.NewSDKClientWithErr(t, nil, 0)), ) t.Run("monitor outbound vote", func(t *testing.T) {