From 9781259f3d2391106405d21314ef9a064e29a138 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:24:08 +0300 Subject: [PATCH 01/22] Attempt to fix scheduler panics (#3568) --- pkg/scheduler/scheduler_test.go | 50 ++++++++++++------------ zetaclient/testutils/testlog/log.go | 19 +++++++++ zetaclient/testutils/testlog/log_test.go | 27 +++++++++++++ 3 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 zetaclient/testutils/testlog/log_test.go diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index ac791953a9..1347c572e7 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -1,18 +1,16 @@ package scheduler import ( - "bytes" "context" "fmt" - "io" "sync/atomic" "testing" "time" cometbft "github.com/cometbft/cometbft/types" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" ) func TestScheduler(t *testing.T) { @@ -42,8 +40,8 @@ func TestScheduler(t *testing.T) { assert.Equal(t, int32(1), counter) // Check logs - assert.Contains(t, ts.logBuffer.String(), "Stopped scheduler task") - assert.Contains(t, ts.logBuffer.String(), `"task.group":"default"`) + assert.Contains(t, ts.logger.String(), "Stopped scheduler task") + assert.Contains(t, ts.logger.String(), `"task.group":"default"`) }) t.Run("More opts", func(t *testing.T) { @@ -81,8 +79,8 @@ func TestScheduler(t *testing.T) { assert.Equal(t, int32(4), counter) // Also check that log fields are present - assert.Contains(t, ts.logBuffer.String(), `"task.name":"counter-inc","task.group":"my-custom-group"`) - assert.Contains(t, ts.logBuffer.String(), `"blockchain":"doge","validators":["alice","bob"]`) + assert.Contains(t, ts.logger.String(), `"task.name":"counter-inc","task.group":"my-custom-group"`) + assert.Contains(t, ts.logger.String(), `"blockchain":"doge","validators":["alice","bob"]`) }) t.Run("Task can stop itself", func(t *testing.T) { @@ -175,12 +173,12 @@ func TestScheduler(t *testing.T) { // ASSERT assert.Equal(t, int32(6), counter) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.001,"ticker.new_interval":0.05`) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.05,"ticker.new_interval":0.1`) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.1,"ticker.new_interval":0.15`) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.15,"ticker.new_interval":0.2`) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.2,"ticker.new_interval":0.25`) - assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":0.25,"ticker.new_interval":0.3`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.001,"ticker.new_interval":0.05`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.05,"ticker.new_interval":0.1`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.1,"ticker.new_interval":0.15`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.15,"ticker.new_interval":0.2`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.2,"ticker.new_interval":0.25`) + assert.Contains(t, ts.logger.String(), `"ticker.old_interval":0.25,"ticker.new_interval":0.3`) }) t.Run("Multiple tasks in different groups", func(t *testing.T) { @@ -229,11 +227,11 @@ func TestScheduler(t *testing.T) { } // Make sure Alice.A and Alice.B are stopped - assert.Regexp(t, shutdownLogPattern("alice", "a"), ts.logBuffer.String()) - assert.Regexp(t, shutdownLogPattern("alice", "b"), ts.logBuffer.String()) + assert.Regexp(t, shutdownLogPattern("alice", "a"), ts.logger.String()) + assert.Regexp(t, shutdownLogPattern("alice", "b"), ts.logger.String()) // But Bob.C is still running - assert.NotRegexp(t, shutdownLogPattern("bob", "c"), ts.logBuffer.String()) + assert.NotRegexp(t, shutdownLogPattern("bob", "c"), ts.logger.String()) // ACT #2 time.Sleep(200 * time.Millisecond) @@ -241,7 +239,7 @@ func TestScheduler(t *testing.T) { // ASSERT #2 // Bob.C is not running - assert.Regexp(t, shutdownLogPattern("bob", "c"), ts.logBuffer.String()) + assert.Regexp(t, shutdownLogPattern("bob", "c"), ts.logger.String()) }) t.Run("Block tick: tick is faster than the block", func(t *testing.T) { @@ -274,8 +272,8 @@ func TestScheduler(t *testing.T) { // ASSERT assert.Equal(t, int64(21), counter) - assert.Contains(t, ts.logBuffer.String(), "Stopped scheduler task") - assert.Contains(t, ts.logBuffer.String(), `"task.type":"block_ticker"`) + assert.Contains(t, ts.logger.String(), "Stopped scheduler task") + assert.Contains(t, ts.logger.String(), `"task.type":"block_ticker"`) }) t.Run("Block tick: tick is slower than the block", func(t *testing.T) { @@ -349,7 +347,7 @@ func TestScheduler(t *testing.T) { // ASSERT // zero indicates that Stop() waits for current iteration to finish (graceful shutdown) assert.Equal(t, int64(0), counter) - assert.Contains(t, ts.logBuffer.String(), "Block channel closed") + assert.Contains(t, ts.logger.String(), "Block channel closed") }) } @@ -357,19 +355,19 @@ type testSuite struct { ctx context.Context scheduler *Scheduler - logger zerolog.Logger - logBuffer *bytes.Buffer + logger *testlog.Log } func newTestSuite(t *testing.T) *testSuite { - logBuffer := &bytes.Buffer{} - logger := zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), logBuffer)) + logger := testlog.New(t) + + scheduler := New(logger.Logger) + t.Cleanup(scheduler.Stop) return &testSuite{ ctx: context.Background(), - scheduler: New(logger), + scheduler: scheduler, logger: logger, - logBuffer: logBuffer, } } diff --git a/zetaclient/testutils/testlog/log.go b/zetaclient/testutils/testlog/log.go index b3d9555a90..e1b224aa60 100644 --- a/zetaclient/testutils/testlog/log.go +++ b/zetaclient/testutils/testlog/log.go @@ -2,7 +2,9 @@ package testlog import ( "bytes" + "fmt" "io" + "strings" "sync" "testing" @@ -39,6 +41,9 @@ func (b *concurrentBytesBuffer) Write(p []byte) (n int, err error) { b.mu.Lock() defer b.mu.Unlock() + const silencePanicSubstring = "Log in goroutine" + defer func() { silencePanic(recover(), silencePanicSubstring) }() + return b.buf.Write(p) } @@ -48,3 +53,17 @@ func (b *concurrentBytesBuffer) string() string { return b.buf.String() } + +func silencePanic(r any, substr string) { + // noop + if r == nil { + return + } + + panicStr := fmt.Sprintf("%v", r) + if strings.Contains(panicStr, substr) { + return + } + + panic(panicStr) +} diff --git a/zetaclient/testutils/testlog/log_test.go b/zetaclient/testutils/testlog/log_test.go new file mode 100644 index 0000000000..ed0b8d9148 --- /dev/null +++ b/zetaclient/testutils/testlog/log_test.go @@ -0,0 +1,27 @@ +package testlog + +import ( + "testing" + "time" +) + +func Test(t *testing.T) { + t.Run("PanicHandling", func(t *testing.T) { + // ARRANGE + tl := New(t) + + // ACT + // Log indefinitely, even after parent test is done + go func() { + for { + tl.Info().Msg("hello from goroutine") + time.Sleep(10 * time.Millisecond) + } + }() + + time.Sleep(500 * time.Millisecond) + }) + + // Let parent test run a bit longer than PanicHandling + time.Sleep(time.Second) +} From ca4094514a4654fe4778d412aa7d8927834d8646 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 21 Feb 2025 08:50:00 -0800 Subject: [PATCH 02/22] chore: add triage comment (#3566) --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +++- .github/ISSUE_TEMPLATE/feature_request.md | 2 ++ .github/ISSUE_TEMPLATE/syncing.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e80b399c61..35524dc29f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,8 @@ assignees: '' --- + + **Describe the Bug** A clear and concise description of what the bug is. @@ -21,4 +23,4 @@ If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS -- Version used +- Version used \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6010eb826e..ea0804fb7c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,8 @@ assignees: '' --- + + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. diff --git a/.github/ISSUE_TEMPLATE/syncing.yaml b/.github/ISSUE_TEMPLATE/syncing.yaml index 6519c730f9..0cc504b910 100644 --- a/.github/ISSUE_TEMPLATE/syncing.yaml +++ b/.github/ISSUE_TEMPLATE/syncing.yaml @@ -1,6 +1,6 @@ name: Having difficulties setting up or syncing a node description: Please fill out the following form to help us understand and resolve your issue with setting up or syncing a node. Provide as much detail as possible to ensure a quick and accurate resolution. -labels: ["infra"] +labels: ["infra", "triage"] body: - type: dropdown attributes: From 87707fdfba956c5ac583893eb26269ea13ba73d1 Mon Sep 17 00:00:00 2001 From: skosito Date: Mon, 24 Feb 2025 15:39:49 +0000 Subject: [PATCH 03/22] bump to latest ethermint (#3571) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c4233dcc4a..95d3bdb583 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/zeta-chain/ethermint v0.0.0-20250210141109-c8cb0fa0d95d + github.com/zeta-chain/ethermint v0.0.0-20250211180824-ea52413a15f3 github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250124151021-87b63e845f1c gitlab.com/thorchain/tss/go-tss v1.6.5 go.nhat.io/grpcmock v0.25.0 diff --git a/go.sum b/go.sum index 0bd54ea89a..2b4de83519 100644 --- a/go.sum +++ b/go.sum @@ -1394,8 +1394,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeta-chain/ethermint v0.0.0-20250210141109-c8cb0fa0d95d h1:6ZVsQ41ziPjce2rOMczlwWKvhBsohjI5ChX+O/0m/YY= -github.com/zeta-chain/ethermint v0.0.0-20250210141109-c8cb0fa0d95d/go.mod h1:Pykye2Mw2eQ8/l80MTr6yNaBm6OI3TkQCmwQGUrL5VM= +github.com/zeta-chain/ethermint v0.0.0-20250211180824-ea52413a15f3 h1:Bco3vCp5owYPZrg3GcnjaTNX51U70z57TaQJBdddpvY= +github.com/zeta-chain/ethermint v0.0.0-20250211180824-ea52413a15f3/go.mod h1:Pykye2Mw2eQ8/l80MTr6yNaBm6OI3TkQCmwQGUrL5VM= github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc h1:FVOttT/f7QCZMkOLssLTY1cbX8pT+HS/kg81zgUAmYE= github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc/go.mod h1:MgO2/CmxFnj6W7v/5hrz3ypco3kHkb8856pRnFkY4xQ= github.com/zeta-chain/go-libp2p v0.0.0-20240710192637-567fbaacc2b4 h1:FmO3HfVdZ7LzxBUfg6sVzV7ilKElQU2DZm8PxJ7KcYI= From 0716d7a44079b90876394afc1f628de88caf0ba9 Mon Sep 17 00:00:00 2001 From: skosito Date: Mon, 24 Feb 2025 16:13:50 +0000 Subject: [PATCH 04/22] feat: sol/spl withdraw and call revert (#3527) * sol withdraw and call and e2e test * fmt * fix solana e2e tests * fix msg hash unit tests * cleanup * bump gateway.so * cleanup unused function * cleanup * PR comments * linter * spl withdraw and call * bump gateway and changelog * bump gateway to fix e2e tests * sol withdraw and call revert * revert for spl withdraw and call * generate * revert gas limit changes * bump gateway * changelog * cleanup * PR comments * use callOnRevert in e2e test * use inst discriminator instead of artificial failed func * PR comments * add comment for message hash --- changelog.md | 5 +- cmd/zetae2e/local/local.go | 2 + contrib/localnet/solana/connected.so | Bin 215704 -> 216520 bytes contrib/localnet/solana/connected_spl.so | Bin 238896 -> 239712 bytes e2e/e2etests/e2etests.go | 44 ++++-- e2e/e2etests/test_solana_withdraw_and_call.go | 11 +- ...lana_withdraw_and_call_revert_with_call.go | 87 ++++++++++++ e2e/e2etests/test_spl_withdraw_and_call.go | 11 +- .../test_spl_withdraw_and_call_revert.go | 101 ++++++++++++++ e2e/runner/solana.go | 6 +- go.mod | 2 +- go.sum | 4 +- pkg/contracts/solana/gateway.go | 3 + pkg/contracts/solana/gateway_message.go | 94 +++++++++++++ pkg/contracts/solana/instruction.go | 95 +++++++++++++ zetaclient/chains/solana/observer/outbound.go | 17 ++- zetaclient/chains/solana/signer/execute.go | 41 +----- .../chains/solana/signer/execute_spl.go | 43 +----- .../chains/solana/signer/increment_nonce.go | 74 ++++++++++ zetaclient/chains/solana/signer/signer.go | 126 +++++++++++++++--- zetaclient/chains/solana/signer/whitelist.go | 41 +----- zetaclient/chains/solana/signer/withdraw.go | 41 +----- .../chains/solana/signer/withdraw_spl.go | 43 +----- 23 files changed, 662 insertions(+), 229 deletions(-) create mode 100644 e2e/e2etests/test_solana_withdraw_and_call_revert_with_call.go create mode 100644 e2e/e2etests/test_spl_withdraw_and_call_revert.go create mode 100644 zetaclient/chains/solana/signer/increment_nonce.go diff --git a/changelog.md b/changelog.md index 94949ecaca..1aa731b7c2 100644 --- a/changelog.md +++ b/changelog.md @@ -10,12 +10,13 @@ * [3455](https://github.com/zeta-chain/node/pull/3455) - add `track-cctx` command to zetatools * [3506](https://github.com/zeta-chain/node/pull/3506) - define `ConfirmationMode` enum and add it to `InboundParams`, `OutboundParams`, `MsgVoteInbound` and `MsgVoteOutbound` * [3469](https://github.com/zeta-chain/node/pull/3469) - add `MsgRemoveInboundTracker` to remove inbound trackers. This message can be triggered by the emergency policy. -* [3450](https://github.com/zeta-chain/node/pull/3450) - SOL withdraw and call integration +* [3450](https://github.com/zeta-chain/node/pull/3450) - integrate SOL withdraw and call * [3538](https://github.com/zeta-chain/node/pull/3538) - implement `MsgUpdateOperationalChainParams` for updating operational-related chain params with operational policy * [3534] (https://github.com/zeta-chain/node/pull/3534) - Add Sui deposit & depositAndCall * [3541](https://github.com/zeta-chain/node/pull/3541) - implement `MsgUpdateZRC20Name` to update the name or symbol of a ZRC20 token -* [3520](https://github.com/zeta-chain/node/pull/3520) - SPL withdraw and call integration * [3439](https://github.com/zeta-chain/node/pull/3439) - use protocol contracts V2 with TON deposits +* [3520](https://github.com/zeta-chain/node/pull/3520) - integrate SPL withdraw and call +* [3527](https://github.com/zeta-chain/node/pull/3527) - integrate SOL/SPL withdraw and call revert * [3522](https://github.com/zeta-chain/node/pull/3522) - add `MsgDisableFastConfirmation` to disable fast confirmation. This message can be triggered by the emergency policy. ### Refactor diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 3bf248c6ad..5d63b785fa 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -405,6 +405,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSolanaDepositName, e2etests.TestSolanaWithdrawName, e2etests.TestSolanaWithdrawAndCallName, + e2etests.TestSolanaWithdrawAndCallRevertWithCallName, e2etests.TestSolanaDepositAndCallName, e2etests.TestSolanaDepositAndCallRevertName, e2etests.TestSolanaDepositAndCallRevertWithDustName, @@ -416,6 +417,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSPLDepositAndCallName, e2etests.TestSPLWithdrawName, e2etests.TestSPLWithdrawAndCallName, + e2etests.TestSPLWithdrawAndCallRevertName, e2etests.TestSPLWithdrawAndCreateReceiverAtaName, e2etests.TestSolanaWhitelistSPLName, } diff --git a/contrib/localnet/solana/connected.so b/contrib/localnet/solana/connected.so index 5363b358ad3222a65a1a39d4b14ded28868b64bc..a755b0d90e865a29ef0c3c25ae923671c68d3cc0 100755 GIT binary patch delta 29620 zcmcJY3s_af`ta8bpm@PHDBCS6Y*2x`z_t?5bW0|3;dq-QPka$FQ^Ym7bC`LHa8H(~kWS z3XsE9{D*LNiA&cKR7j3>9f6J$_ik%Y3Hh>HAlgSxcUy_wSkMj_59!Zt1|R850-mYF zLwm}16W*WQ4WgvT+lDXqAm?3!P+a~uK9dmMVIt9OC*tS&vf9Esdyw>Yxj5fM%SFT{Mu*r2pflzR(&F#@;W=1 zL=V#AIOtP-^tf+0ZSp|@_`MM{pa+WZnHC{QP7+k0Qh#4ScW{!}=&;vk#c0`Y;7q~q zuHff2Mv^wjsU&H!tNjPo9!aFA?+n)0eEUsAF=R=w1B zBe32zL8zOz@;3#vZuM-vGQ3%VY|vx+fHc6}`d@G9KCGCvdxZ=MdOE@{WXYdsPm!WO(f&jp|B3c684~<- z+=FDvpJ?}xqCe5@B#-|@`vn;i@^swK$dW(Nt|vu*qFqfM|A}@v88YzcxbKrCf1+JL ziU#_5i)}G`m?V`Z~1A1Y#1FQVMDf~!u;DqW+3!&{)C~y z4%l-rNgoEoVz4d#Gt>c*JugS0T9P)bv*RBxN)j#e#=Xdx;emMZUXnCi?-nZbo6f`Y zw+)X#KHq^e>qk%EGQjAG61vd(5Zd%1@*t-0!RW{Q+2Jb@+DIOZSn1X%IC_VXbV~sm zlOHzH3x@O_8Zi**-E=~>L1vOZN{_ehCEG>?cznONr87H8_MZ(PYHKUsj-U5Rh>L2onS!>xqXI5c61He4&8Ef}2=riInZVv9$og|GjqZOoRTnUONOU9R=iTOib9gA>ZUy}Z}Ec6ZOHz63i`jXfQ zP3R7pJ~0^GB%3BqLbJ$&iC(b8xKHxJh~^J?Z7f2w^FMrj3UU?OvWh%-BN+$wA`2&* zam;m6FnJrSxNp9>68(^W_04Axe$Pf4qQmj_{lq*a7wsaqrPV>I^*C zLdvIRFu&NhWWU_EVqv{=f7`}%&Z`Za$xazH(>_)IsaTjXM}^pNyFR*eAz~7=N)B> zLcx4b^lx%{{vs?Dl86PKh>+O}wxjQe`@6~L1ljQJ3}$G0cOsfiCM=u*%Wu)b?KrGU ze(a)3C$yamSuzE7Et{6iL5qn?(k}EmDN5Rm!wn=lxg7n2^h;TT&2FSPr2sn^$ik(b zYM*w^6FO2PDOwtc3!z|3_1-laBis6j&IzQ~GC$lFO1#XBZjiKPouOb~E}PQ(KTY^z z0H0tI@PRLSl}z|xC@LcxKJY`gNzn&WFzQDap{7^Dk3rBY+4>@8_w@jNet5Hx?@ zu@@;%n+R(*^}K;R$?$ZOo0w#%7l~dE-4i?ix2ySCaxQ%hYE5RZ@xq_Nh+bpHz4WAf zO$KZ6CTnM7O-n`oS%YsOo2*lm-r-aB7ri5W08RfDX4}ZMj<}jtaW^t=?d$lDLQ=jq z91rP1^y|W(s-ml8_PU|iqX)@d7x9$w7I7hCaob@en)v$U2^H8;slXqV3LN1{wh>l= zq=`T%!FxG*bG^F7_xYWyuKFr@oDqoK_mKd)0tJ%2bP`%XTr!8UnI$4K0)MrSWM!t~zWee6 zHkvWoNoIdK2|dm~@~NLZ^VNPPyO=ixJvDJ|BOh*BiT)riS<~U+V1CwOw2j=(nu1^Q zCJ~#vA$Ri5=9fAT?gSgiE&fl~RKIwuQI<|pAM*X?ex$m=(TnL>uXTfmsl{6?ieISW zx0bZa_C{}ze%WU_>*Powiib&-6Yw0;bxRlQ;7x+IOvHiSBz;Q;xjRV@6kc}vZhT{C^yLnEZe zzdxC`0G_x|FPYUnOKT*NlUuuB{~A)ebp>wROcs6~f!mqL(a(K6ousz1Guv3q(>|!c zkO`1r9)pqC=|`FRcb_49$)-<$Y;5er=@?Ta}mJ^%3+ zYcR~~Y1;#FR1GQKo&k&Igq$h(_I*;26OMQ8W2+EM^6p%+qmM}UU6at8r-9zL2E z1ij{Q-Ut*%!gdFvZe+=BFDTWf-GR85nUwDiK<&xn-G11^Onkrf#qMSj@og^7-bb3g zjmG`mNpyaOPpL2%Yu)X0pA*y@t2-YLqTl2BtmyWr_Q@00lx&Wg?~(GDRW0M^$(wuR zA(^x%0;=xVo*&?$bHmjys{v-!aj;E)18w2qAVb{8v+-WGw5lIgagXcGAn z%;ROJ81evqeuH!`l%2{8!?9B*qTd&Uf4e~<_HA=@x%=cv$y%1Ny`(3J)BZr@LmqDcrH zBK=DIRPSHN?2_x~88V@CJ6cH|mj>c`m=q4_(H0VRXe>+$8x95I-=T|uDvew{^aH+s zhva@|R(r=x;`Dtm>lWtkJ<&<>=J$2DyMaU;ma`5h>P*rOm#}I%GDXeNHUG$wIS7@K z0Y5aC;@{-mA2#DJI@7F9$d?Q*^Y^Y2+As#jEo+SZ?d&zPo_ts)A5WYwi*OU2k9UTZ z%Trmk{&M<#CxA`AE6^x)>NWpphSPv0Kl)*3w!71#dHF>@LZv^cB~AYfhlU9I$@bK| zmqlET`C+MZe$cU4#FmAu@@O=SJT6z4fxkTI$tM&3H7VfX%_mQsq_5?PiET0xHEX8a zc{MY9i63oy8J57+=Jp+DK({qQw}CC5EGOWO4cq9YH0+RpKuT ziA^`H?LW%)eg+@%PDPh46MEb0nLX>2ux&V8t)V#gh*QN2lOX85|5Z@?#K;%@Fl8*bbscqZw zp!QY$*`bu(i-evqxxM#EirmugeUfOGACXT^c(xk>O_I=AlHUGDHpxHnELuy>o#@j+ z8`r9h>$4l;x=Ux}5G9|>g^*jzS$R^mpEdsFNNCfK=6YdxfUWL^GRT0Fi(tb3=Hvyu zY(GgqrH9q)%Ts}P)qaw6Nsk|1CAFuH;&uCp=Q%z8_tpIJ(@9A6%*mgA2A)Hdbj$KL zRE>2|$(=m@_jWbP<13{2muNLg?UnqXa~W9m%p_MY%)s|Bdl1HIuw~;|$Kbo*#7cJW?Y*L;BsCgKu6YS+~;BT+;9MP&6}t z_U)2phn*y>Zi?!?DZi+$HCE|0a_-J%H98df?i!RydfiKgC#)~;1;g>dxqHFtR15js zf2fxA_C;cT5TvBUmoJj^2l1-y#6_ZSXj1QSSAM^T!Ob`IAz9McoMAi}@W_TsZj#zZ z3(+z%{kQ8ZQSOn>Wcu%FLnANp&F^#6U>))Y`~k-d;05y{uCC7C_PC|OcIS70vdaM% z7SeKz^eBX0#pptp-TPq}D3=X}G9xzdyU=qEXc0cFc(LrIRfs*u)pLJO6xJ;MTbI83#VA-AVD^R$B=Wi)Tx=}|q3#rgYbKxd@K zJI|5gAAM|bozdtvIO-ZLFv8vBU8gI&Q7j6hx4qG3)SssLpeeYb##ZZtJQ4Q4K|AzB zX4H|6>4}D-b~LRg)YBhzw7eIJ#`SeHwio1ZS>ZEvG{6^aL*LQUzGwv6N4xu>Ip}Me z<_C#F56EX@wr>8Y+5tJx$9>RP)QOJiivsbzTAI`s72scLZ9)A|mIKZ`AfFv29<-kw z-H^|YdLER|j?NyG&yH>!kk5|N4#;Om(+|pLN6#0@XGa?k$Y)15?*^@X4*BBE2jtVD zSqJ3PqNNAq)1u!F$frf^56Y)SUmcWBi~e~~J}o+YKt3(1JRqMIEk7uq7G*;Ia6UBT zpnO_14iW(U1_?kIi)_9Fkgp?-JRqN-;ffnHb|6ak=u)ida`2;iAY21AI|x4aj)Jf!$kE7?E+38(JPeQ7LS(<*^Qe+H z`)pZ*SRKObvCLsmFp^or+bz$?%#edy^|ns3XOb{ma!%DqM76ggo$^KBZ9f*dL~V_Fh{ zdeO;azzc37ehfN_tCjGhBkbYRBkbXeBcPg~kPWcYW`!;uYcF)GNR;J~3cDz_5n$JQ z9)-;{2DeZgiOSJCG;JK3f~L^hZN&#={UANjHs0+tCX&>{XyXH0@Os zjC#@|uOcr;BgCcFNoX+b`8QOGuFKwMX1i=ByJt;;U4g}(!#aqI7F+h(Cw z7%wih<;KG_g$IH&JZB=jtJIb>m-#&Zo$dTQbPeI}OKe9MAiV>Auf%qK5wsWHbBOBy z0n^x(L$-zgK$UPdUrHmAKry0}<|d(9Jmou@l?>AHkS#q01{#h&WQ$maD%rdUd+Ha- z+4tMgS<8_L7r{nkz1}VAM$2GlTgAj1^yqSwj;k)qLv#DA4-ul1KSmwsKUSb&IJ=e> zuR!7SC#G>*i3Z_{IvTMOndz)`p!sAa^k@^ztxVINfW{*gG%@uwA{7PETWdhGE*160 z)ewhiR$D=XSAj-XPXktgW>y+#<}po59nAs_j;y7XOy9qO!p3^Kjro|M-k8tm^{9QuI@FE!WtH4} z9k^KQX!tsC$%blSGNzV-2NVG+g~`%-dX32nXg~rXCbI$zBakw$mPRwpPNrGMG}U!9 z7c@BG7OiBmqLwx>nSGo3t_LYWrL2c3EcAdaX+0Y)d3Wgf4M^`X_;^c4es7SWp3PnC z3DA0ncF2GU+W$C>&VUKL|8cr71FgWP3TYG5mle_o3i|qbTP$TGCh;OoYJ)x5aHkIN=VB&4R++sGu2H zXd?DGZeP@OcWA54Q24_YG+;9d_xP-$rL62l4I?)ABJa@j&9F>59H+U=zx{DqxfycY zT|pz@#S)Z}nzNA^pQ)fp*}$edG$R}ACa}XhFkErR)|8E+Tj82JGGQVuZ&17w^0S@$7I`7$V0)a8;3Xe~h~JMo!<-mlgA%9_6YtU$HYkbp zF3nZ+@iy7*n%1rKF7+(5Co8v`Cn_kHtNt!cSJ+$#sqfLFTC?#UZBjVbZVuX~DMREv z8n3YDJ~{O^tvTf$J+E-O-K^iQNu76(Mrhdtccf*@U0d;f*6#Oyu_YZuzD~{e4z*c) z2wii>b1T>j7q_x|Ev?2{tLC1q{0wSx#5byJ<-eeCIF+%vpNCZo<IZPNub z#Q_bWk1wHz$kF!TGJ4{OrTes@2I|yulD$3f#k%{p@EdHtS1aBBJ`MjBd3r=@y9wm& zt#s+o`?jQCQ5KgV==T4ZfX3fO6Ja?`yN|qVS8k&i#I|wab)Z~h+f;{a2u0Z@+(Ch| ze#Ko_nTFFNcVS1@je@x|9eocrL6F3JANt#!)3)^c$XE6)cmRs;PSdP9*u->eK&9Yi zJKumj9q}h8Y140Djk`}HeuwQv^+uZUJFJVFPSFm3KsWyBG>!NJrrb8C=n5wHoS?-_ zPB~2*nEdbr4R{R0N%skjehlfO?$d3L;Rzu6Bwg496?ft|&0@0k33`;t5vOP^lZ#GK z{S%PKPf#it! z;l$o;xI-(v^CYeHh8^XpRyYM`LLhk9pbJ{#P@H+67PQ7@lx2I+8pBKL*>p)8?1g69 zHnqX6k)!1#%pdjMxTo!>w%Fu^KA?}E!8V-mfEMT=ey>V;UWfHq_kcF&a5(m=q-Iy( zgN1aWD~`o`3Td$`4wCJzI0AcA(r`Cmu*=gHq|0!_p15NrEnqV80j+ey@yM0BSaD}M zxg$2>o$3^KpJsH#W^LNqju5K}>XEhH;7&Nzqhn=DFK2TToTbQflj#9H-w6uv@Hh={ z$FUxznp1XnR03Y$2T8vurn&Br$+Kr@tvdu?e}?+%aRC0Tl1|hEUpqt78NX9WiC4GC&1% zKSPU|tl3A;8=xZBR?-H>6=$fv3&?+;p+QW(2IXPWa)vHs^2bV=!DMf!NG4ybq?I5& z$~5=sBt74JpD&!D9X!Bq>lqrrWbPRn!Q{3|8qZ{YB~54Y9+aQSrZcph$@eR1Et9F0 zv_n^re~0u;E`;<uh*tg=rs6%TCb-uNg&20Eb!rYu}-4;+u@Rnh34 zSnpQzprzT_c6@# zE$@vd;?M?K-UoZq;eD_PXDiCc1{)~PJGdRzJX^BW`EW5U4#av7t^S!06 zpR$K4b~qM2Tx|0+;d1={BXIQdcpJh|k7%m_I2T7|(}DqTe=!ZTLl9Kl2*yJK z+;0$$W{++G!O*|KFE$t_;W((Dm$BaVM=&-Zm1y6AIL;%xp{1_h^Qe~39oYjZRBi4+ zyby!*9RwlZ0X2ROhE3%l9D++5Xw!>OuRUMHk=U3`7rqFU9+?gAPd&-bZ`;#~7ja)) z-CzT!6^{6qLfeW^9D(2mTKN(#_DDM0k|%o;$1XNpm!74$!{GMbmy4T$KR8PpUdGT$ z4{5=0D8u__**9@?bQq4oE5J7lnts(;+Tks@)zq`JI2>o;k0I>{7-|q};Rw79uRcpX zEw}(;u+q_43Ala6NW4Hv-<7T%iT{azg^o80>Mb3DjDn!7AYr&G-TewCc+FYbG!0G%K3ZH&B!uim`X?Qq(HFf<#ItysR<1s9Z_NI%*)`zrw4m7FCc@OQacsKz^H7hF~!jm7w!#)0OyEY$( zx5f1jX+RQ0hGYwqU@{2VOf!;Tu<06^UQcf&!Qg7#Z1YTJZQSD*8lM97&xU3Sv>9~4 zR!gC4Lh6Z2p^A--v|uT8UC?(}hJzg72$MXS+?`Ha2K_3p(U!Fg77Se7XoHVVYY~L8 zHGRl#0{rFvH+m)3;nY4fdL?u@%N&}{Bm^&B$;LyYO`nR>u}5iDOAlkG|LU<(V|Ir z2JpLk!Ir)n=0Us@x}_B|Fg>)Ht*{_7MbcWkB6lqgmlfclhbL2PU~axd(Z;X0yJfA% zK`m~#HrU--Whh!RwY$YroYCUeZ6mIRXtZe~jGJ8Olb^uY0NL;fR13%zpF-P$to;-p zWtCg-8MG)~eu-9Q;S9Isms)O%y^{ewd>mAaoiD>)9}GpS`Tc0J7`y&(!^() ztH8=_$`03n;dNZ|^;obs0k>4NBnk18rN7FcrU*;M=J1mA7Xkk=Ui%U(f5Ku9nG9dj zfP!s=)vG74j|w1#68~ie;gDXhN&i3A2;~32&R{ZR;HAmn|D{G){!+?!Sb}%W&DRd# zp3MYb$N>jy*y(#L!3TZK*C1ec&Dwmu1I%W6^`gK-RRCED4DagMCFd`-JA_LLaaQ{$+xFI@s9~qF!mh+8Wyl+)Fmv^Ouf;Vvy<$vfUBY!LBp`YisCrU~Ry~ zz}i0ZslY|R+8VtGEEhn@Uuty}OZ_wjcpf-V!*2omX?PW|wg5Ro`qRMLCTjZw-y}i6 zFF`yte`ygYv~8FJ3?FqjUzdT0HWPd)REDJx4ZjXNOv9f6%O4gfx=X-Ho68#g|G|=3 zlfU#gP=sj$Yy^H*!`}f9(eM*s_PMEg4fv5S-~wRn4Sfl$y`gI0Q7!i`NxlEcce+=A zwQV*BSX-b>V0$OB7oiAP+efYo>D&Fp`x&Y5~@E+C{+HPPh|TJC9s0 z$B|qodG=pWC7J@g1w2^8S->MSd=*&RC#;_+%cYef*{xYopD8Gef`^N7W3z|kOb=Nc zI<;>}0~*(>xKrCtntkMD*(XFv6b1*>>MC~^a#mO2SC3QAI#`8Y1-tU8kn{%F)t|v0 z!R)bMS6A_wkcX3Uld;EXl4$P%b~QfOb4B}9u&XON*w2ghK44dO1Yqw_FXTUfx0^-# zi@ZHvw7<;Tvqbx7-d+y&uG;$_r?KaKYH!fCQQ4q{YY|lF9jx!20Trel-K&7ro4N$9 zM%i1Eetc11N9l#Qy+#jYv?l}_Hf zwhK{`flBV4F3yUg)Kg6smebpwEV7SXTEWDwsqloS_CUsAz-&LsE^lyAdt@Q=r~R+u zj&19eq+eE1^F7!~-hw3U?J4B{c07yaw;!r10N70hkfFfv>3H*{eaK`=kzWUYdoQpj z5xA9&teT6KR)gAxT%e zw=@%^x*KkU>gY<3*W->J@{fwk02sUKT{ps^(0!=6g+KE(lM3vk_%pG*nBSC(_8s7_ z-Ex)!tD}LXKL)Jk$@n_3rX`Z4N1)KIUH<8tl-iPI1S+(OVc(enDR;m@`Uj>W@f9)NM{>#CxHcA^PnYta_{0j|efC+Ci z*ww|G%>{jwyhAV1Xl4(+)Utz|3^p~dxJxu0>=@*>2HZPY`orD{)Y=RkYY(JO@am(~ zZ%>}gVHS3%!7lY))!(01D@*xuvw65h{cex1&R1LQ$<^}Eu@CXUE>SslZIxnVE$0U9 zQEJ=r5ZAR$wtQ|kz?rLjSq07$xI*B_FZlEcN`7j*LV@$X;{21PiXEInO8lDV?43L( z?&di!pXZ!CJl6=U#%Cq$r z&k40Wr{0k{xn-(ndtKC>-*|__-+9h? z%yYfKiA}t{<_XU+$i51y`T08mL;lSLFt*|y@&vXx^L7(kyfo=eZF!Dq$Fs4$z%I!C zjH{L?(T#T~6WG$3x0ed6V=uEb1qu~7O5ivHV)w79fi8T2RDpHu`9|YkEwDvB_>{e1 z=b#*+sy7&;9HFXgdOn#CnE5=>o6?EeDqA0x0;;8KC>lLdu|Jz;6?&?0bxz?lM< z3S1+wa}Xat*(4~U1Wpk+N8mDnYXsH>^BJ0WPL`s0g_I(24tu85)JUnok%M`AoWP+& zczZ~wmZASeaSHhR7hzflcoY#hLEu7xQ^Wc6r2X) zx|J<q>fqJCUQqj|^T}ca&ZKi8nYZ&A-YrDnIC==& zlFI~jy}$-5A0|}bIDxGK7Yba>Gwl4<@)?)}juJRU;2eP~1a1`AxK5iNi~|d&fZrVy zI8)$KfolZT5p4$WhtLF$(emG*o2P0OzHUlJ4#ZEt@T<%LJ|$*sy^Q z8!B*|z*d0^dB##Ty$Iz%CVGbzCnbXqpCfRUz#+=ndh-M>aGbz}0$1=1TMXqSUJaNd zaH+sK*?fUZ1+Ev^vW54L;TSqvDyNX73V~|`cHYVdGzpv{aHhajT4ob|y;cDn`Z=FL zl)!~w@pjz~p6dm+?&Iw_8fN)RWf}$JYJr{i^8x$?4iz{?;6#BlTUgFt%4<=u{G|$k zYXo*az!%6UaHzmh0w*@JJ%7pCtdR4U@&qmuxJF>+;KVvU zy;a~mo|C0AULj@Q;R96P=Q*>1XI&%DA&+>DQhskyt*0E0Uxm@kDc}L3i4Tw?aJ9gO zC%k_oz0(Dz>q6@70rgvs40rmz)Rl4k-dlLwI|Pz!d^ZX5K$a;Ob#^d&`@Z;e3L6 zft|y6d!fLQ;k;cpf@iD1&KB9;@@A#lu4rC<3?umfg#uRyoH&a2H@(7hg}{ZQnH}n1 z9X5tBe1dv`vmD0%d`JHp0np+d;eDpAkF6;5(TajICLTJ zpPIsRg~0U!o0cZ?2@;p`T(*qoYJp=u;O%(=>z4EOIDxa11x2I4aVz)$QK>wqtm3&) z;2MDqAMyUl34%gO;~fkFTLexN*qF|zj}bVL=VZysE2Ij6YXpv4$7hf!a24V0&g*%O z+@NLXe<_>-4goT>4)BLq1U6;z_85UPH}du}fnz?^*jfLN`b?t$Hf-WKQs8QVEm^$3 zZZprOEj*WLIEYO+RT>53%&mNan9q5(eolw=b~sN%`#9)at=D*CCVi=o{FpYik3%5* zL6fyW{dL!QCx3y}zsjvn1X1)NOPxwx`$DW7+MkIsI+ckUx|9iLx&_2I{~A7Wh`<(s zqXdoX zOhAd5FxYknI6UJB2aboH1B|2>nMk1Mc@S3W%|sr3iHQn2l?ix!0pY5v0f~OdTVr6X;T=whHQO zx`nAr1$6~I1nU0vg4+3xPy~VfDH;Ip5iEi_Qs6k+e*knPE1e47P`^u=sGwUw(CPsW zI;snTUTnI{XJ(<}f*b-}t%5p-?gn*6Fr;w4$EPsT{=tw>9Gx2s8D!EeAY9cy%Aoe^ zYI+>h86gfvSO5Eb`cOJ91QJ5}5QjkTOu?Eba3S5oyvyirCaUNmCTi$$CY*}hmvRbt+!MH3 z;Cg|jM*fa<0viQ3X_>7rp;`rSq`)x(CkUJ(uvOq}f%7!X@|Q|A3dR)zR}0)IuE#dXnSZ0e zG0GQsYWhOu3p|xed4}4l<^wtloTGf8rUr<__CryXqXbSd@b*UK+auLKQTYu|J461u zUVMVYK%NT)HudN27J>7g09>Q~ED9;H3TVCSrRRSkW;_Zev zS~!`tp>ax!f^pPaJclS>0jO12C2(OD?_VKsi1HpzO&{69$#SPlP+n`P4i(C4EtUP1 z*H|h$f6Zr@vWsUak7v`jJcl$h)IYo9n7zD1Y5~us%4;yS0F440mA6u=JxXA!z-214 z_GblCO&|I*Z;uf; zLE!51vVU^(L=ts@4`95+bK*6g3zgSPY5@wB*F`GV3!J084N~pZGDH2V3QIkof$=WS zArE+t61Y%c-9z3#Re7DH<`>!q=5+RZ396!sDKwno#&eFI=j_fr=LrnEm6u`v5vrWt zs@YfsP82v*;1naDO{Tz&-n?Ds!!guwPfj8E3!K@Dw^s|?=*!y;y?KrixU`?v4*ACg zXch1`6oYxr3E??JIeJuUtW4l+<)~4$XPWu+IpZ|UMtF>JY@{YA6IeG#lK}h+6L^lB z$8)H13Z%v}Ea2@`OIui;;X^)bQLyR8DsYN&$f5>JN#*@(1U4yW9;$zgz}6PF&+vKc z_<+XsJZCCL5Nbdjr{?6>HLc^^VMH}Fli!IFVI?;h*i$ei!{Fd&wI0U$g|G$BCpYsrP;Ur{{;#SAf5mK delta 29096 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 diff --git a/contrib/localnet/solana/connected_spl.so b/contrib/localnet/solana/connected_spl.so index 821a6e5aa9e6d06e4f948c8e658225a25f8b9b1c..7f89b6d221480e0254132c6a876bfe80dff22318 100755 GIT binary patch delta 31302 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 P*6-2$;$YKmOaK1^&+ybp delta 30587 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 diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index e9f45c0840..9211107e1f 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -59,19 +59,21 @@ const ( /* * Solana tests */ - TestSolanaDepositName = "solana_deposit" - TestSolanaWithdrawName = "solana_withdraw" - TestSolanaWithdrawAndCallName = "solana_withdraw_and_call" - TestSolanaDepositAndCallName = "solana_deposit_and_call" - TestSolanaDepositAndCallRevertName = "solana_deposit_and_call_revert" - TestSolanaDepositAndCallRevertWithDustName = "solana_deposit_and_call_revert_with_dust" - TestSolanaDepositRestrictedName = "solana_deposit_restricted" - TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" - TestSPLDepositName = "spl_deposit" - TestSPLDepositAndCallName = "spl_deposit_and_call" - TestSPLWithdrawName = "spl_withdraw" - TestSPLWithdrawAndCallName = "spl_withdraw_and_call" - TestSPLWithdrawAndCreateReceiverAtaName = "spl_withdraw_and_create_receiver_ata" + TestSolanaDepositName = "solana_deposit" + TestSolanaWithdrawName = "solana_withdraw" + TestSolanaWithdrawAndCallName = "solana_withdraw_and_call" + TestSolanaWithdrawAndCallRevertWithCallName = "solana_withdraw_and_call_revert_with_call" + TestSolanaDepositAndCallName = "solana_deposit_and_call" + TestSolanaDepositAndCallRevertName = "solana_deposit_and_call_revert" + TestSolanaDepositAndCallRevertWithDustName = "solana_deposit_and_call_revert_with_dust" + TestSolanaDepositRestrictedName = "solana_deposit_restricted" + TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" + TestSPLDepositName = "spl_deposit" + TestSPLDepositAndCallName = "spl_deposit_and_call" + TestSPLWithdrawName = "spl_withdraw" + TestSPLWithdrawAndCallName = "spl_withdraw_and_call" + TestSPLWithdrawAndCallRevertName = "spl_withdraw_and_call_revert" + TestSPLWithdrawAndCreateReceiverAtaName = "spl_withdraw_and_create_receiver_ata" /** * TON tests @@ -533,6 +535,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestSolanaWithdrawAndCall, ), + runner.NewE2ETest( + TestSolanaWithdrawAndCallRevertWithCallName, + "withdraw SOL from ZEVM and call solana program that reverts", + []runner.ArgDefinition{ + {Description: "amount in lamport", DefaultValue: "1000000"}, + }, + TestSolanaWithdrawAndCallRevertWithCall, + ), runner.NewE2ETest( TestSPLWithdrawAndCallName, "withdraw SPL from ZEVM and call solana program", @@ -541,6 +551,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestSPLWithdrawAndCall, ), + runner.NewE2ETest( + TestSPLWithdrawAndCallRevertName, + "withdraw SPL from ZEVM and call solana program that reverts", + []runner.ArgDefinition{ + {Description: "amount in lamport", DefaultValue: "1000000"}, + }, + TestSPLWithdrawAndCallRevert, + ), runner.NewE2ETest( TestSolanaDepositAndCallName, "deposit SOL into ZEVM and call a contract", diff --git a/e2e/e2etests/test_solana_withdraw_and_call.go b/e2e/e2etests/test_solana_withdraw_and_call.go index 19ebe0f369..fd4af07784 100644 --- a/e2e/e2etests/test_solana_withdraw_and_call.go +++ b/e2e/e2etests/test_solana_withdraw_and_call.go @@ -8,6 +8,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/near/borsh-go" "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" @@ -55,7 +56,15 @@ func TestSolanaWithdrawAndCall(r *runner.E2ERunner, args []string) { require.NoError(r, err) // withdraw and call - tx := r.WithdrawAndCallSOLZRC20(runner.ConnectedProgramID, withdrawAmount, approvedAmount, []byte("hello")) + tx := r.WithdrawAndCallSOLZRC20( + runner.ConnectedProgramID, + withdrawAmount, + approvedAmount, + []byte("hello"), + gatewayzevm.RevertOptions{ + OnRevertGasLimit: big.NewInt(0), + }, + ) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) diff --git a/e2e/e2etests/test_solana_withdraw_and_call_revert_with_call.go b/e2e/e2etests/test_solana_withdraw_and_call_revert_with_call.go new file mode 100644 index 0000000000..01133fa9eb --- /dev/null +++ b/e2e/e2etests/test_solana_withdraw_and_call_revert_with_call.go @@ -0,0 +1,87 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +// TestSolanaWithdrawAndCallRevertWithCall executes withdrawAndCall on zevm and calls connected program on solana +// execution is reverted in connected program on_call function and onRevert is called on ZEVM TestDapp contract +func TestSolanaWithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + withdrawAmount := utils.ParseBigInt(r, args[0]) + + // get ZRC20 SOL balance before withdraw + balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SOL before withdraw: %d", balanceBefore) + + require.Equal(r, 1, balanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal") + + // parse withdraw amount (in lamports), approve amount is 1 SOL + approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) + require.Equal( + r, + -1, + withdrawAmount.Cmp(approvedAmount), + "Withdrawal amount must be less than the approved amount: %v", + approvedAmount, + ) + + // use a random address to get the revert amount + revertAddress := r.TestDAppV2ZEVMAddr + balance, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, revertAddress) + require.NoError(r, err) + require.EqualValues(r, int64(0), balance.Int64()) + + payload := randomPayload(r) + r.AssertTestDAppEVMCalled(false, payload, withdrawAmount) + + // withdraw and call + tx := r.WithdrawAndCallSOLZRC20( + runner.ConnectedProgramID, + withdrawAmount, + approvedAmount, + []byte("revert"), + gatewayzevm.RevertOptions{ + CallOnRevert: true, + RevertAddress: revertAddress, + RevertMessage: []byte(payload), + OnRevertGasLimit: big.NewInt(0), + }, + ) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted) + + // get ZRC20 SOL balance after withdraw + balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SOL after withdraw: %d", balanceAfter) + + r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) + + // check expected sender was used + senderForMsg, err := r.TestDAppV2ZEVM.SenderWithMessage( + &bind.CallOpts{}, + []byte(payload), + ) + require.NoError(r, err) + require.Equal(r, r.ZEVMAuth.From, senderForMsg) + + // check the balance of revert address is equal to withdraw amount + balance, err = r.SOLZRC20.BalanceOf(&bind.CallOpts{}, revertAddress) + require.NoError(r, err) + + require.Equal(r, withdrawAmount.Int64(), balance.Int64()) +} diff --git a/e2e/e2etests/test_spl_withdraw_and_call.go b/e2e/e2etests/test_spl_withdraw_and_call.go index 8b2c2e6a83..c7eaecc95b 100644 --- a/e2e/e2etests/test_spl_withdraw_and_call.go +++ b/e2e/e2etests/test_spl_withdraw_and_call.go @@ -9,6 +9,7 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" @@ -65,7 +66,15 @@ func TestSPLWithdrawAndCall(r *runner.E2ERunner, args []string) { r.Logger.Info("connected pda balance of SPL before withdraw: %s", connectedPdaBalanceBefore.Value.Amount) // withdraw - tx := r.WithdrawAndCallSPLZRC20(runner.ConnectedSPLProgramID, withdrawAmount, approvedAmount, []byte("hello")) + tx := r.WithdrawAndCallSPLZRC20( + runner.ConnectedSPLProgramID, + withdrawAmount, + approvedAmount, + []byte("hello"), + gatewayzevm.RevertOptions{ + OnRevertGasLimit: big.NewInt(0), + }, + ) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) diff --git a/e2e/e2etests/test_spl_withdraw_and_call_revert.go b/e2e/e2etests/test_spl_withdraw_and_call_revert.go new file mode 100644 index 0000000000..46f41061d8 --- /dev/null +++ b/e2e/e2etests/test_spl_withdraw_and_call_revert.go @@ -0,0 +1,101 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + solanacontract "github.com/zeta-chain/node/pkg/contracts/solana" + "github.com/zeta-chain/node/testutil/sample" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +// TestSPLWithdrawAndCall executes withdrawAndCall on zevm and calls connected program on solana +// execution is reverted in connected program on_call function +func TestSPLWithdrawAndCallRevert(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + withdrawAmount := utils.ParseBigInt(r, args[0]) + + // get SPL ZRC20 balance before withdraw + zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL before withdraw: %d", zrc20BalanceBefore) + + require.Equal(r, 1, zrc20BalanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal") + + // parse withdraw amount (in lamports), approve amount is 1 SOL + approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) + require.Equal( + r, + -1, + withdrawAmount.Cmp(approvedAmount), + "Withdrawal amount must be less than the %v", + approvedAmount, + ) + + // load deployer private key + privkey := r.GetSolanaPrivKey() + + // get receiver ata balance before withdraw + receiverAta := r.ResolveSolanaATA(privkey, privkey.PublicKey(), r.SPLAddr) + receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount) + + connected := solana.MustPublicKeyFromBase58(runner.ConnectedSPLProgramID.String()) + connectedPda, err := solanacontract.ComputeConnectedSPLPdaAddress(connected) + require.NoError(r, err) + + connectedPdaAta := r.ResolveSolanaATA(privkey, connectedPda, r.SPLAddr) + connectedPdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance( + r.Ctx, + connectedPdaAta, + rpc.CommitmentConfirmed, + ) + require.NoError(r, err) + r.Logger.Info("connected pda balance of SPL before withdraw: %s", connectedPdaBalanceBefore.Value.Amount) + + // use a random address to get the revert amount + revertAddress := sample.EthAddress() + balance, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, revertAddress) + require.NoError(r, err) + require.EqualValues(r, int64(0), balance.Int64()) + + // withdraw + tx := r.WithdrawAndCallSPLZRC20( + runner.ConnectedSPLProgramID, + withdrawAmount, + approvedAmount, + []byte("revert"), + gatewayzevm.RevertOptions{ + RevertAddress: revertAddress, + OnRevertGasLimit: big.NewInt(0), + }, + ) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted) + + // get SPL ZRC20 balance after withdraw + zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter) + + balanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SOL after withdraw: %d", balanceAfter) + + // check the balance of revert address is equal to withdraw amount + balance, err = r.SPLZRC20.BalanceOf(&bind.CallOpts{}, revertAddress) + require.NoError(r, err) + + require.Equal(r, withdrawAmount.Int64(), balance.Int64()) +} diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 053f09a27a..ce00bc7185 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -481,6 +481,7 @@ func (r *E2ERunner) WithdrawAndCallSOLZRC20( amount *big.Int, approveAmount *big.Int, data []byte, + revertOptions gatewayzevm.RevertOptions, ) *ethtypes.Transaction { // approve tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.GatewayZEVMAddr, approveAmount) @@ -516,7 +517,7 @@ func (r *E2ERunner) WithdrawAndCallSOLZRC20( r.SOLZRC20Addr, msgEncoded, gatewayzevm.CallOptions{GasLimit: big.NewInt(250000)}, - gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, + revertOptions, ) require.NoError(r, err) r.Logger.EVMTransaction(*tx, "withdraw_and_call") @@ -560,6 +561,7 @@ func (r *E2ERunner) WithdrawAndCallSPLZRC20( amount *big.Int, approveAmount *big.Int, data []byte, + revertOptions gatewayzevm.RevertOptions, ) *ethtypes.Transaction { // approve tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.GatewayZEVMAddr, approveAmount) @@ -607,7 +609,7 @@ func (r *E2ERunner) WithdrawAndCallSPLZRC20( r.SPLZRC20Addr, msgEncoded, gatewayzevm.CallOptions{GasLimit: big.NewInt(250000)}, - gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, + revertOptions, ) require.NoError(r, err) r.Logger.EVMTransaction(*tx, "withdraw_and_call") diff --git a/go.mod b/go.mod index 95d3bdb583..32c5e67d91 100644 --- a/go.mod +++ b/go.mod @@ -311,7 +311,7 @@ require ( github.com/montanaflynn/stats v0.7.1 github.com/showa-93/go-mask v0.6.2 github.com/tonkeeper/tongo v1.9.3 - github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250211174435-9680e27af84a + github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250212192042-34095b214e51 ) require ( diff --git a/go.sum b/go.sum index 2b4de83519..ce7c75670f 100644 --- a/go.sum +++ b/go.sum @@ -1404,8 +1404,8 @@ github.com/zeta-chain/go-tss v0.0.0-20241216161449-be92b20f8102 h1:jMb9ydfDFjgdl github.com/zeta-chain/go-tss v0.0.0-20241216161449-be92b20f8102/go.mod h1:nqelgf4HKkqlXaVg8X38a61WfyYB+ivCt6nnjoTIgCc= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250124151021-87b63e845f1c h1:iD7O6gFot1QHM6ggrt96N9eXnZ7vqkg2mFVm7OTaisw= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250124151021-87b63e845f1c/go.mod h1:SjT7QirtJE8stnAe1SlNOanxtfSfijJm3MGJ+Ax7w7w= -github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250211174435-9680e27af84a h1:+TVuNWQK8j7yc9TkH+JOplMae7Ym/yADoIMnO1yIQzo= -github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250211174435-9680e27af84a/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250212192042-34095b214e51 h1:r3cG+whGN8MFwfLG2Y2Irs2x1zyMznUDJHcOgV2X4UM= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250212192042-34095b214e51/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901 h1:9whtN5fjYHfk4yXIuAsYP2EHxImwDWDVUOnZJ2pfL3w= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901/go.mod h1:d2iTC62s9JwKiCMPhcDDXbIZmuzAyJ4lwso0H5QyRbk= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index 6f9f9e3837..0537bf50b7 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -42,6 +42,9 @@ var ( // DiscriminatorExecute returns the discriminator for Solana gateway 'execute' instruction DiscriminatorExecute = idlgateway.IDLGateway.GetDiscriminator("execute") + // DiscriminatorIncrementNonce returns the discriminator for Solana gateway 'increment_nonce' instruction + DiscriminatorIncrementNonce = idlgateway.IDLGateway.GetDiscriminator("increment_nonce") + // DiscriminatorExecuteSPL returns the discriminator for Solana gateway 'execute_spl_token' instruction DiscriminatorExecuteSPL = idlgateway.IDLGateway.GetDiscriminator("execute_spl_token") diff --git a/pkg/contracts/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go index a9a65d307e..96507f907f 100644 --- a/pkg/contracts/solana/gateway_message.go +++ b/pkg/contracts/solana/gateway_message.go @@ -8,14 +8,18 @@ import ( "github.com/gagliardetto/solana-go" ) +// NOTE: Following consts and identifier are used in outbounds message hashes for consistency. +// Instruction specific byte identifiers are used to encode instruction in hash with fixed amount of space. const ( InstructionWithdraw byte = 1 InstructionWithdrawSplToken byte = 2 InstructionWhitelistSplToken byte = 3 InstructionExecute byte = 5 InstructionExecuteSPL byte = 6 + InstructionIncrementNonce byte = 7 ) +// InstructionIdentifier is used at beginning of message hash to make it project specific. var InstructionIdentifier = []byte("ZETACHAIN") // MsgWithdraw is the message for the Solana gateway withdraw instruction @@ -119,6 +123,96 @@ func (msg *MsgWithdraw) Signer() (common.Address, error) { return RecoverSigner(msgHash[:], msgSig[:]) } +// MsgIncrementNonce is the message for the Solana gateway increment_nonce instruction +type MsgIncrementNonce struct { + // chainID is the chain ID of Solana chain + chainID uint64 + + // Nonce is the nonce for the increment_nonce + nonce uint64 + + // amount is the lamports amount for the increment_nonce + amount uint64 + + // signature is the signature of the message + signature [65]byte +} + +// NewMsgIncrementNonce returns a new increment_nonce message +func NewMsgIncrementNonce(chainID, nonce, amount uint64) *MsgIncrementNonce { + return &MsgIncrementNonce{ + chainID: chainID, + nonce: nonce, + amount: amount, + } +} + +// ChainID returns the chain ID of the message +func (msg *MsgIncrementNonce) ChainID() uint64 { + return msg.chainID +} + +// Nonce returns the nonce of the message +func (msg *MsgIncrementNonce) Nonce() uint64 { + return msg.nonce +} + +// Amount returns the amount of the message +func (msg *MsgIncrementNonce) Amount() uint64 { + return msg.amount +} + +// Hash packs the increment_nonce message and computes the hash +func (msg *MsgIncrementNonce) Hash() [32]byte { + var message []byte + buff := make([]byte, 8) + + message = append(message, InstructionIdentifier...) + message = append(message, InstructionIncrementNonce) + + binary.BigEndian.PutUint64(buff, msg.chainID) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.nonce) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.amount) + message = append(message, buff...) + + return crypto.Keccak256Hash(message) +} + +// SetSignature attaches the signature to the message +func (msg *MsgIncrementNonce) SetSignature(signature [65]byte) *MsgIncrementNonce { + msg.signature = signature + return msg +} + +// SigRSV returns the full 65-byte [R+S+V] signature +func (msg *MsgIncrementNonce) SigRSV() [65]byte { + return msg.signature +} + +// SigRS returns the 64-byte [R+S] core part of the signature +func (msg *MsgIncrementNonce) SigRS() [64]byte { + var sig [64]byte + copy(sig[:], msg.signature[:64]) + return sig +} + +// SigV returns the V part (recovery ID) of the signature +func (msg *MsgIncrementNonce) SigV() uint8 { + return msg.signature[64] +} + +// Signer returns the signer of the message +func (msg *MsgIncrementNonce) Signer() (common.Address, error) { + msgHash := msg.Hash() + msgSig := msg.SigRSV() + + return RecoverSigner(msgHash[:], msgSig[:]) +} + // MsgExecute is the message for the Solana gateway execute instruction type MsgExecute struct { // chainID is the chain ID of Solana chain diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go index 1c8fdb22bc..67aa0e73dd 100644 --- a/pkg/contracts/solana/instruction.go +++ b/pkg/contracts/solana/instruction.go @@ -88,6 +88,76 @@ type OutboundInstruction interface { // TokenAmount returns the amount of the instruction TokenAmount() uint64 + + // InstructionDiscriminator returns the discriminator of the instruction + InstructionDiscriminator() [8]byte +} + +var _ OutboundInstruction = (*IncrementNonceInstructionParams)(nil) + +// IncrementNonceInstructionParams contains the parameters for a gateway increment_nonce instruction +type IncrementNonceInstructionParams struct { + // Discriminator is the unique identifier for the increment_nonce instruction + Discriminator [8]byte + + // Amount is the lamports amount for the increment_nonce + Amount uint64 + + // Signature is the ECDSA signature (by TSS) for the increment_nonce + Signature [64]byte + + // RecoveryID is the recovery ID used to recover the public key from ECDSA signature + RecoveryID uint8 + + // MessageHash is the hash of the message signed by TSS + MessageHash [32]byte + + // Nonce is the nonce for the increment_nonce + Nonce uint64 +} + +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *IncrementNonceInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + +// Signer returns the signer of the signature contained +func (inst *IncrementNonceInstructionParams) Signer() (signer common.Address, err error) { + var signature [65]byte + copy(signature[:], inst.Signature[:64]) + signature[64] = inst.RecoveryID + + return RecoverSigner(inst.MessageHash[:], signature[:]) +} + +// GatewayNonce returns the nonce of the instruction +func (inst *IncrementNonceInstructionParams) GatewayNonce() uint64 { + return inst.Nonce +} + +// TokenAmount returns the amount of the instruction +func (inst *IncrementNonceInstructionParams) TokenAmount() uint64 { + return inst.Amount +} + +// ParseInstructionIncrementNonce tries to parse the instruction as a 'increment_nonce'. +// It returns nil if the instruction can't be parsed as a 'increment_nonce'. +func ParseInstructionIncrementNonce( + instruction solana.CompiledInstruction, +) (*IncrementNonceInstructionParams, error) { + // try deserializing instruction as a 'increment_nonce' + inst := &IncrementNonceInstructionParams{} + err := borsh.Deserialize(inst, instruction.Data) + if err != nil { + return nil, errors.Wrap(err, "error deserializing instruction") + } + + // check the discriminator to ensure it's a 'increment_nonce' instruction + if inst.Discriminator != DiscriminatorIncrementNonce { + return nil, fmt.Errorf("not an increment_nonce instruction: %v", inst.Discriminator) + } + + return inst, nil } var _ OutboundInstruction = (*WithdrawInstructionParams)(nil) @@ -113,6 +183,11 @@ type WithdrawInstructionParams struct { Nonce uint64 } +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *WithdrawInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + // Signer returns the signer of the signature contained func (inst *WithdrawInstructionParams) Signer() (signer common.Address, err error) { var signature [65]byte @@ -179,6 +254,11 @@ type ExecuteInstructionParams struct { Nonce uint64 } +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *ExecuteInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + // Signer returns the signer of the signature contained func (inst *ExecuteInstructionParams) Signer() (signer common.Address, err error) { var signature [65]byte @@ -241,6 +321,11 @@ type WithdrawSPLInstructionParams struct { Nonce uint64 } +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *WithdrawSPLInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + // Signer returns the signer of the signature contained func (inst *WithdrawSPLInstructionParams) Signer() (signer common.Address, err error) { var signature [65]byte @@ -309,6 +394,11 @@ type ExecuteSPLInstructionParams struct { Nonce uint64 } +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *ExecuteSPLInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + // Signer returns the signer of the signature contained func (inst *ExecuteSPLInstructionParams) Signer() (signer common.Address, err error) { var signature [65]byte @@ -377,6 +467,11 @@ type WhitelistInstructionParams struct { Nonce uint64 } +// InstructionDiscriminator returns the discriminator of the instruction +func (inst *WhitelistInstructionParams) InstructionDiscriminator() [8]byte { + return inst.Discriminator +} + // Signer returns the signer of the signature contained func (inst *WhitelistInstructionParams) Signer() (signer common.Address, err error) { var signature [65]byte diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index c4153b1db6..21cd8bbf8d 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -109,6 +109,9 @@ func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *crosschai // status was already verified as successful in CheckFinalizedTx outboundStatus := chains.ReceiveStatus_success + if inst.InstructionDiscriminator() == contracts.DiscriminatorIncrementNonce { + outboundStatus = chains.ReceiveStatus_failed + } // compliance check, special handling the cancelled cctx if compliance.IsCctxRestricted(cctx) { @@ -145,10 +148,14 @@ func (ob *Observer) PostVoteOutbound( // so we set retryGasLimit to 0 because the solana gateway withdrawal will always succeed // and the vote msg won't trigger ZEVM interaction const ( - gasLimit = zetacore.PostVoteOutboundGasLimit - retryGasLimit = 0 + gasLimit = zetacore.PostVoteOutboundGasLimit ) + var retryGasLimit uint64 + if msg.Status == chains.ReceiveStatus_failed { + retryGasLimit = zetacore.PostVoteOutboundRevertGasLimit + } + // post vote to zetacore zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) if err != nil { @@ -301,6 +308,12 @@ func ParseGatewayInstruction( return nil, fmt.Errorf("programID %s is not matching gatewayID %s", programID, gatewayID) } + // first check if it was simple nonce increment instruction, which indicates that outbound failed + inst, err := contracts.ParseInstructionIncrementNonce(instruction) + if err == nil { + return inst, nil + } + // parse the outbound instruction switch coinType { case coin.CoinType_Gas: diff --git a/zetaclient/chains/solana/signer/execute.go b/zetaclient/chains/solana/signer/execute.go index 9ec45c2bb5..90d9cca513 100644 --- a/zetaclient/chains/solana/signer/execute.go +++ b/zetaclient/chains/solana/signer/execute.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/errors" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/zeta-chain/node/pkg/chains" @@ -55,8 +54,8 @@ func (signer *Signer) createAndSignMsgExecute( return msg.SetSignature(signature), nil } -// signExecuteTx wraps the execute 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) signExecuteTx(ctx context.Context, msg contracts.MsgExecute) (*solana.Transaction, error) { +// createExecuteInstruction wraps the execute 'msg' into a Solana instruction. +func (signer *Signer) createExecuteInstruction(msg contracts.MsgExecute) (*solana.GenericInstruction, error) { // create execute instruction with program call data dataBytes, err := borsh.Serialize(contracts.ExecuteInstructionParams{ Discriminator: contracts.DiscriminatorExecute, @@ -85,43 +84,11 @@ func (signer *Signer) signExecuteTx(ctx context.Context, msg contracts.MsgExecut } allAccounts := append(predefinedAccounts, msg.RemainingAccounts()...) - inst := solana.GenericInstruction{ + inst := &solana.GenericInstruction{ ProgID: signer.gatewayID, DataBytes: dataBytes, AccountValues: allAccounts, } - // get a recent blockhash - recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - if err != nil { - return nil, errors.Wrap(err, "getLatestBlockhash error") - } - - // create a transaction that wraps the instruction - tx, err := solana.NewTransaction( - []solana.Instruction{ - // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget - // https://github.com/zeta-chain/node/issues/2599 - // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), - // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), - &inst}, - recent.Value.Blockhash, - solana.TransactionPayer(signer.relayerKey.PublicKey()), - ) - if err != nil { - return nil, errors.Wrap(err, "unable to create new tx") - } - - // relayer signs the transaction - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(signer.relayerKey.PublicKey()) { - return signer.relayerKey - } - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "signer unable to sign transaction") - } - - return tx, nil + return inst, nil } diff --git a/zetaclient/chains/solana/signer/execute_spl.go b/zetaclient/chains/solana/signer/execute_spl.go index 2336217660..0620f39517 100644 --- a/zetaclient/chains/solana/signer/execute_spl.go +++ b/zetaclient/chains/solana/signer/execute_spl.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/errors" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/zeta-chain/node/pkg/chains" @@ -85,11 +84,8 @@ func (signer *Signer) createAndSignMsgExecuteSPL( return msg.SetSignature(signature), nil } -// signExecuteSPLTx wraps the execute spl 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) signExecuteSPLTx( - ctx context.Context, - msg contracts.MsgExecuteSPL, -) (*solana.Transaction, error) { +// createExecuteSPLInstruction wraps the execute spl 'msg' into a Solana instruction. +func (signer *Signer) createExecuteSPLInstruction(msg contracts.MsgExecuteSPL) (*solana.GenericInstruction, error) { // create execute spl instruction with program call data dataBytes, err := borsh.Serialize(contracts.ExecuteSPLInstructionParams{ Discriminator: contracts.DiscriminatorExecuteSPL, @@ -130,42 +126,11 @@ func (signer *Signer) signExecuteSPLTx( } allAccounts := append(predefinedAccounts, msg.RemainingAccounts()...) - inst := solana.GenericInstruction{ + inst := &solana.GenericInstruction{ ProgID: signer.gatewayID, DataBytes: dataBytes, AccountValues: allAccounts, } - // get a recent blockhash - recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - if err != nil { - return nil, errors.Wrap(err, "GetLatestBlockhash error") - } - - // create a transaction that wraps the instruction - tx, err := solana.NewTransaction( - []solana.Instruction{ - // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget - // https://github.com/zeta-chain/node/issues/2599 - // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), - // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), - &inst}, - recent.Value.Blockhash, - solana.TransactionPayer(signer.relayerKey.PublicKey()), - ) - if err != nil { - return nil, errors.Wrap(err, "unable to create new tx") - } - - // relayer signs the transaction - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(signer.relayerKey.PublicKey()) { - return signer.relayerKey - } - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "signer unable to sign transaction") - } - return tx, nil + return inst, nil } diff --git a/zetaclient/chains/solana/signer/increment_nonce.go b/zetaclient/chains/solana/signer/increment_nonce.go new file mode 100644 index 0000000000..9716814c01 --- /dev/null +++ b/zetaclient/chains/solana/signer/increment_nonce.go @@ -0,0 +1,74 @@ +package signer + +import ( + "context" + + "cosmossdk.io/errors" + "github.com/gagliardetto/solana-go" + "github.com/near/borsh-go" + + contracts "github.com/zeta-chain/node/pkg/contracts/solana" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// createAndSignMsgIncrementNonce creates and signs a increment_nonce message for gateway increment_nonce instruction with TSS. +func (signer *Signer) createAndSignMsgIncrementNonce( + ctx context.Context, + params *types.OutboundParams, + height uint64, + cancelTx bool, +) (*contracts.MsgIncrementNonce, error) { + chain := signer.Chain() + // #nosec G115 always positive + chainID := uint64(signer.Chain().ChainId) + nonce := params.TssNonce + amount := params.Amount.Uint64() + + // zero out the amount if cancelTx is set. It's legal to withdraw 0 lamports through the gateway. + if cancelTx { + amount = 0 + } + + // prepare increment_nonce msg and compute hash + msg := contracts.NewMsgIncrementNonce(chainID, nonce, amount) + msgHash := msg.Hash() + + // sign the message with TSS to get an ECDSA signature. + // the produced signature is in the [R || S || V] format where V is 0 or 1. + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId) + if err != nil { + return nil, errors.Wrap(err, "key-sign failed") + } + + // attach the signature and return + return msg.SetSignature(signature), nil +} + +// createIncrementNonceInstruction wraps the increment_nonce 'msg' into a Solana instruction. +func (signer *Signer) createIncrementNonceInstruction( + msg contracts.MsgIncrementNonce, +) (*solana.GenericInstruction, error) { + // create increment_nonce instruction with program call data + dataBytes, err := borsh.Serialize(contracts.IncrementNonceInstructionParams{ + Discriminator: contracts.DiscriminatorIncrementNonce, + Amount: msg.Amount(), + Signature: msg.SigRS(), + RecoveryID: msg.SigV(), + MessageHash: msg.Hash(), + Nonce: msg.Nonce(), + }) + if err != nil { + return nil, errors.Wrap(err, "cannot serialize increment_nonce instruction") + } + + inst := &solana.GenericInstruction{ + ProgID: signer.gatewayID, + DataBytes: dataBytes, + AccountValues: []*solana.AccountMeta{ + solana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(), + solana.Meta(signer.pda).WRITE(), + }, + } + + return inst, nil +} diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 18f24a989a..a90d75a313 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -132,6 +132,7 @@ func (signer *Signer) TryProcessOutbound( coinType := cctx.InboundParams.CoinType var tx *solana.Transaction + var fallbackTx *solana.Transaction switch coinType { case coin.CoinType_Cmd: @@ -150,8 +151,14 @@ func (signer *Signer) TryProcessOutbound( logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign execute outbound") return } + incrementNonceTx, err := signer.prepareIncrementNonceTx(ctx, cctx, height, logger) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign increment_nonce outbound") + return + } tx = executeTx + fallbackTx = incrementNonceTx } else { withdrawTx, err := signer.prepareWithdrawTx(ctx, cctx, height, logger) if err != nil { @@ -170,7 +177,14 @@ func (signer *Signer) TryProcessOutbound( return } + incrementNonceTx, err := signer.prepareIncrementNonceTx(ctx, cctx, height, logger) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign increment_nonce outbound") + return + } + tx = executeSPLTx + fallbackTx = incrementNonceTx } else { withdrawSPLTx, err := signer.prepareWithdrawSPLTx(ctx, cctx, height, logger) if err != nil { @@ -196,13 +210,51 @@ func (signer *Signer) TryProcessOutbound( signer.SetRelayerBalanceMetrics(ctx) // broadcast the signed tx to the Solana network - signer.broadcastOutbound(ctx, tx, chainID, nonce, logger, zetacoreClient) + signer.broadcastOutbound(ctx, tx, fallbackTx, chainID, nonce, logger, zetacoreClient) +} + +// signTx creates and signs solana tx containing provided instruction with relayer key. +func (signer *Signer) signTx(ctx context.Context, inst *solana.GenericInstruction) (*solana.Transaction, error) { + // get a recent blockhash + recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + return nil, errors.Wrap(err, "getLatestBlockhash error") + } + + // create a transaction that wraps the instruction + tx, err := solana.NewTransaction( + []solana.Instruction{ + // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget + // https://github.com/zeta-chain/node/issues/2599 + // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), + // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), + inst}, + recent.Value.Blockhash, + solana.TransactionPayer(signer.relayerKey.PublicKey()), + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create new tx") + } + + // relayer signs the transaction + _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(signer.relayerKey.PublicKey()) { + return signer.relayerKey + } + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "signer unable to sign transaction") + } + + return tx, nil } // broadcastOutbound sends the signed transaction to the Solana network func (signer *Signer) broadcastOutbound( ctx context.Context, tx *solana.Transaction, + fallbackTx *solana.Transaction, chainID int64, nonce uint64, logger zerolog.Logger, @@ -247,6 +299,11 @@ func (signer *Signer) broadcastOutbound( rpc.TransactionOpts{PreflightCommitment: rpc.CommitmentProcessed}, ) if err != nil { + // in case it is not failure due to nonce mismatch, replace tx with fallback tx + // probably need a better way to do this, but currently this is the only error to tolerate like this + if !strings.Contains(err.Error(), "NonceMismatch") { + tx = fallbackTx + } logger.Warn().Err(err).Fields(lf).Msgf("SendTransactionWithOpts failed") backOff *= 2 continue @@ -259,6 +316,43 @@ func (signer *Signer) broadcastOutbound( } } +func (signer *Signer) prepareIncrementNonceTx( + ctx context.Context, + cctx *types.CrossChainTx, + height uint64, + logger zerolog.Logger, +) (*solana.Transaction, error) { + params := cctx.GetCurrentOutboundParam() + // compliance check + cancelTx := compliance.IsCctxRestricted(cctx) + if cancelTx { + compliance.PrintComplianceLog( + logger, + signer.Logger().Compliance, + true, + signer.Chain().ChainId, + cctx.Index, + cctx.InboundParams.Sender, + params.Receiver, + "SOL", + ) + } + + // sign gateway increment_nonce message by TSS + msg, err := signer.createAndSignMsgIncrementNonce(ctx, params, height, cancelTx) + if err != nil { + return nil, err + } + + // sign the increment_nonce transaction by relayer key + inst, err := signer.createIncrementNonceInstruction(*msg) + if err != nil { + return nil, errors.Wrap(err, "error creating increment nonce instruction") + } + + return signer.signTx(ctx, inst) +} + func (signer *Signer) prepareWithdrawTx( ctx context.Context, cctx *types.CrossChainTx, @@ -288,12 +382,12 @@ func (signer *Signer) prepareWithdrawTx( } // sign the withdraw transaction by relayer key - tx, err := signer.signWithdrawTx(ctx, *msg) + inst, err := signer.createWithdrawInstruction(*msg) if err != nil { - return nil, errors.Wrap(err, "signWithdrawTx error") + return nil, errors.Wrap(err, "error creating withdraw instruction") } - return tx, nil + return signer.signTx(ctx, inst) } func (signer *Signer) prepareExecuteTx( @@ -351,12 +445,12 @@ func (signer *Signer) prepareExecuteTx( } // sign the execute transaction by relayer key - tx, err := signer.signExecuteTx(ctx, *msgExecute) + inst, err := signer.createExecuteInstruction(*msgExecute) if err != nil { - return nil, errors.Wrap(err, "signExecuteTx error") + return nil, errors.Wrap(err, "error creating execute instruction") } - return tx, nil + return signer.signTx(ctx, inst) } func (signer *Signer) prepareWithdrawSPLTx( @@ -401,12 +495,12 @@ func (signer *Signer) prepareWithdrawSPLTx( } // sign the withdraw transaction by relayer key - tx, err := signer.signWithdrawSPLTx(ctx, *msg) + inst, err := signer.createWithdrawSPLInstruction(*msg) if err != nil { - return nil, errors.Wrap(err, "signWithdrawSPLTx error") + return nil, errors.Wrap(err, "error creating withdraw SPL instruction") } - return tx, nil + return signer.signTx(ctx, inst) } func (signer *Signer) prepareExecuteSPLTx( @@ -473,12 +567,12 @@ func (signer *Signer) prepareExecuteSPLTx( } // sign the execute spl transaction by relayer key - tx, err := signer.signExecuteSPLTx(ctx, *msgExecuteSpl) + inst, err := signer.createExecuteSPLInstruction(*msgExecuteSpl) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error creating execute SPL instruction") } - return tx, nil + return signer.signTx(ctx, inst) } func (signer *Signer) prepareWhitelistTx( @@ -510,12 +604,12 @@ func (signer *Signer) prepareWhitelistTx( } // sign the whitelist transaction by relayer key - tx, err := signer.signWhitelistTx(ctx, msg) + inst, err := signer.createWhitelistInstruction(msg) if err != nil { - return nil, errors.Wrap(err, "signWhitelistTx error") + return nil, errors.Wrap(err, "error creating whitelist instruction") } - return tx, nil + return signer.signTx(ctx, inst) } func (signer *Signer) decodeMintAccountDetails(ctx context.Context, asset string) (token.Mint, error) { diff --git a/zetaclient/chains/solana/signer/whitelist.go b/zetaclient/chains/solana/signer/whitelist.go index e752d7abb9..7dd12c11e4 100644 --- a/zetaclient/chains/solana/signer/whitelist.go +++ b/zetaclient/chains/solana/signer/whitelist.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/errors" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" contracts "github.com/zeta-chain/node/pkg/contracts/solana" @@ -40,8 +39,8 @@ func (signer *Signer) createAndSignMsgWhitelist( return msg.SetSignature(signature), nil } -// signWhitelistTx wraps the whitelist 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhitelist) (*solana.Transaction, error) { +// createWhitelistInstruction wraps the whitelist 'msg' into a Solana instruction. +func (signer *Signer) createWhitelistInstruction(msg *contracts.MsgWhitelist) (*solana.GenericInstruction, error) { // create whitelist_spl_mint instruction with program call data dataBytes, err := borsh.Serialize(contracts.WhitelistInstructionParams{ Discriminator: contracts.DiscriminatorWhitelistSplMint, @@ -54,7 +53,7 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi return nil, errors.Wrap(err, "cannot serialize whitelist_spl_mint instruction") } - inst := solana.GenericInstruction{ + inst := &solana.GenericInstruction{ ProgID: signer.gatewayID, DataBytes: dataBytes, AccountValues: []*solana.AccountMeta{ @@ -66,37 +65,5 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi }, } - // get a recent blockhash - recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - if err != nil { - return nil, errors.Wrap(err, "getLatestBlockhash error") - } - - // create a transaction that wraps the instruction - tx, err := solana.NewTransaction( - []solana.Instruction{ - // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget - // https://github.com/zeta-chain/node/issues/2599 - // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), - // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), - &inst}, - recent.Value.Blockhash, - solana.TransactionPayer(signer.relayerKey.PublicKey()), - ) - if err != nil { - return nil, errors.Wrap(err, "unable to create new tx") - } - - // relayer signs the transaction - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(signer.relayerKey.PublicKey()) { - return signer.relayerKey - } - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "signer unable to sign transaction") - } - - return tx, nil + return inst, nil } diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 20b6582fc7..a95947f014 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/errors" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/zeta-chain/node/pkg/chains" @@ -52,8 +51,8 @@ func (signer *Signer) createAndSignMsgWithdraw( return msg.SetSignature(signature), nil } -// signWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { +// createWithdrawInstruction wraps the withdraw 'msg' into a Solana instruction. +func (signer *Signer) createWithdrawInstruction(msg contracts.MsgWithdraw) (*solana.GenericInstruction, error) { // create withdraw instruction with program call data dataBytes, err := borsh.Serialize(contracts.WithdrawInstructionParams{ Discriminator: contracts.DiscriminatorWithdraw, @@ -67,7 +66,7 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd return nil, errors.Wrap(err, "cannot serialize withdraw instruction") } - inst := solana.GenericInstruction{ + inst := &solana.GenericInstruction{ ProgID: signer.gatewayID, DataBytes: dataBytes, AccountValues: []*solana.AccountMeta{ @@ -77,37 +76,5 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd }, } - // get a recent blockhash - recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - if err != nil { - return nil, errors.Wrap(err, "getLatestBlockhash error") - } - - // create a transaction that wraps the instruction - tx, err := solana.NewTransaction( - []solana.Instruction{ - // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget - // https://github.com/zeta-chain/node/issues/2599 - // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), - // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), - &inst}, - recent.Value.Blockhash, - solana.TransactionPayer(signer.relayerKey.PublicKey()), - ) - if err != nil { - return nil, errors.Wrap(err, "unable to create new tx") - } - - // relayer signs the transaction - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(signer.relayerKey.PublicKey()) { - return signer.relayerKey - } - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "signer unable to sign transaction") - } - - return tx, nil + return inst, nil } diff --git a/zetaclient/chains/solana/signer/withdraw_spl.go b/zetaclient/chains/solana/signer/withdraw_spl.go index 25365b8441..2671806ecb 100644 --- a/zetaclient/chains/solana/signer/withdraw_spl.go +++ b/zetaclient/chains/solana/signer/withdraw_spl.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/errors" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/zeta-chain/node/pkg/chains" @@ -66,11 +65,8 @@ func (signer *Signer) createAndSignMsgWithdrawSPL( return msg.SetSignature(signature), nil } -// signWithdrawSPLTx wraps the withdraw spl 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) signWithdrawSPLTx( - ctx context.Context, - msg contracts.MsgWithdrawSPL, -) (*solana.Transaction, error) { +// createWithdrawSPLInstruction wraps the withdraw spl 'msg' into a Solana instruction. +func (signer *Signer) createWithdrawSPLInstruction(msg contracts.MsgWithdrawSPL) (*solana.GenericInstruction, error) { // create withdraw spl instruction with program call data dataBytes, err := borsh.Serialize(contracts.WithdrawSPLInstructionParams{ Discriminator: contracts.DiscriminatorWithdrawSPL, @@ -95,7 +91,7 @@ func (signer *Signer) signWithdrawSPLTx( return nil, errors.Wrapf(err, "cannot find ATA for %s and mint account %s", msg.To(), msg.MintAccount()) } - inst := solana.GenericInstruction{ + inst := &solana.GenericInstruction{ ProgID: signer.gatewayID, DataBytes: dataBytes, AccountValues: []*solana.AccountMeta{ @@ -110,37 +106,6 @@ func (signer *Signer) signWithdrawSPLTx( solana.Meta(solana.SystemProgramID), }, } - // get a recent blockhash - recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - if err != nil { - return nil, errors.Wrap(err, "getLatestBlockhash error") - } - - // create a transaction that wraps the instruction - tx, err := solana.NewTransaction( - []solana.Instruction{ - // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget - // https://github.com/zeta-chain/node/issues/2599 - // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), - // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), - &inst}, - recent.Value.Blockhash, - solana.TransactionPayer(signer.relayerKey.PublicKey()), - ) - if err != nil { - return nil, errors.Wrap(err, "unable to create new tx") - } - - // relayer signs the transaction - _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(signer.relayerKey.PublicKey()) { - return signer.relayerKey - } - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "signer unable to sign transaction") - } - return tx, nil + return inst, nil } From c47032bb6a691c4c9f5e985405e9fbcacec2807e Mon Sep 17 00:00:00 2001 From: skosito Date: Mon, 24 Feb 2025 17:28:26 +0000 Subject: [PATCH 05/22] test: extend solana withdraw and call unit tests (#3561) * sol withdraw and call and e2e test * fmt * fix solana e2e tests * fix msg hash unit tests * cleanup * bump gateway.so * cleanup unused function * cleanup * PR comments * linter * spl withdraw and call * bump gateway and changelog * bump gateway to fix e2e tests * sol withdraw and call revert * revert for spl withdraw and call * generate * revert gas limit changes * bump gateway * changelog * cleanup * PR comments * use callOnRevert in e2e test * use inst discriminator instead of artificial failed func * add missing gateway msg tests * add outbound tests and data * generate --- pkg/contracts/solana/gateway_message_test.go | 70 +++++ .../chains/solana/observer/outbound_test.go | 225 ++++++++++++++++ ...jrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo.json | 108 ++++++++ ...dxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk.json | 67 +++++ ...Pa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW.json | 244 ++++++++++++++++++ 5 files changed, 714 insertions(+) create mode 100644 zetaclient/testdata/solana/chain_901_outbound_tx_result_4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo.json create mode 100644 zetaclient/testdata/solana/chain_901_outbound_tx_result_5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk.json create mode 100644 zetaclient/testdata/solana/chain_901_outbound_tx_result_d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW.json diff --git a/pkg/contracts/solana/gateway_message_test.go b/pkg/contracts/solana/gateway_message_test.go index 287b5cea47..fa78812387 100644 --- a/pkg/contracts/solana/gateway_message_test.go +++ b/pkg/contracts/solana/gateway_message_test.go @@ -3,6 +3,7 @@ package solana_test import ( "testing" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/gagliardetto/solana-go" @@ -76,3 +77,72 @@ func Test_MsgWithdrawSPLHash(t *testing.T) { require.EqualValues(t, hash[:], wantHashBytes) }) } + +func Test_MsgIncrementNonceHash(t *testing.T) { + t.Run("should calculate expected hash", func(t *testing.T) { + // ARRANGE + // #nosec G115 always positive + chainID := uint64(chains.SolanaLocalnet.ChainId) + nonce := uint64(0) + amount := uint64(1336000) + + wantHash := "7acd44905f89cc36b4542b103a25f2110d25ca469dee854fbf342f1f3ee8ba90" + wantHashBytes := testutil.HexToBytes(t, wantHash) + + // ACT + // create new increment nonce message + hash := contracts.NewMsgIncrementNonce(chainID, nonce, amount).Hash() + + // ASSERT + require.EqualValues(t, hash[:], wantHashBytes) + }) +} + +func Test_MsgExecuteHash(t *testing.T) { + t.Run("should calculate expected hash", func(t *testing.T) { + // ARRANGE + // #nosec G115 always positive + chainID := uint64(chains.SolanaLocalnet.ChainId) + nonce := uint64(0) + amount := uint64(1336000) + to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ") + sender := common.HexToAddress("0x42bd6E2ce4CDb2F58Ed0A0E427F011A0645D5E33") + + wantHash := "7391cf357fd80e7cb3d2a9758932fdea4988d03c87210a2632f03b467728d199" + wantHashBytes := testutil.HexToBytes(t, wantHash) + + // ACT + // create new execute message + hash := contracts.NewMsgExecute(chainID, nonce, amount, to, sender, []byte("hello"), []*solana.AccountMeta{}). + Hash() + + // ASSERT + require.EqualValues(t, hash[:], wantHashBytes) + }) +} + +func Test_MsgExecuteSPLHash(t *testing.T) { + t.Run("should calculate expected hash", func(t *testing.T) { + // ARRANGE + // #nosec G115 always positive + chainID := uint64(chains.SolanaLocalnet.ChainId) + nonce := uint64(0) + amount := uint64(1336000) + mintAccount := solana.MustPublicKeyFromBase58("AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z") + to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ") + toAta, _, err := solana.FindAssociatedTokenAddress(to, mintAccount) + require.NoError(t, err) + sender := common.HexToAddress("0x42bd6E2ce4CDb2F58Ed0A0E427F011A0645D5E33") + + wantHash := "d90f9640faecd76509b4e88fa7d18f130918130b8666179f1597f185a828d3a5" + wantHashBytes := testutil.HexToBytes(t, wantHash) + + // ACT + // create new execute message + hash := contracts.NewMsgExecuteSPL(chainID, nonce, amount, 8, mintAccount, to, toAta, sender, []byte("hello"), []*solana.AccountMeta{}). + Hash() + + // ASSERT + require.EqualValues(t, hash[:], wantHashBytes) + }) +} diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go index 7c7a82e2c6..95a30472b7 100644 --- a/zetaclient/chains/solana/observer/outbound_test.go +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -43,6 +43,15 @@ const ( // withdrawSPLTxTest is local devnet tx result for testing withdrawSPLTxTest = "3NgoR4K9FJq7UunorPRGW9wpqMV8oNvZERejutd7bKmqh3CKEV5DMZndhZn7hQ1i4RhTyHXRWxtR5ZNVHmmjAUSF" + + // executeTxTest is local devnet tx result for testing + executeTxTest = "4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo" + + // executeSPLTxTest is local devnet tx result for testing + executeSPLTxTest = "d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW" + + // incrementNonceTxTest is local devnet tx result for testing + incrementNonceTxTest = "5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk" ) // createTestObserver creates a test observer for testing @@ -515,3 +524,219 @@ func Test_ParseInstructionWithdrawSPL(t *testing.T) { require.Nil(t, inst) }) } + +func Test_ParseInstructionExecute(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := executeTxTest + txAmount := uint64(1000000) + + t.Run("should parse instruction execute", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + // tss address used in local devnet + tssAddress := "0xF2eCA3Fd5a152eb5b9ceBcA7E492C668cA09Cdd3" + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + instruction := tx.Message.Instructions[0] + + // ACT + inst, err := contracts.ParseInstructionExecute(instruction) + require.NoError(t, err) + + // ASSERT + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddress, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 1) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid instruction data", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") + + // ACT + inst, err := contracts.ParseInstructionExecute(instruction) + + // ASSERT + require.ErrorContains(t, err, "error deserializing instruction") + require.Nil(t, inst) + }) + + t.Run("should return error on discriminator mismatch", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] + fakeDiscriminator := "b712469c946da12100980d0000000000" + fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) + require.NoError(t, err) + copy(instruction.Data, fakeDiscriminatorBytes) + + // ACT + inst, err := contracts.ParseInstructionExecute(instruction) + + // ASSERT + require.ErrorContains(t, err, "not an execute instruction") + require.Nil(t, inst) + }) +} + +func Test_ParseInstructionExecuteSPL(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := executeSPLTxTest + txAmount := uint64(1000000) + + t.Run("should parse instruction execute SPL", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + // tss address used in local devnet + tssAddress := "0xF2eCA3Fd5a152eb5b9ceBcA7E492C668cA09Cdd3" + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + instruction := tx.Message.Instructions[0] + + // ACT + inst, err := contracts.ParseInstructionExecuteSPL(instruction) + require.NoError(t, err) + + // ASSERT + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddress, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 6) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid instruction data", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") + + // ACT + inst, err := contracts.ParseInstructionExecuteSPL(instruction) + + // ASSERT + require.ErrorContains(t, err, "error deserializing instruction") + require.Nil(t, inst) + }) + + t.Run("should return error on discriminator mismatch", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] + fakeDiscriminator := "b712469c946da12100980d0000000000" + fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) + require.NoError(t, err) + copy(instruction.Data, fakeDiscriminatorBytes) + + // ACT + inst, err := contracts.ParseInstructionExecuteSPL(instruction) + + // ASSERT + require.ErrorContains(t, err, "not an execute_spl_token instruction") + require.Nil(t, inst) + }) +} + +func Test_ParseInstructionIncrementNonce(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := incrementNonceTxTest + txAmount := uint64(1000000) + + t.Run("should parse instruction increment nonce", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + // tss address used in local devnet + tssAddress := "0xF2eCA3Fd5a152eb5b9ceBcA7E492C668cA09Cdd3" + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + instruction := tx.Message.Instructions[0] + + // ACT + inst, err := contracts.ParseInstructionIncrementNonce(instruction) + require.NoError(t, err) + + // ASSERT + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddress, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 2) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid instruction data", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") + + // ACT + inst, err := contracts.ParseInstructionIncrementNonce(instruction) + + // ASSERT + require.ErrorContains(t, err, "error deserializing instruction") + require.Nil(t, inst) + }) + + t.Run("should return error on discriminator mismatch", func(t *testing.T) { + // ARRANGE + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] + fakeDiscriminator := "b712469c946da12100980d0000000000" + fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) + require.NoError(t, err) + copy(instruction.Data, fakeDiscriminatorBytes) + + // ACT + inst, err := contracts.ParseInstructionIncrementNonce(instruction) + + // ASSERT + require.ErrorContains(t, err, "not an increment_nonce instruction") + require.Nil(t, inst) + }) +} diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo.json new file mode 100644 index 0000000000..1c5b1190cd --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo.json @@ -0,0 +1,108 @@ +{ + "blockTime": 1740070795, + "meta": { + "computeUnitsConsumed": 79707, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "accounts": [ + 3, + 1, + 4, + 5 + ], + "data": "Qj4no588tu2RksabYK3y3pQYzHBqRCDjYKJAKTpJmARF93XWomZBGVyY1UVNe", + "programIdIndex": 2, + "stackHeight": 2 + } + ] + } + ], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: Execute", + "Program log: Computed message hash: [47, 152, 48, 18, 189, 52, 199, 141, 141, 107, 47, 194, 114, 167, 21, 198, 49, 255, 210, 144, 228, 234, 48, 48, 235, 226, 100, 53, 97, 94, 179, 49]", + "Program log: Recovered address [242, 236, 163, 253, 90, 21, 46, 181, 185, 206, 188, 167, 228, 146, 198, 104, 202, 9, 205, 211]", + "Program 4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc invoke [2]", + "Program log: Instruction: OnCall", + "Program log: On call executed with amount 1000000, sender [16, 63, 217, 34, 79, 0, 206, 48, 19, 233, 86, 41, 229, 45, 252, 49, 216, 5, 214, 141] and message hello", + "Program 4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc consumed 11322 of 149084 compute units", + "Program 4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc success", + "Program log: Execute done: destination contract = 1000000, amount = 4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc, sender = [16, 63, 217, 34, 79, 0, 206, 48, 19, 233, 86, 41, 229, 45, 252, 49, 216, 5, 214, 141]", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 79707 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "postBalances": [ + 99999990000, + 25447680, + 1141440, + 1947680, + 999966657520, + 1, + 1141440 + ], + "postTokenBalances": [], + "preBalances": [ + 99999995000, + 26447680, + 1141440, + 1447680, + 999966157520, + 1, + 1141440 + ], + "preTokenBalances": [], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 510, + "transaction": { + "message": { + "accountKeys": [ + "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc", + "VqaCSRDVj4vDupnETzRshW69LsFXovkZwd65EeZA8ru", + "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ", + "11111111111111111111111111111111", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 2, + "numRequiredSignatures": 1 + }, + "instructions": [ + { + "accounts": [ + 0, + 1, + 2, + 3, + 3, + 1, + 4, + 5 + ], + "data": "H3UF2NoybtJj4qWbWhTYMkLNBaVRLTG7GTgkuAaPZ44uKXmaaSZG5Ae4iBXVYqX2zViQzA1Z4kd3Tcuoo73GJ28n7iMYMtgvjNWki6X32b8h21s83TsSDu7BBFPNA1YvC4yaSgUJ4uVjPSf9EfziZsFrtmVrirWh1jqenoKnQXMT799yvJC9rwPnbFk78ZSCekarLBNAEYtQP", + "programIdIndex": 6, + "stackHeight": null + } + ], + "recentBlockhash": "2PAQoYg7XZ4WLo1htU483HK3HG5QCicF4BudqPzAft9M" + }, + "signatures": [ + "4ZuPTkYtBGDyDZNHKyHxEKL98VeaefAMUzmZVL2BrgwCvog7CqpjrRoegXDt6bD7w8dffGKGcDZqFYFi5vkAK8eo" + ] + } +} \ No newline at end of file diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk.json new file mode 100644 index 0000000000..10f28c7bdc --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk.json @@ -0,0 +1,67 @@ +{ + "blockTime": 1740070842, + "meta": { + "computeUnitsConsumed": 47687, + "err": null, + "fee": 5000, + "innerInstructions": [], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: IncrementNonce", + "Program log: Computed message hash: [72, 247, 17, 19, 98, 106, 253, 89, 163, 125, 180, 224, 183, 214, 91, 148, 171, 154, 240, 91, 170, 155, 196, 20, 210, 253, 224, 238, 133, 178, 253, 90]", + "Program log: Recovered address [242, 236, 163, 253, 90, 21, 46, 181, 185, 206, 188, 167, 228, 146, 198, 104, 202, 9, 205, 211]", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 47687 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "postBalances": [ + 99999985000, + 25447680, + 1141440 + ], + "postTokenBalances": [], + "preBalances": [ + 99999990000, + 25447680, + 1141440 + ], + "preTokenBalances": [], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 607, + "transaction": { + "message": { + "accountKeys": [ + "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 1, + "numRequiredSignatures": 1 + }, + "instructions": [ + { + "accounts": [ + 0, + 1 + ], + "data": "tkRdUtxp2aFXkrWEGCDmNQxX6P6uUCt1RvHPWeX3w2cdz37SU815J2ZprZWESkTdL5uxMVAQjSW4mKTzC8coQG8AGwm3Mw3svG2AyGUuUHBuhpWynrMTd9sXD8FJDDtQFpPswxJk9MSZMqKUZnmXd59xVdCQEL4gxL1uq", + "programIdIndex": 2, + "stackHeight": null + } + ], + "recentBlockhash": "9dwcaNuhuKZ7QdSDQvoRt4tY2JZU5oZyRd8hLHSKrFCZ" + }, + "signatures": [ + "5dpFTsscUKCGVQzL9bAUSuEE6yLXaf7d1wMjZa7RLqvtSUtAdfcdxQHNsbfcS2Sfzu4zBVxMJC2KWzuaUUbg1ZGk" + ] + } +} \ No newline at end of file diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW.json new file mode 100644 index 0000000000..decc0edd2c --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW.json @@ -0,0 +1,244 @@ +{ + "blockTime": 1740071109, + "meta": { + "computeUnitsConsumed": 139958, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "accounts": [ + 2, + 6, + 3, + 1 + ], + "data": "gvPShZQhKrzGM", + "programIdIndex": 8, + "stackHeight": 2 + }, + { + "accounts": [ + 4, + 3, + 6, + 1, + 11, + 5, + 8, + 10 + ], + "data": "Qj4no588tu2RksabYK3y3pQYzHBqRCDjYKJAKTpJmARF93XWomZBGVyY1UVNe", + "programIdIndex": 7, + "stackHeight": 2 + }, + { + "accounts": [ + 3, + 6, + 5, + 4 + ], + "data": "gX37MVsfGUBn5", + "programIdIndex": 8, + "stackHeight": 3 + } + ] + } + ], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: ExecuteSplToken", + "Program log: Computed message hash: [127, 87, 107, 159, 251, 178, 101, 24, 62, 24, 74, 254, 65, 174, 173, 71, 102, 52, 208, 68, 228, 95, 93, 44, 249, 190, 171, 234, 176, 226, 53, 68]", + "Program log: Recovered address [242, 236, 163, 253, 90, 21, 46, 181, 185, 206, 188, 167, 228, 146, 198, 104, 202, 9, 205, 211]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6201 of 128629 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program 8iUjRRhUCn8BjrvsWPfj8mguTe9L81ES4oAUApiF8JFC invoke [2]", + "Program log: Instruction: OnCall", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6201 of 108415 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program log: On call executed with amount 1000000, sender [16, 63, 217, 34, 79, 0, 206, 48, 19, 233, 86, 41, 229, 45, 252, 49, 216, 5, 214, 141] and message hello", + "Program 8iUjRRhUCn8BjrvsWPfj8mguTe9L81ES4oAUApiF8JFC consumed 23863 of 119713 compute units", + "Program 8iUjRRhUCn8BjrvsWPfj8mguTe9L81ES4oAUApiF8JFC success", + "Program log: Execute SPL done: amount = 1000000, decimals = 6, recipient = 9d5kAQjewKUki56CfoAnX3CbPt8wskHXJgNqkm82BeNo, mint = B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz, pda = 9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 139958 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "postBalances": [ + 99999970000, + 40942680, + 2039280, + 2039280, + 1447680, + 2039280, + 1461600, + 1141440, + 929020800, + 731913600, + 1, + 999946996960, + 1141440 + ], + "postTokenBalances": [ + { + "accountIndex": 2, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "34900000", + "decimals": 6, + "uiAmount": 34.9, + "uiAmountString": "34.9" + } + }, + { + "accountIndex": 3, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "9d5kAQjewKUki56CfoAnX3CbPt8wskHXJgNqkm82BeNo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "500000", + "decimals": 6, + "uiAmount": 0.5, + "uiAmountString": "0.5" + } + }, + { + "accountIndex": 5, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "99999964600000", + "decimals": 6, + "uiAmount": 99999964.6, + "uiAmountString": "99999964.6" + } + } + ], + "preBalances": [ + 99999975000, + 40942680, + 2039280, + 2039280, + 1447680, + 2039280, + 1461600, + 1141440, + 929020800, + 731913600, + 1, + 999946996960, + 1141440 + ], + "preTokenBalances": [ + { + "accountIndex": 2, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "35900000", + "decimals": 6, + "uiAmount": 35.9, + "uiAmountString": "35.9" + } + }, + { + "accountIndex": 3, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "9d5kAQjewKUki56CfoAnX3CbPt8wskHXJgNqkm82BeNo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "0", + "decimals": 6, + "uiAmount": null, + "uiAmountString": "0" + } + }, + { + "accountIndex": 5, + "mint": "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "owner": "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "99999964100000", + "decimals": 6, + "uiAmount": 99999964.1, + "uiAmountString": "99999964.1" + } + } + ], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 1163, + "transaction": { + "message": { + "accountKeys": [ + "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "G3ov8DJFuqnzb4oSDejp3arUnJ5S4sYgPB5PVhHM3xgs", + "ERncAqevWezEB5HuTRF6mCrnJSRnAUwGVAQKBd3yq4NJ", + "9d5kAQjewKUki56CfoAnX3CbPt8wskHXJgNqkm82BeNo", + "HmHyB8mx19rRLRjpgwcJfNNU2QDx9JkvkSto2fqnxqz4", + "B4QuaxoateLvrWA48MvoYXf1tvBGTErbj6C6t4dyHKTz", + "8iUjRRhUCn8BjrvsWPfj8mguTe9L81ES4oAUApiF8JFC", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "11111111111111111111111111111111", + "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 7, + "numRequiredSignatures": 1 + }, + "instructions": [ + { + "accounts": [ + 0, + 1, + 2, + 6, + 7, + 4, + 3, + 8, + 9, + 10, + 4, + 3, + 6, + 1, + 11, + 5, + 8, + 10 + ], + "data": "28gRk4vboxttdNAEPV6roTvKULX96fmierULcuyofq8NHFmyKatnfoeBCD2pNk9ZWWiVBqu1WRSKRAqgkNrW1fhopFkFjjCM4DdyA6ZtDysa33Sq8K6fknMHMQPDR5BFCK6PL99MTtjgkzJvsHex71ARRG4RowGggapkvR5t876DaS8dzS7Bc44Ai2Z6U7sM31BsQ2Sov7GBUMm", + "programIdIndex": 12, + "stackHeight": null + } + ], + "recentBlockhash": "8ZfeVR14evcAbnjV8upbHRg11Xkh2ZKwLFCXjinUsFLR" + }, + "signatures": [ + "d3WvqtwFws9yftpxSrmwXqb48ZbBVjvxz34zY5Mo9TxaAPxsudPa68nDXZeShvK8UqtM84TgGfpdrgeX65q5WCW" + ] + } +} \ No newline at end of file From 1a9975c9d4c8d1e44409840008c7db43f53d39d5 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 24 Feb 2025 10:25:27 -0800 Subject: [PATCH 06/22] feat(zetacore)!: ensure cctx list is sorted by creation time (#3548) * feat(zeatcore): ensure cctx list is sorted by created_timestamp * remove key * Use autoincrementing index * minimal test coverage * use valid index must also sort results to compare now * separate file * import/export and another test * fix tests, add unordered flag, max page size * test private function and feedback * order descending by default * changelog * fix old cctx tests --- changelog.md | 7 +- docs/openapi/openapi.swagger.yaml | 7 + .../zetacore/crosschain/genesis.proto | 1 + .../zetachain/zetacore/crosschain/query.proto | 3 + .../zetacore/crosschain/genesis_pb.d.ts | 5 + .../zetacore/crosschain/query_pb.d.ts | 8 + x/crosschain/genesis.go | 4 + x/crosschain/genesis_test.go | 1 + x/crosschain/keeper/cctx.go | 9 +- x/crosschain/keeper/cctx_counter.go | 62 ++++ x/crosschain/keeper/cctx_counter_test.go | 23 ++ x/crosschain/keeper/cctx_test.go | 19 +- .../keeper/export_private_functions_test.go | 4 + x/crosschain/keeper/grpc_query_cctx.go | 43 ++- x/crosschain/keeper/grpc_query_cctx_test.go | 57 +++ x/crosschain/types/cctx.go | 12 + x/crosschain/types/genesis.pb.go | 106 ++++-- x/crosschain/types/keys.go | 6 + x/crosschain/types/query.pb.go | 344 ++++++++++-------- 19 files changed, 519 insertions(+), 202 deletions(-) create mode 100644 x/crosschain/keeper/cctx_counter.go create mode 100644 x/crosschain/keeper/cctx_counter_test.go diff --git a/changelog.md b/changelog.md index 1aa731b7c2..ca9fd2d622 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +* The CCTX List RPC (`/zeta-chain/crosschain/cctx`) will now return CCTXs ordered by creation time. CCTXs from before the upgrade will not be displayed. Use the `?unordered=true` parameter to revert to the old behavior. + ### Features * [3414](https://github.com/zeta-chain/node/pull/3414) - support advanced abort workflow (onAbort) @@ -12,12 +16,13 @@ * [3469](https://github.com/zeta-chain/node/pull/3469) - add `MsgRemoveInboundTracker` to remove inbound trackers. This message can be triggered by the emergency policy. * [3450](https://github.com/zeta-chain/node/pull/3450) - integrate SOL withdraw and call * [3538](https://github.com/zeta-chain/node/pull/3538) - implement `MsgUpdateOperationalChainParams` for updating operational-related chain params with operational policy -* [3534] (https://github.com/zeta-chain/node/pull/3534) - Add Sui deposit & depositAndCall +* [3534](https://github.com/zeta-chain/node/pull/3534) - Add Sui deposit & depositAndCall * [3541](https://github.com/zeta-chain/node/pull/3541) - implement `MsgUpdateZRC20Name` to update the name or symbol of a ZRC20 token * [3439](https://github.com/zeta-chain/node/pull/3439) - use protocol contracts V2 with TON deposits * [3520](https://github.com/zeta-chain/node/pull/3520) - integrate SPL withdraw and call * [3527](https://github.com/zeta-chain/node/pull/3527) - integrate SOL/SPL withdraw and call revert * [3522](https://github.com/zeta-chain/node/pull/3522) - add `MsgDisableFastConfirmation` to disable fast confirmation. This message can be triggered by the emergency policy. +* [3548](https://github.com/zeta-chain/node/pull/3548) - ensure cctx list is sorted by creation time ### Refactor diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 9d91f00409..dc7a84a1e4 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -28197,6 +28197,13 @@ paths: in: query required: false type: boolean + - name: unordered + description: |- + Bulk dump all CCTX without ordering. + This is useful for initializing a CCTX indexer. + in: query + required: false + type: boolean tags: - Query /zeta-chain/crosschain/cctx/{chainID}/{nonce}: diff --git a/proto/zetachain/zetacore/crosschain/genesis.proto b/proto/zetachain/zetacore/crosschain/genesis.proto index 97aa1d8dba..da3c9806fa 100644 --- a/proto/zetachain/zetacore/crosschain/genesis.proto +++ b/proto/zetachain/zetacore/crosschain/genesis.proto @@ -26,4 +26,5 @@ message GenesisState { ZetaAccounting zeta_accounting = 12 [ (gogoproto.nullable) = false ]; repeated string FinalizedInbounds = 16; RateLimiterFlags rate_limiter_flags = 17 [ (gogoproto.nullable) = false ]; + uint64 counter = 18; } diff --git a/proto/zetachain/zetacore/crosschain/query.proto b/proto/zetachain/zetacore/crosschain/query.proto index bfbd89ea8e..fc0fe34d85 100644 --- a/proto/zetachain/zetacore/crosschain/query.proto +++ b/proto/zetachain/zetacore/crosschain/query.proto @@ -325,6 +325,9 @@ message QueryGetCctxResponse { CrossChainTx CrossChainTx = 1; } message QueryAllCctxRequest { cosmos.base.query.v1beta1.PageRequest pagination = 1; + // Bulk dump all CCTX without ordering. + // This is useful for initializing a CCTX indexer. + bool unordered = 2; } message QueryAllCctxResponse { diff --git a/typescript/zetachain/zetacore/crosschain/genesis_pb.d.ts b/typescript/zetachain/zetacore/crosschain/genesis_pb.d.ts index 35b98487a7..6b89f5be3b 100644 --- a/typescript/zetachain/zetacore/crosschain/genesis_pb.d.ts +++ b/typescript/zetachain/zetacore/crosschain/genesis_pb.d.ts @@ -64,6 +64,11 @@ export declare class GenesisState extends Message { */ rateLimiterFlags?: RateLimiterFlags; + /** + * @generated from field: uint64 counter = 18; + */ + counter: bigint; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/typescript/zetachain/zetacore/crosschain/query_pb.d.ts b/typescript/zetachain/zetacore/crosschain/query_pb.d.ts index 7b0766beca..fad553d76f 100644 --- a/typescript/zetachain/zetacore/crosschain/query_pb.d.ts +++ b/typescript/zetachain/zetacore/crosschain/query_pb.d.ts @@ -769,6 +769,14 @@ export declare class QueryAllCctxRequest extends Message { */ pagination?: PageRequest; + /** + * Bulk dump all CCTX without ordering. + * This is useful for initializing a CCTX indexer. + * + * @generated from field: bool unordered = 2; + */ + unordered: boolean; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 0f6856d559..41999fa0e6 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -55,6 +55,8 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } k.SetRateLimiterFlags(ctx, genState.RateLimiterFlags) + + k.SetCctxCounter(ctx, genState.Counter) } // ExportGenesis returns the crosschain module's exported genesis. @@ -97,5 +99,7 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis.RateLimiterFlags = rateLimiterFlags } + genesis.Counter = k.GetCctxCounter(ctx) + return &genesis } diff --git a/x/crosschain/genesis_test.go b/x/crosschain/genesis_test.go index 11aa8964b9..f63810d5ab 100644 --- a/x/crosschain/genesis_test.go +++ b/x/crosschain/genesis_test.go @@ -51,6 +51,7 @@ func TestGenesis(t *testing.T) { sample.InboundHashToCctx(t, "0x2"), }, RateLimiterFlags: sample.RateLimiterFlags(), + Counter: 1, } // Init and export diff --git a/x/crosschain/keeper/cctx.go b/x/crosschain/keeper/cctx.go index aee4600b97..c0ece67848 100644 --- a/x/crosschain/keeper/cctx.go +++ b/x/crosschain/keeper/cctx.go @@ -103,7 +103,14 @@ func (k Keeper) SetCrossChainTx(ctx sdk.Context, cctx types.CrossChainTx) { p := types.KeyPrefix(fmt.Sprintf("%s", types.CCTXKey)) store := prefix.NewStore(ctx.KVStore(k.storeKey), p) b := k.cdc.MustMarshal(&cctx) - store.Set(types.KeyPrefix(cctx.Index), b) + cctxIndex := types.KeyPrefix(cctx.Index) + + isUpdate := store.Has(cctxIndex) + store.Set(cctxIndex, b) + + if !isUpdate { + k.setCctxCounterIndex(ctx, cctx) + } } // GetCrossChainTx returns a cctx from its index diff --git a/x/crosschain/keeper/cctx_counter.go b/x/crosschain/keeper/cctx_counter.go new file mode 100644 index 0000000000..979ed5e353 --- /dev/null +++ b/x/crosschain/keeper/cctx_counter.go @@ -0,0 +1,62 @@ +package keeper + +import ( + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +func (k Keeper) getCounterValueStore(ctx sdk.Context) prefix.Store { + return prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.CounterValueKey)) +} + +func (k Keeper) getCounterIndexStore(ctx sdk.Context) prefix.Store { + return prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.CounterIndexKey)) +} + +// GetCctxCounter retrieves the current counter value +// +// Will return 0 if the counter has never been set +func (k Keeper) GetCctxCounter(ctx sdk.Context) uint64 { + store := k.getCounterValueStore(ctx) + storedCounter := store.Get([]byte(types.CounterValueKey)) + + return sdk.BigEndianToUint64(storedCounter) +} + +// SetCctxCounter updates the current counter value +// +// This should only be used in setCctxCounterIndex and in state import +func (k Keeper) SetCctxCounter(ctx sdk.Context, val uint64) { + store := k.getCounterValueStore(ctx) + store.Set([]byte(types.CounterValueKey), sdk.Uint64ToBigEndian(val)) +} + +// getNextCctxCounter retrieves and increments the counter for ordering +func (k Keeper) getNextCctxCounter(ctx sdk.Context) uint64 { + storedCounter := k.GetCctxCounter(ctx) + nextCounter := storedCounter + 1 + k.SetCctxCounter(ctx, nextCounter) + return nextCounter +} + +// setCctxCounterIndex sets a new CCTX in the counter index +// +// note that we use the raw bytes in the index rather than the hex encoded bytes +// like in the main store +func (k Keeper) setCctxCounterIndex(ctx sdk.Context, cctx types.CrossChainTx) { + counterIndexStore := k.getCounterIndexStore(ctx) + nextCounter := k.getNextCctxCounter(ctx) + + cctxIndex, err := cctx.GetCCTXIndexBytes() + if err != nil { + k.Logger(ctx).Error("get cctx index bytes", "err", err) + return + } + + // must use big endian so most significant bytes are first for sortability + // we binary OR the counter so it sorts in descending rather than ascending order by default + nextCounterBytes := sdk.Uint64ToBigEndian(^nextCounter) + counterIndexStore.Set(nextCounterBytes, cctxIndex[:]) +} diff --git a/x/crosschain/keeper/cctx_counter_test.go b/x/crosschain/keeper/cctx_counter_test.go new file mode 100644 index 0000000000..8e8174bb2b --- /dev/null +++ b/x/crosschain/keeper/cctx_counter_test.go @@ -0,0 +1,23 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + testkeeper "github.com/zeta-chain/node/testutil/keeper" +) + +func TestCounter(t *testing.T) { + keeper, ctx, _, _ := testkeeper.CrosschainKeeper(t) + initialCounter := keeper.GetCctxCounter(ctx) + require.Zero(t, initialCounter) + + nextVal := keeper.GetNextCctxCounter(ctx) + require.Greater(t, nextVal, initialCounter) + require.Equal(t, nextVal, keeper.GetCctxCounter(ctx)) + + // also test direct set + nextVal += 1 + keeper.SetCctxCounter(ctx, nextVal) + require.Equal(t, nextVal, keeper.GetCctxCounter(ctx)) +} diff --git a/x/crosschain/keeper/cctx_test.go b/x/crosschain/keeper/cctx_test.go index 29afecb6a8..ba62d0d6c2 100644 --- a/x/crosschain/keeper/cctx_test.go +++ b/x/crosschain/keeper/cctx_test.go @@ -3,6 +3,8 @@ package keeper_test import ( "fmt" "math/rand" + "slices" + "strings" "testing" "cosmossdk.io/math" @@ -85,7 +87,7 @@ func createNCctx(keeper *keeper.Keeper, ctx sdk.Context, n int, tssPubkey string items[i].InboundParams.Amount = math.OneUint() items[i].ZetaFees = math.OneUint() - items[i].Index = fmt.Sprintf("%d", i) + items[i].Index = sample.GetCctxIndexFromString(fmt.Sprintf("%d", i)) items[i].RevertOptions = types.NewEmptyRevertOptions() keeper.SaveCCTXUpdate(ctx, items[i], tssPubkey) @@ -174,17 +176,21 @@ func TestCCTXs(t *testing.T) { } } +func compareCctx(l types.CrossChainTx, r types.CrossChainTx) int { + return strings.Compare(l.Index, r.Index) +} + func TestCCTXGetAll(t *testing.T) { keeper, ctx, _, zk := keepertest.CrosschainKeeper(t) tss := sample.Tss() zk.ObserverKeeper.SetTSS(ctx, tss) items := createNCctx(keeper, ctx, 10, tss.TssPubkey) cctx := keeper.GetAllCrossChainTx(ctx) - c := make([]types.CrossChainTx, len(cctx)) - for i, val := range cctx { - c[i] = val - } - require.Equal(t, items, c) + + slices.SortFunc(items, compareCctx) + slices.SortFunc(cctx, compareCctx) + + require.Equal(t, items, cctx) } // Querier Tests @@ -247,6 +253,7 @@ func TestCCTXQueryPaginated(t *testing.T) { Offset: offset, Limit: limit, CountTotal: total, + Reverse: true, }, } } diff --git a/x/crosschain/keeper/export_private_functions_test.go b/x/crosschain/keeper/export_private_functions_test.go index 0611ed995d..7d79695d13 100644 --- a/x/crosschain/keeper/export_private_functions_test.go +++ b/x/crosschain/keeper/export_private_functions_test.go @@ -18,3 +18,7 @@ func (k Keeper) UpdateInboundHashToCCTX(ctx sdk.Context, cctx types.CrossChainTx func (k Keeper) SetNonceToCCTX(ctx sdk.Context, cctx types.CrossChainTx, tssPubkey string) { k.setNonceToCCTX(ctx, cctx, tssPubkey) } + +func (k Keeper) GetNextCctxCounter(ctx sdk.Context) uint64 { + return k.getNextCctxCounter(ctx) +} diff --git a/x/crosschain/keeper/grpc_query_cctx.go b/x/crosschain/keeper/grpc_query_cctx.go index 80d27c7aea..01ee36c458 100644 --- a/x/crosschain/keeper/grpc_query_cctx.go +++ b/x/crosschain/keeper/grpc_query_cctx.go @@ -22,6 +22,7 @@ const ( MaxLookbackNonce = 1000 DefaultPageSize = 100 + MaxPageSize = 1000 ) func (k Keeper) ZetaAccounting( @@ -42,11 +43,11 @@ func (k Keeper) CctxAll(c context.Context, req *types.QueryAllCctxRequest) (*typ if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") } - var sends []*types.CrossChainTx ctx := sdk.UnwrapSDKContext(c) store := ctx.KVStore(k.storeKey) - sendStore := prefix.NewStore(store, types.KeyPrefix(types.CCTXKey)) + cctxStore := prefix.NewStore(store, types.KeyPrefix(types.CCTXKey)) + counterStore := k.getCounterIndexStore(ctx) if req.Pagination == nil { req.Pagination = &query.PageRequest{} @@ -54,15 +55,37 @@ func (k Keeper) CctxAll(c context.Context, req *types.QueryAllCctxRequest) (*typ if req.Pagination.Limit == 0 { req.Pagination.Limit = DefaultPageSize } + if req.Pagination.Limit > MaxPageSize { + req.Pagination.Limit = MaxPageSize + } - pageRes, err := query.Paginate(sendStore, req.Pagination, func(_ []byte, value []byte) error { - var send types.CrossChainTx - if err := k.cdc.Unmarshal(value, &send); err != nil { - return err - } - sends = append(sends, &send) - return nil - }) + var sends []*types.CrossChainTx + var pageRes *query.PageResponse + var err error + if req.Unordered { + pageRes, err = query.Paginate(cctxStore, req.Pagination, func(_ []byte, value []byte) error { + var send types.CrossChainTx + if err := k.cdc.Unmarshal(value, &send); err != nil { + return err + } + sends = append(sends, &send) + return nil + }) + } else { + pageRes, err = query.Paginate(counterStore, req.Pagination, func(_ []byte, value []byte) error { + cctxIndex, err := types.GetCctxIndexFromArbitraryBytes(value) + if err != nil { + return err + } + var cctx types.CrossChainTx + cctxBytes := cctxStore.Get(types.KeyPrefix(cctxIndex)) + if err := k.cdc.Unmarshal(cctxBytes, &cctx); err != nil { + return err + } + sends = append(sends, &cctx) + return nil + }) + } if err != nil { return nil, status.Error(codes.Internal, err.Error()) diff --git a/x/crosschain/keeper/grpc_query_cctx_test.go b/x/crosschain/keeper/grpc_query_cctx_test.go index 8d7043d9f8..cbda6252b7 100644 --- a/x/crosschain/keeper/grpc_query_cctx_test.go +++ b/x/crosschain/keeper/grpc_query_cctx_test.go @@ -2,6 +2,8 @@ package keeper_test import ( "fmt" + "slices" + "strings" "testing" sdkmath "cosmossdk.io/math" @@ -291,6 +293,18 @@ func TestKeeper_CctxByNonce(t *testing.T) { }) } +func assertCctxIndexEqual(t *testing.T, expectedCctxs []*types.CrossChainTx, cctxs []*types.CrossChainTx) { + t.Helper() + require.Equal(t, len(expectedCctxs), len(cctxs), "slice lengths not equal") + for i, expectedCctx := range expectedCctxs { + require.Equal(t, expectedCctx.Index, cctxs[i].Index, "index missmatch at %v", i) + } +} + +func sortByIndex(l *types.CrossChainTx, r *types.CrossChainTx) int { + return strings.Compare(l.Index, r.Index) +} + func TestKeeper_CctxAll(t *testing.T) { t.Run("empty request", func(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeper(t) @@ -326,4 +340,47 @@ func TestKeeper_CctxAll(t *testing.T) { require.NoError(t, err) require.Len(t, res.CrossChainTx, testPageSize) }) + + t.Run("basic descending ordering", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + chainID := getValidEthChainID() + tss := sample.Tss() + zk.ObserverKeeper.SetTSS(ctx, tss) + createdCctx := createCctxWithNonceRange(t, ctx, *k, 0, 10, chainID, tss, zk) + + res, err := k.CctxAll(ctx, &types.QueryAllCctxRequest{}) + require.NoError(t, err) + slices.Reverse(createdCctx) + assertCctxIndexEqual(t, createdCctx, res.CrossChainTx) + + // also assert unordered query return same number of results + resUnordered, err := k.CctxAll(ctx, &types.QueryAllCctxRequest{ + Unordered: true, + }) + require.NoError(t, err) + require.Len(t, res.CrossChainTx, len(resUnordered.CrossChainTx)) + }) + + t.Run("basic ascending ordering", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + chainID := getValidEthChainID() + tss := sample.Tss() + zk.ObserverKeeper.SetTSS(ctx, tss) + createdCctx := createCctxWithNonceRange(t, ctx, *k, 0, 10, chainID, tss, zk) + + res, err := k.CctxAll(ctx, &types.QueryAllCctxRequest{ + Pagination: &query.PageRequest{ + Reverse: true, + }, + }) + require.NoError(t, err) + assertCctxIndexEqual(t, createdCctx, res.CrossChainTx) + + // also assert unordered query return same number of results + resUnordered, err := k.CctxAll(ctx, &types.QueryAllCctxRequest{ + Unordered: true, + }) + require.NoError(t, err) + require.Len(t, res.CrossChainTx, len(resUnordered.CrossChainTx)) + }) } diff --git a/x/crosschain/types/cctx.go b/x/crosschain/types/cctx.go index 86f371e594..b7d315d7f5 100644 --- a/x/crosschain/types/cctx.go +++ b/x/crosschain/types/cctx.go @@ -269,6 +269,18 @@ func GetCctxIndexFromBytes(sendHash [32]byte) string { return fmt.Sprintf("0x%s", hex.EncodeToString(sendHash[:])) } +// GetCctxIndexFromArbitraryBytes converts an arbitrary byte slice to a CCTX index string. +// Returns an error if the input slice is less than 32 bytes. +func GetCctxIndexFromArbitraryBytes(sendHash []byte) (string, error) { + if len(sendHash) < 32 { + return "", fmt.Errorf("input byte slice length %d is less than required 32 bytes", len(sendHash)) + } + + var indexBytes [32]byte + copy(indexBytes[:], sendHash[:32]) + return GetCctxIndexFromBytes(indexBytes), nil +} + // NewCCTX creates a new CCTX from a MsgVoteInbound message and a TSS pubkey. // It also validates the created cctx func NewCCTX(ctx sdk.Context, msg MsgVoteInbound, tssPubkey string) (CrossChainTx, error) { diff --git a/x/crosschain/types/genesis.pb.go b/x/crosschain/types/genesis.pb.go index d1dbc488e1..27b71e60ce 100644 --- a/x/crosschain/types/genesis.pb.go +++ b/x/crosschain/types/genesis.pb.go @@ -34,6 +34,7 @@ type GenesisState struct { ZetaAccounting ZetaAccounting `protobuf:"bytes,12,opt,name=zeta_accounting,json=zetaAccounting,proto3" json:"zeta_accounting"` FinalizedInbounds []string `protobuf:"bytes,16,rep,name=FinalizedInbounds,proto3" json:"FinalizedInbounds,omitempty"` RateLimiterFlags RateLimiterFlags `protobuf:"bytes,17,opt,name=rate_limiter_flags,json=rateLimiterFlags,proto3" json:"rate_limiter_flags"` + Counter uint64 `protobuf:"varint,18,opt,name=counter,proto3" json:"counter,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -132,6 +133,13 @@ func (m *GenesisState) GetRateLimiterFlags() RateLimiterFlags { return RateLimiterFlags{} } +func (m *GenesisState) GetCounter() uint64 { + if m != nil { + return m.Counter + } + return 0 +} + func init() { proto.RegisterType((*GenesisState)(nil), "zetachain.zetacore.crosschain.GenesisState") } @@ -141,40 +149,41 @@ func init() { } var fileDescriptor_547615497292ea23 = []byte{ - // 515 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0xc7, 0x5b, 0x06, 0x83, 0x79, 0x05, 0x36, 0x6f, 0x48, 0x51, 0x25, 0x42, 0xc5, 0x85, 0x49, - 0xa3, 0x09, 0xda, 0x00, 0x71, 0x65, 0x95, 0xd6, 0x21, 0x2a, 0x01, 0xa1, 0xa7, 0x09, 0xc9, 0xb8, - 0xae, 0x97, 0x58, 0xcb, 0xe2, 0x2a, 0x7e, 0x95, 0x4a, 0x3f, 0x05, 0x9f, 0x88, 0xf3, 0x8e, 0x3b, - 0x72, 0x42, 0xa8, 0xfd, 0x22, 0xc8, 0x8e, 0x57, 0x9a, 0xb6, 0x4a, 0x7a, 0x7b, 0x7a, 0x79, 0xff, - 0xff, 0xef, 0x29, 0x7f, 0x3f, 0x74, 0x38, 0xe6, 0x40, 0x59, 0x44, 0x45, 0xe2, 0x9b, 0x4a, 0xa6, - 0xdc, 0x67, 0xa9, 0x54, 0x2a, 0xeb, 0x85, 0x3c, 0xe1, 0x4a, 0x28, 0x6f, 0x90, 0x4a, 0x90, 0xf8, - 0xe9, 0x6c, 0xd8, 0xbb, 0x1d, 0xf6, 0xfe, 0x0f, 0xd7, 0x8f, 0x8a, 0xbd, 0x4c, 0x49, 0x4c, 0x4d, - 0x60, 0x94, 0x59, 0xd6, 0x9b, 0x25, 0x7c, 0xaa, 0xc8, 0x20, 0x15, 0x8c, 0xdb, 0xf1, 0x77, 0xc5, - 0xe3, 0x22, 0xe9, 0xc9, 0x61, 0xd2, 0x27, 0x11, 0x55, 0x11, 0x01, 0x49, 0x18, 0x9b, 0x81, 0x8e, - 0xd7, 0x53, 0x42, 0x4a, 0xd9, 0x25, 0x4f, 0xad, 0xe8, 0x4d, 0xb1, 0x28, 0xa6, 0x0a, 0x48, 0x2f, - 0x96, 0xec, 0x92, 0x44, 0x5c, 0x84, 0x11, 0x58, 0xd9, 0xeb, 0x62, 0x99, 0x1c, 0xc2, 0x2a, 0xd8, - 0xdb, 0x62, 0x55, 0x4a, 0x81, 0x93, 0x58, 0x5c, 0x09, 0xe0, 0x29, 0xb9, 0x88, 0x69, 0x68, 0x53, - 0xa9, 0xef, 0x87, 0x32, 0x94, 0xa6, 0xf4, 0x75, 0x95, 0x75, 0x9f, 0xff, 0xda, 0x44, 0xb5, 0x76, - 0x96, 0xde, 0x57, 0xa0, 0xc0, 0xf1, 0x05, 0xda, 0xbb, 0x05, 0x77, 0x33, 0x6e, 0x47, 0x28, 0x70, - 0xee, 0x34, 0x36, 0x0e, 0xb6, 0x8f, 0x3c, 0xaf, 0x30, 0x5a, 0xef, 0x53, 0x5e, 0x79, 0x72, 0xf7, - 0xfa, 0xcf, 0xb3, 0x4a, 0xb0, 0xca, 0x10, 0x7f, 0x44, 0xb5, 0x90, 0xaa, 0xcf, 0x3a, 0x34, 0x03, - 0xb8, 0x67, 0x00, 0x2f, 0x4a, 0x00, 0x6d, 0x2b, 0x09, 0x72, 0x62, 0xfc, 0x05, 0x3d, 0x6c, 0xe9, - 0xa1, 0x96, 0x1e, 0xea, 0x8e, 0x94, 0x73, 0xdf, 0xb8, 0x1d, 0x96, 0xb8, 0xcd, 0x6b, 0x82, 0xbc, - 0x03, 0xfe, 0x8e, 0xf6, 0x74, 0x6e, 0x27, 0x3a, 0xb6, 0x33, 0x93, 0x9a, 0x59, 0xf3, 0xc1, 0x5a, - 0xff, 0xa1, 0x93, 0x57, 0x06, 0xab, 0xac, 0x70, 0x8c, 0x9e, 0xd8, 0xe7, 0x74, 0x46, 0x55, 0xd4, - 0x95, 0x2d, 0x06, 0x23, 0xc3, 0xd8, 0x32, 0x8c, 0x57, 0x25, 0x8c, 0x0f, 0x8b, 0x5a, 0xfb, 0xb7, - 0x57, 0x9b, 0x62, 0x8e, 0xf6, 0x17, 0x1e, 0x2f, 0x89, 0x35, 0x6c, 0xdb, 0xc0, 0x9a, 0xeb, 0xc1, - 0xf2, 0xb9, 0x62, 0x91, 0x2c, 0xc5, 0xfa, 0x0d, 0x3d, 0xd6, 0x7a, 0x42, 0x19, 0x93, 0xc3, 0x04, - 0x44, 0x12, 0x3a, 0xb5, 0x46, 0x75, 0x0d, 0xc2, 0x39, 0x07, 0xfa, 0x7e, 0x26, 0xb2, 0x84, 0x47, - 0xe3, 0x5c, 0x17, 0xbf, 0x44, 0xbb, 0xa7, 0x22, 0xa1, 0xb1, 0x18, 0xf3, 0xbe, 0x5d, 0x49, 0x39, - 0x3b, 0x8d, 0x8d, 0x83, 0xad, 0x60, 0xf9, 0x03, 0x66, 0x08, 0x2f, 0x5f, 0x83, 0xb3, 0x6b, 0xd6, - 0xf1, 0x4b, 0xd6, 0x09, 0x28, 0xf0, 0x4e, 0xa6, 0x3b, 0xd5, 0x32, 0xbb, 0xd0, 0x4e, 0xba, 0xd8, - 0x6f, 0x5f, 0x4f, 0xdc, 0xea, 0xcd, 0xc4, 0xad, 0xfe, 0x9d, 0xb8, 0xd5, 0x9f, 0x53, 0xb7, 0x72, - 0x33, 0x75, 0x2b, 0xbf, 0xa7, 0x6e, 0xe5, 0xbc, 0x19, 0x0a, 0x88, 0x86, 0x3d, 0x8f, 0xc9, 0x2b, - 0x73, 0xa9, 0xcd, 0xec, 0x40, 0x13, 0xd9, 0xe7, 0xfe, 0x68, 0xfe, 0x64, 0xe1, 0xc7, 0x80, 0xab, - 0xde, 0xa6, 0x39, 0xc8, 0xe3, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xde, 0x67, 0x86, 0x4a, 0x6b, - 0x05, 0x00, 0x00, + // 529 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0x41, 0x6f, 0x12, 0x41, + 0x14, 0xc7, 0xc1, 0xd6, 0xd6, 0x4e, 0xab, 0xb6, 0xd3, 0x9a, 0x6c, 0x48, 0x5c, 0x89, 0x17, 0x49, + 0x2a, 0x8b, 0x69, 0xd5, 0x78, 0x15, 0x92, 0x52, 0x23, 0x89, 0xba, 0x72, 0x6a, 0x4c, 0xc6, 0x61, + 0x98, 0xee, 0x4e, 0xba, 0xdd, 0x21, 0x33, 0x8f, 0x04, 0xf9, 0x14, 0x7e, 0xac, 0x1e, 0x7b, 0xec, + 0xc9, 0x18, 0xf8, 0x22, 0x66, 0x67, 0x07, 0xec, 0x02, 0xd9, 0xe5, 0xf6, 0x78, 0xbc, 0xff, 0xff, + 0xf7, 0xb2, 0xff, 0x79, 0xe8, 0x78, 0xcc, 0x81, 0xb2, 0x90, 0x8a, 0xb8, 0x61, 0x2a, 0xa9, 0x78, + 0x83, 0x29, 0xa9, 0x75, 0xda, 0x0b, 0x78, 0xcc, 0xb5, 0xd0, 0xde, 0x40, 0x49, 0x90, 0xf8, 0xf9, + 0x7c, 0xd8, 0x9b, 0x0d, 0x7b, 0xff, 0x87, 0x2b, 0x27, 0xf9, 0x5e, 0xa6, 0x24, 0xa6, 0x26, 0x30, + 0x4a, 0x2d, 0x2b, 0xf5, 0x02, 0x3e, 0xd5, 0x64, 0xa0, 0x04, 0xe3, 0x76, 0xfc, 0x43, 0xfe, 0xb8, + 0x88, 0x7b, 0x72, 0x18, 0xf7, 0x49, 0x48, 0x75, 0x48, 0x40, 0x12, 0xc6, 0xe6, 0xa0, 0xd3, 0xf5, + 0x94, 0xa0, 0x28, 0xbb, 0xe2, 0xca, 0x8a, 0xde, 0xe5, 0x8b, 0x22, 0xaa, 0x81, 0xf4, 0x22, 0xc9, + 0xae, 0x48, 0xc8, 0x45, 0x10, 0x82, 0x95, 0xbd, 0xcd, 0x97, 0xc9, 0x21, 0xac, 0x82, 0xbd, 0xcf, + 0x57, 0x29, 0x0a, 0x9c, 0x44, 0xe2, 0x5a, 0x00, 0x57, 0xe4, 0x32, 0xa2, 0x81, 0x4d, 0xa5, 0x72, + 0x14, 0xc8, 0x40, 0x9a, 0xb2, 0x91, 0x54, 0x69, 0xf7, 0xe5, 0xdd, 0x16, 0xda, 0x6b, 0xa7, 0xe9, + 0x7d, 0x07, 0x0a, 0x1c, 0x5f, 0xa2, 0xc3, 0x19, 0xb8, 0x9b, 0x72, 0x3b, 0x42, 0x83, 0xf3, 0xa0, + 0xba, 0x51, 0xdb, 0x3d, 0xf1, 0xbc, 0xdc, 0x68, 0xbd, 0x2f, 0x59, 0x65, 0x73, 0xf3, 0xe6, 0xcf, + 0x8b, 0x92, 0xbf, 0xca, 0x10, 0x7f, 0x46, 0x7b, 0x01, 0xd5, 0x5f, 0x93, 0xd0, 0x0c, 0xe0, 0xa1, + 0x01, 0xbc, 0x2a, 0x00, 0xb4, 0xad, 0xc4, 0xcf, 0x88, 0xf1, 0x37, 0xf4, 0xb8, 0x95, 0x0c, 0xb5, + 0x92, 0xa1, 0xee, 0x48, 0x3b, 0xdb, 0xc6, 0xed, 0xb8, 0xc0, 0xed, 0xbe, 0xc6, 0xcf, 0x3a, 0xe0, + 0x9f, 0xe8, 0x30, 0xc9, 0xad, 0x99, 0xc4, 0x76, 0x6e, 0x52, 0x33, 0x6b, 0x3e, 0x5a, 0xeb, 0x3b, + 0x74, 0xb2, 0x4a, 0x7f, 0x95, 0x15, 0x8e, 0xd0, 0x33, 0xfb, 0x9c, 0xce, 0xa9, 0x0e, 0xbb, 0xb2, + 0xc5, 0x60, 0x64, 0x18, 0x3b, 0x86, 0xf1, 0xa6, 0x80, 0xf1, 0x69, 0x51, 0x6b, 0xbf, 0xf6, 0x6a, + 0x53, 0xcc, 0xd1, 0xd1, 0xc2, 0xe3, 0x25, 0x51, 0x02, 0xdb, 0x35, 0xb0, 0xfa, 0x7a, 0xb0, 0x6c, + 0xae, 0x58, 0xc4, 0x4b, 0xb1, 0xfe, 0x40, 0x4f, 0x13, 0x3d, 0xa1, 0x8c, 0xc9, 0x61, 0x0c, 0x22, + 0x0e, 0x9c, 0xbd, 0x6a, 0x79, 0x0d, 0xc2, 0x05, 0x07, 0xfa, 0x71, 0x2e, 0xb2, 0x84, 0x27, 0xe3, + 0x4c, 0x17, 0xbf, 0x46, 0x07, 0x67, 0x22, 0xa6, 0x91, 0x18, 0xf3, 0xbe, 0x5d, 0x49, 0x3b, 0xfb, + 0xd5, 0x8d, 0xda, 0x8e, 0xbf, 0xfc, 0x07, 0x66, 0x08, 0x2f, 0x5f, 0x83, 0x73, 0x60, 0xd6, 0x69, + 0x14, 0xac, 0xe3, 0x53, 0xe0, 0x9d, 0x54, 0x77, 0x96, 0xc8, 0xec, 0x42, 0xfb, 0x6a, 0xa1, 0x8f, + 0x1d, 0xb4, 0x6d, 0xd6, 0xe3, 0xca, 0xc1, 0xd5, 0x72, 0x6d, 0xd3, 0x9f, 0xfd, 0x6c, 0xb6, 0x6f, + 0x26, 0x6e, 0xf9, 0x76, 0xe2, 0x96, 0xff, 0x4e, 0xdc, 0xf2, 0xef, 0xa9, 0x5b, 0xba, 0x9d, 0xba, + 0xa5, 0xbb, 0xa9, 0x5b, 0xba, 0xa8, 0x07, 0x02, 0xc2, 0x61, 0xcf, 0x63, 0xf2, 0xda, 0xdc, 0x70, + 0x3d, 0x3d, 0xdd, 0x58, 0xf6, 0x79, 0x63, 0x74, 0xff, 0x98, 0xe1, 0xd7, 0x80, 0xeb, 0xde, 0x96, + 0x39, 0xd5, 0xd3, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5a, 0x17, 0x12, 0x52, 0x85, 0x05, 0x00, + 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -197,6 +206,13 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Counter != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.Counter)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x90 + } { size, err := m.RateLimiterFlags.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -380,6 +396,9 @@ func (m *GenesisState) Size() (n int) { } l = m.RateLimiterFlags.Size() n += 2 + l + sovGenesis(uint64(l)) + if m.Counter != 0 { + n += 2 + sovGenesis(uint64(m.Counter)) + } return n } @@ -720,6 +739,25 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 18: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Counter", wireType) + } + m.Counter = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Counter |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index 26422c6101..3d75d3853c 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -43,6 +43,12 @@ const ( // NOTE: Send is the previous name of CCTX and is kept for backward compatibility CCTXKey = "Send-value-" + // CounterValueKey is a static key for storing the cctx counter key for ordering + CounterValueKey = "ctr-value" + + // CounterIndexKey is the prefix to use for the counter index + CounterIndexKey = "ctr-idx-" + LastBlockHeightKey = "LastBlockHeight-value-" FinalizedInboundsKey = "FinalizedInbounds-value-" diff --git a/x/crosschain/types/query.pb.go b/x/crosschain/types/query.pb.go index 42423f9381..fd3a5f8067 100644 --- a/x/crosschain/types/query.pb.go +++ b/x/crosschain/types/query.pb.go @@ -1391,6 +1391,9 @@ func (m *QueryGetCctxResponse) GetCrossChainTx() *CrossChainTx { type QueryAllCctxRequest struct { Pagination *query.PageRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"` + // Bulk dump all CCTX without ordering. + // This is useful for initializing a CCTX indexer. + Unordered bool `protobuf:"varint,2,opt,name=unordered,proto3" json:"unordered,omitempty"` } func (m *QueryAllCctxRequest) Reset() { *m = QueryAllCctxRequest{} } @@ -1433,6 +1436,13 @@ func (m *QueryAllCctxRequest) GetPagination() *query.PageRequest { return nil } +func (m *QueryAllCctxRequest) GetUnordered() bool { + if m != nil { + return m.Unordered + } + return false +} + type QueryAllCctxResponse struct { CrossChainTx []*CrossChainTx `protobuf:"bytes,1,rep,name=CrossChainTx,proto3" json:"CrossChainTx,omitempty"` Pagination *query.PageResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` @@ -2366,156 +2376,157 @@ func init() { } var fileDescriptor_d00cb546ea76908b = []byte{ - // 2372 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0xdb, 0x6f, 0xdc, 0xc6, - 0xf5, 0xf6, 0x68, 0x2d, 0x5f, 0x46, 0xb6, 0x64, 0x8d, 0x65, 0x4b, 0xd9, 0xd8, 0xb2, 0x43, 0xc7, - 0x96, 0x22, 0xff, 0xb4, 0x6b, 0x4b, 0x96, 0x7c, 0x4d, 0x6c, 0x5d, 0x6c, 0x59, 0x3f, 0xc8, 0xb6, - 0xb2, 0x10, 0xea, 0xc2, 0xbd, 0x10, 0x14, 0x77, 0xc2, 0x65, 0x43, 0x91, 0x9b, 0xe5, 0xac, 0xb5, - 0x8e, 0x20, 0xa0, 0x0d, 0xd0, 0x87, 0xbe, 0x15, 0x08, 0x8a, 0xbe, 0xf4, 0xb5, 0x68, 0x81, 0xf6, - 0x21, 0x0f, 0x45, 0x5e, 0x8a, 0x16, 0xe8, 0xdd, 0x68, 0x5a, 0xc0, 0x4d, 0x81, 0xa2, 0xe8, 0x43, - 0x91, 0xda, 0x45, 0xf3, 0xde, 0xbf, 0xa0, 0xe0, 0xf0, 0x70, 0x97, 0x77, 0xce, 0xae, 0xd6, 0x80, - 0xf2, 0x24, 0x92, 0x33, 0xe7, 0xcc, 0xf7, 0x9d, 0x33, 0x97, 0x33, 0xdf, 0x0a, 0xbf, 0xf1, 0x3e, - 0x65, 0x8a, 0x5a, 0x51, 0x74, 0xb3, 0xc8, 0x9f, 0xac, 0x1a, 0x2d, 0xaa, 0x35, 0xcb, 0xb6, 0xdd, - 0x6f, 0xef, 0xd5, 0x69, 0xed, 0x49, 0xa1, 0x5a, 0xb3, 0x98, 0x45, 0x4e, 0x36, 0xbb, 0x16, 0xbc, - 0xae, 0x85, 0x56, 0xd7, 0xfc, 0x84, 0x6a, 0xd9, 0x1b, 0x96, 0x5d, 0x5c, 0x57, 0x6c, 0xea, 0xda, - 0x15, 0x1f, 0x5f, 0x5c, 0xa7, 0x4c, 0xb9, 0x58, 0xac, 0x2a, 0x9a, 0x6e, 0x2a, 0x4c, 0xb7, 0x4c, - 0xd7, 0x55, 0x7e, 0x2a, 0x7d, 0x54, 0xfe, 0x28, 0xf3, 0x67, 0x99, 0x35, 0xc0, 0x66, 0x32, 0xdd, - 0x46, 0x53, 0x6c, 0xb9, 0x5a, 0xd3, 0x55, 0x0a, 0xdd, 0xaf, 0xa4, 0x77, 0xd7, 0xcd, 0x75, 0xab, - 0x6e, 0x96, 0xe5, 0x8a, 0x62, 0x57, 0x64, 0x66, 0xc9, 0xaa, 0xda, 0x1c, 0x68, 0x5a, 0xcc, 0x92, - 0xd5, 0x14, 0xf5, 0x5d, 0x5a, 0x03, 0xa3, 0x99, 0x74, 0x23, 0x43, 0xb1, 0x99, 0xbc, 0x6e, 0x58, - 0xea, 0xbb, 0x72, 0x85, 0xea, 0x5a, 0x85, 0x81, 0xd9, 0xa5, 0x74, 0x33, 0xab, 0xce, 0xe2, 0x06, - 0x9b, 0x4d, 0xb7, 0xaa, 0x29, 0x8c, 0xca, 0x86, 0xbe, 0xa1, 0x33, 0x5a, 0x93, 0xdf, 0x31, 0x14, - 0xcd, 0x06, 0xbb, 0x21, 0xcd, 0xd2, 0x2c, 0xfe, 0x58, 0x74, 0x9e, 0xe0, 0xeb, 0x09, 0xcd, 0xb2, - 0x34, 0x83, 0x16, 0x95, 0xaa, 0x5e, 0x54, 0x4c, 0xd3, 0x62, 0x3c, 0x53, 0x9e, 0xcd, 0x30, 0xa4, - 0x75, 0xc3, 0xd6, 0x8a, 0x8f, 0x2f, 0x3a, 0x7f, 0xdc, 0x06, 0xe9, 0x04, 0xce, 0xbf, 0xed, 0x64, - 0xf9, 0x11, 0x65, 0xca, 0x9c, 0xaa, 0x5a, 0x75, 0x93, 0xe9, 0xa6, 0x56, 0xa2, 0xef, 0xd5, 0xa9, - 0xcd, 0xa4, 0x7b, 0xf8, 0xd5, 0xd8, 0x56, 0xbb, 0x6a, 0x99, 0x36, 0x25, 0x05, 0x7c, 0x54, 0x59, - 0xb7, 0x6a, 0x8c, 0x96, 0x65, 0x87, 0x81, 0xac, 0x6c, 0x38, 0x3d, 0x46, 0xd0, 0x69, 0x34, 0x7e, - 0xb0, 0x34, 0x08, 0x4d, 0xdc, 0x96, 0x37, 0x48, 0xab, 0x78, 0x94, 0xbb, 0x5b, 0xa2, 0xec, 0x01, - 0xc4, 0x64, 0xcd, 0x0d, 0x09, 0x0c, 0x48, 0x46, 0xf0, 0x7e, 0xce, 0x7e, 0x79, 0x91, 0x7b, 0xc9, - 0x95, 0xbc, 0x57, 0x32, 0x84, 0x7b, 0x4d, 0xcb, 0x54, 0xe9, 0x48, 0xcf, 0x69, 0x34, 0xbe, 0xb7, - 0xe4, 0xbe, 0x48, 0xdf, 0x42, 0xf8, 0x54, 0xa2, 0x4b, 0x40, 0xf9, 0x75, 0x3c, 0x60, 0x05, 0x9b, - 0xb8, 0xef, 0xbe, 0xa9, 0x42, 0x21, 0x75, 0x2d, 0x14, 0x42, 0x0e, 0xe7, 0xf7, 0x3e, 0xfd, 0xe7, - 0xa9, 0x3d, 0xa5, 0xb0, 0x33, 0xa9, 0x02, 0xac, 0xe6, 0x0c, 0x23, 0x81, 0xd5, 0x1d, 0x8c, 0x5b, - 0x8b, 0x07, 0x06, 0x3f, 0x57, 0x70, 0x53, 0x52, 0x70, 0x56, 0x5a, 0xc1, 0x5d, 0xa1, 0xb0, 0xd2, - 0x0a, 0xab, 0x8a, 0x46, 0xc1, 0xb6, 0xe4, 0xb3, 0x94, 0xfe, 0xe8, 0xb1, 0x8d, 0x1b, 0x2a, 0x8d, - 0x6d, 0xae, 0x6b, 0x6c, 0xc9, 0x52, 0x80, 0x4b, 0x0f, 0xe7, 0x32, 0x96, 0xc9, 0xc5, 0x05, 0x17, - 0x20, 0xf3, 0x6d, 0x84, 0xcf, 0x26, 0x90, 0x99, 0x7f, 0xb2, 0xe0, 0x40, 0xf2, 0xc2, 0x37, 0x84, - 0x7b, 0x39, 0x44, 0x98, 0x12, 0xee, 0x4b, 0x28, 0xa8, 0x3d, 0x1d, 0x07, 0xf5, 0x2f, 0x08, 0x9f, - 0xcb, 0xc2, 0xf1, 0x45, 0x8b, 0xed, 0x77, 0x10, 0x7e, 0xdd, 0xe3, 0xb4, 0x6c, 0xa6, 0x84, 0xf6, - 0x15, 0x7c, 0xc0, 0xdd, 0xa0, 0xf5, 0x72, 0x70, 0xc1, 0x95, 0xbb, 0x16, 0xdf, 0x3f, 0xfb, 0xf2, - 0x9c, 0x80, 0x05, 0xc2, 0xfb, 0x15, 0xdc, 0xaf, 0x9b, 0x31, 0xd1, 0x9d, 0xcc, 0x88, 0x6e, 0xc8, - 0xab, 0x1b, 0xdc, 0x90, 0xab, 0xee, 0xc5, 0xd6, 0xb7, 0xdc, 0x83, 0x03, 0xdb, 0xdd, 0x5e, 0xee, - 0x7f, 0xf0, 0x2d, 0xf7, 0xc8, 0x50, 0x5f, 0xa8, 0x98, 0x2d, 0xe2, 0xd3, 0xde, 0x2e, 0x0d, 0x03, - 0xdf, 0x55, 0xec, 0xca, 0x9a, 0xb5, 0xa0, 0xb2, 0x86, 0x17, 0xb5, 0xd3, 0xb8, 0x4f, 0x6f, 0xb5, - 0xc1, 0x21, 0xe2, 0xff, 0xe4, 0xcc, 0xea, 0xd7, 0x52, 0xdc, 0x40, 0x44, 0xca, 0x78, 0x50, 0x0f, - 0x37, 0x42, 0x12, 0x2e, 0x88, 0x05, 0xa5, 0x65, 0x07, 0x71, 0x89, 0x3a, 0x94, 0x6e, 0x03, 0x94, - 0x88, 0xc9, 0xa2, 0xc2, 0x14, 0x71, 0x4a, 0xdb, 0x58, 0x4a, 0x73, 0x03, 0x94, 0x1e, 0xe2, 0xc3, - 0x0b, 0x0e, 0x4a, 0xbe, 0x5c, 0xd6, 0x1a, 0x36, 0xe4, 0xf8, 0x7c, 0x06, 0x1d, 0xbf, 0x0d, 0x30, - 0x09, 0xfa, 0x91, 0xbe, 0x01, 0x79, 0x69, 0x4d, 0xb0, 0x68, 0x5e, 0xba, 0x35, 0x9b, 0x3f, 0xf5, - 0xb2, 0x17, 0x3f, 0x58, 0x7a, 0xf6, 0x72, 0x5d, 0xcd, 0x5e, 0xf7, 0x26, 0x76, 0x11, 0x0f, 0x7b, - 0x33, 0x72, 0x49, 0xb1, 0x57, 0x9d, 0xca, 0xd5, 0x77, 0x6a, 0xe9, 0x66, 0x99, 0x36, 0x20, 0xed, - 0xee, 0x8b, 0x24, 0xe3, 0x91, 0xa8, 0x01, 0x70, 0x5f, 0xc0, 0x07, 0xbc, 0x6f, 0x10, 0xe7, 0xb1, - 0x0c, 0xca, 0x4d, 0x17, 0x4d, 0x43, 0x49, 0x01, 0x44, 0x73, 0x86, 0x11, 0x46, 0xd4, 0xad, 0x4c, - 0xfe, 0x18, 0x01, 0x89, 0xc0, 0x18, 0xb1, 0x24, 0x72, 0x1d, 0x91, 0xe8, 0x5e, 0x7e, 0x66, 0x5b, - 0x15, 0xe7, 0x8a, 0x62, 0xb3, 0x79, 0xa7, 0x76, 0xbf, 0xcb, 0x4b, 0xf7, 0xf4, 0x34, 0x6d, 0xb5, - 0xca, 0xca, 0x88, 0x1d, 0x10, 0xfd, 0x32, 0x1e, 0x08, 0x35, 0x09, 0x96, 0x95, 0x61, 0x87, 0x61, - 0x37, 0xfe, 0x13, 0x26, 0x01, 0x74, 0xb7, 0x32, 0xf9, 0x5b, 0xdf, 0x09, 0xd3, 0x16, 0xcf, 0x5c, - 0x17, 0x78, 0x76, 0x2f, 0xcb, 0xe7, 0xf1, 0x51, 0x2f, 0x5b, 0xfe, 0x9d, 0x2b, 0x3e, 0xb5, 0x2b, - 0x70, 0xe3, 0x81, 0xce, 0xf3, 0x4f, 0xee, 0x3b, 0x37, 0x89, 0x4e, 0x2f, 0x20, 0x1a, 0x1e, 0x0a, - 0x0e, 0x0d, 0x51, 0x7b, 0x80, 0x0f, 0xf9, 0xb7, 0x5a, 0xc8, 0x51, 0x3b, 0x3b, 0x76, 0x29, 0xe0, - 0x40, 0xfa, 0x1a, 0x70, 0x9c, 0x33, 0x8c, 0x97, 0xb1, 0x3b, 0x7f, 0x84, 0x80, 0x48, 0xd3, 0x7f, - 0x22, 0x91, 0xdc, 0x8e, 0x88, 0x74, 0x2f, 0xeb, 0xf7, 0xe1, 0x72, 0xba, 0xa2, 0xdb, 0x6c, 0x95, - 0x9a, 0x65, 0xdd, 0xd4, 0xfc, 0x91, 0x49, 0x29, 0x6d, 0x87, 0x70, 0x2f, 0xbf, 0x58, 0xf3, 0xd1, - 0x0f, 0x97, 0xdc, 0x17, 0xe9, 0x43, 0x84, 0x4f, 0xc4, 0x3b, 0x7c, 0x59, 0xa1, 0x90, 0xf0, 0x21, - 0x66, 0x31, 0xc5, 0x80, 0xc1, 0x60, 0x66, 0x05, 0xbe, 0x49, 0x2b, 0x00, 0xaa, 0xa4, 0x30, 0xba, - 0xe2, 0xaa, 0x01, 0xcb, 0x66, 0xb5, 0xee, 0xdf, 0xbf, 0x5c, 0x2e, 0xc8, 0xc7, 0x85, 0x1c, 0xc7, - 0xfb, 0x36, 0x75, 0xb3, 0x6c, 0x6d, 0x72, 0x9f, 0xb9, 0x12, 0xbc, 0x49, 0xdf, 0xcb, 0xe1, 0x93, - 0x09, 0xee, 0x80, 0xe4, 0x71, 0xbc, 0xaf, 0xd2, 0xda, 0xcd, 0x72, 0x25, 0x78, 0x23, 0xf7, 0xf1, - 0x21, 0x55, 0x65, 0x0d, 0x5b, 0xde, 0xd0, 0x6d, 0x9b, 0x96, 0x47, 0x7a, 0xda, 0x27, 0xdf, 0xc7, - 0x1d, 0xdc, 0xe3, 0xf6, 0x64, 0x15, 0x1f, 0x76, 0xfd, 0x55, 0x81, 0x7c, 0xae, 0x83, 0x68, 0x72, - 0x0f, 0x10, 0x29, 0x72, 0x06, 0x1f, 0xe6, 0x91, 0x6b, 0x7a, 0xdc, 0x1b, 0x0d, 0x27, 0x19, 0xc7, - 0x47, 0xaa, 0x8a, 0xcd, 0x64, 0x77, 0xec, 0xc7, 0x8a, 0x51, 0xa7, 0x23, 0xbd, 0x7c, 0x7b, 0xe8, - 0x77, 0xbe, 0x3b, 0xf9, 0xb6, 0xbf, 0xe4, 0x7c, 0x25, 0x05, 0x7c, 0x14, 0x1c, 0x05, 0x3a, 0xef, - 0x73, 0xc5, 0x8d, 0x6a, 0x6b, 0x7e, 0x40, 0xff, 0xeb, 0x38, 0x6f, 0x58, 0x9b, 0xd4, 0x66, 0xb2, - 0xdf, 0x0c, 0x84, 0xa2, 0x91, 0xfd, 0x3c, 0x98, 0xc3, 0x6e, 0x0f, 0xdf, 0xe4, 0x82, 0x2d, 0x7f, - 0x1e, 0x4f, 0xc4, 0x4d, 0xbd, 0x87, 0x3a, 0xab, 0xe8, 0x66, 0x33, 0x57, 0xa9, 0x39, 0x97, 0x7e, - 0xd5, 0x83, 0xcf, 0x0b, 0x39, 0x81, 0x4c, 0xbf, 0x8d, 0xfb, 0x83, 0x12, 0x5d, 0x47, 0x13, 0x5a, - 0xf5, 0x4f, 0xe8, 0x48, 0x0a, 0x62, 0x66, 0x34, 0x99, 0xc5, 0xc3, 0x6a, 0xbd, 0x56, 0xa3, 0x26, - 0x93, 0x37, 0x75, 0x56, 0x29, 0xd7, 0x94, 0x4d, 0x19, 0x26, 0x6b, 0x8e, 0x47, 0xe9, 0x18, 0x34, - 0x3f, 0x84, 0xd6, 0x87, 0xbc, 0x91, 0x4c, 0xe1, 0x63, 0x11, 0xbb, 0x9a, 0xc2, 0x28, 0xcf, 0xf3, - 0xc1, 0xd2, 0xd1, 0x90, 0x95, 0x43, 0xd8, 0x49, 0x62, 0x4b, 0x47, 0x93, 0x69, 0x43, 0xa5, 0xb4, - 0x4c, 0xcb, 0x3c, 0xe3, 0x07, 0x4a, 0x83, 0x35, 0x2f, 0x26, 0xb7, 0xa1, 0xa1, 0x29, 0x87, 0x39, - 0x47, 0xd5, 0x23, 0xca, 0x94, 0xc0, 0xb1, 0x2b, 0xcd, 0x78, 0x3b, 0x4e, 0xa8, 0xb5, 0xb5, 0x74, - 0xee, 0x06, 0x96, 0x0e, 0x24, 0x77, 0x0d, 0x96, 0xf0, 0x82, 0x65, 0x3e, 0xa6, 0x35, 0xa7, 0xee, - 0x5b, 0xb3, 0x1c, 0xf3, 0xc8, 0x99, 0x13, 0xd9, 0xa8, 0xf2, 0xf8, 0x80, 0xa6, 0xd8, 0x2b, 0xcd, - 0xbd, 0xea, 0x60, 0xa9, 0xf9, 0x2e, 0xfd, 0x10, 0xc1, 0x52, 0x8e, 0xba, 0x05, 0x3c, 0xff, 0x87, - 0x07, 0x3d, 0x85, 0x61, 0x49, 0xb1, 0x97, 0x4d, 0xa7, 0xd1, 0x13, 0xe7, 0x22, 0x0d, 0x4e, 0x6f, - 0x2e, 0x09, 0xaa, 0x96, 0x71, 0x87, 0x52, 0xe8, 0xdd, 0x03, 0xb3, 0x3d, 0xdc, 0x40, 0xc6, 0xf1, - 0x80, 0xf3, 0xd7, 0x5f, 0x15, 0xe4, 0x78, 0xae, 0xc3, 0x9f, 0xa5, 0x31, 0xb8, 0xfe, 0xdf, 0xa3, - 0xb6, 0xad, 0x68, 0x74, 0x55, 0xb1, 0x6d, 0xdd, 0xd4, 0x56, 0x5b, 0x1e, 0xbd, 0xe8, 0xde, 0x01, - 0x1d, 0x26, 0xa5, 0x23, 0x10, 0x3b, 0x81, 0x0f, 0xbe, 0xd3, 0x84, 0xe8, 0x12, 0x6a, 0x7d, 0x90, - 0x46, 0xa3, 0x3b, 0xe6, 0x1d, 0x43, 0xd1, 0xbc, 0xeb, 0xb9, 0xf4, 0x01, 0x8a, 0xee, 0x81, 0xd0, - 0x01, 0xfc, 0x2b, 0xf8, 0x48, 0x2d, 0xd4, 0x06, 0x47, 0x6b, 0x31, 0x63, 0x6d, 0x84, 0x5d, 0xc2, - 0x15, 0x24, 0xe2, 0x4e, 0x5a, 0x85, 0x89, 0x16, 0xbc, 0x87, 0x0b, 0x9c, 0x5d, 0xc3, 0x78, 0xbf, - 0xb3, 0xab, 0x38, 0xf7, 0x49, 0x37, 0x39, 0xfb, 0x58, 0x83, 0x5f, 0x25, 0xb7, 0x60, 0x72, 0x86, - 0x3d, 0x02, 0xa7, 0xaf, 0xe2, 0x81, 0x90, 0xe6, 0x0d, 0x94, 0xba, 0xa1, 0x14, 0x4c, 0xfd, 0x64, - 0x0a, 0xf7, 0xf2, 0xd1, 0xc9, 0xa7, 0x08, 0x0f, 0x84, 0xe4, 0x2e, 0xf2, 0x66, 0xc6, 0x10, 0xe9, - 0xa2, 0x70, 0xfe, 0xad, 0x4e, 0xcd, 0x5d, 0xea, 0xd2, 0xad, 0x0f, 0xfe, 0xfa, 0xef, 0x0f, 0x7b, - 0xae, 0x91, 0x2b, 0x5c, 0x67, 0x9f, 0xf4, 0xfd, 0x3a, 0x11, 0xd4, 0xe7, 0xc1, 0xae, 0xb8, 0x05, - 0x25, 0xdf, 0x76, 0x71, 0x8b, 0x17, 0x79, 0xdb, 0xe4, 0x37, 0x08, 0x93, 0x90, 0xf7, 0x39, 0xc3, - 0x10, 0xe3, 0x95, 0x28, 0x0b, 0x8b, 0xf1, 0x4a, 0x96, 0x7a, 0xa5, 0x02, 0xe7, 0x35, 0x4e, 0xce, - 0x89, 0xf1, 0x22, 0x9f, 0x23, 0xfc, 0x4a, 0x94, 0x05, 0xa8, 0x70, 0x64, 0xb1, 0x33, 0x34, 0x41, - 0x41, 0x31, 0x7f, 0x7b, 0x87, 0x5e, 0x80, 0xda, 0x9b, 0x9c, 0xda, 0x65, 0x32, 0x23, 0x46, 0x0d, - 0xcc, 0x21, 0x73, 0xdb, 0xe4, 0x3f, 0x08, 0x8f, 0x04, 0xe7, 0xad, 0x8f, 0xe8, 0x82, 0x20, 0xc4, - 0x34, 0xe1, 0x34, 0xbf, 0xb8, 0x33, 0x27, 0x40, 0xf3, 0x26, 0xa7, 0x79, 0x95, 0x5c, 0x4e, 0xa0, - 0xa9, 0x9b, 0xc9, 0x2c, 0x65, 0xbd, 0xbc, 0x4d, 0x7e, 0x8d, 0xf0, 0x60, 0x84, 0xa8, 0xf0, 0xbc, - 0x8c, 0xd7, 0x2f, 0x85, 0xe7, 0x65, 0x82, 0x26, 0x99, 0x39, 0x2f, 0x83, 0xac, 0x6c, 0xf2, 0x09, - 0xc2, 0xfd, 0x41, 0x5f, 0xe4, 0xaa, 0x08, 0x84, 0xd8, 0xbd, 0x33, 0x7f, 0xad, 0x13, 0x53, 0x40, - 0x3e, 0xcf, 0x91, 0xdf, 0x20, 0xd7, 0x84, 0x90, 0xfb, 0x12, 0x51, 0xdc, 0x82, 0x4d, 0x79, 0x9b, - 0xfc, 0xad, 0x95, 0x12, 0x9f, 0xe2, 0x74, 0x53, 0x70, 0x0f, 0x4b, 0x92, 0xe1, 0xf2, 0xb7, 0x3a, - 0x77, 0x00, 0xe4, 0xde, 0xe2, 0xe4, 0xae, 0x90, 0xd9, 0x74, 0x72, 0x2d, 0xcb, 0xe2, 0x96, 0xef, - 0xd3, 0x36, 0xf9, 0x0c, 0xe1, 0x63, 0xb1, 0x3a, 0x25, 0xb9, 0xd5, 0x46, 0xc8, 0x63, 0x95, 0xd2, - 0xfc, 0xdc, 0x0e, 0x3c, 0xb4, 0x97, 0xbb, 0xa0, 0x75, 0x88, 0xe2, 0x27, 0x08, 0x0f, 0x45, 0x46, - 0x71, 0x56, 0xd4, 0xcd, 0xf6, 0x96, 0x44, 0x87, 0xe9, 0x4b, 0x53, 0x46, 0xa5, 0x0b, 0x9c, 0xdf, - 0x04, 0x19, 0x17, 0xe5, 0x47, 0x7e, 0x8a, 0x5a, 0x5a, 0x1c, 0x99, 0x15, 0x9c, 0x3f, 0x21, 0xd1, - 0x30, 0x7f, 0xb9, 0x6d, 0x3b, 0xc0, 0x5b, 0xe4, 0x78, 0xdf, 0x20, 0x63, 0x09, 0x78, 0x35, 0x30, - 0x70, 0x52, 0x50, 0xa6, 0x8d, 0x6d, 0xf2, 0x23, 0x84, 0xfb, 0x3c, 0x2f, 0x4e, 0xcc, 0x67, 0x05, - 0x43, 0xd6, 0x11, 0xe2, 0x18, 0xe9, 0x52, 0x1a, 0xe3, 0x88, 0x5f, 0x23, 0xa7, 0x32, 0x10, 0x93, - 0x5f, 0x22, 0x7c, 0x24, 0x5c, 0x75, 0x93, 0xeb, 0x22, 0xc3, 0x26, 0x5c, 0x01, 0xf2, 0x37, 0x3a, - 0x33, 0x16, 0x0c, 0xb5, 0x1a, 0xc6, 0xfa, 0x7b, 0x84, 0xfb, 0x7c, 0x85, 0xb5, 0xd8, 0xd9, 0x9f, - 0x55, 0xc0, 0x8b, 0x9d, 0xfd, 0x99, 0xd5, 0xbd, 0x34, 0xc1, 0xd9, 0xbc, 0x4e, 0xa4, 0x04, 0x36, - 0xbe, 0xcb, 0x08, 0x79, 0x8a, 0x22, 0xea, 0xa4, 0x70, 0xb5, 0x19, 0xaf, 0xad, 0x0a, 0x57, 0x9b, - 0x09, 0x7a, 0xa9, 0x34, 0xcb, 0xe1, 0x5f, 0x20, 0x85, 0x04, 0xf8, 0x46, 0xd0, 0xae, 0x39, 0xfd, - 0x9d, 0x1a, 0x33, 0xe4, 0xb3, 0x9d, 0xb3, 0x7c, 0x27, 0x6c, 0x92, 0xd5, 0xdf, 0xcc, 0xb3, 0x3c, - 0xc4, 0x86, 0xfc, 0x00, 0xe1, 0xbd, 0x7c, 0xf3, 0x99, 0x12, 0x0c, 0xa3, 0x7f, 0x93, 0x9c, 0x6e, - 0xcb, 0x06, 0x10, 0x9e, 0xe7, 0x08, 0xcf, 0x92, 0x33, 0x49, 0x93, 0x1f, 0x4e, 0x32, 0x1e, 0xe4, - 0x9f, 0x21, 0xdc, 0xe7, 0x53, 0x7d, 0xc5, 0xea, 0x8c, 0x58, 0xa5, 0xb8, 0x33, 0xb0, 0x33, 0x1c, - 0x6c, 0x91, 0x4c, 0xa6, 0x82, 0x8d, 0xdc, 0x3f, 0xbe, 0x8f, 0xf0, 0x7e, 0xef, 0x28, 0x9a, 0x12, - 0xcc, 0x68, 0xdb, 0x81, 0x0d, 0x29, 0xbf, 0xd2, 0x19, 0x8e, 0xf5, 0x24, 0x79, 0x35, 0x05, 0x2b, - 0xf9, 0xd8, 0x59, 0x80, 0x41, 0xbd, 0x89, 0x08, 0x55, 0x60, 0xf1, 0xaa, 0x6d, 0xfe, 0x7a, 0x47, - 0xb6, 0xa2, 0x3b, 0x87, 0x0f, 0xe4, 0x7f, 0x11, 0x1e, 0x4d, 0x17, 0xca, 0xc8, 0x72, 0x07, 0x58, - 0xe2, 0x15, 0xbb, 0xfc, 0xff, 0x77, 0xc3, 0x15, 0xb0, 0xbc, 0xca, 0x59, 0x4e, 0x93, 0x8b, 0xd9, - 0x2c, 0xc3, 0x8c, 0x3e, 0x46, 0xb8, 0x3f, 0xf8, 0xbf, 0x5c, 0x62, 0x2b, 0x20, 0xf6, 0xbf, 0xc3, - 0xc4, 0x2a, 0xed, 0xf8, 0x7f, 0x1d, 0x93, 0x26, 0x39, 0x89, 0x31, 0x72, 0x36, 0x81, 0xc4, 0xfb, - 0x41, 0x94, 0x0e, 0xf0, 0xa0, 0xea, 0x26, 0x06, 0x3c, 0x56, 0xc7, 0x13, 0x03, 0x1e, 0x2f, 0xf2, - 0x65, 0x02, 0x37, 0x82, 0x28, 0x9d, 0x52, 0x21, 0x2c, 0x0a, 0x89, 0x95, 0x0a, 0x09, 0xf2, 0x95, - 0x58, 0xa9, 0x90, 0x24, 0x6d, 0x65, 0x96, 0x0a, 0x61, 0xa1, 0x2a, 0x4c, 0x80, 0xff, 0x58, 0xd0, - 0x36, 0x01, 0xff, 0x2f, 0x16, 0x6d, 0x13, 0x08, 0xfc, 0x3e, 0xd1, 0x0e, 0x01, 0x17, 0xeb, 0x9f, - 0x10, 0x3e, 0xf4, 0xa0, 0xce, 0xd6, 0x1a, 0xbb, 0x44, 0x8d, 0x12, 0x90, 0x36, 0x9a, 0x58, 0x63, - 0x8e, 0x82, 0x5f, 0xb8, 0xfa, 0x5a, 0xb3, 0xcb, 0x2e, 0xd0, 0xa1, 0xb2, 0x4e, 0x60, 0x3f, 0x23, - 0xf2, 0x2f, 0x84, 0x8f, 0x87, 0xf0, 0xef, 0x4a, 0x05, 0xea, 0x1a, 0x27, 0x75, 0x89, 0x4c, 0x09, - 0x90, 0x0a, 0xcb, 0x4f, 0xee, 0x4d, 0x39, 0x8e, 0xe2, 0x2e, 0xd2, 0x9e, 0x6e, 0x70, 0x82, 0xb3, - 0xe4, 0x52, 0xe2, 0x7d, 0x32, 0x81, 0x1f, 0x17, 0x9e, 0x7e, 0xce, 0x35, 0x9b, 0x8e, 0x66, 0xe1, - 0x4b, 0x52, 0x9d, 0xb2, 0x0e, 0x7f, 0x1f, 0x1f, 0xf2, 0x0c, 0xd0, 0xef, 0x2e, 0x81, 0xe6, 0x3a, - 0x67, 0x30, 0x43, 0xa6, 0x53, 0x18, 0x24, 0xaa, 0x33, 0xff, 0x40, 0x98, 0x04, 0x29, 0xed, 0x1e, - 0x69, 0x26, 0x5b, 0xe6, 0x0c, 0xe3, 0x0e, 0x91, 0xfb, 0x1d, 0xd7, 0xd4, 0xfc, 0x9d, 0x76, 0x89, - 0x28, 0x93, 0x55, 0x0d, 0x04, 0x99, 0xe5, 0x7b, 0xbf, 0xf9, 0xf9, 0x47, 0x13, 0x68, 0x7e, 0xe9, - 0xe9, 0xf3, 0x51, 0xf4, 0xec, 0xf9, 0x28, 0xfa, 0xec, 0xf9, 0x28, 0xfa, 0xee, 0x8b, 0xd1, 0x3d, - 0xcf, 0x5e, 0x8c, 0xee, 0xf9, 0xfb, 0x8b, 0xd1, 0x3d, 0x8f, 0x26, 0x35, 0x9d, 0x55, 0xea, 0xeb, - 0x05, 0xd5, 0xda, 0xf0, 0x7b, 0x34, 0xad, 0x32, 0x2d, 0x36, 0xfc, 0x8e, 0xd9, 0x93, 0x2a, 0xb5, - 0xd7, 0xf7, 0xf1, 0xbb, 0xf0, 0xf4, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xdf, 0x71, 0x32, - 0x00, 0x32, 0x00, 0x00, + // 2386 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0xdd, 0x6f, 0x1c, 0x57, + 0x15, 0xcf, 0xf5, 0xc6, 0x8e, 0x7d, 0x9d, 0xd8, 0xf1, 0x8d, 0x13, 0xbb, 0x5b, 0xc7, 0x49, 0x27, + 0x4d, 0xec, 0x3a, 0x78, 0x37, 0xb1, 0x63, 0xe7, 0xb3, 0x4d, 0xfc, 0x91, 0x38, 0x46, 0x4e, 0xe2, + 0xae, 0x2c, 0x82, 0x02, 0x62, 0x34, 0x9e, 0xbd, 0x9d, 0x1d, 0x3a, 0x9e, 0xd9, 0xce, 0xcc, 0xc6, + 0x9b, 0x5a, 0x96, 0xa0, 0x12, 0x0f, 0xbc, 0x21, 0x55, 0x88, 0x17, 0x5e, 0x11, 0x48, 0xf0, 0xd0, + 0x07, 0xd4, 0x17, 0x04, 0x12, 0xdf, 0x11, 0x05, 0x29, 0x14, 0x09, 0x21, 0x1e, 0x50, 0x49, 0x10, + 0x7d, 0xe7, 0x2f, 0x40, 0x73, 0xe7, 0xcc, 0xee, 0x7c, 0xcf, 0xdd, 0xf5, 0x46, 0x72, 0x9f, 0x3c, + 0x33, 0xf7, 0x9e, 0x73, 0x7f, 0xbf, 0x73, 0xce, 0xfd, 0xfa, 0xad, 0xf1, 0x1b, 0xef, 0x53, 0x5b, + 0x92, 0x2b, 0x92, 0xaa, 0x17, 0xd9, 0x93, 0x61, 0xd2, 0xa2, 0x6c, 0x1a, 0x96, 0xe5, 0x7e, 0x7b, + 0xaf, 0x46, 0xcd, 0x27, 0x85, 0xaa, 0x69, 0xd8, 0x06, 0x39, 0xd9, 0xe8, 0x5a, 0xf0, 0xba, 0x16, + 0x9a, 0x5d, 0xf3, 0x53, 0xb2, 0x61, 0x6d, 0x19, 0x56, 0x71, 0x53, 0xb2, 0xa8, 0x6b, 0x57, 0x7c, + 0x7c, 0x71, 0x93, 0xda, 0xd2, 0xc5, 0x62, 0x55, 0x52, 0x54, 0x5d, 0xb2, 0x55, 0x43, 0x77, 0x5d, + 0xe5, 0x67, 0xd2, 0x47, 0x65, 0x8f, 0x22, 0x7b, 0x16, 0xed, 0x3a, 0xd8, 0x4c, 0xa7, 0xdb, 0x28, + 0x92, 0x25, 0x56, 0x4d, 0x55, 0xa6, 0xd0, 0xfd, 0x4a, 0x7a, 0x77, 0x55, 0xdf, 0x34, 0x6a, 0x7a, + 0x59, 0xac, 0x48, 0x56, 0x45, 0xb4, 0x0d, 0x51, 0x96, 0x1b, 0x03, 0xcd, 0xf2, 0x59, 0xda, 0xa6, + 0x24, 0xbf, 0x4b, 0x4d, 0x30, 0x9a, 0x4b, 0x37, 0xd2, 0x24, 0xcb, 0x16, 0x37, 0x35, 0x43, 0x7e, + 0x57, 0xac, 0x50, 0x55, 0xa9, 0xd8, 0x60, 0x76, 0x29, 0xdd, 0xcc, 0xa8, 0xd9, 0x71, 0x83, 0xcd, + 0xa7, 0x5b, 0x99, 0x92, 0x4d, 0x45, 0x4d, 0xdd, 0x52, 0x6d, 0x6a, 0x8a, 0xef, 0x68, 0x92, 0x62, + 0x81, 0xdd, 0xb0, 0x62, 0x28, 0x06, 0x7b, 0x2c, 0x3a, 0x4f, 0xf0, 0x75, 0x4c, 0x31, 0x0c, 0x45, + 0xa3, 0x45, 0xa9, 0xaa, 0x16, 0x25, 0x5d, 0x37, 0x6c, 0x96, 0x29, 0xcf, 0x66, 0x04, 0xd2, 0xba, + 0x65, 0x29, 0xc5, 0xc7, 0x17, 0x9d, 0x3f, 0x6e, 0x83, 0x30, 0x86, 0xf3, 0x6f, 0x3b, 0x59, 0x7e, + 0x44, 0x6d, 0x69, 0x41, 0x96, 0x8d, 0x9a, 0x6e, 0xab, 0xba, 0x52, 0xa2, 0xef, 0xd5, 0xa8, 0x65, + 0x0b, 0xf7, 0xf0, 0xab, 0xb1, 0xad, 0x56, 0xd5, 0xd0, 0x2d, 0x4a, 0x0a, 0xf8, 0x98, 0xb4, 0x69, + 0x98, 0x36, 0x2d, 0x8b, 0x0e, 0x03, 0x51, 0xda, 0x72, 0x7a, 0x8c, 0xa2, 0xd3, 0x68, 0xb2, 0xaf, + 0x34, 0x04, 0x4d, 0xcc, 0x96, 0x35, 0x08, 0xeb, 0x78, 0x9c, 0xb9, 0x5b, 0xa1, 0xf6, 0x03, 0x88, + 0xc9, 0x86, 0x1b, 0x12, 0x18, 0x90, 0x8c, 0xe2, 0x43, 0x8c, 0xfd, 0xea, 0x32, 0xf3, 0x92, 0x2b, + 0x79, 0xaf, 0x64, 0x18, 0x77, 0xeb, 0x86, 0x2e, 0xd3, 0xd1, 0xae, 0xd3, 0x68, 0xf2, 0x60, 0xc9, + 0x7d, 0x11, 0xbe, 0x8d, 0xf0, 0xa9, 0x44, 0x97, 0x80, 0xf2, 0x1b, 0x78, 0xd0, 0x08, 0x36, 0x31, + 0xdf, 0xfd, 0x33, 0x85, 0x42, 0xea, 0x5c, 0x28, 0x84, 0x1c, 0x2e, 0x1e, 0x7c, 0xfa, 0xaf, 0x53, + 0x07, 0x4a, 0x61, 0x67, 0x42, 0x05, 0x58, 0x2d, 0x68, 0x5a, 0x02, 0xab, 0x3b, 0x18, 0x37, 0x27, + 0x0f, 0x0c, 0x7e, 0xae, 0xe0, 0xa6, 0xa4, 0xe0, 0xcc, 0xb4, 0x82, 0x3b, 0x43, 0x61, 0xa6, 0x15, + 0xd6, 0x25, 0x85, 0x82, 0x6d, 0xc9, 0x67, 0x29, 0xfc, 0xc9, 0x63, 0x1b, 0x37, 0x54, 0x1a, 0xdb, + 0x5c, 0xc7, 0xd8, 0x92, 0x95, 0x00, 0x97, 0x2e, 0xc6, 0x65, 0x22, 0x93, 0x8b, 0x0b, 0x2e, 0x40, + 0xe6, 0x3b, 0x08, 0x9f, 0x4d, 0x20, 0xb3, 0xf8, 0x64, 0xc9, 0x81, 0xe4, 0x85, 0x6f, 0x18, 0x77, + 0x33, 0x88, 0x50, 0x12, 0xee, 0x4b, 0x28, 0xa8, 0x5d, 0x6d, 0x07, 0xf5, 0xaf, 0x08, 0x9f, 0xcb, + 0xc2, 0xf1, 0x45, 0x8b, 0xed, 0x77, 0x11, 0x7e, 0xdd, 0xe3, 0xb4, 0xaa, 0xa7, 0x84, 0xf6, 0x15, + 0xdc, 0xeb, 0x2e, 0xd0, 0x6a, 0x39, 0x38, 0xe1, 0xca, 0x1d, 0x8b, 0xef, 0x5f, 0x7c, 0x79, 0x4e, + 0xc0, 0x02, 0xe1, 0xfd, 0x1a, 0x1e, 0x50, 0xf5, 0x98, 0xe8, 0x4e, 0x67, 0x44, 0x37, 0xe4, 0xd5, + 0x0d, 0x6e, 0xc8, 0x55, 0xe7, 0x62, 0xeb, 0x9b, 0xee, 0xc1, 0x81, 0xad, 0x4e, 0x4f, 0xf7, 0x3f, + 0xfa, 0xa6, 0x7b, 0x64, 0xa8, 0x2f, 0x54, 0xcc, 0x96, 0xf1, 0x69, 0x6f, 0x95, 0x86, 0x81, 0xef, + 0x4a, 0x56, 0x65, 0xc3, 0x58, 0x92, 0xed, 0xba, 0x17, 0xb5, 0xd3, 0xb8, 0x5f, 0x6d, 0xb6, 0xc1, + 0x26, 0xe2, 0xff, 0xe4, 0x54, 0xf5, 0x6b, 0x29, 0x6e, 0x20, 0x22, 0x65, 0x3c, 0xa4, 0x86, 0x1b, + 0x21, 0x09, 0x17, 0xf8, 0x82, 0xd2, 0xb4, 0x83, 0xb8, 0x44, 0x1d, 0x0a, 0xb7, 0x01, 0x4a, 0xc4, + 0x64, 0x59, 0xb2, 0x25, 0x7e, 0x4a, 0xbb, 0x58, 0x48, 0x73, 0x03, 0x94, 0x1e, 0xe2, 0x23, 0x4b, + 0x0e, 0x4a, 0x36, 0x5d, 0x36, 0xea, 0x16, 0xe4, 0xf8, 0x7c, 0x06, 0x1d, 0xbf, 0x0d, 0x30, 0x09, + 0xfa, 0x11, 0xbe, 0x09, 0x79, 0x69, 0x16, 0x58, 0x34, 0x2f, 0x9d, 0xaa, 0xe6, 0x4f, 0xbd, 0xec, + 0xc5, 0x0f, 0x96, 0x9e, 0xbd, 0x5c, 0x47, 0xb3, 0xd7, 0xb9, 0xc2, 0x2e, 0xe2, 0x11, 0xaf, 0x22, + 0x57, 0x24, 0x6b, 0xdd, 0x39, 0xb9, 0xfa, 0x76, 0x2d, 0x55, 0x2f, 0xd3, 0x3a, 0xa4, 0xdd, 0x7d, + 0x11, 0x44, 0x3c, 0x1a, 0x35, 0x00, 0xee, 0x4b, 0xb8, 0xd7, 0xfb, 0x06, 0x71, 0x9e, 0xc8, 0xa0, + 0xdc, 0x70, 0xd1, 0x30, 0x14, 0x24, 0x40, 0xb4, 0xa0, 0x69, 0x61, 0x44, 0x9d, 0xca, 0xe4, 0x4f, + 0x10, 0x90, 0x08, 0x8c, 0x11, 0x4b, 0x22, 0xd7, 0x16, 0x89, 0xce, 0xe5, 0x67, 0xbe, 0x79, 0xe2, + 0x5c, 0x93, 0x2c, 0x7b, 0xd1, 0x39, 0xbb, 0xdf, 0x65, 0x47, 0xf7, 0xf4, 0x34, 0xed, 0x34, 0x8f, + 0x95, 0x11, 0x3b, 0x20, 0xfa, 0x55, 0x3c, 0x18, 0x6a, 0xe2, 0x3c, 0x56, 0x86, 0x1d, 0x86, 0xdd, + 0xf8, 0x77, 0x98, 0x04, 0xd0, 0x9d, 0xca, 0xe4, 0xef, 0x7c, 0x3b, 0x4c, 0x4b, 0x3c, 0x73, 0x1d, + 0xe0, 0xd9, 0xb9, 0x2c, 0x9f, 0xc7, 0xc7, 0xbc, 0x6c, 0xf9, 0x57, 0xae, 0xf8, 0xd4, 0xae, 0xc1, + 0x8d, 0x07, 0x3a, 0x2f, 0x3e, 0xb9, 0xef, 0xdc, 0x24, 0xda, 0xbd, 0x80, 0x28, 0x78, 0x38, 0x38, + 0x34, 0x44, 0xed, 0x01, 0x3e, 0xec, 0x5f, 0x6a, 0x21, 0x47, 0xad, 0xac, 0xd8, 0xa5, 0x80, 0x03, + 0x61, 0x07, 0x38, 0x2e, 0x68, 0xda, 0x4b, 0x58, 0x9d, 0xc9, 0x18, 0xee, 0xab, 0xe9, 0x86, 0x59, + 0xa6, 0x26, 0x2d, 0x33, 0x86, 0xbd, 0xa5, 0xe6, 0x07, 0xe1, 0x23, 0x04, 0x34, 0x1b, 0xa3, 0x27, + 0xd2, 0xcc, 0xed, 0x89, 0x66, 0xe7, 0x6a, 0xe2, 0x3e, 0x5c, 0x5d, 0xd7, 0x54, 0xcb, 0x5e, 0xa7, + 0x7a, 0x59, 0xd5, 0x15, 0x7f, 0xdc, 0x52, 0x0e, 0xbe, 0xc3, 0xb8, 0x9b, 0x5d, 0xbb, 0xd9, 0xe8, + 0x47, 0x4a, 0xee, 0x8b, 0xf0, 0x21, 0xc2, 0x63, 0xf1, 0x0e, 0x5f, 0x56, 0x28, 0x04, 0x7c, 0xd8, + 0x36, 0x6c, 0x49, 0x83, 0xc1, 0xa0, 0xee, 0x02, 0xdf, 0x84, 0x35, 0x00, 0x55, 0x92, 0x6c, 0xba, + 0xe6, 0x6a, 0x05, 0xab, 0x7a, 0xb5, 0xe6, 0x5f, 0xdd, 0x5c, 0x2e, 0xc8, 0xc7, 0x85, 0x9c, 0xc0, + 0x3d, 0xdb, 0xaa, 0x5e, 0x36, 0xb6, 0x99, 0xcf, 0x5c, 0x09, 0xde, 0x84, 0xef, 0xe7, 0xf0, 0xc9, + 0x04, 0x77, 0x40, 0xf2, 0x04, 0xee, 0xa9, 0x34, 0xd7, 0xba, 0x5c, 0x09, 0xde, 0xc8, 0x7d, 0x7c, + 0x58, 0x96, 0xed, 0xba, 0x25, 0x6e, 0xa9, 0x96, 0xc5, 0x2a, 0xa8, 0x65, 0xf2, 0xfd, 0xcc, 0xc1, + 0x3d, 0x66, 0x4f, 0xd6, 0xf1, 0x11, 0xd7, 0x5f, 0x15, 0xc8, 0xe7, 0xda, 0x88, 0x26, 0xf3, 0x00, + 0x91, 0x22, 0x67, 0xf0, 0x11, 0x16, 0xb9, 0x86, 0xc7, 0x83, 0xd1, 0x70, 0x92, 0x49, 0x7c, 0xb4, + 0x2a, 0x59, 0xb6, 0xe8, 0x8e, 0xfd, 0x58, 0xd2, 0x6a, 0x74, 0xb4, 0x9b, 0x2d, 0x1e, 0x03, 0xce, + 0x77, 0x27, 0xdf, 0xd6, 0x57, 0x9c, 0xaf, 0xa4, 0x80, 0x8f, 0x81, 0xa3, 0x40, 0xe7, 0x1e, 0x57, + 0xfa, 0xa8, 0x36, 0xeb, 0x03, 0xfa, 0x5f, 0xc7, 0x79, 0xcd, 0xd8, 0xa6, 0x96, 0x2d, 0xfa, 0xcd, + 0x40, 0x46, 0x1a, 0x3d, 0xc4, 0x82, 0x39, 0xe2, 0xf6, 0xf0, 0x15, 0x17, 0x6c, 0x08, 0x8b, 0x78, + 0x2a, 0xae, 0xf4, 0x1e, 0xaa, 0x76, 0x45, 0xd5, 0x1b, 0xb9, 0x4a, 0xcd, 0xb9, 0xf0, 0xeb, 0x2e, + 0x7c, 0x9e, 0xcb, 0x09, 0x64, 0xfa, 0x6d, 0x3c, 0x10, 0x14, 0xf0, 0xda, 0x2a, 0x68, 0xd9, 0x5f, + 0xd0, 0x91, 0x14, 0xc4, 0x54, 0x34, 0x99, 0xc7, 0x23, 0x72, 0xcd, 0x34, 0xa9, 0x6e, 0x8b, 0xdb, + 0xaa, 0x5d, 0x29, 0x9b, 0xd2, 0xb6, 0x08, 0xc5, 0x9a, 0x63, 0x51, 0x3a, 0x0e, 0xcd, 0x0f, 0xa1, + 0xf5, 0x21, 0x6b, 0x24, 0x33, 0xf8, 0x78, 0xc4, 0xce, 0x94, 0x6c, 0xca, 0xf2, 0xdc, 0x57, 0x3a, + 0x16, 0xb2, 0x72, 0x08, 0x3b, 0x49, 0x6c, 0xaa, 0x6c, 0x22, 0xad, 0xcb, 0x94, 0x96, 0x69, 0x99, + 0x65, 0xbc, 0xb7, 0x34, 0x64, 0x7a, 0x31, 0xb9, 0x0d, 0x0d, 0x0d, 0xb1, 0xcc, 0xd9, 0xc8, 0x1e, + 0x51, 0x5b, 0x0a, 0x6c, 0xca, 0xc2, 0x9c, 0xb7, 0xe2, 0x84, 0x5a, 0x9b, 0x53, 0xe7, 0x6e, 0x60, + 0xea, 0x40, 0x72, 0x37, 0x60, 0x0a, 0x2f, 0x19, 0xfa, 0x63, 0x6a, 0x3a, 0xa7, 0xc2, 0x0d, 0xc3, + 0x31, 0x8f, 0xec, 0x48, 0x91, 0x85, 0x2a, 0x8f, 0x7b, 0x15, 0xc9, 0x5a, 0x6b, 0xac, 0x55, 0x7d, + 0xa5, 0xc6, 0xbb, 0xf0, 0x23, 0x04, 0x53, 0x39, 0xea, 0x16, 0xf0, 0x7c, 0x09, 0x0f, 0x79, 0xfa, + 0xc3, 0x8a, 0x64, 0xad, 0xea, 0x4e, 0xa3, 0x27, 0xdd, 0x45, 0x1a, 0x9c, 0xde, 0x4c, 0x30, 0x94, + 0x0d, 0xed, 0x0e, 0xa5, 0xd0, 0xbb, 0x0b, 0xaa, 0x3d, 0xdc, 0x40, 0x26, 0xf1, 0xa0, 0xf3, 0xd7, + 0x7f, 0x66, 0xc8, 0xb1, 0x5c, 0x87, 0x3f, 0x0b, 0x13, 0x20, 0x0e, 0xdc, 0xa3, 0x96, 0x25, 0x29, + 0x74, 0x5d, 0xb2, 0x2c, 0x55, 0x57, 0xd6, 0x9b, 0x1e, 0xbd, 0xe8, 0xde, 0x01, 0x95, 0x26, 0xa5, + 0x23, 0x10, 0x1b, 0xc3, 0x7d, 0xef, 0x34, 0x20, 0xba, 0x84, 0x9a, 0x1f, 0x84, 0xf1, 0xe8, 0x8a, + 0x79, 0x47, 0x93, 0x14, 0xef, 0xf2, 0x2e, 0x7c, 0x80, 0xa2, 0x6b, 0x20, 0x74, 0x00, 0xff, 0x12, + 0x3e, 0x6a, 0x86, 0xda, 0x60, 0xe3, 0x2d, 0x66, 0xcc, 0x8d, 0xb0, 0x4b, 0xb8, 0xa0, 0x44, 0xdc, + 0x09, 0xeb, 0x50, 0x68, 0xc1, 0x5b, 0x3a, 0xc7, 0xde, 0x35, 0x82, 0x0f, 0x39, 0xab, 0x8a, 0x73, + 0xdb, 0x74, 0x93, 0xd3, 0x63, 0xd7, 0xd9, 0x45, 0x73, 0x07, 0x8a, 0x33, 0xec, 0x11, 0x38, 0x7d, + 0x1d, 0x0f, 0x86, 0x14, 0x71, 0xa0, 0xd4, 0x09, 0x1d, 0x61, 0xe6, 0xa7, 0x33, 0xb8, 0x9b, 0x8d, + 0x4e, 0x3e, 0x45, 0x78, 0x30, 0x24, 0x86, 0x91, 0x37, 0x33, 0x86, 0x48, 0x97, 0x8c, 0xf3, 0x6f, + 0xb5, 0x6b, 0xee, 0x52, 0x17, 0x6e, 0x7d, 0xf0, 0xb7, 0xff, 0x7c, 0xd8, 0x75, 0x8d, 0x5c, 0x61, + 0x2a, 0xfc, 0xb4, 0xef, 0xb7, 0x8b, 0xa0, 0x7a, 0x0f, 0x76, 0xc5, 0x1d, 0x38, 0x10, 0xee, 0x16, + 0x77, 0xd8, 0x11, 0x70, 0x97, 0xfc, 0x16, 0x61, 0x12, 0xf2, 0xbe, 0xa0, 0x69, 0x7c, 0xbc, 0x12, + 0x45, 0x63, 0x3e, 0x5e, 0xc9, 0x42, 0xb0, 0x50, 0x60, 0xbc, 0x26, 0xc9, 0x39, 0x3e, 0x5e, 0xe4, + 0x73, 0x84, 0x5f, 0x89, 0xb2, 0x00, 0x8d, 0x8e, 0x2c, 0xb7, 0x87, 0x26, 0x28, 0x37, 0xe6, 0x6f, + 0xef, 0xd1, 0x0b, 0x50, 0x7b, 0x93, 0x51, 0xbb, 0x4c, 0xe6, 0xf8, 0xa8, 0x81, 0x39, 0x64, 0x6e, + 0x97, 0xfc, 0x17, 0xe1, 0xd1, 0x60, 0xdd, 0xfa, 0x88, 0x2e, 0x71, 0x42, 0x4c, 0x93, 0x55, 0xf3, + 0xcb, 0x7b, 0x73, 0x02, 0x34, 0x6f, 0x32, 0x9a, 0x57, 0xc9, 0xe5, 0x04, 0x9a, 0xaa, 0x9e, 0xcc, + 0x52, 0x54, 0xcb, 0xbb, 0xe4, 0x37, 0x08, 0x0f, 0x45, 0x88, 0x72, 0xd7, 0x65, 0xbc, 0xba, 0xc9, + 0x5d, 0x97, 0x09, 0x8a, 0x65, 0x66, 0x5d, 0x06, 0x59, 0x59, 0xe4, 0x13, 0x84, 0x07, 0x82, 0xbe, + 0xc8, 0x55, 0x1e, 0x08, 0xb1, 0x6b, 0x67, 0xfe, 0x5a, 0x3b, 0xa6, 0x80, 0x7c, 0x91, 0x21, 0xbf, + 0x41, 0xae, 0x71, 0x21, 0xf7, 0x25, 0xa2, 0xb8, 0x03, 0x8b, 0xf2, 0x2e, 0xf9, 0x7b, 0x33, 0x25, + 0x3e, 0x3d, 0xea, 0x26, 0xe7, 0x1a, 0x96, 0x24, 0xd2, 0xe5, 0x6f, 0xb5, 0xef, 0x00, 0xc8, 0xbd, + 0xc5, 0xc8, 0x5d, 0x21, 0xf3, 0xe9, 0xe4, 0x9a, 0x96, 0xc5, 0x1d, 0xdf, 0xa7, 0x5d, 0xf2, 0x19, + 0xc2, 0xc7, 0x63, 0x55, 0x4c, 0x72, 0xab, 0x85, 0x90, 0xc7, 0xea, 0xa8, 0xf9, 0x85, 0x3d, 0x78, + 0x68, 0x2d, 0x77, 0x41, 0xeb, 0x10, 0xc5, 0x4f, 0x10, 0x1e, 0x8e, 0x8c, 0xe2, 0xcc, 0xa8, 0x9b, + 0xad, 0x4d, 0x89, 0x36, 0xd3, 0x97, 0xa6, 0x9b, 0x0a, 0x17, 0x18, 0xbf, 0x29, 0x32, 0xc9, 0xcb, + 0x8f, 0xfc, 0x0c, 0x35, 0x95, 0x3a, 0x32, 0xcf, 0x59, 0x3f, 0x21, 0x49, 0x31, 0x7f, 0xb9, 0x65, + 0x3b, 0xc0, 0x5b, 0x64, 0x78, 0xdf, 0x20, 0x13, 0x09, 0x78, 0x15, 0x30, 0x70, 0x52, 0x50, 0xa6, + 0xf5, 0x5d, 0xf2, 0x63, 0x84, 0xfb, 0x3d, 0x2f, 0x4e, 0xcc, 0xe7, 0x39, 0x43, 0xd6, 0x16, 0xe2, + 0x18, 0x61, 0x53, 0x98, 0x60, 0x88, 0x5f, 0x23, 0xa7, 0x32, 0x10, 0x93, 0x5f, 0x21, 0x7c, 0x34, + 0x7c, 0xea, 0x26, 0xd7, 0x79, 0x86, 0x4d, 0xb8, 0x02, 0xe4, 0x6f, 0xb4, 0x67, 0xcc, 0x19, 0x6a, + 0x39, 0x8c, 0xf5, 0x0f, 0x08, 0xf7, 0xfb, 0x0e, 0xd6, 0x7c, 0x7b, 0x7f, 0xd6, 0x01, 0x9e, 0x6f, + 0xef, 0xcf, 0x3c, 0xdd, 0x0b, 0x53, 0x8c, 0xcd, 0xeb, 0x44, 0x48, 0x60, 0xe3, 0xbb, 0x8c, 0x90, + 0xa7, 0x28, 0xa2, 0x5d, 0x72, 0x9f, 0x36, 0xe3, 0x95, 0x57, 0xee, 0xd3, 0x66, 0x82, 0x9a, 0x2a, + 0xcc, 0x33, 0xf8, 0x17, 0x48, 0x21, 0x01, 0xbe, 0x16, 0xb4, 0x6b, 0x94, 0xbf, 0x73, 0xc6, 0x0c, + 0xf9, 0x6c, 0x65, 0x2f, 0xdf, 0x0b, 0x9b, 0x64, 0x6d, 0x38, 0x73, 0x2f, 0x0f, 0xb1, 0x21, 0x3f, + 0x44, 0xf8, 0x20, 0x5b, 0x7c, 0x66, 0x38, 0xc3, 0xe8, 0x5f, 0x24, 0x67, 0x5b, 0xb2, 0x01, 0x84, + 0xe7, 0x19, 0xc2, 0xb3, 0xe4, 0x4c, 0x52, 0xf1, 0xc3, 0x4e, 0xc6, 0x82, 0xfc, 0x73, 0x84, 0xfb, + 0x7d, 0x9a, 0x30, 0xdf, 0x39, 0x23, 0x56, 0x47, 0x6e, 0x0f, 0xec, 0x1c, 0x03, 0x5b, 0x24, 0xd3, + 0xa9, 0x60, 0x23, 0xf7, 0x8f, 0x1f, 0x20, 0x7c, 0xc8, 0xdb, 0x8a, 0x66, 0x38, 0x33, 0xda, 0x72, + 0x60, 0x43, 0xca, 0xaf, 0x70, 0x86, 0x61, 0x3d, 0x49, 0x5e, 0x4d, 0xc1, 0x4a, 0x3e, 0x76, 0x26, + 0x60, 0x50, 0x6f, 0x22, 0x5c, 0x27, 0xb0, 0x78, 0xd5, 0x36, 0x7f, 0xbd, 0x2d, 0x5b, 0xde, 0x95, + 0xc3, 0x07, 0xf2, 0x7f, 0x08, 0x8f, 0xa7, 0x0b, 0x65, 0x64, 0xb5, 0x0d, 0x2c, 0xf1, 0x8a, 0x5d, + 0xfe, 0xcb, 0x9d, 0x70, 0x05, 0x2c, 0xaf, 0x32, 0x96, 0xb3, 0xe4, 0x62, 0x36, 0xcb, 0x30, 0xa3, + 0x8f, 0x11, 0x1e, 0x08, 0xfe, 0xa7, 0x17, 0xdf, 0x0c, 0x88, 0xfd, 0xdf, 0x31, 0xbe, 0x93, 0x76, + 0xfc, 0x3f, 0x96, 0x09, 0xd3, 0x8c, 0xc4, 0x04, 0x39, 0x9b, 0x40, 0xe2, 0xfd, 0x20, 0x4a, 0x07, + 0x78, 0x50, 0x75, 0xe3, 0x03, 0x1e, 0xab, 0xe3, 0xf1, 0x01, 0x8f, 0x17, 0xf9, 0x32, 0x81, 0x6b, + 0x41, 0x94, 0xce, 0x51, 0x21, 0x2c, 0x0a, 0xf1, 0x1d, 0x15, 0x12, 0xe4, 0x2b, 0xbe, 0xa3, 0x42, + 0x92, 0xb4, 0x95, 0x79, 0x54, 0x08, 0x0b, 0x55, 0x61, 0x02, 0xec, 0xc7, 0x82, 0x96, 0x09, 0xf8, + 0x7f, 0xb1, 0x68, 0x99, 0x40, 0xe0, 0xf7, 0x89, 0x56, 0x08, 0xb8, 0x58, 0xff, 0x8c, 0xf0, 0xe1, + 0x07, 0x35, 0x7b, 0xa3, 0xbe, 0x4f, 0xd4, 0x28, 0x0e, 0x69, 0xa3, 0x81, 0x35, 0x66, 0x2b, 0xf8, + 0xa5, 0xab, 0xaf, 0x35, 0xba, 0xec, 0x03, 0x1d, 0x2a, 0x6b, 0x07, 0xf6, 0x33, 0x22, 0xff, 0x46, + 0xf8, 0x44, 0x08, 0xff, 0xbe, 0x54, 0xa0, 0xae, 0x31, 0x52, 0x97, 0xc8, 0x0c, 0x07, 0xa9, 0xb0, + 0xfc, 0xe4, 0xde, 0x94, 0xe3, 0x28, 0xee, 0x23, 0xed, 0xe9, 0x06, 0x23, 0x38, 0x4f, 0x2e, 0x25, + 0xde, 0x27, 0x13, 0xf8, 0x31, 0xe1, 0xe9, 0x17, 0x4c, 0xb3, 0x69, 0xab, 0x0a, 0x5f, 0x92, 0xea, + 0x94, 0xb5, 0xf9, 0xfb, 0xf8, 0x90, 0x67, 0x80, 0x7e, 0x7f, 0x09, 0x34, 0xd7, 0x19, 0x83, 0x39, + 0x32, 0x9b, 0xc2, 0x20, 0x51, 0x9d, 0xf9, 0x27, 0xc2, 0x24, 0x48, 0x69, 0xff, 0x48, 0x33, 0xd9, + 0x32, 0x67, 0x18, 0x77, 0x88, 0xdc, 0xef, 0x99, 0xa6, 0xe6, 0xef, 0xb4, 0x4f, 0x44, 0x99, 0xac, + 0xd3, 0x40, 0x90, 0x59, 0xbe, 0xfb, 0x5b, 0x9f, 0x7f, 0x34, 0x85, 0x16, 0x57, 0x9e, 0x3e, 0x1f, + 0x47, 0xcf, 0x9e, 0x8f, 0xa3, 0xcf, 0x9e, 0x8f, 0xa3, 0xef, 0xbd, 0x18, 0x3f, 0xf0, 0xec, 0xc5, + 0xf8, 0x81, 0x7f, 0xbc, 0x18, 0x3f, 0xf0, 0x68, 0x5a, 0x51, 0xed, 0x4a, 0x6d, 0xb3, 0x20, 0x1b, + 0x5b, 0x7e, 0x8f, 0xba, 0x51, 0xa6, 0xc5, 0xba, 0xdf, 0xb1, 0xfd, 0xa4, 0x4a, 0xad, 0xcd, 0x1e, + 0x76, 0x17, 0x9e, 0xfd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xa3, 0x7e, 0xf5, 0x1e, 0x32, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -4859,6 +4870,16 @@ func (m *QueryAllCctxRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Unordered { + i-- + if m.Unordered { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } if m.Pagination != nil { { size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i]) @@ -5952,6 +5973,9 @@ func (m *QueryAllCctxRequest) Size() (n int) { l = m.Pagination.Size() n += 1 + l + sovQuery(uint64(l)) } + if m.Unordered { + n += 2 + } return n } @@ -9003,6 +9027,26 @@ func (m *QueryAllCctxRequest) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Unordered", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Unordered = bool(v != 0) default: iNdEx = preIndex skippy, err := skipQuery(dAtA[iNdEx:]) From 12886428057e8e307050b273d231297c46663f28 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Mon, 24 Feb 2025 19:26:07 +0100 Subject: [PATCH 07/22] test: move all e2e test contracts in the same location (#3556) * move from pkg * remove unused contracts * generate * Revert "generate" This reverts commit bde10eac0fa188186ebd43fad39377751b0ef7ab. * Revert "remove unused contracts" This reverts commit b20e229d51a896937167002ad63c9e5b1e675cfb. * move testutil contracts in packages * move all contracts to e2e * fix test --- cmd/zetae2e/config/contracts.go | 2 +- .../contracts => e2e/contracts/dapp}/Dapp.abi | 0 .../contracts => e2e/contracts/dapp}/Dapp.bin | 0 .../contracts => e2e/contracts/dapp}/Dapp.go | 2 +- .../contracts/dapp}/Dapp.json | 0 .../contracts => e2e/contracts/dapp}/Dapp.sol | 0 e2e/contracts/dapp/bindings.go | 8 ++++ .../contracts/dappreverter}/DappReverter.abi | 0 .../contracts/dappreverter}/DappReverter.bin | 0 .../contracts/dappreverter}/DappReverter.go | 2 +- .../contracts/dappreverter}/DappReverter.json | 0 .../contracts/dappreverter}/DappReverter.sol | 0 e2e/contracts/dappreverter/bindings.go | 7 +++ .../contracts/depositor}/Depositor.abi | 0 .../contracts/depositor}/Depositor.bin | 0 .../contracts/depositor}/Depositor.go | 2 +- .../contracts/depositor}/Depositor.json | 0 .../contracts/depositor}/Depositor.sol | 0 e2e/contracts/depositor/bindings.go | 7 +++ .../contracts/erc1967proxy/ERC1967Proxy.abi | 0 .../contracts/erc1967proxy/ERC1967Proxy.bin | 0 .../contracts/erc1967proxy/ERC1967Proxy.go | 0 .../contracts/erc1967proxy/bindings.go | 0 .../contracts/example}/Example.abi | 0 .../contracts/example}/Example.bin | 0 .../contracts/example}/Example.go | 2 +- .../contracts/example}/Example.json | 0 .../contracts/example}/Example.sol | 0 e2e/contracts/example/bindings.go | 7 +++ .../gatewayzevmcaller/GatewayZEVMCaller.abi | 0 .../gatewayzevmcaller/GatewayZEVMCaller.bin | 0 .../gatewayzevmcaller/GatewayZEVMCaller.go | 0 .../gatewayzevmcaller/GatewayZEVMCaller.json | 0 .../gatewayzevmcaller/GatewayZEVMCaller.sol | 0 .../contracts/gatewayzevmcaller/bindings.go | 0 .../contracts/reverter}/Reverter.abi | 0 .../contracts/reverter}/Reverter.bin | 0 .../contracts/reverter}/Reverter.go | 2 +- .../contracts/reverter}/Reverter.json | 0 .../contracts/reverter}/Reverter.sol | 0 e2e/contracts/reverter/bindings.go | 7 +++ .../contracts/testdappv2/TestDAppV2.abi | 0 .../contracts/testdappv2/TestDAppV2.bin | 0 .../contracts/testdappv2/TestDAppV2.go | 0 .../contracts/testdappv2/TestDAppV2.json | 0 .../contracts/testdappv2/TestDAppV2.sol | 0 {pkg => e2e}/contracts/testdappv2/bindings.go | 0 .../contracts/withdrawerv2}/Withdrawer.abi | 0 .../contracts/withdrawerv2}/Withdrawer.bin | 0 .../contracts/withdrawerv2}/Withdrawer.go | 2 +- .../contracts/withdrawerv2}/Withdrawer.json | 0 .../contracts/withdrawerv2}/Withdrawer.sol | 0 e2e/contracts/withdrawerv2/bindings.go | 7 +++ .../legacy/test_erc20_multiple_deposits.go | 2 +- .../legacy/test_erc20_multiple_withdraws.go | 2 +- e2e/e2etests/legacy/test_eth_deposit_call.go | 5 +- e2e/e2etests/test_bitcoin_deposit_call.go | 2 +- .../test_bitcoin_std_deposit_and_call.go | 2 +- ...oin_std_memo_inscribed_deposit_and_call.go | 2 +- e2e/e2etests/test_deploy_contract.go | 2 +- ..._eth_withdraw_and_call_through_contract.go | 2 +- .../test_solana_deposit_and_call_revert.go | 2 +- e2e/e2etests/test_solana_deposit_call.go | 2 +- e2e/e2etests/test_spl_deposit_and_call.go | 2 +- e2e/e2etests/test_ton_deposit_and_call.go | 2 +- e2e/e2etests/test_ton_deposit_refund.go | 2 +- .../test_zevm_to_evm_call_through_contract.go | 2 +- e2e/runner/runner.go | 2 +- e2e/runner/setup_evm.go | 4 +- e2e/runner/setup_zevm.go | 4 +- e2e/runner/testdapp.go | 2 +- e2e/runner/zevm.go | 2 +- e2e/utils/contracts.go | 2 +- precompiles/bank/method_test.go | 2 +- precompiles/staking/staking_test.go | 2 +- testutil/contracts/bindings.go | 38 --------------- ...ctx_orchestrator_validate_outbound_test.go | 10 ++-- x/crosschain/keeper/utils_test.go | 2 +- x/fungible/keeper/deposits_test.go | 9 ++-- x/fungible/keeper/evm_test.go | 47 +++++++++---------- .../keeper/zevm_message_passing_test.go | 13 ++--- 81 files changed, 117 insertions(+), 110 deletions(-) rename {testutil/contracts => e2e/contracts/dapp}/Dapp.abi (100%) rename {testutil/contracts => e2e/contracts/dapp}/Dapp.bin (100%) rename {testutil/contracts => e2e/contracts/dapp}/Dapp.go (99%) rename {testutil/contracts => e2e/contracts/dapp}/Dapp.json (100%) rename {testutil/contracts => e2e/contracts/dapp}/Dapp.sol (100%) create mode 100644 e2e/contracts/dapp/bindings.go rename {testutil/contracts => e2e/contracts/dappreverter}/DappReverter.abi (100%) rename {testutil/contracts => e2e/contracts/dappreverter}/DappReverter.bin (100%) rename {testutil/contracts => e2e/contracts/dappreverter}/DappReverter.go (99%) rename {testutil/contracts => e2e/contracts/dappreverter}/DappReverter.json (100%) rename {testutil/contracts => e2e/contracts/dappreverter}/DappReverter.sol (100%) create mode 100644 e2e/contracts/dappreverter/bindings.go rename {testutil/contracts => e2e/contracts/depositor}/Depositor.abi (100%) rename {testutil/contracts => e2e/contracts/depositor}/Depositor.bin (100%) rename {testutil/contracts => e2e/contracts/depositor}/Depositor.go (99%) rename {testutil/contracts => e2e/contracts/depositor}/Depositor.json (100%) rename {testutil/contracts => e2e/contracts/depositor}/Depositor.sol (100%) create mode 100644 e2e/contracts/depositor/bindings.go rename {pkg => e2e}/contracts/erc1967proxy/ERC1967Proxy.abi (100%) rename {pkg => e2e}/contracts/erc1967proxy/ERC1967Proxy.bin (100%) rename {pkg => e2e}/contracts/erc1967proxy/ERC1967Proxy.go (100%) rename {pkg => e2e}/contracts/erc1967proxy/bindings.go (100%) rename {testutil/contracts => e2e/contracts/example}/Example.abi (100%) rename {testutil/contracts => e2e/contracts/example}/Example.bin (100%) rename {testutil/contracts => e2e/contracts/example}/Example.go (99%) rename {testutil/contracts => e2e/contracts/example}/Example.json (100%) rename {testutil/contracts => e2e/contracts/example}/Example.sol (100%) create mode 100644 e2e/contracts/example/bindings.go rename {pkg => e2e}/contracts/gatewayzevmcaller/GatewayZEVMCaller.abi (100%) rename {pkg => e2e}/contracts/gatewayzevmcaller/GatewayZEVMCaller.bin (100%) rename {pkg => e2e}/contracts/gatewayzevmcaller/GatewayZEVMCaller.go (100%) rename {pkg => e2e}/contracts/gatewayzevmcaller/GatewayZEVMCaller.json (100%) rename {pkg => e2e}/contracts/gatewayzevmcaller/GatewayZEVMCaller.sol (100%) rename {pkg => e2e}/contracts/gatewayzevmcaller/bindings.go (100%) rename {testutil/contracts => e2e/contracts/reverter}/Reverter.abi (100%) rename {testutil/contracts => e2e/contracts/reverter}/Reverter.bin (100%) rename {testutil/contracts => e2e/contracts/reverter}/Reverter.go (99%) rename {testutil/contracts => e2e/contracts/reverter}/Reverter.json (100%) rename {testutil/contracts => e2e/contracts/reverter}/Reverter.sol (100%) create mode 100644 e2e/contracts/reverter/bindings.go rename {pkg => e2e}/contracts/testdappv2/TestDAppV2.abi (100%) rename {pkg => e2e}/contracts/testdappv2/TestDAppV2.bin (100%) rename {pkg => e2e}/contracts/testdappv2/TestDAppV2.go (100%) rename {pkg => e2e}/contracts/testdappv2/TestDAppV2.json (100%) rename {pkg => e2e}/contracts/testdappv2/TestDAppV2.sol (100%) rename {pkg => e2e}/contracts/testdappv2/bindings.go (100%) rename {testutil/contracts => e2e/contracts/withdrawerv2}/Withdrawer.abi (100%) rename {testutil/contracts => e2e/contracts/withdrawerv2}/Withdrawer.bin (100%) rename {testutil/contracts => e2e/contracts/withdrawerv2}/Withdrawer.go (99%) rename {testutil/contracts => e2e/contracts/withdrawerv2}/Withdrawer.json (100%) rename {testutil/contracts => e2e/contracts/withdrawerv2}/Withdrawer.sol (100%) create mode 100644 e2e/contracts/withdrawerv2/bindings.go delete mode 100644 testutil/contracts/bindings.go diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 1c3f871d38..445067338c 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -19,11 +19,11 @@ import ( "github.com/zeta-chain/node/e2e/config" "github.com/zeta-chain/node/e2e/contracts/contextapp" "github.com/zeta-chain/node/e2e/contracts/erc20" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" "github.com/zeta-chain/node/e2e/contracts/zevmswap" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-core/contracts/uniswapv2factory.sol" uniswapv2router "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol" fungibletypes "github.com/zeta-chain/node/x/fungible/types" diff --git a/testutil/contracts/Dapp.abi b/e2e/contracts/dapp/Dapp.abi similarity index 100% rename from testutil/contracts/Dapp.abi rename to e2e/contracts/dapp/Dapp.abi diff --git a/testutil/contracts/Dapp.bin b/e2e/contracts/dapp/Dapp.bin similarity index 100% rename from testutil/contracts/Dapp.bin rename to e2e/contracts/dapp/Dapp.bin diff --git a/testutil/contracts/Dapp.go b/e2e/contracts/dapp/Dapp.go similarity index 99% rename from testutil/contracts/Dapp.go rename to e2e/contracts/dapp/Dapp.go index cfd07f12e4..c541cd20eb 100644 --- a/testutil/contracts/Dapp.go +++ b/e2e/contracts/dapp/Dapp.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package dapp import ( "errors" diff --git a/testutil/contracts/Dapp.json b/e2e/contracts/dapp/Dapp.json similarity index 100% rename from testutil/contracts/Dapp.json rename to e2e/contracts/dapp/Dapp.json diff --git a/testutil/contracts/Dapp.sol b/e2e/contracts/dapp/Dapp.sol similarity index 100% rename from testutil/contracts/Dapp.sol rename to e2e/contracts/dapp/Dapp.sol diff --git a/e2e/contracts/dapp/bindings.go b/e2e/contracts/dapp/bindings.go new file mode 100644 index 0000000000..d26bc4e504 --- /dev/null +++ b/e2e/contracts/dapp/bindings.go @@ -0,0 +1,8 @@ +// Dapp +// +//go:generate sh -c "solc --evm-version paris Dapp.sol --combined-json abi,bin | jq '.contracts.\"Dapp.sol:Dapp\"' > Dapp.json" +//go:generate sh -c "cat Dapp.json | jq .abi > Dapp.abi" +//go:generate sh -c "cat Dapp.json | jq .bin | tr -d '\"' > Dapp.bin" +//go:generate sh -c "abigen --abi Dapp.abi --bin Dapp.bin --pkg dapp --type Dapp --out Dapp.go" + +package dapp diff --git a/testutil/contracts/DappReverter.abi b/e2e/contracts/dappreverter/DappReverter.abi similarity index 100% rename from testutil/contracts/DappReverter.abi rename to e2e/contracts/dappreverter/DappReverter.abi diff --git a/testutil/contracts/DappReverter.bin b/e2e/contracts/dappreverter/DappReverter.bin similarity index 100% rename from testutil/contracts/DappReverter.bin rename to e2e/contracts/dappreverter/DappReverter.bin diff --git a/testutil/contracts/DappReverter.go b/e2e/contracts/dappreverter/DappReverter.go similarity index 99% rename from testutil/contracts/DappReverter.go rename to e2e/contracts/dappreverter/DappReverter.go index 450f44173c..af4783960b 100644 --- a/testutil/contracts/DappReverter.go +++ b/e2e/contracts/dappreverter/DappReverter.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package dappreverter import ( "errors" diff --git a/testutil/contracts/DappReverter.json b/e2e/contracts/dappreverter/DappReverter.json similarity index 100% rename from testutil/contracts/DappReverter.json rename to e2e/contracts/dappreverter/DappReverter.json diff --git a/testutil/contracts/DappReverter.sol b/e2e/contracts/dappreverter/DappReverter.sol similarity index 100% rename from testutil/contracts/DappReverter.sol rename to e2e/contracts/dappreverter/DappReverter.sol diff --git a/e2e/contracts/dappreverter/bindings.go b/e2e/contracts/dappreverter/bindings.go new file mode 100644 index 0000000000..9ecc3f7daf --- /dev/null +++ b/e2e/contracts/dappreverter/bindings.go @@ -0,0 +1,7 @@ +// DappReverter +//go:generate sh -c "solc --evm-version paris DappReverter.sol --combined-json abi,bin | jq '.contracts.\"DappReverter.sol:DappReverter\"' > DappReverter.json" +//go:generate sh -c "cat DappReverter.json | jq .abi > DappReverter.abi" +//go:generate sh -c "cat DappReverter.json | jq .bin | tr -d '\"' > DappReverter.bin" +//go:generate sh -c "abigen --abi DappReverter.abi --bin DappReverter.bin --pkg dappreverter --type DappReverter --out DappReverter.go" + +package dappreverter diff --git a/testutil/contracts/Depositor.abi b/e2e/contracts/depositor/Depositor.abi similarity index 100% rename from testutil/contracts/Depositor.abi rename to e2e/contracts/depositor/Depositor.abi diff --git a/testutil/contracts/Depositor.bin b/e2e/contracts/depositor/Depositor.bin similarity index 100% rename from testutil/contracts/Depositor.bin rename to e2e/contracts/depositor/Depositor.bin diff --git a/testutil/contracts/Depositor.go b/e2e/contracts/depositor/Depositor.go similarity index 99% rename from testutil/contracts/Depositor.go rename to e2e/contracts/depositor/Depositor.go index b545be3e03..9234b498da 100644 --- a/testutil/contracts/Depositor.go +++ b/e2e/contracts/depositor/Depositor.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package depositor import ( "errors" diff --git a/testutil/contracts/Depositor.json b/e2e/contracts/depositor/Depositor.json similarity index 100% rename from testutil/contracts/Depositor.json rename to e2e/contracts/depositor/Depositor.json diff --git a/testutil/contracts/Depositor.sol b/e2e/contracts/depositor/Depositor.sol similarity index 100% rename from testutil/contracts/Depositor.sol rename to e2e/contracts/depositor/Depositor.sol diff --git a/e2e/contracts/depositor/bindings.go b/e2e/contracts/depositor/bindings.go new file mode 100644 index 0000000000..9022af9951 --- /dev/null +++ b/e2e/contracts/depositor/bindings.go @@ -0,0 +1,7 @@ +// Depositor +//go:generate sh -c "solc --evm-version paris Depositor.sol --combined-json abi,bin | jq '.contracts.\"Depositor.sol:Depositor\"' > Depositor.json" +//go:generate sh -c "cat Depositor.json | jq .abi > Depositor.abi" +//go:generate sh -c "cat Depositor.json | jq .bin | tr -d '\"' > Depositor.bin" +//go:generate sh -c "abigen --abi Depositor.abi --bin Depositor.bin --pkg depositor --type Depositor --out Depositor.go" + +package depositor diff --git a/pkg/contracts/erc1967proxy/ERC1967Proxy.abi b/e2e/contracts/erc1967proxy/ERC1967Proxy.abi similarity index 100% rename from pkg/contracts/erc1967proxy/ERC1967Proxy.abi rename to e2e/contracts/erc1967proxy/ERC1967Proxy.abi diff --git a/pkg/contracts/erc1967proxy/ERC1967Proxy.bin b/e2e/contracts/erc1967proxy/ERC1967Proxy.bin similarity index 100% rename from pkg/contracts/erc1967proxy/ERC1967Proxy.bin rename to e2e/contracts/erc1967proxy/ERC1967Proxy.bin diff --git a/pkg/contracts/erc1967proxy/ERC1967Proxy.go b/e2e/contracts/erc1967proxy/ERC1967Proxy.go similarity index 100% rename from pkg/contracts/erc1967proxy/ERC1967Proxy.go rename to e2e/contracts/erc1967proxy/ERC1967Proxy.go diff --git a/pkg/contracts/erc1967proxy/bindings.go b/e2e/contracts/erc1967proxy/bindings.go similarity index 100% rename from pkg/contracts/erc1967proxy/bindings.go rename to e2e/contracts/erc1967proxy/bindings.go diff --git a/testutil/contracts/Example.abi b/e2e/contracts/example/Example.abi similarity index 100% rename from testutil/contracts/Example.abi rename to e2e/contracts/example/Example.abi diff --git a/testutil/contracts/Example.bin b/e2e/contracts/example/Example.bin similarity index 100% rename from testutil/contracts/Example.bin rename to e2e/contracts/example/Example.bin diff --git a/testutil/contracts/Example.go b/e2e/contracts/example/Example.go similarity index 99% rename from testutil/contracts/Example.go rename to e2e/contracts/example/Example.go index 1ded198bc4..b7bfc48582 100644 --- a/testutil/contracts/Example.go +++ b/e2e/contracts/example/Example.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package example import ( "errors" diff --git a/testutil/contracts/Example.json b/e2e/contracts/example/Example.json similarity index 100% rename from testutil/contracts/Example.json rename to e2e/contracts/example/Example.json diff --git a/testutil/contracts/Example.sol b/e2e/contracts/example/Example.sol similarity index 100% rename from testutil/contracts/Example.sol rename to e2e/contracts/example/Example.sol diff --git a/e2e/contracts/example/bindings.go b/e2e/contracts/example/bindings.go new file mode 100644 index 0000000000..2a6e3c7bc7 --- /dev/null +++ b/e2e/contracts/example/bindings.go @@ -0,0 +1,7 @@ +// Example +//go:generate sh -c "solc --evm-version paris Example.sol --combined-json abi,bin | jq '.contracts.\"Example.sol:Example\"' > Example.json" +//go:generate sh -c "cat Example.json | jq .abi > Example.abi" +//go:generate sh -c "cat Example.json | jq .bin | tr -d '\"' > Example.bin" +//go:generate sh -c "abigen --abi Example.abi --bin Example.bin --pkg example --type Example --out Example.go" + +package example diff --git a/pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.abi b/e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.abi similarity index 100% rename from pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.abi rename to e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.abi diff --git a/pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.bin b/e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.bin similarity index 100% rename from pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.bin rename to e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.bin diff --git a/pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.go b/e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.go similarity index 100% rename from pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.go rename to e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.go diff --git a/pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.json b/e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.json similarity index 100% rename from pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.json rename to e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.json diff --git a/pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.sol b/e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.sol similarity index 100% rename from pkg/contracts/gatewayzevmcaller/GatewayZEVMCaller.sol rename to e2e/contracts/gatewayzevmcaller/GatewayZEVMCaller.sol diff --git a/pkg/contracts/gatewayzevmcaller/bindings.go b/e2e/contracts/gatewayzevmcaller/bindings.go similarity index 100% rename from pkg/contracts/gatewayzevmcaller/bindings.go rename to e2e/contracts/gatewayzevmcaller/bindings.go diff --git a/testutil/contracts/Reverter.abi b/e2e/contracts/reverter/Reverter.abi similarity index 100% rename from testutil/contracts/Reverter.abi rename to e2e/contracts/reverter/Reverter.abi diff --git a/testutil/contracts/Reverter.bin b/e2e/contracts/reverter/Reverter.bin similarity index 100% rename from testutil/contracts/Reverter.bin rename to e2e/contracts/reverter/Reverter.bin diff --git a/testutil/contracts/Reverter.go b/e2e/contracts/reverter/Reverter.go similarity index 99% rename from testutil/contracts/Reverter.go rename to e2e/contracts/reverter/Reverter.go index 7d9cb13bc3..8872ad655d 100644 --- a/testutil/contracts/Reverter.go +++ b/e2e/contracts/reverter/Reverter.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package reverter import ( "errors" diff --git a/testutil/contracts/Reverter.json b/e2e/contracts/reverter/Reverter.json similarity index 100% rename from testutil/contracts/Reverter.json rename to e2e/contracts/reverter/Reverter.json diff --git a/testutil/contracts/Reverter.sol b/e2e/contracts/reverter/Reverter.sol similarity index 100% rename from testutil/contracts/Reverter.sol rename to e2e/contracts/reverter/Reverter.sol diff --git a/e2e/contracts/reverter/bindings.go b/e2e/contracts/reverter/bindings.go new file mode 100644 index 0000000000..9c37a2f4cf --- /dev/null +++ b/e2e/contracts/reverter/bindings.go @@ -0,0 +1,7 @@ +// Reverter +//go:generate sh -c "solc --evm-version paris Reverter.sol --combined-json abi,bin | jq '.contracts.\"Reverter.sol:Reverter\"' > Reverter.json" +//go:generate sh -c "cat Reverter.json | jq .abi > Reverter.abi" +//go:generate sh -c "cat Reverter.json | jq .bin | tr -d '\"' > Reverter.bin" +//go:generate sh -c "abigen --abi Reverter.abi --bin Reverter.bin --pkg reverter --type Reverter --out Reverter.go" + +package reverter diff --git a/pkg/contracts/testdappv2/TestDAppV2.abi b/e2e/contracts/testdappv2/TestDAppV2.abi similarity index 100% rename from pkg/contracts/testdappv2/TestDAppV2.abi rename to e2e/contracts/testdappv2/TestDAppV2.abi diff --git a/pkg/contracts/testdappv2/TestDAppV2.bin b/e2e/contracts/testdappv2/TestDAppV2.bin similarity index 100% rename from pkg/contracts/testdappv2/TestDAppV2.bin rename to e2e/contracts/testdappv2/TestDAppV2.bin diff --git a/pkg/contracts/testdappv2/TestDAppV2.go b/e2e/contracts/testdappv2/TestDAppV2.go similarity index 100% rename from pkg/contracts/testdappv2/TestDAppV2.go rename to e2e/contracts/testdappv2/TestDAppV2.go diff --git a/pkg/contracts/testdappv2/TestDAppV2.json b/e2e/contracts/testdappv2/TestDAppV2.json similarity index 100% rename from pkg/contracts/testdappv2/TestDAppV2.json rename to e2e/contracts/testdappv2/TestDAppV2.json diff --git a/pkg/contracts/testdappv2/TestDAppV2.sol b/e2e/contracts/testdappv2/TestDAppV2.sol similarity index 100% rename from pkg/contracts/testdappv2/TestDAppV2.sol rename to e2e/contracts/testdappv2/TestDAppV2.sol diff --git a/pkg/contracts/testdappv2/bindings.go b/e2e/contracts/testdappv2/bindings.go similarity index 100% rename from pkg/contracts/testdappv2/bindings.go rename to e2e/contracts/testdappv2/bindings.go diff --git a/testutil/contracts/Withdrawer.abi b/e2e/contracts/withdrawerv2/Withdrawer.abi similarity index 100% rename from testutil/contracts/Withdrawer.abi rename to e2e/contracts/withdrawerv2/Withdrawer.abi diff --git a/testutil/contracts/Withdrawer.bin b/e2e/contracts/withdrawerv2/Withdrawer.bin similarity index 100% rename from testutil/contracts/Withdrawer.bin rename to e2e/contracts/withdrawerv2/Withdrawer.bin diff --git a/testutil/contracts/Withdrawer.go b/e2e/contracts/withdrawerv2/Withdrawer.go similarity index 99% rename from testutil/contracts/Withdrawer.go rename to e2e/contracts/withdrawerv2/Withdrawer.go index 2e3c6aad5a..44fe1bc139 100644 --- a/testutil/contracts/Withdrawer.go +++ b/e2e/contracts/withdrawerv2/Withdrawer.go @@ -1,7 +1,7 @@ // Code generated - DO NOT EDIT. // This file is a generated binding and any manual changes will be lost. -package contracts +package withdrawerv2 import ( "errors" diff --git a/testutil/contracts/Withdrawer.json b/e2e/contracts/withdrawerv2/Withdrawer.json similarity index 100% rename from testutil/contracts/Withdrawer.json rename to e2e/contracts/withdrawerv2/Withdrawer.json diff --git a/testutil/contracts/Withdrawer.sol b/e2e/contracts/withdrawerv2/Withdrawer.sol similarity index 100% rename from testutil/contracts/Withdrawer.sol rename to e2e/contracts/withdrawerv2/Withdrawer.sol diff --git a/e2e/contracts/withdrawerv2/bindings.go b/e2e/contracts/withdrawerv2/bindings.go new file mode 100644 index 0000000000..4ad00f4730 --- /dev/null +++ b/e2e/contracts/withdrawerv2/bindings.go @@ -0,0 +1,7 @@ +// Withdrawer +//go:generate sh -c "solc --evm-version paris Withdrawer.sol --combined-json abi,bin | jq '.contracts.\"Withdrawer.sol:Withdrawer\"' > Withdrawer.json" +//go:generate sh -c "cat Withdrawer.json | jq .abi > Withdrawer.abi" +//go:generate sh -c "cat Withdrawer.json | jq .bin | tr -d '\"' > Withdrawer.bin" +//go:generate sh -c "abigen --abi Withdrawer.abi --bin Withdrawer.bin --pkg withdrawerv2 --type Withdrawer --out Withdrawer.go" + +package withdrawerv2 diff --git a/e2e/e2etests/legacy/test_erc20_multiple_deposits.go b/e2e/e2etests/legacy/test_erc20_multiple_deposits.go index d11d410206..634c5d06b3 100644 --- a/e2e/e2etests/legacy/test_erc20_multiple_deposits.go +++ b/e2e/e2etests/legacy/test_erc20_multiple_deposits.go @@ -7,9 +7,9 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/depositor" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" ) func TestMultipleERC20Deposit(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/legacy/test_erc20_multiple_withdraws.go b/e2e/e2etests/legacy/test_erc20_multiple_withdraws.go index 08f06b5986..f3c581222c 100644 --- a/e2e/e2etests/legacy/test_erc20_multiple_withdraws.go +++ b/e2e/e2etests/legacy/test_erc20_multiple_withdraws.go @@ -6,9 +6,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/withdrawerv2" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" ) func TestMultipleERC20Withdraws(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/legacy/test_eth_deposit_call.go b/e2e/e2etests/legacy/test_eth_deposit_call.go index b0d63ebbf1..c206906026 100644 --- a/e2e/e2etests/legacy/test_eth_deposit_call.go +++ b/e2e/e2etests/legacy/test_eth_deposit_call.go @@ -4,9 +4,10 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/e2e/contracts/example" + testcontract "github.com/zeta-chain/node/e2e/contracts/reverter" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" "github.com/zeta-chain/node/x/crosschain/types" ) @@ -18,7 +19,7 @@ func TestEtherDepositAndCall(r *runner.E2ERunner, args []string) { value := utils.ParseBigInt(r, args[0]) r.Logger.Info("Deploying example contract") - exampleAddr, _, exampleContract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) + exampleAddr, _, exampleContract, err := example.DeployExample(r.ZEVMAuth, r.ZEVMClient) require.NoError(r, err) r.Logger.Info("Example contract deployed") diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index 3cd7491192..b74f1061f0 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -5,9 +5,9 @@ import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go index 77af601aa2..1f99732500 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go @@ -5,10 +5,10 @@ import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/memo" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) 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 92f907a20a..baf83ee05a 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 @@ -5,10 +5,10 @@ import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/memo" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) diff --git a/e2e/e2etests/test_deploy_contract.go b/e2e/e2etests/test_deploy_contract.go index 4a0c8474da..4d93de03ac 100644 --- a/e2e/e2etests/test_deploy_contract.go +++ b/e2e/e2etests/test_deploy_contract.go @@ -6,9 +6,9 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" ) // deployFunc is a function that deploys a contract diff --git a/e2e/e2etests/test_eth_withdraw_and_call_through_contract.go b/e2e/e2etests/test_eth_withdraw_and_call_through_contract.go index 3e92b70a70..71c6e002ea 100644 --- a/e2e/e2etests/test_eth_withdraw_and_call_through_contract.go +++ b/e2e/e2etests/test_eth_withdraw_and_call_through_contract.go @@ -6,9 +6,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/e2e/contracts/gatewayzevmcaller" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - gatewayzevmcaller "github.com/zeta-chain/node/pkg/contracts/gatewayzevmcaller" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/e2etests/test_solana_deposit_and_call_revert.go b/e2e/e2etests/test_solana_deposit_and_call_revert.go index 4b0791a3c8..18a1cffd87 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_revert.go +++ b/e2e/e2etests/test_solana_deposit_and_call_revert.go @@ -3,9 +3,9 @@ package e2etests import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/reverter" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/e2etests/test_solana_deposit_call.go b/e2e/e2etests/test_solana_deposit_call.go index 9a7bc566ba..f506844fe2 100644 --- a/e2e/e2etests/test_solana_deposit_call.go +++ b/e2e/e2etests/test_solana_deposit_call.go @@ -3,9 +3,9 @@ package e2etests import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/e2etests/test_spl_deposit_and_call.go b/e2e/e2etests/test_spl_deposit_and_call.go index 945147abc2..4987f86b9a 100644 --- a/e2e/e2etests/test_spl_deposit_and_call.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -7,9 +7,9 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go index 44fde2cb8d..0f329a272d 100644 --- a/e2e/e2etests/test_ton_deposit_and_call.go +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -4,10 +4,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" - testcontract "github.com/zeta-chain/node/testutil/contracts" ) func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_ton_deposit_refund.go b/e2e/e2etests/test_ton_deposit_refund.go index 47d45c840f..bcce8b7a72 100644 --- a/e2e/e2etests/test_ton_deposit_refund.go +++ b/e2e/e2etests/test_ton_deposit_refund.go @@ -3,9 +3,9 @@ package e2etests import ( "github.com/stretchr/testify/require" + testcontract "github.com/zeta-chain/node/e2e/contracts/reverter" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - testcontract "github.com/zeta-chain/node/testutil/contracts" cctypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/e2etests/test_zevm_to_evm_call_through_contract.go b/e2e/e2etests/test_zevm_to_evm_call_through_contract.go index 1417513836..05f2cab5f3 100644 --- a/e2e/e2etests/test_zevm_to_evm_call_through_contract.go +++ b/e2e/e2etests/test_zevm_to_evm_call_through_contract.go @@ -6,9 +6,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/e2e/contracts/gatewayzevmcaller" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - gatewayzevmcaller "github.com/zeta-chain/node/pkg/contracts/gatewayzevmcaller" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index bfb0027fe7..41beb314ac 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -34,12 +34,12 @@ import ( "github.com/zeta-chain/node/e2e/config" "github.com/zeta-chain/node/e2e/contracts/contextapp" "github.com/zeta-chain/node/e2e/contracts/erc20" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" "github.com/zeta-chain/node/e2e/contracts/zevmswap" tonrunner "github.com/zeta-chain/node/e2e/runner/ton" "github.com/zeta-chain/node/e2e/txserver" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/constant" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-core/contracts/uniswapv2factory.sol" uniswapv2router "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol" diff --git a/e2e/runner/setup_evm.go b/e2e/runner/setup_evm.go index 91923a845c..651e9b83d6 100644 --- a/e2e/runner/setup_evm.go +++ b/e2e/runner/setup_evm.go @@ -10,10 +10,10 @@ import ( erc20custodyv2 "github.com/zeta-chain/protocol-contracts/pkg/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" + "github.com/zeta-chain/node/e2e/contracts/erc1967proxy" "github.com/zeta-chain/node/e2e/contracts/erc20" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" ) // SetupEVM setup contracts on EVM with v2 contracts diff --git a/e2e/runner/setup_zevm.go b/e2e/runner/setup_zevm.go index 172bd54330..f8fb26ba79 100644 --- a/e2e/runner/setup_zevm.go +++ b/e2e/runner/setup_zevm.go @@ -15,11 +15,11 @@ import ( connectorzevm "github.com/zeta-chain/protocol-contracts/pkg/zetaconnectorzevm.sol" "github.com/zeta-chain/protocol-contracts/pkg/zrc20.sol" + "github.com/zeta-chain/node/e2e/contracts/erc1967proxy" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" "github.com/zeta-chain/node/e2e/txserver" e2eutils "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-core/contracts/uniswapv2factory.sol" uniswapv2router "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol" fungibletypes "github.com/zeta-chain/node/x/fungible/types" diff --git a/e2e/runner/testdapp.go b/e2e/runner/testdapp.go index f9c63d7c67..85f5272296 100644 --- a/e2e/runner/testdapp.go +++ b/e2e/runner/testdapp.go @@ -7,7 +7,7 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" ) // AssertTestDAppZEVMCalled is a function that asserts the values of the test dapp on the ZEVM diff --git a/e2e/runner/zevm.go b/e2e/runner/zevm.go index a42b8e2a4f..d2af986062 100644 --- a/e2e/runner/zevm.go +++ b/e2e/runner/zevm.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" + "github.com/zeta-chain/node/e2e/contracts/gatewayzevmcaller" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/contracts/gatewayzevmcaller" "github.com/zeta-chain/node/pkg/retry" "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" diff --git a/e2e/utils/contracts.go b/e2e/utils/contracts.go index 68893234e1..d60899ad2f 100644 --- a/e2e/utils/contracts.go +++ b/e2e/utils/contracts.go @@ -6,7 +6,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" - testcontract "github.com/zeta-chain/node/testutil/contracts" + testcontract "github.com/zeta-chain/node/e2e/contracts/example" ) const ( diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index 7d2a26b4bd..cde8597971 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -1,6 +1,7 @@ package bank import ( + "github.com/zeta-chain/node/e2e/contracts/erc1967proxy" "math/big" "testing" @@ -18,7 +19,6 @@ import ( evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" "github.com/zeta-chain/ethermint/x/evm/statedb" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" "github.com/zeta-chain/node/pkg/ptr" precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/testutil/keeper" diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index 7773490e1d..bd86951579 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -2,6 +2,7 @@ package staking import ( "encoding/json" + "github.com/zeta-chain/node/e2e/contracts/erc1967proxy" "testing" "math/big" @@ -28,7 +29,6 @@ import ( "github.com/zeta-chain/ethermint/x/evm/statedb" "github.com/zeta-chain/node/cmd/zetacored/config" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/precompiles/prototype" "github.com/zeta-chain/node/testutil/keeper" diff --git a/testutil/contracts/bindings.go b/testutil/contracts/bindings.go deleted file mode 100644 index 70db08dca4..0000000000 --- a/testutil/contracts/bindings.go +++ /dev/null @@ -1,38 +0,0 @@ -// Example -//go:generate sh -c "solc --evm-version paris Example.sol --combined-json abi,bin | jq '.contracts.\"Example.sol:Example\"' > Example.json" -//go:generate sh -c "cat Example.json | jq .abi > Example.abi" -//go:generate sh -c "cat Example.json | jq .bin | tr -d '\"' > Example.bin" -//go:generate sh -c "abigen --abi Example.abi --bin Example.bin --pkg contracts --type Example --out Example.go" - -// Reverter -//go:generate sh -c "solc --evm-version paris Reverter.sol --combined-json abi,bin | jq '.contracts.\"Reverter.sol:Reverter\"' > Reverter.json" -//go:generate sh -c "cat Reverter.json | jq .abi > Reverter.abi" -//go:generate sh -c "cat Reverter.json | jq .bin | tr -d '\"' > Reverter.bin" -//go:generate sh -c "abigen --abi Reverter.abi --bin Reverter.bin --pkg contracts --type Reverter --out Reverter.go" - -// Depositor -//go:generate sh -c "solc --evm-version paris Depositor.sol --combined-json abi,bin | jq '.contracts.\"Depositor.sol:Depositor\"' > Depositor.json" -//go:generate sh -c "cat Depositor.json | jq .abi > Depositor.abi" -//go:generate sh -c "cat Depositor.json | jq .bin | tr -d '\"' > Depositor.bin" -//go:generate sh -c "abigen --abi Depositor.abi --bin Depositor.bin --pkg contracts --type Depositor --out Depositor.go" - -// Withdrawer -//go:generate sh -c "solc --evm-version paris Withdrawer.sol --combined-json abi,bin | jq '.contracts.\"Withdrawer.sol:Withdrawer\"' > Withdrawer.json" -//go:generate sh -c "cat Withdrawer.json | jq .abi > Withdrawer.abi" -//go:generate sh -c "cat Withdrawer.json | jq .bin | tr -d '\"' > Withdrawer.bin" -//go:generate sh -c "abigen --abi Withdrawer.abi --bin Withdrawer.bin --pkg contracts --type Withdrawer --out Withdrawer.go" - -// Dapp -// -//go:generate sh -c "solc --evm-version paris Dapp.sol --combined-json abi,bin | jq '.contracts.\"Dapp.sol:Dapp\"' > Dapp.json" -//go:generate sh -c "cat Dapp.json | jq .abi > Dapp.abi" -//go:generate sh -c "cat Dapp.json | jq .bin | tr -d '\"' > Dapp.bin" -//go:generate sh -c "abigen --abi Dapp.abi --bin Dapp.bin --pkg contracts --type Dapp --out Dapp.go" - -// DappReverter -//go:generate sh -c "solc --evm-version paris DappReverter.sol --combined-json abi,bin | jq '.contracts.\"DappReverter.sol:DappReverter\"' > DappReverter.json" -//go:generate sh -c "cat DappReverter.json | jq .abi > DappReverter.abi" -//go:generate sh -c "cat DappReverter.json | jq .bin | tr -d '\"' > DappReverter.bin" -//go:generate sh -c "abigen --abi DappReverter.abi --bin DappReverter.bin --pkg contracts --type DappReverter --out DappReverter.go" - -package contracts diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_outbound_test.go b/x/crosschain/keeper/cctx_orchestrator_validate_outbound_test.go index bd4a986344..02f5c3f0d9 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_outbound_test.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_outbound_test.go @@ -3,20 +3,20 @@ package keeper_test import ( "encoding/base64" "errors" - "github.com/ethereum/go-ethereum/core/vm" - evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "github.com/zeta-chain/node/e2e/contracts/dapp" "math/big" "testing" cosmoserror "cosmossdk.io/errors" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/ethermint/x/evm/statedb" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/testutil/contracts" keepertest "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/x/crosschain/types" @@ -408,7 +408,7 @@ func TestKeeper_ValidateFailedOutbound(t *testing.T) { cctx.RelayedMessage = base64.StdEncoding.EncodeToString([]byte("sample message")) deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) - dAppContract, err := zk.FungibleKeeper.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := zk.FungibleKeeper.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) cctx.InboundParams.Sender = dAppContract.String() @@ -424,7 +424,7 @@ func TestKeeper_ValidateFailedOutbound(t *testing.T) { require.Equal(t, types.CctxStatus_Reverted, cctx.CctxStatus.Status) require.Equal(t, cctx.GetCurrentOutboundParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) - dappAbi, err := contracts.DappMetaData.GetAbi() + dappAbi, err := dapp.DappMetaData.GetAbi() require.NoError(t, err) res, err := zk.FungibleKeeper.CallEVM( ctx, diff --git a/x/crosschain/keeper/utils_test.go b/x/crosschain/keeper/utils_test.go index 0c4ee9bfe1..1b277aad25 100644 --- a/x/crosschain/keeper/utils_test.go +++ b/x/crosschain/keeper/utils_test.go @@ -2,6 +2,7 @@ package keeper_test import ( + "github.com/zeta-chain/node/e2e/contracts/erc1967proxy" "math/big" "testing" @@ -12,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" - "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol" "github.com/zeta-chain/node/pkg/ptr" fungibletypes "github.com/zeta-chain/node/x/fungible/types" diff --git a/x/fungible/keeper/deposits_test.go b/x/fungible/keeper/deposits_test.go index e75bc5aa20..7c9f3e2a15 100644 --- a/x/fungible/keeper/deposits_test.go +++ b/x/fungible/keeper/deposits_test.go @@ -5,7 +5,9 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/zeta-chain/node/cmd/zetacored/config" - "github.com/zeta-chain/node/testutil/contracts" + "github.com/zeta-chain/node/e2e/contracts/example" + "github.com/zeta-chain/node/e2e/contracts/reverter" + "github.com/zeta-chain/node/e2e/contracts/testdappv2" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "math/big" "testing" @@ -15,7 +17,6 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/contracts/testdappv2" keepertest "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -414,7 +415,7 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chain, "foobar", "foobar") - example, err := k.DeployContract(ctx, contracts.ExampleMetaData) + example, err := k.DeployContract(ctx, example.ExampleMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, example) @@ -454,7 +455,7 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chain, "foobar", "foobar") - reverter, err := k.DeployContract(ctx, contracts.ReverterMetaData) + reverter, err := k.DeployContract(ctx, reverter.ReverterMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, reverter) diff --git a/x/fungible/keeper/evm_test.go b/x/fungible/keeper/evm_test.go index c9d3d98674..e124ff42a0 100644 --- a/x/fungible/keeper/evm_test.go +++ b/x/fungible/keeper/evm_test.go @@ -3,33 +3,32 @@ package keeper_test import ( "encoding/json" "fmt" - "math/big" "testing" sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - "github.com/zeta-chain/protocol-contracts/pkg/erc1967proxy.sol" "github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol" "github.com/zeta-chain/protocol-contracts/pkg/systemcontract.sol" "github.com/zeta-chain/protocol-contracts/pkg/wzeta.sol" "github.com/zeta-chain/protocol-contracts/pkg/zrc20.sol" + "github.com/zeta-chain/node/e2e/contracts/dapp" + "github.com/zeta-chain/node/e2e/contracts/dappreverter" + "github.com/zeta-chain/node/e2e/contracts/example" + "github.com/zeta-chain/node/e2e/contracts/reverter" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/server/config" - "github.com/zeta-chain/node/testutil/contracts" keepertest "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -202,7 +201,7 @@ func assertExampleBarValue( address common.Address, expected int64, ) { - exampleABI, err := contracts.ExampleMetaData.GetAbi() + exampleABI, err := example.ExampleMetaData.GetAbi() require.NoError(t, err) res, err := k.CallEVM( ctx, @@ -563,7 +562,7 @@ func TestKeeper_DepositZRC20AndCallContract(t *testing.T) { chainID := getValidChainID(t) - example, err := k.DeployContract(ctx, contracts.ExampleMetaData) + example, err := k.DeployContract(ctx, example.ExampleMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, example) @@ -591,9 +590,9 @@ func TestKeeper_DepositZRC20AndCallContract(t *testing.T) { deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chainID, "foobar", "FOOBAR") - example, err := k.DeployContract(ctx, contracts.ExampleMetaData) + exampleContract, err := k.DeployContract(ctx, example.ExampleMetaData) require.NoError(t, err) - assertContractDeployment(t, sdkk.EvmKeeper, ctx, example) + assertContractDeployment(t, sdkk.EvmKeeper, ctx, exampleContract) res, err := k.CallDepositAndCall( ctx, @@ -603,24 +602,24 @@ func TestKeeper_DepositZRC20AndCallContract(t *testing.T) { ChainID: big.NewInt(chainID), }, zrc20, - example, + exampleContract, big.NewInt(42), []byte(""), ) require.NoError(t, err) require.False(t, types.IsContractReverted(res, err)) - balance, err := k.BalanceOfZRC4(ctx, zrc20, example) + balance, err := k.BalanceOfZRC4(ctx, zrc20, exampleContract) require.NoError(t, err) require.Equal(t, int64(42), balance.Int64()) // check onCrossChainCall has been called - exampleABI, err := contracts.ExampleMetaData.GetAbi() + exampleABI, err := example.ExampleMetaData.GetAbi() require.NoError(t, err) res, err = k.CallEVM( ctx, *exampleABI, types.ModuleAddressEVM, - example, + exampleContract, big.NewInt(0), nil, false, @@ -645,7 +644,7 @@ func TestKeeper_DepositZRC20AndCallContract(t *testing.T) { zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chainID, "foobar", "FOOBAR") // Deploy reverter - reverter, err := k.DeployContract(ctx, contracts.ReverterMetaData) + reverter, err := k.DeployContract(ctx, reverter.ReverterMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, reverter) @@ -697,10 +696,10 @@ func TestKeeper_CallEVMWithData(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) // Deploy example - contract, err := k.DeployContract(ctx, contracts.ExampleMetaData) + contract, err := k.DeployContract(ctx, example.ExampleMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, contract) - abi, err := contracts.ExampleMetaData.GetAbi() + abi, err := example.ExampleMetaData.GetAbi() require.NoError(t, err) // doRevert make contract reverted @@ -1486,7 +1485,7 @@ func TestKeeper_CallOnReceiveZevmConnector(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -1510,7 +1509,7 @@ func TestKeeper_CallOnReceiveZevmConnector(t *testing.T) { require.NoError(t, err) - dappAbi, err := contracts.DappMetaData.GetAbi() + dappAbi, err := dapp.DappMetaData.GetAbi() require.NoError(t, err) res, err := k.CallEVM( ctx, @@ -1536,7 +1535,7 @@ func TestKeeper_CallOnReceiveZevmConnector(t *testing.T) { k, ctx, sdkk, _ := keepertest.FungibleKeeper(t) _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -1553,7 +1552,7 @@ func TestKeeper_CallOnReceiveZevmConnector(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappReverterMetaData) + dAppContract, err := k.DeployContract(ctx, dappreverter.DappReverterMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -1572,7 +1571,7 @@ func TestKeeper_CallOnRevertZevmConnector(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) senderAddress := dAppContract @@ -1594,7 +1593,7 @@ func TestKeeper_CallOnRevertZevmConnector(t *testing.T) { ) require.NoError(t, err) - dappAbi, err := contracts.DappMetaData.GetAbi() + dappAbi, err := dapp.DappMetaData.GetAbi() require.NoError(t, err) res, err := k.CallEVM( ctx, @@ -1620,7 +1619,7 @@ func TestKeeper_CallOnRevertZevmConnector(t *testing.T) { k, ctx, sdkk, _ := keepertest.FungibleKeeper(t) _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -1638,7 +1637,7 @@ func TestKeeper_CallOnRevertZevmConnector(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappReverterMetaData) + dAppContract, err := k.DeployContract(ctx, dappreverter.DappReverterMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) diff --git a/x/fungible/keeper/zevm_message_passing_test.go b/x/fungible/keeper/zevm_message_passing_test.go index 9b89adb2cc..a09f9f241f 100644 --- a/x/fungible/keeper/zevm_message_passing_test.go +++ b/x/fungible/keeper/zevm_message_passing_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "github.com/zeta-chain/node/e2e/contracts/dapp" + "github.com/zeta-chain/node/e2e/contracts/dappreverter" "math/big" "testing" @@ -14,7 +16,6 @@ import ( "github.com/zeta-chain/ethermint/x/evm/statedb" "github.com/zeta-chain/node/cmd/zetacored/config" - "github.com/zeta-chain/node/testutil/contracts" keepertest "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/x/fungible/types" @@ -26,7 +27,7 @@ func TestKeeper_ZEVMDepositAndCallContract(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -48,7 +49,7 @@ func TestKeeper_ZEVMDepositAndCallContract(t *testing.T) { ) require.NoError(t, err) - dappAbi, err := contracts.DappMetaData.GetAbi() + dappAbi, err := dapp.DappMetaData.GetAbi() require.NoError(t, err) res, err := k.CallEVM( ctx, @@ -172,7 +173,7 @@ func TestKeeper_ZEVMRevertAndCallContract(t *testing.T) { _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) - dAppContract, err := k.DeployContract(ctx, contracts.DappMetaData) + dAppContract, err := k.DeployContract(ctx, dapp.DappMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) @@ -196,7 +197,7 @@ func TestKeeper_ZEVMRevertAndCallContract(t *testing.T) { ) require.NoError(t, err) - dappAbi, err := contracts.DappMetaData.GetAbi() + dappAbi, err := dapp.DappMetaData.GetAbi() require.NoError(t, err) res, err := k.CallEVM( ctx, @@ -323,7 +324,7 @@ func TestKeeper_ZEVMRevertAndCallContract(t *testing.T) { k, ctx, sdkk, _ := keepertest.FungibleKeeper(t) _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) - dAppContract, err := k.DeployContract(ctx, contracts.DappReverterMetaData) + dAppContract, err := k.DeployContract(ctx, dappreverter.DappReverterMetaData) require.NoError(t, err) assertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract) From 2c7e6d3c8b2e201331bfdb7bcdbf236e0276c6d2 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 24 Feb 2025 13:17:46 -0800 Subject: [PATCH 08/22] refactor(zetaclient): make EVM getLogs call once (#3570) * refactor(zetaclient): make EVM getLogs call once * Also combine v1 getLogs calls --- zetaclient/chains/evm/observer/inbound.go | 154 ++++++++++------ zetaclient/chains/evm/observer/v2_inbound.go | 177 +++++++------------ 2 files changed, 164 insertions(+), 167 deletions(-) diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 6b62216b5e..ec320dbad1 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -12,7 +12,7 @@ import ( "strings" sdkmath "cosmossdk.io/math" - "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" @@ -140,18 +140,6 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, Str(logs.FieldMethod, "observeInboundInBlockRange"). Uint64("start_block", startBlock).Uint64("to_block", toBlock).Logger() - // task 1: query evm chain for zeta sent logs (read at most 100 blocks in one go) - lastScannedZetaSent, err := ob.ObserveZetaSent(ctx, startBlock, toBlock) - if err != nil { - logger.Error().Err(err).Msg("error observing zeta sent events from ZetaConnector contract") - } - - // task 2: query evm chain for deposited logs (read at most 100 blocks in one go) - lastScannedDeposited, err := ob.ObserveERC20Deposited(ctx, startBlock, toBlock) - if err != nil { - logger.Error().Err(err).Msg("error observing deposited events from ERC20Custody contract") - } - // task 3: query the incoming tx to TSS address (read at most 100 blocks in one go) lastScannedTssRecvd, err := ob.ObserveTSSReceive(ctx, startBlock, toBlock) if err != nil { @@ -162,20 +150,42 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, // TODO: make this a separate go routine in outbound.go after switching to smart contract V2 ob.FilterTSSOutbound(ctx, startBlock, toBlock) - // query the gateway logs - // TODO: refactor in a more declarative design. Example: storing the list of contract and events to listen in an array - // https://github.com/zeta-chain/node/issues/2493 - lastScannedGatewayDeposit, err := ob.ObserveGatewayDeposit(ctx, startBlock, toBlock) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error observing deposit events from Gateway contract") - } - lastScannedGatewayCall, err := ob.ObserveGatewayCall(ctx, startBlock, toBlock) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error observing call events from Gateway contract") - } - lastScannedGatewayDepositAndCall, err := ob.ObserveGatewayDepositAndCall(ctx, startBlock, toBlock) + var ( + lastScannedZetaSent = startBlock - 1 + lastScannedDeposited = startBlock - 1 + lastScannedGatewayDeposit = startBlock - 1 + lastScannedGatewayCall = startBlock - 1 + lastScannedGatewayDepositAndCall = startBlock - 1 + ) + + logs, err := ob.fetchLogs(ctx, startBlock, toBlock) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error observing depositAndCall events from Gateway contract") + ob.Logger().Inbound.Error().Err(err).Msg("get gateway logs") + } else { + // handle connector contract deposit + lastScannedZetaSent, err = ob.observeZetaSent(ctx, startBlock, toBlock, logs) + if err != nil { + logger.Error().Err(err).Msg("error observing zeta sent events from ZetaConnector contract") + } + + // handle legacy erc20 direct deposit logs + lastScannedDeposited, err = ob.observeERC20Deposited(ctx, startBlock, toBlock, logs) + if err != nil { + logger.Error().Err(err).Msg("error observing deposited events from ERC20Custody contract") + } + + lastScannedGatewayDeposit, err = ob.observeGatewayDeposit(ctx, startBlock, toBlock, logs) + if err != nil { + ob.Logger().Inbound.Error().Err(err).Msg("error observing deposit events from Gateway contract") + } + lastScannedGatewayCall, err = ob.observeGatewayCall(ctx, startBlock, toBlock, logs) + if err != nil { + ob.Logger().Inbound.Error().Err(err).Msg("error observing call events from Gateway contract") + } + lastScannedGatewayDepositAndCall, err = ob.observeGatewayDepositAndCall(ctx, startBlock, toBlock, logs) + if err != nil { + ob.Logger().Inbound.Error().Err(err).Msg("error observing depositAndCall events from Gateway contract") + } } // note: using the lowest height for all events is not perfect, @@ -192,9 +202,46 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, return lowestLastScannedBlock } -// ObserveZetaSent queries the ZetaSent event from the connector contract and posts to zetacore +func (ob *Observer) fetchLogs(ctx context.Context, startBlock, toBlock uint64) ([]ethtypes.Log, error) { + gatewayAddr, _, err := ob.GetGatewayContract() + if err != nil { + return nil, errors.Wrap(err, "can't get gateway contract") + } + + erc20Addr, _, err := ob.GetERC20CustodyContract() + if err != nil { + return nil, errors.Wrap(err, "can't get erc20 custody contract") + } + + connectorAddr, _, err := ob.GetConnectorContract() + if err != nil { + return nil, errors.Wrap(err, "can't get connector contract") + } + + addresses := []ethcommon.Address{gatewayAddr, erc20Addr, connectorAddr} + + logs, err := ob.evmClient.FilterLogs(ctx, ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(startBlock), + ToBlock: new(big.Int).SetUint64(toBlock), + Addresses: addresses, + }) + if err != nil { + return nil, errors.Wrap(err, "filter logs") + } + + // increment prom counter + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().Name).Inc() + + return logs, nil +} + +// observeZetaSent queries the ZetaSent event from the connector contract and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserveZetaSent(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { +func (ob *Observer) observeZetaSent( + ctx context.Context, + startBlock, toBlock uint64, + logs []ethtypes.Log, +) (uint64, error) { app, err := zctx.FromContext(ctx) if err != nil { return 0, err @@ -206,29 +253,24 @@ func (ob *Observer) ObserveZetaSent(ctx context.Context, startBlock, toBlock uin // we have to re-scan from this block next time return startBlock - 1, errors.Wrap(err, "error getting connector contract") } - iter, err := connector.FilterZetaSent(&bind.FilterOpts{ - Start: startBlock, - End: &toBlock, - Context: ctx, - }, []ethcommon.Address{}, []*big.Int{}) - if err != nil { - // we have to re-scan from this block next time - return startBlock - 1, errors.Wrap(err, "error filtering ZetaSent events") - } // collect and sort events by block number, then tx index, then log index (ascending) events := make([]*zetaconnector.ZetaConnectorNonEthZetaSent, 0) - for iter.Next() { + for _, log := range logs { // sanity check tx event - err := common.ValidateEvmTxLog(&iter.Event.Raw, addrConnector, "", common.TopicsZetaSent) + err := common.ValidateEvmTxLog(&log, addrConnector, "", common.TopicsZetaSent) + if err != nil { + continue + } + event, err := connector.ParseZetaSent(log) if err == nil { - events = append(events, iter.Event) + events = append(events, event) continue } ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) + log.TxHash.Hex(), ob.Chain().ChainId, log.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -275,9 +317,13 @@ func (ob *Observer) ObserveZetaSent(ctx context.Context, startBlock, toBlock uin return toBlock, nil } -// ObserveERC20Deposited queries the ERC20CustodyDeposited event from the ERC20Custody contract and posts to zetacore +// observeERC20Deposited queries the ERC20CustodyDeposited event from the ERC20Custody contract and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserveERC20Deposited(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { +func (ob *Observer) observeERC20Deposited( + ctx context.Context, + startBlock, toBlock uint64, + logs []ethtypes.Log, +) (uint64, error) { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { @@ -285,29 +331,23 @@ func (ob *Observer) ObserveERC20Deposited(ctx context.Context, startBlock, toBlo return startBlock - 1, errors.Wrap(err, "error getting ERC20Custody contract") } - iter, err := erc20custodyContract.FilterDeposited(&bind.FilterOpts{ - Start: startBlock, - End: &toBlock, - Context: ctx, - }, []ethcommon.Address{}) - if err != nil { - // we have to re-scan from this block next time - return startBlock - 1, errors.Wrap(err, "error filtering ERC20 Deposited events") - } - // collect and sort events by block number, then tx index, then log index (ascending) events := make([]*erc20custody.ERC20CustodyDeposited, 0) - for iter.Next() { + for _, log := range logs { // sanity check tx event - err := common.ValidateEvmTxLog(&iter.Event.Raw, addrCustody, "", common.TopicsDeposited) + err := common.ValidateEvmTxLog(&log, addrCustody, "", common.TopicsDeposited) + if err != nil { + continue + } + event, err := erc20custodyContract.ParseDeposited(log) if err == nil { - events = append(events, iter.Event) + events = append(events, event) continue } ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) + log.TxHash.Hex(), ob.Chain().ChainId, log.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index 04126364ae..282ff94ca9 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -8,8 +8,8 @@ import ( "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" - "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" "github.com/zeta-chain/node/pkg/coin" @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/node/zetaclient/compliance" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/logs" - "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -59,9 +58,13 @@ func (ob *Observer) isEventProcessable( return true } -// ObserveGatewayDeposit queries the gateway contract for deposit events +// observeGatewayDeposit queries the gateway contract for deposit events // returns the last block successfully scanned -func (ob *Observer) ObserveGatewayDeposit(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { +func (ob *Observer) observeGatewayDeposit( + ctx context.Context, + startBlock, toBlock uint64, + rawLogs []ethtypes.Log, +) (uint64, error) { // filter ERC20CustodyDeposited logs gatewayAddr, gatewayContract, err := ob.GetGatewayContract() if err != nil { @@ -69,27 +72,8 @@ func (ob *Observer) ObserveGatewayDeposit(ctx context.Context, startBlock, toBlo return startBlock - 1, errors.Wrap(err, "can't get gateway contract") } - // get iterator for the events for the block range - eventIterator, err := gatewayContract.FilterDeposited(&bind.FilterOpts{ - Start: startBlock, - End: &toBlock, - Context: ctx, - }, []ethcommon.Address{}, []ethcommon.Address{}) - if err != nil { - return startBlock - 1, errors.Wrapf( - err, - "error filtering deposits from block %d to %d for chain %d", - startBlock, - toBlock, - ob.Chain().ChainId, - ) - } - // parse and validate events - events := ob.parseAndValidateDepositEvents(eventIterator, gatewayAddr) - - // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().Name).Inc() + events := ob.parseAndValidateDepositEvents(rawLogs, gatewayAddr, gatewayContract) // post to zetacore lastScanned := uint64(0) @@ -124,22 +108,26 @@ func (ob *Observer) ObserveGatewayDeposit(ctx context.Context, startBlock, toBlo // parseAndValidateDepositEvents collects and sorts events by block number, tx index, and log index func (ob *Observer) parseAndValidateDepositEvents( - iterator *gatewayevm.GatewayEVMDepositedIterator, + rawLogs []ethtypes.Log, gatewayAddr ethcommon.Address, + gatewayContract *gatewayevm.GatewayEVM, ) []*gatewayevm.GatewayEVMDeposited { - // collect and sort validEvents by block number, then tx index, then log index (ascending) validEvents := make([]*gatewayevm.GatewayEVMDeposited, 0) - for iterator.Next() { - err := common.ValidateEvmTxLog(&iterator.Event.Raw, gatewayAddr, "", common.TopicsGatewayDeposit) - if err == nil { - validEvents = append(validEvents, iterator.Event) + for _, log := range rawLogs { + err := common.ValidateEvmTxLog(&log, gatewayAddr, "", common.TopicsGatewayDeposit) + if err != nil { + continue + } + depositedEvent, err := gatewayContract.ParseDeposited(log) + if err != nil { + ob.Logger(). + Inbound.Warn(). + Stringer(logs.FieldTx, log.TxHash). + Uint64(logs.FieldBlock, log.BlockNumber). + Msg("invalid Deposited event") continue } - ob.Logger(). - Inbound.Warn(). - Stringer(logs.FieldTx, iterator.Event.Raw.TxHash). - Uint64(logs.FieldBlock, iterator.Event.Raw.BlockNumber). - Msg("invalid Deposited event") + validEvents = append(validEvents, depositedEvent) } // order events by height, tx index and event index (ascending) @@ -211,50 +199,29 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) ) } -// ObserveGatewayCall queries the gateway contract for call events +// observeGatewayCall queries the gateway contract for call events // returns the last block successfully scanned // TODO: there are lot of similarities between this function and ObserveGatewayDeposit // logic should be factorized using interfaces and generics // https://github.com/zeta-chain/node/issues/2493 -func (ob *Observer) ObserveGatewayCall(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { - // filter ERC20CustodyDeposited logs +func (ob *Observer) observeGatewayCall( + ctx context.Context, + startBlock, toBlock uint64, + rawLogs []ethtypes.Log, +) (uint64, error) { gatewayAddr, gatewayContract, err := ob.GetGatewayContract() if err != nil { // lastScanned is startBlock - 1 return startBlock - 1, errors.Wrap(err, "can't get gateway contract") } - // get iterator for the events for the block range - eventIterator, err := gatewayContract.FilterCalled(&bind.FilterOpts{ - Start: startBlock, - End: &toBlock, - Context: ctx, - }, []ethcommon.Address{}, []ethcommon.Address{}) - if err != nil { - return startBlock - 1, errors.Wrapf( - err, - "error filtering calls from block %d to %d for chain %d", - startBlock, - toBlock, - ob.Chain().ChainId, - ) - } - - // parse and validate events - events := ob.parseAndValidateCallEvents(eventIterator, gatewayAddr) - - // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().Name).Inc() - - // post to zetacore + events := ob.parseAndValidateCallEvents(rawLogs, gatewayAddr, gatewayContract) lastScanned := uint64(0) for _, event := range events { - // remember which block we are scanning (there could be multiple events in the same block) if event.Raw.BlockNumber > lastScanned { lastScanned = event.Raw.BlockNumber } - // check if the event is processable if !ob.isEventProcessable(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { continue } @@ -268,33 +235,35 @@ func (ob *Observer) ObserveGatewayCall(ctx context.Context, startBlock, toBlock _, err = ob.PostVoteInbound(ctx, &msg, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { - // decrement the last scanned block so we have to re-scan from this block next time return lastScanned - 1, errors.Wrap(err, "error posting vote inbound") } } - // successfully processed all events in [startBlock, toBlock] return toBlock, nil } // parseAndValidateCallEvents collects and sorts events by block number, tx index, and log index func (ob *Observer) parseAndValidateCallEvents( - iterator *gatewayevm.GatewayEVMCalledIterator, + rawLogs []ethtypes.Log, gatewayAddr ethcommon.Address, + gatewayContract *gatewayevm.GatewayEVM, ) []*gatewayevm.GatewayEVMCalled { - // collect and sort validEvents by block number, then tx index, then log index (ascending) validEvents := make([]*gatewayevm.GatewayEVMCalled, 0) - for iterator.Next() { - err := common.ValidateEvmTxLog(&iterator.Event.Raw, gatewayAddr, "", common.TopicsGatewayCall) - if err == nil { - validEvents = append(validEvents, iterator.Event) + for _, log := range rawLogs { + err := common.ValidateEvmTxLog(&log, gatewayAddr, "", common.TopicsGatewayCall) + if err != nil { + continue + } + calledEvent, err := gatewayContract.ParseCalled(log) + if err != nil { + ob.Logger(). + Inbound.Warn(). + Stringer(logs.FieldTx, log.TxHash). + Uint64(logs.FieldBlock, log.BlockNumber). + Msg("invalid Called event") continue } - ob.Logger(). - Inbound.Warn(). - Stringer(logs.FieldTx, iterator.Event.Raw.TxHash). - Uint64(logs.FieldBlock, iterator.Event.Raw.BlockNumber). - Msg("invalid Called event") + validEvents = append(validEvents, calledEvent) } // order events by height, tx index and event index (ascending) @@ -350,38 +319,21 @@ func (ob *Observer) newCallInboundVote(event *gatewayevm.GatewayEVMCalled) types ) } -// ObserveGatewayDepositAndCall queries the gateway contract for deposit and call events +// observeGatewayDepositAndCall queries the gateway contract for deposit and call events // returns the last block successfully scanned -func (ob *Observer) ObserveGatewayDepositAndCall(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { +func (ob *Observer) observeGatewayDepositAndCall( + ctx context.Context, + startBlock, toBlock uint64, + rawLogs []ethtypes.Log, +) (uint64, error) { gatewayAddr, gatewayContract, err := ob.GetGatewayContract() if err != nil { // lastScanned is startBlock - 1 return startBlock - 1, errors.Wrap(err, "can't get gateway contract") } - // get iterator for the events for the block range - eventIterator, err := gatewayContract.FilterDepositedAndCalled(&bind.FilterOpts{ - Start: startBlock, - End: &toBlock, - Context: ctx, - }, []ethcommon.Address{}, []ethcommon.Address{}) - if err != nil { - return startBlock - 1, errors.Wrapf( - err, - "error filtering deposits from block %d to %d for chain %d", - startBlock, - toBlock, - ob.Chain().ChainId, - ) - } - - // parse and validate events - events := ob.parseAndValidateDepositAndCallEvents(eventIterator, gatewayAddr) - - // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().Name).Inc() + events := ob.parseAndValidateDepositAndCallEvents(rawLogs, gatewayAddr, gatewayContract) - // post to zetacore lastScanned := uint64(0) for _, event := range events { // remember which block we are scanning (there could be multiple events in the same block) @@ -414,22 +366,27 @@ func (ob *Observer) ObserveGatewayDepositAndCall(ctx context.Context, startBlock // parseAndValidateDepositAndCallEvents collects and sorts events by block number, tx index, and log index func (ob *Observer) parseAndValidateDepositAndCallEvents( - iterator *gatewayevm.GatewayEVMDepositedAndCalledIterator, + rawLogs []ethtypes.Log, gatewayAddr ethcommon.Address, + gatewayContract *gatewayevm.GatewayEVM, ) []*gatewayevm.GatewayEVMDepositedAndCalled { // collect and sort validEvents by block number, then tx index, then log index (ascending) validEvents := make([]*gatewayevm.GatewayEVMDepositedAndCalled, 0) - for iterator.Next() { - err := common.ValidateEvmTxLog(&iterator.Event.Raw, gatewayAddr, "", common.TopicsGatewayDepositAndCall) - if err == nil { - validEvents = append(validEvents, iterator.Event) + for _, log := range rawLogs { + err := common.ValidateEvmTxLog(&log, gatewayAddr, "", common.TopicsGatewayDepositAndCall) + if err != nil { + continue + } + depositAndCallEvent, err := gatewayContract.ParseDepositedAndCalled(log) + if err != nil { + ob.Logger(). + Inbound.Warn(). + Stringer(logs.FieldTx, log.TxHash). + Uint64(logs.FieldBlock, log.BlockNumber). + Msg("invalid DepositedAndCalled event") continue } - ob.Logger(). - Inbound.Warn(). - Stringer(logs.FieldTx, iterator.Event.Raw.TxHash). - Uint64(logs.FieldBlock, iterator.Event.Raw.BlockNumber). - Msg("invalid DepositedAndCalled event") + validEvents = append(validEvents, depositAndCallEvent) } // order events by height, tx index and event index (ascending) From c42760704491b3878408eb10028e4320b8eb662c Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Tue, 25 Feb 2025 00:56:32 -0800 Subject: [PATCH 09/22] chore: upgrade cosmovisor (#3575) --- Dockerfile-localnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-localnet b/Dockerfile-localnet index dd329a712d..2f8c90bf47 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -28,7 +28,7 @@ RUN --mount=type=cache,target="/root/.cache/go-build" \ make install install-zetae2e FROM ghcr.io/zeta-chain/golang:1.23.3-bookworm AS cosmovisor-build -RUN go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.7.0 +RUN go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.7.1 FROM ghcr.io/zeta-chain/golang:1.23.3-bookworm AS base-runtime From 380b545eadbcb0a586045c9f8d9d52ed5bce2495 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:34:50 +0300 Subject: [PATCH 10/22] fix(testlog): fix panics after test finished (#3574) * Fix panics after test finished [2] * add mu --- zetaclient/testutils/testlog/log.go | 45 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/zetaclient/testutils/testlog/log.go b/zetaclient/testutils/testlog/log.go index e1b224aa60..aaed545025 100644 --- a/zetaclient/testutils/testlog/log.go +++ b/zetaclient/testutils/testlog/log.go @@ -3,7 +3,6 @@ package testlog import ( "bytes" "fmt" - "io" "strings" "sync" "testing" @@ -13,45 +12,51 @@ import ( type Log struct { zerolog.Logger - buf *concurrentBytesBuffer -} - -type concurrentBytesBuffer struct { + t *testing.T buf *bytes.Buffer - mu sync.RWMutex + mu sync.Mutex } // New creates a new Log instance with a buffer and a test writer. func New(t *testing.T) *Log { - buf := &concurrentBytesBuffer{ + log := &Log{ + t: t, buf: &bytes.Buffer{}, - mu: sync.RWMutex{}, } - log := zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), buf)) + log.Logger = zerolog.New(log) - return &Log{Logger: log, buf: buf} + return log } func (log *Log) String() string { - return log.buf.string() + log.mu.Lock() + defer log.mu.Unlock() + return log.buf.String() } -func (b *concurrentBytesBuffer) Write(p []byte) (n int, err error) { - b.mu.Lock() - defer b.mu.Unlock() +func (log *Log) Write(p []byte) (n int, err error) { + log.mu.Lock() + defer log.mu.Unlock() + // silence panics in case this log line is written AFTER test termination. const silencePanicSubstring = "Log in goroutine" defer func() { silencePanic(recover(), silencePanicSubstring) }() - return b.buf.Write(p) -} + // write to the buffer first + n, err = log.buf.Write(p) + if err != nil { + return n, fmt.Errorf("failed to write to buffer: %w", err) + } + + // Strip trailing newline because t.Log always adds one. + // (copied from zerolog NewTestWriter) + p = bytes.TrimRight(p, "\n") -func (b *concurrentBytesBuffer) string() string { - b.mu.RLock() - defer b.mu.RUnlock() + // Then write to test output + log.t.Log(string(p)) - return b.buf.String() + return len(p), nil } func silencePanic(r any, substr string) { From d3eb81c4ec29d3ac8ef93a93b983a695f0519e9b Mon Sep 17 00:00:00 2001 From: Tanmay Date: Tue, 25 Feb 2025 11:54:56 -0500 Subject: [PATCH 11/22] test(`e2e`): solana upgrade contract e2e test (#3536) * sol withdraw and call and e2e test * fmt * fix solana e2e tests * fix msg hash unit tests * cleanup * bump gateway.so * cleanup unused function * cleanup * PR comments * linter * add solana upgrade * add changelog * add changelog * generate contracts * generate new contracts from solana programs repo * add comments for VerifySolanaContractsUpgrade * remove unnecessary True function * refactor VerifySolanaContractsUpgrade to call a new function in the upgraded contract * update contract upgrade verification logic * format code * add testSolana flag check for upgrading solana contracts * add log line for triggerSolanaUpgrade failure * update solana dockerfile --------- Co-authored-by: skosito --- changelog.md | 1 + cmd/zetae2e/local/local.go | 12 +- contrib/localnet/docker-compose.yml | 2 + .../localnet/orchestrator/start-zetae2e.sh | 2 + contrib/localnet/scripts/gateway-upgrade.so | Bin 0 -> 341664 bytes contrib/localnet/solana/Dockerfile | 14 ++ contrib/localnet/solana/gateway.so | Bin 408072 -> 365904 bytes contrib/localnet/solana/gateway_upgrade.so | Bin 0 -> 366904 bytes contrib/localnet/solana/start-solana.sh | 21 +++ e2e/runner/contract_upgrade_solana.go | 129 ++++++++++++++++++ e2e/runner/verify.go | 4 +- pkg/contracts/solana/pda.go | 3 + 12 files changed, 183 insertions(+), 5 deletions(-) create mode 100755 contrib/localnet/scripts/gateway-upgrade.so create mode 100755 contrib/localnet/solana/gateway_upgrade.so create mode 100644 e2e/runner/contract_upgrade_solana.go diff --git a/changelog.md b/changelog.md index ca9fd2d622..13173b4fa9 100644 --- a/changelog.md +++ b/changelog.md @@ -39,6 +39,7 @@ * [3430](https://github.com/zeta-chain/node/pull/3430) - add simulation test for MsgWithDrawEmission * [3503](https://github.com/zeta-chain/node/pull/3503) - add check in e2e test to ensure deletion of stale ballots +* [3536](https://github.com/zeta-chain/node/pull/3536) - add e2e test for upgrading solana gateway program ## v28.0.0 diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 5d63b785fa..38ca0363dc 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -12,6 +12,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" + "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" zetae2econfig "github.com/zeta-chain/node/cmd/zetae2e/config" @@ -53,10 +54,9 @@ const ( var ( TestTimeout = 20 * time.Minute ErrTopLevelTimeout = errors.New("top level test timeout") + noError = testutil.NoError ) -var noError = testutil.NoError - // NewLocalCmd returns the local command // which runs the E2E tests locally on the machine with localnet for each blockchain func NewLocalCmd() *cobra.Command { @@ -288,7 +288,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if upgradeContracts { deployerRunner.UpgradeGatewaysAndERC20Custody() } - // always mint ERC20 before every test execution deployerRunner.MintERC20OnEVM(1e10) @@ -495,6 +494,13 @@ func localE2ETest(cmd *cobra.Command, _ []string) { logger.Print("✅ e2e tests completed in %s", time.Since(testStartTime).String()) + if testSolana { + require.True( + deployerRunner, + deployerRunner.VerifySolanaContractsUpgrade(conf.AdditionalAccounts.UserSolana.SolanaPrivateKey.String()), + ) + } + if testTSSMigration { TSSMigration(deployerRunner, logger, verbose, conf) } diff --git a/contrib/localnet/docker-compose.yml b/contrib/localnet/docker-compose.yml index 41b22d4c7e..346aa3f420 100644 --- a/contrib/localnet/docker-compose.yml +++ b/contrib/localnet/docker-compose.yml @@ -230,6 +230,8 @@ services: mynetwork: ipv4_address: 172.20.0.103 entrypoint: ["/usr/bin/start-solana.sh"] + volumes: + - ssh:/root/.ssh ton: # figure out why E2E fail with MyLocalTon v124 @ deposit: deployer.CreateWallet(..) diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 9d547157c1..b5f7d43564 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -10,6 +10,8 @@ # Trap signals and forward to children trap 'kill -- -$$' SIGINT SIGTERM +/usr/sbin/sshd + get_zetacored_version() { retries=10 node_info="" diff --git a/contrib/localnet/scripts/gateway-upgrade.so b/contrib/localnet/scripts/gateway-upgrade.so new file mode 100755 index 0000000000000000000000000000000000000000..8d48973fca7a803e1f254c5216d8fa686b088a6f GIT binary patch literal 341664 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 literal 0 HcmV?d00001 diff --git a/contrib/localnet/solana/Dockerfile b/contrib/localnet/solana/Dockerfile index 21713112d4..dc1b9a6b8f 100644 --- a/contrib/localnet/solana/Dockerfile +++ b/contrib/localnet/solana/Dockerfile @@ -1,14 +1,28 @@ FROM ghcr.io/zeta-chain/solana-docker:2.0.24 +# Set working directory for the container WORKDIR /data + +# Setup SSH server +# Expose port 22 for SSH access +EXPOSE 22 +# Install and configure SSH server +RUN apt update && apt install -y openssh-server + +# Copy and make executable the main Solana startup script COPY ./start-solana.sh /usr/bin/start-solana.sh RUN chmod +x /usr/bin/start-solana.sh + +# Copy Solana program files and their keypairs COPY ./gateway.so . COPY ./gateway-keypair.json . COPY ./connected.so . COPY ./connected-keypair.json . COPY ./connected_spl.so . COPY ./connected_spl-keypair.json . +COPY ./gateway_upgrade.so . + +RUN mkdir /run/sshd ENTRYPOINT [ "bash" ] CMD [ "/usr/bin/start-solana.sh" ] \ No newline at end of file diff --git a/contrib/localnet/solana/gateway.so b/contrib/localnet/solana/gateway.so index 82d484b91c81a4ccb9118bac8f2cbdc3fcb9ea82..2f24b7b6240c10d2f2177c664e63de7a054016e3 100755 GIT binary patch literal 365904 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

#4=B{3aGKzY&8U3kARO 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

^FEai>k0 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 {tQ)EC5#BWGU5yQxQ><>(*eC0WH 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 Date: Tue, 25 Feb 2025 21:59:35 +0300 Subject: [PATCH 12/22] Fix config resolution. add unit tests (#3586) --- zetaclient/chains/ton/config/config.go | 11 ++- zetaclient/chains/ton/config/config_test.go | 81 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 zetaclient/chains/ton/config/config_test.go diff --git a/zetaclient/chains/ton/config/config.go b/zetaclient/chains/ton/config/config.go index c970a68baa..e0621fb813 100644 --- a/zetaclient/chains/ton/config/config.go +++ b/zetaclient/chains/ton/config/config.go @@ -59,11 +59,16 @@ func FromPath(path string) (*GlobalConfigurationFile, error) { // FromSource returns a parsed configuration file from a URL or a file path. func FromSource(ctx context.Context, urlOrPath string) (*GlobalConfigurationFile, error) { - if u, err := url.Parse(urlOrPath); err == nil { - return FromURL(ctx, u.String()) + if cfg, err := FromPath(urlOrPath); err == nil { + return cfg, nil } - return FromPath(urlOrPath) + u, err := url.Parse(urlOrPath) + if err != nil { + return nil, errors.Wrap(err, "failed to parse URL") + } + + return FromURL(ctx, u.String()) } // FetchGasConfig fetches gas price from the config. diff --git a/zetaclient/chains/ton/config/config_test.go b/zetaclient/chains/ton/config/config_test.go new file mode 100644 index 0000000000..c9202cf858 --- /dev/null +++ b/zetaclient/chains/ton/config/config_test.go @@ -0,0 +1,81 @@ +package config + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfig(t *testing.T) { + ctx := context.Background() + + t.Run("FromSource_path", func(t *testing.T) { + // ARRANGE + tmpDir := t.TempDir() + defer os.RemoveAll(tmpDir) + + configPath := filepath.Join(tmpDir, "config.json") + require.NoError(t, os.WriteFile(configPath, sampleConfigBytes(t), 0644)) + + // ACT + cfg, err := FromSource(ctx, configPath) + + // ASSERT + require.NoError(t, err) + require.NotNil(t, cfg) + }) + + t.Run("FromSource_url", func(t *testing.T) { + // ARRANGE + cfgBytes := sampleConfigBytes(t) + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Write(cfgBytes) + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + url := fmt.Sprintf("%s/config.json", server.URL) + + // ACT + cfg, err := FromSource(ctx, url) + + // ASSERT + require.NoError(t, err) + require.NotNil(t, cfg) + }) +} + +func sampleConfigBytes(t *testing.T) []byte { + // https://ton.org/testnet-global.config.json + const sampleConfig = `{ + "liteservers": [ + { + "ip": 822907680, + "port": 27842, + "provided":"Beavis", + "id": { + "@type": "pub.ed25519", + "key": "sU7QavX2F964iI9oToP9gffQpCQIoOLppeqL/pdPvpM=" + } + }, + { + "ip": 1091956407, + "port": 16351, + "id": { + "@type": "pub.ed25519", + "key": "Mf/JGvcWAvcrN3oheze8RF/ps6p7oL6ifrIzFmGQFQ8=" + } + } + ] + }` + + return []byte(sampleConfig) +} From 79ec10c369f0fe92596e3ab2258c1efab9c1cea8 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Tue, 25 Feb 2025 12:40:04 -0800 Subject: [PATCH 13/22] fix(e2e): only write bootstrap once (#3577) --- contrib/localnet/scripts/start-zetaclientd.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 1627c1cd10..7d1e41d5ec 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -69,7 +69,10 @@ RELAYER_KEY_PATH="$HOME/.zetacored/relayer-keys" mkdir -p "${RELAYER_KEY_PATH}" mkdir -p "$HOME/.tss/" -zetae2e local get-zetaclient-bootstrap > "$HOME/.tss/address_book.seed" +address_book_path="$HOME/.tss/address_book.seed" +if [[ ! -f "$address_book_path" ]]; then + zetae2e local get-zetaclient-bootstrap > $address_book_path +fi MYIP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1) From eba1a602b5037159acdba1e9c7d2b2e9586c6df0 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 26 Feb 2025 00:09:38 -0800 Subject: [PATCH 14/22] refactor(zetaclient): unexport internal evm observer methods (#3587) --- zetaclient/chains/evm/observer/inbound.go | 74 ++++++++-------- .../chains/evm/observer/inbound_test.go | 54 ++++++------ zetaclient/chains/evm/observer/observer.go | 85 ++++++------------- .../chains/evm/observer/observer_gas_test.go | 2 +- .../chains/evm/observer/observer_test.go | 25 +++--- zetaclient/chains/evm/observer/outbound.go | 52 ++++++------ .../chains/evm/observer/outbound_test.go | 57 ++++++------- zetaclient/chains/evm/observer/v2_inbound.go | 6 +- .../chains/evm/observer/v2_inbound_tracker.go | 2 +- 9 files changed, 162 insertions(+), 195 deletions(-) diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index ec320dbad1..db4e3c475b 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -43,7 +43,7 @@ func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { for _, tracker := range trackers { // query tx and receipt - tx, _, err := ob.TransactionByHash(ctx, tracker.TxHash) + tx, _, err := ob.transactionByHash(ctx, tracker.TxHash) if err != nil { return errors.Wrapf( err, @@ -77,11 +77,11 @@ func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { // try processing the tracker for v1 inbound switch tracker.CoinType { case coin.CoinType_Zeta: - _, err = ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) + _, err = ob.checkAndVoteInboundTokenZeta(ctx, tx, receipt, true) case coin.CoinType_ERC20: - _, err = ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) + _, err = ob.checkAndVoteInboundTokenERC20(ctx, tx, receipt, true) case coin.CoinType_Gas: - _, err = ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, true) + _, err = ob.checkAndVoteInboundTokenGas(ctx, tx, receipt, true) default: return fmt.Errorf( "unknown coin type %s for inbound %s chain %d", @@ -141,14 +141,14 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, Uint64("start_block", startBlock).Uint64("to_block", toBlock).Logger() // task 3: query the incoming tx to TSS address (read at most 100 blocks in one go) - lastScannedTssRecvd, err := ob.ObserveTSSReceive(ctx, startBlock, toBlock) + lastScannedTssRecvd, err := ob.observeTSSReceive(ctx, startBlock, toBlock) if err != nil { logger.Error().Err(err).Msg("error observing TSS received gas asset") } // task 4: filter the outbounds from TSS address to supplement outbound trackers // TODO: make this a separate go routine in outbound.go after switching to smart contract V2 - ob.FilterTSSOutbound(ctx, startBlock, toBlock) + ob.filterTSSOutbound(ctx, startBlock, toBlock) var ( lastScannedZetaSent = startBlock - 1 @@ -203,17 +203,17 @@ func (ob *Observer) observeInboundInBlockRange(ctx context.Context, startBlock, } func (ob *Observer) fetchLogs(ctx context.Context, startBlock, toBlock uint64) ([]ethtypes.Log, error) { - gatewayAddr, _, err := ob.GetGatewayContract() + gatewayAddr, _, err := ob.getGatewayContract() if err != nil { return nil, errors.Wrap(err, "can't get gateway contract") } - erc20Addr, _, err := ob.GetERC20CustodyContract() + erc20Addr, _, err := ob.getERC20CustodyContract() if err != nil { return nil, errors.Wrap(err, "can't get erc20 custody contract") } - connectorAddr, _, err := ob.GetConnectorContract() + connectorAddr, _, err := ob.getConnectorContract() if err != nil { return nil, errors.Wrap(err, "can't get connector contract") } @@ -248,7 +248,7 @@ func (ob *Observer) observeZetaSent( } // filter ZetaSent logs - addrConnector, connector, err := ob.GetConnectorContract() + addrConnector, connector, err := ob.getConnectorContract() if err != nil { // we have to re-scan from this block next time return startBlock - 1, errors.Wrap(err, "error getting connector contract") @@ -301,7 +301,7 @@ func (ob *Observer) observeZetaSent( } guard[event.Raw.TxHash.Hex()] = true - msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) + msg := ob.buildInboundVoteMsgForZetaSentEvent(app, event) if msg == nil { continue } @@ -325,7 +325,7 @@ func (ob *Observer) observeERC20Deposited( logs []ethtypes.Log, ) (uint64, error) { // filter ERC20CustodyDeposited logs - addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() + addrCustody, erc20custodyContract, err := ob.getERC20CustodyContract() if err != nil { // we have to re-scan from this block next time return startBlock - 1, errors.Wrap(err, "error getting ERC20Custody contract") @@ -370,7 +370,7 @@ func (ob *Observer) observeERC20Deposited( if event.Raw.BlockNumber > beingScanned { beingScanned = event.Raw.BlockNumber } - tx, _, err := ob.TransactionByHash(ctx, event.Raw.TxHash.Hex()) + tx, _, err := ob.transactionByHash(ctx, event.Raw.TxHash.Hex()) if err != nil { // we have to re-scan from this block next time return beingScanned - 1, errors.Wrapf(err, "error getting transaction %s", event.Raw.TxHash.Hex()) @@ -385,7 +385,7 @@ func (ob *Observer) observeERC20Deposited( } guard[event.Raw.TxHash.Hex()] = true - msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) + msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) if msg != nil { _, err = ob.PostVoteInbound(ctx, msg, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { @@ -398,13 +398,13 @@ func (ob *Observer) observeERC20Deposited( return toBlock, nil } -// ObserveTSSReceive queries the incoming gas asset to TSS address and posts to zetacore +// observeTSSReceive queries the incoming gas asset to TSS address and posts to zetacore // returns the last block successfully scanned -func (ob *Observer) ObserveTSSReceive(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { +func (ob *Observer) observeTSSReceive(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { // query incoming gas asset for bn := startBlock; bn <= toBlock; bn++ { // observe TSS received gas token in block 'bn' - err := ob.ObserveTSSReceiveInBlock(ctx, bn) + err := ob.observeTSSReceiveInBlock(ctx, bn) if err != nil { // we have to re-scan from this block next time return bn - 1, errors.Wrapf(err, "error observing TSS received gas asset in block %d", bn) @@ -415,8 +415,8 @@ func (ob *Observer) ObserveTSSReceive(ctx context.Context, startBlock, toBlock u return toBlock, nil } -// CheckAndVoteInboundTokenZeta checks and votes on the given inbound Zeta token -func (ob *Observer) CheckAndVoteInboundTokenZeta( +// checkAndVoteInboundTokenZeta checks and votes on the given inbound Zeta token +func (ob *Observer) checkAndVoteInboundTokenZeta( ctx context.Context, tx *client.Transaction, receipt *ethtypes.Receipt, @@ -437,7 +437,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( } // get zeta connector contract - addrConnector, connector, err := ob.GetConnectorContract() + addrConnector, connector, err := ob.getConnectorContract() if err != nil { return "", err } @@ -450,7 +450,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( // sanity check tx event err = common.ValidateEvmTxLog(&event.Raw, addrConnector, tx.Hash, common.TopicsZetaSent) if err == nil { - msg = ob.BuildInboundVoteMsgForZetaSentEvent(app, event) + msg = ob.buildInboundVoteMsgForZetaSentEvent(app, event) } else { ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err @@ -470,8 +470,8 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( return msg.Digest(), nil } -// CheckAndVoteInboundTokenERC20 checks and votes on the given inbound ERC20 token -func (ob *Observer) CheckAndVoteInboundTokenERC20( +// checkAndVoteInboundTokenERC20 checks and votes on the given inbound ERC20 token +func (ob *Observer) checkAndVoteInboundTokenERC20( ctx context.Context, tx *client.Transaction, receipt *ethtypes.Receipt, @@ -487,7 +487,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } // get erc20 custody contract - addrCustody, custody, err := ob.GetERC20CustodyContract() + addrCustody, custody, err := ob.getERC20CustodyContract() if err != nil { return "", err } @@ -501,7 +501,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( // sanity check tx event err = common.ValidateEvmTxLog(&zetaDeposited.Raw, addrCustody, tx.Hash, common.TopicsDeposited) if err == nil { - msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) + msg = ob.buildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err @@ -521,8 +521,8 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( return msg.Digest(), nil } -// CheckAndVoteInboundTokenGas checks and votes on the given inbound gas token -func (ob *Observer) CheckAndVoteInboundTokenGas( +// checkAndVoteInboundTokenGas checks and votes on the given inbound gas token +func (ob *Observer) checkAndVoteInboundTokenGas( ctx context.Context, tx *client.Transaction, receipt *ethtypes.Receipt, @@ -547,7 +547,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( sender := ethcommon.HexToAddress(tx.From) // build inbound vote message and post vote - msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) + msg := ob.buildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. ob.Logger().Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) @@ -560,8 +560,8 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( return msg.Digest(), nil } -// BuildInboundVoteMsgForDepositedEvent builds a inbound vote message for a Deposited event -func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( +// buildInboundVoteMsgForDepositedEvent builds a inbound vote message for a Deposited event +func (ob *Observer) buildInboundVoteMsgForDepositedEvent( event *erc20custody.ERC20CustodyDeposited, sender ethcommon.Address, ) *types.MsgVoteInbound { @@ -616,8 +616,8 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( ) } -// BuildInboundVoteMsgForZetaSentEvent builds a inbound vote message for a ZetaSent event -func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( +// buildInboundVoteMsgForZetaSentEvent builds a inbound vote message for a ZetaSent event +func (ob *Observer) buildInboundVoteMsgForZetaSentEvent( appContext *zctx.AppContext, event *zetaconnector.ZetaConnectorNonEthZetaSent, ) *types.MsgVoteInbound { @@ -669,8 +669,8 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ) } -// BuildInboundVoteMsgForTokenSentToTSS builds a inbound vote message for a token sent to TSS -func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( +// buildInboundVoteMsgForTokenSentToTSS builds a inbound vote message for a token sent to TSS +func (ob *Observer) buildInboundVoteMsgForTokenSentToTSS( tx *client.Transaction, sender ethcommon.Address, blockNumber uint64, @@ -719,8 +719,8 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( ) } -// ObserveTSSReceiveInBlock queries the incoming gas asset to TSS address in a single block and posts votes -func (ob *Observer) ObserveTSSReceiveInBlock(ctx context.Context, blockNumber uint64) error { +// observeTSSReceiveInBlock queries the incoming gas asset to TSS address in a single block and posts votes +func (ob *Observer) observeTSSReceiveInBlock(ctx context.Context, blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(ctx, blockNumber) if err != nil { return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) @@ -733,7 +733,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(ctx context.Context, blockNumber ui return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } - _, err = ob.CheckAndVoteInboundTokenGas(ctx, &tx, receipt, true) + _, err = ob.checkAndVoteInboundTokenGas(ctx, &tx, receipt, true) if err != nil { return errors.Wrapf( err, diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 71b4ecc94c..2bbd57e673 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "encoding/hex" @@ -41,7 +41,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) - ballot, err := ob.CheckAndVoteInboundTokenZeta(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenZeta(ob.ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -59,7 +59,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe() - 2) - _, err := ob.CheckAndVoteInboundTokenZeta(ob.ctx, tx, receipt, false) + _, err := ob.checkAndVoteInboundTokenZeta(ob.ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if no ZetaSent event", func(t *testing.T) { @@ -77,7 +77,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) - ballot, err := ob.CheckAndVoteInboundTokenZeta(ob.ctx, tx, receipt, true) + ballot, err := ob.checkAndVoteInboundTokenZeta(ob.ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -98,7 +98,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) // ACT - _, err := ob.CheckAndVoteInboundTokenZeta(ob.ctx, tx, receipt, true) + _, err := ob.checkAndVoteInboundTokenZeta(ob.ctx, tx, receipt, true) // ASSERT require.ErrorContains(t, err, "emitter address mismatch") @@ -126,7 +126,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) - ballot, err := ob.CheckAndVoteInboundTokenERC20(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenERC20(ob.ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -144,7 +144,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe() - 2) - _, err := ob.CheckAndVoteInboundTokenERC20(ob.ctx, tx, receipt, false) + _, err := ob.checkAndVoteInboundTokenERC20(ob.ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if no Deposit event", func(t *testing.T) { @@ -162,7 +162,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) - ballot, err := ob.CheckAndVoteInboundTokenERC20(ob.ctx, tx, receipt, true) + ballot, err := ob.checkAndVoteInboundTokenERC20(ob.ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -184,7 +184,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { ob.WithLastBlock(receipt.BlockNumber.Uint64() + ob.chainParams.InboundConfirmationSafe()) // ACT - _, err := ob.CheckAndVoteInboundTokenERC20(ob.ctx, tx, receipt, true) + _, err := ob.checkAndVoteInboundTokenERC20(ob.ctx, tx, receipt, true) // ASSERT require.ErrorContains(t, err, "emitter address mismatch") @@ -213,7 +213,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { ob := newTestSuite(t) ob.WithLastBlock(lastBlock) - ballot, err := ob.CheckAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -225,7 +225,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { ob := newTestSuite(t) ob.WithLastBlock(lastBlock) - _, err := ob.CheckAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) + _, err := ob.checkAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) t.Run("should not act if receiver is not TSS", func(t *testing.T) { @@ -237,7 +237,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { ob := newTestSuite(t) ob.WithLastBlock(lastBlock) - ballot, err := ob.CheckAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) }) @@ -250,7 +250,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { ob := newTestSuite(t) ob.WithLastBlock(lastBlock) - ballot, err := ob.CheckAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) }) @@ -263,7 +263,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { ob := newTestSuite(t) ob.WithLastBlock(lastBlock) - ballot, err := ob.CheckAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) + ballot, err := ob.checkAndVoteInboundTokenGas(ob.ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) }) @@ -289,7 +289,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { } t.Run("should return vote msg for archived ZetaSent event", func(t *testing.T) { - msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) + msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.NotNil(t, msg) require.Equal(t, cctx.InboundParams.BallotIndex, msg.Digest()) }) @@ -297,21 +297,21 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { sender := event.ZetaTxSenderAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{sender} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) + msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.DestinationAddress) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) + msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if txOrigin is restricted", func(t *testing.T) { txOrigin := event.SourceTxOriginAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{txOrigin} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) + msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) } @@ -337,26 +337,26 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { } t.Run("should return vote msg for archived Deposited event", func(t *testing.T) { - msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) + msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.NotNil(t, msg) require.Equal(t, cctx.InboundParams.BallotIndex, msg.Digest()) }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{sender.Hex()} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) + msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.Recipient) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) + msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) t.Run("should return nil msg on donation transaction", func(t *testing.T) { event.Message = []byte(constant.DonationMessage) - msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) + msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) } @@ -390,7 +390,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { } t.Run("should return vote msg for archived gas token transfer to TSS", func(t *testing.T) { - msg := ob.BuildInboundVoteMsgForTokenSentToTSS( + msg := ob.buildInboundVoteMsgForTokenSentToTSS( tx, ethcommon.HexToAddress(tx.From), receipt.BlockNumber.Uint64(), @@ -401,7 +401,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{tx.From} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForTokenSentToTSS( + msg := ob.buildInboundVoteMsgForTokenSentToTSS( tx, ethcommon.HexToAddress(tx.From), receipt.BlockNumber.Uint64(), @@ -415,7 +415,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { txCopy.Input = message // use other address as receiver cfg.ComplianceConfig.RestrictedAddresses = []string{testutils.OtherAddress1} config.LoadComplianceConfig(cfg) - msg := ob.BuildInboundVoteMsgForTokenSentToTSS( + msg := ob.buildInboundVoteMsgForTokenSentToTSS( txCopy, ethcommon.HexToAddress(txCopy.From), receipt.BlockNumber.Uint64(), @@ -423,7 +423,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.Nil(t, msg) }) t.Run("should return nil msg on donation transaction", func(t *testing.T) { - msg := ob.BuildInboundVoteMsgForTokenSentToTSS(txDonation, + msg := ob.buildInboundVoteMsgForTokenSentToTSS(txDonation, ethcommon.HexToAddress(txDonation.From), receiptDonation.BlockNumber.Uint64()) require.Nil(t, msg) }) @@ -492,7 +492,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { tt.mockEVMClient(ob.evmMock) } - err := ob.ObserveTSSReceiveInBlock(ob.ctx, blockNumber) + err := ob.observeTSSReceiveInBlock(ob.ctx, blockNumber) if tt.errMsg != "" { require.ErrorContains(t, err, tt.errMsg) return diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 3c2430d3e3..051c3ec97a 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -13,8 +13,6 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/protocol-contracts/pkg/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" - "github.com/zeta-chain/protocol-contracts/pkg/zeta.non-eth.sol" - zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.non-eth.sol" "github.com/zeta-chain/node/zetaclient/chains/base" @@ -63,79 +61,54 @@ func New(baseObserver *base.Observer, client interfaces.EVMRPCClient) (*Observer } // load last block scanned - if err := ob.LoadLastBlockScanned(context.Background()); err != nil { + if err := ob.loadLastBlockScanned(context.Background()); err != nil { return nil, errors.Wrap(err, "unable to load last block scanned") } return ob, nil } -// GetConnectorContract returns the non-Eth connector address and binder -func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { +// getConnectorContract returns the non-Eth connector address and binder +func (ob *Observer) getConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { addr := ethcommon.HexToAddress(ob.ChainParams().ConnectorContractAddress) contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) return addr, contract, err } -// GetConnectorContractEth returns the Eth connector address and binder -func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { - addr := ethcommon.HexToAddress(ob.ChainParams().ConnectorContractAddress) - contract, err := FetchConnectorContractEth(addr, ob.evmClient) - return addr, contract, err -} - -// GetERC20CustodyContract returns ERC20Custody contract address and binder -func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { +// getERC20CustodyContract returns ERC20Custody contract address and binder +func (ob *Observer) getERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.ChainParams().Erc20CustodyContractAddress) contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } -// GetERC20CustodyV2Contract returns ERC20Custody contract address and binder +// getERC20CustodyV2Contract returns ERC20Custody contract address and binder // NOTE: we use the same address as gateway v1 // this simplify the migration process v1 will be completely removed in the future // currently the ABI for withdraw is identical, therefore both contract instances can be used -func (ob *Observer) GetERC20CustodyV2Contract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { +func (ob *Observer) getERC20CustodyV2Contract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.ChainParams().Erc20CustodyContractAddress) contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } -// GetGatewayContract returns the gateway contract address and binder -func (ob *Observer) GetGatewayContract() (ethcommon.Address, *gatewayevm.GatewayEVM, error) { +// getGatewayContract returns the gateway contract address and binder +func (ob *Observer) getGatewayContract() (ethcommon.Address, *gatewayevm.GatewayEVM, error) { addr := ethcommon.HexToAddress(ob.ChainParams().GatewayAddress) contract, err := gatewayevm.NewGatewayEVM(addr, ob.evmClient) return addr, contract, err } -// FetchConnectorContractEth returns the Eth connector address and binder -// TODO(revamp): move this to a contract package -func FetchConnectorContractEth( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*zetaconnectoreth.ZetaConnectorEth, error) { - return zetaconnectoreth.NewZetaConnectorEth(addr, client) -} - -// FetchZetaTokenContract returns the non-Eth ZETA token binder -// TODO(revamp): move this to a contract package -func FetchZetaTokenContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*zeta.ZetaNonEth, error) { - return zeta.NewZetaNonEth(addr, client) -} - -// SetTxNReceipt sets the receipt and transaction in memory -func (ob *Observer) SetTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction) { +// setTxNReceipt sets the receipt and transaction in memory +func (ob *Observer) setTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] = receipt ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] = transaction } -// GetTxNReceipt gets the receipt and transaction from memory -func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction) { +// getTxNReceipt gets the receipt and transaction from memory +func (ob *Observer) getTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() receipt := ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] @@ -143,8 +116,8 @@ func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Tr return receipt, transaction } -// IsTxConfirmed returns true if there is a confirmed tx for 'nonce' -func (ob *Observer) IsTxConfirmed(nonce uint64) bool { +// isTxConfirmed returns true if there is a confirmed tx for 'nonce' +func (ob *Observer) isTxConfirmed(nonce uint64) bool { ob.Mu().Lock() defer ob.Mu().Unlock() id := ob.OutboundID(nonce) @@ -152,8 +125,8 @@ func (ob *Observer) IsTxConfirmed(nonce uint64) bool { return ob.outboundConfirmedReceipts[id] != nil && ob.outboundConfirmedTransactions[id] != nil } -// CheckTxInclusion returns nil only if tx is included at the position indicated by the receipt ([block, index]) -func (ob *Observer) CheckTxInclusion(ctx context.Context, tx *ethtypes.Transaction, receipt *ethtypes.Receipt) error { +// checkTxInclusion returns nil only if tx is included at the position indicated by the receipt ([block, index]) +func (ob *Observer) checkTxInclusion(ctx context.Context, tx *ethtypes.Transaction, receipt *ethtypes.Receipt) error { block, err := ob.GetBlockByNumberCached(ctx, receipt.BlockNumber.Uint64()) if err != nil { return errors.Wrapf(err, "GetBlockByNumberCached error for block %d txHash %s nonce %d", @@ -168,7 +141,7 @@ func (ob *Observer) CheckTxInclusion(ctx context.Context, tx *ethtypes.Transacti txAtIndex := block.Transactions[receipt.TransactionIndex] if !strings.EqualFold(txAtIndex.Hash, tx.Hash().Hex()) { - ob.RemoveCachedBlock(receipt.BlockNumber.Uint64()) // clean stale block from cache + ob.removeCachedBlock(receipt.BlockNumber.Uint64()) // clean stale block from cache return fmt.Errorf("transaction at index %d has different hash %s, txHash %s nonce %d block %d", receipt.TransactionIndex, txAtIndex.Hash, tx.Hash(), tx.Nonce(), receipt.BlockNumber.Uint64()) } @@ -176,8 +149,8 @@ func (ob *Observer) CheckTxInclusion(ctx context.Context, tx *ethtypes.Transacti return nil } -// TransactionByHash query transaction by hash via JSON-RPC -func (ob *Observer) TransactionByHash(ctx context.Context, txHash string) (*client.Transaction, bool, error) { +// transactionByHash query transaction by hash via JSON-RPC +func (ob *Observer) transactionByHash(ctx context.Context, txHash string) (*client.Transaction, bool, error) { tx, err := ob.evmClient.TransactionByHashCustom(ctx, txHash) if err != nil { return nil, false, err @@ -189,10 +162,6 @@ func (ob *Observer) TransactionByHash(ctx context.Context, txHash string) (*clie return tx, tx.BlockNumber == nil, nil } -func (ob *Observer) TransactionReceipt(ctx context.Context, hash ethcommon.Hash) (*ethtypes.Receipt, error) { - return ob.evmClient.TransactionReceipt(ctx, hash) -} - // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(ctx context.Context, blockNumber uint64) (*client.Block, error) { @@ -206,7 +175,7 @@ func (ob *Observer) GetBlockByNumberCached(ctx context.Context, blockNumber uint return nil, fmt.Errorf("block number %d is too large", blockNumber) } // #nosec G115 always in range, checked above - block, err := ob.BlockByNumber(ctx, int(blockNumber)) + block, err := ob.blockByNumber(ctx, int(blockNumber)) if err != nil { return nil, err } @@ -214,13 +183,13 @@ func (ob *Observer) GetBlockByNumberCached(ctx context.Context, blockNumber uint return block, nil } -// RemoveCachedBlock remove block from cache -func (ob *Observer) RemoveCachedBlock(blockNumber uint64) { +// removeCachedBlock remove block from cache +func (ob *Observer) removeCachedBlock(blockNumber uint64) { ob.BlockCache().Remove(blockNumber) } -// BlockByNumber query block by number via JSON-RPC -func (ob *Observer) BlockByNumber(ctx context.Context, blockNumber int) (*client.Block, error) { +// blockByNumber query block by number via JSON-RPC +func (ob *Observer) blockByNumber(ctx context.Context, blockNumber int) (*client.Block, error) { block, err := ob.evmClient.BlockByNumberCustom(ctx, big.NewInt(int64(blockNumber))) if err != nil { return nil, err @@ -237,9 +206,9 @@ func (ob *Observer) BlockByNumber(ctx context.Context, blockNumber int) (*client return block, nil } -// LoadLastBlockScanned loads the last scanned block from the database +// loadLastBlockScanned loads the last scanned block from the database // TODO(revamp): move to a db file -func (ob *Observer) LoadLastBlockScanned(ctx context.Context) error { +func (ob *Observer) loadLastBlockScanned(ctx context.Context) error { err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) if err != nil { return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) diff --git a/zetaclient/chains/evm/observer/observer_gas_test.go b/zetaclient/chains/evm/observer/observer_gas_test.go index 4adb63533c..513a6772ad 100644 --- a/zetaclient/chains/evm/observer/observer_gas_test.go +++ b/zetaclient/chains/evm/observer/observer_gas_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "context" diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index e3b3f1512c..dbf375d317 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "context" @@ -23,7 +23,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" - "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/metrics" @@ -195,7 +194,7 @@ func Test_NewObserver(t *testing.T) { tt.logger, ) require.NoError(t, err) - ob, err := observer.New(baseObserver, tt.evmClient) + ob, err := New(baseObserver, tt.evmClient) // check result if tt.fail { @@ -220,7 +219,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { ob.WriteLastBlockScannedToDB(123) // load last block scanned - err := ob.LoadLastBlockScanned(ctx) + err := ob.loadLastBlockScanned(ctx) require.NoError(t, err) require.EqualValues(t, 123, ob.LastBlockScanned()) }) @@ -231,7 +230,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { defer os.Unsetenv(envvar) // load last block scanned - err := ob.LoadLastBlockScanned(ctx) + err := ob.loadLastBlockScanned(ctx) require.ErrorContains(t, err, "error LoadLastBlockScanned") }) t.Run("should fail on RPC error", func(t *testing.T) { @@ -246,7 +245,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { obOther.evmMock.On("BlockNumber", mock.Anything).Return(uint64(0), fmt.Errorf("error RPC")) // load last block scanned - err := obOther.LoadLastBlockScanned(ctx) + err := obOther.loadLastBlockScanned(ctx) require.ErrorContains(t, err, "error RPC") }) } @@ -289,7 +288,7 @@ func Test_BlockCache(t *testing.T) { // delete non-existing block should not panic blockNumber := uint64(123) - ts.RemoveCachedBlock(blockNumber) + ts.removeCachedBlock(blockNumber) // add a block block := &client.Block{Number: 123} @@ -301,7 +300,7 @@ func Test_BlockCache(t *testing.T) { require.EqualValues(t, block, result) // delete the block should not panic - ts.RemoveCachedBlock(blockNumber) + ts.removeCachedBlock(blockNumber) }) } @@ -325,7 +324,7 @@ func Test_CheckTxInclusion(t *testing.T) { ts.BlockCache().Add(blockNumber, block) t.Run("should pass for archived outbound", func(t *testing.T) { - err := ts.CheckTxInclusion(ts.ctx, tx, receipt) + err := ts.checkTxInclusion(ts.ctx, tx, receipt) require.NoError(t, err) }) t.Run("should fail on tx index out of range", func(t *testing.T) { @@ -333,7 +332,7 @@ func Test_CheckTxInclusion(t *testing.T) { copyReceipt := *receipt // #nosec G115 non negative value copyReceipt.TransactionIndex = uint(len(block.Transactions)) - err := ts.CheckTxInclusion(ts.ctx, tx, ©Receipt) + err := ts.checkTxInclusion(ts.ctx, tx, ©Receipt) require.ErrorContains(t, err, "out of range") }) t.Run("should fail on tx hash mismatch", func(t *testing.T) { @@ -343,7 +342,7 @@ func Test_CheckTxInclusion(t *testing.T) { ts.BlockCache().Add(blockNumber, block) // check inclusion should fail - err := ts.CheckTxInclusion(ts.ctx, tx, receipt) + err := ts.checkTxInclusion(ts.ctx, tx, receipt) require.ErrorContains(t, err, "has different hash") // wrong block should be removed from cache @@ -384,7 +383,7 @@ func Test_VoteOutboundBallot(t *testing.T) { } type testSuite struct { - *observer.Observer + *Observer ctx context.Context appContext *zctx.AppContext chainParams *observertypes.ChainParams @@ -433,7 +432,7 @@ func newTestSuite(t *testing.T, opts ...func(*testSuiteConfig)) *testSuite { baseObserver, err := base.NewObserver(chain, chainParams, zetacore, tss, 1000, nil, database, logger) require.NoError(t, err) - ob, err := observer.New(baseObserver, evmMock) + ob, err := New(baseObserver, evmMock) require.NoError(t, err) ob.WithLastBlock(1) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index c72975a7f5..a6e6b0a500 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -50,7 +50,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { for _, tracker := range trackers { // go to next tracker if this one already has a confirmed tx nonce := tracker.Nonce - if ob.IsTxConfirmed(nonce) { + if ob.isTxConfirmed(nonce) { continue } @@ -73,7 +73,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { // should be only one txHash confirmed for each nonce. if txCount == 1 { - ob.SetTxNReceipt(nonce, outboundReceipt, outbound) + ob.setTxNReceipt(nonce, outboundReceipt, outbound) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) @@ -87,8 +87,8 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { return nil } -// PostVoteOutbound posts vote to zetacore for the confirmed outbound -func (ob *Observer) PostVoteOutbound( +// postVoteOutbound posts vote to zetacore for the confirmed outbound +func (ob *Observer) postVoteOutbound( ctx context.Context, cctxIndex string, receipt *ethtypes.Receipt, @@ -156,27 +156,27 @@ func (ob *Observer) VoteOutboundIfConfirmed( ) (bool, error) { // skip if outbound is not confirmed nonce := cctx.GetCurrentOutboundParam().TssNonce - if !ob.IsTxConfirmed(nonce) { + if !ob.isTxConfirmed(nonce) { return true, nil } - receipt, transaction := ob.GetTxNReceipt(nonce) + receipt, transaction := ob.getTxNReceipt(nonce) sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) logger := ob.Logger().Outbound.With().Str("sendID", sendID).Logger() // get connector and erc20Custody contracts - connectorAddr, connector, err := ob.GetConnectorContract() + connectorAddr, connector, err := ob.getConnectorContract() if err != nil { return true, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) } - custodyAddr, custody, err := ob.GetERC20CustodyContract() + custodyAddr, custody, err := ob.getERC20CustodyContract() if err != nil { return true, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) } - gatewayAddr, gateway, err := ob.GetGatewayContract() + gatewayAddr, gateway, err := ob.getGatewayContract() if err != nil { return true, errors.Wrap(err, "error getting gateway for chain") } - _, custodyV2, err := ob.GetERC20CustodyV2Contract() + _, custodyV2, err := ob.getERC20CustodyV2Contract() if err != nil { return true, errors.Wrapf(err, "error getting erc20 custody v2 for chain %d", ob.Chain().ChainId) } @@ -194,7 +194,7 @@ func (ob *Observer) VoteOutboundIfConfirmed( if receipt.Status == ethtypes.ReceiptStatusSuccessful { receiveStatus = chains.ReceiveStatus_success } - ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) + ob.postVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) return false, nil } @@ -220,7 +220,7 @@ func (ob *Observer) VoteOutboundIfConfirmed( } // post vote to zetacore - ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) + ob.postVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) return false, nil } @@ -260,7 +260,7 @@ func parseOutboundReceivedValue( switch cointype { case coin.CoinType_Zeta: if receipt.Status == ethtypes.ReceiptStatusSuccessful { - receivedLog, revertedLog, err := ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) if err != nil { return nil, chains.ReceiveStatus_failed, err } @@ -273,7 +273,7 @@ func parseOutboundReceivedValue( } case coin.CoinType_ERC20: if receipt.Status == ethtypes.ReceiptStatusSuccessful { - withdrawn, err := ParseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) if err != nil { return nil, chains.ReceiveStatus_failed, err } @@ -288,9 +288,9 @@ func parseOutboundReceivedValue( return receiveValue, receiveStatus, nil } -// ParseAndCheckZetaEvent parses and checks ZetaReceived/ZetaReverted event from the outbound receipt +// parseAndCheckZetaEvent parses and checks ZetaReceived/ZetaReverted event from the outbound receipt // It either returns an ZetaReceived or an ZetaReverted event, or an error if no event found -func ParseAndCheckZetaEvent( +func parseAndCheckZetaEvent( cctx *crosschaintypes.CrossChainTx, receipt *ethtypes.Receipt, connectorAddr ethcommon.Address, @@ -347,8 +347,8 @@ func ParseAndCheckZetaEvent( return nil, nil, errors.New("no ZetaReceived/ZetaReverted event found") } -// ParseAndCheckWithdrawnEvent parses and checks erc20 Withdrawn event from the outbound receipt -func ParseAndCheckWithdrawnEvent( +// parseAndCheckWithdrawnEvent parses and checks erc20 Withdrawn event from the outbound receipt +func parseAndCheckWithdrawnEvent( cctx *crosschaintypes.CrossChainTx, receipt *ethtypes.Receipt, custodyAddr ethcommon.Address, @@ -380,16 +380,16 @@ func ParseAndCheckWithdrawnEvent( return nil, errors.New("no ERC20 Withdrawn event found") } -// FilterTSSOutbound filters the outbounds from TSS address to supplement outbound trackers -func (ob *Observer) FilterTSSOutbound(ctx context.Context, startBlock, toBlock uint64) { +// filterTSSOutbound filters the outbounds from TSS address to supplement outbound trackers +func (ob *Observer) filterTSSOutbound(ctx context.Context, startBlock, toBlock uint64) { // filters the outbounds from TSS address block by block for bn := startBlock; bn <= toBlock; bn++ { - ob.FilterTSSOutboundInBlock(ctx, bn) + ob.filterTSSOutboundInBlock(ctx, bn) } } -// FilterTSSOutboundInBlock filters the outbounds in a single block to supplement outbound trackers -func (ob *Observer) FilterTSSOutboundInBlock(ctx context.Context, blockNumber uint64) { +// filterTSSOutboundInBlock filters the outbounds in a single block to supplement outbound trackers +func (ob *Observer) filterTSSOutboundInBlock(ctx context.Context, blockNumber uint64) { // query block and ignore error (we don't rescan as we are only supplementing outbound trackers) block, err := ob.GetBlockByNumberCached(ctx, blockNumber) if err != nil { @@ -405,9 +405,9 @@ func (ob *Observer) FilterTSSOutboundInBlock(ctx context.Context, blockNumber ui if ethcommon.HexToAddress(tx.From) == ob.TSS().PubKey().AddressEVM() { // #nosec G115 nonce always positive nonce := uint64(tx.Nonce) - if !ob.IsTxConfirmed(nonce) { + if !ob.isTxConfirmed(nonce) { if receipt, txx, ok := ob.checkConfirmedTx(ctx, tx.Hash, nonce); ok { - ob.SetTxNReceipt(nonce, receipt, txx) + ob.setTxNReceipt(nonce, receipt, txx) ob.Logger(). Outbound.Info(). Msgf("TSS outbound detected on chain %d nonce %d tx %s", ob.Chain().ChainId, nonce, tx.Hash) @@ -491,7 +491,7 @@ func (ob *Observer) checkConfirmedTx( // cross-check tx inclusion against the block // Note: a guard for false BlockNumber in receipt. The blob-carrying tx won't come here - err = ob.CheckTxInclusion(ctx, transaction, receipt) + err = ob.checkTxInclusion(ctx, transaction, receipt) if err != nil { logger.Error().Err(err).Msg("CheckTxInclusion error") return nil, nil, false diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 750e7d09f9..6ca7d2a6f7 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "context" @@ -12,7 +12,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/testutil/sample" - "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" @@ -48,7 +47,7 @@ func Test_IsOutboundProcessed(t *testing.T) { t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { // create evm observer and set outbound and receipt ob := newTestSuite(t) - ob.SetTxNReceipt(nonce, receipt, outbound) + ob.setTxNReceipt(nonce, receipt, outbound) // post outbound vote continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) @@ -64,7 +63,7 @@ func Test_IsOutboundProcessed(t *testing.T) { // create evm observer and set outbound and receipt ob := newTestSuite(t) - ob.SetTxNReceipt(nonce, receipt, outbound) + ob.setTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address cfg := config.Config{ @@ -88,7 +87,7 @@ func Test_IsOutboundProcessed(t *testing.T) { t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { // create evm observer and set outbound and receipt ob := newTestSuite(t) - ob.SetTxNReceipt(nonce, receipt, outbound) + ob.setTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail chainParamsNew := ob.ChainParams() @@ -135,7 +134,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { // create evm observer and set outbound and receipt ob := newTestSuite(t) - ob.SetTxNReceipt(nonce, receipt, outbound) + ob.setTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI @@ -179,7 +178,7 @@ func Test_PostVoteOutbound(t *testing.T) { // create evm client using mock zetacore client and post outbound vote ob := newTestSuite(t) - ob.PostVoteOutbound( + ob.postVoteOutbound( ctx, cctx.Index, receipt, @@ -212,7 +211,7 @@ func Test_ParseZetaReceived(t *testing.T) { ) t.Run("should parse ZetaReceived event from archived outbound receipt", func(t *testing.T) { - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) require.NoError(t, err) require.NotNil(t, receivedLog) require.Nil(t, revertedLog) @@ -220,7 +219,7 @@ func Test_ParseZetaReceived(t *testing.T) { t.Run("should fail on connector address mismatch", func(t *testing.T) { // use an arbitrary address to make validation fail fakeConnectorAddress := sample.EthAddress() - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, fakeConnectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, fakeConnectorAddress, connector) require.ErrorContains(t, err, "error validating ZetaReceived event") require.Nil(t, revertedLog) require.Nil(t, receivedLog) @@ -229,7 +228,7 @@ func Test_ParseZetaReceived(t *testing.T) { // load cctx and set receiver address to an arbitrary address fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeCctx.GetCurrentOutboundParam().Receiver = sample.EthAddress().Hex() - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "receiver address mismatch") require.Nil(t, revertedLog) require.Nil(t, receivedLog) @@ -239,14 +238,14 @@ func Test_ParseZetaReceived(t *testing.T) { fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeAmount := sample.UintInRange(0, fakeCctx.GetCurrentOutboundParam().Amount.Uint64()-1) fakeCctx.GetCurrentOutboundParam().Amount = fakeAmount - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "amount mismatch") require.Nil(t, revertedLog) require.Nil(t, receivedLog) }) t.Run("should fail on cctx index mismatch", func(t *testing.T) { cctx.Index = sample.Hash().Hex() // use an arbitrary index - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "cctx index mismatch") require.Nil(t, revertedLog) require.Nil(t, receivedLog) @@ -262,7 +261,7 @@ func Test_ParseZetaReceived(t *testing.T) { testutils.EventZetaReceived, ) receipt.Logs = receipt.Logs[:1] // the 2nd log is ZetaReceived event - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "no ZetaReceived/ZetaReverted event") require.Nil(t, revertedLog) require.Nil(t, receivedLog) @@ -287,7 +286,7 @@ func Test_ParseZetaReverted(t *testing.T) { ) t.Run("should parse ZetaReverted event from archived outbound receipt", func(t *testing.T) { - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) require.NoError(t, err) require.Nil(t, receivedLog) require.NotNil(t, revertedLog) @@ -295,7 +294,7 @@ func Test_ParseZetaReverted(t *testing.T) { t.Run("should fail on connector address mismatch", func(t *testing.T) { // use an arbitrary address to make validation fail fakeConnectorAddress := sample.EthAddress() - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, fakeConnectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, fakeConnectorAddress, connector) require.ErrorContains(t, err, "error validating ZetaReverted event") require.Nil(t, receivedLog) require.Nil(t, revertedLog) @@ -304,7 +303,7 @@ func Test_ParseZetaReverted(t *testing.T) { // load cctx and set receiver address to an arbitrary address fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeCctx.InboundParams.Sender = sample.EthAddress().Hex() // the receiver is the sender for reverted ccxt - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "receiver address mismatch") require.Nil(t, revertedLog) require.Nil(t, receivedLog) @@ -314,14 +313,14 @@ func Test_ParseZetaReverted(t *testing.T) { fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeAmount := sample.UintInRange(0, fakeCctx.GetCurrentOutboundParam().Amount.Uint64()-1) fakeCctx.GetCurrentOutboundParam().Amount = fakeAmount - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(fakeCctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "amount mismatch") require.Nil(t, revertedLog) require.Nil(t, receivedLog) }) t.Run("should fail on cctx index mismatch", func(t *testing.T) { cctx.Index = sample.Hash().Hex() // use an arbitrary index to make validation fail - receivedLog, revertedLog, err := observer.ParseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) + receivedLog, revertedLog, err := parseAndCheckZetaEvent(cctx, receipt, connectorAddress, connector) require.ErrorContains(t, err, "cctx index mismatch") require.Nil(t, receivedLog) require.Nil(t, revertedLog) @@ -346,14 +345,14 @@ func Test_ParseERC20WithdrawnEvent(t *testing.T) { ) t.Run("should parse ERC20 Withdrawn event from archived outbound receipt", func(t *testing.T) { - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) require.NoError(t, err) require.NotNil(t, withdrawn) }) t.Run("should fail on erc20 custody address mismatch", func(t *testing.T) { // use an arbitrary address to make validation fail fakeCustodyAddress := sample.EthAddress() - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(cctx, receipt, fakeCustodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(cctx, receipt, fakeCustodyAddress, custody) require.ErrorContains(t, err, "error validating Withdrawn event") require.Nil(t, withdrawn) }) @@ -361,7 +360,7 @@ func Test_ParseERC20WithdrawnEvent(t *testing.T) { // load cctx and set receiver address to an arbitrary address fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeCctx.GetCurrentOutboundParam().Receiver = sample.EthAddress().Hex() - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) require.ErrorContains(t, err, "receiver address mismatch") require.Nil(t, withdrawn) }) @@ -369,7 +368,7 @@ func Test_ParseERC20WithdrawnEvent(t *testing.T) { // load cctx and set asset to an arbitrary address fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeCctx.InboundParams.Asset = sample.EthAddress().Hex() - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) require.ErrorContains(t, err, "asset mismatch") require.Nil(t, withdrawn) }) @@ -378,7 +377,7 @@ func Test_ParseERC20WithdrawnEvent(t *testing.T) { fakeCctx := testutils.LoadCctxByNonce(t, chainID, nonce) fakeAmount := sample.UintInRange(0, fakeCctx.GetCurrentOutboundParam().Amount.Uint64()-1) fakeCctx.GetCurrentOutboundParam().Amount = fakeAmount - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(fakeCctx, receipt, custodyAddress, custody) require.ErrorContains(t, err, "amount mismatch") require.Nil(t, withdrawn) }) @@ -393,7 +392,7 @@ func Test_ParseERC20WithdrawnEvent(t *testing.T) { testutils.EventERC20Withdraw, ) receipt.Logs = receipt.Logs[:1] // the 2nd log is Withdrawn event - withdrawn, err := observer.ParseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) + withdrawn, err := parseAndCheckWithdrawnEvent(cctx, receipt, custodyAddress, custody) require.ErrorContains(t, err, "no ERC20 Withdrawn event") require.Nil(t, withdrawn) }) @@ -431,14 +430,14 @@ func Test_FilterTSSOutbound(t *testing.T) { ob.WithLastBlock(blockNumber + confirmations - 1) // filter TSS outbound - ob.FilterTSSOutbound(ctx, blockNumber, blockNumber) + ob.filterTSSOutbound(ctx, blockNumber, blockNumber) // tx should be confirmed after filtering - found := ob.IsTxConfirmed(outboundNonce) + found := ob.isTxConfirmed(outboundNonce) require.True(t, found) // retrieve tx and receipt - receipt, tx = ob.GetTxNReceipt(outboundNonce) + receipt, tx = ob.getTxNReceipt(outboundNonce) require.NotNil(t, tx) require.NotNil(t, receipt) require.Equal(t, outboundHash, tx.Hash()) @@ -451,10 +450,10 @@ func Test_FilterTSSOutbound(t *testing.T) { ob.evmMock.On("BlockByNumberCustom", mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) // filter TSS outbound - ob.FilterTSSOutbound(ctx, blockNumber, blockNumber) + ob.filterTSSOutbound(ctx, blockNumber, blockNumber) // tx should be confirmed after filtering - found := ob.IsTxConfirmed(outboundNonce) + found := ob.isTxConfirmed(outboundNonce) require.False(t, found) }) } diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index 282ff94ca9..69cdc8fb83 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -66,7 +66,7 @@ func (ob *Observer) observeGatewayDeposit( rawLogs []ethtypes.Log, ) (uint64, error) { // filter ERC20CustodyDeposited logs - gatewayAddr, gatewayContract, err := ob.GetGatewayContract() + gatewayAddr, gatewayContract, err := ob.getGatewayContract() if err != nil { // lastScanned is startBlock - 1 return startBlock - 1, errors.Wrap(err, "can't get gateway contract") @@ -209,7 +209,7 @@ func (ob *Observer) observeGatewayCall( startBlock, toBlock uint64, rawLogs []ethtypes.Log, ) (uint64, error) { - gatewayAddr, gatewayContract, err := ob.GetGatewayContract() + gatewayAddr, gatewayContract, err := ob.getGatewayContract() if err != nil { // lastScanned is startBlock - 1 return startBlock - 1, errors.Wrap(err, "can't get gateway contract") @@ -326,7 +326,7 @@ func (ob *Observer) observeGatewayDepositAndCall( startBlock, toBlock uint64, rawLogs []ethtypes.Log, ) (uint64, error) { - gatewayAddr, gatewayContract, err := ob.GetGatewayContract() + gatewayAddr, gatewayContract, err := ob.getGatewayContract() if err != nil { // lastScanned is startBlock - 1 return startBlock - 1, errors.Wrap(err, "can't get gateway contract") diff --git a/zetaclient/chains/evm/observer/v2_inbound_tracker.go b/zetaclient/chains/evm/observer/v2_inbound_tracker.go index af7cb7c19f..f6df049f96 100644 --- a/zetaclient/chains/evm/observer/v2_inbound_tracker.go +++ b/zetaclient/chains/evm/observer/v2_inbound_tracker.go @@ -24,7 +24,7 @@ func (ob *Observer) ProcessInboundTrackerV2( tx *client.Transaction, receipt *ethtypes.Receipt, ) error { - gatewayAddr, gateway, err := ob.GetGatewayContract() + gatewayAddr, gateway, err := ob.getGatewayContract() if err != nil { ob.Logger().Inbound.Debug().Err(err).Msg("error getting gateway contract for processing inbound tracker") return ErrGatewayNotSet From 1be98a3053b0e56706b1a3576e6b77ddc6077f5d Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Wed, 26 Feb 2025 09:09:49 +0100 Subject: [PATCH 15/22] fix: invalid evm chain check for encore address (#3580) --- pkg/chains/chain.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index d5cb15ab0c..1ccb56600d 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -56,13 +56,14 @@ func (chain Chain) IsExternalChain() bool { // on EVM chain, it is 20Bytes // on Bitcoin chain, it is P2WPKH address, []byte(bech32 encoded string) func (chain Chain) EncodeAddress(b []byte) (string, error) { - switch chain.Consensus { - case Consensus_ethereum: + if chain.Vm == Vm_evm { addr := ethcommon.BytesToAddress(b) if addr == (ethcommon.Address{}) { return "", fmt.Errorf("invalid EVM address") } return addr.Hex(), nil + } + switch chain.Consensus { case Consensus_bitcoin: addrStr := string(b) chainParams, err := GetBTCChainParams(chain.ChainId) From 280c75c3821f799ed7a9cc70916443f643f4bef2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:26:18 +0300 Subject: [PATCH 16/22] feat(zetaclient): SUI withdrawals (#3562) * Simplify gw * Add client.GetOwnedObjectID() * Add gatewayObjectID * Update cursor parsing to be consistent w/ gw address chain params * Add defaultInterval for scheduler * Implement Sui-related crypto. Improve gateway * Add sui.SerializeSignatureECDSA * update gw params + scheduler boilerplate * Withdrawals WIP * Deduplicate Sui ECDSA signer * Add Withdraw event * Add PostOutboundTracker * Add ProcessCCTX test cases * Add DeserializeSignatureECDSA * Minor TSS getters improvement * Minor crypto improvement * MarkOutbound * Add ProcessOutboundTrackers w/ unit test * Implement VoteOutbound with unit test * Implement sui.scheduleCCTX(...) * Address PR comments * Address PR comments [2] * update changelog --- changelog.md | 1 + cmd/zetaclientd/start.go | 2 +- e2e/config/config.go | 8 +- e2e/runner/setup_sui.go | 6 +- e2e/utils/sui/signer.go | 114 ------ e2e/utils/sui/signer_test.go | 96 ------ pkg/contracts/sui/crypto.go | 165 +++++++++ pkg/contracts/sui/crypto_test.go | 160 +++++++++ pkg/contracts/sui/{inbound.go => deposit.go} | 29 +- pkg/contracts/sui/gateway.go | 141 ++++++-- pkg/contracts/sui/gateway_test.go | 85 +++-- pkg/contracts/sui/withdrawal.go | 73 ++++ pkg/scheduler/scheduler.go | 20 +- pkg/scheduler/scheduler_test.go | 8 +- zetaclient/chains/sui/client/client.go | 29 +- .../chains/sui/client/client_live_test.go | 23 ++ zetaclient/chains/sui/observer/inbound.go | 26 +- zetaclient/chains/sui/observer/observer.go | 27 ++ .../chains/sui/observer/observer_test.go | 197 ++++++++++- zetaclient/chains/sui/observer/outbound.go | 326 ++++++++++++++++++ zetaclient/chains/sui/signer/signer.go | 117 ++++++- zetaclient/chains/sui/signer/signer_test.go | 210 +++++++++++ .../chains/sui/signer/signer_tracker.go | 69 ++++ zetaclient/chains/sui/signer/signer_tx.go | 84 +++++ .../chains/sui/signer/signer_withdrawcap.go | 72 ++++ zetaclient/chains/sui/sui.go | 126 ++++++- zetaclient/logs/fields.go | 1 + zetaclient/orchestrator/v2_bootstrap.go | 12 +- zetaclient/orchestrator/v2_bootstrap_test.go | 6 + .../orchestrator/v2_orchestrator_test.go | 2 +- zetaclient/testutils/constant.go | 2 +- zetaclient/testutils/mocks/sui_client.go | 84 +++++ zetaclient/testutils/mocks/sui_gen.go | 6 + zetaclient/tss/crypto.go | 12 +- 34 files changed, 2003 insertions(+), 336 deletions(-) delete mode 100644 e2e/utils/sui/signer.go delete mode 100644 e2e/utils/sui/signer_test.go create mode 100644 pkg/contracts/sui/crypto.go create mode 100644 pkg/contracts/sui/crypto_test.go rename pkg/contracts/sui/{inbound.go => deposit.go} (67%) create mode 100644 pkg/contracts/sui/withdrawal.go create mode 100644 zetaclient/chains/sui/observer/outbound.go create mode 100644 zetaclient/chains/sui/signer/signer_test.go create mode 100644 zetaclient/chains/sui/signer/signer_tracker.go create mode 100644 zetaclient/chains/sui/signer/signer_tx.go create mode 100644 zetaclient/chains/sui/signer/signer_withdrawcap.go diff --git a/changelog.md b/changelog.md index 13173b4fa9..2c3dc1b073 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ * [3527](https://github.com/zeta-chain/node/pull/3527) - integrate SOL/SPL withdraw and call revert * [3522](https://github.com/zeta-chain/node/pull/3522) - add `MsgDisableFastConfirmation` to disable fast confirmation. This message can be triggered by the emergency policy. * [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 ### Refactor diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 80dbf283d4..eab0d134a7 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -133,7 +133,7 @@ func Start(_ *cobra.Command, _ []string) error { // Orchestrator wraps the zetacore client and adds the observers and signer maps to it. // This is the high level object used for CCTX interactions // It also handles background configuration updates from zetacore - taskScheduler := scheduler.New(logger.Std) + taskScheduler := scheduler.New(logger.Std, 0) maestroDeps := &orchestrator.Dependencies{ Zetacore: zetacoreClient, TSS: tss, diff --git a/e2e/config/config.go b/e2e/config/config.go index b6a24f1dd8..a66096fd72 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -15,7 +15,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "gopkg.in/yaml.v3" - sui_utils "github.com/zeta-chain/node/e2e/utils/sui" + "github.com/zeta-chain/node/pkg/contracts/sui" ) // DoubleQuotedString forces a string to be double quoted when marshaling to yaml. @@ -403,13 +403,13 @@ func (a Account) PrivateKey() (*ecdsa.PrivateKey, error) { } // SuiAddress derives the blake2b hash from the private key -func (a Account) SuiSigner() (*sui_utils.SignerSecp256k1, error) { +func (a Account) SuiSigner() (*sui.SignerSecp256k1, error) { privateKeyBytes, err := hex.DecodeString(a.RawPrivateKey.String()) if err != nil { return nil, fmt.Errorf("decode private key: %w", err) } - signer := sui_utils.NewSignerSecp256k1FromSecretKey(privateKeyBytes) - return signer, nil + + return sui.NewSignerSecp256k1(privateKeyBytes), nil } // Validate that the address and the private key specified in the diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index ad855dbc6c..8ec476567b 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -23,7 +23,7 @@ func (r *E2ERunner) SetupSui(faucetURL string) { client := r.Clients.Sui - publishReq, err := client.Publish(r.Ctx, models.PublishRequest{ + publishTx, err := client.Publish(r.Ctx, models.PublishRequest{ Sender: deployerAddress, CompiledModules: []string{suicontract.GatewayBytecodeBase64()}, Dependencies: []string{ @@ -34,11 +34,11 @@ func (r *E2ERunner) SetupSui(faucetURL string) { }) require.NoError(r, err, "create publish tx") - signature, err := deployerSigner.SignTransactionBlock(publishReq.TxBytes) + signature, err := deployerSigner.SignTxBlock(publishTx) require.NoError(r, err, "sign transaction") resp, err := client.SuiExecuteTransactionBlock(r.Ctx, models.SuiExecuteTransactionBlockRequest{ - TxBytes: publishReq.TxBytes, + TxBytes: publishTx.TxBytes, Signature: []string{signature}, Options: models.SuiTransactionBlockOptions{ ShowEffects: true, diff --git a/e2e/utils/sui/signer.go b/e2e/utils/sui/signer.go deleted file mode 100644 index 729abf0d2a..0000000000 --- a/e2e/utils/sui/signer.go +++ /dev/null @@ -1,114 +0,0 @@ -package sui - -import ( - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "fmt" - - "github.com/block-vision/sui-go-sdk/signer" - "github.com/decred/dcrd/dcrec/secp256k1/v4" - secp256k1_ecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" - "golang.org/x/crypto/blake2b" -) - -type SignerSecp256k1 struct { - privkey *secp256k1.PrivateKey -} - -func NewSignerSecp256k1FromSecretKey(secret []byte) *SignerSecp256k1 { - privKey := secp256k1.PrivKeyFromBytes(secret) - - return &SignerSecp256k1{ - privkey: privKey, - } -} - -// GetPublicKey returns the compressed public key bytes -func (s *SignerSecp256k1) GetPublicKey() []byte { - pub := s.privkey.PubKey() - - // Create compressed public key format: - // 0x02/0x03 | x-coordinate (32 bytes) - x := pub.X().Bytes() - - // Ensure x coordinate is 32 bytes with leading zeros if needed - paddedX := make([]byte, 32) - copy(paddedX[32-len(x):], x) - - // Prefix with 0x02 for even Y or 0x03 for odd Y - prefix := byte(0x02) - if pub.Y().Bit(0) == 1 { - prefix = 0x03 - } - - return append([]byte{prefix}, paddedX...) -} - -// GetFlaggedPublicKey returns GetPublicKey flagged for use with wallets -// and command line tools -func (s *SignerSecp256k1) GetFlaggedPublicKey() []byte { - return append([]byte{signer.SigntureFlagSecp256k1}, s.GetPublicKey()...) -} - -func (s *SignerSecp256k1) Address() string { - // Get the public key bytes - pubKeyBytes := s.GetPublicKey() - - // Create BLAKE2b hash - hash, err := blake2b.New256(nil) - if err != nil { - // This will never happen with nil key - panic(err) - } - - hash.Write([]byte{signer.SigntureFlagSecp256k1}) - hash.Write(pubKeyBytes) - addrBytes := hash.Sum(nil) - // convert to 0x hex - return "0x" + hex.EncodeToString(addrBytes) -} - -type SignedMessageSerializedSig struct { - Message string `json:"message"` - Signature string `json:"signature"` -} - -func ToSerializedSignature(signature, pubKey []byte) string { - signatureLen := len(signature) - pubKeyLen := len(pubKey) - serializedSignature := make([]byte, 1+signatureLen+pubKeyLen) - serializedSignature[0] = byte(signer.SigntureFlagSecp256k1) - copy(serializedSignature[1:], signature) - copy(serializedSignature[1+signatureLen:], pubKey) - return base64.StdEncoding.EncodeToString(serializedSignature) -} - -// SignTransactionBlock signs an encoded transaction block -func (s *SignerSecp256k1) SignTransactionBlock(txBytesEncoded string) (string, error) { - txBytes, err := base64.StdEncoding.DecodeString(txBytesEncoded) - if err != nil { - return "", fmt.Errorf("decode tx bytes: %w", err) - } - message := messageWithIntent(txBytes) - digest1 := blake2b.Sum256(message) - // this additional hash is required for secp256k1 but not ed25519 - digest2 := sha256.Sum256(digest1[:]) - - sigBytes := secp256k1_ecdsa.SignCompact(s.privkey, digest2[:], false) - // Take R and S, skip recovery byte - sigBytes = sigBytes[1:] - - signature := ToSerializedSignature(sigBytes, s.GetPublicKey()) - return signature, nil -} - -func messageWithIntent(message []byte) []byte { - intent := IntentBytes - intentMessage := make([]byte, len(intent)+len(message)) - copy(intentMessage, intent) - copy(intentMessage[len(intent):], message) - return intentMessage -} - -var IntentBytes = []byte{0, 0, 0} diff --git a/e2e/utils/sui/signer_test.go b/e2e/utils/sui/signer_test.go deleted file mode 100644 index 81de3b2a00..0000000000 --- a/e2e/utils/sui/signer_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package sui - -import ( - "bytes" - "encoding/base64" - "testing" - - sui_signer "github.com/block-vision/sui-go-sdk/signer" - "github.com/cosmos/cosmos-sdk/types/bech32" - "github.com/stretchr/testify/require" -) - -// test vector: -// see https://github.com/MystenLabs/sui/blob/199f06d25ce85f0270a1a5a0396156bb2b83122c/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts - -var VALID_SECP256K1_SECRET_KEY = []byte{ - 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, - 174, 253, 102, 39, 170, 146, 46, 252, 4, 143, 236, 12, 136, 28, -} - -var VALID_SECP256K1_PUBLIC_KEY = []byte{ - 2, 29, 21, 35, 7, 198, 183, 43, 14, 208, 65, 139, 14, 112, 205, 128, 231, 245, - 41, 91, 141, 134, 245, 114, 45, 63, 82, 19, 251, 210, 57, 79, 54, -} - -func TestSignerSecp256k1FromSecretKey(t *testing.T) { - // Create signer from secret key - signer := NewSignerSecp256k1FromSecretKey(VALID_SECP256K1_SECRET_KEY) - - // Get public key bytes - pubKey := signer.GetPublicKey() - - // Compare with expected public key - if !bytes.Equal(pubKey, VALID_SECP256K1_PUBLIC_KEY) { - t.Errorf("Public key mismatch\nexpected: %v\ngot: %v", VALID_SECP256K1_PUBLIC_KEY, pubKey) - } -} - -// Test keypairs generated and exported with -// -// sui client new-address secp256k1 -// sui keytool export -// -// See https://github.com/sui-foundation/sips/blob/main/sips/sip-15.md for encoding info -func TestSuiSecp256k1Keypair(t *testing.T) { - tests := []struct { - name string - privKey string - expectedAddress string - expectedPubkeyBase64 string - }{ - { - name: "example 1", - privKey: "suiprivkey1q8h7ejwfcdn6gc2x9kddtd9vld3kvuvtr9gdtj9qcf7nqnw94tvl79cwfq4", - expectedAddress: "0x68f6d05fd44366bd431405e8ea32e2d2f8e330d98e0089c9447ecfbbdf6e236f", - expectedPubkeyBase64: "AQOhmtkY2bTZGXRZmXZLo495i5Dz+FgJvM7bbnUCWlL2hg==", - }, - { - name: "example 2", - privKey: "suiprivkey1qxghtp2vncr94s8h7ctvgf58gy27l9nch75ka2jh37qr90xuyxhlwk5khxc", - expectedAddress: "0x8ec6f13affbf48c73550567f2a1cb8f05474c0133ebf745f91a2f3b971c1ec9a", - expectedPubkeyBase64: "AQIgu/14lUhVMEWjIB0RQ80ARJiH/xQIw4KJTEhDhHcjEQ==", - }, - { - name: "example 3", - privKey: "suiprivkey1q99wkv3fj352cn97yu5r9jwqhcvyyk6t9scwrftyjgqgflandfgc70r74hg", - expectedAddress: "0xa0f6b839f7945065ebdd030cec8e6e30d01a74c8cb31b1945909fd426c2cef80", - expectedPubkeyBase64: "AQIRTpSIUP2GOAWTEHOTYbyUjlfxpHKwPvsgwZw3G6h/RQ==", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, data, err := bech32.DecodeAndConvert(tt.privKey) - require.NoError(t, err) - - // assert this is a secp256k1 private key - require.Equal(t, byte(sui_signer.SigntureFlagSecp256k1), data[0]) - - flagLessData := data[1:] - - signer := NewSignerSecp256k1FromSecretKey(flagLessData) - - address := signer.Address() - require.Equal(t, tt.expectedAddress, address) - - pubkeyBytes := signer.GetFlaggedPublicKey() - pubkeyEncoded := base64.StdEncoding.EncodeToString(pubkeyBytes) - require.Equal(t, tt.expectedPubkeyBase64, pubkeyEncoded) - - // we don't have a good way outside e2e to verify the signature is correct, but let's exercise it anyway - _, err = signer.SignTransactionBlock("ZXhhbXBsZQo=") - require.NoError(t, err) - }) - } -} diff --git a/pkg/contracts/sui/crypto.go b/pkg/contracts/sui/crypto.go new file mode 100644 index 0000000000..c9f9eb67f3 --- /dev/null +++ b/pkg/contracts/sui/crypto.go @@ -0,0 +1,165 @@ +package sui + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + secp256k1signer "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + "golang.org/x/crypto/blake2b" +) + +const flagSecp256k1 = 0x01 + +// Digest calculates tx digest (hash) for further signing by TSS. +func Digest(tx models.TxnMetaData) ([32]byte, error) { + txBytes, err := base64.StdEncoding.DecodeString(tx.TxBytes) + if err != nil { + return [32]byte{}, errors.Wrap(err, "failed to decode tx bytes") + } + + message := messageWithIntentPrefix(txBytes) + + // "When invoking the signing API, you must first hash the intent message of the tx + // data to 32 bytes using Blake2b ... For ECDSA Secp256k1 and Secp256r1, + // you must use SHA256 as the internal hash function" + // https://docs.sui.io/concepts/cryptography/transaction-auth/signatures#signature-requirements + return blake2b.Sum256(message), nil +} + +// https://github.com/MystenLabs/sui/blob/0dc1a38f800fc2d8fabe11477fdef702058cf00d/crates/sui-types/src/intent.rs +// #1 = IntentScope(transactionData=0) +// #2 = Version(0) +// #3 = AppId(Sui=0) +var defaultIntent = []byte{0, 0, 0} + +// Constructs binary message with intent prefix. +// https://docs.sui.io/concepts/cryptography/transaction-auth/intent-signing#structs +func messageWithIntentPrefix(message []byte) []byte { + glued := make([]byte, len(defaultIntent)+len(message)) + copy(glued, defaultIntent) + copy(glued[len(defaultIntent):], message) + + return glued +} + +// AddressFromPubKeyECDSA converts ECDSA public key to Sui address. +// https://docs.sui.io/concepts/cryptography/transaction-auth/keys-addresses +// https://docs.sui.io/concepts/cryptography/transaction-auth/signatures +func AddressFromPubKeyECDSA(pk *ecdsa.PublicKey) string { + pubBytes := elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) + + raw := make([]byte, 1+len(pubBytes)) + raw[0] = flagSecp256k1 + copy(raw[1:], pubBytes) + + addrBytes := blake2b.Sum256(raw) + + return "0x" + hex.EncodeToString(addrBytes[:]) +} + +// SerializeSignatureECDSA serializes secp256k1 sig (R|S|V) and a publicKey into base64 string +// https://docs.sui.io/concepts/cryptography/transaction-auth/signatures +func SerializeSignatureECDSA(signature [65]byte, pubKey *ecdsa.PublicKey) (string, error) { + // we don't need the last V byte for recovery + const sigLen = 64 + const pubKeyLen = 33 + + pubKeyBytes := crypto.CompressPubkey(pubKey) + + // should not happen + if len(pubKeyBytes) != pubKeyLen { + return "", errors.Errorf("invalid pubKey length (got %d, want %d)", len(pubKeyBytes), pubKeyLen) + } + + serialized := make([]byte, 1+sigLen+pubKeyLen) + serialized[0] = flagSecp256k1 + copy(serialized[1:], signature[:sigLen]) + copy(serialized[1+sigLen:], pubKeyBytes) + + return base64.StdEncoding.EncodeToString(serialized), nil +} + +// DeserializeSignatureECDSA deserializes SUI secp256k1 signature. +// Returns ECDSA public key and signature. +// Sequence: `flag(1b) + sig(64b) + compressedPubKey(33b)` +func DeserializeSignatureECDSA(sigBase64 string) (*ecdsa.PublicKey, [64]byte, error) { + const ( + flagLen = 1 + sigLen = 64 + pubLen = 33 + expectedLen = flagLen + sigLen + pubLen + pubOffset = flagLen + sigLen + ) + + sigBytes, err := base64.StdEncoding.DecodeString(sigBase64) + switch { + case err != nil: + return nil, [64]byte{}, errors.Wrap(err, "failed to decode signature") + case len(sigBytes) != expectedLen: + return nil, [64]byte{}, errors.Errorf("invalid sig length (got %d, want %d)", len(sigBytes), expectedLen) + case sigBytes[0] != flagSecp256k1: + return nil, [64]byte{}, errors.Errorf("invalid sig flag (got %d, want %d)", sigBytes[0], flagSecp256k1) + case len(sigBytes[pubOffset:]) != pubLen: + return nil, [64]byte{}, errors.Errorf( + "invalid pubKey length (got %d, want %d)", + len(sigBytes[pubOffset:]), + pubLen, + ) + } + + pk, err := crypto.DecompressPubkey(sigBytes[pubOffset:]) + if err != nil { + return nil, [64]byte{}, errors.Wrap(err, "failed to decompress public key") + } + + var sig [64]byte + copy(sig[:], sigBytes[flagLen:pubOffset]) + + return pk, sig, nil +} + +// SignerSecp256k1 represents Sui Secp256k1 signer. +type SignerSecp256k1 struct { + pk *secp256k1.PrivateKey + address string +} + +// NewSignerSecp256k1 creates new SignerSecp256k1. +func NewSignerSecp256k1(privateKeyBytes []byte) *SignerSecp256k1 { + pk := secp256k1.PrivKeyFromBytes(privateKeyBytes) + address := AddressFromPubKeyECDSA(pk.PubKey().ToECDSA()) + + return &SignerSecp256k1{pk: pk, address: address} +} + +func (s *SignerSecp256k1) Address() string { + return s.address +} + +func (s *SignerSecp256k1) SignTxBlock(tx models.TxnMetaData) (string, error) { + digest, err := Digest(tx) + if err != nil { + return "", errors.Wrap(err, "unable to get digest") + } + + // Another hashing is required for ECDSA. + // https://docs.sui.io/concepts/cryptography/transaction-auth/signatures#signature-requirements + digestWrapped := sha256.Sum256(digest[:]) + + // returns V[1b] | R[32b] | S[32b], But we need R | S | V + sig := secp256k1signer.SignCompact(s.pk, digestWrapped[:], false) + + var sigReordered [65]byte + copy(sigReordered[0:32], sig[1:33]) // Copy R[32] + copy(sigReordered[32:64], sig[33:65]) // Copy S[32] + sigReordered[64] = sig[0] // Move V[1] to the end + + return SerializeSignatureECDSA(sigReordered, &s.pk.ToECDSA().PublicKey) +} diff --git a/pkg/contracts/sui/crypto_test.go b/pkg/contracts/sui/crypto_test.go new file mode 100644 index 0000000000..34a471b78f --- /dev/null +++ b/pkg/contracts/sui/crypto_test.go @@ -0,0 +1,160 @@ +package sui + +import ( + "encoding/base64" + "encoding/hex" + "testing" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCrypto(t *testing.T) { + t.Run("Digest", func(t *testing.T) { + // ARRANGE + // Given a tx (imagine client.MoveCall(...) result) + // + // Data was generated using sui cli: + // sui client transfer-sui --to "0xac5bceec1b789ff840d7d4e6ce4ce61c90d190a7f8c4f4ddf0bff6ee2413c33c" \ + // --sui-coin-object-id "0x0466a9a57add505b7b85ac485054f9b71f574f4504d9c70acd8f73ef11e0dc30" \ + // --gas-budget 500000 --serialize-unsigned-transaction + // + // https://docs.sui.io/concepts/cryptography/transaction-auth/offline-signing#sign + tx := models.TxnMetaData{ + TxBytes: "AAABACCsW87sG3if+EDX1ObOTOYckNGQp/jE9N3wv/buJBPDPAEBAQABAACsW87sG3if+EDX1ObOTOYckNGQp/jE9N3wv/buJBPDPAEEZqmlet1QW3uFrEhQVPm3H1dPRQTZxwrNj3PvEeDcMPCkyhwAAAAAICNNoyg5v4obnoVYDWw0XhxB6Tq/b+OPXnJKPc2+QM5QrFvO7Bt4n/hA19TmzkzmHJDRkKf4xPTd8L/27iQTwzzuAgAAAAAAACChBwAAAAAAAA==", + } + + // Given expected digest based on SUI cli: + // https://docs.sui.io/concepts/cryptography/transaction-auth/offline-signing#sign + // sui keytool sign --address "..." --data "$txBytesBase64" --json | jq ".digest" + const expectedDigestBase64 = "A1NY74R1IScWR/GPtOMNHVY/RwTNzWHlUbOkwp3911g=" + + // ACT + digest, err := Digest(tx) + + digestBase64 := base64.StdEncoding.EncodeToString(digest[:]) + + // ASSERT + require.NoError(t, err) + require.Equal(t, expectedDigestBase64, digestBase64) + }) + + t.Run("AddressFromPubKeyECDSA", func(t *testing.T) { + // `$> sui keytool generate secp256k1` + for _, tt := range []struct { + pubKey string + address string + }{ + { + pubKey: "AQJz6a5yi6Wtf55atMWlW/ZA4Xdd6lJKC22u3Xi/h9yeBw==", + address: "0xccf49bfb6c8159f5e53c80f5b6ecf748e4af89c8c10c27d24302207b2bc97744", + }, + { + pubKey: "AQKUgO1kyhheTjbzYYhP67nxDD1UZwEhqkLyX1KRBm1xTQ==", + address: "0x2dc141f8a8d8a3fe397054f538dcc8207fd5edf4a1415c54b7d5a4dc124d9b3e", + }, + { + pubKey: "AQIgwiNQwm529+fvaKW/n5ITbaQVUToZq+ZIpNjjOw7Spw==", + address: "0x17012be22c34ad1396f8af272b2e7b0edb529b3441912bd532faf874bf2c9262", + }, + } { + // ARRANGE + pubKeyBytes, err := base64.StdEncoding.DecodeString(tt.pubKey) + require.NoError(t, err) + + // type_flag + compression_flag + 32bytes + require.Equal(t, 1+1+32, len(pubKeyBytes)) + + pk, err := crypto.DecompressPubkey(pubKeyBytes[1:]) + require.NoError(t, err) + + // ACT + addr := AddressFromPubKeyECDSA(pk) + + // ASSERT + assert.Equal(t, tt.address, addr) + } + }) + + t.Run("SerializeSignatureECDSA", func(t *testing.T) { + // ARRANGE + // Given a pubKey + enc, _ := hex.DecodeString( + "04760c4460e5336ac9bbd87952a3c7ec4363fc0a97bd31c86430806e287b437fd1b01abc6e1db640cf3106b520344af1d58b00b57823db3e1407cbc433e1b6d04d", + ) + pubKey, err := crypto.UnmarshalPubkey(enc) + require.NoError(t, err) + + // Given signature + signature := [65]byte{0, 1, 3} + + // ACT + res, err := SerializeSignatureECDSA(signature, pubKey) + + // ASSERT + require.NoError(t, err) + + // Check signature + resBin, err := base64.StdEncoding.DecodeString(res) + require.NoError(t, err) + require.Equal(t, (1 + 64 + 33), len(resBin)) + + // ACT 2 + pubKey2, signature2, err := DeserializeSignatureECDSA(res) + + // ASSERT 2 + require.NoError(t, err) + assert.True(t, pubKey2.Equal(pubKey)) + assert.Equal(t, signature[:64], signature2[:]) + }) + + t.Run("SignerSecp256k1", func(t *testing.T) { + for _, tt := range []struct { + privKey string + address string + }{ + { + privKey: "suiprivkey1q8h7ejwfcdn6gc2x9kddtd9vld3kvuvtr9gdtj9qcf7nqnw94tvl79cwfq4", + address: "0x68f6d05fd44366bd431405e8ea32e2d2f8e330d98e0089c9447ecfbbdf6e236f", + }, + { + privKey: "suiprivkey1qxghtp2vncr94s8h7ctvgf58gy27l9nch75ka2jh37qr90xuyxhlwk5khxc", + address: "0x8ec6f13affbf48c73550567f2a1cb8f05474c0133ebf745f91a2f3b971c1ec9a", + }, + { + privKey: "suiprivkey1q99wkv3fj352cn97yu5r9jwqhcvyyk6t9scwrftyjgqgflandfgc70r74hg", + address: "0xa0f6b839f7945065ebdd030cec8e6e30d01a74c8cb31b1945909fd426c2cef80", + }, + } { + t.Run(tt.privKey, func(t *testing.T) { + // ARRANGE + // Given a private key + _, privateKeyBytes, err := bech32.DecodeAndConvert(tt.privKey) + require.NoError(t, err) + require.Equal(t, byte(flagSecp256k1), privateKeyBytes[0]) + + // Given signer (pk imported w/o flag byte) + signer := NewSignerSecp256k1(privateKeyBytes[1:]) + + // ACT + // Check signer's Sui address + address := signer.Address() + + // Sign some stub tx + // We don't have a good way outside e2e to verify the signature is correct, + // but let's exercise it anyway + const exampleBase64 = "ZXhhbXBsZQo=" + _, errSign := signer.SignTxBlock(models.TxnMetaData{ + TxBytes: exampleBase64, + }) + + // ASSERT + require.Equal(t, tt.address, address) + require.NoError(t, errSign) + }) + } + }) +} diff --git a/pkg/contracts/sui/inbound.go b/pkg/contracts/sui/deposit.go similarity index 67% rename from pkg/contracts/sui/inbound.go rename to pkg/contracts/sui/deposit.go index 7727bca719..59fbad3faf 100644 --- a/pkg/contracts/sui/inbound.go +++ b/pkg/contracts/sui/deposit.go @@ -7,9 +7,8 @@ import ( "github.com/pkg/errors" ) -// Inbound represents data for a Sui inbound, -// it is parsed from a deposit/depositAndCall event -type Inbound struct { +// Deposit represents data for a Sui deposit/depositAndCall event +type Deposit struct { // Note: CoinType is what is used as Asset field in the ForeignCoin object CoinType CoinType Amount math.Uint @@ -19,61 +18,61 @@ type Inbound struct { IsCrossChainCall bool } -func (d *Inbound) IsGasDeposit() bool { +func (d *Deposit) IsGas() bool { return d.CoinType == SUI } -func parseInbound(event models.SuiEventResponse, eventType EventType) (Inbound, error) { +func parseDeposit(event models.SuiEventResponse, eventType EventType) (Deposit, error) { parsedJSON := event.ParsedJson coinType, err := extractStr(parsedJSON, "coin_type") if err != nil { - return Inbound{}, err + return Deposit{}, err } amountRaw, err := extractStr(parsedJSON, "amount") if err != nil { - return Inbound{}, err + return Deposit{}, err } amount, err := math.ParseUint(amountRaw) if err != nil { - return Inbound{}, errors.Wrap(err, "unable to parse amount") + return Deposit{}, errors.Wrap(err, "unable to parse amount") } sender, err := extractStr(parsedJSON, "sender") if err != nil { - return Inbound{}, err + return Deposit{}, err } receiverRaw, err := extractStr(parsedJSON, "receiver") if err != nil { - return Inbound{}, err + return Deposit{}, err } receiver := ethcommon.HexToAddress(receiverRaw) if receiver == (ethcommon.Address{}) { - return Inbound{}, errors.Errorf("invalid receiver address %q", receiverRaw) + return Deposit{}, errors.Errorf("invalid receiver address %q", receiverRaw) } var isCrosschainCall bool var payload []byte - if eventType == DepositAndCall { + if eventType == DepositAndCallEvent { isCrosschainCall = true payloadRaw, ok := parsedJSON["payload"].([]any) if !ok { - return Inbound{}, errors.New("invalid payload") + return Deposit{}, errors.New("invalid payload") } payload, err = convertPayload(payloadRaw) if err != nil { - return Inbound{}, errors.Wrap(err, "unable to convert payload") + return Deposit{}, errors.Wrap(err, "unable to convert payload") } } - return Inbound{ + return Deposit{ CoinType: CoinType(coinType), Amount: amount, Sender: sender, diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index 0122c0432a..190d90e783 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -1,8 +1,10 @@ package sui import ( + "fmt" "strconv" "strings" + "sync" "github.com/block-vision/sui-go-sdk/models" "github.com/pkg/errors" @@ -16,7 +18,13 @@ type EventType string // Gateway contains the API to read inbounds and sign outbounds to the Sui gateway type Gateway struct { + // packageID is the package ID of the gateway packageID string + + // gatewayObjectID is the object ID of the gateway struct + objectID string + + mu sync.RWMutex } // SUI is the coin type for SUI, native gas token @@ -24,8 +32,9 @@ const SUI CoinType = "0000000000000000000000000000000000000000000000000000000000 // Event types const ( - Deposit EventType = "DepositEvent" - DepositAndCall EventType = "DepositAndCallEvent" + DepositEvent EventType = "DepositEvent" + DepositAndCallEvent EventType = "DepositAndCallEvent" + WithdrawEvent EventType = "WithdrawEvent" ) const moduleName = "gateway" @@ -33,11 +42,20 @@ const moduleName = "gateway" // ErrParseEvent event parse error var ErrParseEvent = errors.New("event parse error") -// NewGateway creates a new Sui gateway -// Note: packageID is the equivalent for gateway address or program ID on Solana -// It's what will be set in gateway chain params -func NewGateway(packageID string) *Gateway { - return &Gateway{packageID: packageID} +// NewGatewayFromPairID creates a new Sui Gateway +// from pair of `$packageID,$gatewayObjectID` +func NewGatewayFromPairID(pair string) (*Gateway, error) { + packageID, gatewayObjectID, err := parsePair(pair) + if err != nil { + return nil, err + } + + return NewGateway(packageID, gatewayObjectID), nil +} + +// NewGateway creates a new Sui Gateway. +func NewGateway(packageID string, gatewayObjectID string) *Gateway { + return &Gateway{packageID: packageID, objectID: gatewayObjectID} } // Event represents generic event wrapper @@ -47,29 +65,76 @@ type Event struct { EventType EventType content any - inbound bool } -// IsInbound checks whether event is Inbound. -func (e *Event) IsInbound() bool { return e.inbound } +func (e *Event) IsDeposit() bool { + return e.EventType == DepositEvent || e.EventType == DepositAndCallEvent +} -// Inbound extract Inbound. -func (e *Event) Inbound() (Inbound, error) { - if !e.inbound { - return Inbound{}, errors.Errorf("not an inbound (%+v)", e.content) +// Deposit extract DepositData. +func (e *Event) Deposit() (Deposit, error) { + v, ok := e.content.(Deposit) + if !ok { + return Deposit{}, errors.Errorf("invalid content type %T", e.content) } - return e.content.(Inbound), nil + return v, nil +} + +func (e *Event) IsWithdraw() bool { + return e.EventType == WithdrawEvent } +// Withdrawal extract WithdrawData. +func (e *Event) Withdrawal() (Withdrawal, error) { + v, ok := e.content.(Withdrawal) + if !ok { + return Withdrawal{}, errors.Errorf("invalid content type %T", e.content) + } + + return v, nil +} + +// PackageID returns object id of Gateway code func (gw *Gateway) PackageID() string { + gw.mu.RLock() + defer gw.mu.RUnlock() return gw.packageID } +// ObjectID returns Gateway's struct object id +func (gw *Gateway) ObjectID() string { + gw.mu.RLock() + defer gw.mu.RUnlock() + return gw.objectID +} + +// Module returns Gateway's module name func (gw *Gateway) Module() string { return moduleName } +// WithdrawCapType returns struct type of the WithdrawCap +func (gw *Gateway) WithdrawCapType() string { + return fmt.Sprintf("%s::%s::WithdrawCap", gw.PackageID(), moduleName) +} + +// UpdateIDs updates packageID and objectID. +func (gw *Gateway) UpdateIDs(pair string) error { + packageID, gatewayObjectID, err := parsePair(pair) + if err != nil { + return err + } + + gw.mu.Lock() + defer gw.mu.Unlock() + + gw.packageID = packageID + gw.objectID = gatewayObjectID + + return nil +} + // ParseEvent parses Event. func (gw *Gateway) ParseEvent(event models.SuiEventResponse) (Event, error) { // basic validation @@ -107,15 +172,15 @@ func (gw *Gateway) ParseEvent(event models.SuiEventResponse) (Event, error) { var ( eventType = descriptor.eventType - inbound bool content any ) // Parse specific events switch eventType { - case Deposit, DepositAndCall: - inbound = true - content, err = parseInbound(event, eventType) + case DepositEvent, DepositAndCallEvent: + content, err = parseDeposit(event, eventType) + case WithdrawEvent: + content, err = parseWithdrawal(event, eventType) default: return Event{}, errors.Wrapf(ErrParseEvent, "unknown event %q", eventType) } @@ -128,12 +193,35 @@ func (gw *Gateway) ParseEvent(event models.SuiEventResponse) (Event, error) { TxHash: txHash, EventIndex: eventID, EventType: eventType, - - content: content, - inbound: inbound, + content: content, }, nil } +// ParseTxWithdrawal a syntax sugar around ParseEvent and Withdrawal. +func (gw *Gateway) ParseTxWithdrawal(tx models.SuiTransactionBlockResponse) (event Event, w Withdrawal, err error) { + if len(tx.Events) == 0 { + err = errors.New("missing events") + return event, w, err + } + + event, err = gw.ParseEvent(tx.Events[0]) + if err != nil { + return event, w, err + } + + if !event.IsWithdraw() { + err = errors.Errorf("invalid event type %s", event.EventType) + return event, w, err + } + + w, err = event.Withdrawal() + if err != nil { + return event, w, err + } + + return event, w, err +} + type eventDescriptor struct { packageID string module string @@ -184,3 +272,12 @@ func convertPayload(data []any) ([]byte, error) { return payload, nil } + +func parsePair(pair string) (string, string, error) { + parts := strings.Split(pair, ",") + if len(parts) != 2 { + return "", "", errors.Errorf("invalid pair %q", pair) + } + + return parts[0], parts[1], nil +} diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index 87cad900a8..fb723f4d3d 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -15,6 +15,7 @@ func TestParseEvent(t *testing.T) { // stubs const ( packageID = "0x3e9fb7c01ef0d97911ccfec79306d9de2d58daa996bd3469da0f6d640cc443cf" + gatewayID = "0x444fb7c01ef0d97911ccfec79306d9de2d58daa996bd3469da0f6d640cc443aa" sender = "0x70386a9a912d9f7a603263abfbd8faae861df0ee5f8e2dbdf731fbd159f10e52" txHash = "HjxLMxMXNz8YfUc2qT4e4CrogKvGeHRbDW7Arr6ntzqq" ) @@ -23,7 +24,7 @@ func TestParseEvent(t *testing.T) { return fmt.Sprintf("%s::%s::%s", packageID, moduleName, t) } - gw := NewGateway(packageID) + gw := NewGateway(packageID, gatewayID) receiverAlice := sample.EthAddress() receiverBob := sample.EthAddress() @@ -50,19 +51,18 @@ func TestParseEvent(t *testing.T) { }, assert: func(t *testing.T, raw models.SuiEventResponse, out Event) { assert.Equal(t, txHash, out.TxHash) - assert.Equal(t, Deposit, out.EventType) + assert.Equal(t, DepositEvent, out.EventType) assert.Equal(t, uint64(0), out.EventIndex) - assert.True(t, out.IsInbound()) - - inbound, err := out.Inbound() + deposit, err := out.Deposit() require.NoError(t, err) - assert.Equal(t, SUI, inbound.CoinType) - assert.True(t, math.NewUint(100).Equal(inbound.Amount)) - assert.Equal(t, sender, inbound.Sender) - assert.Equal(t, receiverAlice, inbound.Receiver) - assert.False(t, inbound.IsCrossChainCall) + assert.Equal(t, SUI, deposit.CoinType) + assert.True(t, math.NewUint(100).Equal(deposit.Amount)) + assert.Equal(t, sender, deposit.Sender) + assert.Equal(t, receiverAlice, deposit.Receiver) + assert.False(t, deposit.IsCrossChainCall) + assert.True(t, deposit.IsGas()) }, }, { @@ -82,20 +82,18 @@ func TestParseEvent(t *testing.T) { }, assert: func(t *testing.T, raw models.SuiEventResponse, out Event) { assert.Equal(t, txHash, out.TxHash) - assert.Equal(t, DepositAndCall, out.EventType) + assert.Equal(t, DepositAndCallEvent, out.EventType) assert.Equal(t, uint64(1), out.EventIndex) - assert.True(t, out.IsInbound()) - - inbound, err := out.Inbound() + deposit, err := out.Deposit() require.NoError(t, err) - assert.Equal(t, SUI, inbound.CoinType) - assert.True(t, math.NewUint(200).Equal(inbound.Amount)) - assert.Equal(t, sender, inbound.Sender) - assert.Equal(t, receiverBob, inbound.Receiver) - assert.True(t, inbound.IsCrossChainCall) - assert.Equal(t, []byte{0, 1, 2}, inbound.Payload) + assert.Equal(t, SUI, deposit.CoinType) + assert.True(t, math.NewUint(200).Equal(deposit.Amount)) + assert.Equal(t, sender, deposit.Sender) + assert.Equal(t, receiverBob, deposit.Receiver) + assert.True(t, deposit.IsCrossChainCall) + assert.Equal(t, []byte{0, 1, 2}, deposit.Payload) }, }, { @@ -115,20 +113,47 @@ func TestParseEvent(t *testing.T) { }, assert: func(t *testing.T, raw models.SuiEventResponse, out Event) { assert.Equal(t, txHash, out.TxHash) - assert.Equal(t, DepositAndCall, out.EventType) + assert.Equal(t, DepositAndCallEvent, out.EventType) assert.Equal(t, uint64(1), out.EventIndex) - assert.True(t, out.IsInbound()) + deposit, err := out.Deposit() + require.NoError(t, err) + + assert.Equal(t, SUI, deposit.CoinType) + assert.True(t, math.NewUint(200).Equal(deposit.Amount)) + assert.Equal(t, sender, deposit.Sender) + assert.Equal(t, receiverBob, deposit.Receiver) + assert.True(t, deposit.IsCrossChainCall) + assert.Equal(t, []byte{}, deposit.Payload) + }, + }, + { + name: "withdraw", + event: models.SuiEventResponse{ + Id: models.EventId{TxDigest: txHash, EventSeq: "1"}, + PackageId: packageID, + Sender: sender, + Type: eventType("WithdrawEvent"), + ParsedJson: map[string]any{ + "coin_type": string(SUI), + "amount": "200", + "sender": sender, + "receiver": receiverBob.String(), + "nonce": "123", + }, + }, + assert: func(t *testing.T, raw models.SuiEventResponse, out Event) { + assert.Equal(t, txHash, out.TxHash) + assert.Equal(t, WithdrawEvent, out.EventType) - inbound, err := out.Inbound() + wd, err := out.Withdrawal() require.NoError(t, err) - assert.Equal(t, SUI, inbound.CoinType) - assert.True(t, math.NewUint(200).Equal(inbound.Amount)) - assert.Equal(t, sender, inbound.Sender) - assert.Equal(t, receiverBob, inbound.Receiver) - assert.True(t, inbound.IsCrossChainCall) - assert.Equal(t, []byte{}, inbound.Payload) + assert.Equal(t, SUI, wd.CoinType) + assert.True(t, math.NewUint(200).Equal(wd.Amount)) + assert.Equal(t, sender, wd.Sender) + assert.Equal(t, receiverBob.String(), wd.Receiver) + assert.True(t, wd.IsGas()) }, }, // ERRORS @@ -169,7 +194,7 @@ func TestParseEvent(t *testing.T) { event: models.SuiEventResponse{ Id: models.EventId{TxDigest: txHash, EventSeq: "0"}, PackageId: packageID, - Type: fmt.Sprintf("%s::%s::%s", packageID, "not_a_gateway", Deposit), + Type: fmt.Sprintf("%s::%s::%s", packageID, "not_a_gateway", DepositEvent), TransactionModule: "foo", }, errContains: "module mismatch", diff --git a/pkg/contracts/sui/withdrawal.go b/pkg/contracts/sui/withdrawal.go new file mode 100644 index 0000000000..523b533b38 --- /dev/null +++ b/pkg/contracts/sui/withdrawal.go @@ -0,0 +1,73 @@ +package sui + +import ( + "strconv" + + "cosmossdk.io/math" + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" +) + +// Withdrawal represents data for a Sui withdraw event +type Withdrawal struct { + CoinType CoinType + Amount math.Uint + Sender string + Receiver string + Nonce uint64 +} + +func (d *Withdrawal) IsGas() bool { + return d.CoinType == SUI +} + +func parseWithdrawal(event models.SuiEventResponse, eventType EventType) (Withdrawal, error) { + if eventType != WithdrawEvent { + return Withdrawal{}, errors.Errorf("invalid event type %q", eventType) + } + + parsedJSON := event.ParsedJson + + coinType, err := extractStr(parsedJSON, "coin_type") + if err != nil { + return Withdrawal{}, err + } + + amountRaw, err := extractStr(parsedJSON, "amount") + if err != nil { + return Withdrawal{}, err + } + + amount, err := math.ParseUint(amountRaw) + if err != nil { + return Withdrawal{}, errors.Wrap(err, "unable to parse amount") + } + + sender, err := extractStr(parsedJSON, "sender") + if err != nil { + return Withdrawal{}, err + } + + receiver, err := extractStr(parsedJSON, "receiver") + if err != nil { + return Withdrawal{}, err + } + + nonceRaw, err := extractStr(parsedJSON, "nonce") + if err != nil { + return Withdrawal{}, err + } + + nonce, err := strconv.ParseUint(nonceRaw, 10, 64) + if err != nil { + return Withdrawal{}, errors.Wrap(err, "unable to parse nonce") + } + + return Withdrawal{ + CoinType: CoinType(coinType), + Amount: amount, + Sender: sender, + Receiver: receiver, + Nonce: nonce, + }, nil +} diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 71d5490ff8..5cdc114688 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -20,9 +20,10 @@ import ( // Scheduler represents background task scheduler. type Scheduler struct { - tasks map[uuid.UUID]*Task - mu sync.RWMutex - logger zerolog.Logger + tasks map[uuid.UUID]*Task + mu sync.RWMutex + logger zerolog.Logger + defaultInterval time.Duration } // Executable arbitrary function that can be executed. @@ -68,10 +69,15 @@ type taskOpts struct { } // New Scheduler instance. -func New(logger zerolog.Logger) *Scheduler { +func New(logger zerolog.Logger, defaultInterval time.Duration) *Scheduler { + if defaultInterval <= 0 { + defaultInterval = time.Second * 10 + } + return &Scheduler{ - tasks: make(map[uuid.UUID]*Task), - logger: logger.With().Str("module", "scheduler").Logger(), + tasks: make(map[uuid.UUID]*Task), + logger: logger.With().Str("module", "scheduler").Logger(), + defaultInterval: defaultInterval, } } @@ -87,7 +93,7 @@ func (s *Scheduler) Register(ctx context.Context, exec Executable, opts ...Opt) } config := &taskOpts{ - interval: time.Second * 10, + interval: s.defaultInterval, } for _, opt := range opts { diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index 1347c572e7..d578285768 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -34,10 +34,10 @@ func TestScheduler(t *testing.T) { ts.scheduler.Stop() // ASSERT - // Counter should be 1 because we invoke a task once on a start, - // once after 10 second (default interval), + // Counter should be 2 because we invoke a task once on a start, + // once after 1 second // and then at T=1.5s we stop the scheduler. - assert.Equal(t, int32(1), counter) + assert.Equal(t, int32(2), counter) // Check logs assert.Contains(t, ts.logger.String(), "Stopped scheduler task") @@ -361,7 +361,7 @@ type testSuite struct { func newTestSuite(t *testing.T) *testSuite { logger := testlog.New(t) - scheduler := New(logger.Logger) + scheduler := New(logger.Logger, time.Second) t.Cleanup(scheduler.Stop) return &testSuite{ diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 72772b6024..f6493650ca 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -130,9 +130,34 @@ func (p *EventQuery) asRequest() (models.SuiXQueryEventsRequest, error) { }, nil } +// GetOwnedObjectID returns the first owned object ID by owner address and struct type. +// If no objects found or multiple objects found, returns error. +func (c *Client) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) { + res, err := c.SuiXGetOwnedObjects(ctx, models.SuiXGetOwnedObjectsRequest{ + Address: ownerAddress, + Query: models.SuiObjectResponseQuery{ + Filter: map[string]any{ + "StructType": structType, + }, + }, + Limit: 1, + }) + + switch { + case err != nil: + return "", errors.Wrap(err, "unable to get owned objects") + case len(res.Data) == 0: + return "", errors.New("no objects found") + case len(res.Data) > 1: + return "", errors.New("multiple objects found") + } + + return res.Data[0].Data.ObjectId, nil +} + // EncodeCursor encodes event ID into cursor. func EncodeCursor(id models.EventId) string { - return fmt.Sprintf("%s#%s", id.TxDigest, id.EventSeq) + return fmt.Sprintf("%s,%s", id.TxDigest, id.EventSeq) } // DecodeCursor decodes cursor into event ID. @@ -141,7 +166,7 @@ func DecodeCursor(cursor string) (*models.EventId, error) { return nil, nil } - parts := strings.Split(cursor, "#") + parts := strings.Split(cursor, ",") if len(parts) != 2 { return nil, errors.New("invalid cursor format") } diff --git a/zetaclient/chains/sui/client/client_live_test.go b/zetaclient/chains/sui/client/client_live_test.go index 8f74f86fda..f9494e1af2 100644 --- a/zetaclient/chains/sui/client/client_live_test.go +++ b/zetaclient/chains/sui/client/client_live_test.go @@ -93,6 +93,29 @@ func TestClientLive(t *testing.T) { eventsEqual(t, a, resCombined[i]) } }) + + t.Run("GetOwnedObjectID", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, RpcMainnet) + + // Given admin wallet us Cetus DEX team + // (yeah, it took some time to find it) + const ownerAddress = "0xdbfd0b17fa804c98f51d552b050fb7f850b85db96fa2a0d79e50119525814a47" + + // Given AdminCap struct type of Cetus DEX + // (they use it for upgrades and stuff) + const structType = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb::config::AdminCap" + + // ACT + // Get owned object id as we would fetch Gateway's WithdrawCap + // that should belong to TSS + objectID, err := ts.GetOwnedObjectID(ts.ctx, ownerAddress, structType) + + // ASSERT + // https://suiscan.xyz/mainnet/object/0x89c1a321291d15ddae5a086c9abc533dff697fde3d89e0ca836c41af73e36a75 + require.NoError(t, err) + require.Equal(t, "0x89c1a321291d15ddae5a086c9abc533dff697fde3d89e0ca836c41af73e36a75", objectID) + }) } type testSuite struct { diff --git a/zetaclient/chains/sui/observer/inbound.go b/zetaclient/chains/sui/observer/inbound.go index 46fd3ff41e..f0eb368fe1 100644 --- a/zetaclient/chains/sui/observer/inbound.go +++ b/zetaclient/chains/sui/observer/inbound.go @@ -103,8 +103,6 @@ func (ob *Observer) processInboundEvent( return nil case err != nil: return errors.Wrap(err, "unable to parse event") - case !event.IsInbound(): - ob.Logger().Inbound.Info().Msg("Not an inbound event. Skipping") case event.EventIndex != 0: // Is it possible to have multiple events per tx? // e.g. contract "A" calls Gateway multiple times in a single tx (deposit to multiple accounts) @@ -161,14 +159,16 @@ func (ob *Observer) constructInboundVote( event sui.Event, tx models.SuiTransactionBlockResponse, ) (*cctypes.MsgVoteInbound, error) { - inbound, err := event.Inbound() + deposit, err := event.Deposit() if err != nil { return nil, errors.Wrap(err, "unable to extract inbound") } coinType := coin.CoinType_Gas - if !inbound.IsGasDeposit() { + asset := "" + if !deposit.IsGas() { coinType = coin.CoinType_ERC20 + asset = string(deposit.CoinType) } // Sui uses checkpoint seq num instead of block height @@ -177,21 +177,15 @@ func (ob *Observer) constructInboundVote( return nil, errors.Wrap(err, "unable to parse checkpoint") } - // Empty or full SUI coin name - var asset string - if !inbound.IsGasDeposit() { - asset = string(inbound.CoinType) - } - return cctypes.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), - inbound.Sender, + deposit.Sender, ob.Chain().ChainId, - inbound.Sender, - inbound.Receiver.String(), + deposit.Sender, + deposit.Receiver.String(), ob.ZetacoreClient().Chain().ChainId, - inbound.Amount, - hex.EncodeToString(inbound.Payload), + deposit.Amount, + hex.EncodeToString(deposit.Payload), event.TxHash, checkpointSeqNum, zetacore.PostVoteInboundCallOptionsGasLimit, @@ -202,6 +196,6 @@ func (ob *Observer) constructInboundVote( false, cctypes.InboundStatus_SUCCESS, cctypes.ConfirmationMode_SAFE, - cctypes.WithCrossChainCall(inbound.IsCrossChainCall), + cctypes.WithCrossChainCall(deposit.IsCrossChainCall), ), nil } diff --git a/zetaclient/chains/sui/observer/observer.go b/zetaclient/chains/sui/observer/observer.go index 823326856b..47df76b0aa 100644 --- a/zetaclient/chains/sui/observer/observer.go +++ b/zetaclient/chains/sui/observer/observer.go @@ -4,6 +4,7 @@ import ( "context" "os" "strconv" + "sync" "time" "github.com/block-vision/sui-go-sdk/models" @@ -19,6 +20,13 @@ type Observer struct { *base.Observer client RPC gateway *sui.Gateway + + // nonce -> sui outbound tx + txMap map[uint64]models.SuiTransactionBlockResponse + txMu sync.RWMutex + + latestGasPrice uint64 + gasPriceMu sync.RWMutex } // RPC represents subset of Sui RPC methods. @@ -41,9 +49,13 @@ func New(baseObserver *base.Observer, client RPC, gateway *sui.Gateway) *Observe Observer: baseObserver, client: client, gateway: gateway, + txMap: make(map[uint64]models.SuiTransactionBlockResponse), } } +// Gateway returns Sui gateway. +func (ob *Observer) Gateway() *sui.Gateway { return ob.gateway } + // CheckRPCStatus checks the RPC status of the chain. func (ob *Observer) CheckRPCStatus(ctx context.Context) error { blockTime, err := ob.client.HealthCheck(ctx) @@ -90,6 +102,8 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { // no priority fee for Sui const priorityFee = 0 + ob.setLatestGasPrice(gasPrice) + _, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), gasPrice, priorityFee, epochNum) if err != nil { return errors.Wrap(err, "unable to post vote for gas price") @@ -98,6 +112,19 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { return nil } +func (ob *Observer) getLatestGasPrice() uint64 { + ob.gasPriceMu.RLock() + defer ob.gasPriceMu.RUnlock() + + return ob.latestGasPrice +} + +func (ob *Observer) setLatestGasPrice(price uint64) { + ob.gasPriceMu.Lock() + defer ob.gasPriceMu.Unlock() + ob.latestGasPrice = price +} + // ensureCursor ensures tx scroll cursor for inbound observations func (ob *Observer) ensureCursor(ctx context.Context) error { if ob.LastTxScanned() != "" { diff --git a/zetaclient/chains/sui/observer/observer_test.go b/zetaclient/chains/sui/observer/observer_test.go index 48823cf24d..c14efbef0e 100644 --- a/zetaclient/chains/sui/observer/observer_test.go +++ b/zetaclient/chains/sui/observer/observer_test.go @@ -24,6 +24,8 @@ import ( "github.com/zeta-chain/node/zetaclient/testutils/testlog" ) +var someArgStub = map[string]any{} + func TestObserver(t *testing.T) { t.Run("PostGasPrice", func(t *testing.T) { // ARRANGE @@ -86,13 +88,13 @@ func TestObserver(t *testing.T) { expectedQuery := client.EventQuery{ PackageID: ts.gateway.PackageID(), Module: ts.gateway.Module(), - Cursor: "ABC123_first_tx#0", + Cursor: "ABC123_first_tx,0", Limit: client.DefaultEventsLimit, } // ...two of which are valid (1 & 3) events := []models.SuiEventResponse{ - ts.SampleEvent("TX_1_ok", string(sui.Deposit), map[string]any{ + ts.SampleEvent("TX_1_ok", string(sui.DepositEvent), map[string]any{ "coin_type": string(sui.SUI), "amount": "200", "sender": "SUI_BOB", @@ -104,7 +106,7 @@ func TestObserver(t *testing.T) { "sender": "SUI_BOB", "receiver": evmBob.String(), }), - ts.SampleEvent("TX_3_ok", string(sui.DepositAndCall), map[string]any{ + ts.SampleEvent("TX_3_ok", string(sui.DepositAndCallEvent), map[string]any{ // USDC "coin_type": usdc, "amount": "300", @@ -112,7 +114,7 @@ func TestObserver(t *testing.T) { "receiver": evmAlice.String(), "payload": []any{float64(1), float64(2), float64(3)}, }), - ts.SampleEvent("TX_4_invalid_data", string(sui.Deposit), map[string]any{ + ts.SampleEvent("TX_4_invalid_data", string(sui.DepositEvent), map[string]any{ "coin_type": string(sui.SUI), "amount": "hello", "sender": "SUI_BOB", @@ -136,7 +138,7 @@ func TestObserver(t *testing.T) { require.NoError(t, err) // Check that final cursor is on INVALID event, that's expected - assert.Equal(t, "TX_4_invalid_data#0", ts.LastTxScanned()) + assert.Equal(t, "TX_4_invalid_data,0", ts.LastTxScanned()) // Check for transactions assert.Equal(t, 2, len(ts.inboundVotesBag)) @@ -193,7 +195,7 @@ func TestObserver(t *testing.T) { evmAlice := sample.EthAddress() ts.OnGetTx("TX_TRACKER_1", "15000", true, []models.SuiEventResponse{ - ts.SampleEvent("TX_TRACKER_1", string(sui.Deposit), map[string]any{ + ts.SampleEvent("TX_TRACKER_1", string(sui.DepositEvent), map[string]any{ "coin_type": string(sui.SUI), "amount": "1000", "sender": "SUI_ALICE", @@ -221,6 +223,151 @@ func TestObserver(t *testing.T) { assert.Equal(t, math.NewUint(1000), vote.Amount) assert.Equal(t, evmAlice.String(), vote.Receiver) }) + + t.Run("ProcessOutboundTrackers", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given cctx + const nonce = 333 + cctx := sample.CrossChainTxV2(t, "0x123") + cctx.OutboundParams = []*cctypes.OutboundParams{{TssNonce: nonce}} + + ts.MockCCTXByNonce(cctx) + + // Given outbound tracker + const digest = "0xSuiTxHash" + tracker := cctypes.OutboundTracker{ + Index: "0xAAA", + ChainId: ts.Chain().ChainId, + Nonce: nonce, + HashList: []*cctypes.TxHash{{TxHash: digest}}, + } + + ts.MockOutboundTrackers([]cctypes.OutboundTracker{tracker}) + + // Given Sui tx signature + sigBase64, err := sui.SerializeSignatureECDSA([65]byte{1, 2, 3}, ts.TSS().PubKey().AsECDSA()) + require.NoError(t, err) + + // Given Sui tx + tx := models.SuiTransactionBlockResponse{ + Digest: digest, + Checkpoint: "123", + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{Status: "success"}, + }, + Transaction: models.SuiTransactionBlock{ + Data: models.SuiTransactionBlockData{ + Transaction: models.SuiTransactionBlockKind{ + Inputs: []models.SuiCallArg{ + someArgStub, + someArgStub, + map[string]any{ + "type": "pure", + "valueType": "u64", + "value": fmt.Sprintf("%d", nonce), + }, + someArgStub, + someArgStub, + }, + }, + }, + TxSignatures: []string{sigBase64}, + }, + } + + ts.MockGetTxOnce(tx) + + // ACT + err = ts.ProcessOutboundTrackers(ts.ctx) + + // ASSERT + require.NoError(t, err) + assert.True(t, ts.OutboundCreated(nonce)) + assert.False(t, ts.OutboundCreated(nonce+1)) + }) + + t.Run("VoteOutbound", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given Sui Gateway + gw := ts.Gateway() + + // Given cctx + const nonce = 333 + cctx := sample.CrossChainTxV2(t, "0x123") + cctx.OutboundParams = []*cctypes.OutboundParams{{TssNonce: nonce}} + + // Given Sui receiver + const receiver = "0xAliceOnSui" + + // Given a valid Sui outbound tx with Withdrawal event + const digest = "0xSuiTxDigest" + tx := models.SuiTransactionBlockResponse{ + Digest: digest, + Checkpoint: "999", + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{Status: "success"}, + GasUsed: models.GasCostSummary{ + ComputationCost: "200", + StorageCost: "300", + StorageRebate: "50", + }, + }, + Events: []models.SuiEventResponse{{ + Id: models.EventId{TxDigest: digest, EventSeq: "1"}, + PackageId: gw.PackageID(), + Sender: ts.TSS().PubKey().AddressSui(), + Type: fmt.Sprintf("%s::%s::%s", gw.PackageID(), gw.Module(), "WithdrawEvent"), + ParsedJson: map[string]any{ + "coin_type": string(sui.SUI), + "amount": "200", + "sender": ts.TSS().PubKey().AddressSui(), + "receiver": receiver, + "nonce": fmt.Sprintf("%d", nonce), + }, + }}, + } + + // What was fetched during ProcessOutboundTracker(...) + ts.setTx(tx, nonce) + + // Given a gas price that was set during PostGasPrice(...) + ts.setLatestGasPrice(1000) + + // Given outbound votes catcher + ts.CatchOutboundVotes() + + // ACT + err := ts.VoteOutbound(ts.ctx, cctx) + + // ASSERT + require.NoError(t, err) + require.Len(t, ts.outboundVotesBag, 1) + + vote := ts.outboundVotesBag[0] + + // common + assert.Equal(t, chains.ReceiveStatus_success, vote.Status) + assert.Equal(t, cctx.Index, vote.CctxHash) + assert.Equal(t, uint64(nonce), vote.OutboundTssNonce) + assert.Equal(t, ts.Chain().ChainId, vote.OutboundChain) + + // digest + checkpoint + assert.Equal(t, digest, vote.ObservedOutboundHash) + assert.Equal(t, uint64(999), vote.ObservedOutboundBlockHeight) + + // amount + assert.Equal(t, coin.CoinType_Gas, vote.CoinType) + assert.Equal(t, uint64(200), vote.ValueReceived.Uint64()) + + // gas + assert.Equal(t, uint64(maxGasLimit), vote.ObservedOutboundEffectiveGasLimit) + assert.Equal(t, uint64(1000), vote.ObservedOutboundEffectiveGasPrice.Uint64()) + assert.Equal(t, uint64(200+300-50), vote.ObservedOutboundGasUsed) + }) } type testSuite struct { @@ -232,7 +379,8 @@ type testSuite struct { log *testlog.Log gateway *sui.Gateway - inboundVotesBag []*cctypes.MsgVoteInbound + inboundVotesBag []*cctypes.MsgVoteInbound + outboundVotesBag []*cctypes.MsgVoteOutbound *Observer } @@ -244,8 +392,6 @@ func newTestSuite(t *testing.T) *testSuite { chainParams := mocks.MockChainParams(chain.ChainId, 10) require.NotEmpty(t, chainParams.GatewayAddress) - // todo zctx with chain & params (in future PRs) - zetacore := mocks.NewZetacoreClient(t). WithZetaChain(). WithKeys(&keys.Keys{ @@ -268,7 +414,8 @@ func newTestSuite(t *testing.T) *testSuite { suiMock := mocks.NewSuiClient(t) - gw := sui.NewGateway(chainParams.GatewayAddress) + gw, err := sui.NewGatewayFromPairID(chainParams.GatewayAddress) + require.NoError(t, err) observer := New(baseObserver, suiMock, gw) @@ -315,6 +462,10 @@ func (ts *testSuite) OnGetTx(digest, checkpoint string, showEvents bool, events ts.suiMock.On("SuiGetTransactionBlock", mock.Anything, req).Return(res, nil).Once() } +func (ts *testSuite) MockGetTxOnce(tx models.SuiTransactionBlockResponse) { + ts.suiMock.On("SuiGetTransactionBlock", mock.Anything, mock.Anything).Return(tx, nil).Once() +} + func (ts *testSuite) CatchInboundVotes() { callback := func(_ context.Context, _, _ uint64, msg *cctypes.MsgVoteInbound) (string, string, error) { ts.inboundVotesBag = append(ts.inboundVotesBag, msg) @@ -326,3 +477,29 @@ func (ts *testSuite) CatchInboundVotes() { Return(callback). Maybe() } + +func (ts *testSuite) CatchOutboundVotes() { + callback := func(_ context.Context, _, _ uint64, msg *cctypes.MsgVoteOutbound) (string, string, error) { + ts.outboundVotesBag = append(ts.outboundVotesBag, msg) + return "", "", nil + } + + ts.zetaMock. + On("PostVoteOutbound", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(callback). + Maybe() +} + +func (ts *testSuite) MockCCTXByNonce(cctx *cctypes.CrossChainTx) *mock.Call { + nonce := cctx.GetCurrentOutboundParam().TssNonce + + return ts.zetaMock. + On("GetCctxByNonce", ts.ctx, ts.Chain().ChainId, nonce). + Return(cctx, nil) +} + +func (ts *testSuite) MockOutboundTrackers(trackers []cctypes.OutboundTracker) *mock.Call { + return ts.zetaMock. + On("GetAllOutboundTrackerByChain", mock.Anything, ts.Chain().ChainId, mock.Anything). + Return(trackers, nil) +} diff --git a/zetaclient/chains/sui/observer/outbound.go b/zetaclient/chains/sui/observer/outbound.go new file mode 100644 index 0000000000..f927552dea --- /dev/null +++ b/zetaclient/chains/sui/observer/outbound.go @@ -0,0 +1,326 @@ +package observer + +import ( + "context" + "strconv" + + "cosmossdk.io/math" + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/contracts/sui" + cctypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/logs" + "github.com/zeta-chain/node/zetaclient/zetacore" +) + +// https://github.com/zeta-chain/protocol-contracts-sui/blob/9d08a70817d8cc7cf799b9ae12c59b6e0b8aaab9/sources/gateway.move#L125 +// (excluding last arg of `ctx`) +const expectedWithdrawArgs = 5 + +// 50 SUI +// https://docs.sui.io/concepts/tokenomics/gas-in-sui#gas-budgets +const maxGasLimit = 50_000_000_000 + +// OutboundCreated checks if the outbound tx exists in the memory +// and has valid nonce & signature +func (ob *Observer) OutboundCreated(nonce uint64) bool { + _, ok := ob.getTx(nonce) + return ok +} + +// ProcessOutboundTrackers loads all freshly-included Sui transactions in-memory +// for further voting by Observer-Signer. +func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { + chainID := ob.Chain().ChainId + + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) + if err != nil { + return errors.Wrap(err, "unable to get outbound trackers") + } + + for _, tracker := range trackers { + nonce := tracker.Nonce + + // already loaded + if _, ok := ob.getTx(nonce); ok { + continue + } + + // should not happen + if len(tracker.HashList) == 0 { + // we don't want to block other cctxs, so let's error and continue + ob.Logger().Outbound.Error(). + Str(logs.FieldMethod, "ProcessOutboundTrackers"). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTracker, tracker.Index). + Msg("Tracker hash list is empty!") + continue + } + + digest := tracker.HashList[0].TxHash + + cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) + if err != nil { + return errors.Wrapf(err, "unable to get cctx by nonce %d (sui digest %q)", tracker.Nonce, digest) + } + + if err := ob.loadOutboundTx(ctx, cctx, digest); err != nil { + // we don't want to block other cctxs, so let's error and continue + ob.Logger().Outbound. + Error().Err(err). + Str(logs.FieldMethod, "ProcessOutboundTrackers"). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, digest). + Msg("Unable to load outbound transaction") + } + } + + return nil +} + +// VoteOutbound calculates outbound result based on cctx and in-mem Sui tx +// and votes the ballot to zetacore. +func (ob *Observer) VoteOutbound(ctx context.Context, cctx *cctypes.CrossChainTx) error { + chainID := ob.Chain().ChainId + nonce := cctx.GetCurrentOutboundParam().TssNonce + + // should be fetched by ProcessOutboundTrackers routine + // if exists, we can safely assume it's authentic and nonce is valid + tx, ok := ob.getTx(nonce) + if !ok { + return errors.Errorf("missing tx for nonce %d", nonce) + } + + // used instead of block height + checkpoint, err := strconv.ParseUint(tx.Checkpoint, 10, 64) + if err != nil { + return errors.Wrap(err, "unable to parse checkpoint") + } + + // parse status, coinType, and amount + var ( + status = chains.ReceiveStatus_failed + coinType = coin.CoinType_Gas + amount = math.NewUint(0) + isSuccess = tx.Effects.Status.Status == "success" + ) + + if isSuccess { + status = chains.ReceiveStatus_success + + _, w, err := ob.gateway.ParseTxWithdrawal(tx) + if err != nil { + return errors.Wrap(err, "unable to parse tx withdrawal") + } + + if !w.IsGas() { + coinType = coin.CoinType_ERC20 + } + + amount = w.Amount + } + + // Gas parameters + // Gas price *might* change once per epoch (~24h), so using the latest value is fine. + // #nosec G115 - always in range + outboundGasPrice := math.NewInt(int64(ob.getLatestGasPrice())) + + // This might happen after zetacore restart when PostGasPrice has not been called yet. retry later. + if outboundGasPrice.IsZero() { + return errors.New("latest gas price is zero") + } + + outboundGasUsed, err := parseGasUsed(tx) + if err != nil { + return errors.Wrap(err, "unable to parse gas used") + } + + // Create message + msg := cctypes.NewMsgVoteOutbound( + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + cctx.Index, + tx.Digest, + checkpoint, + outboundGasUsed, + outboundGasPrice, + maxGasLimit, + amount, + status, + chainID, + nonce, + coinType, + cctypes.ConfirmationMode_SAFE, + ) + + // TODO compliance checks + // https://github.com/zeta-chain/node/issues/3584 + + if err := ob.postVoteOutbound(ctx, msg); err != nil { + return errors.Wrap(err, "unable to post vote outbound") + } + + ob.unsetTx(nonce) + + return nil +} + +// loadOutboundTx loads cross-chain outbound tx by digest and ensures its authenticity. +func (ob *Observer) loadOutboundTx(ctx context.Context, cctx *cctypes.CrossChainTx, digest string) error { + res, err := ob.client.SuiGetTransactionBlock(ctx, models.SuiGetTransactionBlockRequest{ + Digest: digest, + Options: models.SuiTransactionBlockOptions{ + ShowEvents: true, + ShowInput: true, + ShowEffects: true, + }, + }) + + if err != nil { + return errors.Wrap(err, "unable to get tx") + } + + if err := ob.validateOutbound(cctx, res); err != nil { + return errors.Wrap(err, "tx validation failed") + } + + ob.setTx(res, cctx.GetCurrentOutboundParam().TssNonce) + + return nil +} + +// validateOutbound validates the authenticity of the outbound transaction. +// Note that it doesn't care about successful execution (e.g. something failed). +func (ob *Observer) validateOutbound(cctx *cctypes.CrossChainTx, tx models.SuiTransactionBlockResponse) error { + nonce := cctx.GetCurrentOutboundParam().TssNonce + + inputs := tx.Transaction.Data.Transaction.Inputs + + // Check args length + if len(inputs) != expectedWithdrawArgs { + return errors.Errorf("invalid number of input arguments (got %d, want %d)", len(inputs), expectedWithdrawArgs) + } + + txNonce, err := parseNonceFromWithdrawInputs(inputs) + if err != nil { + return errors.Wrap(err, "unable to parse nonce from inputs") + } + + if txNonce != nonce { + return errors.Errorf("nonce mismatch (tx nonce %d, cctx nonce %d)", txNonce, nonce) + } + + if len(tx.Transaction.TxSignatures) == 0 { + return errors.New("missing tx signature") + } + + pubKey, _, err := sui.DeserializeSignatureECDSA(tx.Transaction.TxSignatures[0]) + if err != nil { + return errors.Wrap(err, "unable to deserialize tx signature") + } + + if !ob.TSS().PubKey().AsECDSA().Equal(pubKey) { + return errors.New("pubKey mismatch") + } + + return nil +} + +func (ob *Observer) postVoteOutbound(ctx context.Context, msg *cctypes.MsgVoteOutbound) error { + const gasLimit = zetacore.PostVoteOutboundGasLimit + + retryGasLimit := uint64(0) + if msg.Status == chains.ReceiveStatus_failed { + retryGasLimit = zetacore.PostVoteOutboundRevertGasLimit + } + + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) + switch { + case err != nil: + return errors.Wrap(err, "unable to post vote outbound") + case zetaTxHash != "": + ob.Logger().Outbound.Info(). + Str(logs.FieldZetaTx, zetaTxHash). + Str(logs.FieldBallot, ballot). + Msg("PostVoteOutbound: posted outbound vote successfully") + } + + return nil +} + +func (ob *Observer) getTx(nonce uint64) (models.SuiTransactionBlockResponse, bool) { + ob.txMu.RLock() + defer ob.txMu.RUnlock() + + tx, ok := ob.txMap[nonce] + + return tx, ok +} + +func (ob *Observer) setTx(tx models.SuiTransactionBlockResponse, nonce uint64) { + ob.txMu.Lock() + defer ob.txMu.Unlock() + + ob.txMap[nonce] = tx +} + +func (ob *Observer) unsetTx(nonce uint64) { + ob.txMu.Lock() + defer ob.txMu.Unlock() + + delete(ob.txMap, nonce) +} + +func parseNonceFromWithdrawInputs(inputs []models.SuiCallArg) (uint64, error) { + if len(inputs) != expectedWithdrawArgs { + return 0, errors.New("invalid number of input arguments") + } + + const nonceIdx = 2 + + // { + // "type": "pure", + // "valueType": "u64", + // "value": "12345" + // } + raw := inputs[nonceIdx] + + if raw["type"] != "pure" || raw["valueType"] != "u64" { + return 0, errors.Errorf("invalid nonce object %+v", raw) + } + + return strconv.ParseUint(raw["value"].(string), 10, 64) +} + +func parseGasUsed(tx models.SuiTransactionBlockResponse) (uint64, error) { + gas := tx.Effects.GasUsed + + compCost, err := parseUint64(gas.ComputationCost) + if err != nil { + return 0, errors.Wrap(err, "comp cost") + } + + storageCost, err := parseUint64(gas.StorageCost) + if err != nil { + return 0, errors.Wrap(err, "storage cost") + } + + storageRebate, err := parseUint64(gas.StorageRebate) + if err != nil { + return 0, errors.Wrap(err, "storage rebate") + } + + // should not happen + if (compCost + storageCost) < storageRebate { + return 0, errors.New("storage rebate exceeds total costs") + } + + return compCost + storageCost - storageRebate, nil +} + +func parseUint64(v string) (uint64, error) { + return strconv.ParseUint(v, 10, 64) +} diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 98c2088750..68b260bc5a 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -1,13 +1,124 @@ package signer -import "github.com/zeta-chain/node/zetaclient/chains/base" +import ( + "context" + "crypto/sha256" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/bg" + "github.com/zeta-chain/node/pkg/contracts/sui" + cctypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/logs" +) // Signer Sui outbound transaction signer. type Signer struct { *base.Signer + client RPC + gateway *sui.Gateway + withdrawCap *withdrawCap + + zetacore interfaces.ZetacoreClient +} + +// RPC represents Sui rpc. +type RPC interface { + GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) + + MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) + SuiExecuteTransactionBlock( + ctx context.Context, + req models.SuiExecuteTransactionBlockRequest, + ) (models.SuiTransactionBlockResponse, error) + SuiGetTransactionBlock( + ctx context.Context, + req models.SuiGetTransactionBlockRequest, + ) (models.SuiTransactionBlockResponse, error) } // New Signer constructor. -func New(baseSigner *base.Signer) *Signer { - return &Signer{Signer: baseSigner} +func New( + baseSigner *base.Signer, + client RPC, + gateway *sui.Gateway, + zetacore interfaces.ZetacoreClient, +) *Signer { + return &Signer{ + Signer: baseSigner, + client: client, + gateway: gateway, + zetacore: zetacore, + withdrawCap: &withdrawCap{}, + } +} + +// ProcessCCTX schedules outbound cross-chain transaction. +// Build --> Sign --> Broadcast --(async)--> Wait for execution --> PostOutboundTracker +func (s *Signer) ProcessCCTX(ctx context.Context, cctx *cctypes.CrossChainTx, zetaHeight uint64) error { + var ( + outboundID = base.OutboundIDFromCCTX(cctx) + nonce = cctx.GetCurrentOutboundParam().TssNonce + ) + + s.MarkOutbound(outboundID, true) + defer func() { s.MarkOutbound(outboundID, false) }() + + tx, err := s.buildWithdrawal(ctx, cctx) + if err != nil { + return errors.Wrap(err, "unable to build withdrawal tx") + } + + sig, err := s.signTx(ctx, tx, zetaHeight, nonce) + if err != nil { + return errors.Wrap(err, "unable to sign tx") + } + + txDigest, err := s.broadcast(ctx, tx, sig) + if err != nil { + // todo we might need additional error handling + // for the case when the tx is already broadcasted by another zetaclient + // (e.g. suppress error) + return errors.Wrap(err, "unable to broadcast tx") + } + + logger := s.Logger().Std.With(). + Str(logs.FieldMethod, "reportToOutboundTracker"). + Int64(logs.FieldChain, s.Chain().ChainId). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, txDigest). + Logger() + + ctx = logger.WithContext(ctx) + + bg.Work(ctx, + func(ctx context.Context) error { return s.reportOutboundTracker(ctx, nonce, txDigest) }, + bg.WithLogger(logger), + bg.WithName("report_outbound_tracker"), + ) + + return nil +} + +func (s *Signer) signTx(ctx context.Context, tx models.TxnMetaData, zetaHeight, nonce uint64) ([65]byte, error) { + digest, err := sui.Digest(tx) + if err != nil { + return [65]byte{}, errors.Wrap(err, "unable to get digest") + } + + // Another hashing is required for ECDSA. + // https://docs.sui.io/concepts/cryptography/transaction-auth/signatures#signature-requirements + digestWrapped := sha256.Sum256(digest[:]) + + // Send TSS signature request. + return s.TSS().Sign( + ctx, + digestWrapped[:], + zetaHeight, + nonce, + s.Chain().ChainId, + ) } diff --git a/zetaclient/chains/sui/signer/signer_test.go b/zetaclient/chains/sui/signer/signer_test.go new file mode 100644 index 0000000000..e98a2d5e42 --- /dev/null +++ b/zetaclient/chains/sui/signer/signer_test.go @@ -0,0 +1,210 @@ +package signer + +import ( + "context" + "encoding/base64" + "fmt" + "testing" + "time" + + "cosmossdk.io/math" + "github.com/block-vision/sui-go-sdk/models" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/contracts/sui" + "github.com/zeta-chain/node/testutil/sample" + cc "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" +) + +func TestSigner(t *testing.T) { + t.Run("ProcessCCTX", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + const zetaHeight = 1000 + + // Given cctx + nonce := uint64(123) + amount := math.NewUint(100_000) + receiver := "0xdecb47015beebed053c19ef48fe4d722fa3870f567133d235ebe3a70da7b0000" + + cctx := sample.CrossChainTxV2(t, "0xABC123") + cctx.InboundParams.CoinType = coin.CoinType_Gas + cctx.OutboundParams = []*cc.OutboundParams{{ + Receiver: receiver, + ReceiverChainId: ts.Chain.ChainId, + CoinType: coin.CoinType_Gas, + Amount: amount, + TssNonce: nonce, + }} + + // Given mocked WithdrawCapID + const withdrawCapID = "0xWithdrawCapID" + ts.MockWithdrawCapID(withdrawCapID) + + // Given expected MoveCall + txBytes := base64.StdEncoding.EncodeToString([]byte("raw_tx_bytes")) + + ts.MockMoveCall(func(req models.MoveCallRequest) { + require.Equal(t, ts.TSS.PubKey().AddressSui(), req.Signer) + require.Equal(t, ts.Gateway.PackageID(), req.PackageObjectId) + require.Equal(t, "withdraw", req.Function) + + expectedArgs := []any{ + ts.Gateway.ObjectID(), + amount.String(), + fmt.Sprintf("%d", nonce), + receiver, + withdrawCapID, + } + require.Equal(t, expectedArgs, req.Arguments) + }, txBytes) + + // Given expected SuiExecuteTransactionBlock + const digest = "0xTransactionBlockDigest" + ts.MockExec(func(req models.SuiExecuteTransactionBlockRequest) { + require.Equal(t, txBytes, req.TxBytes) + require.NotEmpty(t, req.Signature) + }, digest) + + // Given included tx from Sui RPC + ts.SuiMock. + On("SuiGetTransactionBlock", mock.Anything, mock.Anything). + Return(models.SuiTransactionBlockResponse{ + Digest: digest, + Checkpoint: "1000000", + }, nil) + + // ACT + err := ts.Signer.ProcessCCTX(ts.Ctx, cctx, zetaHeight) + + // ASSERT + require.NoError(t, err) + + // Wait for vote posting + wait := func() bool { + if len(ts.TrackerBag) == 0 { + return false + } + + vote := ts.TrackerBag[0] + return vote.hash == digest && vote.nonce == nonce + } + + require.Eventually(t, wait, 5*time.Second, 100*time.Millisecond) + }) +} + +type testSuite struct { + t *testing.T + Ctx context.Context + + Chain chains.Chain + + TSS *mocks.TSS + Zetacore *mocks.ZetacoreClient + SuiMock *mocks.SuiClient + Gateway *sui.Gateway + + *Signer + + TrackerBag []testTracker +} + +func newTestSuite(t *testing.T) *testSuite { + var ( + ctx = context.Background() + + chain = chains.SuiMainnet + chainParams = mocks.MockChainParams(chain.ChainId, 10) + + tss = mocks.NewTSS(t) + zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) + + testLogger = testlog.New(t) + logger = base.Logger{Std: testLogger.Logger, Compliance: testLogger.Logger} + ) + + suiMock := mocks.NewSuiClient(t) + + gw, err := sui.NewGatewayFromPairID(chainParams.GatewayAddress) + require.NoError(t, err) + + baseSigner := base.NewSigner(chain, tss, logger) + signer := New(baseSigner, suiMock, gw, zetacore) + + ts := &testSuite{ + t: t, + Ctx: ctx, + Chain: chain, + TSS: tss, + Zetacore: zetacore, + SuiMock: suiMock, + Gateway: gw, + Signer: signer, + } + + // Setup mocks + ts.Zetacore.On("Chain").Return(chain).Maybe() + + ts.setupTrackersBag() + + return ts +} + +func (ts *testSuite) MockWithdrawCapID(id string) { + tss, structType := ts.TSS.PubKey().AddressSui(), ts.Gateway.WithdrawCapType() + ts.SuiMock.On("GetOwnedObjectID", mock.Anything, tss, structType).Return(id, nil) +} + +func (ts *testSuite) MockMoveCall(assert func(req models.MoveCallRequest), txBytesBase64 string) { + call := func(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + assert(req) + return models.TxnMetaData{TxBytes: txBytesBase64}, nil + } + + ts.SuiMock.On("MoveCall", mock.Anything, mock.Anything).Return(call) +} + +func (ts *testSuite) MockExec(assert func(req models.SuiExecuteTransactionBlockRequest), digest string) { + call := func( + ctx context.Context, + req models.SuiExecuteTransactionBlockRequest, + ) (models.SuiTransactionBlockResponse, error) { + assert(req) + return models.SuiTransactionBlockResponse{Digest: digest}, nil + } + + ts.SuiMock.On("SuiExecuteTransactionBlock", mock.Anything, mock.Anything).Return(call) +} + +type testTracker struct { + nonce uint64 + hash string +} + +func (ts *testSuite) setupTrackersBag() { + catcher := func(args mock.Arguments) { + require.Equal(ts.t, ts.Chain.ChainId, args.Get(1).(int64)) + nonce := args.Get(2).(uint64) + txHash := args.Get(3).(string) + + ts.t.Logf("Adding outbound tracker: nonce=%d, hash=%s", nonce, txHash) + + ts.TrackerBag = append(ts.TrackerBag, testTracker{nonce, txHash}) + } + + ts.Zetacore.On( + "PostOutboundTracker", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Maybe().Run(catcher).Return("", nil) +} diff --git a/zetaclient/chains/sui/signer/signer_tracker.go b/zetaclient/chains/sui/signer/signer_tracker.go new file mode 100644 index 0000000000..36af4f8154 --- /dev/null +++ b/zetaclient/chains/sui/signer/signer_tracker.go @@ -0,0 +1,69 @@ +package signer + +import ( + "context" + "time" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// reportOutboundTracker queries the tx and sends its digest to the outbound tracker +// for further processing by the Observer. +func (s *Signer) reportOutboundTracker(ctx context.Context, nonce uint64, digest string) error { + // approx Sui checkpoint interval + const interval = 3 * time.Second + + // some sanity timeout + const maxTimeout = time.Minute + + logger := zerolog.Ctx(ctx) + + alreadySet := s.SetBeingReportedFlag(digest) + if alreadySet { + logger.Info().Msg("Outbound is already being observed for the tracker") + return nil + } + + start := time.Now() + attempts := 0 + + req := models.SuiGetTransactionBlockRequest{Digest: digest} + + defer s.ClearBeingReportedFlag(digest) + + for { + switch { + case time.Since(start) > maxTimeout: + return errors.Errorf("timeout reached (%s)", maxTimeout.String()) + case attempts == 0: + // best case we'd be able to report the tx ~immediately + time.Sleep(interval / 2) + default: + time.Sleep(interval) + } + attempts++ + + res, err := s.client.SuiGetTransactionBlock(ctx, req) + switch { + case ctx.Err() != nil: + return errors.Wrap(ctx.Err(), "Failed to get transaction block") + case err != nil: + logger.Error().Err(err).Msg("Failed to get transaction block") + continue + case res.Checkpoint == "": + // should not happen + logger.Error().Msg("Checkpoint is empty") + continue + default: + return s.postTrackerVote(ctx, nonce, digest) + } + } +} + +// note that at this point we don't care whether tx was successful or not. +func (s *Signer) postTrackerVote(ctx context.Context, nonce uint64, digest string) error { + _, err := s.zetacore.PostOutboundTracker(ctx, s.Chain().ChainId, nonce, digest) + return err +} diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go new file mode 100644 index 0000000000..b6e3ea5cf9 --- /dev/null +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -0,0 +1,84 @@ +package signer + +import ( + "context" + "strconv" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/contracts/sui" + cctypes "github.com/zeta-chain/node/x/crosschain/types" +) + +const funcWithdraw = "withdraw" + +// buildWithdrawal builds unsigned withdrawal transaction using CCTX and Sui RPC +// https://github.com/zeta-chain/protocol-contracts-sui/blob/0245ad3a2eb4001381625070fd76c87c165589b2/sources/gateway.move#L117 +func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx) (tx models.TxnMetaData, err error) { + params := cctx.GetCurrentOutboundParam() + coinType := "" + + // Basic common-sense validation & coin-type determination + switch { + case params.ReceiverChainId != s.Chain().ChainId: + return tx, errors.Errorf("invalid receiver chain id %d", params.ReceiverChainId) + case cctx.ProtocolContractVersion != cctypes.ProtocolContractVersion_V2: + return tx, errors.Errorf("invalid protocol version %q", cctx.ProtocolContractVersion) + case cctx.InboundParams == nil: + return tx, errors.New("inbound params are nil") + case cctx.InboundParams.IsCrossChainCall: + return tx, errors.New("withdrawAndCall is not supported yet") + case params.CoinType == coin.CoinType_Gas: + coinType = string(sui.SUI) + case params.CoinType == coin.CoinType_ERC20: + coinType = cctx.InboundParams.Asset + default: + return tx, errors.Errorf("unsupported coin type %q", params.CoinType.String()) + } + + var ( + nonce = strconv.FormatUint(params.TssNonce, 10) + recipient = params.Receiver + amount = params.Amount.String() + gasBudget = strconv.FormatUint(params.CallOptions.GasLimit, 10) + ) + + withdrawCapID, err := s.getWithdrawCapIDCached(ctx) + if err != nil { + return tx, errors.Wrap(err, "unable to get withdraw cap ID") + } + + req := models.MoveCallRequest{ + Signer: s.TSS().PubKey().AddressSui(), + PackageObjectId: s.gateway.PackageID(), + Module: s.gateway.Module(), + Function: funcWithdraw, + TypeArguments: []any{coinType}, + Arguments: []any{s.gateway.ObjectID(), amount, nonce, recipient, withdrawCapID}, + GasBudget: gasBudget, + } + + return s.client.MoveCall(ctx, req) +} + +// broadcast attaches signature to tx and broadcasts it to Sui network. Returns tx digest. +func (s *Signer) broadcast(ctx context.Context, tx models.TxnMetaData, sig [65]byte) (string, error) { + sigBase64, err := sui.SerializeSignatureECDSA(sig, s.TSS().PubKey().AsECDSA()) + if err != nil { + return "", errors.Wrap(err, "unable to serialize signature") + } + + req := models.SuiExecuteTransactionBlockRequest{ + TxBytes: tx.TxBytes, + Signature: []string{sigBase64}, + } + + res, err := s.client.SuiExecuteTransactionBlock(ctx, req) + if err != nil { + return "", errors.Wrap(err, "unable to execute tx block") + } + + return res.Digest, nil +} diff --git a/zetaclient/chains/sui/signer/signer_withdrawcap.go b/zetaclient/chains/sui/signer/signer_withdrawcap.go new file mode 100644 index 0000000000..2c38b495db --- /dev/null +++ b/zetaclient/chains/sui/signer/signer_withdrawcap.go @@ -0,0 +1,72 @@ +package signer + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" +) + +const withdrawCapTTL = 5 * time.Minute + +// withdrawCap represents WithdrawCap (capability) object +// that is required as a "permission" to withdraw funds. +// Should belong to TSS address on Sui. +type withdrawCap struct { + objectID string + mu sync.RWMutex + fetchedAt time.Time +} + +func (wc *withdrawCap) valid() bool { + wc.mu.RLock() + defer wc.mu.RUnlock() + + if wc.objectID == "" { + return false + } + + return time.Since(wc.fetchedAt) < withdrawCapTTL +} + +func (wc *withdrawCap) set(objectID string) { + wc.mu.Lock() + defer wc.mu.Unlock() + + wc.objectID = objectID + wc.fetchedAt = time.Now() +} + +// getWithdrawCapIDCached getWithdrawCapID with withdrawCapTTL cache. +func (s *Signer) getWithdrawCapIDCached(ctx context.Context) (string, error) { + if s.withdrawCap.valid() { + return s.withdrawCap.objectID, nil + } + + s.Logger().Std.Info().Msg("WithdrawCap cache expired, fetching new objectID") + + objectID, err := s.getWithdrawCapID(ctx) + if err != nil { + return "", errors.Wrap(err, "unable to get withdraw cap ID") + } + + s.withdrawCap.set(objectID) + + s.Logger().Std.Info().Str("sui.object_id", objectID).Msg("WithdrawCap objectID fetched") + + return objectID, nil +} + +// getWithdrawCapID returns the objectID of the WithdrawCap. Should belong to TSS address on Sui. +func (s *Signer) getWithdrawCapID(ctx context.Context) (string, error) { + owner := s.TSS().PubKey().AddressSui() + structType := s.gateway.WithdrawCapType() + + objectID, err := s.client.GetOwnedObjectID(ctx, owner, structType) + if err != nil { + return "", errors.Wrap(err, "unable to get owned object ID") + } + + return objectID, nil +} diff --git a/zetaclient/chains/sui/sui.go b/zetaclient/chains/sui/sui.go index 61a3a43dcc..60a44912c5 100644 --- a/zetaclient/chains/sui/sui.go +++ b/zetaclient/chains/sui/sui.go @@ -6,10 +6,13 @@ import ( "time" "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/scheduler" "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/sui/observer" "github.com/zeta-chain/node/zetaclient/chains/sui/signer" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -22,6 +25,14 @@ type Sui struct { signer *signer.Signer } +const ( + // outboundLookbackFactor is the factor to determine how many nonces to look back for pending cctxs + // For example, give OutboundScheduleLookahead of 120, pending NonceLow of 1000 and factor of 1.0, + // the scheduler need to be able to pick up and schedule any pending cctx with nonce < 880 (1000 - 120 * 1.0) + // NOTE: 1.0 means look back the same number of cctxs as we look ahead + outboundLookbackFactor = 1.0 +) + // New Sui observer-signer constructor. func New(scheduler *scheduler.Scheduler, observer *observer.Observer, signer *signer.Signer) *Sui { return &Sui{scheduler, observer, signer} @@ -71,6 +82,10 @@ func (s *Sui) Start(ctx context.Context) error { return ticker.DurationFromUint64Seconds(s.observer.ChainParams().InboundTicker) }) + optOutboundInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(s.observer.ChainParams().OutboundTicker) + }) + optGasInterval := scheduler.IntervalUpdater(func() time.Duration { return ticker.DurationFromUint64Seconds(s.observer.ChainParams().GasPriceTicker) }) @@ -87,6 +102,7 @@ func (s *Sui) Start(ctx context.Context) error { register(s.observer.ProcessInboundTrackers, "process_inbound_trackers", optInboundInterval, optInboundSkipper) register(s.observer.CheckRPCStatus, "check_rpc_status") register(s.observer.PostGasPrice, "post_gas_price", optGasInterval, optGenericSkipper) + register(s.observer.ProcessOutboundTrackers, "process_outbound_trackers", optOutboundInterval, optOutboundSkipper) // CCTX scheduler (every zetachain block) register(s.scheduleCCTX, "schedule_cctx", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) @@ -105,7 +121,113 @@ func (s *Sui) group() scheduler.Group { } // scheduleCCTX schedules outbound cross-chain transactions. -func (s *Sui) scheduleCCTX(_ context.Context) error { - // todo +func (s *Sui) scheduleCCTX(ctx context.Context) error { + if err := s.updateChainParams(ctx); err != nil { + return errors.Wrap(err, "unable to update chain params") + } + + zetaBlock, delay, err := scheduler.BlockFromContextWithDelay(ctx) + if err != nil { + return errors.Wrap(err, "unable to get zeta block from context") + } + + time.Sleep(delay) + + cctxList, _, err := s.observer.ZetacoreClient().ListPendingCCTX(ctx, s.observer.Chain()) + if err != nil { + return errors.Wrap(err, "unable to list pending cctx") + } + + // noop + if len(cctxList) == 0 { + return nil + } + + var ( + // #nosec G115 always in range + zetaHeight = uint64(zetaBlock.Block.Height) + chainID = s.observer.Chain().ChainId + + lookahead = s.observer.ChainParams().OutboundScheduleLookahead + // #nosec G115 always in range + lookback = uint64(float64(lookahead) * outboundLookbackFactor) + + firstNonce = cctxList[0].GetCurrentOutboundParam().TssNonce + maxNonce = firstNonce + lookback + ) + + for i := range cctxList { + var ( + cctx = cctxList[i] + outboundID = base.OutboundIDFromCCTX(cctx) + outboundParams = cctx.GetCurrentOutboundParam() + nonce = outboundParams.TssNonce + ) + + switch { + case int64(i) == lookahead: + // take only first N cctxs + return nil + case outboundParams.ReceiverChainId != chainID: + // should not happen + s.outboundLogger(outboundID).Error().Msg("chain id mismatch") + continue + case nonce >= maxNonce: + return fmt.Errorf("nonce %d is too high (%s). Earliest nonce %d", nonce, outboundID, firstNonce) + case s.signer.IsOutboundActive(outboundID): + // cctx is already being processed & broadcasted by signer + continue + case s.observer.OutboundCreated(nonce): + // ProcessOutboundTrackers HAS fetched existing Sui outbound, + // Let's report this by voting to zetacore + if err := s.observer.VoteOutbound(ctx, cctx); err != nil { + s.outboundLogger(outboundID).Error().Err(err).Msg("VoteOutbound failed") + } + continue + } + + // Here we have a cctx that needs to be scheduled. Let's invoke async operation. + // - Signer will build, sign & broadcast the tx. + // - It will also monitor Sui to report outbound tracker + // so we'd have a pair of (tss_nonce -> sui tx hash) + // - Then this pair will be handled by ProcessOutboundTrackers -> OutboundCreated -> VoteOutbound + bg.Work(ctx, func(ctx context.Context) error { + if err := s.signer.ProcessCCTX(ctx, cctx, zetaHeight); err != nil { + s.outboundLogger(outboundID).Error().Err(err).Msg("ProcessCCTX failed") + } + + return nil + }) + } + return nil } + +func (s *Sui) updateChainParams(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + chain, err := app.GetChain(s.observer.Chain().ChainId) + if err != nil { + return err + } + + params := chain.Params() + + s.observer.SetChainParams(*params) + + // note that address should be in format of `$packageID,$gatewayObjectID` + if err := s.observer.Gateway().UpdateIDs(params.GatewayAddress); err != nil { + return errors.Wrap(err, "unable to update gateway ids") + } + + return nil +} + +func (s *Sui) outboundLogger(id string) *zerolog.Logger { + l := s.observer.Logger().Outbound.With().Str("outbound.id", id).Logger() + + return &l +} diff --git a/zetaclient/logs/fields.go b/zetaclient/logs/fields.go index 6f0b60b423..9c267030db 100644 --- a/zetaclient/logs/fields.go +++ b/zetaclient/logs/fields.go @@ -8,6 +8,7 @@ const ( FieldChain = "chain" FieldChainNetwork = "chain_network" FieldNonce = "nonce" + FieldTracker = "tracker_id" FieldTx = "tx" FieldOutboundID = "outbound_id" FieldBlock = "block" diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 76fd5927d7..b1f573ad34 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -200,18 +200,22 @@ func (oc *V2) bootstrapSui(ctx context.Context, chain zctx.Chain) (*sui.Sui, err return nil, errors.Wrap(errSkipChain, "unable to find sui config") } - baseObserver, err := oc.newBaseObserver(chain, chain.Name()) + // note that gw address should be in format of `$packageID,$gatewayObjectID` + gateway, err := suigateway.NewGatewayFromPairID(chain.Params().GatewayAddress) if err != nil { - return nil, errors.Wrap(err, "unable to create base observer") + return nil, errors.Wrap(err, "unable to create gateway") } suiClient := suiclient.NewFromEndpoint(cfg.Endpoint) - gateway := suigateway.NewGateway(chain.Params().GatewayAddress) + baseObserver, err := oc.newBaseObserver(chain, chain.Name()) + if err != nil { + return nil, errors.Wrap(err, "unable to create base observer") + } observer := suiobserver.New(baseObserver, suiClient, gateway) - signer := suisigner.New(oc.newBaseSigner(chain)) + signer := suisigner.New(oc.newBaseSigner(chain), suiClient, gateway, oc.deps.Zetacore) return sui.New(oc.scheduler, observer, signer), nil } diff --git a/zetaclient/orchestrator/v2_bootstrap_test.go b/zetaclient/orchestrator/v2_bootstrap_test.go index dc40592459..2c2741006e 100644 --- a/zetaclient/orchestrator/v2_bootstrap_test.go +++ b/zetaclient/orchestrator/v2_bootstrap_test.go @@ -75,6 +75,12 @@ func TestBootstrap(t *testing.T) { cfg.EVMChainConfigs[chains.Polygon.ChainId] = config.EVMConfig{ Endpoint: maticServer.Endpoint, } + + // disable other chains + cfg.BTCChainConfigs = nil + cfg.SolanaConfig.Endpoint = "" + cfg.SuiConfig.Endpoint = "" + cfg.TONConfig.LiteClientConfigURL = "" }) // Mock zetacore calls diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go index 76b2422d12..8565a9ca4a 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -123,7 +123,7 @@ func newTestSuite(t *testing.T) *testSuite { // Services var ( - schedulerService = scheduler.New(logger.Logger) + schedulerService = scheduler.New(logger.Logger, time.Second) zetacore = mocks.NewZetacoreClient(t) tss = mocks.NewTSS(t) ) diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index d010a9dde6..46b578ccf6 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -44,7 +44,7 @@ var GatewayAddresses = map[int64]string{ chains.SolanaMainnet.ChainId: "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis", // stub, will be replaced with real address later - chains.SuiMainnet.ChainId: "0x5d4b302506645c37ff133b98fff50a5ae14841659738d6d733d59d0d217a9fff", + chains.SuiMainnet.ChainId: "0x5d4b302506645c37ff133b98fff50a5ae14841659738d6d733d59d0d217a9fff,0xffff302506645c37ff133b98fff50a5ae14841659738d6d733d59d0d217a9aaa", } // ConnectorAddresses contains constants ERC20 connector addresses for testing diff --git a/zetaclient/testutils/mocks/sui_client.go b/zetaclient/testutils/mocks/sui_client.go index 94341298fb..a1b0e3b35f 100644 --- a/zetaclient/testutils/mocks/sui_client.go +++ b/zetaclient/testutils/mocks/sui_client.go @@ -47,6 +47,34 @@ func (_m *SuiClient) GetLatestCheckpoint(ctx context.Context) (models.Checkpoint return r0, r1 } +// GetOwnedObjectID provides a mock function with given fields: ctx, ownerAddress, structType +func (_m *SuiClient) GetOwnedObjectID(ctx context.Context, ownerAddress string, structType string) (string, error) { + ret := _m.Called(ctx, ownerAddress, structType) + + if len(ret) == 0 { + panic("no return value specified for GetOwnedObjectID") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok { + return rf(ctx, ownerAddress, structType) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { + r0 = rf(ctx, ownerAddress, structType) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, ownerAddress, structType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HealthCheck provides a mock function with given fields: ctx func (_m *SuiClient) HealthCheck(ctx context.Context) (time.Time, error) { ret := _m.Called(ctx) @@ -75,6 +103,34 @@ func (_m *SuiClient) HealthCheck(ctx context.Context) (time.Time, error) { return r0, r1 } +// MoveCall provides a mock function with given fields: ctx, req +func (_m *SuiClient) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for MoveCall") + } + + var r0 models.TxnMetaData + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, models.MoveCallRequest) (models.TxnMetaData, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, models.MoveCallRequest) models.TxnMetaData); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Get(0).(models.TxnMetaData) + } + + if rf, ok := ret.Get(1).(func(context.Context, models.MoveCallRequest) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // QueryModuleEvents provides a mock function with given fields: ctx, q func (_m *SuiClient) QueryModuleEvents(ctx context.Context, q client.EventQuery) ([]models.SuiEventResponse, string, error) { ret := _m.Called(ctx, q) @@ -112,6 +168,34 @@ func (_m *SuiClient) QueryModuleEvents(ctx context.Context, q client.EventQuery) return r0, r1, r2 } +// SuiExecuteTransactionBlock provides a mock function with given fields: ctx, req +func (_m *SuiClient) SuiExecuteTransactionBlock(ctx context.Context, req models.SuiExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for SuiExecuteTransactionBlock") + } + + var r0 models.SuiTransactionBlockResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, models.SuiExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, models.SuiExecuteTransactionBlockRequest) models.SuiTransactionBlockResponse); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Get(0).(models.SuiTransactionBlockResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, models.SuiExecuteTransactionBlockRequest) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SuiGetObject provides a mock function with given fields: ctx, req func (_m *SuiClient) SuiGetObject(ctx context.Context, req models.SuiGetObjectRequest) (models.SuiObjectResponse, error) { ret := _m.Called(ctx, req) diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index 1210ce7ad8..a2cfd4fb54 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -18,6 +18,7 @@ type suiClient interface { HealthCheck(ctx context.Context) (time.Time, error) GetLatestCheckpoint(ctx context.Context) (models.CheckpointResponse, error) QueryModuleEvents(ctx context.Context, q client.EventQuery) ([]models.SuiEventResponse, string, error) + GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) SuiXQueryEvents(ctx context.Context, req models.SuiXQueryEventsRequest) (models.PaginatedEventsResponse, error) @@ -26,4 +27,9 @@ type suiClient interface { ctx context.Context, req models.SuiGetTransactionBlockRequest, ) (models.SuiTransactionBlockResponse, error) + MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) + SuiExecuteTransactionBlock( + ctx context.Context, + req models.SuiExecuteTransactionBlockRequest, + ) (models.SuiTransactionBlockResponse, error) } diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 731b4344e3..1a8e343ee2 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -20,6 +20,7 @@ import ( "gitlab.com/thorchain/tss/go-tss/keysign" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/contracts/sui" "github.com/zeta-chain/node/pkg/cosmos" ) @@ -87,6 +88,10 @@ func NewPubKeyFromECDSAHexString(raw string) (PubKey, error) { return NewPubKeyFromECDSA(*pk) } +func (k PubKey) AsECDSA() *ecdsa.PublicKey { + return k.ecdsaPubKey +} + // Bytes marshals pubKey to bytes either as compressed or uncompressed slice. // // In ECDSA, a compressed pubKey includes only the X and a parity bit for the Y, @@ -95,7 +100,7 @@ func NewPubKeyFromECDSAHexString(raw string) (PubKey, error) { func (k PubKey) Bytes(compress bool) []byte { pk := k.ecdsaPubKey if compress { - return elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) + return crypto.CompressPubkey(pk) } return crypto.FromECDSAPub(pk) @@ -131,6 +136,11 @@ func (k PubKey) AddressEVM() eth.Address { return crypto.PubkeyToAddress(*k.ecdsaPubKey) } +// AddressSui returns Sui address of the public key. +func (k PubKey) AddressSui() string { + return sui.AddressFromPubKeyECDSA(k.ecdsaPubKey) +} + // VerifySignature checks that keysign.Signature is valid and origins from expected TSS public key. // Also returns signature as [65]byte (R, S, V) func VerifySignature(sig keysign.Signature, pk PubKey, hash []byte) ([65]byte, error) { From e8ae60f778f9f9e496178a33259942ae4a20f1ef Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 26 Feb 2025 06:20:34 -0800 Subject: [PATCH 17/22] chore: remove gov v1beta1 endpoints from docs (#3589) * chore: remove gov v1beta1 endpoints from docs * also restore missing upgrade module * set deprecated option * Add deprecated options and fully qualified naming --- docs/openapi/openapi.html | 3 + docs/openapi/openapi.swagger.yaml | 11185 +++++++--------- proto/buf.openapi.yaml | 2 +- .../zetachain/zetacore/crosschain/query.proto | 8 + .../zetacore/lightclient/query.proto | 7 + scripts/protoc-gen-openapi.sh | 15 +- x/crosschain/types/query.pb.go | 310 +- x/lightclient/types/query.pb.go | 127 +- 8 files changed, 5190 insertions(+), 6467 deletions(-) diff --git a/docs/openapi/openapi.html b/docs/openapi/openapi.html index 20f878256f..550722c00d 100644 --- a/docs/openapi/openapi.html +++ b/docs/openapi/openapi.html @@ -24,6 +24,9 @@ dom_id: "#swagger-ui", deepLinking: true, layout: "BaseLayout", + showExtensions: true, + showCommonExtensions: true, + defaultModelExpandDepth: 5 }); } diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index dc7a84a1e4..dd56e3b4e7 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -9155,74 +9155,20 @@ paths: type: boolean tags: - Query - /cosmos/gov/v1beta1/params/{params_type}: + /cosmos/gov/v1/constitution: get: - summary: Params queries all parameters of the gov module. - operationId: GovParams + summary: Constitution queries the chain's constitution. + operationId: Constitution responses: '200': description: A successful response. schema: type: object properties: - voting_params: - description: voting_params defines the parameters related to voting. - type: object - properties: - voting_period: - type: string - description: Duration of the voting period. - deposit_params: - description: deposit_params defines the parameters related to deposit. - type: object - properties: - min_deposit: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: Minimum deposit for a proposal to enter voting period. - max_deposit_period: - type: string - description: >- - Maximum period for Atom holders to deposit on a proposal. Initial value: 2 - - months. - tally_params: - description: tally_params defines the parameters related to tally. - type: object - properties: - quorum: - type: string - format: byte - description: >- - Minimum percentage of total stake needed to vote for a result to be - - considered valid. - threshold: - type: string - format: byte - description: >- - Minimum proportion of Yes votes for proposal to pass. Default value: 0.5. - veto_threshold: - type: string - format: byte - description: >- - Minimum value of Veto votes to Total votes ratio for proposal to be - - vetoed. Default value: 1/3. - description: >- - QueryParamsResponse is the response type for the Query/Params RPC method. + constitution: + type: string + title: >- + QueryConstitutionResponse is the response type for the Query/Constitution RPC method default: description: An unexpected error response. schema: @@ -9386,277 +9332,193 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - parameters: - - name: params_type - description: >- - params_type defines which parameters to query for, can be one of "voting", - - "tallying" or "deposit". - in: path - required: true - type: string tags: - Query - /cosmos/gov/v1beta1/proposals: + /cosmos/gov/v1/params/{params_type}: get: - summary: Proposals queries all proposals based on given status. - operationId: Proposals + summary: Params queries all parameters of the gov module. + operationId: GovV1Params responses: '200': description: A successful response. schema: type: object properties: - proposals: - type: array - items: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - content: + voting_params: + description: |- + Deprecated: Prefer to use `params` instead. + voting_params defines the parameters related to voting. + type: object + properties: + voting_period: + type: string + description: Duration of the voting period. + deposit_params: + description: |- + Deprecated: Prefer to use `params` instead. + deposit_params defines the parameters related to deposit. + type: object + properties: + min_deposit: + type: array + items: type: object properties: - type_url: + denom: type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: + amount: type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack + Coin defines a token with a denomination and an amount. - methods only use the fully qualified type name after the last '/' + NOTE: The amount field is an Int which implements the custom method - in the type URL, for example "foo.bar.com/x/y.z" will yield type + signatures required by gogoproto. + description: Minimum deposit for a proposal to enter voting period. + max_deposit_period: + type: string + description: >- + Maximum period for Atom holders to deposit on a proposal. Initial value: 2 - name "y.z". + months. + tally_params: + description: |- + Deprecated: Prefer to use `params` instead. + tally_params defines the parameters related to tally. + type: object + properties: + quorum: + type: string + description: >- + Minimum percentage of total stake needed to vote for a result to be - JSON + considered valid. + threshold: + type: string + description: >- + Minimum proportion of Yes votes for proposal to pass. Default value: 0.5. + veto_threshold: + type: string + description: >- + Minimum value of Veto votes to Total votes ratio for proposal to be - The JSON representation of an `Any` value uses the regular + vetoed. Default value: 1/3. + params: + description: |- + params defines all the paramaters of x/gov module. - representation of the deserialized, embedded message, with an + Since: cosmos-sdk 0.47 + type: object + properties: + min_deposit: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. - additional field `@type` which contains the type URL. Example: + NOTE: The amount field is an Int which implements the custom method - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + signatures required by gogoproto. + description: Minimum deposit for a proposal to enter voting period. + max_deposit_period: + type: string + description: >- + Maximum period for Atom holders to deposit on a proposal. Initial value: 2 - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + months. + voting_period: + type: string + description: Duration of the voting period. + quorum: + type: string + description: >- + Minimum percentage of total stake needed to vote for a result to be - If the embedded message type is well-known and has a custom JSON + considered valid. + threshold: + type: string + description: >- + Minimum proportion of Yes votes for proposal to pass. Default value: 0.5. + veto_threshold: + type: string + description: >- + Minimum value of Veto votes to Total votes ratio for proposal to be - representation, that representation will be embedded adding a field + vetoed. Default value: 1/3. + min_initial_deposit_ratio: + type: string + description: >- + The ratio representing the proportion of the deposit value that must be paid at proposal submission. + proposal_cancel_ratio: + type: string + description: >- + The cancel ratio which will not be returned back to the depositors when a proposal is cancelled. - `value` which holds the custom JSON in addition to the `@type` + Since: cosmos-sdk 0.50 + proposal_cancel_dest: + type: string + description: >- + The address which will receive (proposal_cancel_ratio * deposit) proposal deposits. - field. Example (for message [google.protobuf.Duration][]): + If empty, the (proposal_cancel_ratio * deposit) proposal deposits will be burned. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - status: - description: status defines the proposal status. - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - final_tally_result: - description: >- - final_tally_result is the final tally result of the proposal. When + Since: cosmos-sdk 0.50 + expedited_voting_period: + type: string + description: |- + Duration of the voting period of an expedited proposal. - querying a proposal via gRPC, this field is not populated until the + Since: cosmos-sdk 0.50 + expedited_threshold: + type: string + description: >- + Minimum proportion of Yes votes for proposal to pass. Default value: 0.67. - proposal's voting period has ended. + Since: cosmos-sdk 0.50 + expedited_min_deposit: + type: array + items: type: object properties: - 'yes': - type: string - description: yes is the number of yes votes on a proposal. - abstain: - type: string - description: >- - abstain is the number of abstain votes on a proposal. - 'no': + denom: type: string - description: no is the number of no votes on a proposal. - no_with_veto: + amount: type: string - description: >- - no_with_veto is the number of no with veto votes on a proposal. - submit_time: - type: string - format: date-time - description: submit_time is the time of proposal submission. - deposit_end_time: - type: string - format: date-time - description: deposit_end_time is the end time for deposition. - total_deposit: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. + description: >- + Coin defines a token with a denomination and an amount. - NOTE: The amount field is an Int which implements the custom method + NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - description: total_deposit is the total deposit on the proposal. - voting_start_time: - type: string - format: date-time - description: >- - voting_start_time is the starting time to vote on a proposal. - voting_end_time: - type: string - format: date-time - description: voting_end_time is the end time of voting on a proposal. - description: >- - Proposal defines the core field members of a governance proposal. - description: proposals defines all the requested governance proposals. - pagination: - description: pagination defines the pagination in the response. - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + signatures required by gogoproto. + description: >- + Minimum expedited deposit for a proposal to enter voting period. + burn_vote_quorum: + type: boolean + title: burn deposits if a proposal does not meet quorum + burn_proposal_deposit_prevote: + type: boolean + title: burn deposits if the proposal does not enter voting period + burn_vote_veto: + type: boolean + title: burn deposits if quorum with vote type no_veto is met + min_deposit_ratio: type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total + description: >- + The ratio representing the proportion of the deposit value minimum that must be met when making a deposit. - was set, its value is undefined otherwise - description: >- - QueryProposalsResponse is the response type for the Query/Proposals RPC + Default value: 0.01. Meaning that for a chain with a min_deposit of 100stake, a deposit of 1stake would be - method. + required. + + Since: cosmos-sdk 0.50 + description: >- + QueryParamsResponse is the response type for the Query/Params RPC method. default: description: An unexpected error response. schema: @@ -9821,329 +9683,308 @@ paths: "value": "1.212s" } parameters: - - name: proposal_status - description: |- - proposal_status defines the status of the proposals. - - - PROPOSAL_STATUS_UNSPECIFIED: PROPOSAL_STATUS_UNSPECIFIED defines the default proposal status. - - PROPOSAL_STATUS_DEPOSIT_PERIOD: PROPOSAL_STATUS_DEPOSIT_PERIOD defines a proposal status during the deposit - period. - - PROPOSAL_STATUS_VOTING_PERIOD: PROPOSAL_STATUS_VOTING_PERIOD defines a proposal status during the voting - period. - - PROPOSAL_STATUS_PASSED: PROPOSAL_STATUS_PASSED defines a proposal status of a proposal that has - passed. - - PROPOSAL_STATUS_REJECTED: PROPOSAL_STATUS_REJECTED defines a proposal status of a proposal that has - been rejected. - - PROPOSAL_STATUS_FAILED: PROPOSAL_STATUS_FAILED defines a proposal status of a proposal that has - failed. - in: query - required: false - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - - name: voter - description: voter defines the voter address for the proposals. - in: query - required: false - type: string - - name: depositor - description: depositor defines the deposit addresses from the proposals. - in: query - required: false - type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit + - name: params_type description: >- - limit is the total number of results to be returned in the result page. + params_type defines which parameters to query for, can be one of "voting", - If left empty it will default to a value to be set by each app. - in: query - required: false + "tallying" or "deposit". + in: path + required: true type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}: + /cosmos/gov/v1/proposals: get: - summary: Proposal queries proposal details based on ProposalID. - operationId: Proposal + summary: Proposals queries all proposals based on given status. + operationId: GovV1Proposal responses: '200': description: A successful response. schema: type: object properties: - proposal: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - content: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized + proposals: + type: array + items: + type: object + properties: + id: + type: string + format: uint64 + description: id defines the unique id of the proposal. + messages: + type: array + items: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized - protocol buffer message. This string must contain at least + protocol buffer message. This string must contain at least - one "/" character. The last segment of the URL's path must represent + one "/" character. The last segment of the URL's path must represent - the fully qualified name of the type (as in + the fully qualified name of the type (as in - `path/google.protobuf.Duration`). The name should be in a canonical form + `path/google.protobuf.Duration`). The name should be in a canonical form - (e.g., leading "." is not accepted). + (e.g., leading "." is not accepted). - In practice, teams usually precompile into the binary all types that they + In practice, teams usually precompile into the binary all types that they - expect it to use in the context of Any. However, for URLs which use the + expect it to use in the context of Any. However, for URLs which use the - scheme `http`, `https`, or no scheme, one can optionally set up a type + scheme `http`, `https`, or no scheme, one can optionally set up a type - server that maps type URLs to message definitions as follows: + server that maps type URLs to message definitions as follows: - * If no scheme is provided, `https` is assumed. + * If no scheme is provided, `https` is assumed. - * An HTTP GET on the URL must yield a [google.protobuf.Type][] + * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) - Note: this functionality is not currently available in the official + Note: this functionality is not currently available in the official - protobuf release, and it is not used for type URLs beginning with + protobuf release, and it is not used for type URLs beginning with - type.googleapis.com. + type.googleapis.com. - Schemes other than `http`, `https` (or the empty scheme) might be + Schemes other than `http`, `https` (or the empty scheme) might be - used with implementation specific semantics. - value: - type: string - format: byte + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a + `Any` contains an arbitrary serialized protocol buffer message along with a - URL that describes the type of the serialized message. + URL that describes the type of the serialized message. - Protobuf library provides support to pack/unpack Any values in the form + Protobuf library provides support to pack/unpack Any values in the form - of utility functions or additional generated methods of the Any type. + of utility functions or additional generated methods of the Any type. - Example 1: Pack and unpack a message in C++. + Example 1: Pack and unpack a message in C++. - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - Example 2: Pack and unpack a message in Java. + Example 2: Pack and unpack a message in Java. - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } - Example 3: Pack and unpack a message in Python. + Example 3: Pack and unpack a message in Python. - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - Example 4: Pack and unpack a message in Go + Example 4: Pack and unpack a message in Go - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - The pack methods provided by protobuf library will by default use + The pack methods provided by protobuf library will by default use - 'type.googleapis.com/full.type.name' as the type URL and the unpack + 'type.googleapis.com/full.type.name' as the type URL and the unpack - methods only use the fully qualified type name after the last '/' + methods only use the fully qualified type name after the last '/' - in the type URL, for example "foo.bar.com/x/y.z" will yield type + in the type URL, for example "foo.bar.com/x/y.z" will yield type - name "y.z". + name "y.z". - JSON + JSON - The JSON representation of an `Any` value uses the regular + The JSON representation of an `Any` value uses the regular - representation of the deserialized, embedded message, with an + representation of the deserialized, embedded message, with an - additional field `@type` which contains the type URL. Example: + additional field `@type` which contains the type URL. Example: - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - If the embedded message type is well-known and has a custom JSON + If the embedded message type is well-known and has a custom JSON - representation, that representation will be embedded adding a field + representation, that representation will be embedded adding a field - `value` which holds the custom JSON in addition to the `@type` + `value` which holds the custom JSON in addition to the `@type` - field. Example (for message [google.protobuf.Duration][]): + field. Example (for message [google.protobuf.Duration][]): - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - status: - description: status defines the proposal status. - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - final_tally_result: - description: >- - final_tally_result is the final tally result of the proposal. When + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + messages are the arbitrary messages to be executed if the proposal passes. + status: + description: status defines the proposal status. + type: string + enum: + - PROPOSAL_STATUS_UNSPECIFIED + - PROPOSAL_STATUS_DEPOSIT_PERIOD + - PROPOSAL_STATUS_VOTING_PERIOD + - PROPOSAL_STATUS_PASSED + - PROPOSAL_STATUS_REJECTED + - PROPOSAL_STATUS_FAILED + default: PROPOSAL_STATUS_UNSPECIFIED + final_tally_result: + description: >- + final_tally_result is the final tally result of the proposal. When - querying a proposal via gRPC, this field is not populated until the + querying a proposal via gRPC, this field is not populated until the - proposal's voting period has ended. - type: object - properties: - 'yes': - type: string - description: yes is the number of yes votes on a proposal. - abstain: - type: string - description: abstain is the number of abstain votes on a proposal. - 'no': - type: string - description: no is the number of no votes on a proposal. - no_with_veto: - type: string - description: >- - no_with_veto is the number of no with veto votes on a proposal. - submit_time: - type: string - format: date-time - description: submit_time is the time of proposal submission. - deposit_end_time: - type: string - format: date-time - description: deposit_end_time is the end time for deposition. - total_deposit: - type: array - items: + proposal's voting period has ended. type: object properties: - denom: + yes_count: type: string - amount: + description: yes_count is the number of yes votes on a proposal. + abstain_count: + type: string + description: >- + abstain_count is the number of abstain votes on a proposal. + no_count: + type: string + description: no_count is the number of no votes on a proposal. + no_with_veto_count: type: string + description: >- + no_with_veto_count is the number of no with veto votes on a proposal. + submit_time: + type: string + format: date-time + description: submit_time is the time of proposal submission. + deposit_end_time: + type: string + format: date-time + description: deposit_end_time is the end time for deposition. + total_deposit: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + + signatures required by gogoproto. + description: total_deposit is the total deposit on the proposal. + voting_start_time: + type: string + format: date-time description: >- - Coin defines a token with a denomination and an amount. + voting_start_time is the starting time to vote on a proposal. + voting_end_time: + type: string + format: date-time + description: voting_end_time is the end time of voting on a proposal. + metadata: + type: string + title: >- + metadata is any arbitrary metadata attached to the proposal. - NOTE: The amount field is an Int which implements the custom method + the recommended format of the metadata is to be found here: - signatures required by gogoproto. - description: total_deposit is the total deposit on the proposal. - voting_start_time: + https://docs.cosmos.network/v0.47/modules/gov#proposal-3 + title: + type: string + description: 'Since: cosmos-sdk 0.47' + title: title is the title of the proposal + summary: + type: string + description: 'Since: cosmos-sdk 0.47' + title: summary is a short summary of the proposal + proposer: + type: string + description: 'Since: cosmos-sdk 0.47' + title: proposer is the address of the proposal sumbitter + expedited: + type: boolean + description: 'Since: cosmos-sdk 0.50' + title: expedited defines if the proposal is expedited + failed_reason: + type: string + description: 'Since: cosmos-sdk 0.50' + title: failed_reason defines the reason why the proposal failed + description: >- + Proposal defines the core field members of a governance proposal. + description: proposals defines all the requested governance proposals. + pagination: + description: pagination defines the pagination in the response. + type: object + properties: + next_key: type: string - format: date-time - description: >- - voting_start_time is the starting time to vote on a proposal. - voting_end_time: + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string - format: date-time - description: voting_end_time is the end time of voting on a proposal. - description: >- - Proposal defines the core field members of a governance proposal. + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total + + was set, its value is undefined otherwise description: >- - QueryProposalResponse is the response type for the Query/Proposal RPC method. + QueryProposalsResponse is the response type for the Query/Proposals RPC + + method. default: description: An unexpected error response. schema: @@ -10308,51 +10149,571 @@ paths: "value": "1.212s" } parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. - in: path - required: true + - name: proposal_status + description: |- + proposal_status defines the status of the proposals. + + - PROPOSAL_STATUS_UNSPECIFIED: PROPOSAL_STATUS_UNSPECIFIED defines the default proposal status. + - PROPOSAL_STATUS_DEPOSIT_PERIOD: PROPOSAL_STATUS_DEPOSIT_PERIOD defines a proposal status during the deposit + period. + - PROPOSAL_STATUS_VOTING_PERIOD: PROPOSAL_STATUS_VOTING_PERIOD defines a proposal status during the voting + period. + - PROPOSAL_STATUS_PASSED: PROPOSAL_STATUS_PASSED defines a proposal status of a proposal that has + passed. + - PROPOSAL_STATUS_REJECTED: PROPOSAL_STATUS_REJECTED defines a proposal status of a proposal that has + been rejected. + - PROPOSAL_STATUS_FAILED: PROPOSAL_STATUS_FAILED defines a proposal status of a proposal that has + failed. + in: query + required: false + type: string + enum: + - PROPOSAL_STATUS_UNSPECIFIED + - PROPOSAL_STATUS_DEPOSIT_PERIOD + - PROPOSAL_STATUS_VOTING_PERIOD + - PROPOSAL_STATUS_PASSED + - PROPOSAL_STATUS_REJECTED + - PROPOSAL_STATUS_FAILED + default: PROPOSAL_STATUS_UNSPECIFIED + - name: voter + description: voter defines the voter address for the proposals. + in: query + required: false + type: string + - name: depositor + description: depositor defines the deposit addresses from the proposals. + in: query + required: false + type: string + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false type: string format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}/deposits: + /cosmos/gov/v1/proposals/{proposal_id}: get: - summary: Deposits queries all deposits of a single proposal. - operationId: Deposits + summary: Proposal queries proposal details based on ProposalID. + operationId: GovV1Proposal responses: '200': description: A successful response. schema: type: object properties: - deposits: - type: array - items: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - depositor: - type: string - description: >- - depositor defines the deposit addresses from the proposals. - amount: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - + proposal: + type: object + properties: + id: + type: string + format: uint64 + description: id defines the unique id of the proposal. + messages: + type: array + items: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a canonical form + + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + + expect it to use in the context of Any. However, for URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally set up a type + + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a + + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + JSON + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + messages are the arbitrary messages to be executed if the proposal passes. + status: + description: status defines the proposal status. + type: string + enum: + - PROPOSAL_STATUS_UNSPECIFIED + - PROPOSAL_STATUS_DEPOSIT_PERIOD + - PROPOSAL_STATUS_VOTING_PERIOD + - PROPOSAL_STATUS_PASSED + - PROPOSAL_STATUS_REJECTED + - PROPOSAL_STATUS_FAILED + default: PROPOSAL_STATUS_UNSPECIFIED + final_tally_result: + description: >- + final_tally_result is the final tally result of the proposal. When + + querying a proposal via gRPC, this field is not populated until the + + proposal's voting period has ended. + type: object + properties: + yes_count: + type: string + description: yes_count is the number of yes votes on a proposal. + abstain_count: + type: string + description: >- + abstain_count is the number of abstain votes on a proposal. + no_count: + type: string + description: no_count is the number of no votes on a proposal. + no_with_veto_count: + type: string + description: >- + no_with_veto_count is the number of no with veto votes on a proposal. + submit_time: + type: string + format: date-time + description: submit_time is the time of proposal submission. + deposit_end_time: + type: string + format: date-time + description: deposit_end_time is the end time for deposition. + total_deposit: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + + signatures required by gogoproto. + description: total_deposit is the total deposit on the proposal. + voting_start_time: + type: string + format: date-time + description: >- + voting_start_time is the starting time to vote on a proposal. + voting_end_time: + type: string + format: date-time + description: voting_end_time is the end time of voting on a proposal. + metadata: + type: string + title: >- + metadata is any arbitrary metadata attached to the proposal. + + the recommended format of the metadata is to be found here: + + https://docs.cosmos.network/v0.47/modules/gov#proposal-3 + title: + type: string + description: 'Since: cosmos-sdk 0.47' + title: title is the title of the proposal + summary: + type: string + description: 'Since: cosmos-sdk 0.47' + title: summary is a short summary of the proposal + proposer: + type: string + description: 'Since: cosmos-sdk 0.47' + title: proposer is the address of the proposal sumbitter + expedited: + type: boolean + description: 'Since: cosmos-sdk 0.50' + title: expedited defines if the proposal is expedited + failed_reason: + type: string + description: 'Since: cosmos-sdk 0.50' + title: failed_reason defines the reason why the proposal failed + description: >- + Proposal defines the core field members of a governance proposal. + description: >- + QueryProposalResponse is the response type for the Query/Proposal RPC method. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a canonical form + + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + + expect it to use in the context of Any. However, for URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally set up a type + + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a + + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + JSON + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + parameters: + - name: proposal_id + description: proposal_id defines the unique id of the proposal. + in: path + required: true + type: string + format: uint64 + tags: + - Query + /cosmos/gov/v1/proposals/{proposal_id}/deposits: + get: + summary: Deposits queries all deposits of a single proposal. + operationId: GovV1Deposit + responses: + '200': + description: A successful response. + schema: + type: object + properties: + deposits: + type: array + items: + type: object + properties: + proposal_id: + type: string + format: uint64 + description: proposal_id defines the unique id of the proposal. + depositor: + type: string + description: >- + depositor defines the deposit addresses from the proposals. + amount: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. description: amount to be deposited by depositor. description: >- @@ -10601,11 +10962,11 @@ paths: type: boolean tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}/deposits/{depositor}: + /cosmos/gov/v1/proposals/{proposal_id}/deposits/{depositor}: get: summary: >- - Deposit queries single deposit information based on proposalID, depositor address. - operationId: Deposit + Deposit queries single deposit information based on proposalID, depositAddr. + operationId: GovV1Deposit responses: '200': description: A successful response. @@ -10822,10 +11183,10 @@ paths: type: string tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}/tally: + /cosmos/gov/v1/proposals/{proposal_id}/tally: get: summary: TallyResult queries the tally of a proposal vote. - operationId: TallyResult + operationId: GovV1TallyResult responses: '200': description: A successful response. @@ -10836,19 +11197,20 @@ paths: description: tally defines the requested tally. type: object properties: - 'yes': + yes_count: type: string - description: yes is the number of yes votes on a proposal. - abstain: + description: yes_count is the number of yes votes on a proposal. + abstain_count: type: string - description: abstain is the number of abstain votes on a proposal. - 'no': + description: >- + abstain_count is the number of abstain votes on a proposal. + no_count: type: string - description: no is the number of no votes on a proposal. - no_with_veto: + description: no_count is the number of no votes on a proposal. + no_with_veto_count: type: string description: >- - no_with_veto is the number of no with veto votes on a proposal. + no_with_veto_count is the number of no with veto votes on a proposal. description: >- QueryTallyResultResponse is the response type for the Query/Tally RPC method. default: @@ -11023,10 +11385,10 @@ paths: format: uint64 tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}/votes: + /cosmos/gov/v1/proposals/{proposal_id}/votes: get: summary: Votes queries votes of a given proposal. - operationId: Votes + operationId: GovV1Votes responses: '200': description: A successful response. @@ -11045,21 +11407,6 @@ paths: voter: type: string description: voter is the voter address of the proposal. - option: - description: >- - Deprecated: Prefer to use `options` instead. This field is set in queries - - if and only if `len(options) == 1` and that option has weight 1. In all - - other cases, this field will default to VOTE_OPTION_UNSPECIFIED. - type: string - enum: - - VOTE_OPTION_UNSPECIFIED - - VOTE_OPTION_YES - - VOTE_OPTION_ABSTAIN - - VOTE_OPTION_NO - - VOTE_OPTION_NO_WITH_VETO - default: VOTE_OPTION_UNSPECIFIED options: type: array items: @@ -11082,12 +11429,13 @@ paths: weight is the vote weight associated with the vote option. description: >- WeightedVoteOption defines a unit of vote for vote split. + description: options is the weighted vote options. + metadata: + type: string + title: >- + metadata is any arbitrary metadata attached to the vote. - Since: cosmos-sdk 0.43 - description: |- - options is the weighted vote options. - - Since: cosmos-sdk 0.43 + the recommended format of the metadata is to be found here: https://docs.cosmos.network/v0.47/modules/gov#vote-5 description: >- Vote defines a vote on a governance proposal. @@ -11334,10 +11682,10 @@ paths: type: boolean tags: - Query - /cosmos/gov/v1beta1/proposals/{proposal_id}/votes/{voter}: + /cosmos/gov/v1/proposals/{proposal_id}/votes/{voter}: get: summary: Vote queries voted information based on proposalID, voterAddr. - operationId: Vote + operationId: GovV1Vote responses: '200': description: A successful response. @@ -11354,21 +11702,6 @@ paths: voter: type: string description: voter is the voter address of the proposal. - option: - description: >- - Deprecated: Prefer to use `options` instead. This field is set in queries - - if and only if `len(options) == 1` and that option has weight 1. In all - - other cases, this field will default to VOTE_OPTION_UNSPECIFIED. - type: string - enum: - - VOTE_OPTION_UNSPECIFIED - - VOTE_OPTION_YES - - VOTE_OPTION_ABSTAIN - - VOTE_OPTION_NO - - VOTE_OPTION_NO_WITH_VETO - default: VOTE_OPTION_UNSPECIFIED options: type: array items: @@ -11391,12 +11724,13 @@ paths: weight is the vote weight associated with the vote option. description: >- WeightedVoteOption defines a unit of vote for vote split. + description: options is the weighted vote options. + metadata: + type: string + title: >- + metadata is any arbitrary metadata attached to the vote. - Since: cosmos-sdk 0.43 - description: |- - options is the weighted vote options. - - Since: cosmos-sdk 0.43 + the recommended format of the metadata is to be found here: https://docs.cosmos.network/v0.47/modules/gov#vote-5 description: >- Vote defines a vote on a governance proposal. @@ -11580,20 +11914,30 @@ paths: type: string tags: - Query - /cosmos/gov/v1/constitution: + /cosmos/params/v1beta1/params: get: - summary: Constitution queries the chain's constitution. - operationId: Constitution + summary: |- + Params queries a specific parameter of a module, given its subspace and + key. + operationId: Params responses: '200': description: A successful response. schema: type: object properties: - constitution: - type: string - title: >- - QueryConstitutionResponse is the response type for the Query/Constitution RPC method + param: + description: param defines the queried parameter. + type: object + properties: + subspace: + type: string + key: + type: string + value: + type: string + description: >- + QueryParamsResponse is response type for the Query/Params RPC method. default: description: An unexpected error response. schema: @@ -11613,337 +11957,465 @@ paths: properties: type_url: type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. value: type: string format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. + parameters: + - name: subspace + description: subspace defines the module to query the parameter for. + in: query + required: false + type: string + - name: key + description: key defines the key of the parameter in the subspace. + in: query + required: false + type: string + tags: + - Query + /cosmos/params/v1beta1/subspaces: + get: + summary: >- + Subspaces queries for all registered subspaces and all keys for a subspace. + description: 'Since: cosmos-sdk 0.46' + operationId: Subspaces + responses: + '200': + description: A successful response. + schema: + type: object + properties: + subspaces: + type: array + items: + type: object + properties: + subspace: + type: string + keys: + type: array + items: + type: string description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON + Subspace defines a parameter subspace name and all the keys that exist for - representation, that representation will be embedded adding a field + the subspace. - `value` which holds the custom JSON in addition to the `@type` + Since: cosmos-sdk 0.46 + description: >- + QuerySubspacesResponse defines the response types for querying for all - field. Example (for message [google.protobuf.Duration][]): + registered subspaces and all keys for a subspace. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } + Since: cosmos-sdk 0.46 + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte tags: - Query - /cosmos/gov/v1/params/{params_type}: + /cosmos/slashing/v1beta1/params: get: - summary: Params queries all parameters of the gov module. - operationId: GovV1Params + summary: Params queries the parameters of slashing module + operationId: SlashingParams responses: '200': description: A successful response. schema: type: object properties: - voting_params: - description: |- - Deprecated: Prefer to use `params` instead. - voting_params defines the parameters related to voting. + params: type: object properties: - voting_period: + signed_blocks_window: type: string - description: Duration of the voting period. - deposit_params: - description: |- - Deprecated: Prefer to use `params` instead. - deposit_params defines the parameters related to deposit. - type: object - properties: - min_deposit: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: Minimum deposit for a proposal to enter voting period. - max_deposit_period: + format: int64 + min_signed_per_window: type: string - description: >- - Maximum period for Atom holders to deposit on a proposal. Initial value: 2 - - months. - tally_params: - description: |- - Deprecated: Prefer to use `params` instead. - tally_params defines the parameters related to tally. - type: object - properties: - quorum: + format: byte + downtime_jail_duration: type: string - description: >- - Minimum percentage of total stake needed to vote for a result to be - - considered valid. - threshold: + slash_fraction_double_sign: type: string - description: >- - Minimum proportion of Yes votes for proposal to pass. Default value: 0.5. - veto_threshold: + format: byte + slash_fraction_downtime: type: string - description: >- - Minimum value of Veto votes to Total votes ratio for proposal to be - - vetoed. Default value: 1/3. - params: - description: |- - params defines all the paramaters of x/gov module. - - Since: cosmos-sdk 0.47 - type: object - properties: - min_deposit: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string + format: byte + description: >- + Params represents the parameters used for by the slashing module. + title: >- + QueryParamsResponse is the response type for the Query/Params RPC method + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + tags: + - Query + /cosmos/slashing/v1beta1/signing_infos: + get: + summary: SigningInfos queries signing info of all validators + operationId: SigningInfos + responses: + '200': + description: A successful response. + schema: + type: object + properties: + info: + type: array + items: + type: object + properties: + address: + type: string + start_height: + type: string + format: int64 + title: >- + Height at which validator was first a candidate OR was un-jailed + index_offset: + type: string + format: int64 description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: Minimum deposit for a proposal to enter voting period. - max_deposit_period: - type: string - description: >- - Maximum period for Atom holders to deposit on a proposal. Initial value: 2 + Index which is incremented every time a validator is bonded in a block and - months. - voting_period: - type: string - description: Duration of the voting period. - quorum: - type: string - description: >- - Minimum percentage of total stake needed to vote for a result to be + _may_ have signed a pre-commit or not. This in conjunction with the - considered valid. - threshold: - type: string - description: >- - Minimum proportion of Yes votes for proposal to pass. Default value: 0.5. - veto_threshold: - type: string - description: >- - Minimum value of Veto votes to Total votes ratio for proposal to be + signed_blocks_window param determines the index in the missed block bitmap. + jailed_until: + type: string + format: date-time + description: >- + Timestamp until which the validator is jailed due to liveness downtime. + tombstoned: + type: boolean + description: >- + Whether or not a validator has been tombstoned (killed out of validator - vetoed. Default value: 1/3. - min_initial_deposit_ratio: - type: string - description: >- - The ratio representing the proportion of the deposit value that must be paid at proposal submission. - proposal_cancel_ratio: - type: string - description: >- - The cancel ratio which will not be returned back to the depositors when a proposal is cancelled. + set). It is set once the validator commits an equivocation or for any other - Since: cosmos-sdk 0.50 - proposal_cancel_dest: - type: string - description: >- - The address which will receive (proposal_cancel_ratio * deposit) proposal deposits. + configured misbehavior. + missed_blocks_counter: + type: string + format: int64 + description: >- + A counter of missed (unsigned) blocks. It is used to avoid unnecessary - If empty, the (proposal_cancel_ratio * deposit) proposal deposits will be burned. + reads in the missed block bitmap. + description: >- + ValidatorSigningInfo defines a validator's signing info for monitoring their - Since: cosmos-sdk 0.50 - expedited_voting_period: + liveness activity. + title: info is the signing info of all validators + pagination: + type: object + properties: + next_key: type: string + format: byte description: |- - Duration of the voting period of an expedited proposal. - - Since: cosmos-sdk 0.50 - expedited_threshold: + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string - description: >- - Minimum proportion of Yes votes for proposal to pass. Default value: 0.67. - - Since: cosmos-sdk 0.50 - expedited_min_deposit: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total - signatures required by gogoproto. - description: >- - Minimum expedited deposit for a proposal to enter voting period. - burn_vote_quorum: - type: boolean - title: burn deposits if a proposal does not meet quorum - burn_proposal_deposit_prevote: - type: boolean - title: burn deposits if the proposal does not enter voting period - burn_vote_veto: - type: boolean - title: burn deposits if quorum with vote type no_veto is met - min_deposit_ratio: - type: string - description: >- - The ratio representing the proportion of the deposit value minimum that must be met when making a deposit. + was set, its value is undefined otherwise + description: >- + PageResponse is to be embedded in gRPC response messages where the - Default value: 0.01. Meaning that for a chain with a min_deposit of 100stake, a deposit of 1stake would be + corresponding request message has used PageRequest. - required. + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + title: >- + QuerySigningInfosResponse is the response type for the Query/SigningInfos RPC - Since: cosmos-sdk 0.50 - description: >- - QueryParamsResponse is the response type for the Query/Params RPC method. + method + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + parameters: + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean + tags: + - Query + /cosmos/slashing/v1beta1/signing_infos/{cons_address}: + get: + summary: SigningInfo queries the signing info of given cons address + operationId: SigningInfo + responses: + '200': + description: A successful response. + schema: + type: object + properties: + val_signing_info: + type: object + properties: + address: + type: string + start_height: + type: string + format: int64 + title: >- + Height at which validator was first a candidate OR was un-jailed + index_offset: + type: string + format: int64 + description: >- + Index which is incremented every time a validator is bonded in a block and + + _may_ have signed a pre-commit or not. This in conjunction with the + + signed_blocks_window param determines the index in the missed block bitmap. + jailed_until: + type: string + format: date-time + description: >- + Timestamp until which the validator is jailed due to liveness downtime. + tombstoned: + type: boolean + description: >- + Whether or not a validator has been tombstoned (killed out of validator + + set). It is set once the validator commits an equivocation or for any other + + configured misbehavior. + missed_blocks_counter: + type: string + format: int64 + description: >- + A counter of missed (unsigned) blocks. It is used to avoid unnecessary + + reads in the missed block bitmap. + description: >- + ValidatorSigningInfo defines a validator's signing info for monitoring their + + liveness activity. + title: >- + val_signing_info is the signing info of requested val cons address + title: >- + QuerySigningInfoResponse is the response type for the Query/SigningInfo RPC + + method + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + parameters: + - name: cons_address + description: cons_address is the address to query signing info of + in: path + required: true + type: string + tags: + - Query + /cosmos/staking/v1beta1/delegations/{delegator_addr}: + get: + summary: >- + DelegatorDelegations queries all delegations of a given delegator address. + description: >- + When called from another module, this query might consume a high amount of + + gas if the pagination field is incorrectly set. + operationId: DelegatorDelegations + responses: + '200': + description: A successful response. + schema: + type: object + properties: + delegation_responses: + type: array + items: + type: object + properties: + delegation: + type: object + properties: + delegator_address: + type: string + description: >- + delegator_address is the encoded address of the delegator. + validator_address: + type: string + description: >- + validator_address is the encoded address of the validator. + shares: + type: string + description: shares define the delegation shares received. + description: >- + Delegation represents the bond with tokens held by an account. It is + + owned by one delegator, and is associated with the voting power of one + + validator. + balance: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + + signatures required by gogoproto. + description: >- + DelegationResponse is equivalent to Delegation except that it contains a + + balance in addition to shares which is more suitable for client responses. + description: >- + delegation_responses defines all the delegations' info of a delegator. + pagination: + description: pagination defines the pagination in the response. + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total + + was set, its value is undefined otherwise + description: |- + QueryDelegatorDelegationsResponse is response type for the + Query/DelegatorDelegations RPC method. default: description: An unexpected error response. schema: @@ -12108,286 +12580,510 @@ paths: "value": "1.212s" } parameters: - - name: params_type - description: >- - params_type defines which parameters to query for, can be one of "voting", - - "tallying" or "deposit". + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. in: path required: true type: string + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean tags: - Query - /cosmos/gov/v1/proposals: + /cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations: get: - summary: Proposals queries all proposals based on given status. - operationId: GovV1Proposal + summary: Redelegations queries redelegations of given address. + description: >- + When called from another module, this query might consume a high amount of + + gas if the pagination field is incorrectly set. + operationId: Redelegations responses: '200': description: A successful response. schema: type: object properties: - proposals: + redelegation_responses: type: array items: type: object properties: - id: - type: string - format: uint64 - description: id defines the unique id of the proposal. - messages: + redelegation: + type: object + properties: + delegator_address: + type: string + description: >- + delegator_address is the bech32-encoded address of the delegator. + validator_src_address: + type: string + description: >- + validator_src_address is the validator redelegation source operator address. + validator_dst_address: + type: string + description: >- + validator_dst_address is the validator redelegation destination operator address. + entries: + type: array + items: + type: object + properties: + creation_height: + type: string + format: int64 + description: >- + creation_height defines the height which the redelegation took place. + completion_time: + type: string + format: date-time + description: >- + completion_time defines the unix time for redelegation completion. + initial_balance: + type: string + description: >- + initial_balance defines the initial balance when redelegation started. + shares_dst: + type: string + description: >- + shares_dst is the amount of destination-validator shares created by redelegation. + unbonding_id: + type: string + format: uint64 + title: >- + Incrementing id that uniquely identifies this entry + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + Strictly positive if this entry's unbonding has been stopped by external modules + description: >- + RedelegationEntry defines a redelegation object with relevant metadata. + description: entries are the redelegation entries. + description: >- + Redelegation contains the list of a particular delegator's redelegating bonds + + from a particular source validator to a particular destination validator. + entries: type: array items: type: object properties: - type_url: - type: string + redelegation_entry: + type: object + properties: + creation_height: + type: string + format: int64 + description: >- + creation_height defines the height which the redelegation took place. + completion_time: + type: string + format: date-time + description: >- + completion_time defines the unix time for redelegation completion. + initial_balance: + type: string + description: >- + initial_balance defines the initial balance when redelegation started. + shares_dst: + type: string + description: >- + shares_dst is the amount of destination-validator shares created by redelegation. + unbonding_id: + type: string + format: uint64 + title: >- + Incrementing id that uniquely identifies this entry + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + Strictly positive if this entry's unbonding has been stopped by external modules description: >- - A URL/resource name that uniquely identifies the type of the serialized + RedelegationEntry defines a redelegation object with relevant metadata. + balance: + type: string + description: >- + RedelegationEntryResponse is equivalent to a RedelegationEntry except that it - protocol buffer message. This string must contain at least + contains a balance in addition to shares which is more suitable for client - one "/" character. The last segment of the URL's path must represent + responses. + description: >- + RedelegationResponse is equivalent to a Redelegation except that its entries - the fully qualified name of the type (as in + contain a balance in addition to shares which is more suitable for client - `path/google.protobuf.Duration`). The name should be in a canonical form + responses. + pagination: + description: pagination defines the pagination in the response. + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total - (e.g., leading "." is not accepted). + was set, its value is undefined otherwise + description: >- + QueryRedelegationsResponse is response type for the Query/Redelegations RPC - In practice, teams usually precompile into the binary all types that they + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized - expect it to use in the context of Any. However, for URLs which use the + protocol buffer message. This string must contain at least - scheme `http`, `https`, or no scheme, one can optionally set up a type + one "/" character. The last segment of the URL's path must represent - server that maps type URLs to message definitions as follows: + the fully qualified name of the type (as in - * If no scheme is provided, `https` is assumed. + `path/google.protobuf.Duration`). The name should be in a canonical form - * An HTTP GET on the URL must yield a [google.protobuf.Type][] + (e.g., leading "." is not accepted). - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the + In practice, teams usually precompile into the binary all types that they - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + expect it to use in the context of Any. However, for URLs which use the - Note: this functionality is not currently available in the official + scheme `http`, `https`, or no scheme, one can optionally set up a type - protobuf release, and it is not used for type URLs beginning with + server that maps type URLs to message definitions as follows: - type.googleapis.com. + * If no scheme is provided, `https` is assumed. - Schemes other than `http`, `https` (or the empty scheme) might be + * An HTTP GET on the URL must yield a [google.protobuf.Type][] - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the - URL that describes the type of the serialized message. + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) - Protobuf library provides support to pack/unpack Any values in the form + Note: this functionality is not currently available in the official - of utility functions or additional generated methods of the Any type. + protobuf release, and it is not used for type URLs beginning with - Example 1: Pack and unpack a message in C++. + type.googleapis.com. - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } + Schemes other than `http`, `https` (or the empty scheme) might be - Example 2: Pack and unpack a message in Java. + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } + URL that describes the type of the serialized message. - Example 3: Pack and unpack a message in Python. + Protobuf library provides support to pack/unpack Any values in the form - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... + of utility functions or additional generated methods of the Any type. - Example 4: Pack and unpack a message in Go + Example 1: Pack and unpack a message in C++. - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - The pack methods provided by protobuf library will by default use + Example 2: Pack and unpack a message in Java. - 'type.googleapis.com/full.type.name' as the type URL and the unpack + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } - methods only use the fully qualified type name after the last '/' + Example 3: Pack and unpack a message in Python. - in the type URL, for example "foo.bar.com/x/y.z" will yield type + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - name "y.z". + Example 4: Pack and unpack a message in Go - JSON + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - The JSON representation of an `Any` value uses the regular + The pack methods provided by protobuf library will by default use - representation of the deserialized, embedded message, with an + 'type.googleapis.com/full.type.name' as the type URL and the unpack - additional field `@type` which contains the type URL. Example: + methods only use the fully qualified type name after the last '/' - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + in the type URL, for example "foo.bar.com/x/y.z" will yield type - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + name "y.z". - If the embedded message type is well-known and has a custom JSON + JSON - representation, that representation will be embedded adding a field + The JSON representation of an `Any` value uses the regular - `value` which holds the custom JSON in addition to the `@type` + representation of the deserialized, embedded message, with an - field. Example (for message [google.protobuf.Duration][]): + additional field `@type` which contains the type URL. Example: - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - messages are the arbitrary messages to be executed if the proposal passes. - status: - description: status defines the proposal status. - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - final_tally_result: - description: >- - final_tally_result is the final tally result of the proposal. When + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - querying a proposal via gRPC, this field is not populated until the + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - proposal's voting period has ended. - type: object - properties: - yes_count: - type: string - description: yes_count is the number of yes votes on a proposal. - abstain_count: - type: string - description: >- - abstain_count is the number of abstain votes on a proposal. - no_count: - type: string - description: no_count is the number of no votes on a proposal. - no_with_veto_count: - type: string - description: >- - no_with_veto_count is the number of no with veto votes on a proposal. - submit_time: + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + parameters: + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. + in: path + required: true + type: string + - name: src_validator_addr + description: src_validator_addr defines the validator address to redelegate from. + in: query + required: false + type: string + - name: dst_validator_addr + description: dst_validator_addr defines the validator address to redelegate to. + in: query + required: false + type: string + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean + tags: + - Query + /cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations: + get: + summary: >- + DelegatorUnbondingDelegations queries all unbonding delegations of a given + + delegator address. + description: >- + When called from another module, this query might consume a high amount of + + gas if the pagination field is incorrectly set. + operationId: DelegatorUnbondingDelegations + responses: + '200': + description: A successful response. + schema: + type: object + properties: + unbonding_responses: + type: array + items: + type: object + properties: + delegator_address: type: string - format: date-time - description: submit_time is the time of proposal submission. - deposit_end_time: + description: >- + delegator_address is the encoded address of the delegator. + validator_address: type: string - format: date-time - description: deposit_end_time is the end time for deposition. - total_deposit: + description: >- + validator_address is the encoded address of the validator. + entries: type: array items: type: object properties: - denom: + creation_height: type: string - amount: + format: int64 + description: >- + creation_height is the height which the unbonding took place. + completion_time: type: string + format: date-time + description: >- + completion_time is the unix time for unbonding completion. + initial_balance: + type: string + description: >- + initial_balance defines the tokens initially scheduled to receive at completion. + balance: + type: string + description: >- + balance defines the tokens to receive at completion. + unbonding_id: + type: string + format: uint64 + title: >- + Incrementing id that uniquely identifies this entry + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + Strictly positive if this entry's unbonding has been stopped by external modules description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: total_deposit is the total deposit on the proposal. - voting_start_time: - type: string - format: date-time - description: >- - voting_start_time is the starting time to vote on a proposal. - voting_end_time: - type: string - format: date-time - description: voting_end_time is the end time of voting on a proposal. - metadata: - type: string - title: >- - metadata is any arbitrary metadata attached to the proposal. - - the recommended format of the metadata is to be found here: - - https://docs.cosmos.network/v0.47/modules/gov#proposal-3 - title: - type: string - description: 'Since: cosmos-sdk 0.47' - title: title is the title of the proposal - summary: - type: string - description: 'Since: cosmos-sdk 0.47' - title: summary is a short summary of the proposal - proposer: - type: string - description: 'Since: cosmos-sdk 0.47' - title: proposer is the address of the proposal sumbitter - expedited: - type: boolean - description: 'Since: cosmos-sdk 0.50' - title: expedited defines if the proposal is expedited - failed_reason: - type: string - description: 'Since: cosmos-sdk 0.50' - title: failed_reason defines the reason why the proposal failed + UnbondingDelegationEntry defines an unbonding object with relevant metadata. + description: entries are the unbonding delegation entries. description: >- - Proposal defines the core field members of a governance proposal. - description: proposals defines all the requested governance proposals. + UnbondingDelegation stores all of a single delegator's unbonding bonds + + for a single validator in an time-ordered list. pagination: description: pagination defines the pagination in the response. type: object @@ -12407,9 +13103,9 @@ paths: was set, its value is undefined otherwise description: >- - QueryProposalsResponse is the response type for the Query/Proposals RPC + QueryUnbondingDelegatorDelegationsResponse is response type for the - method. + Query/UnbondingDelegatorDelegations RPC method. default: description: An unexpected error response. schema: @@ -12574,41 +13270,10 @@ paths: "value": "1.212s" } parameters: - - name: proposal_status - description: |- - proposal_status defines the status of the proposals. - - - PROPOSAL_STATUS_UNSPECIFIED: PROPOSAL_STATUS_UNSPECIFIED defines the default proposal status. - - PROPOSAL_STATUS_DEPOSIT_PERIOD: PROPOSAL_STATUS_DEPOSIT_PERIOD defines a proposal status during the deposit - period. - - PROPOSAL_STATUS_VOTING_PERIOD: PROPOSAL_STATUS_VOTING_PERIOD defines a proposal status during the voting - period. - - PROPOSAL_STATUS_PASSED: PROPOSAL_STATUS_PASSED defines a proposal status of a proposal that has - passed. - - PROPOSAL_STATUS_REJECTED: PROPOSAL_STATUS_REJECTED defines a proposal status of a proposal that has - been rejected. - - PROPOSAL_STATUS_FAILED: PROPOSAL_STATUS_FAILED defines a proposal status of a proposal that has - failed. - in: query - required: false - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - - name: voter - description: voter defines the voter address for the proposals. - in: query - required: false - type: string - - name: depositor - description: depositor defines the deposit addresses from the proposals. - in: query - required: false + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. + in: path + required: true type: string - name: pagination.key description: |- @@ -12661,26 +13326,32 @@ paths: type: boolean tags: - Query - /cosmos/gov/v1/proposals/{proposal_id}: + /cosmos/staking/v1beta1/delegators/{delegator_addr}/validators: get: - summary: Proposal queries proposal details based on ProposalID. - operationId: GovV1Proposal + summary: |- + DelegatorValidators queries all validators info for given delegator + address. + description: >- + When called from another module, this query might consume a high amount of + + gas if the pagination field is incorrectly set. + operationId: StakingDelegatorValidators responses: '200': description: A successful response. schema: type: object properties: - proposal: - type: object - properties: - id: - type: string - format: uint64 - description: id defines the unique id of the proposal. - messages: - type: array - items: + validators: + type: array + items: + type: object + properties: + operator_address: + type: string + description: >- + operator_address defines the address of the validator's operator; bech encoded in JSON. + consensus_pubkey: type: object properties: type_url: @@ -12829,107 +13500,143 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - description: >- - messages are the arbitrary messages to be executed if the proposal passes. - status: - description: status defines the proposal status. - type: string - enum: - - PROPOSAL_STATUS_UNSPECIFIED - - PROPOSAL_STATUS_DEPOSIT_PERIOD - - PROPOSAL_STATUS_VOTING_PERIOD - - PROPOSAL_STATUS_PASSED - - PROPOSAL_STATUS_REJECTED - - PROPOSAL_STATUS_FAILED - default: PROPOSAL_STATUS_UNSPECIFIED - final_tally_result: - description: >- - final_tally_result is the final tally result of the proposal. When - - querying a proposal via gRPC, this field is not populated until the - - proposal's voting period has ended. - type: object - properties: - yes_count: - type: string - description: yes_count is the number of yes votes on a proposal. - abstain_count: - type: string - description: >- - abstain_count is the number of abstain votes on a proposal. - no_count: - type: string - description: no_count is the number of no votes on a proposal. - no_with_veto_count: - type: string - description: >- - no_with_veto_count is the number of no with veto votes on a proposal. - submit_time: - type: string - format: date-time - description: submit_time is the time of proposal submission. - deposit_end_time: - type: string - format: date-time - description: deposit_end_time is the end time for deposition. - total_deposit: - type: array - items: + jailed: + type: boolean + description: >- + jailed defined whether the validator has been jailed from bonded status or not. + status: + description: >- + status is the validator status (bonded/unbonding/unbonded). + type: string + enum: + - BOND_STATUS_UNSPECIFIED + - BOND_STATUS_UNBONDED + - BOND_STATUS_UNBONDING + - BOND_STATUS_BONDED + default: BOND_STATUS_UNSPECIFIED + tokens: + type: string + description: >- + tokens define the delegated tokens (incl. self-delegation). + delegator_shares: + type: string + description: >- + delegator_shares defines total shares issued to a validator's delegators. + description: + description: >- + description defines the description terms for the validator. type: object properties: - denom: + moniker: type: string - amount: + description: >- + moniker defines a human-readable name for the validator. + identity: type: string + description: >- + identity defines an optional identity signature (ex. UPort or Keybase). + website: + type: string + description: website defines an optional website link. + security_contact: + type: string + description: >- + security_contact defines an optional email for security contact. + details: + type: string + description: details define other optional details. + unbonding_height: + type: string + format: int64 description: >- - Coin defines a token with a denomination and an amount. + unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. + unbonding_time: + type: string + format: date-time + description: >- + unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. + commission: + description: commission defines the commission parameters. + type: object + properties: + commission_rates: + description: >- + commission_rates defines the initial commission rates to be used for creating a validator. + type: object + properties: + rate: + type: string + description: >- + rate is the commission rate charged to delegators, as a fraction. + max_rate: + type: string + description: >- + max_rate defines the maximum commission rate which validator can ever charge, as a fraction. + max_change_rate: + type: string + description: >- + max_change_rate defines the maximum daily increase of the validator commission, as a fraction. + update_time: + type: string + format: date-time + description: >- + update_time is the last time the commission rate was changed. + min_self_delegation: + type: string + description: >- + min_self_delegation is the validator's self declared minimum self delegation. - NOTE: The amount field is an Int which implements the custom method + Since: cosmos-sdk 0.46 + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + strictly positive if this validator's unbonding has been stopped by external modules + unbonding_ids: + type: array + items: + type: string + format: uint64 + title: >- + list of unbonding ids, each uniquely identifing an unbonding of this validator + description: >- + Validator defines a validator, together with the total amount of the - signatures required by gogoproto. - description: total_deposit is the total deposit on the proposal. - voting_start_time: - type: string - format: date-time - description: >- - voting_start_time is the starting time to vote on a proposal. - voting_end_time: - type: string - format: date-time - description: voting_end_time is the end time of voting on a proposal. - metadata: - type: string - title: >- - metadata is any arbitrary metadata attached to the proposal. + Validator's bond shares and their exchange rate to coins. Slashing results in - the recommended format of the metadata is to be found here: + a decrease in the exchange rate, allowing correct calculation of future - https://docs.cosmos.network/v0.47/modules/gov#proposal-3 - title: - type: string - description: 'Since: cosmos-sdk 0.47' - title: title is the title of the proposal - summary: - type: string - description: 'Since: cosmos-sdk 0.47' - title: summary is a short summary of the proposal - proposer: + undelegations without iterating over delegators. When coins are delegated to + + this validator, the validator is credited with a delegation whose number of + + bond shares is based on the amount of coins delegated divided by the current + + exchange rate. Voting power can be calculated as total bonded shares + + multiplied by exchange rate. + description: validators defines the validators' info of a delegator. + pagination: + description: pagination defines the pagination in the response. + type: object + properties: + next_key: type: string - description: 'Since: cosmos-sdk 0.47' - title: proposer is the address of the proposal sumbitter - expedited: - type: boolean - description: 'Since: cosmos-sdk 0.50' - title: expedited defines if the proposal is expedited - failed_reason: + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string - description: 'Since: cosmos-sdk 0.50' - title: failed_reason defines the reason why the proposal failed - description: >- - Proposal defines the core field members of a governance proposal. - description: >- - QueryProposalResponse is the response type for the Query/Proposal RPC method. + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total + + was set, its value is undefined otherwise + description: |- + QueryDelegatorValidatorsResponse is response type for the + Query/DelegatorValidators RPC method. default: description: An unexpected error response. schema: @@ -13094,78 +13801,348 @@ paths: "value": "1.212s" } parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. in: path required: true type: string + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean tags: - Query - /cosmos/gov/v1/proposals/{proposal_id}/deposits: + /cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}: get: - summary: Deposits queries all deposits of a single proposal. - operationId: GovV1Deposit + summary: |- + DelegatorValidator queries validator info for given delegator validator + pair. + operationId: DelegatorValidator responses: '200': description: A successful response. schema: type: object properties: - deposits: - type: array - items: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - depositor: - type: string - description: >- - depositor defines the deposit addresses from the proposals. - amount: - type: array - items: + validator: + type: object + properties: + operator_address: + type: string + description: >- + operator_address defines the address of the validator's operator; bech encoded in JSON. + consensus_pubkey: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a canonical form + + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + + expect it to use in the context of Any. However, for URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally set up a type + + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a + + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + JSON + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + jailed: + type: boolean + description: >- + jailed defined whether the validator has been jailed from bonded status or not. + status: + description: >- + status is the validator status (bonded/unbonding/unbonded). + type: string + enum: + - BOND_STATUS_UNSPECIFIED + - BOND_STATUS_UNBONDED + - BOND_STATUS_UNBONDING + - BOND_STATUS_BONDED + default: BOND_STATUS_UNSPECIFIED + tokens: + type: string + description: >- + tokens define the delegated tokens (incl. self-delegation). + delegator_shares: + type: string + description: >- + delegator_shares defines total shares issued to a validator's delegators. + description: + description: >- + description defines the description terms for the validator. + type: object + properties: + moniker: + type: string + description: >- + moniker defines a human-readable name for the validator. + identity: + type: string + description: >- + identity defines an optional identity signature (ex. UPort or Keybase). + website: + type: string + description: website defines an optional website link. + security_contact: + type: string + description: >- + security_contact defines an optional email for security contact. + details: + type: string + description: details define other optional details. + unbonding_height: + type: string + format: int64 + description: >- + unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. + unbonding_time: + type: string + format: date-time + description: >- + unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. + commission: + description: commission defines the commission parameters. + type: object + properties: + commission_rates: + description: >- + commission_rates defines the initial commission rates to be used for creating a validator. type: object properties: - denom: + rate: type: string - amount: + description: >- + rate is the commission rate charged to delegators, as a fraction. + max_rate: + type: string + description: >- + max_rate defines the maximum commission rate which validator can ever charge, as a fraction. + max_change_rate: type: string + description: >- + max_change_rate defines the maximum daily increase of the validator commission, as a fraction. + update_time: + type: string + format: date-time description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: amount to be deposited by depositor. - description: >- - Deposit defines an amount deposited by an account address to an active - - proposal. - description: deposits defines the requested deposits. - pagination: - description: pagination defines the pagination in the response. - type: object - properties: - next_key: + update_time is the last time the commission rate was changed. + min_self_delegation: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + description: >- + min_self_delegation is the validator's self declared minimum self delegation. + + Since: cosmos-sdk 0.46 + unbonding_on_hold_ref_count: type: string - format: uint64 + format: int64 title: >- - total is total number of results available if PageRequest.count_total + strictly positive if this validator's unbonding has been stopped by external modules + unbonding_ids: + type: array + items: + type: string + format: uint64 + title: >- + list of unbonding ids, each uniquely identifing an unbonding of this validator + description: >- + Validator defines a validator, together with the total amount of the - was set, its value is undefined otherwise - description: >- - QueryDepositsResponse is the response type for the Query/Deposits RPC method. + Validator's bond shares and their exchange rate to coins. Slashing results in + + a decrease in the exchange rate, allowing correct calculation of future + + undelegations without iterating over delegators. When coins are delegated to + + this validator, the validator is credited with a delegation whose number of + + bond shares is based on the amount of coins delegated divided by the current + + exchange rate. Voting power can be calculated as total bonded shares + + multiplied by exchange rate. + description: |- + QueryDelegatorValidatorResponse response type for the + Query/DelegatorValidator RPC method. default: description: An unexpected error response. schema: @@ -13330,1517 +14307,384 @@ paths: "value": "1.212s" } parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. in: path required: true type: string - format: uint64 - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false + - name: validator_addr + description: validator_addr defines the validator address to query for. + in: path + required: true type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean tags: - Query - /cosmos/gov/v1/proposals/{proposal_id}/deposits/{depositor}: + /cosmos/staking/v1beta1/historical_info/{height}: get: - summary: >- - Deposit queries single deposit information based on proposalID, depositAddr. - operationId: GovV1Deposit + summary: HistoricalInfo queries the historical info for given height. + operationId: HistoricalInfo responses: '200': description: A successful response. schema: type: object properties: - deposit: + hist: + description: hist defines the historical info at the given height. type: object properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - depositor: - type: string - description: >- - depositor defines the deposit addresses from the proposals. - amount: + header: + type: object + properties: + version: + title: basic block info + type: object + properties: + block: + type: string + format: uint64 + app: + type: string + format: uint64 + description: >- + Consensus captures the consensus rules for processing a block in the blockchain, + + including all blockchain data structures and the rules of the application's + + state transition machine. + chain_id: + type: string + height: + type: string + format: int64 + time: + type: string + format: date-time + last_block_id: + title: prev block info + type: object + properties: + hash: + type: string + format: byte + part_set_header: + type: object + properties: + total: + type: integer + format: int64 + hash: + type: string + format: byte + title: PartsetHeader + last_commit_hash: + type: string + format: byte + title: hashes of block data + data_hash: + type: string + format: byte + validators_hash: + type: string + format: byte + title: hashes from the app output from the prev block + next_validators_hash: + type: string + format: byte + consensus_hash: + type: string + format: byte + app_hash: + type: string + format: byte + last_results_hash: + type: string + format: byte + evidence_hash: + type: string + format: byte + title: consensus info + proposer_address: + type: string + format: byte + description: Header defines the structure of a block header. + valset: type: array items: type: object properties: - denom: - type: string - amount: + operator_address: type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: amount to be deposited by depositor. - description: >- - Deposit defines an amount deposited by an account address to an active - - proposal. - description: >- - QueryDepositResponse is the response type for the Query/Deposit RPC method. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } + description: >- + operator_address defines the address of the validator's operator; bech encoded in JSON. + consensus_pubkey: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized - Example 2: Pack and unpack a message in Java. + protocol buffer message. This string must contain at least - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } + one "/" character. The last segment of the URL's path must represent - Example 3: Pack and unpack a message in Python. + the fully qualified name of the type (as in - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... + `path/google.protobuf.Duration`). The name should be in a canonical form - Example 4: Pack and unpack a message in Go + (e.g., leading "." is not accepted). - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + In practice, teams usually precompile into the binary all types that they - The pack methods provided by protobuf library will by default use + expect it to use in the context of Any. However, for URLs which use the - 'type.googleapis.com/full.type.name' as the type URL and the unpack + scheme `http`, `https`, or no scheme, one can optionally set up a type - methods only use the fully qualified type name after the last '/' + server that maps type URLs to message definitions as follows: - in the type URL, for example "foo.bar.com/x/y.z" will yield type + * If no scheme is provided, `https` is assumed. - name "y.z". + * An HTTP GET on the URL must yield a [google.protobuf.Type][] - JSON + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the - The JSON representation of an `Any` value uses the regular + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) - representation of the deserialized, embedded message, with an + Note: this functionality is not currently available in the official - additional field `@type` which contains the type URL. Example: + protobuf release, and it is not used for type URLs beginning with - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + type.googleapis.com. - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + Schemes other than `http`, `https` (or the empty scheme) might be - If the embedded message type is well-known and has a custom JSON + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a - representation, that representation will be embedded adding a field + URL that describes the type of the serialized message. - `value` which holds the custom JSON in addition to the `@type` + Protobuf library provides support to pack/unpack Any values in the form - field. Example (for message [google.protobuf.Duration][]): + of utility functions or additional generated methods of the Any type. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. - in: path - required: true - type: string - format: uint64 - - name: depositor - description: depositor defines the deposit addresses from the proposals. - in: path - required: true - type: string - tags: - - Query - /cosmos/gov/v1/proposals/{proposal_id}/tally: - get: - summary: TallyResult queries the tally of a proposal vote. - operationId: GovV1TallyResult - responses: - '200': - description: A successful response. - schema: - type: object - properties: - tally: - description: tally defines the requested tally. - type: object - properties: - yes_count: - type: string - description: yes_count is the number of yes votes on a proposal. - abstain_count: - type: string - description: >- - abstain_count is the number of abstain votes on a proposal. - no_count: - type: string - description: no_count is the number of no votes on a proposal. - no_with_veto_count: - type: string - description: >- - no_with_veto_count is the number of no with veto votes on a proposal. - description: >- - QueryTallyResultResponse is the response type for the Query/Tally RPC method. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized + Example 1: Pack and unpack a message in C++. - protocol buffer message. This string must contain at least + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - one "/" character. The last segment of the URL's path must represent + Example 2: Pack and unpack a message in Java. - the fully qualified name of the type (as in + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } - `path/google.protobuf.Duration`). The name should be in a canonical form + Example 3: Pack and unpack a message in Python. - (e.g., leading "." is not accepted). + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - In practice, teams usually precompile into the binary all types that they + Example 4: Pack and unpack a message in Go - expect it to use in the context of Any. However, for URLs which use the + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - scheme `http`, `https`, or no scheme, one can optionally set up a type + The pack methods provided by protobuf library will by default use - server that maps type URLs to message definitions as follows: + 'type.googleapis.com/full.type.name' as the type URL and the unpack - * If no scheme is provided, `https` is assumed. + methods only use the fully qualified type name after the last '/' - * An HTTP GET on the URL must yield a [google.protobuf.Type][] + in the type URL, for example "foo.bar.com/x/y.z" will yield type - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the + name "y.z". - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + JSON - Note: this functionality is not currently available in the official + The JSON representation of an `Any` value uses the regular - protobuf release, and it is not used for type URLs beginning with + representation of the deserialized, embedded message, with an - type.googleapis.com. + additional field `@type` which contains the type URL. Example: - Schemes other than `http`, `https` (or the empty scheme) might be + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - URL that describes the type of the serialized message. + If the embedded message type is well-known and has a custom JSON - Protobuf library provides support to pack/unpack Any values in the form + representation, that representation will be embedded adding a field - of utility functions or additional generated methods of the Any type. + `value` which holds the custom JSON in addition to the `@type` - Example 1: Pack and unpack a message in C++. + field. Example (for message [google.protobuf.Duration][]): - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. - in: path - required: true - type: string - format: uint64 - tags: - - Query - /cosmos/gov/v1/proposals/{proposal_id}/votes: - get: - summary: Votes queries votes of a given proposal. - operationId: GovV1Votes - responses: - '200': - description: A successful response. - schema: - type: object - properties: - votes: - type: array - items: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - voter: - type: string - description: voter is the voter address of the proposal. - options: - type: array - items: - type: object - properties: - option: - description: >- - option defines the valid vote options, it must not contain duplicate vote options. - type: string - enum: - - VOTE_OPTION_UNSPECIFIED - - VOTE_OPTION_YES - - VOTE_OPTION_ABSTAIN - - VOTE_OPTION_NO - - VOTE_OPTION_NO_WITH_VETO - default: VOTE_OPTION_UNSPECIFIED - weight: - type: string - description: >- - weight is the vote weight associated with the vote option. - description: >- - WeightedVoteOption defines a unit of vote for vote split. - description: options is the weighted vote options. - metadata: - type: string - title: >- - metadata is any arbitrary metadata attached to the vote. - - the recommended format of the metadata is to be found here: https://docs.cosmos.network/v0.47/modules/gov#vote-5 - description: >- - Vote defines a vote on a governance proposal. - - A Vote consists of a proposal ID, the voter, and the vote option. - description: votes defines the queried votes. - pagination: - description: pagination defines the pagination in the response. - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total - - was set, its value is undefined otherwise - description: >- - QueryVotesResponse is the response type for the Query/Votes RPC method. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. - in: path - required: true - type: string - format: uint64 - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean - tags: - - Query - /cosmos/gov/v1/proposals/{proposal_id}/votes/{voter}: - get: - summary: Vote queries voted information based on proposalID, voterAddr. - operationId: GovV1Vote - responses: - '200': - description: A successful response. - schema: - type: object - properties: - vote: - type: object - properties: - proposal_id: - type: string - format: uint64 - description: proposal_id defines the unique id of the proposal. - voter: - type: string - description: voter is the voter address of the proposal. - options: - type: array - items: - type: object - properties: - option: + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + jailed: + type: boolean description: >- - option defines the valid vote options, it must not contain duplicate vote options. + jailed defined whether the validator has been jailed from bonded status or not. + status: + description: >- + status is the validator status (bonded/unbonding/unbonded). type: string enum: - - VOTE_OPTION_UNSPECIFIED - - VOTE_OPTION_YES - - VOTE_OPTION_ABSTAIN - - VOTE_OPTION_NO - - VOTE_OPTION_NO_WITH_VETO - default: VOTE_OPTION_UNSPECIFIED - weight: + - BOND_STATUS_UNSPECIFIED + - BOND_STATUS_UNBONDED + - BOND_STATUS_UNBONDING + - BOND_STATUS_BONDED + default: BOND_STATUS_UNSPECIFIED + tokens: type: string description: >- - weight is the vote weight associated with the vote option. - description: >- - WeightedVoteOption defines a unit of vote for vote split. - description: options is the weighted vote options. - metadata: - type: string - title: >- - metadata is any arbitrary metadata attached to the vote. - - the recommended format of the metadata is to be found here: https://docs.cosmos.network/v0.47/modules/gov#vote-5 - description: >- - Vote defines a vote on a governance proposal. - - A Vote consists of a proposal ID, the voter, and the vote option. - description: >- - QueryVoteResponse is the response type for the Query/Vote RPC method. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - parameters: - - name: proposal_id - description: proposal_id defines the unique id of the proposal. - in: path - required: true - type: string - format: uint64 - - name: voter - description: voter defines the voter address for the proposals. - in: path - required: true - type: string - tags: - - Query - /cosmos/params/v1beta1/params: - get: - summary: |- - Params queries a specific parameter of a module, given its subspace and - key. - operationId: Params - responses: - '200': - description: A successful response. - schema: - type: object - properties: - param: - description: param defines the queried parameter. - type: object - properties: - subspace: - type: string - key: - type: string - value: - type: string - description: >- - QueryParamsResponse is response type for the Query/Params RPC method. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: subspace - description: subspace defines the module to query the parameter for. - in: query - required: false - type: string - - name: key - description: key defines the key of the parameter in the subspace. - in: query - required: false - type: string - tags: - - Query - /cosmos/params/v1beta1/subspaces: - get: - summary: >- - Subspaces queries for all registered subspaces and all keys for a subspace. - description: 'Since: cosmos-sdk 0.46' - operationId: Subspaces - responses: - '200': - description: A successful response. - schema: - type: object - properties: - subspaces: - type: array - items: - type: object - properties: - subspace: - type: string - keys: - type: array - items: - type: string - description: >- - Subspace defines a parameter subspace name and all the keys that exist for - - the subspace. - - Since: cosmos-sdk 0.46 - description: >- - QuerySubspacesResponse defines the response types for querying for all - - registered subspaces and all keys for a subspace. - - Since: cosmos-sdk 0.46 - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - tags: - - Query - /cosmos/slashing/v1beta1/params: - get: - summary: Params queries the parameters of slashing module - operationId: SlashingParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - type: object - properties: - signed_blocks_window: - type: string - format: int64 - min_signed_per_window: - type: string - format: byte - downtime_jail_duration: - type: string - slash_fraction_double_sign: - type: string - format: byte - slash_fraction_downtime: - type: string - format: byte - description: >- - Params represents the parameters used for by the slashing module. - title: >- - QueryParamsResponse is the response type for the Query/Params RPC method - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - tags: - - Query - /cosmos/slashing/v1beta1/signing_infos: - get: - summary: SigningInfos queries signing info of all validators - operationId: SigningInfos - responses: - '200': - description: A successful response. - schema: - type: object - properties: - info: - type: array - items: - type: object - properties: - address: - type: string - start_height: - type: string - format: int64 - title: >- - Height at which validator was first a candidate OR was un-jailed - index_offset: - type: string - format: int64 - description: >- - Index which is incremented every time a validator is bonded in a block and - - _may_ have signed a pre-commit or not. This in conjunction with the - - signed_blocks_window param determines the index in the missed block bitmap. - jailed_until: - type: string - format: date-time - description: >- - Timestamp until which the validator is jailed due to liveness downtime. - tombstoned: - type: boolean - description: >- - Whether or not a validator has been tombstoned (killed out of validator - - set). It is set once the validator commits an equivocation or for any other - - configured misbehavior. - missed_blocks_counter: - type: string - format: int64 - description: >- - A counter of missed (unsigned) blocks. It is used to avoid unnecessary - - reads in the missed block bitmap. - description: >- - ValidatorSigningInfo defines a validator's signing info for monitoring their - - liveness activity. - title: info is the signing info of all validators - pagination: - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total - - was set, its value is undefined otherwise - description: >- - PageResponse is to be embedded in gRPC response messages where the - - corresponding request message has used PageRequest. - - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - title: >- - QuerySigningInfosResponse is the response type for the Query/SigningInfos RPC - - method - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean - tags: - - Query - /cosmos/slashing/v1beta1/signing_infos/{cons_address}: - get: - summary: SigningInfo queries the signing info of given cons address - operationId: SigningInfo - responses: - '200': - description: A successful response. - schema: - type: object - properties: - val_signing_info: - type: object - properties: - address: - type: string - start_height: - type: string - format: int64 - title: >- - Height at which validator was first a candidate OR was un-jailed - index_offset: - type: string - format: int64 - description: >- - Index which is incremented every time a validator is bonded in a block and - - _may_ have signed a pre-commit or not. This in conjunction with the - - signed_blocks_window param determines the index in the missed block bitmap. - jailed_until: - type: string - format: date-time - description: >- - Timestamp until which the validator is jailed due to liveness downtime. - tombstoned: - type: boolean - description: >- - Whether or not a validator has been tombstoned (killed out of validator - - set). It is set once the validator commits an equivocation or for any other - - configured misbehavior. - missed_blocks_counter: - type: string - format: int64 - description: >- - A counter of missed (unsigned) blocks. It is used to avoid unnecessary - - reads in the missed block bitmap. - description: >- - ValidatorSigningInfo defines a validator's signing info for monitoring their - - liveness activity. - title: >- - val_signing_info is the signing info of requested val cons address - title: >- - QuerySigningInfoResponse is the response type for the Query/SigningInfo RPC - - method - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: cons_address - description: cons_address is the address to query signing info of - in: path - required: true - type: string - tags: - - Query - /cosmos/staking/v1beta1/delegations/{delegator_addr}: - get: - summary: >- - DelegatorDelegations queries all delegations of a given delegator address. - description: >- - When called from another module, this query might consume a high amount of - - gas if the pagination field is incorrectly set. - operationId: DelegatorDelegations - responses: - '200': - description: A successful response. - schema: - type: object - properties: - delegation_responses: - type: array - items: - type: object - properties: - delegation: - type: object - properties: - delegator_address: + tokens define the delegated tokens (incl. self-delegation). + delegator_shares: type: string description: >- - delegator_address is the encoded address of the delegator. - validator_address: + delegator_shares defines total shares issued to a validator's delegators. + description: + description: >- + description defines the description terms for the validator. + type: object + properties: + moniker: + type: string + description: >- + moniker defines a human-readable name for the validator. + identity: + type: string + description: >- + identity defines an optional identity signature (ex. UPort or Keybase). + website: + type: string + description: website defines an optional website link. + security_contact: + type: string + description: >- + security_contact defines an optional email for security contact. + details: + type: string + description: details define other optional details. + unbonding_height: + type: string + format: int64 + description: >- + unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. + unbonding_time: + type: string + format: date-time + description: >- + unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. + commission: + description: commission defines the commission parameters. + type: object + properties: + commission_rates: + description: >- + commission_rates defines the initial commission rates to be used for creating a validator. + type: object + properties: + rate: + type: string + description: >- + rate is the commission rate charged to delegators, as a fraction. + max_rate: + type: string + description: >- + max_rate defines the maximum commission rate which validator can ever charge, as a fraction. + max_change_rate: + type: string + description: >- + max_change_rate defines the maximum daily increase of the validator commission, as a fraction. + update_time: + type: string + format: date-time + description: >- + update_time is the last time the commission rate was changed. + min_self_delegation: type: string description: >- - validator_address is the encoded address of the validator. - shares: + min_self_delegation is the validator's self declared minimum self delegation. + + Since: cosmos-sdk 0.46 + unbonding_on_hold_ref_count: type: string - description: shares define the delegation shares received. + format: int64 + title: >- + strictly positive if this validator's unbonding has been stopped by external modules + unbonding_ids: + type: array + items: + type: string + format: uint64 + title: >- + list of unbonding ids, each uniquely identifing an unbonding of this validator description: >- - Delegation represents the bond with tokens held by an account. It is + Validator defines a validator, together with the total amount of the - owned by one delegator, and is associated with the voting power of one + Validator's bond shares and their exchange rate to coins. Slashing results in - validator. - balance: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. + a decrease in the exchange rate, allowing correct calculation of future - NOTE: The amount field is an Int which implements the custom method + undelegations without iterating over delegators. When coins are delegated to - signatures required by gogoproto. - description: >- - DelegationResponse is equivalent to Delegation except that it contains a + this validator, the validator is credited with a delegation whose number of - balance in addition to shares which is more suitable for client responses. - description: >- - delegation_responses defines all the delegations' info of a delegator. - pagination: - description: pagination defines the pagination in the response. - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total + bond shares is based on the amount of coins delegated divided by the current - was set, its value is undefined otherwise - description: |- - QueryDelegatorDelegationsResponse is response type for the - Query/DelegatorDelegations RPC method. + exchange rate. Voting power can be calculated as total bonded shares + + multiplied by exchange rate. + description: >- + QueryHistoricalInfoResponse is response type for the Query/HistoricalInfo RPC + + method. default: description: An unexpected error response. schema: @@ -15005,210 +14849,54 @@ paths: "value": "1.212s" } parameters: - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. + - name: height + description: height defines at which height to query the historical info. in: path required: true type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean + format: int64 tags: - Query - /cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations: + /cosmos/staking/v1beta1/params: get: - summary: Redelegations queries redelegations of given address. - description: >- - When called from another module, this query might consume a high amount of - - gas if the pagination field is incorrectly set. - operationId: Redelegations + summary: Parameters queries the staking parameters. + operationId: StakingParams responses: '200': description: A successful response. schema: type: object properties: - redelegation_responses: - type: array - items: - type: object - properties: - redelegation: - type: object - properties: - delegator_address: - type: string - description: >- - delegator_address is the bech32-encoded address of the delegator. - validator_src_address: - type: string - description: >- - validator_src_address is the validator redelegation source operator address. - validator_dst_address: - type: string - description: >- - validator_dst_address is the validator redelegation destination operator address. - entries: - type: array - items: - type: object - properties: - creation_height: - type: string - format: int64 - description: >- - creation_height defines the height which the redelegation took place. - completion_time: - type: string - format: date-time - description: >- - completion_time defines the unix time for redelegation completion. - initial_balance: - type: string - description: >- - initial_balance defines the initial balance when redelegation started. - shares_dst: - type: string - description: >- - shares_dst is the amount of destination-validator shares created by redelegation. - unbonding_id: - type: string - format: uint64 - title: >- - Incrementing id that uniquely identifies this entry - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - Strictly positive if this entry's unbonding has been stopped by external modules - description: >- - RedelegationEntry defines a redelegation object with relevant metadata. - description: entries are the redelegation entries. - description: >- - Redelegation contains the list of a particular delegator's redelegating bonds - - from a particular source validator to a particular destination validator. - entries: - type: array - items: - type: object - properties: - redelegation_entry: - type: object - properties: - creation_height: - type: string - format: int64 - description: >- - creation_height defines the height which the redelegation took place. - completion_time: - type: string - format: date-time - description: >- - completion_time defines the unix time for redelegation completion. - initial_balance: - type: string - description: >- - initial_balance defines the initial balance when redelegation started. - shares_dst: - type: string - description: >- - shares_dst is the amount of destination-validator shares created by redelegation. - unbonding_id: - type: string - format: uint64 - title: >- - Incrementing id that uniquely identifies this entry - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - Strictly positive if this entry's unbonding has been stopped by external modules - description: >- - RedelegationEntry defines a redelegation object with relevant metadata. - balance: - type: string - description: >- - RedelegationEntryResponse is equivalent to a RedelegationEntry except that it - - contains a balance in addition to shares which is more suitable for client - - responses. - description: >- - RedelegationResponse is equivalent to a Redelegation except that its entries - - contain a balance in addition to shares which is more suitable for client - - responses. - pagination: - description: pagination defines the pagination in the response. + params: + description: params holds all the parameters of this module. type: object properties: - next_key: + unbonding_time: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + description: unbonding_time is the time duration of unbonding. + max_validators: + type: integer + format: int64 + description: max_validators is the maximum number of validators. + max_entries: + type: integer + format: int64 + description: >- + max_entries is the max entries for either unbonding delegation or redelegation (per pair/trio). + historical_entries: + type: integer + format: int64 + description: >- + historical_entries is the number of historical entries to persist. + bond_denom: + type: string + description: bond_denom defines the bondable coin denomination. + min_commission_rate: type: string - format: uint64 title: >- - total is total number of results available if PageRequest.count_total - - was set, its value is undefined otherwise + min_commission_rate is the chain-wide minimum commission rate that a validator can charge their delegators description: >- - QueryRedelegationsResponse is response type for the Query/Redelegations RPC - - method. + QueryParamsResponse is response type for the Query/Params RPC method. default: description: An unexpected error response. schema: @@ -15372,165 +15060,27 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - parameters: - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path - required: true - type: string - - name: src_validator_addr - description: src_validator_addr defines the validator address to redelegate from. - in: query - required: false - type: string - - name: dst_validator_addr - description: dst_validator_addr defines the validator address to redelegate to. - in: query - required: false - type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean tags: - Query - /cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations: + /cosmos/staking/v1beta1/pool: get: - summary: >- - DelegatorUnbondingDelegations queries all unbonding delegations of a given - - delegator address. - description: >- - When called from another module, this query might consume a high amount of - - gas if the pagination field is incorrectly set. - operationId: DelegatorUnbondingDelegations + summary: Pool queries the pool info. + operationId: Pool responses: '200': description: A successful response. schema: type: object properties: - unbonding_responses: - type: array - items: - type: object - properties: - delegator_address: - type: string - description: >- - delegator_address is the encoded address of the delegator. - validator_address: - type: string - description: >- - validator_address is the encoded address of the validator. - entries: - type: array - items: - type: object - properties: - creation_height: - type: string - format: int64 - description: >- - creation_height is the height which the unbonding took place. - completion_time: - type: string - format: date-time - description: >- - completion_time is the unix time for unbonding completion. - initial_balance: - type: string - description: >- - initial_balance defines the tokens initially scheduled to receive at completion. - balance: - type: string - description: >- - balance defines the tokens to receive at completion. - unbonding_id: - type: string - format: uint64 - title: >- - Incrementing id that uniquely identifies this entry - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - Strictly positive if this entry's unbonding has been stopped by external modules - description: >- - UnbondingDelegationEntry defines an unbonding object with relevant metadata. - description: entries are the unbonding delegation entries. - description: >- - UnbondingDelegation stores all of a single delegator's unbonding bonds - - for a single validator in an time-ordered list. - pagination: - description: pagination defines the pagination in the response. + pool: + description: pool defines the pool info. type: object properties: - next_key: + not_bonded_tokens: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + bonded_tokens: type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total - - was set, its value is undefined otherwise - description: >- - QueryUnbondingDelegatorDelegationsResponse is response type for the - - Query/UnbondingDelegatorDelegations RPC method. + description: QueryPoolResponse is response type for the Query/Pool RPC method. default: description: An unexpected error response. schema: @@ -15694,73 +15244,16 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - parameters: - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path - required: true - type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean tags: - Query - /cosmos/staking/v1beta1/delegators/{delegator_addr}/validators: + /cosmos/staking/v1beta1/validators: get: - summary: |- - DelegatorValidators queries all validators info for given delegator - address. + summary: Validators queries all validators that match the given status. description: >- When called from another module, this query might consume a high amount of gas if the pagination field is incorrectly set. - operationId: StakingDelegatorValidators + operationId: Validators responses: '200': description: A successful response. @@ -16040,7 +15533,7 @@ paths: exchange rate. Voting power can be calculated as total bonded shares multiplied by exchange rate. - description: validators defines the validators' info of a delegator. + description: validators contains all the queried validators. pagination: description: pagination defines the pagination in the response. type: object @@ -16059,9 +15552,8 @@ paths: total is total number of results available if PageRequest.count_total was set, its value is undefined otherwise - description: |- - QueryDelegatorValidatorsResponse is response type for the - Query/DelegatorValidators RPC method. + title: >- + QueryValidatorsResponse is response type for the Query/Validators RPC method default: description: An unexpected error response. schema: @@ -16226,10 +15718,10 @@ paths: "value": "1.212s" } parameters: - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path - required: true + - name: status + description: status enables to query for validators matching a given status. + in: query + required: false type: string - name: pagination.key description: |- @@ -16282,12 +15774,10 @@ paths: type: boolean tags: - Query - /cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}: + /cosmos/staking/v1beta1/validators/{validator_addr}: get: - summary: |- - DelegatorValidator queries validator info for given delegator validator - pair. - operationId: DelegatorValidator + summary: Validator queries validator info for given validator address. + operationId: Validator responses: '200': description: A successful response. @@ -16565,9 +16055,8 @@ paths: exchange rate. Voting power can be calculated as total bonded shares multiplied by exchange rate. - description: |- - QueryDelegatorValidatorResponse response type for the - Query/DelegatorValidator RPC method. + title: >- + QueryValidatorResponse is response type for the Query/Validator RPC method default: description: An unexpected error response. schema: @@ -16732,11 +16221,6 @@ paths: "value": "1.212s" } parameters: - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path - required: true - type: string - name: validator_addr description: validator_addr defines the validator address to query for. in: path @@ -16744,372 +16228,83 @@ paths: type: string tags: - Query - /cosmos/staking/v1beta1/historical_info/{height}: + /cosmos/staking/v1beta1/validators/{validator_addr}/delegations: get: - summary: HistoricalInfo queries the historical info for given height. - operationId: HistoricalInfo + summary: ValidatorDelegations queries delegate info for given validator. + description: >- + When called from another module, this query might consume a high amount of + + gas if the pagination field is incorrectly set. + operationId: ValidatorDelegations responses: '200': description: A successful response. schema: type: object properties: - hist: - description: hist defines the historical info at the given height. - type: object - properties: - header: - type: object - properties: - version: - title: basic block info - type: object - properties: - block: - type: string - format: uint64 - app: - type: string - format: uint64 - description: >- - Consensus captures the consensus rules for processing a block in the blockchain, - - including all blockchain data structures and the rules of the application's - - state transition machine. - chain_id: - type: string - height: - type: string - format: int64 - time: - type: string - format: date-time - last_block_id: - title: prev block info - type: object - properties: - hash: - type: string - format: byte - part_set_header: - type: object - properties: - total: - type: integer - format: int64 - hash: - type: string - format: byte - title: PartsetHeader - last_commit_hash: - type: string - format: byte - title: hashes of block data - data_hash: - type: string - format: byte - validators_hash: - type: string - format: byte - title: hashes from the app output from the prev block - next_validators_hash: - type: string - format: byte - consensus_hash: - type: string - format: byte - app_hash: - type: string - format: byte - last_results_hash: - type: string - format: byte - evidence_hash: - type: string - format: byte - title: consensus info - proposer_address: - type: string - format: byte - description: Header defines the structure of a block header. - valset: - type: array - items: + delegation_responses: + type: array + items: + type: object + properties: + delegation: type: object properties: - operator_address: - type: string - description: >- - operator_address defines the address of the validator's operator; bech encoded in JSON. - consensus_pubkey: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - jailed: - type: boolean - description: >- - jailed defined whether the validator has been jailed from bonded status or not. - status: - description: >- - status is the validator status (bonded/unbonding/unbonded). - type: string - enum: - - BOND_STATUS_UNSPECIFIED - - BOND_STATUS_UNBONDED - - BOND_STATUS_UNBONDING - - BOND_STATUS_BONDED - default: BOND_STATUS_UNSPECIFIED - tokens: - type: string - description: >- - tokens define the delegated tokens (incl. self-delegation). - delegator_shares: - type: string - description: >- - delegator_shares defines total shares issued to a validator's delegators. - description: - description: >- - description defines the description terms for the validator. - type: object - properties: - moniker: - type: string - description: >- - moniker defines a human-readable name for the validator. - identity: - type: string - description: >- - identity defines an optional identity signature (ex. UPort or Keybase). - website: - type: string - description: website defines an optional website link. - security_contact: - type: string - description: >- - security_contact defines an optional email for security contact. - details: - type: string - description: details define other optional details. - unbonding_height: - type: string - format: int64 - description: >- - unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. - unbonding_time: + delegator_address: type: string - format: date-time description: >- - unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. - commission: - description: commission defines the commission parameters. - type: object - properties: - commission_rates: - description: >- - commission_rates defines the initial commission rates to be used for creating a validator. - type: object - properties: - rate: - type: string - description: >- - rate is the commission rate charged to delegators, as a fraction. - max_rate: - type: string - description: >- - max_rate defines the maximum commission rate which validator can ever charge, as a fraction. - max_change_rate: - type: string - description: >- - max_change_rate defines the maximum daily increase of the validator commission, as a fraction. - update_time: - type: string - format: date-time - description: >- - update_time is the last time the commission rate was changed. - min_self_delegation: + delegator_address is the encoded address of the delegator. + validator_address: type: string description: >- - min_self_delegation is the validator's self declared minimum self delegation. - - Since: cosmos-sdk 0.46 - unbonding_on_hold_ref_count: + validator_address is the encoded address of the validator. + shares: type: string - format: int64 - title: >- - strictly positive if this validator's unbonding has been stopped by external modules - unbonding_ids: - type: array - items: - type: string - format: uint64 - title: >- - list of unbonding ids, each uniquely identifing an unbonding of this validator + description: shares define the delegation shares received. description: >- - Validator defines a validator, together with the total amount of the - - Validator's bond shares and their exchange rate to coins. Slashing results in - - a decrease in the exchange rate, allowing correct calculation of future + Delegation represents the bond with tokens held by an account. It is - undelegations without iterating over delegators. When coins are delegated to + owned by one delegator, and is associated with the voting power of one - this validator, the validator is credited with a delegation whose number of + validator. + balance: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. - bond shares is based on the amount of coins delegated divided by the current + NOTE: The amount field is an Int which implements the custom method - exchange rate. Voting power can be calculated as total bonded shares + signatures required by gogoproto. + description: >- + DelegationResponse is equivalent to Delegation except that it contains a - multiplied by exchange rate. - description: >- - QueryHistoricalInfoResponse is response type for the Query/HistoricalInfo RPC + balance in addition to shares which is more suitable for client responses. + pagination: + description: pagination defines the pagination in the response. + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if PageRequest.count_total - method. + was set, its value is undefined otherwise + title: |- + QueryValidatorDelegationsResponse is response type for the + Query/ValidatorDelegations RPC method default: description: An unexpected error response. schema: @@ -17274,54 +16469,114 @@ paths: "value": "1.212s" } parameters: - - name: height - description: height defines at which height to query the historical info. + - name: validator_addr + description: validator_addr defines the validator address to query for. in: path required: true type: string - format: int64 + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean tags: - Query - /cosmos/staking/v1beta1/params: + /cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}: get: - summary: Parameters queries the staking parameters. - operationId: StakingParams + summary: Delegation queries delegate info for given validator delegator pair. + operationId: Delegation responses: '200': description: A successful response. schema: type: object properties: - params: - description: params holds all the parameters of this module. + delegation_response: type: object properties: - unbonding_time: - type: string - description: unbonding_time is the time duration of unbonding. - max_validators: - type: integer - format: int64 - description: max_validators is the maximum number of validators. - max_entries: - type: integer - format: int64 + delegation: + type: object + properties: + delegator_address: + type: string + description: >- + delegator_address is the encoded address of the delegator. + validator_address: + type: string + description: >- + validator_address is the encoded address of the validator. + shares: + type: string + description: shares define the delegation shares received. description: >- - max_entries is the max entries for either unbonding delegation or redelegation (per pair/trio). - historical_entries: - type: integer - format: int64 + Delegation represents the bond with tokens held by an account. It is + + owned by one delegator, and is associated with the voting power of one + + validator. + balance: + type: object + properties: + denom: + type: string + amount: + type: string description: >- - historical_entries is the number of historical entries to persist. - bond_denom: - type: string - description: bond_denom defines the bondable coin denomination. - min_commission_rate: - type: string - title: >- - min_commission_rate is the chain-wide minimum commission rate that a validator can charge their delegators + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + + signatures required by gogoproto. + description: >- + DelegationResponse is equivalent to Delegation except that it contains a + + balance in addition to shares which is more suitable for client responses. description: >- - QueryParamsResponse is response type for the Query/Params RPC method. + QueryDelegationResponse is response type for the Query/Delegation RPC method. default: description: An unexpected error response. schema: @@ -17485,27 +16740,82 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } + parameters: + - name: validator_addr + description: validator_addr defines the validator address to query for. + in: path + required: true + type: string + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. + in: path + required: true + type: string tags: - Query - /cosmos/staking/v1beta1/pool: + /cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation: get: - summary: Pool queries the pool info. - operationId: Pool + summary: |- + UnbondingDelegation queries unbonding info for given validator delegator + pair. + operationId: UnbondingDelegation responses: '200': description: A successful response. schema: type: object properties: - pool: - description: pool defines the pool info. + unbond: type: object properties: - not_bonded_tokens: + delegator_address: type: string - bonded_tokens: + description: delegator_address is the encoded address of the delegator. + validator_address: type: string - description: QueryPoolResponse is response type for the Query/Pool RPC method. + description: validator_address is the encoded address of the validator. + entries: + type: array + items: + type: object + properties: + creation_height: + type: string + format: int64 + description: >- + creation_height is the height which the unbonding took place. + completion_time: + type: string + format: date-time + description: >- + completion_time is the unix time for unbonding completion. + initial_balance: + type: string + description: >- + initial_balance defines the tokens initially scheduled to receive at completion. + balance: + type: string + description: balance defines the tokens to receive at completion. + unbonding_id: + type: string + format: uint64 + title: Incrementing id that uniquely identifies this entry + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + Strictly positive if this entry's unbonding has been stopped by external modules + description: >- + UnbondingDelegationEntry defines an unbonding object with relevant metadata. + description: entries are the unbonding delegation entries. + description: >- + UnbondingDelegation stores all of a single delegator's unbonding bonds + + for a single validator in an time-ordered list. + description: >- + QueryDelegationResponse is response type for the Query/UnbondingDelegation + + RPC method. default: description: An unexpected error response. schema: @@ -17669,296 +16979,87 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } + parameters: + - name: validator_addr + description: validator_addr defines the validator address to query for. + in: path + required: true + type: string + - name: delegator_addr + description: delegator_addr defines the delegator address to query for. + in: path + required: true + type: string tags: - Query - /cosmos/staking/v1beta1/validators: + /cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations: get: - summary: Validators queries all validators that match the given status. + summary: >- + ValidatorUnbondingDelegations queries unbonding delegations of a validator. description: >- When called from another module, this query might consume a high amount of gas if the pagination field is incorrectly set. - operationId: Validators + operationId: ValidatorUnbondingDelegations responses: '200': description: A successful response. schema: type: object properties: - validators: + unbonding_responses: type: array items: type: object properties: - operator_address: - type: string - description: >- - operator_address defines the address of the validator's operator; bech encoded in JSON. - consensus_pubkey: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - jailed: - type: boolean - description: >- - jailed defined whether the validator has been jailed from bonded status or not. - status: - description: >- - status is the validator status (bonded/unbonding/unbonded). - type: string - enum: - - BOND_STATUS_UNSPECIFIED - - BOND_STATUS_UNBONDED - - BOND_STATUS_UNBONDING - - BOND_STATUS_BONDED - default: BOND_STATUS_UNSPECIFIED - tokens: - type: string - description: >- - tokens define the delegated tokens (incl. self-delegation). - delegator_shares: - type: string - description: >- - delegator_shares defines total shares issued to a validator's delegators. - description: - description: >- - description defines the description terms for the validator. - type: object - properties: - moniker: - type: string - description: >- - moniker defines a human-readable name for the validator. - identity: - type: string - description: >- - identity defines an optional identity signature (ex. UPort or Keybase). - website: - type: string - description: website defines an optional website link. - security_contact: - type: string - description: >- - security_contact defines an optional email for security contact. - details: - type: string - description: details define other optional details. - unbonding_height: - type: string - format: int64 - description: >- - unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. - unbonding_time: + delegator_address: type: string - format: date-time description: >- - unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. - commission: - description: commission defines the commission parameters. - type: object - properties: - commission_rates: - description: >- - commission_rates defines the initial commission rates to be used for creating a validator. - type: object - properties: - rate: - type: string - description: >- - rate is the commission rate charged to delegators, as a fraction. - max_rate: - type: string - description: >- - max_rate defines the maximum commission rate which validator can ever charge, as a fraction. - max_change_rate: - type: string - description: >- - max_change_rate defines the maximum daily increase of the validator commission, as a fraction. - update_time: - type: string - format: date-time - description: >- - update_time is the last time the commission rate was changed. - min_self_delegation: + delegator_address is the encoded address of the delegator. + validator_address: type: string description: >- - min_self_delegation is the validator's self declared minimum self delegation. - - Since: cosmos-sdk 0.46 - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - strictly positive if this validator's unbonding has been stopped by external modules - unbonding_ids: + validator_address is the encoded address of the validator. + entries: type: array items: - type: string - format: uint64 - title: >- - list of unbonding ids, each uniquely identifing an unbonding of this validator + type: object + properties: + creation_height: + type: string + format: int64 + description: >- + creation_height is the height which the unbonding took place. + completion_time: + type: string + format: date-time + description: >- + completion_time is the unix time for unbonding completion. + initial_balance: + type: string + description: >- + initial_balance defines the tokens initially scheduled to receive at completion. + balance: + type: string + description: >- + balance defines the tokens to receive at completion. + unbonding_id: + type: string + format: uint64 + title: >- + Incrementing id that uniquely identifies this entry + unbonding_on_hold_ref_count: + type: string + format: int64 + title: >- + Strictly positive if this entry's unbonding has been stopped by external modules + description: >- + UnbondingDelegationEntry defines an unbonding object with relevant metadata. + description: entries are the unbonding delegation entries. description: >- - Validator defines a validator, together with the total amount of the - - Validator's bond shares and their exchange rate to coins. Slashing results in - - a decrease in the exchange rate, allowing correct calculation of future - - undelegations without iterating over delegators. When coins are delegated to - - this validator, the validator is credited with a delegation whose number of - - bond shares is based on the amount of coins delegated divided by the current - - exchange rate. Voting power can be calculated as total bonded shares + UnbondingDelegation stores all of a single delegator's unbonding bonds - multiplied by exchange rate. - description: validators contains all the queried validators. + for a single validator in an time-ordered list. pagination: description: pagination defines the pagination in the response. type: object @@ -17977,8 +17078,10 @@ paths: total is total number of results available if PageRequest.count_total was set, its value is undefined otherwise - title: >- - QueryValidatorsResponse is response type for the Query/Validators RPC method + description: >- + QueryValidatorUnbondingDelegationsResponse is response type for the + + Query/ValidatorUnbondingDelegations RPC method. default: description: An unexpected error response. schema: @@ -18143,10 +17246,10 @@ paths: "value": "1.212s" } parameters: - - name: status - description: status enables to query for validators matching a given status. - in: query - required: false + - name: validator_addr + description: validator_addr defines the validator address to query for. + in: path + required: true type: string - name: pagination.key description: |- @@ -18199,289 +17302,16 @@ paths: type: boolean tags: - Query - /cosmos/staking/v1beta1/validators/{validator_addr}: - get: - summary: Validator queries validator info for given validator address. - operationId: Validator + /cosmos/tx/v1beta1/decode: + post: + summary: TxDecode decodes the transaction. + description: 'Since: cosmos-sdk 0.47' + operationId: TxDecode responses: '200': description: A successful response. schema: - type: object - properties: - validator: - type: object - properties: - operator_address: - type: string - description: >- - operator_address defines the address of the validator's operator; bech encoded in JSON. - consensus_pubkey: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - jailed: - type: boolean - description: >- - jailed defined whether the validator has been jailed from bonded status or not. - status: - description: >- - status is the validator status (bonded/unbonding/unbonded). - type: string - enum: - - BOND_STATUS_UNSPECIFIED - - BOND_STATUS_UNBONDED - - BOND_STATUS_UNBONDING - - BOND_STATUS_BONDED - default: BOND_STATUS_UNSPECIFIED - tokens: - type: string - description: >- - tokens define the delegated tokens (incl. self-delegation). - delegator_shares: - type: string - description: >- - delegator_shares defines total shares issued to a validator's delegators. - description: - description: >- - description defines the description terms for the validator. - type: object - properties: - moniker: - type: string - description: >- - moniker defines a human-readable name for the validator. - identity: - type: string - description: >- - identity defines an optional identity signature (ex. UPort or Keybase). - website: - type: string - description: website defines an optional website link. - security_contact: - type: string - description: >- - security_contact defines an optional email for security contact. - details: - type: string - description: details define other optional details. - unbonding_height: - type: string - format: int64 - description: >- - unbonding_height defines, if unbonding, the height at which this validator has begun unbonding. - unbonding_time: - type: string - format: date-time - description: >- - unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. - commission: - description: commission defines the commission parameters. - type: object - properties: - commission_rates: - description: >- - commission_rates defines the initial commission rates to be used for creating a validator. - type: object - properties: - rate: - type: string - description: >- - rate is the commission rate charged to delegators, as a fraction. - max_rate: - type: string - description: >- - max_rate defines the maximum commission rate which validator can ever charge, as a fraction. - max_change_rate: - type: string - description: >- - max_change_rate defines the maximum daily increase of the validator commission, as a fraction. - update_time: - type: string - format: date-time - description: >- - update_time is the last time the commission rate was changed. - min_self_delegation: - type: string - description: >- - min_self_delegation is the validator's self declared minimum self delegation. - - Since: cosmos-sdk 0.46 - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - strictly positive if this validator's unbonding has been stopped by external modules - unbonding_ids: - type: array - items: - type: string - format: uint64 - title: >- - list of unbonding ids, each uniquely identifing an unbonding of this validator - description: >- - Validator defines a validator, together with the total amount of the - - Validator's bond shares and their exchange rate to coins. Slashing results in - - a decrease in the exchange rate, allowing correct calculation of future - - undelegations without iterating over delegators. When coins are delegated to - - this validator, the validator is credited with a delegation whose number of - - bond shares is based on the amount of coins delegated divided by the current - - exchange rate. Voting power can be calculated as total bonded shares - - multiplied by exchange rate. - title: >- - QueryValidatorResponse is response type for the Query/Validator RPC method + $ref: '#/definitions/cosmos.tx.v1beta1.TxDecodeResponse' default: description: An unexpected error response. schema: @@ -18646,90 +17476,42 @@ paths: "value": "1.212s" } parameters: - - name: validator_addr - description: validator_addr defines the validator address to query for. - in: path + - name: body + in: body required: true - type: string - tags: - - Query - /cosmos/staking/v1beta1/validators/{validator_addr}/delegations: - get: - summary: ValidatorDelegations queries delegate info for given validator. - description: >- - When called from another module, this query might consume a high amount of + schema: + type: object + properties: + tx_bytes: + type: string + format: byte + description: tx_bytes is the raw transaction. + description: |- + TxDecodeRequest is the request type for the Service.TxDecode + RPC method. - gas if the pagination field is incorrectly set. - operationId: ValidatorDelegations + Since: cosmos-sdk 0.47 + tags: + - Service + /cosmos/tx/v1beta1/decode/amino: + post: + summary: TxDecodeAmino decodes an Amino transaction from encoded bytes to JSON. + description: 'Since: cosmos-sdk 0.47' + operationId: TxDecodeAmino responses: '200': description: A successful response. schema: type: object properties: - delegation_responses: - type: array - items: - type: object - properties: - delegation: - type: object - properties: - delegator_address: - type: string - description: >- - delegator_address is the encoded address of the delegator. - validator_address: - type: string - description: >- - validator_address is the encoded address of the validator. - shares: - type: string - description: shares define the delegation shares received. - description: >- - Delegation represents the bond with tokens held by an account. It is - - owned by one delegator, and is associated with the voting power of one - - validator. - balance: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: >- - DelegationResponse is equivalent to Delegation except that it contains a + amino_json: + type: string + description: >- + TxDecodeAminoResponse is the response type for the Service.TxDecodeAmino - balance in addition to shares which is more suitable for client responses. - pagination: - description: pagination defines the pagination in the response. - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if PageRequest.count_total + RPC method. - was set, its value is undefined otherwise - title: |- - QueryValidatorDelegationsResponse is response type for the - Query/ValidatorDelegations RPC method + Since: cosmos-sdk 0.47 default: description: An unexpected error response. schema: @@ -18894,114 +17676,43 @@ paths: "value": "1.212s" } parameters: - - name: validator_addr - description: validator_addr defines the validator address to query for. - in: path + - name: body + in: body required: true - type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. + schema: + type: object + properties: + amino_binary: + type: string + format: byte + description: >- + TxDecodeAminoRequest is the request type for the Service.TxDecodeAmino - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean - tags: - - Query - /cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}: - get: - summary: Delegation queries delegate info for given validator delegator pair. - operationId: Delegation + RPC method. + + Since: cosmos-sdk 0.47 + tags: + - Service + /cosmos/tx/v1beta1/encode: + post: + summary: TxEncode encodes the transaction. + description: 'Since: cosmos-sdk 0.47' + operationId: TxEncode responses: '200': description: A successful response. schema: type: object properties: - delegation_response: - type: object - properties: - delegation: - type: object - properties: - delegator_address: - type: string - description: >- - delegator_address is the encoded address of the delegator. - validator_address: - type: string - description: >- - validator_address is the encoded address of the validator. - shares: - type: string - description: shares define the delegation shares received. - description: >- - Delegation represents the bond with tokens held by an account. It is - - owned by one delegator, and is associated with the voting power of one - - validator. - balance: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - - signatures required by gogoproto. - description: >- - DelegationResponse is equivalent to Delegation except that it contains a + tx_bytes: + type: string + format: byte + description: tx_bytes is the encoded transaction bytes. + description: |- + TxEncodeResponse is the response type for the + Service.TxEncode method. - balance in addition to shares which is more suitable for client responses. - description: >- - QueryDelegationResponse is response type for the Query/Delegation RPC method. + Since: cosmos-sdk 0.47 default: description: An unexpected error response. schema: @@ -19166,81 +17877,33 @@ paths: "value": "1.212s" } parameters: - - name: validator_addr - description: validator_addr defines the validator address to query for. - in: path - required: true - type: string - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path + - name: body + in: body required: true - type: string + schema: + $ref: '#/definitions/cosmos.tx.v1beta1.TxEncodeRequest' tags: - - Query - /cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation: - get: - summary: |- - UnbondingDelegation queries unbonding info for given validator delegator - pair. - operationId: UnbondingDelegation + - Service + /cosmos/tx/v1beta1/encode/amino: + post: + summary: TxEncodeAmino encodes an Amino transaction from JSON to encoded bytes. + description: 'Since: cosmos-sdk 0.47' + operationId: TxEncodeAmino responses: '200': description: A successful response. schema: type: object properties: - unbond: - type: object - properties: - delegator_address: - type: string - description: delegator_address is the encoded address of the delegator. - validator_address: - type: string - description: validator_address is the encoded address of the validator. - entries: - type: array - items: - type: object - properties: - creation_height: - type: string - format: int64 - description: >- - creation_height is the height which the unbonding took place. - completion_time: - type: string - format: date-time - description: >- - completion_time is the unix time for unbonding completion. - initial_balance: - type: string - description: >- - initial_balance defines the tokens initially scheduled to receive at completion. - balance: - type: string - description: balance defines the tokens to receive at completion. - unbonding_id: - type: string - format: uint64 - title: Incrementing id that uniquely identifies this entry - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - Strictly positive if this entry's unbonding has been stopped by external modules - description: >- - UnbondingDelegationEntry defines an unbonding object with relevant metadata. - description: entries are the unbonding delegation entries. - description: >- - UnbondingDelegation stores all of a single delegator's unbonding bonds - - for a single validator in an time-ordered list. + amino_binary: + type: string + format: byte description: >- - QueryDelegationResponse is response type for the Query/UnbondingDelegation + TxEncodeAminoResponse is the response type for the Service.TxEncodeAmino RPC method. + + Since: cosmos-sdk 0.47 default: description: An unexpected error response. schema: @@ -19405,108 +18068,252 @@ paths: "value": "1.212s" } parameters: - - name: validator_addr - description: validator_addr defines the validator address to query for. - in: path - required: true - type: string - - name: delegator_addr - description: delegator_addr defines the delegator address to query for. - in: path + - name: body + in: body required: true - type: string - tags: - - Query - /cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations: - get: - summary: >- - ValidatorUnbondingDelegations queries unbonding delegations of a validator. - description: >- - When called from another module, this query might consume a high amount of + schema: + type: object + properties: + amino_json: + type: string + description: >- + TxEncodeAminoRequest is the request type for the Service.TxEncodeAmino - gas if the pagination field is incorrectly set. - operationId: ValidatorUnbondingDelegations + RPC method. + + Since: cosmos-sdk 0.47 + tags: + - Service + /cosmos/tx/v1beta1/simulate: + post: + summary: Simulate simulates executing a transaction for estimating gas usage. + operationId: Simulate responses: '200': description: A successful response. schema: type: object properties: - unbonding_responses: - type: array - items: - type: object - properties: - delegator_address: - type: string - description: >- - delegator_address is the encoded address of the delegator. - validator_address: - type: string - description: >- - validator_address is the encoded address of the validator. - entries: - type: array - items: - type: object - properties: - creation_height: - type: string - format: int64 - description: >- - creation_height is the height which the unbonding took place. - completion_time: - type: string - format: date-time - description: >- - completion_time is the unix time for unbonding completion. - initial_balance: - type: string - description: >- - initial_balance defines the tokens initially scheduled to receive at completion. - balance: - type: string - description: >- - balance defines the tokens to receive at completion. - unbonding_id: - type: string - format: uint64 - title: >- - Incrementing id that uniquely identifies this entry - unbonding_on_hold_ref_count: - type: string - format: int64 - title: >- - Strictly positive if this entry's unbonding has been stopped by external modules - description: >- - UnbondingDelegationEntry defines an unbonding object with relevant metadata. - description: entries are the unbonding delegation entries. - description: >- - UnbondingDelegation stores all of a single delegator's unbonding bonds - - for a single validator in an time-ordered list. - pagination: - description: pagination defines the pagination in the response. + gas_info: + description: gas_info is the information about gas used in the simulation. type: object properties: - next_key: + gas_wanted: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + format: uint64 + description: >- + GasWanted is the maximum units of work we allow this tx to perform. + gas_used: type: string format: uint64 - title: >- - total is total number of results available if PageRequest.count_total + description: GasUsed is the amount of gas actually consumed. + result: + description: result is the result of the simulation. + type: object + properties: + data: + type: string + format: byte + description: >- + Data is any data returned from message or handler execution. It MUST be - was set, its value is undefined otherwise - description: >- - QueryValidatorUnbondingDelegationsResponse is response type for the + length prefixed in order to separate data from multiple message executions. - Query/ValidatorUnbondingDelegations RPC method. + Deprecated. This field is still populated, but prefer msg_response instead + + because it also contains the Msg response typeURL. + log: + type: string + description: >- + Log contains the log information from message or handler execution. + events: + type: array + items: + type: object + properties: + type: + type: string + attributes: + type: array + items: + type: object + properties: + key: + type: string + value: + type: string + index: + type: boolean + description: >- + EventAttribute is a single key-value pair, associated with an event. + description: >- + Event allows application developers to attach additional information to + + ResponseFinalizeBlock and ResponseCheckTx. + + Later, transactions may be queried using these events. + description: >- + Events contains a slice of Event objects that were emitted during message + + or handler execution. + msg_responses: + type: array + items: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a canonical form + + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + + expect it to use in the context of Any. However, for URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally set up a type + + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a + + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + JSON + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + msg_responses contains the Msg handler responses type packed in Anys. + + Since: cosmos-sdk 0.46 + description: |- + SimulateResponse is the response type for the + Service.SimulateRPC method. default: description: An unexpected error response. schema: @@ -19671,72 +18478,22 @@ paths: "value": "1.212s" } parameters: - - name: validator_addr - description: validator_addr defines the validator address to query for. - in: path + - name: body + in: body required: true - type: string - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean + schema: + $ref: '#/definitions/cosmos.tx.v1beta1.SimulateRequest' tags: - - Query - /cosmos/tx/v1beta1/decode: - post: - summary: TxDecode decodes the transaction. - description: 'Since: cosmos-sdk 0.47' - operationId: TxDecode + - Service + /cosmos/tx/v1beta1/txs: + get: + summary: GetTxsEvent fetches txs by event. + operationId: GetTxsEvent responses: '200': description: A successful response. schema: - $ref: '#/definitions/cosmos.tx.v1beta1.TxDecodeResponse' + $ref: '#/definitions/cosmos.tx.v1beta1.GetTxsEventResponse' default: description: An unexpected error response. schema: @@ -19901,42 +18658,399 @@ paths: "value": "1.212s" } parameters: - - name: body - in: body - required: true - schema: - type: object - properties: - tx_bytes: - type: string - format: byte - description: tx_bytes is the raw transaction. - description: |- - TxDecodeRequest is the request type for the Service.TxDecode - RPC method. + - name: events + description: >- + events is the list of transaction event type. - Since: cosmos-sdk 0.47 + Deprecated post v0.47.x: use query instead, which should contain a valid + + events query. + in: query + required: false + type: array + items: + type: string + collectionFormat: multi + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include + + a count of the total number of items available for pagination in UIs. + + count_total is only respected when offset is used. It is ignored when key + + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. + + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean + - name: order_by + description: |2- + - ORDER_BY_UNSPECIFIED: ORDER_BY_UNSPECIFIED specifies an unknown sorting order. OrderBy defaults + to ASC in this case. + - ORDER_BY_ASC: ORDER_BY_ASC defines ascending order + - ORDER_BY_DESC: ORDER_BY_DESC defines descending order + in: query + required: false + type: string + enum: + - ORDER_BY_UNSPECIFIED + - ORDER_BY_ASC + - ORDER_BY_DESC + default: ORDER_BY_UNSPECIFIED + - name: page + description: |- + page is the page number to query, starts at 1. If not provided, will + default to first page. + in: query + required: false + type: string + format: uint64 + - name: limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: query + description: >- + query defines the transaction event query that is proxied to Tendermint's + + TxSearch RPC method. The query must be valid. + + Since cosmos-sdk 0.50 + in: query + required: false + type: string tags: - Service - /cosmos/tx/v1beta1/decode/amino: post: - summary: TxDecodeAmino decodes an Amino transaction from encoded bytes to JSON. - description: 'Since: cosmos-sdk 0.47' - operationId: TxDecodeAmino + summary: BroadcastTx broadcast transaction. + operationId: BroadcastTx responses: '200': description: A successful response. schema: type: object properties: - amino_json: - type: string - description: >- - TxDecodeAminoResponse is the response type for the Service.TxDecodeAmino + tx_response: + type: object + properties: + height: + type: string + format: int64 + title: The block height + txhash: + type: string + description: The transaction hash. + codespace: + type: string + title: Namespace for the Code + code: + type: integer + format: int64 + description: Response code. + data: + type: string + description: Result bytes, if any. + raw_log: + type: string + description: >- + The output of the application's logger (raw string). May be - RPC method. + non-deterministic. + logs: + type: array + items: + type: object + properties: + msg_index: + type: integer + format: int64 + log: + type: string + events: + type: array + items: + type: object + properties: + type: + type: string + attributes: + type: array + items: + type: object + properties: + key: + type: string + value: + type: string + description: >- + Attribute defines an attribute wrapper where the key and value are - Since: cosmos-sdk 0.47 + strings instead of raw bytes. + description: >- + StringEvent defines en Event object wrapper where all the attributes + + contain key/value pairs that are strings instead of raw bytes. + description: >- + Events contains a slice of Event objects that were emitted during some + + execution. + description: >- + ABCIMessageLog defines a structure containing an indexed tx ABCI message log. + description: >- + The output of the application's logger (typed). May be non-deterministic. + info: + type: string + description: Additional information. May be non-deterministic. + gas_wanted: + type: string + format: int64 + description: Amount of gas requested for transaction. + gas_used: + type: string + format: int64 + description: Amount of gas consumed by transaction. + tx: + type: object + properties: + type_url: + type: string + description: >- + A URL/resource name that uniquely identifies the type of the serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a canonical form + + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + + expect it to use in the context of Any. However, for URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally set up a type + + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + value: + type: string + format: byte + description: >- + Must be a valid serialized protocol buffer of the above specified type. + description: >- + `Any` contains an arbitrary serialized protocol buffer message along with a + + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + JSON + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + timestamp: + type: string + description: >- + Time of the previous block. For heights > 1, it's the weighted median of + + the timestamps of the valid votes in the block.LastCommit. For height == 1, + + it's genesis time. + events: + type: array + items: + type: object + properties: + type: + type: string + attributes: + type: array + items: + type: object + properties: + key: + type: string + value: + type: string + index: + type: boolean + description: >- + EventAttribute is a single key-value pair, associated with an event. + description: >- + Event allows application developers to attach additional information to + + ResponseFinalizeBlock and ResponseCheckTx. + + Later, transactions may be queried using these events. + description: >- + Events defines all the events emitted by processing a transaction. Note, + + these events include those emitted by processing all the messages and those + + emitted from the ante. Whereas Logs contains the events, with + + additional metadata, emitted only by processing the messages. + + Since: cosmos-sdk 0.42.11, 0.44.5, 0.45 + description: >- + TxResponse defines a structure containing relevant tx data and metadata. The + + tags are stringified and the log is JSON decoded. + description: |- + BroadcastTxResponse is the response type for the + Service.BroadcastTx method. default: description: An unexpected error response. schema: @@ -20107,37 +19221,48 @@ paths: schema: type: object properties: - amino_binary: + tx_bytes: type: string format: byte + description: tx_bytes is the raw transaction. + mode: + type: string + enum: + - BROADCAST_MODE_UNSPECIFIED + - BROADCAST_MODE_BLOCK + - BROADCAST_MODE_SYNC + - BROADCAST_MODE_ASYNC + default: BROADCAST_MODE_UNSPECIFIED + description: >- + BroadcastMode specifies the broadcast mode for the TxService.Broadcast RPC + + method. + + - BROADCAST_MODE_UNSPECIFIED: zero-value for mode ordering + - BROADCAST_MODE_BLOCK: DEPRECATED: use BROADCAST_MODE_SYNC instead, + BROADCAST_MODE_BLOCK is not supported by the SDK from v0.47.x onwards. + + - BROADCAST_MODE_SYNC: BROADCAST_MODE_SYNC defines a tx broadcasting mode where the client waits + for a CheckTx execution response only. + + - BROADCAST_MODE_ASYNC: BROADCAST_MODE_ASYNC defines a tx broadcasting mode where the client + returns immediately. description: >- - TxDecodeAminoRequest is the request type for the Service.TxDecodeAmino + BroadcastTxRequest is the request type for the Service.BroadcastTxRequest RPC method. - - Since: cosmos-sdk 0.47 tags: - Service - /cosmos/tx/v1beta1/encode: - post: - summary: TxEncode encodes the transaction. - description: 'Since: cosmos-sdk 0.47' - operationId: TxEncode + /cosmos/tx/v1beta1/txs/block/{height}: + get: + summary: GetBlockWithTxs fetches a block with decoded txs. + description: 'Since: cosmos-sdk 0.45.2' + operationId: GetBlockWithTxs responses: '200': description: A successful response. schema: - type: object - properties: - tx_bytes: - type: string - format: byte - description: tx_bytes is the encoded transaction bytes. - description: |- - TxEncodeResponse is the response type for the - Service.TxEncode method. - - Since: cosmos-sdk 0.47 + $ref: '#/definitions/cosmos.tx.v1beta1.GetBlockWithTxsResponse' default: description: An unexpected error response. schema: @@ -20289,46 +19414,85 @@ paths: "lastName": } - If the embedded message type is well-known and has a custom JSON + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + parameters: + - name: height + description: height is the height of the block to query. + in: path + required: true + type: string + format: int64 + - name: pagination.key + description: |- + key is a value returned in PageResponse.next_key to begin + querying the next page most efficiently. Only one of offset or key + should be set. + in: query + required: false + type: string + format: byte + - name: pagination.offset + description: >- + offset is a numeric offset that can be used when key is unavailable. + + It is less efficient than using key. Only one of offset or key should + + be set. + in: query + required: false + type: string + format: uint64 + - name: pagination.limit + description: >- + limit is the total number of results to be returned in the result page. + + If left empty it will default to a value to be set by each app. + in: query + required: false + type: string + format: uint64 + - name: pagination.count_total + description: >- + count_total is set to true to indicate that the result set should include - representation, that representation will be embedded adding a field + a count of the total number of items available for pagination in UIs. - `value` which holds the custom JSON in addition to the `@type` + count_total is only respected when offset is used. It is ignored when key - field. Example (for message [google.protobuf.Duration][]): + is set. + in: query + required: false + type: boolean + - name: pagination.reverse + description: >- + reverse is set to true if results are to be returned in the descending order. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - parameters: - - name: body - in: body - required: true - schema: - $ref: '#/definitions/cosmos.tx.v1beta1.TxEncodeRequest' + Since: cosmos-sdk 0.43 + in: query + required: false + type: boolean tags: - Service - /cosmos/tx/v1beta1/encode/amino: - post: - summary: TxEncodeAmino encodes an Amino transaction from JSON to encoded bytes. - description: 'Since: cosmos-sdk 0.47' - operationId: TxEncodeAmino + /cosmos/tx/v1beta1/txs/{hash}: + get: + summary: GetTx fetches a tx by hash. + operationId: GetTx responses: '200': description: A successful response. schema: - type: object - properties: - amino_binary: - type: string - format: byte - description: >- - TxEncodeAminoResponse is the response type for the Service.TxEncodeAmino - - RPC method. - - Since: cosmos-sdk 0.47 + $ref: '#/definitions/cosmos.tx.v1beta1.GetTxResponse' default: description: An unexpected error response. schema: @@ -20493,252 +19657,31 @@ paths: "value": "1.212s" } parameters: - - name: body - in: body + - name: hash + description: hash is the tx hash to query, encoded as a hex string. + in: path required: true - schema: - type: object - properties: - amino_json: - type: string - description: >- - TxEncodeAminoRequest is the request type for the Service.TxEncodeAmino - - RPC method. - - Since: cosmos-sdk 0.47 + type: string tags: - Service - /cosmos/tx/v1beta1/simulate: - post: - summary: Simulate simulates executing a transaction for estimating gas usage. - operationId: Simulate + /cosmos/upgrade/v1beta1/applied_plan/{name}: + get: + summary: AppliedPlan queries a previously applied upgrade plan by its name. + operationId: AppliedPlan responses: '200': description: A successful response. schema: type: object properties: - gas_info: - description: gas_info is the information about gas used in the simulation. - type: object - properties: - gas_wanted: - type: string - format: uint64 - description: >- - GasWanted is the maximum units of work we allow this tx to perform. - gas_used: - type: string - format: uint64 - description: GasUsed is the amount of gas actually consumed. - result: - description: result is the result of the simulation. - type: object - properties: - data: - type: string - format: byte - description: >- - Data is any data returned from message or handler execution. It MUST be - - length prefixed in order to separate data from multiple message executions. - - Deprecated. This field is still populated, but prefer msg_response instead - - because it also contains the Msg response typeURL. - log: - type: string - description: >- - Log contains the log information from message or handler execution. - events: - type: array - items: - type: object - properties: - type: - type: string - attributes: - type: array - items: - type: object - properties: - key: - type: string - value: - type: string - index: - type: boolean - description: >- - EventAttribute is a single key-value pair, associated with an event. - description: >- - Event allows application developers to attach additional information to - - ResponseFinalizeBlock and ResponseCheckTx. - - Later, transactions may be queried using these events. - description: >- - Events contains a slice of Event objects that were emitted during message - - or handler execution. - msg_responses: - type: array - items: - type: object - properties: - type_url: - type: string - description: >- - A URL/resource name that uniquely identifies the type of the serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a canonical form - - (e.g., leading "." is not accepted). - - In practice, teams usually precompile into the binary all types that they - - expect it to use in the context of Any. However, for URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally set up a type - - server that maps type URLs to message definitions as follows: - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - value: - type: string - format: byte - description: >- - Must be a valid serialized protocol buffer of the above specified type. - description: >- - `Any` contains an arbitrary serialized protocol buffer message along with a - - URL that describes the type of the serialized message. - - Protobuf library provides support to pack/unpack Any values in the form - - of utility functions or additional generated methods of the Any type. - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - // or ... - if (any.isSameTypeAs(Foo.getDefaultInstance())) { - foo = any.unpack(Foo.getDefaultInstance()); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - JSON - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - msg_responses contains the Msg handler responses type packed in Anys. + height: + type: string + format: int64 + description: height is the block height at which the plan was applied. + description: >- + QueryAppliedPlanResponse is the response type for the Query/AppliedPlan RPC - Since: cosmos-sdk 0.46 - description: |- - SimulateResponse is the response type for the - Service.SimulateRPC method. + method. default: description: An unexpected error response. schema: @@ -20903,22 +19846,28 @@ paths: "value": "1.212s" } parameters: - - name: body - in: body + - name: name + description: name is the name of the applied plan to query for. + in: path required: true - schema: - $ref: '#/definitions/cosmos.tx.v1beta1.SimulateRequest' + type: string tags: - - Service - /cosmos/tx/v1beta1/txs: + - Query + /cosmos/upgrade/v1beta1/authority: get: - summary: GetTxsEvent fetches txs by event. - operationId: GetTxsEvent + summary: Returns the account with authority to conduct upgrades + description: 'Since: cosmos-sdk 0.46' + operationId: Authority responses: '200': description: A successful response. schema: - $ref: '#/definitions/cosmos.tx.v1beta1.GetTxsEventResponse' + type: object + properties: + address: + type: string + description: 'Since: cosmos-sdk 0.46' + title: QueryAuthorityResponse is the response type for Query/Authority default: description: An unexpected error response. schema: @@ -21082,201 +20031,58 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - parameters: - - name: events - description: >- - events is the list of transaction event type. - - Deprecated post v0.47.x: use query instead, which should contain a valid - - events query. - in: query - required: false - type: array - items: - type: string - collectionFormat: multi - - name: pagination.key - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - in: query - required: false - type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. - - It is less efficient than using key. Only one of offset or key should - - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include - - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean - - name: order_by - description: |2- - - ORDER_BY_UNSPECIFIED: ORDER_BY_UNSPECIFIED specifies an unknown sorting order. OrderBy defaults - to ASC in this case. - - ORDER_BY_ASC: ORDER_BY_ASC defines ascending order - - ORDER_BY_DESC: ORDER_BY_DESC defines descending order - in: query - required: false - type: string - enum: - - ORDER_BY_UNSPECIFIED - - ORDER_BY_ASC - - ORDER_BY_DESC - default: ORDER_BY_UNSPECIFIED - - name: page - description: |- - page is the page number to query, starts at 1. If not provided, will - default to first page. - in: query - required: false - type: string - format: uint64 - - name: limit - description: >- - limit is the total number of results to be returned in the result page. - - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: query - description: >- - query defines the transaction event query that is proxied to Tendermint's - - TxSearch RPC method. The query must be valid. - - Since cosmos-sdk 0.50 - in: query - required: false - type: string tags: - - Service - post: - summary: BroadcastTx broadcast transaction. - operationId: BroadcastTx + - Query + /cosmos/upgrade/v1beta1/current_plan: + get: + summary: CurrentPlan queries the current upgrade plan. + operationId: CurrentPlan responses: '200': description: A successful response. schema: type: object properties: - tx_response: + plan: + description: plan is the current upgrade plan. type: object properties: - height: - type: string - format: int64 - title: The block height - txhash: - type: string - description: The transaction hash. - codespace: - type: string - title: Namespace for the Code - code: - type: integer - format: int64 - description: Response code. - data: - type: string - description: Result bytes, if any. - raw_log: + name: type: string description: >- - The output of the application's logger (raw string). May be + Sets the name for the upgrade. This name will be used by the upgraded - non-deterministic. - logs: - type: array - items: - type: object - properties: - msg_index: - type: integer - format: int64 - log: - type: string - events: - type: array - items: - type: object - properties: - type: - type: string - attributes: - type: array - items: - type: object - properties: - key: - type: string - value: - type: string - description: >- - Attribute defines an attribute wrapper where the key and value are + version of the software to apply any special "on-upgrade" commands during - strings instead of raw bytes. - description: >- - StringEvent defines en Event object wrapper where all the attributes + the first BeginBlock method after the upgrade is applied. It is also used - contain key/value pairs that are strings instead of raw bytes. - description: >- - Events contains a slice of Event objects that were emitted during some + to detect whether a software version can handle a given upgrade. If no - execution. - description: >- - ABCIMessageLog defines a structure containing an indexed tx ABCI message log. - description: >- - The output of the application's logger (typed). May be non-deterministic. - info: + upgrade handler with this name has been set in the software, it will be + + assumed that the software is out-of-date when the upgrade Time or Height is + + reached and the software will exit. + time: type: string - description: Additional information. May be non-deterministic. - gas_wanted: + format: date-time + description: >- + Deprecated: Time based upgrades have been deprecated. Time based upgrade logic + + has been removed from the SDK. + + If this field is not empty, an error will be thrown. + height: type: string format: int64 - description: Amount of gas requested for transaction. - gas_used: + description: The height at which the upgrade must be performed. + info: type: string - format: int64 - description: Amount of gas consumed by transaction. - tx: + title: >- + Any application specific upgrade info to be included on-chain + + such as a git commit that validators could automatically upgrade to + upgraded_client_state: type: object properties: type_url: @@ -21425,57 +20231,10 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - timestamp: - type: string - description: >- - Time of the previous block. For heights > 1, it's the weighted median of - - the timestamps of the valid votes in the block.LastCommit. For height == 1, - - it's genesis time. - events: - type: array - items: - type: object - properties: - type: - type: string - attributes: - type: array - items: - type: object - properties: - key: - type: string - value: - type: string - index: - type: boolean - description: >- - EventAttribute is a single key-value pair, associated with an event. - description: >- - Event allows application developers to attach additional information to - - ResponseFinalizeBlock and ResponseCheckTx. - - Later, transactions may be queried using these events. - description: >- - Events defines all the events emitted by processing a transaction. Note, - - these events include those emitted by processing all the messages and those - - emitted from the ante. Whereas Logs contains the events, with - - additional metadata, emitted only by processing the messages. - - Since: cosmos-sdk 0.42.11, 0.44.5, 0.45 - description: >- - TxResponse defines a structure containing relevant tx data and metadata. The + description: >- + QueryCurrentPlanResponse is the response type for the Query/CurrentPlan RPC - tags are stringified and the log is JSON decoded. - description: |- - BroadcastTxResponse is the response type for the - Service.BroadcastTx method. + method. default: description: An unexpected error response. schema: @@ -21639,55 +20398,43 @@ paths: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - parameters: - - name: body - in: body - required: true + tags: + - Query + /cosmos/upgrade/v1beta1/module_versions: + get: + summary: ModuleVersions queries the list of module versions from state. + description: 'Since: cosmos-sdk 0.43' + operationId: ModuleVersions + responses: + '200': + description: A successful response. schema: type: object properties: - tx_bytes: - type: string - format: byte - description: tx_bytes is the raw transaction. - mode: - type: string - enum: - - BROADCAST_MODE_UNSPECIFIED - - BROADCAST_MODE_BLOCK - - BROADCAST_MODE_SYNC - - BROADCAST_MODE_ASYNC - default: BROADCAST_MODE_UNSPECIFIED - description: >- - BroadcastMode specifies the broadcast mode for the TxService.Broadcast RPC - - method. - - - BROADCAST_MODE_UNSPECIFIED: zero-value for mode ordering - - BROADCAST_MODE_BLOCK: DEPRECATED: use BROADCAST_MODE_SYNC instead, - BROADCAST_MODE_BLOCK is not supported by the SDK from v0.47.x onwards. - - - BROADCAST_MODE_SYNC: BROADCAST_MODE_SYNC defines a tx broadcasting mode where the client waits - for a CheckTx execution response only. + module_versions: + type: array + items: + type: object + properties: + name: + type: string + title: name of the app module + version: + type: string + format: uint64 + title: consensus version of the app module + description: |- + ModuleVersion specifies a module and its consensus version. - - BROADCAST_MODE_ASYNC: BROADCAST_MODE_ASYNC defines a tx broadcasting mode where the client - returns immediately. + Since: cosmos-sdk 0.43 + description: >- + module_versions is a list of module names with their consensus versions. description: >- - BroadcastTxRequest is the request type for the Service.BroadcastTxRequest + QueryModuleVersionsResponse is the response type for the Query/ModuleVersions RPC method. - tags: - - Service - /cosmos/tx/v1beta1/txs/block/{height}: - get: - summary: GetBlockWithTxs fetches a block with decoded txs. - description: 'Since: cosmos-sdk 0.45.2' - operationId: GetBlockWithTxs - responses: - '200': - description: A successful response. - schema: - $ref: '#/definitions/cosmos.tx.v1beta1.GetBlockWithTxsResponse' + + Since: cosmos-sdk 0.43 default: description: An unexpected error response. schema: @@ -21852,72 +20599,45 @@ paths: "value": "1.212s" } parameters: - - name: height - description: height is the height of the block to query. - in: path - required: true - type: string - format: int64 - - name: pagination.key + - name: module_name description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. + module_name is a field to query a specific module + consensus version from state. Leaving this empty will + fetch the full list of module versions from state. in: query required: false type: string - format: byte - - name: pagination.offset - description: >- - offset is a numeric offset that can be used when key is unavailable. + tags: + - Query + /cosmos/upgrade/v1beta1/upgraded_consensus_state/{last_height}: + get: + summary: >- + UpgradedConsensusState queries the consensus state that will serve - It is less efficient than using key. Only one of offset or key should + as a trusted kernel for the next version of this chain. It will only be - be set. - in: query - required: false - type: string - format: uint64 - - name: pagination.limit - description: >- - limit is the total number of results to be returned in the result page. + stored at the last height of this chain. - If left empty it will default to a value to be set by each app. - in: query - required: false - type: string - format: uint64 - - name: pagination.count_total - description: >- - count_total is set to true to indicate that the result set should include + UpgradedConsensusState RPC not supported with legacy querier - a count of the total number of items available for pagination in UIs. - - count_total is only respected when offset is used. It is ignored when key - - is set. - in: query - required: false - type: boolean - - name: pagination.reverse - description: >- - reverse is set to true if results are to be returned in the descending order. + This rpc is deprecated now that IBC has its own replacement - Since: cosmos-sdk 0.43 - in: query - required: false - type: boolean - tags: - - Service - /cosmos/tx/v1beta1/txs/{hash}: - get: - summary: GetTx fetches a tx by hash. - operationId: GetTx + (https://github.com/cosmos/ibc-go/blob/2c880a22e9f9cc75f62b527ca94aa75ce1106001/proto/ibc/core/client/v1/query.proto#L54) + operationId: UpgradedConsensusState responses: '200': description: A successful response. schema: - $ref: '#/definitions/cosmos.tx.v1beta1.GetTxResponse' + type: object + properties: + upgraded_consensus_state: + type: string + format: byte + title: 'Since: cosmos-sdk 0.43' + description: >- + QueryUpgradedConsensusStateResponse is the response type for the Query/UpgradedConsensusState + + RPC method. default: description: An unexpected error response. schema: @@ -22082,13 +20802,16 @@ paths: "value": "1.212s" } parameters: - - name: hash - description: hash is the tx hash to query, encoded as a hex string. + - name: last_height + description: |- + last height of the current chain must be sent in request + as this is the height under which next consensus state is stored in: path required: true type: string + format: int64 tags: - - Service + - Query /cosmos/authz/v1beta1/grants: get: summary: Returns list of `Authorization`, granted to the grantee by the granter. @@ -28077,20 +26800,20 @@ paths: format: byte tags: - Query - /zeta-chain/authority/authorization/{msg_url}: + /zeta-chain/authority/authorization/{msgUrl}: get: - operationId: Query_Authorization + operationId: Authorization responses: "200": description: A successful response. schema: - $ref: '#/definitions/authorityQueryAuthorizationResponse' + $ref: '#/definitions/zetachain.zetacore.authority.QueryAuthorizationResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: msg_url + - name: msgUrl in: path required: true type: string @@ -28098,61 +26821,61 @@ paths: - Query /zeta-chain/authority/authorizations: get: - operationId: Query_AuthorizationList + operationId: AuthorizationList responses: "200": description: A successful response. schema: - $ref: '#/definitions/authorityQueryAuthorizationListResponse' + $ref: '#/definitions/zetachain.zetacore.authority.QueryAuthorizationListResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/authority/chainInfo: get: summary: Queries ChainInfo - operationId: Query_ChainInfo + operationId: ChainInfo responses: "200": description: A successful response. schema: - $ref: '#/definitions/authorityQueryGetChainInfoResponse' + $ref: '#/definitions/zetachain.zetacore.authority.QueryGetChainInfoResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/authority/policies: get: summary: Queries Policies - operationId: Query_Policies + operationId: Policies responses: "200": description: A successful response. schema: - $ref: '#/definitions/authorityQueryGetPoliciesResponse' + $ref: '#/definitions/zetachain.zetacore.authority.QueryGetPoliciesResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/crosschain/cctx: get: summary: Queries a list of cctx items. - operationId: Query_CctxAll + operationId: CctxAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28180,7 +26903,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28209,16 +26932,16 @@ paths: /zeta-chain/crosschain/cctx/{chainID}/{nonce}: get: summary: Queries a cctx by nonce. - operationId: Query_CctxByNonce + operationId: CctxByNonce responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chainID in: path @@ -28235,16 +26958,16 @@ paths: /zeta-chain/crosschain/cctx/{index}: get: summary: Queries a send by index. - operationId: Query_Cctx + operationId: Cctx responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: index in: path @@ -28254,16 +26977,16 @@ paths: - Query /zeta-chain/crosschain/convertGasToZeta: get: - operationId: Query_ConvertGasToZeta + operationId: ConvertGasToZeta responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryConvertGasToZetaResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryConvertGasToZetaResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chainId in: query @@ -28279,16 +27002,16 @@ paths: /zeta-chain/crosschain/gasPrice: get: summary: Queries a list of gasPrice items. - operationId: Query_GasPriceAll + operationId: GasPriceAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllGasPriceResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllGasPriceResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28316,7 +27039,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28338,16 +27061,16 @@ paths: /zeta-chain/crosschain/gasPrice/{index}: get: summary: Queries a gasPrice by index. - operationId: Query_GasPrice + operationId: GasPrice responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetGasPriceResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetGasPriceResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: index in: path @@ -28358,16 +27081,16 @@ paths: /zeta-chain/crosschain/inTxHashToCctx: get: summary: 'Deprecated(v17): use InboundHashToCctxAll' - operationId: Query_InTxHashToCctxAll + operationId: InTxHashToCctxAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundHashToCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundHashToCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28395,7 +27118,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28414,19 +27137,20 @@ paths: type: boolean tags: - Query + deprecated: true /zeta-chain/crosschain/inTxHashToCctx/{inboundHash}: get: summary: 'Deprecated(v17): use InboundHashToCctx' - operationId: Query_InTxHashToCctx + operationId: InTxHashToCctx responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetInboundHashToCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetInboundHashToCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: inboundHash in: path @@ -28434,19 +27158,20 @@ paths: type: string tags: - Query + deprecated: true /zeta-chain/crosschain/inTxHashToCctxData/{inboundHash}: get: summary: 'Deprecated(v17): use InboundHashToCctxData' - operationId: Query_InTxHashToCctxData + operationId: InTxHashToCctxData responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryInboundHashToCctxDataResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryInboundHashToCctxDataResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: inboundHash in: path @@ -28454,19 +27179,20 @@ paths: type: string tags: - Query + deprecated: true /zeta-chain/crosschain/inTxTracker: get: summary: 'Deprecated(v17): use InboundTrackerAll' - operationId: Query_InTxTrackerAll + operationId: InTxTrackerAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundTrackersResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundTrackersResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28494,7 +27220,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28513,21 +27239,22 @@ paths: type: boolean tags: - Query - /zeta-chain/crosschain/inTxTrackerByChain/{chain_id}: + deprecated: true + /zeta-chain/crosschain/inTxTrackerByChain/{chainId}: get: summary: 'Deprecated(v17): use InboundTrackerAllByChain' - operationId: Query_InTxTrackerAllByChain + operationId: InTxTrackerAllByChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundTrackerByChainResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundTrackerByChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -28558,7 +27285,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28577,19 +27304,20 @@ paths: type: boolean tags: - Query + deprecated: true /zeta-chain/crosschain/inboundHashToCctx: get: summary: Queries a list of InboundHashToCctx items. - operationId: Query_InboundHashToCctxAll + operationId: InboundHashToCctxAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundHashToCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundHashToCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28617,7 +27345,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28639,16 +27367,16 @@ paths: /zeta-chain/crosschain/inboundHashToCctx/{inboundHash}: get: summary: Queries a InboundHashToCctx by index. - operationId: Query_InboundHashToCctx + operationId: InboundHashToCctx responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetInboundHashToCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetInboundHashToCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: inboundHash in: path @@ -28659,16 +27387,16 @@ paths: /zeta-chain/crosschain/inboundHashToCctxData/{inboundHash}: get: summary: Queries a InboundHashToCctx data by index. - operationId: Query_InboundHashToCctxData + operationId: InboundHashToCctxData responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryInboundHashToCctxDataResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryInboundHashToCctxDataResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: inboundHash in: path @@ -28676,44 +27404,44 @@ paths: type: string tags: - Query - /zeta-chain/crosschain/inboundTracker/{chain_id}/{tx_hash}: + /zeta-chain/crosschain/inboundTracker/{chainId}/{txHash}: get: - operationId: Query_InboundTracker + operationId: InboundTracker responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryInboundTrackerResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryInboundTrackerResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string format: int64 - - name: tx_hash + - name: txHash in: path required: true type: string tags: - Query - /zeta-chain/crosschain/inboundTrackerByChain/{chain_id}: + /zeta-chain/crosschain/inboundTrackerByChain/{chainId}: get: - operationId: Query_InboundTrackerAllByChain + operationId: InboundTrackerAllByChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundTrackerByChainResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundTrackerByChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -28744,7 +27472,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28765,16 +27493,16 @@ paths: - Query /zeta-chain/crosschain/inboundTrackers: get: - operationId: Query_InboundTrackerAll + operationId: InboundTrackerAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllInboundTrackersResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllInboundTrackersResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28802,7 +27530,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28824,16 +27552,16 @@ paths: /zeta-chain/crosschain/lastBlockHeight: get: summary: Queries a list of lastBlockHeight items. - operationId: Query_LastBlockHeightAll + operationId: LastBlockHeightAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllLastBlockHeightResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllLastBlockHeightResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28861,7 +27589,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28883,16 +27611,16 @@ paths: /zeta-chain/crosschain/lastBlockHeight/{index}: get: summary: Queries a lastBlockHeight by index. - operationId: Query_LastBlockHeight + operationId: LastBlockHeight responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetLastBlockHeightResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetLastBlockHeightResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: index in: path @@ -28903,31 +27631,31 @@ paths: /zeta-chain/crosschain/lastZetaHeight: get: summary: Queries a list of lastMetaHeight items. - operationId: Query_LastZetaHeight + operationId: LastZetaHeight responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryLastZetaHeightResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryLastZetaHeightResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/crosschain/outTxTracker: get: summary: 'Deprecated(v17): use OutboundTrackerAll' - operationId: Query_OutTxTrackerAll + operationId: OutTxTrackerAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllOutboundTrackerResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllOutboundTrackerResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -28955,7 +27683,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -28974,19 +27702,20 @@ paths: type: boolean tags: - Query + deprecated: true /zeta-chain/crosschain/outTxTracker/{chainID}/{nonce}: get: summary: 'Deprecated(v17): use OutboundTracker' - operationId: Query_OutTxTracker + operationId: OutTxTracker responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetOutboundTrackerResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetOutboundTrackerResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chainID in: path @@ -29000,19 +27729,20 @@ paths: format: uint64 tags: - Query + deprecated: true /zeta-chain/crosschain/outTxTrackerByChain/{chain}: get: summary: 'Deprecated(v17): use OutboundTrackerAllByChain' - operationId: Query_OutTxTrackerAllByChain + operationId: OutTxTrackerAllByChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllOutboundTrackerByChainResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllOutboundTrackerByChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chain in: path @@ -29045,7 +27775,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29064,19 +27794,20 @@ paths: type: boolean tags: - Query + deprecated: true /zeta-chain/crosschain/outboundTracker: get: summary: Queries a list of OutboundTracker items. - operationId: Query_OutboundTrackerAll + operationId: OutboundTrackerAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllOutboundTrackerResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllOutboundTrackerResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29104,7 +27835,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29126,16 +27857,16 @@ paths: /zeta-chain/crosschain/outboundTracker/{chainID}/{nonce}: get: summary: Queries a outbound tracker by index. - operationId: Query_OutboundTracker + operationId: OutboundTracker responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryGetOutboundTrackerResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryGetOutboundTrackerResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chainID in: path @@ -29151,16 +27882,16 @@ paths: - Query /zeta-chain/crosschain/outboundTrackerByChain/{chain}: get: - operationId: Query_OutboundTrackerAllByChain + operationId: OutboundTrackerAllByChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryAllOutboundTrackerByChainResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryAllOutboundTrackerByChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: chain in: path @@ -29193,7 +27924,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29215,18 +27946,18 @@ paths: /zeta-chain/crosschain/pendingCctx: get: summary: Queries a list of pending cctxs. - operationId: Query_ListPendingCctx + operationId: ListPendingCctx responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryListPendingCctxResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryListPendingCctxResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: query required: false type: string @@ -29241,16 +27972,16 @@ paths: /zeta-chain/crosschain/pendingCctxWithinRateLimit: get: summary: Queries a list of pending cctxs within rate limit. - operationId: Query_ListPendingCctxWithinRateLimit + operationId: ListPendingCctxWithinRateLimit responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryListPendingCctxWithinRateLimitResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryListPendingCctxWithinRateLimitResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: limit in: query @@ -29261,46 +27992,46 @@ paths: - Query /zeta-chain/crosschain/protocolFee: get: - operationId: Query_ProtocolFee + operationId: ProtocolFee responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryMessagePassingProtocolFeeResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryMessagePassingProtocolFeeResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/crosschain/rateLimiterFlags: get: summary: Queries the rate limiter flags - operationId: Query_RateLimiterFlags + operationId: RateLimiterFlags responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryRateLimiterFlagsResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryRateLimiterFlagsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/crosschain/rateLimiterInput: get: summary: Queries the input data of rate limiter. - operationId: Query_RateLimiterInput + operationId: RateLimiterInput responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryRateLimiterInputResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryRateLimiterInputResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: limit in: query @@ -29316,61 +28047,61 @@ paths: - Query /zeta-chain/crosschain/zetaAccounting: get: - operationId: Query_ZetaAccounting + operationId: ZetaAccounting responses: "200": description: A successful response. schema: - $ref: '#/definitions/crosschainQueryZetaAccountingResponse' + $ref: '#/definitions/zetachain.zetacore.crosschain.QueryZetaAccountingResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/emissions/list_addresses: get: summary: Queries a list of ListBalances items. - operationId: Query_ListPoolAddresses + operationId: ListPoolAddresses responses: "200": description: A successful response. schema: - $ref: '#/definitions/emissionsQueryListPoolAddressesResponse' + $ref: '#/definitions/zetachain.zetacore.emissions.QueryListPoolAddressesResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/emissions/params: get: summary: Parameters queries the parameters of the module. - operationId: Query_Params + operationId: Params responses: "200": description: A successful response. schema: - $ref: '#/definitions/emissionsQueryParamsResponse' + $ref: '#/definitions/zetachain.zetacore.emissions.QueryParamsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/emissions/show_available_emissions/{address}: get: summary: Queries a list of ShowAvailableEmissions items. - operationId: Query_ShowAvailableEmissions + operationId: ShowAvailableEmissions responses: "200": description: A successful response. schema: - $ref: '#/definitions/emissionsQueryShowAvailableEmissionsResponse' + $ref: '#/definitions/zetachain.zetacore.emissions.QueryShowAvailableEmissionsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: address in: path @@ -29381,16 +28112,16 @@ paths: /zeta-chain/fungible/code_hash/{address}: get: summary: Code hash query the code hash of a contract. - operationId: Query_CodeHash + operationId: CodeHash responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryCodeHashResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryCodeHashResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: address in: path @@ -29401,16 +28132,16 @@ paths: /zeta-chain/fungible/foreign_coins: get: summary: Queries a list of ForeignCoins items. - operationId: Query_ForeignCoinsAll + operationId: ForeignCoinsAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryAllForeignCoinsResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryAllForeignCoinsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29438,7 +28169,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29460,16 +28191,16 @@ paths: /zeta-chain/fungible/foreign_coins/{index}: get: summary: Queries a ForeignCoins by index. - operationId: Query_ForeignCoins + operationId: ForeignCoins responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryGetForeignCoinsResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryGetForeignCoinsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: index in: path @@ -29480,33 +28211,33 @@ paths: /zeta-chain/fungible/gas_stability_pool_address: get: summary: Queries the address of a gas stability pool on a given chain. - operationId: Query_GasStabilityPoolAddress + operationId: GasStabilityPoolAddress responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryGetGasStabilityPoolAddressResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryGetGasStabilityPoolAddressResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query - /zeta-chain/fungible/gas_stability_pool_balance/{chain_id}: + /zeta-chain/fungible/gas_stability_pool_balance/{chainId}: get: summary: Queries the balance of a gas stability pool on a given chain. - operationId: Query_GasStabilityPoolBalance + operationId: GasStabilityPoolBalance responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryGetGasStabilityPoolBalanceResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryGetGasStabilityPoolBalanceResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -29516,30 +28247,30 @@ paths: /zeta-chain/fungible/system_contract: get: summary: Queries SystemContract - operationId: Query_SystemContract + operationId: SystemContract responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryGetSystemContractResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryGetSystemContractResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/lightclient/block_headers: get: - operationId: Query_BlockHeaderAll + operationId: BlockHeaderAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryAllBlockHeaderResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryAllBlockHeaderResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29567,7 +28298,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29586,38 +28317,40 @@ paths: type: boolean tags: - Query - /zeta-chain/lightclient/block_headers/{block_hash}: + deprecated: true + /zeta-chain/lightclient/block_headers/{blockHash}: get: - operationId: Query_BlockHeader + operationId: BlockHeader responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryGetBlockHeaderResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryGetBlockHeaderResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: block_hash + - name: blockHash in: path required: true type: string format: byte tags: - Query + deprecated: true /zeta-chain/lightclient/chain_state: get: - operationId: Query_ChainStateAll + operationId: ChainStateAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryAllChainStateResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryAllChainStateResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29645,7 +28378,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29664,77 +28397,81 @@ paths: type: boolean tags: - Query - /zeta-chain/lightclient/chain_state/{chain_id}: + deprecated: true + /zeta-chain/lightclient/chain_state/{chainId}: get: - operationId: Query_ChainState + operationId: ChainState responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryGetChainStateResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryGetChainStateResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string format: int64 tags: - Query + deprecated: true /zeta-chain/lightclient/header_enabled_chains: get: - operationId: Query_HeaderEnabledChains + operationId: HeaderEnabledChains responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryHeaderEnabledChainsResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryHeaderEnabledChainsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query + deprecated: true /zeta-chain/lightclient/header_supported_chains: get: - operationId: Query_HeaderSupportedChains + operationId: HeaderSupportedChains responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryHeaderSupportedChainsResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryHeaderSupportedChainsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query + deprecated: true /zeta-chain/lightclient/prove: get: - operationId: Query_Prove + operationId: Prove responses: "200": description: A successful response. schema: - $ref: '#/definitions/lightclientQueryProveResponse' + $ref: '#/definitions/zetachain.zetacore.lightclient.QueryProveResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: query required: false type: string format: int64 - - name: tx_hash + - name: txHash in: query required: false type: string - - name: proof.ethereum_proof.keys + - name: proof.ethereumProof.keys in: query required: false type: array @@ -29742,7 +28479,7 @@ paths: type: string format: byte collectionFormat: multi - - name: proof.ethereum_proof.values + - name: proof.ethereumProof.values in: query required: false type: array @@ -29750,62 +28487,63 @@ paths: type: string format: byte collectionFormat: multi - - name: proof.bitcoin_proof.tx_bytes + - name: proof.bitcoinProof.txBytes in: query required: false type: string format: byte - - name: proof.bitcoin_proof.path + - name: proof.bitcoinProof.path in: query required: false type: string format: byte - - name: proof.bitcoin_proof.index + - name: proof.bitcoinProof.index in: query required: false type: integer format: int64 - - name: block_hash + - name: blockHash in: query required: false type: string - - name: tx_index + - name: txIndex in: query required: false type: string format: int64 tags: - Query + deprecated: true /zeta-chain/observer/TSS: get: summary: Queries a tSS by index. - operationId: Query_TSS + operationId: TSS responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetTSSResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetTSSResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query - /zeta-chain/observer/ballot_by_identifier/{ballot_identifier}: + /zeta-chain/observer/ballot_by_identifier/{ballotIdentifier}: get: summary: Queries a list of VoterByIdentifier items. - operationId: Query_BallotByIdentifier + operationId: BallotByIdentifier responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryBallotByIdentifierResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryBallotByIdentifierResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: ballot_identifier + - name: ballotIdentifier in: path required: true type: string @@ -29814,16 +28552,16 @@ paths: /zeta-chain/observer/ballots: get: summary: Query all ballots - operationId: Query_Ballots + operationId: Ballots responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryBallotsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryBallotsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29851,7 +28589,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29870,21 +28608,21 @@ paths: type: boolean tags: - Query - /zeta-chain/observer/blame_by_chain_and_nonce/{chain_id}/{nonce}: + /zeta-chain/observer/blame_by_chain_and_nonce/{chainId}/{nonce}: get: summary: Queries a list of VoterByIdentifier items. - operationId: Query_BlamesByChainAndNonce + operationId: BlamesByChainAndNonce responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryBlameByChainAndNonceResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryBlameByChainAndNonceResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -29896,21 +28634,21 @@ paths: format: int64 tags: - Query - /zeta-chain/observer/blame_by_identifier/{blame_identifier}: + /zeta-chain/observer/blame_by_identifier/{blameIdentifier}: get: summary: Queries a list of VoterByIdentifier items. - operationId: Query_BlameByIdentifier + operationId: BlameByIdentifier responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryBlameByIdentifierResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryBlameByIdentifierResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: blame_identifier + - name: blameIdentifier in: path required: true type: string @@ -29919,16 +28657,16 @@ paths: /zeta-chain/observer/chainNonces: get: summary: Queries a list of chainNonces items. - operationId: Query_ChainNoncesAll + operationId: ChainNoncesAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryAllChainNoncesResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryAllChainNoncesResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -29956,7 +28694,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -29975,21 +28713,21 @@ paths: type: boolean tags: - Query - /zeta-chain/observer/chainNonces/{chain_id}: + /zeta-chain/observer/chainNonces/{chainId}: get: summary: Queries a chainNonces by index. - operationId: Query_ChainNonces + operationId: ChainNonces responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetChainNoncesResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetChainNoncesResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -29998,31 +28736,67 @@ paths: - Query /zeta-chain/observer/crosschain_flags: get: - operationId: Query_CrosschainFlags + operationId: CrosschainFlags + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetCrosschainFlagsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/google.rpc.Status' + tags: + - Query + /zeta-chain/observer/getAllTssFundsMigrators: + get: + summary: Queries all TssFundMigratorInfo + operationId: TssFundsMigratorInfoAll + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/zetachain.zetacore.observer.QueryTssFundsMigratorInfoAllResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/google.rpc.Status' + tags: + - Query + /zeta-chain/observer/getTssFundsMigrator: + get: + summary: Queries the TssFundMigratorInfo for a specific chain + operationId: TssFundsMigratorInfo responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetCrosschainFlagsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryTssFundsMigratorInfoResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' + parameters: + - name: chainId + in: query + required: false + type: string + format: int64 tags: - Query /zeta-chain/observer/get_all_blame_records: get: summary: Queries a list of VoterByIdentifier items. - operationId: Query_GetAllBlameRecords + operationId: GetAllBlameRecords responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryAllBlameRecordsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryAllBlameRecordsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -30050,7 +28824,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -30072,140 +28846,104 @@ paths: /zeta-chain/observer/get_chain_params: get: summary: Queries a list of GetChainParams items. - operationId: Query_GetChainParams + operationId: GetChainParams responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetChainParamsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetChainParamsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query - /zeta-chain/observer/get_chain_params_for_chain/{chain_id}: + /zeta-chain/observer/get_chain_params_for_chain/{chainId}: get: summary: Queries a list of GetChainParamsForChain items. - operationId: Query_GetChainParamsForChain + operationId: GetChainParamsForChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetChainParamsForChainResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetChainParamsForChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string format: int64 tags: - Query - /zeta-chain/observer/get_tss_address/{bitcoin_chain_id}: + /zeta-chain/observer/get_tss_address/{bitcoinChainId}: get: summary: Queries a list of GetTssAddress items. - operationId: Query_GetTssAddress + operationId: GetTssAddress responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetTssAddressResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetTssAddressResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: bitcoin_chain_id + - name: bitcoinChainId in: path required: true type: string format: int64 tags: - Query - /zeta-chain/observer/get_tss_address_historical/{finalized_zeta_height}/{bitcoin_chain_id}: + /zeta-chain/observer/get_tss_address_historical/{finalizedZetaHeight}/{bitcoinChainId}: get: - operationId: Query_GetTssAddressByFinalizedHeight + operationId: GetTssAddressByFinalizedHeight responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetTssAddressByFinalizedHeightResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetTssAddressByFinalizedHeightResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: finalized_zeta_height + - name: finalizedZetaHeight in: path required: true type: string format: int64 - - name: bitcoin_chain_id + - name: bitcoinChainId in: path required: true type: string format: int64 tags: - Query - /zeta-chain/observer/getAllTssFundsMigrators: - get: - summary: Queries all TssFundMigratorInfo - operationId: Query_TssFundsMigratorInfoAll - responses: - "200": - description: A successful response. - schema: - $ref: '#/definitions/observerQueryTssFundsMigratorInfoAllResponse' - default: - description: An unexpected error response. - schema: - $ref: '#/definitions/googlerpcStatus' - tags: - - Query - /zeta-chain/observer/getTssFundsMigrator: - get: - summary: Queries the TssFundMigratorInfo for a specific chain - operationId: Query_TssFundsMigratorInfo - responses: - "200": - description: A successful response. - schema: - $ref: '#/definitions/observerQueryTssFundsMigratorInfoResponse' - default: - description: An unexpected error response. - schema: - $ref: '#/definitions/googlerpcStatus' - parameters: - - name: chain_id - in: query - required: false - type: string - format: int64 - tags: - - Query - /zeta-chain/observer/has_voted/{ballot_identifier}/{voter_address}: + /zeta-chain/observer/has_voted/{ballotIdentifier}/{voterAddress}: get: summary: Query if a voter has voted for a ballot - operationId: Query_HasVoted + operationId: HasVoted responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryHasVotedResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryHasVotedResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: ballot_identifier + - name: ballotIdentifier in: path required: true type: string - - name: voter_address + - name: voterAddress in: path required: true type: string @@ -30214,31 +28952,31 @@ paths: /zeta-chain/observer/keygen: get: summary: Queries a keygen by index. - operationId: Query_Keygen + operationId: Keygen responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetKeygenResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetKeygenResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/observer/nodeAccount: get: summary: Queries a list of nodeAccount items. - operationId: Query_NodeAccountAll + operationId: NodeAccountAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryAllNodeAccountResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryAllNodeAccountResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -30266,7 +29004,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -30288,16 +29026,16 @@ paths: /zeta-chain/observer/nodeAccount/{index}: get: summary: Queries a nodeAccount by index. - operationId: Query_NodeAccount + operationId: NodeAccount responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryGetNodeAccountResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryGetNodeAccountResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: index in: path @@ -30308,45 +29046,45 @@ paths: /zeta-chain/observer/observer_set: get: summary: Queries a list of ObserversByChainAndType items. - operationId: Query_ObserverSet + operationId: ObserverSet responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryObserverSetResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryObserverSetResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/observer/operationalFlags: get: summary: Queries operational flags - operationId: Query_OperationalFlags + operationId: OperationalFlags responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryOperationalFlagsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryOperationalFlagsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/observer/pendingNonces: get: - operationId: Query_PendingNoncesAll + operationId: PendingNoncesAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryAllPendingNoncesResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryAllPendingNoncesResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -30374,7 +29112,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -30393,20 +29131,20 @@ paths: type: boolean tags: - Query - /zeta-chain/observer/pendingNonces/{chain_id}: + /zeta-chain/observer/pendingNonces/{chainId}: get: - operationId: Query_PendingNoncesByChain + operationId: PendingNoncesByChain responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryPendingNoncesByChainResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryPendingNoncesByChainResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - - name: chain_id + - name: chainId in: path required: true type: string @@ -30415,30 +29153,30 @@ paths: - Query /zeta-chain/observer/supportedChains: get: - operationId: Query_SupportedChains + operationId: SupportedChains responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQuerySupportedChainsResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QuerySupportedChainsResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/observer/tssHistory: get: - operationId: Query_TssHistory + operationId: TssHistory responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryTssHistoryResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryTssHistoryResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' parameters: - name: pagination.key description: |- @@ -30466,7 +29204,7 @@ paths: required: false type: string format: uint64 - - name: pagination.count_total + - name: pagination.countTotal description: |- count_total is set to true to indicate that the result set should include a count of the total number of items available for pagination in UIs. @@ -30488,31 +29226,31 @@ paths: /zeta-chain/zetacore/fungible/gas_stability_pool_balance: get: summary: Queries all gas stability pool balances. - operationId: Query_GasStabilityPoolBalanceAll + operationId: GasStabilityPoolBalanceAll responses: "200": description: A successful response. schema: - $ref: '#/definitions/fungibleQueryAllGasStabilityPoolBalanceResponse' + $ref: '#/definitions/zetachain.zetacore.fungible.QueryAllGasStabilityPoolBalanceResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /zeta-chain/zetacore/observer/show_observer_count: get: summary: Queries a list of ShowObserverCount items. - operationId: Query_ShowObserverCount + operationId: ShowObserverCount responses: "200": description: A successful response. schema: - $ref: '#/definitions/observerQueryShowObserverCountResponse' + $ref: '#/definitions/zetachain.zetacore.observer.QueryShowObserverCountResponse' default: description: An unexpected error response. schema: - $ref: '#/definitions/googlerpcStatus' + $ref: '#/definitions/google.rpc.Status' tags: - Query /ethermint/evm/v1/account/{address}: @@ -34532,6 +33270,13 @@ definitions: reverse is set to true if results are to be returned in the descending order. Since: cosmos-sdk 0.43 + countTotal: + type: boolean + description: |- + count_total is set to true to indicate that the result set should include + a count of the total number of items available for pagination in UIs. + count_total is only respected when offset is used. It is ignored when key + is set. description: |- message SomeRequest { Foo some_parameter = 1; @@ -34556,6 +33301,13 @@ definitions: title: |- total is total number of results available if PageRequest.count_total was set, its value is undefined otherwise + nextKey: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. description: |- PageResponse is to be embedded in gRPC response messages where the corresponding request message has used PageRequest. @@ -34616,6 +33368,8 @@ definitions: format: byte description: >- Must be a valid serialized protocol buffer of the above specified type. + '@type': + type: string description: >- `Any` contains an arbitrary serialized protocol buffer message along with a @@ -34709,6 +33463,7 @@ definitions: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } + additionalProperties: {} grpc.gateway.runtime.Error: type: object properties: @@ -58618,81 +57373,86 @@ definitions: field's configuration is global (not module specific). description: QueryConfigRequest is the Query/Config response type. - QueryAllGasStabilityPoolBalanceResponseBalance: + google.rpc.Status: type: object properties: - chain_id: - type: string - format: int64 - balance: + code: + type: integer + format: int32 + message: type: string - authorityAuthorization: + details: + type: array + items: + type: object + $ref: '#/definitions/google.protobuf.Any' + zetachain.zetacore.authority.Authorization: type: object properties: - msg_url: + msgUrl: type: string title: The URL of the message that needs to be authorized - authorized_policy: - $ref: '#/definitions/authorityPolicyType' + authorizedPolicy: + $ref: '#/definitions/zetachain.zetacore.authority.PolicyType' title: The policy that is authorized to access the message title: |- Authorization defines the authorization required to access use a message which needs special permissions - authorityAuthorizationList: + zetachain.zetacore.authority.AuthorizationList: type: object properties: authorizations: type: array items: type: object - $ref: '#/definitions/authorityAuthorization' + $ref: '#/definitions/zetachain.zetacore.authority.Authorization' title: AuthorizationList holds the list of authorizations on zetachain - authorityChainInfo: + zetachain.zetacore.authority.ChainInfo: type: object properties: chains: type: array items: type: object - $ref: '#/definitions/chainsChain' + $ref: '#/definitions/zetachain.zetacore.pkg.chains.Chain' title: |- ChainInfo contains static information about the chains This structure is used to dynamically update these info on a live network before hardcoding the values in a upgrade - authorityMsgAddAuthorizationResponse: + zetachain.zetacore.authority.MsgAddAuthorizationResponse: type: object description: MsgAddAuthorizationResponse defines the MsgAddAuthorizationResponse service. - authorityMsgRemoveAuthorizationResponse: + zetachain.zetacore.authority.MsgRemoveAuthorizationResponse: type: object description: |- MsgRemoveAuthorizationResponse defines the MsgRemoveAuthorizationResponse service. - authorityMsgRemoveChainInfoResponse: + zetachain.zetacore.authority.MsgRemoveChainInfoResponse: type: object description: MsgRemoveChainInfoResponse defines the MsgRemoveChainInfoResponse service. - authorityMsgUpdateChainInfoResponse: + zetachain.zetacore.authority.MsgUpdateChainInfoResponse: type: object description: MsgUpdateChainInfoResponse defines the MsgUpdateChainInfoResponse service. - authorityMsgUpdatePoliciesResponse: + zetachain.zetacore.authority.MsgUpdatePoliciesResponse: type: object description: MsgUpdatePoliciesResponse defines the MsgUpdatePoliciesResponse service. - authorityPolicies: + zetachain.zetacore.authority.Policies: type: object properties: items: type: array items: type: object - $ref: '#/definitions/authorityPolicy' + $ref: '#/definitions/zetachain.zetacore.authority.Policy' title: Policy contains info about authority policies - authorityPolicy: + zetachain.zetacore.authority.Policy: type: object properties: - policy_type: - $ref: '#/definitions/authorityPolicyType' + policyType: + $ref: '#/definitions/zetachain.zetacore.authority.PolicyType' address: type: string - authorityPolicyType: + zetachain.zetacore.authority.PolicyType: type: string enum: - groupEmergency @@ -58710,214 +57470,47 @@ definitions: Used for empty policy, no action is allowed title: PolicyType defines the type of policy - authorityQueryAuthorizationListResponse: + zetachain.zetacore.authority.QueryAuthorizationListResponse: type: object properties: - authorization_list: - $ref: '#/definitions/authorityAuthorizationList' + authorizationList: + $ref: '#/definitions/zetachain.zetacore.authority.AuthorizationList' title: |- QueryAuthorizationListResponse is the response type for the Query/AuthorizationList RPC - authorityQueryAuthorizationResponse: + zetachain.zetacore.authority.QueryAuthorizationResponse: type: object properties: authorization: - $ref: '#/definitions/authorityAuthorization' + $ref: '#/definitions/zetachain.zetacore.authority.Authorization' description: |- QueryAuthorizationResponse is the response type for the Query/Authorization RPC method. - authorityQueryGetChainInfoResponse: + zetachain.zetacore.authority.QueryGetChainInfoResponse: type: object properties: - chain_info: - $ref: '#/definitions/authorityChainInfo' + chainInfo: + $ref: '#/definitions/zetachain.zetacore.authority.ChainInfo' description: |- QueryGetChainInfoResponse is the response type for the Query/ChainInfo RPC method. - authorityQueryGetPoliciesResponse: + zetachain.zetacore.authority.QueryGetPoliciesResponse: type: object properties: policies: - $ref: '#/definitions/authorityPolicies' + $ref: '#/definitions/zetachain.zetacore.authority.Policies' description: |- QueryGetPoliciesResponse is the response type for the Query/Policies RPC method. - chainsCCTXGateway: - type: string - enum: - - zevm - - observers - default: zevm - description: |- - - zevm: zevm is the internal CCTX gateway to process outbound on the ZEVM and read - inbound events from the ZEVM only used for ZetaChain chains - - observers: observers is the CCTX gateway for chains relying on the observer set to - observe inbounds and TSS for outbounds - title: CCTXGateway describes for the chain the gateway used to handle CCTX outbounds - chainsChain: - type: object - properties: - chain_id: - type: string - format: int64 - title: ChainId is the unique identifier of the chain - chain_name: - $ref: '#/definitions/chainsChainName' - title: |- - ChainName is the name of the chain - Deprecated(v19): replaced with Name - network: - $ref: '#/definitions/chainsNetwork' - title: Network is the network of the chain - network_type: - $ref: '#/definitions/chainsNetworkType' - description: 'NetworkType is the network type of the chain: mainnet, testnet, etc..' - vm: - $ref: '#/definitions/chainsVm' - title: Vm is the virtual machine used in the chain - consensus: - $ref: '#/definitions/chainsConsensus' - title: Consensus is the underlying consensus algorithm used by the chain - is_external: - type: boolean - title: IsExternal describe if the chain is ZetaChain or external - cctx_gateway: - $ref: '#/definitions/chainsCCTXGateway' - title: CCTXGateway is the gateway used to handle CCTX outbounds - name: - type: string - title: Name is the name of the chain - title: |- - Chain represents static data about a blockchain network - it is identified by a unique chain ID - chainsChainName: - type: string - enum: - - empty - - eth_mainnet - - zeta_mainnet - - btc_mainnet - - polygon_mainnet - - bsc_mainnet - - goerli_testnet - - mumbai_testnet - - bsc_testnet - - zeta_testnet - - btc_testnet - - sepolia_testnet - - goerli_localnet - - btc_regtest - - amoy_testnet - - optimism_mainnet - - optimism_sepolia - - base_mainnet - - base_sepolia - - solana_mainnet - - solana_devnet - - solana_localnet - default: empty - title: |- - ChainName represents the name of the chain - Deprecated(v19): replaced with Chain.Name as string - chainsConsensus: - type: string - enum: - - ethereum - - tendermint - - bitcoin - - op_stack - - solana_consensus - - catchain_consensus - - snowman - - arbitrum_nitro - - sui_consensus - default: ethereum - description: |- - - catchain_consensus: ton - - snowman: avalanche - title: |- - Consensus represents the consensus algorithm used by the chain - this can represent the consensus of a L1 - this can also represent the solution of a L2 - chainsNetwork: - type: string - enum: - - eth - - zeta - - btc - - polygon - - bsc - - optimism - - base - - solana - - ton - - avalanche - - arbitrum - - worldchain - - sui - default: eth - title: |- - Network represents the network of the chain - there is a single instance of the network on mainnet - then the network can have eventual testnets or devnets - chainsNetworkType: - type: string - enum: - - mainnet - - testnet - - privnet - - devnet - default: mainnet - title: |- - NetworkType represents the network type of the chain - Mainnet, Testnet, Privnet, Devnet - chainsReceiveStatus: - type: string - enum: - - created - - success - - failed - default: created - description: '- created: Created is used for inbounds' - title: |- - ReceiveStatus represents the status of an outbound - TODO: Rename and move - https://github.com/zeta-chain/node/issues/2257 - chainsVm: - type: string - enum: - - no_vm - - evm - - svm - - tvm - - mvm_sui - default: no_vm - title: |- - Vm represents the virtual machine type of the chain to support smart - contracts - coinCoinType: - type: string - enum: - - Zeta - - Gas - - ERC20 - - Cmd - - NoAssetCall - default: Zeta - title: |- - - Gas: Ether, BNB, Matic, Klay, BTC, etc - - ERC20: ERC20 token - - Cmd: no asset, used for admin command - - NoAssetCall: no asset, used for contract call - crosschainCallOptions: + zetachain.zetacore.crosschain.CallOptions: type: object properties: - gas_limit: + gasLimit: type: string format: uint64 - is_arbitrary_call: + isArbitraryCall: type: boolean - crosschainCctxStatus: + zetachain.zetacore.crosschain.CctxStatus: type: string enum: - PendingInbound @@ -58934,7 +57527,7 @@ definitions: - PendingRevert: outbound cannot succeed; should revert inbound - Reverted: inbound reverted. - Aborted: inbound tx error or invalid paramters and cannot revert; just abort. - crosschainConfirmationMode: + zetachain.zetacore.crosschain.ConfirmationMode: type: string enum: - SAFE @@ -58943,53 +57536,53 @@ definitions: title: |- - SAFE: an inbound/outbound is confirmed using safe confirmation count - FAST: an inbound/outbound is confirmed using fast confirmation count - crosschainConversion: + zetachain.zetacore.crosschain.Conversion: type: object properties: zrc20: type: string rate: type: string - crosschainCrossChainTx: + zetachain.zetacore.crosschain.CrossChainTx: type: object properties: creator: type: string index: type: string - zeta_fees: + zetaFees: type: string - relayed_message: + relayedMessage: type: string title: Not used by protocol , just relayed across - cctx_status: - $ref: '#/definitions/zetacorecrosschainStatus' - inbound_params: - $ref: '#/definitions/crosschainInboundParams' - outbound_params: + cctxStatus: + $ref: '#/definitions/zetachain.zetacore.crosschain.Status' + inboundParams: + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundParams' + outboundParams: type: array items: type: object - $ref: '#/definitions/crosschainOutboundParams' - protocol_contract_version: - $ref: '#/definitions/crosschainProtocolContractVersion' - revert_options: - $ref: '#/definitions/crosschainRevertOptions' - crosschainGasPrice: + $ref: '#/definitions/zetachain.zetacore.crosschain.OutboundParams' + protocolContractVersion: + $ref: '#/definitions/zetachain.zetacore.crosschain.ProtocolContractVersion' + revertOptions: + $ref: '#/definitions/zetachain.zetacore.crosschain.RevertOptions' + zetachain.zetacore.crosschain.GasPrice: type: object properties: creator: type: string index: type: string - chain_id: + chainId: type: string format: int64 signers: type: array items: type: string - block_nums: + blockNums: type: array items: type: string @@ -58999,69 +57592,69 @@ definitions: items: type: string format: uint64 - median_index: + medianIndex: type: string format: uint64 title: index of the median gas price in the prices array - priority_fees: + priorityFees: type: array items: type: string format: uint64 title: priority fees for EIP-1559 - crosschainInboundHashToCctx: + zetachain.zetacore.crosschain.InboundHashToCctx: type: object properties: - inbound_hash: + inboundHash: type: string - cctx_index: + cctxIndex: type: array items: type: string - crosschainInboundParams: + zetachain.zetacore.crosschain.InboundParams: type: object properties: sender: type: string title: this address is the immediate contract/EOA that calls - sender_chain_id: + senderChainId: type: string format: int64 title: the Connector.send() - tx_origin: + txOrigin: type: string title: this address is the EOA that signs the inbound tx - coin_type: - $ref: '#/definitions/coinCoinType' + coinType: + $ref: '#/definitions/zetachain.zetacore.pkg.coin.CoinType' asset: type: string title: for ERC20 coin type, the asset is an address of the ERC20 contract amount: type: string - observed_hash: + observedHash: type: string - observed_external_height: + observedExternalHeight: type: string format: uint64 - ballot_index: + ballotIndex: type: string - finalized_zeta_height: + finalizedZetaHeight: type: string format: uint64 - tx_finalization_status: - $ref: '#/definitions/crosschainTxFinalizationStatus' - is_cross_chain_call: + txFinalizationStatus: + $ref: '#/definitions/zetachain.zetacore.crosschain.TxFinalizationStatus' + isCrossChainCall: type: boolean title: |- this field describes if a smart contract call should be made for a inbound with assets only used for protocol contract version 2 status: - $ref: '#/definitions/crosschainInboundStatus' + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundStatus' title: status of the inbound observation - confirmation_mode: - $ref: '#/definitions/crosschainConfirmationMode' + confirmationMode: + $ref: '#/definitions/zetachain.zetacore.crosschain.ConfirmationMode' title: confirmation mode used for the inbound - crosschainInboundStatus: + zetachain.zetacore.crosschain.InboundStatus: type: string enum: - SUCCESS @@ -59073,17 +57666,17 @@ definitions: depositor fee - INVALID_RECEIVER_ADDRESS: the receiver address parsed from the inbound is invalid title: InboundStatus represents the status of an observed inbound - crosschainInboundTracker: + zetachain.zetacore.crosschain.InboundTracker: type: object properties: - chain_id: + chainId: type: string format: int64 - tx_hash: + txHash: type: string - coin_type: - $ref: '#/definitions/coinCoinType' - crosschainLastBlockHeight: + coinType: + $ref: '#/definitions/zetachain.zetacore.pkg.coin.CoinType' + zetachain.zetacore.crosschain.LastBlockHeight: type: object properties: creator: @@ -59098,119 +57691,119 @@ definitions: lastOutboundHeight: type: string format: uint64 - crosschainMsgAbortStuckCCTXResponse: + zetachain.zetacore.crosschain.MsgAbortStuckCCTXResponse: type: object - crosschainMsgAddInboundTrackerResponse: + zetachain.zetacore.crosschain.MsgAddInboundTrackerResponse: type: object - crosschainMsgAddOutboundTrackerResponse: + zetachain.zetacore.crosschain.MsgAddOutboundTrackerResponse: type: object properties: - is_removed: + isRemoved: type: boolean title: if the tx was removed from the tracker due to no pending cctx - crosschainMsgMigrateERC20CustodyFundsResponse: + zetachain.zetacore.crosschain.MsgMigrateERC20CustodyFundsResponse: type: object properties: - cctx_index: + cctxIndex: type: string - crosschainMsgMigrateTssFundsResponse: + zetachain.zetacore.crosschain.MsgMigrateTssFundsResponse: type: object - crosschainMsgRefundAbortedCCTXResponse: + zetachain.zetacore.crosschain.MsgRefundAbortedCCTXResponse: type: object - crosschainMsgRemoveInboundTrackerResponse: + zetachain.zetacore.crosschain.MsgRemoveInboundTrackerResponse: type: object - crosschainMsgRemoveOutboundTrackerResponse: + zetachain.zetacore.crosschain.MsgRemoveOutboundTrackerResponse: type: object - crosschainMsgUpdateERC20CustodyPauseStatusResponse: + zetachain.zetacore.crosschain.MsgUpdateERC20CustodyPauseStatusResponse: type: object properties: - cctx_index: + cctxIndex: type: string - crosschainMsgUpdateRateLimiterFlagsResponse: + zetachain.zetacore.crosschain.MsgUpdateRateLimiterFlagsResponse: type: object - crosschainMsgUpdateTssAddressResponse: + zetachain.zetacore.crosschain.MsgUpdateTssAddressResponse: type: object - crosschainMsgVoteGasPriceResponse: + zetachain.zetacore.crosschain.MsgVoteGasPriceResponse: type: object - crosschainMsgVoteInboundResponse: + zetachain.zetacore.crosschain.MsgVoteInboundResponse: type: object - crosschainMsgVoteOutboundResponse: + zetachain.zetacore.crosschain.MsgVoteOutboundResponse: type: object - crosschainMsgWhitelistERC20Response: + zetachain.zetacore.crosschain.MsgWhitelistERC20Response: type: object properties: - zrc20_address: + zrc20Address: type: string - cctx_index: + cctxIndex: type: string - crosschainOutboundParams: + zetachain.zetacore.crosschain.OutboundParams: type: object properties: receiver: type: string - receiver_chainId: + receiverChainId: type: string format: int64 - coin_type: - $ref: '#/definitions/coinCoinType' + coinType: + $ref: '#/definitions/zetachain.zetacore.pkg.coin.CoinType' amount: type: string - tss_nonce: + tssNonce: type: string format: uint64 - gas_limit: + gasLimit: type: string format: uint64 title: Deprecated (v21), use CallOptions - gas_price: + gasPrice: type: string - gas_priority_fee: + gasPriorityFee: type: string hash: type: string title: |- the above are commands for zetaclients the following fields are used when the outbound tx is mined - ballot_index: + ballotIndex: type: string - observed_external_height: + observedExternalHeight: type: string format: uint64 - gas_used: + gasUsed: type: string format: uint64 - effective_gas_price: + effectiveGasPrice: type: string - effective_gas_limit: + effectiveGasLimit: type: string format: uint64 - tss_pubkey: - type: string - tx_finalization_status: - $ref: '#/definitions/crosschainTxFinalizationStatus' - call_options: - $ref: '#/definitions/crosschainCallOptions' - confirmation_mode: - $ref: '#/definitions/crosschainConfirmationMode' + tssPubkey: + type: string + txFinalizationStatus: + $ref: '#/definitions/zetachain.zetacore.crosschain.TxFinalizationStatus' + callOptions: + $ref: '#/definitions/zetachain.zetacore.crosschain.CallOptions' + confirmationMode: + $ref: '#/definitions/zetachain.zetacore.crosschain.ConfirmationMode' title: confirmation mode used for the outbound - crosschainOutboundTracker: + zetachain.zetacore.crosschain.OutboundTracker: type: object properties: index: type: string title: 'format: "chain-nonce"' - chain_id: + chainId: type: string format: int64 nonce: type: string format: uint64 - hash_list: + hashList: type: array items: type: object - $ref: '#/definitions/crosschainTxHash' - crosschainProtocolContractVersion: + $ref: '#/definitions/zetachain.zetacore.crosschain.TxHash' + zetachain.zetacore.crosschain.ProtocolContractVersion: type: string enum: - V1 @@ -59219,87 +57812,87 @@ definitions: title: |- ProtocolContractVersion represents the version of the protocol contract used for cctx workflow - crosschainQueryAllCctxResponse: + zetachain.zetacore.crosschain.QueryAllCctxResponse: type: object properties: CrossChainTx: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllGasPriceResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllGasPriceResponse: type: object properties: GasPrice: type: array items: type: object - $ref: '#/definitions/crosschainGasPrice' + $ref: '#/definitions/zetachain.zetacore.crosschain.GasPrice' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllInboundHashToCctxResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllInboundHashToCctxResponse: type: object properties: inboundHashToCctx: type: array items: type: object - $ref: '#/definitions/crosschainInboundHashToCctx' + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundHashToCctx' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllInboundTrackerByChainResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllInboundTrackerByChainResponse: type: object properties: inboundTracker: type: array items: type: object - $ref: '#/definitions/crosschainInboundTracker' + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundTracker' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllInboundTrackersResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllInboundTrackersResponse: type: object properties: inboundTracker: type: array items: type: object - $ref: '#/definitions/crosschainInboundTracker' + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundTracker' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllLastBlockHeightResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllLastBlockHeightResponse: type: object properties: LastBlockHeight: type: array items: type: object - $ref: '#/definitions/crosschainLastBlockHeight' + $ref: '#/definitions/zetachain.zetacore.crosschain.LastBlockHeight' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllOutboundTrackerByChainResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllOutboundTrackerByChainResponse: type: object properties: outboundTracker: type: array items: type: object - $ref: '#/definitions/crosschainOutboundTracker' + $ref: '#/definitions/zetachain.zetacore.crosschain.OutboundTracker' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryAllOutboundTrackerResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryAllOutboundTrackerResponse: type: object properties: outboundTracker: type: array items: type: object - $ref: '#/definitions/crosschainOutboundTracker' + $ref: '#/definitions/zetachain.zetacore.crosschain.OutboundTracker' pagination: - $ref: '#/definitions/v1beta1PageResponse' - crosschainQueryConvertGasToZetaResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.crosschain.QueryConvertGasToZetaResponse: type: object properties: outboundGasInZeta: @@ -59309,121 +57902,121 @@ definitions: ZetaBlockHeight: type: string format: uint64 - crosschainQueryGetCctxResponse: + zetachain.zetacore.crosschain.QueryGetCctxResponse: type: object properties: CrossChainTx: - $ref: '#/definitions/crosschainCrossChainTx' - crosschainQueryGetGasPriceResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' + zetachain.zetacore.crosschain.QueryGetGasPriceResponse: type: object properties: GasPrice: - $ref: '#/definitions/crosschainGasPrice' - crosschainQueryGetInboundHashToCctxResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.GasPrice' + zetachain.zetacore.crosschain.QueryGetInboundHashToCctxResponse: type: object properties: inboundHashToCctx: - $ref: '#/definitions/crosschainInboundHashToCctx' - crosschainQueryGetLastBlockHeightResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundHashToCctx' + zetachain.zetacore.crosschain.QueryGetLastBlockHeightResponse: type: object properties: LastBlockHeight: - $ref: '#/definitions/crosschainLastBlockHeight' - crosschainQueryGetOutboundTrackerResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.LastBlockHeight' + zetachain.zetacore.crosschain.QueryGetOutboundTrackerResponse: type: object properties: outboundTracker: - $ref: '#/definitions/crosschainOutboundTracker' - crosschainQueryInboundHashToCctxDataResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.OutboundTracker' + zetachain.zetacore.crosschain.QueryInboundHashToCctxDataResponse: type: object properties: CrossChainTxs: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' - crosschainQueryInboundTrackerResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' + zetachain.zetacore.crosschain.QueryInboundTrackerResponse: type: object properties: - inbound_tracker: - $ref: '#/definitions/crosschainInboundTracker' - crosschainQueryLastZetaHeightResponse: + inboundTracker: + $ref: '#/definitions/zetachain.zetacore.crosschain.InboundTracker' + zetachain.zetacore.crosschain.QueryLastZetaHeightResponse: type: object properties: Height: type: string format: int64 - crosschainQueryListPendingCctxResponse: + zetachain.zetacore.crosschain.QueryListPendingCctxResponse: type: object properties: CrossChainTx: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' totalPending: type: string format: uint64 - crosschainQueryListPendingCctxWithinRateLimitResponse: + zetachain.zetacore.crosschain.QueryListPendingCctxWithinRateLimitResponse: type: object properties: - cross_chain_tx: + crossChainTx: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' - total_pending: + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' + totalPending: type: string format: uint64 - current_withdraw_window: + currentWithdrawWindow: type: string format: int64 - current_withdraw_rate: + currentWithdrawRate: type: string - rate_limit_exceeded: + rateLimitExceeded: type: boolean - crosschainQueryMessagePassingProtocolFeeResponse: + zetachain.zetacore.crosschain.QueryMessagePassingProtocolFeeResponse: type: object properties: feeInZeta: type: string - crosschainQueryRateLimiterFlagsResponse: + zetachain.zetacore.crosschain.QueryRateLimiterFlagsResponse: type: object properties: rateLimiterFlags: - $ref: '#/definitions/crosschainRateLimiterFlags' - crosschainQueryRateLimiterInputResponse: + $ref: '#/definitions/zetachain.zetacore.crosschain.RateLimiterFlags' + zetachain.zetacore.crosschain.QueryRateLimiterInputResponse: type: object properties: height: type: string format: int64 - cctxs_missed: + cctxsMissed: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' - cctxs_pending: + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' + cctxsPending: type: array items: type: object - $ref: '#/definitions/crosschainCrossChainTx' - total_pending: + $ref: '#/definitions/zetachain.zetacore.crosschain.CrossChainTx' + totalPending: type: string format: uint64 - past_cctxs_value: + pastCctxsValue: type: string - pending_cctxs_value: + pendingCctxsValue: type: string - lowest_pending_cctx_height: + lowestPendingCctxHeight: type: string format: int64 - crosschainQueryZetaAccountingResponse: + zetachain.zetacore.crosschain.QueryZetaAccountingResponse: type: object properties: - aborted_zeta_amount: + abortedZetaAmount: type: string - crosschainRateLimiterFlags: + zetachain.zetacore.crosschain.RateLimiterFlags: type: object properties: enabled: @@ -59439,24 +58032,56 @@ definitions: type: array items: type: object - $ref: '#/definitions/crosschainConversion' + $ref: '#/definitions/zetachain.zetacore.crosschain.Conversion' title: conversion in azeta per token - crosschainRevertOptions: + zetachain.zetacore.crosschain.RevertOptions: type: object properties: - revert_address: + revertAddress: type: string - call_on_revert: + callOnRevert: type: boolean - abort_address: + abortAddress: type: string - revert_message: + revertMessage: type: string format: byte - revert_gas_limit: + revertGasLimit: type: string title: RevertOptions represents the options for reverting a cctx - crosschainTxFinalizationStatus: + zetachain.zetacore.crosschain.Status: + type: object + properties: + status: + $ref: '#/definitions/zetachain.zetacore.crosschain.CctxStatus' + statusMessage: + type: string + description: |- + status_message carries information about the status transitions: + why they were triggered, old and new status. + errorMessage: + type: string + description: |- + error_message carries information about the error that caused the tx + to be PendingRevert, Reverted or Aborted. + lastUpdateTimestamp: + type: string + format: int64 + isAbortRefunded: + type: boolean + createdTimestamp: + type: string + format: int64 + description: when the CCTX was created. only populated on new transactions. + errorMessageRevert: + type: string + title: |- + error_message_revert carries information about the revert outbound tx , + which is created if the first outbound tx fails + errorMessageAbort: + type: string + title: error_message_abort carries information when aborting the CCTX fails + zetachain.zetacore.crosschain.TxFinalizationStatus: type: string enum: - NotFinalized @@ -59467,58 +58092,75 @@ definitions: - NotFinalized: the corresponding tx is not finalized - Finalized: the corresponding tx is finalized but not executed yet - Executed: the corresponding tx is executed - crosschainTxHash: + zetachain.zetacore.crosschain.TxHash: type: object properties: - tx_hash: + txHash: type: string - tx_signer: + txSigner: type: string proven: type: boolean - cryptoPubKeySet: + zetachain.zetacore.emissions.MsgUpdateParamsResponse: + type: object + zetachain.zetacore.emissions.MsgWithdrawEmissionResponse: + type: object + zetachain.zetacore.emissions.Params: type: object properties: - secp256k1: + validatorEmissionPercentage: type: string - ed25519: + observerEmissionPercentage: type: string - title: PubKeySet contains two pub keys , secp256k1 and ed25519 - emissionsMsgUpdateParamsResponse: - type: object - emissionsMsgWithdrawEmissionResponse: - type: object - emissionsQueryListPoolAddressesResponse: + tssSignerEmissionPercentage: + type: string + observerSlashAmount: + type: string + ballotMaturityBlocks: + type: string + format: int64 + blockRewardAmount: + type: string + title: |- + Params defines the parameters for the module. + Sample values: + ValidatorEmissionPercentage: "00.50", + ObserverEmissionPercentage: "00.25", + TssSignerEmissionPercentage: "00.25", + ObserverSlashAmount: 100000000000000000, + BallotMaturityBlocks: 100, + BlockRewardAmount: 9620949074074074074.074070733466756687, + zetachain.zetacore.emissions.QueryListPoolAddressesResponse: type: object properties: - undistributed_observer_balances_address: + undistributedObserverBalancesAddress: type: string - undistributed_tss_balances_address: + undistributedTssBalancesAddress: type: string - emission_module_address: + emissionModuleAddress: type: string - emissionsQueryParamsResponse: + zetachain.zetacore.emissions.QueryParamsResponse: type: object properties: params: - $ref: '#/definitions/zetacoreemissionsParams' + $ref: '#/definitions/zetachain.zetacore.emissions.Params' description: params holds all the parameters of this module. description: QueryParamsResponse is response type for the Query/Params RPC method. - emissionsQueryShowAvailableEmissionsResponse: + zetachain.zetacore.emissions.QueryShowAvailableEmissionsResponse: type: object properties: amount: type: string - fungibleForeignCoins: + zetachain.zetacore.fungible.ForeignCoins: type: object properties: - zrc20_contract_address: + zrc20ContractAddress: type: string description: index title: string index = 1; asset: type: string - foreign_chain_id: + foreignChainId: type: string format: int64 decimals: @@ -59528,21 +58170,21 @@ definitions: type: string symbol: type: string - coin_type: - $ref: '#/definitions/coinCoinType' - gas_limit: + coinType: + $ref: '#/definitions/zetachain.zetacore.pkg.coin.CoinType' + gasLimit: type: string format: uint64 paused: type: boolean - liquidity_cap: + liquidityCap: type: string - fungibleMsgDeployFungibleCoinZRC20Response: + zetachain.zetacore.fungible.MsgDeployFungibleCoinZRC20Response: type: object properties: address: type: string - fungibleMsgDeploySystemContractsResponse: + zetachain.zetacore.fungible.MsgDeploySystemContractsResponse: type: object properties: uniswapV2Factory: @@ -59555,111 +58197,106 @@ definitions: type: string systemContract: type: string - fungibleMsgPauseZRC20Response: + zetachain.zetacore.fungible.MsgPauseZRC20Response: type: object - fungibleMsgRemoveForeignCoinResponse: + zetachain.zetacore.fungible.MsgRemoveForeignCoinResponse: type: object - fungibleMsgUnpauseZRC20Response: + zetachain.zetacore.fungible.MsgUnpauseZRC20Response: type: object - fungibleMsgUpdateContractBytecodeResponse: + zetachain.zetacore.fungible.MsgUpdateContractBytecodeResponse: type: object - fungibleMsgUpdateGatewayContractResponse: + zetachain.zetacore.fungible.MsgUpdateGatewayContractResponse: type: object - fungibleMsgUpdateSystemContractResponse: + zetachain.zetacore.fungible.MsgUpdateSystemContractResponse: type: object - fungibleMsgUpdateZRC20LiquidityCapResponse: + zetachain.zetacore.fungible.MsgUpdateZRC20LiquidityCapResponse: type: object - fungibleMsgUpdateZRC20NameResponse: + zetachain.zetacore.fungible.MsgUpdateZRC20NameResponse: type: object - fungibleMsgUpdateZRC20WithdrawFeeResponse: + zetachain.zetacore.fungible.MsgUpdateZRC20WithdrawFeeResponse: type: object - fungibleQueryAllForeignCoinsResponse: + zetachain.zetacore.fungible.QueryAllForeignCoinsResponse: type: object properties: foreignCoins: type: array items: type: object - $ref: '#/definitions/fungibleForeignCoins' + $ref: '#/definitions/zetachain.zetacore.fungible.ForeignCoins' pagination: - $ref: '#/definitions/v1beta1PageResponse' - fungibleQueryAllGasStabilityPoolBalanceResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.fungible.QueryAllGasStabilityPoolBalanceResponse: type: object properties: balances: type: array items: type: object - $ref: '#/definitions/QueryAllGasStabilityPoolBalanceResponseBalance' - fungibleQueryCodeHashResponse: + $ref: '#/definitions/zetachain.zetacore.fungible.QueryAllGasStabilityPoolBalanceResponse.Balance' + zetachain.zetacore.fungible.QueryAllGasStabilityPoolBalanceResponse.Balance: type: object properties: - code_hash: + chainId: + type: string + format: int64 + balance: + type: string + zetachain.zetacore.fungible.QueryCodeHashResponse: + type: object + properties: + codeHash: type: string - fungibleQueryGetForeignCoinsResponse: + zetachain.zetacore.fungible.QueryGetForeignCoinsResponse: type: object properties: foreignCoins: - $ref: '#/definitions/fungibleForeignCoins' - fungibleQueryGetGasStabilityPoolAddressResponse: + $ref: '#/definitions/zetachain.zetacore.fungible.ForeignCoins' + zetachain.zetacore.fungible.QueryGetGasStabilityPoolAddressResponse: type: object properties: - cosmos_address: + cosmosAddress: type: string - evm_address: + evmAddress: type: string - fungibleQueryGetGasStabilityPoolBalanceResponse: + zetachain.zetacore.fungible.QueryGetGasStabilityPoolBalanceResponse: type: object properties: balance: type: string - fungibleQueryGetSystemContractResponse: + zetachain.zetacore.fungible.QueryGetSystemContractResponse: type: object properties: SystemContract: - $ref: '#/definitions/fungibleSystemContract' - fungibleSystemContract: + $ref: '#/definitions/zetachain.zetacore.fungible.SystemContract' + zetachain.zetacore.fungible.SystemContract: type: object properties: - system_contract: + systemContract: type: string - connector_zevm: + connectorZevm: type: string gateway: type: string - googlerpcStatus: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - $ref: '#/definitions/protobufAny' - lightclientChainState: + zetachain.zetacore.lightclient.ChainState: type: object properties: - chain_id: + chainId: type: string format: int64 - latest_height: + latestHeight: type: string format: int64 - earliest_height: + earliestHeight: type: string format: int64 - latest_block_hash: + latestBlockHash: type: string format: byte title: ChainState defines the overall state of the block headers for a given chain - lightclientHeaderSupportedChain: + zetachain.zetacore.lightclient.HeaderSupportedChain: type: object properties: - chain_id: + chainId: type: string format: int64 enabled: @@ -59667,106 +58304,106 @@ definitions: title: |- HeaderSupportedChain is a structure containing information of weather a chain is enabled or not for block header verification - lightclientMsgDisableHeaderVerificationResponse: + zetachain.zetacore.lightclient.MsgDisableHeaderVerificationResponse: type: object - lightclientMsgEnableHeaderVerificationResponse: + zetachain.zetacore.lightclient.MsgEnableHeaderVerificationResponse: type: object - lightclientQueryAllBlockHeaderResponse: + zetachain.zetacore.lightclient.QueryAllBlockHeaderResponse: type: object properties: - block_headers: + blockHeaders: type: array items: type: object - $ref: '#/definitions/proofsBlockHeader' + $ref: '#/definitions/zetachain.zetacore.pkg.proofs.BlockHeader' pagination: - $ref: '#/definitions/v1beta1PageResponse' - lightclientQueryAllChainStateResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.lightclient.QueryAllChainStateResponse: type: object properties: - chain_state: + chainState: type: array items: type: object - $ref: '#/definitions/lightclientChainState' + $ref: '#/definitions/zetachain.zetacore.lightclient.ChainState' pagination: - $ref: '#/definitions/v1beta1PageResponse' - lightclientQueryGetBlockHeaderResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.lightclient.QueryGetBlockHeaderResponse: type: object properties: - block_header: - $ref: '#/definitions/proofsBlockHeader' - lightclientQueryGetChainStateResponse: + blockHeader: + $ref: '#/definitions/zetachain.zetacore.pkg.proofs.BlockHeader' + zetachain.zetacore.lightclient.QueryGetChainStateResponse: type: object properties: - chain_state: - $ref: '#/definitions/lightclientChainState' - lightclientQueryHeaderEnabledChainsResponse: + chainState: + $ref: '#/definitions/zetachain.zetacore.lightclient.ChainState' + zetachain.zetacore.lightclient.QueryHeaderEnabledChainsResponse: type: object properties: - header_enabled_chains: + headerEnabledChains: type: array items: type: object - $ref: '#/definitions/lightclientHeaderSupportedChain' - lightclientQueryHeaderSupportedChainsResponse: + $ref: '#/definitions/zetachain.zetacore.lightclient.HeaderSupportedChain' + zetachain.zetacore.lightclient.QueryHeaderSupportedChainsResponse: type: object properties: - header_supported_chains: + headerSupportedChains: type: array items: type: object - $ref: '#/definitions/lightclientHeaderSupportedChain' - lightclientQueryProveResponse: + $ref: '#/definitions/zetachain.zetacore.lightclient.HeaderSupportedChain' + zetachain.zetacore.lightclient.QueryProveResponse: type: object properties: valid: type: boolean - observerBallot: + zetachain.zetacore.observer.Ballot: type: object properties: index: type: string - ballot_identifier: + ballotIdentifier: type: string - voter_list: + voterList: type: array items: type: string votes: type: array items: - $ref: '#/definitions/observerVoteType' - observation_type: - $ref: '#/definitions/observerObservationType' - ballot_threshold: + $ref: '#/definitions/zetachain.zetacore.observer.VoteType' + observationType: + $ref: '#/definitions/zetachain.zetacore.observer.ObservationType' + ballotThreshold: type: string - ballot_status: - $ref: '#/definitions/observerBallotStatus' - ballot_creation_height: + ballotStatus: + $ref: '#/definitions/zetachain.zetacore.observer.BallotStatus' + ballotCreationHeight: type: string format: int64 title: https://github.com/zeta-chain/node/issues/939 - observerBallotStatus: + zetachain.zetacore.observer.BallotStatus: type: string enum: - BallotFinalized_SuccessObservation - BallotFinalized_FailureObservation - BallotInProgress default: BallotFinalized_SuccessObservation - observerBlame: + zetachain.zetacore.observer.Blame: type: object properties: index: type: string - failure_reason: + failureReason: type: string nodes: type: array items: type: object - $ref: '#/definitions/observerNode' - observerChainNonces: + $ref: '#/definitions/zetachain.zetacore.observer.Node' + zetachain.zetacore.observer.ChainNonces: type: object properties: creator: @@ -59774,7 +58411,7 @@ definitions: index: type: string title: 'deprecated(v19): index has been replaced by chain_id for unique identifier' - chain_id: + chainId: type: string format: int64 nonce: @@ -59787,87 +58424,87 @@ definitions: finalizedHeight: type: string format: uint64 - observerChainParams: + zetachain.zetacore.observer.ChainParams: type: object properties: - chain_id: + chainId: type: string format: int64 - confirmation_count: + confirmationCount: type: string format: uint64 title: 'Deprecated(v28): use confirmation_params instead' - gas_price_ticker: + gasPriceTicker: type: string format: uint64 - inbound_ticker: + inboundTicker: type: string format: uint64 - outbound_ticker: + outboundTicker: type: string format: uint64 - watch_utxo_ticker: + watchUtxoTicker: type: string format: uint64 - zeta_token_contract_address: + zetaTokenContractAddress: type: string - connector_contract_address: + connectorContractAddress: type: string - erc20_custody_contract_address: + erc20CustodyContractAddress: type: string - outbound_schedule_interval: + outboundScheduleInterval: type: string format: int64 - outbound_schedule_lookahead: + outboundScheduleLookahead: type: string format: int64 - ballot_threshold: + ballotThreshold: type: string - min_observer_delegation: + minObserverDelegation: type: string - is_supported: + isSupported: type: boolean - gateway_address: + gatewayAddress: type: string - confirmation_params: - $ref: '#/definitions/observerConfirmationParams' + confirmationParams: + $ref: '#/definitions/zetachain.zetacore.observer.ConfirmationParams' title: Advanced confirmation parameters for chain to support fast observation - observerChainParamsList: + zetachain.zetacore.observer.ChainParamsList: type: object properties: - chain_params: + chainParams: type: array items: type: object - $ref: '#/definitions/observerChainParams' - observerConfirmationParams: + $ref: '#/definitions/zetachain.zetacore.observer.ChainParams' + zetachain.zetacore.observer.ConfirmationParams: type: object properties: - safe_inbound_count: + safeInboundCount: type: string format: uint64 description: |- This is the safe number of confirmations to wait before an inbound is considered finalized. - fast_inbound_count: + fastInboundCount: type: string format: uint64 description: |- This is the number of confirmations for fast inbound observation, which is shorter than safe_inbound_count. - safe_outbound_count: + safeOutboundCount: type: string format: uint64 description: |- This is the safe number of confirmations to wait before an outbound is considered finalized. - fast_outbound_count: + fastOutboundCount: type: string format: uint64 description: |- This is the number of confirmations for fast outbound observation, which is shorter than safe_outbound_count. - observerCrosschainFlags: + zetachain.zetacore.observer.CrosschainFlags: type: object properties: isInboundEnabled: @@ -59875,8 +58512,8 @@ definitions: isOutboundEnabled: type: boolean gasPriceIncreaseFlags: - $ref: '#/definitions/observerGasPriceIncreaseFlags' - observerGasPriceIncreaseFlags: + $ref: '#/definitions/zetachain.zetacore.observer.GasPriceIncreaseFlags' + zetachain.zetacore.observer.GasPriceIncreaseFlags: type: object properties: epochLength: @@ -59899,11 +58536,11 @@ definitions: title: |- Maximum number of pending crosschain transactions to check for gas price increase - observerKeygen: + zetachain.zetacore.observer.Keygen: type: object properties: status: - $ref: '#/definitions/observerKeygenStatus' + $ref: '#/definitions/zetachain.zetacore.observer.KeygenStatus' title: 0--to generate key; 1--generated; 2--error granteePubkeys: type: array @@ -59913,76 +58550,76 @@ definitions: type: string format: int64 title: the blocknum that the key needs to be generated - observerKeygenStatus: + zetachain.zetacore.observer.KeygenStatus: type: string enum: - PendingKeygen - KeyGenSuccess - KeyGenFailed default: PendingKeygen - observerLastObserverCount: + zetachain.zetacore.observer.LastObserverCount: type: object properties: count: type: string format: uint64 - last_change_height: + lastChangeHeight: type: string format: int64 - observerMsgAddObserverResponse: + zetachain.zetacore.observer.MsgAddObserverResponse: type: object - observerMsgDisableCCTXResponse: + zetachain.zetacore.observer.MsgDisableCCTXResponse: type: object - observerMsgDisableFastConfirmationResponse: + zetachain.zetacore.observer.MsgDisableFastConfirmationResponse: type: object - observerMsgEnableCCTXResponse: + zetachain.zetacore.observer.MsgEnableCCTXResponse: type: object - observerMsgRemoveChainParamsResponse: + zetachain.zetacore.observer.MsgRemoveChainParamsResponse: type: object - observerMsgResetChainNoncesResponse: + zetachain.zetacore.observer.MsgResetChainNoncesResponse: type: object - observerMsgUpdateChainParamsResponse: + zetachain.zetacore.observer.MsgUpdateChainParamsResponse: type: object - observerMsgUpdateGasPriceIncreaseFlagsResponse: + zetachain.zetacore.observer.MsgUpdateGasPriceIncreaseFlagsResponse: type: object - observerMsgUpdateKeygenResponse: + zetachain.zetacore.observer.MsgUpdateKeygenResponse: type: object - observerMsgUpdateObserverResponse: + zetachain.zetacore.observer.MsgUpdateObserverResponse: type: object - observerMsgUpdateOperationalChainParamsResponse: + zetachain.zetacore.observer.MsgUpdateOperationalChainParamsResponse: type: object - observerMsgUpdateOperationalFlagsResponse: + zetachain.zetacore.observer.MsgUpdateOperationalFlagsResponse: type: object - observerMsgVoteBlameResponse: + zetachain.zetacore.observer.MsgVoteBlameResponse: type: object - observerMsgVoteBlockHeaderResponse: + zetachain.zetacore.observer.MsgVoteBlockHeaderResponse: type: object properties: - ballot_created: + ballotCreated: type: boolean - vote_finalized: + voteFinalized: type: boolean - observerMsgVoteTSSResponse: + zetachain.zetacore.observer.MsgVoteTSSResponse: type: object properties: - ballot_created: + ballotCreated: type: boolean - vote_finalized: + voteFinalized: type: boolean - keygen_success: + keygenSuccess: type: boolean - observerNode: + zetachain.zetacore.observer.Node: type: object properties: - pub_key: + pubKey: type: string - blame_data: + blameData: type: string format: byte - blame_signature: + blameSignature: type: string format: byte - observerNodeAccount: + zetachain.zetacore.observer.NodeAccount: type: object properties: operator: @@ -59990,10 +58627,10 @@ definitions: granteeAddress: type: string granteePubkey: - $ref: '#/definitions/cryptoPubKeySet' + $ref: '#/definitions/zetachain.zetacore.pkg.crypto.PubKeySet' nodeStatus: - $ref: '#/definitions/observerNodeStatus' - observerNodeStatus: + $ref: '#/definitions/zetachain.zetacore.observer.NodeStatus' + zetachain.zetacore.observer.NodeStatus: type: string enum: - Unknown @@ -60003,7 +58640,7 @@ definitions: - Active - Disabled default: Unknown - observerObservationType: + zetachain.zetacore.observer.ObservationType: type: string enum: - EmptyObserverType @@ -60012,243 +58649,243 @@ definitions: - TSSKeyGen - TSSKeySign default: EmptyObserverType - observerObserverUpdateReason: + zetachain.zetacore.observer.ObserverUpdateReason: type: string enum: - Undefined - Tombstoned - AdminUpdate default: Undefined - observerOperationalFlags: + zetachain.zetacore.observer.OperationalFlags: type: object properties: - restart_height: + restartHeight: type: string format: int64 description: |- Height for a coordinated zetaclient restart. Will be ignored if missed. - signer_block_time_offset: + signerBlockTimeOffset: type: string description: |- Offset from the zetacore block time to initiate signing. Should be calculated and set based on max(zetaclient_core_block_latency). - minimum_version: + minimumVersion: type: string description: |- Minimum version of zetaclient that is allowed to run. This must be either a valid semver string (v23.0.1) or empty. If empty, all versions are allowed. description: Flags for the top-level operation of zetaclient. - observerPendingNonces: + zetachain.zetacore.observer.PendingNonces: type: object properties: - nonce_low: + nonceLow: type: string format: int64 - nonce_high: + nonceHigh: type: string format: int64 - chain_id: + chainId: type: string format: int64 tss: type: string title: store key is tss+chainid - observerQueryAllBlameRecordsResponse: + zetachain.zetacore.observer.QueryAllBlameRecordsResponse: type: object properties: - blame_info: + blameInfo: type: array items: type: object - $ref: '#/definitions/observerBlame' + $ref: '#/definitions/zetachain.zetacore.observer.Blame' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerQueryAllChainNoncesResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.QueryAllChainNoncesResponse: type: object properties: ChainNonces: type: array items: type: object - $ref: '#/definitions/observerChainNonces' + $ref: '#/definitions/zetachain.zetacore.observer.ChainNonces' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerQueryAllNodeAccountResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.QueryAllNodeAccountResponse: type: object properties: NodeAccount: type: array items: type: object - $ref: '#/definitions/observerNodeAccount' + $ref: '#/definitions/zetachain.zetacore.observer.NodeAccount' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerQueryAllPendingNoncesResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.QueryAllPendingNoncesResponse: type: object properties: - pending_nonces: + pendingNonces: type: array items: type: object - $ref: '#/definitions/observerPendingNonces' + $ref: '#/definitions/zetachain.zetacore.observer.PendingNonces' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerQueryBallotByIdentifierResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.QueryBallotByIdentifierResponse: type: object properties: - ballot_identifier: + ballotIdentifier: type: string voters: type: array items: type: object - $ref: '#/definitions/observerVoterList' - observation_type: - $ref: '#/definitions/observerObservationType' - ballot_status: - $ref: '#/definitions/observerBallotStatus' - observerQueryBallotsResponse: + $ref: '#/definitions/zetachain.zetacore.observer.VoterList' + observationType: + $ref: '#/definitions/zetachain.zetacore.observer.ObservationType' + ballotStatus: + $ref: '#/definitions/zetachain.zetacore.observer.BallotStatus' + zetachain.zetacore.observer.QueryBallotsResponse: type: object properties: ballots: type: array items: type: object - $ref: '#/definitions/observerBallot' + $ref: '#/definitions/zetachain.zetacore.observer.Ballot' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerQueryBlameByChainAndNonceResponse: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.QueryBlameByChainAndNonceResponse: type: object properties: - blame_info: + blameInfo: type: array items: type: object - $ref: '#/definitions/observerBlame' - observerQueryBlameByIdentifierResponse: + $ref: '#/definitions/zetachain.zetacore.observer.Blame' + zetachain.zetacore.observer.QueryBlameByIdentifierResponse: type: object properties: - blame_info: - $ref: '#/definitions/observerBlame' - observerQueryGetChainNoncesResponse: + blameInfo: + $ref: '#/definitions/zetachain.zetacore.observer.Blame' + zetachain.zetacore.observer.QueryGetChainNoncesResponse: type: object properties: ChainNonces: - $ref: '#/definitions/observerChainNonces' - observerQueryGetChainParamsForChainResponse: + $ref: '#/definitions/zetachain.zetacore.observer.ChainNonces' + zetachain.zetacore.observer.QueryGetChainParamsForChainResponse: type: object properties: - chain_params: - $ref: '#/definitions/observerChainParams' - observerQueryGetChainParamsResponse: + chainParams: + $ref: '#/definitions/zetachain.zetacore.observer.ChainParams' + zetachain.zetacore.observer.QueryGetChainParamsResponse: type: object properties: - chain_params: - $ref: '#/definitions/observerChainParamsList' - observerQueryGetCrosschainFlagsResponse: + chainParams: + $ref: '#/definitions/zetachain.zetacore.observer.ChainParamsList' + zetachain.zetacore.observer.QueryGetCrosschainFlagsResponse: type: object properties: - crosschain_flags: - $ref: '#/definitions/observerCrosschainFlags' - observerQueryGetKeygenResponse: + crosschainFlags: + $ref: '#/definitions/zetachain.zetacore.observer.CrosschainFlags' + zetachain.zetacore.observer.QueryGetKeygenResponse: type: object properties: keygen: - $ref: '#/definitions/observerKeygen' - observerQueryGetNodeAccountResponse: + $ref: '#/definitions/zetachain.zetacore.observer.Keygen' + zetachain.zetacore.observer.QueryGetNodeAccountResponse: type: object properties: - node_account: - $ref: '#/definitions/observerNodeAccount' - observerQueryGetTSSResponse: + nodeAccount: + $ref: '#/definitions/zetachain.zetacore.observer.NodeAccount' + zetachain.zetacore.observer.QueryGetTSSResponse: type: object properties: TSS: - $ref: '#/definitions/observerTSS' - observerQueryGetTssAddressByFinalizedHeightResponse: + $ref: '#/definitions/zetachain.zetacore.observer.TSS' + zetachain.zetacore.observer.QueryGetTssAddressByFinalizedHeightResponse: type: object properties: eth: type: string btc: type: string - observerQueryGetTssAddressResponse: + zetachain.zetacore.observer.QueryGetTssAddressResponse: type: object properties: eth: type: string btc: type: string - observerQueryHasVotedResponse: + zetachain.zetacore.observer.QueryHasVotedResponse: type: object properties: - has_voted: + hasVoted: type: boolean - observerQueryObserverSetResponse: + zetachain.zetacore.observer.QueryObserverSetResponse: type: object properties: observers: type: array items: type: string - observerQueryOperationalFlagsResponse: + zetachain.zetacore.observer.QueryOperationalFlagsResponse: type: object properties: - operational_flags: - $ref: '#/definitions/observerOperationalFlags' - observerQueryPendingNoncesByChainResponse: + operationalFlags: + $ref: '#/definitions/zetachain.zetacore.observer.OperationalFlags' + zetachain.zetacore.observer.QueryPendingNoncesByChainResponse: type: object properties: - pending_nonces: - $ref: '#/definitions/observerPendingNonces' - observerQueryShowObserverCountResponse: + pendingNonces: + $ref: '#/definitions/zetachain.zetacore.observer.PendingNonces' + zetachain.zetacore.observer.QueryShowObserverCountResponse: type: object properties: - last_observer_count: - $ref: '#/definitions/observerLastObserverCount' - observerQuerySupportedChainsResponse: + lastObserverCount: + $ref: '#/definitions/zetachain.zetacore.observer.LastObserverCount' + zetachain.zetacore.observer.QuerySupportedChainsResponse: type: object properties: chains: type: array items: type: object - $ref: '#/definitions/chainsChain' - observerQueryTssFundsMigratorInfoAllResponse: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.Chain' + zetachain.zetacore.observer.QueryTssFundsMigratorInfoAllResponse: type: object properties: - tss_funds_migrators: + tssFundsMigrators: type: array items: type: object - $ref: '#/definitions/observerTssFundMigratorInfo' - observerQueryTssFundsMigratorInfoResponse: + $ref: '#/definitions/zetachain.zetacore.observer.TssFundMigratorInfo' + zetachain.zetacore.observer.QueryTssFundsMigratorInfoResponse: type: object properties: - tss_funds_migrator: - $ref: '#/definitions/observerTssFundMigratorInfo' - observerQueryTssHistoryResponse: + tssFundsMigrator: + $ref: '#/definitions/zetachain.zetacore.observer.TssFundMigratorInfo' + zetachain.zetacore.observer.QueryTssHistoryResponse: type: object properties: - tss_list: + tssList: type: array items: type: object - $ref: '#/definitions/observerTSS' + $ref: '#/definitions/zetachain.zetacore.observer.TSS' pagination: - $ref: '#/definitions/v1beta1PageResponse' - observerTSS: + $ref: '#/definitions/cosmos.base.query.v1beta1.PageResponse' + zetachain.zetacore.observer.TSS: type: object properties: - tss_pubkey: + tssPubkey: type: string - tss_participant_list: + tssParticipantList: type: array items: type: string - operator_address_list: + operatorAddressList: type: array items: type: string @@ -60258,15 +58895,15 @@ definitions: keyGenZetaHeight: type: string format: int64 - observerTssFundMigratorInfo: + zetachain.zetacore.observer.TssFundMigratorInfo: type: object properties: - chain_id: + chainId: type: string format: int64 - migration_cctx_index: + migrationCctxIndex: type: string - observerVoteType: + zetachain.zetacore.observer.VoteType: type: string enum: - SuccessObservation @@ -60277,21 +58914,189 @@ definitions: - FailureObservation: Failure observation means , the the message that - NotYetVoted: this voter is observing failed / reverted . It does not mean it was unable to observe. - observerVoterList: + zetachain.zetacore.observer.VoterList: type: object properties: - voter_address: + voterAddress: type: string - vote_type: - $ref: '#/definitions/observerVoteType' - pkgproofsProof: + voteType: + $ref: '#/definitions/zetachain.zetacore.observer.VoteType' + zetachain.zetacore.pkg.chains.CCTXGateway: + type: string + enum: + - zevm + - observers + default: zevm + description: |- + - zevm: zevm is the internal CCTX gateway to process outbound on the ZEVM and read + inbound events from the ZEVM only used for ZetaChain chains + - observers: observers is the CCTX gateway for chains relying on the observer set to + observe inbounds and TSS for outbounds + title: CCTXGateway describes for the chain the gateway used to handle CCTX outbounds + zetachain.zetacore.pkg.chains.Chain: + type: object + properties: + chainId: + type: string + format: int64 + title: ChainId is the unique identifier of the chain + chainName: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.ChainName' + title: |- + ChainName is the name of the chain + Deprecated(v19): replaced with Name + network: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.Network' + title: Network is the network of the chain + networkType: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.NetworkType' + description: 'NetworkType is the network type of the chain: mainnet, testnet, etc..' + vm: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.Vm' + title: Vm is the virtual machine used in the chain + consensus: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.Consensus' + title: Consensus is the underlying consensus algorithm used by the chain + isExternal: + type: boolean + title: IsExternal describe if the chain is ZetaChain or external + cctxGateway: + $ref: '#/definitions/zetachain.zetacore.pkg.chains.CCTXGateway' + title: CCTXGateway is the gateway used to handle CCTX outbounds + name: + type: string + title: Name is the name of the chain + title: |- + Chain represents static data about a blockchain network + it is identified by a unique chain ID + zetachain.zetacore.pkg.chains.ChainName: + type: string + enum: + - empty + - eth_mainnet + - zeta_mainnet + - btc_mainnet + - polygon_mainnet + - bsc_mainnet + - goerli_testnet + - mumbai_testnet + - bsc_testnet + - zeta_testnet + - btc_testnet + - sepolia_testnet + - goerli_localnet + - btc_regtest + - amoy_testnet + - optimism_mainnet + - optimism_sepolia + - base_mainnet + - base_sepolia + - solana_mainnet + - solana_devnet + - solana_localnet + default: empty + title: |- + ChainName represents the name of the chain + Deprecated(v19): replaced with Chain.Name as string + zetachain.zetacore.pkg.chains.Consensus: + type: string + enum: + - ethereum + - tendermint + - bitcoin + - op_stack + - solana_consensus + - catchain_consensus + - snowman + - arbitrum_nitro + - sui_consensus + default: ethereum + description: |- + - catchain_consensus: ton + - snowman: avalanche + title: |- + Consensus represents the consensus algorithm used by the chain + this can represent the consensus of a L1 + this can also represent the solution of a L2 + zetachain.zetacore.pkg.chains.Network: + type: string + enum: + - eth + - zeta + - btc + - polygon + - bsc + - optimism + - base + - solana + - ton + - avalanche + - arbitrum + - worldchain + - sui + default: eth + title: |- + Network represents the network of the chain + there is a single instance of the network on mainnet + then the network can have eventual testnets or devnets + zetachain.zetacore.pkg.chains.NetworkType: + type: string + enum: + - mainnet + - testnet + - privnet + - devnet + default: mainnet + title: |- + NetworkType represents the network type of the chain + Mainnet, Testnet, Privnet, Devnet + zetachain.zetacore.pkg.chains.ReceiveStatus: + type: string + enum: + - created + - success + - failed + default: created + description: '- created: Created is used for inbounds' + title: |- + ReceiveStatus represents the status of an outbound + TODO: Rename and move + https://github.com/zeta-chain/node/issues/2257 + zetachain.zetacore.pkg.chains.Vm: + type: string + enum: + - no_vm + - evm + - svm + - tvm + - mvm_sui + default: no_vm + title: |- + Vm represents the virtual machine type of the chain to support smart + contracts + zetachain.zetacore.pkg.coin.CoinType: + type: string + enum: + - Zeta + - Gas + - ERC20 + - Cmd + - NoAssetCall + default: Zeta + title: |- + - Gas: Ether, BNB, Matic, Klay, BTC, etc + - ERC20: ERC20 token + - Cmd: no asset, used for admin command + - NoAssetCall: no asset, used for contract call + zetachain.zetacore.pkg.crypto.PubKeySet: type: object properties: - ethereum_proof: - $ref: '#/definitions/proofsethereumProof' - bitcoin_proof: - $ref: '#/definitions/proofsbitcoinProof' - proofsBlockHeader: + secp256k1: + type: string + ed25519: + type: string + title: PubKeySet contains two pub keys , secp256k1 and ed25519 + zetachain.zetacore.pkg.proofs.BlockHeader: type: object properties: height: @@ -60300,30 +59105,37 @@ definitions: hash: type: string format: byte - parent_hash: + parentHash: type: string format: byte - chain_id: + chainId: type: string format: int64 header: - $ref: '#/definitions/proofsHeaderData' + $ref: '#/definitions/zetachain.zetacore.pkg.proofs.HeaderData' title: chain specific header - proofsHeaderData: + zetachain.zetacore.pkg.proofs.HeaderData: type: object properties: - ethereum_header: + ethereumHeader: type: string format: byte title: binary encoded headers; RLP for ethereum - bitcoin_header: + bitcoinHeader: type: string format: byte title: 80-byte little-endian encoded binary data - proofsbitcoinProof: + zetachain.zetacore.pkg.proofs.Proof: type: object properties: - tx_bytes: + ethereumProof: + $ref: '#/definitions/zetachain.zetacore.pkg.proofs.ethereum.Proof' + bitcoinProof: + $ref: '#/definitions/zetachain.zetacore.pkg.proofs.bitcoin.Proof' + zetachain.zetacore.pkg.proofs.bitcoin.Proof: + type: object + properties: + txBytes: type: string format: byte path: @@ -60332,7 +59144,7 @@ definitions: index: type: integer format: int64 - proofsethereumProof: + zetachain.zetacore.pkg.proofs.ethereum.Proof: type: object properties: keys: @@ -60345,137 +59157,6 @@ definitions: items: type: string format: byte - protobufAny: - type: object - properties: - '@type': - type: string - additionalProperties: {} - v1beta1PageRequest: - type: object - properties: - key: - type: string - format: byte - description: |- - key is a value returned in PageResponse.next_key to begin - querying the next page most efficiently. Only one of offset or key - should be set. - offset: - type: string - format: uint64 - description: |- - offset is a numeric offset that can be used when key is unavailable. - It is less efficient than using key. Only one of offset or key should - be set. - limit: - type: string - format: uint64 - description: |- - limit is the total number of results to be returned in the result page. - If left empty it will default to a value to be set by each app. - count_total: - type: boolean - description: |- - count_total is set to true to indicate that the result set should include - a count of the total number of items available for pagination in UIs. - count_total is only respected when offset is used. It is ignored when key - is set. - reverse: - type: boolean - description: |- - reverse is set to true if results are to be returned in the descending order. - - Since: cosmos-sdk 0.43 - description: |- - message SomeRequest { - Foo some_parameter = 1; - PageRequest pagination = 2; - } - title: |- - PageRequest is to be embedded in gRPC request messages for efficient - pagination. Ex: - v1beta1PageResponse: - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: |- - total is total number of results available if PageRequest.count_total - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. - - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - zetacorecrosschainStatus: - type: object - properties: - status: - $ref: '#/definitions/crosschainCctxStatus' - status_message: - type: string - description: |- - status_message carries information about the status transitions: - why they were triggered, old and new status. - error_message: - type: string - description: |- - error_message carries information about the error that caused the tx - to be PendingRevert, Reverted or Aborted. - lastUpdate_timestamp: - type: string - format: int64 - isAbortRefunded: - type: boolean - created_timestamp: - type: string - format: int64 - description: when the CCTX was created. only populated on new transactions. - error_message_revert: - type: string - title: |- - error_message_revert carries information about the revert outbound tx , - which is created if the first outbound tx fails - error_message_abort: - type: string - title: error_message_abort carries information when aborting the CCTX fails - zetacoreemissionsParams: - type: object - properties: - validator_emission_percentage: - type: string - observer_emission_percentage: - type: string - tss_signer_emission_percentage: - type: string - observer_slash_amount: - type: string - ballot_maturity_blocks: - type: string - format: int64 - block_reward_amount: - type: string - title: |- - Params defines the parameters for the module. - Sample values: - ValidatorEmissionPercentage: "00.50", - ObserverEmissionPercentage: "00.25", - TssSignerEmissionPercentage: "00.25", - ObserverSlashAmount: 100000000000000000, - BallotMaturityBlocks: 100, - BlockRewardAmount: 9620949074074074074.074070733466756687, ethermint.evm.v1.ChainConfig: type: object properties: diff --git a/proto/buf.openapi.yaml b/proto/buf.openapi.yaml index 0a2666f814..f478fe587e 100644 --- a/proto/buf.openapi.yaml +++ b/proto/buf.openapi.yaml @@ -6,4 +6,4 @@ plugins: - name: openapiv2 out: . strategy: all - opt: allow_merge=true,merge_file_name=zetachain,output_format=yaml,json_names_for_fields=false + opt: allow_merge=true,merge_file_name=zetachain,output_format=yaml,openapi_naming_strategy=fqn,simple_operation_ids=true,enable_rpc_deprecation=true diff --git a/proto/zetachain/zetacore/crosschain/query.proto b/proto/zetachain/zetacore/crosschain/query.proto index fc0fe34d85..23e4b0d218 100644 --- a/proto/zetachain/zetacore/crosschain/query.proto +++ b/proto/zetachain/zetacore/crosschain/query.proto @@ -166,12 +166,14 @@ service Query { returns (QueryGetOutboundTrackerResponse) { option (google.api.http).get = "/zeta-chain/crosschain/outTxTracker/{chainID}/{nonce}"; + option deprecated = true; } // Deprecated(v17): use OutboundTrackerAll rpc OutTxTrackerAll(QueryAllOutboundTrackerRequest) returns (QueryAllOutboundTrackerResponse) { option (google.api.http).get = "/zeta-chain/crosschain/outTxTracker"; + option deprecated = true; } // Deprecated(v17): use OutboundTrackerAllByChain @@ -179,6 +181,7 @@ service Query { returns (QueryAllOutboundTrackerByChainResponse) { option (google.api.http).get = "/zeta-chain/crosschain/outTxTrackerByChain/{chain}"; + option deprecated = true; } // Deprecated(v17): use InboundTrackerAllByChain @@ -186,12 +189,14 @@ service Query { returns (QueryAllInboundTrackerByChainResponse) { option (google.api.http).get = "/zeta-chain/crosschain/inTxTrackerByChain/{chain_id}"; + option deprecated = true; } // Deprecated(v17): use InboundTrackerAll rpc InTxTrackerAll(QueryAllInboundTrackersRequest) returns (QueryAllInboundTrackersResponse) { option (google.api.http).get = "/zeta-chain/crosschain/inTxTracker"; + option deprecated = true; } // Deprecated(v17): use InboundHashToCctx @@ -199,6 +204,7 @@ service Query { returns (QueryGetInboundHashToCctxResponse) { option (google.api.http).get = "/zeta-chain/crosschain/inTxHashToCctx/{inboundHash}"; + option deprecated = true; } // Deprecated(v17): use InboundHashToCctxData @@ -206,12 +212,14 @@ service Query { returns (QueryInboundHashToCctxDataResponse) { option (google.api.http).get = "/zeta-chain/crosschain/inTxHashToCctxData/{inboundHash}"; + option deprecated = true; } // Deprecated(v17): use InboundHashToCctxAll rpc InTxHashToCctxAll(QueryAllInboundHashToCctxRequest) returns (QueryAllInboundHashToCctxResponse) { option (google.api.http).get = "/zeta-chain/crosschain/inTxHashToCctx"; + option deprecated = true; } } diff --git a/proto/zetachain/zetacore/lightclient/query.proto b/proto/zetachain/zetacore/lightclient/query.proto index 77f2fdeb1d..e07c0077ab 100644 --- a/proto/zetachain/zetacore/lightclient/query.proto +++ b/proto/zetachain/zetacore/lightclient/query.proto @@ -17,39 +17,46 @@ service Query { rpc BlockHeaderAll(QueryAllBlockHeaderRequest) returns (QueryAllBlockHeaderResponse) { option (google.api.http).get = "/zeta-chain/lightclient/block_headers"; + option deprecated = true; } rpc BlockHeader(QueryGetBlockHeaderRequest) returns (QueryGetBlockHeaderResponse) { option (google.api.http).get = "/zeta-chain/lightclient/block_headers/{block_hash}"; + option deprecated = true; } rpc ChainStateAll(QueryAllChainStateRequest) returns (QueryAllChainStateResponse) { option (google.api.http).get = "/zeta-chain/lightclient/chain_state"; + option deprecated = true; } rpc ChainState(QueryGetChainStateRequest) returns (QueryGetChainStateResponse) { option (google.api.http).get = "/zeta-chain/lightclient/chain_state/{chain_id}"; + option deprecated = true; } rpc Prove(QueryProveRequest) returns (QueryProveResponse) { option (google.api.http).get = "/zeta-chain/lightclient/prove"; + option deprecated = true; } rpc HeaderSupportedChains(QueryHeaderSupportedChainsRequest) returns (QueryHeaderSupportedChainsResponse) { option (google.api.http).get = "/zeta-chain/lightclient/header_supported_chains"; + option deprecated = true; } rpc HeaderEnabledChains(QueryHeaderEnabledChainsRequest) returns (QueryHeaderEnabledChainsResponse) { option (google.api.http).get = "/zeta-chain/lightclient/header_enabled_chains"; + option deprecated = true; } } diff --git a/scripts/protoc-gen-openapi.sh b/scripts/protoc-gen-openapi.sh index 3a4a40e097..d668b93443 100755 --- a/scripts/protoc-gen-openapi.sh +++ b/scripts/protoc-gen-openapi.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.16.2 +set -eo pipefail + +go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.26.1 go mod download @@ -54,12 +56,19 @@ keys=$(yq e '.paths | keys' $OUTPUT_DIR/$MERGED_SWAGGER_FILE) for key in $keys; do # Check if key starts with '/cosmos/NAME' if [[ $key == "/cosmos/"* ]]; then + # Exclude paths starting with /cosmos/gov/v1beta1 + # these endpoints are broken post v0.47 upgrade + if [[ $key == "/cosmos/gov/v1beta1"* ]]; then + yq e "del(.paths.\"$key\")" -i $OUTPUT_DIR/$MERGED_SWAGGER_FILE + continue + fi + # Extract NAME name=$(echo $key | cut -d '/' -f 3) # Check if the standard module is not imported in the app.go if ! grep -q "github.com/cosmos/cosmos-sdk/x/$name" $APP_GO; then - # Keep the standard "base" and "tx" endpoints - if [[ $name == "base" || $name == "tx" ]]; then + # Keep the standard "base", "tx", and "upgrade" endpoints + if [[ $name == "base" || $name == "tx" || $name == "upgrade" ]]; then continue fi # If not found, delete the key from the YAML file in-place diff --git a/x/crosschain/types/query.pb.go b/x/crosschain/types/query.pb.go index fd3a5f8067..f911313fdc 100644 --- a/x/crosschain/types/query.pb.go +++ b/x/crosschain/types/query.pb.go @@ -2376,157 +2376,157 @@ func init() { } var fileDescriptor_d00cb546ea76908b = []byte{ - // 2386 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0xdd, 0x6f, 0x1c, 0x57, - 0x15, 0xcf, 0xf5, 0xc6, 0x8e, 0x7d, 0x9d, 0xd8, 0xf1, 0x8d, 0x13, 0xbb, 0x5b, 0xc7, 0x49, 0x27, - 0x4d, 0xec, 0x3a, 0x78, 0x37, 0xb1, 0x63, 0xe7, 0xb3, 0x4d, 0xfc, 0x91, 0x38, 0x46, 0x4e, 0xe2, - 0xae, 0x2c, 0x82, 0x02, 0x62, 0x34, 0x9e, 0xbd, 0x9d, 0x1d, 0x3a, 0x9e, 0xd9, 0xce, 0xcc, 0xc6, - 0x9b, 0x5a, 0x96, 0xa0, 0x12, 0x0f, 0xbc, 0x21, 0x55, 0x88, 0x17, 0x5e, 0x11, 0x48, 0xf0, 0xd0, - 0x07, 0xd4, 0x17, 0x04, 0x12, 0xdf, 0x11, 0x05, 0x29, 0x14, 0x09, 0x21, 0x1e, 0x50, 0x49, 0x10, - 0x7d, 0xe7, 0x2f, 0x40, 0x73, 0xe7, 0xcc, 0xee, 0x7c, 0xcf, 0xdd, 0xf5, 0x46, 0x72, 0x9f, 0x3c, - 0x33, 0xf7, 0x9e, 0x73, 0x7f, 0xbf, 0x73, 0xce, 0xfd, 0xfa, 0xad, 0xf1, 0x1b, 0xef, 0x53, 0x5b, - 0x92, 0x2b, 0x92, 0xaa, 0x17, 0xd9, 0x93, 0x61, 0xd2, 0xa2, 0x6c, 0x1a, 0x96, 0xe5, 0x7e, 0x7b, - 0xaf, 0x46, 0xcd, 0x27, 0x85, 0xaa, 0x69, 0xd8, 0x06, 0x39, 0xd9, 0xe8, 0x5a, 0xf0, 0xba, 0x16, - 0x9a, 0x5d, 0xf3, 0x53, 0xb2, 0x61, 0x6d, 0x19, 0x56, 0x71, 0x53, 0xb2, 0xa8, 0x6b, 0x57, 0x7c, - 0x7c, 0x71, 0x93, 0xda, 0xd2, 0xc5, 0x62, 0x55, 0x52, 0x54, 0x5d, 0xb2, 0x55, 0x43, 0x77, 0x5d, - 0xe5, 0x67, 0xd2, 0x47, 0x65, 0x8f, 0x22, 0x7b, 0x16, 0xed, 0x3a, 0xd8, 0x4c, 0xa7, 0xdb, 0x28, - 0x92, 0x25, 0x56, 0x4d, 0x55, 0xa6, 0xd0, 0xfd, 0x4a, 0x7a, 0x77, 0x55, 0xdf, 0x34, 0x6a, 0x7a, - 0x59, 0xac, 0x48, 0x56, 0x45, 0xb4, 0x0d, 0x51, 0x96, 0x1b, 0x03, 0xcd, 0xf2, 0x59, 0xda, 0xa6, - 0x24, 0xbf, 0x4b, 0x4d, 0x30, 0x9a, 0x4b, 0x37, 0xd2, 0x24, 0xcb, 0x16, 0x37, 0x35, 0x43, 0x7e, - 0x57, 0xac, 0x50, 0x55, 0xa9, 0xd8, 0x60, 0x76, 0x29, 0xdd, 0xcc, 0xa8, 0xd9, 0x71, 0x83, 0xcd, - 0xa7, 0x5b, 0x99, 0x92, 0x4d, 0x45, 0x4d, 0xdd, 0x52, 0x6d, 0x6a, 0x8a, 0xef, 0x68, 0x92, 0x62, - 0x81, 0xdd, 0xb0, 0x62, 0x28, 0x06, 0x7b, 0x2c, 0x3a, 0x4f, 0xf0, 0x75, 0x4c, 0x31, 0x0c, 0x45, - 0xa3, 0x45, 0xa9, 0xaa, 0x16, 0x25, 0x5d, 0x37, 0x6c, 0x96, 0x29, 0xcf, 0x66, 0x04, 0xd2, 0xba, - 0x65, 0x29, 0xc5, 0xc7, 0x17, 0x9d, 0x3f, 0x6e, 0x83, 0x30, 0x86, 0xf3, 0x6f, 0x3b, 0x59, 0x7e, - 0x44, 0x6d, 0x69, 0x41, 0x96, 0x8d, 0x9a, 0x6e, 0xab, 0xba, 0x52, 0xa2, 0xef, 0xd5, 0xa8, 0x65, - 0x0b, 0xf7, 0xf0, 0xab, 0xb1, 0xad, 0x56, 0xd5, 0xd0, 0x2d, 0x4a, 0x0a, 0xf8, 0x98, 0xb4, 0x69, - 0x98, 0x36, 0x2d, 0x8b, 0x0e, 0x03, 0x51, 0xda, 0x72, 0x7a, 0x8c, 0xa2, 0xd3, 0x68, 0xb2, 0xaf, - 0x34, 0x04, 0x4d, 0xcc, 0x96, 0x35, 0x08, 0xeb, 0x78, 0x9c, 0xb9, 0x5b, 0xa1, 0xf6, 0x03, 0x88, - 0xc9, 0x86, 0x1b, 0x12, 0x18, 0x90, 0x8c, 0xe2, 0x43, 0x8c, 0xfd, 0xea, 0x32, 0xf3, 0x92, 0x2b, - 0x79, 0xaf, 0x64, 0x18, 0x77, 0xeb, 0x86, 0x2e, 0xd3, 0xd1, 0xae, 0xd3, 0x68, 0xf2, 0x60, 0xc9, - 0x7d, 0x11, 0xbe, 0x8d, 0xf0, 0xa9, 0x44, 0x97, 0x80, 0xf2, 0x1b, 0x78, 0xd0, 0x08, 0x36, 0x31, - 0xdf, 0xfd, 0x33, 0x85, 0x42, 0xea, 0x5c, 0x28, 0x84, 0x1c, 0x2e, 0x1e, 0x7c, 0xfa, 0xaf, 0x53, - 0x07, 0x4a, 0x61, 0x67, 0x42, 0x05, 0x58, 0x2d, 0x68, 0x5a, 0x02, 0xab, 0x3b, 0x18, 0x37, 0x27, - 0x0f, 0x0c, 0x7e, 0xae, 0xe0, 0xa6, 0xa4, 0xe0, 0xcc, 0xb4, 0x82, 0x3b, 0x43, 0x61, 0xa6, 0x15, - 0xd6, 0x25, 0x85, 0x82, 0x6d, 0xc9, 0x67, 0x29, 0xfc, 0xc9, 0x63, 0x1b, 0x37, 0x54, 0x1a, 0xdb, - 0x5c, 0xc7, 0xd8, 0x92, 0x95, 0x00, 0x97, 0x2e, 0xc6, 0x65, 0x22, 0x93, 0x8b, 0x0b, 0x2e, 0x40, - 0xe6, 0x3b, 0x08, 0x9f, 0x4d, 0x20, 0xb3, 0xf8, 0x64, 0xc9, 0x81, 0xe4, 0x85, 0x6f, 0x18, 0x77, - 0x33, 0x88, 0x50, 0x12, 0xee, 0x4b, 0x28, 0xa8, 0x5d, 0x6d, 0x07, 0xf5, 0xaf, 0x08, 0x9f, 0xcb, - 0xc2, 0xf1, 0x45, 0x8b, 0xed, 0x77, 0x11, 0x7e, 0xdd, 0xe3, 0xb4, 0xaa, 0xa7, 0x84, 0xf6, 0x15, - 0xdc, 0xeb, 0x2e, 0xd0, 0x6a, 0x39, 0x38, 0xe1, 0xca, 0x1d, 0x8b, 0xef, 0x5f, 0x7c, 0x79, 0x4e, - 0xc0, 0x02, 0xe1, 0xfd, 0x1a, 0x1e, 0x50, 0xf5, 0x98, 0xe8, 0x4e, 0x67, 0x44, 0x37, 0xe4, 0xd5, - 0x0d, 0x6e, 0xc8, 0x55, 0xe7, 0x62, 0xeb, 0x9b, 0xee, 0xc1, 0x81, 0xad, 0x4e, 0x4f, 0xf7, 0x3f, - 0xfa, 0xa6, 0x7b, 0x64, 0xa8, 0x2f, 0x54, 0xcc, 0x96, 0xf1, 0x69, 0x6f, 0x95, 0x86, 0x81, 0xef, - 0x4a, 0x56, 0x65, 0xc3, 0x58, 0x92, 0xed, 0xba, 0x17, 0xb5, 0xd3, 0xb8, 0x5f, 0x6d, 0xb6, 0xc1, - 0x26, 0xe2, 0xff, 0xe4, 0x54, 0xf5, 0x6b, 0x29, 0x6e, 0x20, 0x22, 0x65, 0x3c, 0xa4, 0x86, 0x1b, - 0x21, 0x09, 0x17, 0xf8, 0x82, 0xd2, 0xb4, 0x83, 0xb8, 0x44, 0x1d, 0x0a, 0xb7, 0x01, 0x4a, 0xc4, - 0x64, 0x59, 0xb2, 0x25, 0x7e, 0x4a, 0xbb, 0x58, 0x48, 0x73, 0x03, 0x94, 0x1e, 0xe2, 0x23, 0x4b, - 0x0e, 0x4a, 0x36, 0x5d, 0x36, 0xea, 0x16, 0xe4, 0xf8, 0x7c, 0x06, 0x1d, 0xbf, 0x0d, 0x30, 0x09, - 0xfa, 0x11, 0xbe, 0x09, 0x79, 0x69, 0x16, 0x58, 0x34, 0x2f, 0x9d, 0xaa, 0xe6, 0x4f, 0xbd, 0xec, - 0xc5, 0x0f, 0x96, 0x9e, 0xbd, 0x5c, 0x47, 0xb3, 0xd7, 0xb9, 0xc2, 0x2e, 0xe2, 0x11, 0xaf, 0x22, - 0x57, 0x24, 0x6b, 0xdd, 0x39, 0xb9, 0xfa, 0x76, 0x2d, 0x55, 0x2f, 0xd3, 0x3a, 0xa4, 0xdd, 0x7d, - 0x11, 0x44, 0x3c, 0x1a, 0x35, 0x00, 0xee, 0x4b, 0xb8, 0xd7, 0xfb, 0x06, 0x71, 0x9e, 0xc8, 0xa0, - 0xdc, 0x70, 0xd1, 0x30, 0x14, 0x24, 0x40, 0xb4, 0xa0, 0x69, 0x61, 0x44, 0x9d, 0xca, 0xe4, 0x4f, - 0x10, 0x90, 0x08, 0x8c, 0x11, 0x4b, 0x22, 0xd7, 0x16, 0x89, 0xce, 0xe5, 0x67, 0xbe, 0x79, 0xe2, - 0x5c, 0x93, 0x2c, 0x7b, 0xd1, 0x39, 0xbb, 0xdf, 0x65, 0x47, 0xf7, 0xf4, 0x34, 0xed, 0x34, 0x8f, - 0x95, 0x11, 0x3b, 0x20, 0xfa, 0x55, 0x3c, 0x18, 0x6a, 0xe2, 0x3c, 0x56, 0x86, 0x1d, 0x86, 0xdd, - 0xf8, 0x77, 0x98, 0x04, 0xd0, 0x9d, 0xca, 0xe4, 0xef, 0x7c, 0x3b, 0x4c, 0x4b, 0x3c, 0x73, 0x1d, - 0xe0, 0xd9, 0xb9, 0x2c, 0x9f, 0xc7, 0xc7, 0xbc, 0x6c, 0xf9, 0x57, 0xae, 0xf8, 0xd4, 0xae, 0xc1, - 0x8d, 0x07, 0x3a, 0x2f, 0x3e, 0xb9, 0xef, 0xdc, 0x24, 0xda, 0xbd, 0x80, 0x28, 0x78, 0x38, 0x38, - 0x34, 0x44, 0xed, 0x01, 0x3e, 0xec, 0x5f, 0x6a, 0x21, 0x47, 0xad, 0xac, 0xd8, 0xa5, 0x80, 0x03, - 0x61, 0x07, 0x38, 0x2e, 0x68, 0xda, 0x4b, 0x58, 0x9d, 0xc9, 0x18, 0xee, 0xab, 0xe9, 0x86, 0x59, - 0xa6, 0x26, 0x2d, 0x33, 0x86, 0xbd, 0xa5, 0xe6, 0x07, 0xe1, 0x23, 0x04, 0x34, 0x1b, 0xa3, 0x27, - 0xd2, 0xcc, 0xed, 0x89, 0x66, 0xe7, 0x6a, 0xe2, 0x3e, 0x5c, 0x5d, 0xd7, 0x54, 0xcb, 0x5e, 0xa7, - 0x7a, 0x59, 0xd5, 0x15, 0x7f, 0xdc, 0x52, 0x0e, 0xbe, 0xc3, 0xb8, 0x9b, 0x5d, 0xbb, 0xd9, 0xe8, - 0x47, 0x4a, 0xee, 0x8b, 0xf0, 0x21, 0xc2, 0x63, 0xf1, 0x0e, 0x5f, 0x56, 0x28, 0x04, 0x7c, 0xd8, - 0x36, 0x6c, 0x49, 0x83, 0xc1, 0xa0, 0xee, 0x02, 0xdf, 0x84, 0x35, 0x00, 0x55, 0x92, 0x6c, 0xba, - 0xe6, 0x6a, 0x05, 0xab, 0x7a, 0xb5, 0xe6, 0x5f, 0xdd, 0x5c, 0x2e, 0xc8, 0xc7, 0x85, 0x9c, 0xc0, - 0x3d, 0xdb, 0xaa, 0x5e, 0x36, 0xb6, 0x99, 0xcf, 0x5c, 0x09, 0xde, 0x84, 0xef, 0xe7, 0xf0, 0xc9, - 0x04, 0x77, 0x40, 0xf2, 0x04, 0xee, 0xa9, 0x34, 0xd7, 0xba, 0x5c, 0x09, 0xde, 0xc8, 0x7d, 0x7c, - 0x58, 0x96, 0xed, 0xba, 0x25, 0x6e, 0xa9, 0x96, 0xc5, 0x2a, 0xa8, 0x65, 0xf2, 0xfd, 0xcc, 0xc1, - 0x3d, 0x66, 0x4f, 0xd6, 0xf1, 0x11, 0xd7, 0x5f, 0x15, 0xc8, 0xe7, 0xda, 0x88, 0x26, 0xf3, 0x00, - 0x91, 0x22, 0x67, 0xf0, 0x11, 0x16, 0xb9, 0x86, 0xc7, 0x83, 0xd1, 0x70, 0x92, 0x49, 0x7c, 0xb4, - 0x2a, 0x59, 0xb6, 0xe8, 0x8e, 0xfd, 0x58, 0xd2, 0x6a, 0x74, 0xb4, 0x9b, 0x2d, 0x1e, 0x03, 0xce, - 0x77, 0x27, 0xdf, 0xd6, 0x57, 0x9c, 0xaf, 0xa4, 0x80, 0x8f, 0x81, 0xa3, 0x40, 0xe7, 0x1e, 0x57, - 0xfa, 0xa8, 0x36, 0xeb, 0x03, 0xfa, 0x5f, 0xc7, 0x79, 0xcd, 0xd8, 0xa6, 0x96, 0x2d, 0xfa, 0xcd, - 0x40, 0x46, 0x1a, 0x3d, 0xc4, 0x82, 0x39, 0xe2, 0xf6, 0xf0, 0x15, 0x17, 0x6c, 0x08, 0x8b, 0x78, - 0x2a, 0xae, 0xf4, 0x1e, 0xaa, 0x76, 0x45, 0xd5, 0x1b, 0xb9, 0x4a, 0xcd, 0xb9, 0xf0, 0xeb, 0x2e, - 0x7c, 0x9e, 0xcb, 0x09, 0x64, 0xfa, 0x6d, 0x3c, 0x10, 0x14, 0xf0, 0xda, 0x2a, 0x68, 0xd9, 0x5f, - 0xd0, 0x91, 0x14, 0xc4, 0x54, 0x34, 0x99, 0xc7, 0x23, 0x72, 0xcd, 0x34, 0xa9, 0x6e, 0x8b, 0xdb, - 0xaa, 0x5d, 0x29, 0x9b, 0xd2, 0xb6, 0x08, 0xc5, 0x9a, 0x63, 0x51, 0x3a, 0x0e, 0xcd, 0x0f, 0xa1, - 0xf5, 0x21, 0x6b, 0x24, 0x33, 0xf8, 0x78, 0xc4, 0xce, 0x94, 0x6c, 0xca, 0xf2, 0xdc, 0x57, 0x3a, - 0x16, 0xb2, 0x72, 0x08, 0x3b, 0x49, 0x6c, 0xaa, 0x6c, 0x22, 0xad, 0xcb, 0x94, 0x96, 0x69, 0x99, - 0x65, 0xbc, 0xb7, 0x34, 0x64, 0x7a, 0x31, 0xb9, 0x0d, 0x0d, 0x0d, 0xb1, 0xcc, 0xd9, 0xc8, 0x1e, - 0x51, 0x5b, 0x0a, 0x6c, 0xca, 0xc2, 0x9c, 0xb7, 0xe2, 0x84, 0x5a, 0x9b, 0x53, 0xe7, 0x6e, 0x60, - 0xea, 0x40, 0x72, 0x37, 0x60, 0x0a, 0x2f, 0x19, 0xfa, 0x63, 0x6a, 0x3a, 0xa7, 0xc2, 0x0d, 0xc3, - 0x31, 0x8f, 0xec, 0x48, 0x91, 0x85, 0x2a, 0x8f, 0x7b, 0x15, 0xc9, 0x5a, 0x6b, 0xac, 0x55, 0x7d, - 0xa5, 0xc6, 0xbb, 0xf0, 0x23, 0x04, 0x53, 0x39, 0xea, 0x16, 0xf0, 0x7c, 0x09, 0x0f, 0x79, 0xfa, - 0xc3, 0x8a, 0x64, 0xad, 0xea, 0x4e, 0xa3, 0x27, 0xdd, 0x45, 0x1a, 0x9c, 0xde, 0x4c, 0x30, 0x94, - 0x0d, 0xed, 0x0e, 0xa5, 0xd0, 0xbb, 0x0b, 0xaa, 0x3d, 0xdc, 0x40, 0x26, 0xf1, 0xa0, 0xf3, 0xd7, - 0x7f, 0x66, 0xc8, 0xb1, 0x5c, 0x87, 0x3f, 0x0b, 0x13, 0x20, 0x0e, 0xdc, 0xa3, 0x96, 0x25, 0x29, - 0x74, 0x5d, 0xb2, 0x2c, 0x55, 0x57, 0xd6, 0x9b, 0x1e, 0xbd, 0xe8, 0xde, 0x01, 0x95, 0x26, 0xa5, - 0x23, 0x10, 0x1b, 0xc3, 0x7d, 0xef, 0x34, 0x20, 0xba, 0x84, 0x9a, 0x1f, 0x84, 0xf1, 0xe8, 0x8a, - 0x79, 0x47, 0x93, 0x14, 0xef, 0xf2, 0x2e, 0x7c, 0x80, 0xa2, 0x6b, 0x20, 0x74, 0x00, 0xff, 0x12, - 0x3e, 0x6a, 0x86, 0xda, 0x60, 0xe3, 0x2d, 0x66, 0xcc, 0x8d, 0xb0, 0x4b, 0xb8, 0xa0, 0x44, 0xdc, - 0x09, 0xeb, 0x50, 0x68, 0xc1, 0x5b, 0x3a, 0xc7, 0xde, 0x35, 0x82, 0x0f, 0x39, 0xab, 0x8a, 0x73, - 0xdb, 0x74, 0x93, 0xd3, 0x63, 0xd7, 0xd9, 0x45, 0x73, 0x07, 0x8a, 0x33, 0xec, 0x11, 0x38, 0x7d, - 0x1d, 0x0f, 0x86, 0x14, 0x71, 0xa0, 0xd4, 0x09, 0x1d, 0x61, 0xe6, 0xa7, 0x33, 0xb8, 0x9b, 0x8d, - 0x4e, 0x3e, 0x45, 0x78, 0x30, 0x24, 0x86, 0x91, 0x37, 0x33, 0x86, 0x48, 0x97, 0x8c, 0xf3, 0x6f, - 0xb5, 0x6b, 0xee, 0x52, 0x17, 0x6e, 0x7d, 0xf0, 0xb7, 0xff, 0x7c, 0xd8, 0x75, 0x8d, 0x5c, 0x61, - 0x2a, 0xfc, 0xb4, 0xef, 0xb7, 0x8b, 0xa0, 0x7a, 0x0f, 0x76, 0xc5, 0x1d, 0x38, 0x10, 0xee, 0x16, - 0x77, 0xd8, 0x11, 0x70, 0x97, 0xfc, 0x16, 0x61, 0x12, 0xf2, 0xbe, 0xa0, 0x69, 0x7c, 0xbc, 0x12, - 0x45, 0x63, 0x3e, 0x5e, 0xc9, 0x42, 0xb0, 0x50, 0x60, 0xbc, 0x26, 0xc9, 0x39, 0x3e, 0x5e, 0xe4, - 0x73, 0x84, 0x5f, 0x89, 0xb2, 0x00, 0x8d, 0x8e, 0x2c, 0xb7, 0x87, 0x26, 0x28, 0x37, 0xe6, 0x6f, - 0xef, 0xd1, 0x0b, 0x50, 0x7b, 0x93, 0x51, 0xbb, 0x4c, 0xe6, 0xf8, 0xa8, 0x81, 0x39, 0x64, 0x6e, - 0x97, 0xfc, 0x17, 0xe1, 0xd1, 0x60, 0xdd, 0xfa, 0x88, 0x2e, 0x71, 0x42, 0x4c, 0x93, 0x55, 0xf3, - 0xcb, 0x7b, 0x73, 0x02, 0x34, 0x6f, 0x32, 0x9a, 0x57, 0xc9, 0xe5, 0x04, 0x9a, 0xaa, 0x9e, 0xcc, - 0x52, 0x54, 0xcb, 0xbb, 0xe4, 0x37, 0x08, 0x0f, 0x45, 0x88, 0x72, 0xd7, 0x65, 0xbc, 0xba, 0xc9, - 0x5d, 0x97, 0x09, 0x8a, 0x65, 0x66, 0x5d, 0x06, 0x59, 0x59, 0xe4, 0x13, 0x84, 0x07, 0x82, 0xbe, - 0xc8, 0x55, 0x1e, 0x08, 0xb1, 0x6b, 0x67, 0xfe, 0x5a, 0x3b, 0xa6, 0x80, 0x7c, 0x91, 0x21, 0xbf, - 0x41, 0xae, 0x71, 0x21, 0xf7, 0x25, 0xa2, 0xb8, 0x03, 0x8b, 0xf2, 0x2e, 0xf9, 0x7b, 0x33, 0x25, - 0x3e, 0x3d, 0xea, 0x26, 0xe7, 0x1a, 0x96, 0x24, 0xd2, 0xe5, 0x6f, 0xb5, 0xef, 0x00, 0xc8, 0xbd, - 0xc5, 0xc8, 0x5d, 0x21, 0xf3, 0xe9, 0xe4, 0x9a, 0x96, 0xc5, 0x1d, 0xdf, 0xa7, 0x5d, 0xf2, 0x19, - 0xc2, 0xc7, 0x63, 0x55, 0x4c, 0x72, 0xab, 0x85, 0x90, 0xc7, 0xea, 0xa8, 0xf9, 0x85, 0x3d, 0x78, - 0x68, 0x2d, 0x77, 0x41, 0xeb, 0x10, 0xc5, 0x4f, 0x10, 0x1e, 0x8e, 0x8c, 0xe2, 0xcc, 0xa8, 0x9b, - 0xad, 0x4d, 0x89, 0x36, 0xd3, 0x97, 0xa6, 0x9b, 0x0a, 0x17, 0x18, 0xbf, 0x29, 0x32, 0xc9, 0xcb, - 0x8f, 0xfc, 0x0c, 0x35, 0x95, 0x3a, 0x32, 0xcf, 0x59, 0x3f, 0x21, 0x49, 0x31, 0x7f, 0xb9, 0x65, - 0x3b, 0xc0, 0x5b, 0x64, 0x78, 0xdf, 0x20, 0x13, 0x09, 0x78, 0x15, 0x30, 0x70, 0x52, 0x50, 0xa6, - 0xf5, 0x5d, 0xf2, 0x63, 0x84, 0xfb, 0x3d, 0x2f, 0x4e, 0xcc, 0xe7, 0x39, 0x43, 0xd6, 0x16, 0xe2, - 0x18, 0x61, 0x53, 0x98, 0x60, 0x88, 0x5f, 0x23, 0xa7, 0x32, 0x10, 0x93, 0x5f, 0x21, 0x7c, 0x34, - 0x7c, 0xea, 0x26, 0xd7, 0x79, 0x86, 0x4d, 0xb8, 0x02, 0xe4, 0x6f, 0xb4, 0x67, 0xcc, 0x19, 0x6a, - 0x39, 0x8c, 0xf5, 0x0f, 0x08, 0xf7, 0xfb, 0x0e, 0xd6, 0x7c, 0x7b, 0x7f, 0xd6, 0x01, 0x9e, 0x6f, - 0xef, 0xcf, 0x3c, 0xdd, 0x0b, 0x53, 0x8c, 0xcd, 0xeb, 0x44, 0x48, 0x60, 0xe3, 0xbb, 0x8c, 0x90, - 0xa7, 0x28, 0xa2, 0x5d, 0x72, 0x9f, 0x36, 0xe3, 0x95, 0x57, 0xee, 0xd3, 0x66, 0x82, 0x9a, 0x2a, - 0xcc, 0x33, 0xf8, 0x17, 0x48, 0x21, 0x01, 0xbe, 0x16, 0xb4, 0x6b, 0x94, 0xbf, 0x73, 0xc6, 0x0c, - 0xf9, 0x6c, 0x65, 0x2f, 0xdf, 0x0b, 0x9b, 0x64, 0x6d, 0x38, 0x73, 0x2f, 0x0f, 0xb1, 0x21, 0x3f, - 0x44, 0xf8, 0x20, 0x5b, 0x7c, 0x66, 0x38, 0xc3, 0xe8, 0x5f, 0x24, 0x67, 0x5b, 0xb2, 0x01, 0x84, - 0xe7, 0x19, 0xc2, 0xb3, 0xe4, 0x4c, 0x52, 0xf1, 0xc3, 0x4e, 0xc6, 0x82, 0xfc, 0x73, 0x84, 0xfb, - 0x7d, 0x9a, 0x30, 0xdf, 0x39, 0x23, 0x56, 0x47, 0x6e, 0x0f, 0xec, 0x1c, 0x03, 0x5b, 0x24, 0xd3, - 0xa9, 0x60, 0x23, 0xf7, 0x8f, 0x1f, 0x20, 0x7c, 0xc8, 0xdb, 0x8a, 0x66, 0x38, 0x33, 0xda, 0x72, - 0x60, 0x43, 0xca, 0xaf, 0x70, 0x86, 0x61, 0x3d, 0x49, 0x5e, 0x4d, 0xc1, 0x4a, 0x3e, 0x76, 0x26, - 0x60, 0x50, 0x6f, 0x22, 0x5c, 0x27, 0xb0, 0x78, 0xd5, 0x36, 0x7f, 0xbd, 0x2d, 0x5b, 0xde, 0x95, - 0xc3, 0x07, 0xf2, 0x7f, 0x08, 0x8f, 0xa7, 0x0b, 0x65, 0x64, 0xb5, 0x0d, 0x2c, 0xf1, 0x8a, 0x5d, - 0xfe, 0xcb, 0x9d, 0x70, 0x05, 0x2c, 0xaf, 0x32, 0x96, 0xb3, 0xe4, 0x62, 0x36, 0xcb, 0x30, 0xa3, - 0x8f, 0x11, 0x1e, 0x08, 0xfe, 0xa7, 0x17, 0xdf, 0x0c, 0x88, 0xfd, 0xdf, 0x31, 0xbe, 0x93, 0x76, - 0xfc, 0x3f, 0x96, 0x09, 0xd3, 0x8c, 0xc4, 0x04, 0x39, 0x9b, 0x40, 0xe2, 0xfd, 0x20, 0x4a, 0x07, - 0x78, 0x50, 0x75, 0xe3, 0x03, 0x1e, 0xab, 0xe3, 0xf1, 0x01, 0x8f, 0x17, 0xf9, 0x32, 0x81, 0x6b, - 0x41, 0x94, 0xce, 0x51, 0x21, 0x2c, 0x0a, 0xf1, 0x1d, 0x15, 0x12, 0xe4, 0x2b, 0xbe, 0xa3, 0x42, - 0x92, 0xb4, 0x95, 0x79, 0x54, 0x08, 0x0b, 0x55, 0x61, 0x02, 0xec, 0xc7, 0x82, 0x96, 0x09, 0xf8, - 0x7f, 0xb1, 0x68, 0x99, 0x40, 0xe0, 0xf7, 0x89, 0x56, 0x08, 0xb8, 0x58, 0xff, 0x8c, 0xf0, 0xe1, - 0x07, 0x35, 0x7b, 0xa3, 0xbe, 0x4f, 0xd4, 0x28, 0x0e, 0x69, 0xa3, 0x81, 0x35, 0x66, 0x2b, 0xf8, - 0xa5, 0xab, 0xaf, 0x35, 0xba, 0xec, 0x03, 0x1d, 0x2a, 0x6b, 0x07, 0xf6, 0x33, 0x22, 0xff, 0x46, - 0xf8, 0x44, 0x08, 0xff, 0xbe, 0x54, 0xa0, 0xae, 0x31, 0x52, 0x97, 0xc8, 0x0c, 0x07, 0xa9, 0xb0, - 0xfc, 0xe4, 0xde, 0x94, 0xe3, 0x28, 0xee, 0x23, 0xed, 0xe9, 0x06, 0x23, 0x38, 0x4f, 0x2e, 0x25, - 0xde, 0x27, 0x13, 0xf8, 0x31, 0xe1, 0xe9, 0x17, 0x4c, 0xb3, 0x69, 0xab, 0x0a, 0x5f, 0x92, 0xea, - 0x94, 0xb5, 0xf9, 0xfb, 0xf8, 0x90, 0x67, 0x80, 0x7e, 0x7f, 0x09, 0x34, 0xd7, 0x19, 0x83, 0x39, - 0x32, 0x9b, 0xc2, 0x20, 0x51, 0x9d, 0xf9, 0x27, 0xc2, 0x24, 0x48, 0x69, 0xff, 0x48, 0x33, 0xd9, - 0x32, 0x67, 0x18, 0x77, 0x88, 0xdc, 0xef, 0x99, 0xa6, 0xe6, 0xef, 0xb4, 0x4f, 0x44, 0x99, 0xac, - 0xd3, 0x40, 0x90, 0x59, 0xbe, 0xfb, 0x5b, 0x9f, 0x7f, 0x34, 0x85, 0x16, 0x57, 0x9e, 0x3e, 0x1f, - 0x47, 0xcf, 0x9e, 0x8f, 0xa3, 0xcf, 0x9e, 0x8f, 0xa3, 0xef, 0xbd, 0x18, 0x3f, 0xf0, 0xec, 0xc5, - 0xf8, 0x81, 0x7f, 0xbc, 0x18, 0x3f, 0xf0, 0x68, 0x5a, 0x51, 0xed, 0x4a, 0x6d, 0xb3, 0x20, 0x1b, - 0x5b, 0x7e, 0x8f, 0xba, 0x51, 0xa6, 0xc5, 0xba, 0xdf, 0xb1, 0xfd, 0xa4, 0x4a, 0xad, 0xcd, 0x1e, - 0x76, 0x17, 0x9e, 0xfd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xa3, 0x7e, 0xf5, 0x1e, 0x32, - 0x00, 0x00, + // 2394 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5a, 0xcd, 0x6f, 0xdc, 0xc6, + 0x15, 0xf7, 0x68, 0x2d, 0x59, 0x7a, 0xb2, 0x25, 0x6b, 0x2c, 0x5b, 0xca, 0x46, 0x96, 0x1d, 0x3a, + 0xb6, 0x14, 0xb9, 0xda, 0xb5, 0x25, 0x4b, 0xfe, 0x4a, 0x62, 0xeb, 0xc3, 0x96, 0x55, 0xc8, 0xb6, + 0xb2, 0x10, 0xea, 0xc2, 0x2d, 0x4a, 0x50, 0xdc, 0xc9, 0x2e, 0x1b, 0x8a, 0xdc, 0x90, 0x5c, 0x6b, + 0x1d, 0x41, 0x40, 0x1b, 0xa0, 0x40, 0x7a, 0x2b, 0x10, 0x14, 0xbd, 0xf4, 0x5a, 0xb4, 0x87, 0x1e, + 0x72, 0x28, 0x72, 0x0c, 0xda, 0xa6, 0x68, 0x8d, 0x06, 0x45, 0xd3, 0x04, 0x28, 0x7a, 0x69, 0x11, + 0xd8, 0x41, 0x73, 0xef, 0x5f, 0x50, 0x70, 0xf8, 0xb8, 0xcb, 0xef, 0xe5, 0xae, 0xd6, 0x80, 0x7c, + 0x12, 0xc9, 0x99, 0xf7, 0xe6, 0xfd, 0xde, 0x7b, 0xf3, 0xe6, 0xcd, 0x6f, 0x05, 0xaf, 0xbd, 0xc7, + 0x2c, 0x49, 0x2e, 0x4b, 0x8a, 0x96, 0xe7, 0x4f, 0xba, 0xc1, 0xf2, 0xb2, 0xa1, 0x9b, 0xa6, 0xf3, + 0xed, 0xdd, 0x2a, 0x33, 0x1e, 0xe7, 0x2a, 0x86, 0x6e, 0xe9, 0xf4, 0x64, 0x7d, 0x6a, 0xce, 0x9d, + 0x9a, 0x6b, 0x4c, 0xcd, 0x4e, 0xc9, 0xba, 0xb9, 0xa5, 0x9b, 0xf9, 0x4d, 0xc9, 0x64, 0x8e, 0x5c, + 0xfe, 0xd1, 0xc5, 0x4d, 0x66, 0x49, 0x17, 0xf3, 0x15, 0xa9, 0xa4, 0x68, 0x92, 0xa5, 0xe8, 0x9a, + 0xa3, 0x2a, 0x3b, 0x93, 0xbc, 0x2a, 0x7f, 0x14, 0xf9, 0xb3, 0x68, 0xd5, 0x50, 0x66, 0x3a, 0x59, + 0xa6, 0x24, 0x99, 0x62, 0xc5, 0x50, 0x64, 0x86, 0xd3, 0xaf, 0x24, 0x4f, 0x57, 0xb4, 0x4d, 0xbd, + 0xaa, 0x15, 0xc5, 0xb2, 0x64, 0x96, 0x45, 0x4b, 0x17, 0x65, 0xb9, 0xbe, 0xd0, 0x6c, 0x3a, 0x49, + 0xcb, 0x90, 0xe4, 0x77, 0x98, 0x81, 0x42, 0x73, 0xc9, 0x42, 0xaa, 0x64, 0x5a, 0xe2, 0xa6, 0xaa, + 0xcb, 0xef, 0x88, 0x65, 0xa6, 0x94, 0xca, 0x16, 0x8a, 0x5d, 0x4a, 0x16, 0xd3, 0xab, 0x56, 0xd4, + 0x62, 0xf3, 0xc9, 0x52, 0x86, 0x64, 0x31, 0x51, 0x55, 0xb6, 0x14, 0x8b, 0x19, 0xe2, 0xdb, 0xaa, + 0x54, 0x32, 0x51, 0x6e, 0xb8, 0xa4, 0x97, 0x74, 0xfe, 0x98, 0xb7, 0x9f, 0xf0, 0xeb, 0x58, 0x49, + 0xd7, 0x4b, 0x2a, 0xcb, 0x4b, 0x15, 0x25, 0x2f, 0x69, 0x9a, 0x6e, 0xf1, 0x48, 0xb9, 0x32, 0x23, + 0x18, 0xd6, 0x2d, 0xb3, 0x94, 0x7f, 0x74, 0xd1, 0xfe, 0xe3, 0x0c, 0x08, 0x63, 0x90, 0x7d, 0xcb, + 0x8e, 0xf2, 0x43, 0x66, 0x49, 0x0b, 0xb2, 0xac, 0x57, 0x35, 0x4b, 0xd1, 0x4a, 0x05, 0xf6, 0x6e, + 0x95, 0x99, 0x96, 0x70, 0x17, 0x5e, 0x8e, 0x1c, 0x35, 0x2b, 0xba, 0x66, 0x32, 0x9a, 0x83, 0x63, + 0xd2, 0xa6, 0x6e, 0x58, 0xac, 0x28, 0xda, 0x08, 0x44, 0x69, 0xcb, 0x9e, 0x31, 0x4a, 0x4e, 0x93, + 0xc9, 0xbe, 0xc2, 0x10, 0x0e, 0x71, 0x59, 0x3e, 0x20, 0xac, 0xc3, 0x38, 0x57, 0xb7, 0xc2, 0xac, + 0xfb, 0xe8, 0x93, 0x0d, 0xc7, 0x25, 0xb8, 0x20, 0x1d, 0x85, 0x43, 0x1c, 0xfd, 0xea, 0x32, 0xd7, + 0x92, 0x29, 0xb8, 0xaf, 0x74, 0x18, 0xba, 0x35, 0x5d, 0x93, 0xd9, 0x68, 0xd7, 0x69, 0x32, 0x79, + 0xb0, 0xe0, 0xbc, 0x08, 0x3f, 0x26, 0x70, 0x2a, 0x56, 0x25, 0x5a, 0xf9, 0x03, 0x18, 0xd4, 0xfd, + 0x43, 0x5c, 0x77, 0xff, 0x4c, 0x2e, 0x97, 0xb8, 0x17, 0x72, 0x01, 0x85, 0x8b, 0x07, 0x9f, 0xfc, + 0xe7, 0xd4, 0x81, 0x42, 0x50, 0x99, 0x50, 0x46, 0x54, 0x0b, 0xaa, 0x1a, 0x83, 0xea, 0x36, 0x40, + 0x63, 0xf3, 0xe0, 0xe2, 0xe7, 0x72, 0x4e, 0x48, 0x72, 0xf6, 0x4e, 0xcb, 0x39, 0x3b, 0x14, 0x77, + 0x5a, 0x6e, 0x5d, 0x2a, 0x31, 0x94, 0x2d, 0x78, 0x24, 0x85, 0xbf, 0xba, 0x68, 0xa3, 0x96, 0x4a, + 0x42, 0x9b, 0xe9, 0x18, 0x5a, 0xba, 0xe2, 0xc3, 0xd2, 0xc5, 0xb1, 0x4c, 0x34, 0xc5, 0xe2, 0x18, + 0xe7, 0x03, 0xf3, 0x13, 0x02, 0x67, 0x63, 0xc0, 0x2c, 0x3e, 0x5e, 0xb2, 0x4d, 0x72, 0xdd, 0x37, + 0x0c, 0xdd, 0xdc, 0x44, 0x4c, 0x09, 0xe7, 0x25, 0xe0, 0xd4, 0xae, 0xb6, 0x9d, 0xfa, 0x0f, 0x02, + 0xe7, 0x9a, 0xd9, 0xf1, 0xa2, 0xf9, 0xf6, 0xa7, 0x04, 0x5e, 0x75, 0x31, 0xad, 0x6a, 0x09, 0xae, + 0x7d, 0x09, 0x7a, 0x9d, 0x02, 0xad, 0x14, 0xfd, 0x1b, 0xae, 0xd8, 0x31, 0xff, 0xfe, 0xcd, 0x13, + 0xe7, 0x18, 0x5b, 0xd0, 0xbd, 0xdf, 0x83, 0x01, 0x45, 0x8b, 0xf0, 0xee, 0x74, 0x13, 0xef, 0x06, + 0xb4, 0x3a, 0xce, 0x0d, 0xa8, 0xea, 0x9c, 0x6f, 0x3d, 0xdb, 0xdd, 0xbf, 0xb0, 0xd9, 0xe9, 0xed, + 0xfe, 0x17, 0xcf, 0x76, 0x0f, 0x2d, 0xf5, 0x42, 0xf9, 0x6c, 0x19, 0x4e, 0xbb, 0x55, 0x1a, 0x17, + 0xbe, 0x23, 0x99, 0xe5, 0x0d, 0x7d, 0x49, 0xb6, 0x6a, 0xae, 0xd7, 0x4e, 0x43, 0xbf, 0xd2, 0x18, + 0xc3, 0x43, 0xc4, 0xfb, 0xc9, 0xce, 0xea, 0x57, 0x12, 0xd4, 0xa0, 0x47, 0x8a, 0x30, 0xa4, 0x04, + 0x07, 0x31, 0x08, 0x17, 0xd2, 0x39, 0xa5, 0x21, 0x87, 0x7e, 0x09, 0x2b, 0x14, 0x6e, 0xa1, 0x29, + 0x21, 0x91, 0x65, 0xc9, 0x92, 0xd2, 0x43, 0xda, 0x05, 0x21, 0x49, 0x0d, 0x42, 0x7a, 0x00, 0x47, + 0x96, 0x6c, 0x2b, 0xf9, 0x76, 0xd9, 0xa8, 0x99, 0x18, 0xe3, 0xf3, 0x4d, 0xe0, 0x78, 0x65, 0x10, + 0x89, 0x5f, 0x8f, 0xf0, 0x43, 0x8c, 0x4b, 0x23, 0xc1, 0xc2, 0x71, 0xe9, 0x54, 0x36, 0x7f, 0xe1, + 0x46, 0x2f, 0x7a, 0xb1, 0xe4, 0xe8, 0x65, 0x3a, 0x1a, 0xbd, 0xce, 0x25, 0x76, 0x1e, 0x46, 0xdc, + 0x8c, 0x5c, 0x91, 0xcc, 0x75, 0xbb, 0x73, 0xf5, 0x9c, 0x5a, 0x8a, 0x56, 0x64, 0x35, 0x0c, 0xbb, + 0xf3, 0x22, 0x88, 0x30, 0x1a, 0x16, 0x40, 0xec, 0x4b, 0xd0, 0xeb, 0x7e, 0x43, 0x3f, 0x4f, 0x34, + 0x81, 0x5c, 0x57, 0x51, 0x17, 0x14, 0x24, 0xb4, 0x68, 0x41, 0x55, 0x83, 0x16, 0x75, 0x2a, 0x92, + 0xbf, 0x21, 0x08, 0xc2, 0xb7, 0x46, 0x24, 0x88, 0x4c, 0x5b, 0x20, 0x3a, 0x17, 0x9f, 0xf9, 0x46, + 0xc7, 0xb9, 0x26, 0x99, 0xd6, 0xa2, 0xdd, 0xbb, 0xdf, 0xe1, 0xad, 0x7b, 0x72, 0x98, 0x76, 0x1a, + 0x6d, 0x65, 0x48, 0x0e, 0x81, 0x7e, 0x17, 0x06, 0x03, 0x43, 0x29, 0xdb, 0xca, 0xa0, 0xc2, 0xa0, + 0x1a, 0xef, 0x09, 0x13, 0x63, 0x74, 0xa7, 0x22, 0xf9, 0x27, 0xcf, 0x09, 0xd3, 0x12, 0xce, 0x4c, + 0x07, 0x70, 0x76, 0x2e, 0xca, 0xe7, 0xe1, 0x98, 0x1b, 0x2d, 0x6f, 0xe5, 0x8a, 0x0e, 0xed, 0x1a, + 0xde, 0x78, 0x70, 0xf2, 0xe2, 0xe3, 0x7b, 0xf6, 0x4d, 0xa2, 0xdd, 0x0b, 0x48, 0x09, 0x86, 0xfd, + 0x4b, 0xa3, 0xd7, 0xee, 0xc3, 0x61, 0x6f, 0xa9, 0xc5, 0x18, 0xb5, 0x52, 0xb1, 0x0b, 0x3e, 0x05, + 0xc2, 0x0e, 0x62, 0x5c, 0x50, 0xd5, 0xe7, 0x50, 0x9d, 0xe9, 0x18, 0xf4, 0x55, 0x35, 0xdd, 0x28, + 0x32, 0x83, 0x15, 0x39, 0xc2, 0xde, 0x42, 0xe3, 0x83, 0xf0, 0x11, 0x41, 0x98, 0xf5, 0xd5, 0x63, + 0x61, 0x66, 0xf6, 0x04, 0xb3, 0x73, 0x39, 0x71, 0x0f, 0xaf, 0xae, 0x6b, 0x8a, 0x69, 0xad, 0x33, + 0xad, 0xa8, 0x68, 0x25, 0xaf, 0xdf, 0x12, 0x1a, 0xdf, 0x61, 0xe8, 0xe6, 0xd7, 0x6e, 0xbe, 0xfa, + 0x91, 0x82, 0xf3, 0x22, 0x7c, 0x48, 0x60, 0x2c, 0x5a, 0xe1, 0xf3, 0x72, 0x85, 0x00, 0x87, 0x2d, + 0xdd, 0x92, 0x54, 0x5c, 0x0c, 0xf3, 0xce, 0xf7, 0x4d, 0x58, 0x43, 0xa3, 0x0a, 0x92, 0xc5, 0xd6, + 0x1c, 0xae, 0x60, 0x55, 0xab, 0x54, 0xbd, 0xd5, 0xcd, 0xc1, 0x42, 0x3c, 0x58, 0xe8, 0x09, 0xe8, + 0xd9, 0x56, 0xb4, 0xa2, 0xbe, 0xcd, 0x75, 0x66, 0x0a, 0xf8, 0x26, 0xfc, 0x3c, 0x03, 0x27, 0x63, + 0xd4, 0x21, 0xc8, 0x13, 0xd0, 0x53, 0x6e, 0xd4, 0xba, 0x4c, 0x01, 0xdf, 0xe8, 0x3d, 0x38, 0x2c, + 0xcb, 0x56, 0xcd, 0x14, 0xb7, 0x14, 0xd3, 0xe4, 0x19, 0xd4, 0x32, 0xf8, 0x7e, 0xae, 0xe0, 0x2e, + 0x97, 0xa7, 0xeb, 0x70, 0xc4, 0xd1, 0x57, 0x41, 0xf0, 0x99, 0x36, 0xbc, 0xc9, 0x35, 0xa0, 0xa7, + 0xe8, 0x19, 0x38, 0xc2, 0x3d, 0x57, 0xd7, 0x78, 0x30, 0xec, 0x4e, 0x3a, 0x09, 0x47, 0x2b, 0x92, + 0x69, 0x89, 0xce, 0xda, 0x8f, 0x24, 0xb5, 0xca, 0x46, 0xbb, 0x79, 0xf1, 0x18, 0xb0, 0xbf, 0xdb, + 0xf1, 0x36, 0xbf, 0x63, 0x7f, 0xa5, 0x39, 0x38, 0x86, 0x8a, 0x7c, 0x93, 0x7b, 0x1c, 0xea, 0xa3, + 0xd2, 0xc8, 0x0f, 0x9c, 0x7f, 0x1d, 0xb2, 0xaa, 0xbe, 0xcd, 0x4c, 0x4b, 0xf4, 0x8a, 0x21, 0x8d, + 0x34, 0x7a, 0x88, 0x3b, 0x73, 0xc4, 0x99, 0xe1, 0x49, 0x2e, 0x3c, 0x10, 0x16, 0x61, 0x2a, 0x2a, + 0xf5, 0x1e, 0x28, 0x56, 0x59, 0xd1, 0xea, 0xb1, 0x4a, 0x8c, 0xb9, 0xf0, 0x87, 0x2e, 0x38, 0x9f, + 0x4a, 0x09, 0x46, 0xfa, 0x2d, 0x18, 0xf0, 0x13, 0x78, 0x6d, 0x25, 0xb4, 0xec, 0x4d, 0xe8, 0x50, + 0x08, 0x22, 0x32, 0x9a, 0xce, 0xc3, 0x88, 0x5c, 0x35, 0x0c, 0xa6, 0x59, 0xe2, 0xb6, 0x62, 0x95, + 0x8b, 0x86, 0xb4, 0x2d, 0x62, 0xb2, 0x66, 0xb8, 0x97, 0x8e, 0xe3, 0xf0, 0x03, 0x1c, 0x7d, 0xc0, + 0x07, 0xe9, 0x0c, 0x1c, 0x0f, 0xc9, 0x19, 0x92, 0xc5, 0x78, 0x9c, 0xfb, 0x0a, 0xc7, 0x02, 0x52, + 0x36, 0x60, 0x3b, 0x88, 0x0d, 0x96, 0x4d, 0x64, 0x35, 0x99, 0xb1, 0x22, 0x2b, 0xf2, 0x88, 0xf7, + 0x16, 0x86, 0x0c, 0xd7, 0x27, 0xb7, 0x70, 0xa0, 0x4e, 0x96, 0xd9, 0x07, 0xd9, 0x43, 0x66, 0x49, + 0xbe, 0x43, 0x59, 0x98, 0x73, 0x2b, 0x4e, 0x60, 0xb4, 0xb1, 0x75, 0xee, 0xf8, 0xb6, 0x0e, 0x06, + 0x77, 0x03, 0xb7, 0xf0, 0x92, 0xae, 0x3d, 0x62, 0x86, 0xdd, 0x15, 0x6e, 0xe8, 0xb6, 0x78, 0xe8, + 0x44, 0x0a, 0x15, 0xaa, 0x2c, 0xf4, 0x96, 0x24, 0x73, 0xad, 0x5e, 0xab, 0xfa, 0x0a, 0xf5, 0x77, + 0xe1, 0x57, 0x04, 0xb7, 0x72, 0x58, 0x2d, 0xda, 0xf3, 0x2d, 0x18, 0x72, 0xf9, 0x87, 0x15, 0xc9, + 0x5c, 0xd5, 0xec, 0x41, 0x97, 0xba, 0x0b, 0x0d, 0xd8, 0xb3, 0x39, 0x61, 0x28, 0xeb, 0xea, 0x6d, + 0xc6, 0x70, 0x76, 0x17, 0x66, 0x7b, 0x70, 0x80, 0x4e, 0xc2, 0xa0, 0xfd, 0xd7, 0xdb, 0x33, 0x64, + 0x78, 0xac, 0x83, 0x9f, 0x85, 0x09, 0x24, 0x07, 0xee, 0x32, 0xd3, 0x94, 0x4a, 0x6c, 0x5d, 0x32, + 0x4d, 0x45, 0x2b, 0xad, 0x37, 0x34, 0xba, 0xde, 0xbd, 0x8d, 0x2c, 0x4d, 0xc2, 0x44, 0x04, 0x36, + 0x06, 0x7d, 0x6f, 0xd7, 0x4d, 0x74, 0x00, 0x35, 0x3e, 0x08, 0xe3, 0xe1, 0x8a, 0x79, 0x5b, 0x95, + 0x4a, 0xee, 0xe5, 0x5d, 0x78, 0x9f, 0x84, 0x6b, 0x20, 0x4e, 0x40, 0xfd, 0x12, 0x1c, 0x35, 0x02, + 0x63, 0x78, 0xf0, 0xe6, 0x9b, 0xec, 0x8d, 0xa0, 0x4a, 0xbc, 0xa0, 0x84, 0xd4, 0x09, 0xeb, 0x98, + 0x68, 0xfe, 0x5b, 0x7a, 0x8a, 0xb3, 0x6b, 0x04, 0x0e, 0xd9, 0x55, 0xc5, 0xbe, 0x6d, 0x3a, 0xc1, + 0xe9, 0xb1, 0x6a, 0xfc, 0xa2, 0xb9, 0x83, 0xc9, 0x19, 0xd4, 0x88, 0x98, 0xbe, 0x0f, 0x83, 0x01, + 0x46, 0x1c, 0x21, 0x75, 0x82, 0x47, 0x98, 0xf9, 0x74, 0x06, 0xba, 0xf9, 0xea, 0xf4, 0x0b, 0x02, + 0x83, 0x01, 0x32, 0x8c, 0xbe, 0xd1, 0x64, 0x89, 0x64, 0xca, 0x38, 0xfb, 0x66, 0xbb, 0xe2, 0x0e, + 0x74, 0xe1, 0xe6, 0xfb, 0x5f, 0x7e, 0xfd, 0x61, 0xd7, 0x35, 0x7a, 0x85, 0xb3, 0xf0, 0xd3, 0x9e, + 0xdf, 0x2e, 0xfc, 0xec, 0x3d, 0xca, 0xe5, 0x77, 0xb0, 0x21, 0xdc, 0xcd, 0xef, 0xf0, 0x16, 0x70, + 0x97, 0x7e, 0x4a, 0x80, 0x06, 0xb4, 0x2f, 0xa8, 0x6a, 0x3a, 0x5c, 0xb1, 0xa4, 0x71, 0x3a, 0x5c, + 0xf1, 0x44, 0xb0, 0x90, 0xe3, 0xb8, 0x26, 0xe9, 0xb9, 0x74, 0xb8, 0xe8, 0x37, 0x04, 0x5e, 0x0a, + 0xa3, 0x40, 0x8e, 0x8e, 0x2e, 0xb7, 0x67, 0x8d, 0x9f, 0x6e, 0xcc, 0xde, 0xda, 0xa3, 0x16, 0x84, + 0xf6, 0x06, 0x87, 0x76, 0x99, 0xce, 0xa5, 0x83, 0x86, 0xe2, 0x18, 0xb9, 0x5d, 0xfa, 0x5f, 0x02, + 0xa3, 0xfe, 0xbc, 0xf5, 0x00, 0x5d, 0x4a, 0x69, 0x62, 0x12, 0xad, 0x9a, 0x5d, 0xde, 0x9b, 0x12, + 0x84, 0x79, 0x83, 0xc3, 0xbc, 0x4a, 0x2f, 0xc7, 0xc0, 0x54, 0xb4, 0x78, 0x94, 0xa2, 0x52, 0xdc, + 0xa5, 0x7f, 0x24, 0x30, 0x14, 0x02, 0x9a, 0x3a, 0x2f, 0xa3, 0xd9, 0xcd, 0xd4, 0x79, 0x19, 0xc3, + 0x58, 0x36, 0xcd, 0x4b, 0x3f, 0x2a, 0x93, 0x7e, 0x46, 0x60, 0xc0, 0xaf, 0x8b, 0x5e, 0x4d, 0x63, + 0x42, 0x64, 0xed, 0xcc, 0x5e, 0x6b, 0x47, 0x14, 0x2d, 0x5f, 0xe4, 0x96, 0xbf, 0x4e, 0xaf, 0xa5, + 0xb2, 0xdc, 0x13, 0x88, 0xfc, 0x0e, 0x16, 0xe5, 0x5d, 0xfa, 0xcf, 0x46, 0x48, 0x3c, 0x7c, 0xd4, + 0x8d, 0x94, 0x35, 0x2c, 0x8e, 0xa4, 0xcb, 0xde, 0x6c, 0x5f, 0x01, 0x82, 0x7b, 0x93, 0x83, 0xbb, + 0x42, 0xe7, 0x93, 0xc1, 0x35, 0x24, 0xf3, 0x3b, 0x9e, 0x4f, 0xbb, 0xf4, 0x2b, 0x02, 0xc7, 0x23, + 0x59, 0x4c, 0x7a, 0xb3, 0x05, 0x97, 0x47, 0xf2, 0xa8, 0xd9, 0x85, 0x3d, 0x68, 0x68, 0x2d, 0x76, + 0x7e, 0xe9, 0x00, 0xc4, 0xcf, 0x08, 0x0c, 0x87, 0x56, 0xb1, 0x77, 0xd4, 0x8d, 0xd6, 0xb6, 0x44, + 0x9b, 0xe1, 0x4b, 0xe2, 0x4d, 0x85, 0x0b, 0x1c, 0xdf, 0x14, 0x9d, 0x4c, 0x8b, 0x8f, 0xfe, 0x96, + 0x34, 0x98, 0x3a, 0x3a, 0x9f, 0x32, 0x7f, 0x02, 0x94, 0x62, 0xf6, 0x72, 0xcb, 0x72, 0x68, 0x6f, + 0x9e, 0xdb, 0xfb, 0x1a, 0x9d, 0x88, 0xb1, 0xb7, 0x84, 0x02, 0x76, 0x08, 0x8a, 0xac, 0xb6, 0x4b, + 0x7f, 0x4d, 0xa0, 0xdf, 0xd5, 0x62, 0xfb, 0x7c, 0x3e, 0xa5, 0xcb, 0xda, 0xb2, 0x38, 0x82, 0xd8, + 0x14, 0x26, 0xb8, 0xc5, 0xaf, 0xd0, 0x53, 0x4d, 0x2c, 0xa6, 0x9f, 0x10, 0x38, 0x1a, 0xec, 0xba, + 0xe9, 0xf5, 0x34, 0xcb, 0xc6, 0x5c, 0x01, 0xb2, 0xaf, 0xb7, 0x27, 0x9c, 0xd2, 0xd5, 0x72, 0xd0, + 0xd6, 0x3f, 0x13, 0xe8, 0xf7, 0x34, 0xd6, 0xe9, 0xce, 0xfe, 0x66, 0x0d, 0x7c, 0xba, 0xb3, 0xbf, + 0x69, 0x77, 0x2f, 0x4c, 0x71, 0x34, 0xaf, 0x52, 0x21, 0x06, 0x8d, 0xe7, 0x32, 0x42, 0x9f, 0x90, + 0x10, 0x77, 0x99, 0xba, 0xdb, 0x8c, 0x66, 0x5e, 0x53, 0x77, 0x9b, 0x31, 0x6c, 0xaa, 0x30, 0xcf, + 0xcd, 0xbf, 0x40, 0x73, 0x31, 0xe6, 0xab, 0x7e, 0xb9, 0x7a, 0xfa, 0xdb, 0x3d, 0x66, 0x40, 0x67, + 0x2b, 0x67, 0xf9, 0x5e, 0xd0, 0xc4, 0x73, 0xc3, 0x4d, 0xcf, 0xf2, 0x00, 0x1a, 0xfa, 0x4b, 0x02, + 0x07, 0x79, 0xf1, 0x99, 0x49, 0xe9, 0x46, 0x6f, 0x91, 0x9c, 0x6d, 0x49, 0x06, 0x2d, 0x3c, 0xcf, + 0x2d, 0x3c, 0x4b, 0xcf, 0xc4, 0x25, 0x3f, 0x9e, 0x64, 0xdc, 0xc9, 0xbf, 0x23, 0xd0, 0xef, 0xe1, + 0x84, 0xd3, 0xf5, 0x19, 0x91, 0x3c, 0x72, 0x7b, 0xc6, 0xce, 0x71, 0x63, 0xf3, 0x74, 0x3a, 0xd1, + 0xd8, 0xd0, 0xfd, 0xe3, 0x17, 0x04, 0x0e, 0xb9, 0x47, 0xd1, 0x4c, 0xca, 0x88, 0xb6, 0xec, 0xd8, + 0x00, 0xf3, 0x2b, 0x9c, 0xe1, 0xb6, 0x9e, 0xa4, 0x2f, 0x27, 0xd8, 0x4a, 0x3f, 0xb6, 0x37, 0xa0, + 0x9f, 0x6f, 0xa2, 0xa9, 0x3a, 0xb0, 0x68, 0xd6, 0x36, 0x7b, 0xbd, 0x2d, 0xd9, 0xb4, 0x95, 0xc3, + 0x63, 0xe4, 0xff, 0x08, 0x8c, 0x27, 0x13, 0x65, 0x74, 0xb5, 0x0d, 0x5b, 0xa2, 0x19, 0xbb, 0xec, + 0xb7, 0x3b, 0xa1, 0x0a, 0x51, 0x5e, 0xe5, 0x28, 0x67, 0xe9, 0xc5, 0xe6, 0x28, 0x83, 0x88, 0x3e, + 0x26, 0x30, 0xe0, 0xff, 0x4f, 0xaf, 0x74, 0x3b, 0x20, 0xf2, 0x7f, 0xc7, 0xd2, 0x75, 0xda, 0xd1, + 0xff, 0x58, 0x26, 0x4c, 0x73, 0x10, 0x13, 0xf4, 0x6c, 0x0c, 0x88, 0xf7, 0xfc, 0x56, 0xda, 0x86, + 0xfb, 0x59, 0xb7, 0x74, 0x86, 0x47, 0xf2, 0x78, 0xe9, 0x0c, 0x8f, 0x26, 0xf9, 0x9a, 0x1a, 0xae, + 0xfa, 0xad, 0xb4, 0x5b, 0x85, 0x20, 0x29, 0x94, 0xae, 0x55, 0x88, 0xa1, 0xaf, 0xd2, 0xb5, 0x0a, + 0x71, 0xd4, 0x56, 0xd3, 0x56, 0x21, 0x48, 0x54, 0x05, 0x01, 0xf0, 0x1f, 0x0b, 0x5a, 0x06, 0xe0, + 0xfd, 0xc5, 0xa2, 0x65, 0x00, 0xbe, 0xdf, 0x27, 0x5a, 0x01, 0xe0, 0xd8, 0xfa, 0x77, 0x02, 0x87, + 0xef, 0x57, 0xad, 0x8d, 0xda, 0xbe, 0x62, 0xa3, 0x12, 0xa9, 0x8d, 0xba, 0xad, 0xe1, 0xa3, 0xe0, + 0x83, 0x2e, 0x42, 0x7f, 0xef, 0x50, 0x6c, 0xf5, 0x59, 0xfb, 0x86, 0x8a, 0x8a, 0x3f, 0x84, 0xbd, + 0xa0, 0x6c, 0x08, 0x5f, 0x13, 0x38, 0x11, 0x80, 0xb0, 0x8f, 0x79, 0xa8, 0x4b, 0x74, 0x26, 0x05, + 0xae, 0x00, 0x09, 0x65, 0xc3, 0x7c, 0xc6, 0xaf, 0xcc, 0x51, 0x28, 0xf7, 0x1d, 0x09, 0x35, 0x4f, + 0x2f, 0xc5, 0x5e, 0x2c, 0x63, 0x20, 0x8a, 0x4a, 0x91, 0xa3, 0xfc, 0x84, 0xf3, 0x37, 0x6d, 0xa5, + 0xe3, 0x73, 0x62, 0xa0, 0xa6, 0x9b, 0x34, 0x02, 0x1e, 0x48, 0x36, 0x80, 0x2f, 0x11, 0xc0, 0x7e, + 0xe4, 0x6b, 0xe6, 0xe8, 0x6c, 0x02, 0x88, 0x38, 0xb2, 0xc6, 0x46, 0xf5, 0x6f, 0x02, 0xd4, 0x8f, + 0x6a, 0xbf, 0x91, 0x35, 0x49, 0xc4, 0x67, 0xd0, 0xee, 0x30, 0xbe, 0x27, 0x9c, 0x68, 0xf3, 0xce, + 0xdb, 0x57, 0x4c, 0x4d, 0x7c, 0x8b, 0xe0, 0x07, 0xf7, 0x41, 0x17, 0xc9, 0x76, 0xff, 0xe8, 0x9b, + 0x8f, 0xa6, 0xc8, 0xe2, 0xca, 0x93, 0xa7, 0xe3, 0xe4, 0xf3, 0xa7, 0xe3, 0xe4, 0xab, 0xa7, 0xe3, + 0xe4, 0x67, 0xcf, 0xc6, 0x0f, 0x7c, 0xfe, 0x6c, 0xfc, 0xc0, 0xbf, 0x9e, 0x8d, 0x1f, 0x78, 0x38, + 0x5d, 0x52, 0xac, 0x72, 0x75, 0x33, 0x27, 0xeb, 0x5b, 0x5e, 0xa5, 0x9a, 0x5e, 0x64, 0xf9, 0x9a, + 0x57, 0xb7, 0xf5, 0xb8, 0xc2, 0xcc, 0xcd, 0x1e, 0x7e, 0x47, 0x9e, 0xfd, 0x7f, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x7d, 0x8f, 0x8a, 0x3e, 0x36, 0x32, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2824,6 +2824,7 @@ func (c *queryClient) RateLimiterInput(ctx context.Context, in *QueryRateLimiter return out, nil } +// Deprecated: Do not use. func (c *queryClient) OutTxTracker(ctx context.Context, in *QueryGetOutboundTrackerRequest, opts ...grpc.CallOption) (*QueryGetOutboundTrackerResponse, error) { out := new(QueryGetOutboundTrackerResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/OutTxTracker", in, out, opts...) @@ -2833,6 +2834,7 @@ func (c *queryClient) OutTxTracker(ctx context.Context, in *QueryGetOutboundTrac return out, nil } +// Deprecated: Do not use. func (c *queryClient) OutTxTrackerAll(ctx context.Context, in *QueryAllOutboundTrackerRequest, opts ...grpc.CallOption) (*QueryAllOutboundTrackerResponse, error) { out := new(QueryAllOutboundTrackerResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/OutTxTrackerAll", in, out, opts...) @@ -2842,6 +2844,7 @@ func (c *queryClient) OutTxTrackerAll(ctx context.Context, in *QueryAllOutboundT return out, nil } +// Deprecated: Do not use. func (c *queryClient) OutTxTrackerAllByChain(ctx context.Context, in *QueryAllOutboundTrackerByChainRequest, opts ...grpc.CallOption) (*QueryAllOutboundTrackerByChainResponse, error) { out := new(QueryAllOutboundTrackerByChainResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/OutTxTrackerAllByChain", in, out, opts...) @@ -2851,6 +2854,7 @@ func (c *queryClient) OutTxTrackerAllByChain(ctx context.Context, in *QueryAllOu return out, nil } +// Deprecated: Do not use. func (c *queryClient) InTxTrackerAllByChain(ctx context.Context, in *QueryAllInboundTrackerByChainRequest, opts ...grpc.CallOption) (*QueryAllInboundTrackerByChainResponse, error) { out := new(QueryAllInboundTrackerByChainResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/InTxTrackerAllByChain", in, out, opts...) @@ -2860,6 +2864,7 @@ func (c *queryClient) InTxTrackerAllByChain(ctx context.Context, in *QueryAllInb return out, nil } +// Deprecated: Do not use. func (c *queryClient) InTxTrackerAll(ctx context.Context, in *QueryAllInboundTrackersRequest, opts ...grpc.CallOption) (*QueryAllInboundTrackersResponse, error) { out := new(QueryAllInboundTrackersResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/InTxTrackerAll", in, out, opts...) @@ -2869,6 +2874,7 @@ func (c *queryClient) InTxTrackerAll(ctx context.Context, in *QueryAllInboundTra return out, nil } +// Deprecated: Do not use. func (c *queryClient) InTxHashToCctx(ctx context.Context, in *QueryGetInboundHashToCctxRequest, opts ...grpc.CallOption) (*QueryGetInboundHashToCctxResponse, error) { out := new(QueryGetInboundHashToCctxResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/InTxHashToCctx", in, out, opts...) @@ -2878,6 +2884,7 @@ func (c *queryClient) InTxHashToCctx(ctx context.Context, in *QueryGetInboundHas return out, nil } +// Deprecated: Do not use. func (c *queryClient) InTxHashToCctxData(ctx context.Context, in *QueryInboundHashToCctxDataRequest, opts ...grpc.CallOption) (*QueryInboundHashToCctxDataResponse, error) { out := new(QueryInboundHashToCctxDataResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/InTxHashToCctxData", in, out, opts...) @@ -2887,6 +2894,7 @@ func (c *queryClient) InTxHashToCctxData(ctx context.Context, in *QueryInboundHa return out, nil } +// Deprecated: Do not use. func (c *queryClient) InTxHashToCctxAll(ctx context.Context, in *QueryAllInboundHashToCctxRequest, opts ...grpc.CallOption) (*QueryAllInboundHashToCctxResponse, error) { out := new(QueryAllInboundHashToCctxResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.crosschain.Query/InTxHashToCctxAll", in, out, opts...) diff --git a/x/lightclient/types/query.pb.go b/x/lightclient/types/query.pb.go index b3e5d137d7..f6ee9688ec 100644 --- a/x/lightclient/types/query.pb.go +++ b/x/lightclient/types/query.pb.go @@ -702,67 +702,67 @@ func init() { } var fileDescriptor_1ff0d7827c501c48 = []byte{ - // 951 bytes of a gzipped FileDescriptorProto + // 960 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x96, 0xcf, 0x6f, 0x1b, 0x45, - 0x14, 0xc7, 0x33, 0x4d, 0xdd, 0x1f, 0xcf, 0x29, 0x12, 0xd3, 0x56, 0x49, 0x17, 0xea, 0xa4, 0x5b, - 0x42, 0x2b, 0xa3, 0xec, 0xd4, 0xa6, 0x14, 0xe1, 0x4a, 0x40, 0x82, 0x20, 0xad, 0x10, 0x52, 0xba, - 0x15, 0x17, 0x2e, 0xd6, 0x7a, 0x3d, 0xdd, 0x5d, 0x65, 0xb3, 0xb3, 0xdd, 0x99, 0x58, 0x2e, 0x55, - 0x25, 0xc4, 0x5f, 0x80, 0x40, 0x48, 0xfc, 0x19, 0x48, 0x5c, 0x2a, 0x21, 0x2e, 0x1c, 0x50, 0x6f, - 0x54, 0xe2, 0x00, 0x27, 0x84, 0x12, 0x24, 0xfe, 0x0d, 0xb4, 0x33, 0xe3, 0xee, 0x24, 0x59, 0xc7, - 0x1b, 0x37, 0xa7, 0xfd, 0x35, 0xef, 0xbd, 0xcf, 0xf7, 0x3b, 0xcf, 0x6f, 0x0c, 0xcd, 0x2f, 0xa9, - 0xf0, 0xfc, 0xd0, 0x8b, 0x12, 0x22, 0xef, 0x58, 0x46, 0x49, 0x1c, 0x05, 0xa1, 0xf0, 0xe3, 0x88, - 0x26, 0x82, 0x3c, 0xdc, 0xa6, 0xd9, 0x23, 0x27, 0xcd, 0x98, 0x60, 0xb8, 0xf1, 0x62, 0xad, 0x33, - 0x5a, 0xeb, 0x18, 0x6b, 0xad, 0xa6, 0xcf, 0xf8, 0x16, 0xe3, 0xa4, 0xe7, 0x71, 0xaa, 0x02, 0xc9, - 0xa0, 0xd5, 0xa3, 0xc2, 0x6b, 0x91, 0xd4, 0x0b, 0xa2, 0xc4, 0x13, 0x11, 0x4b, 0x54, 0x2e, 0xeb, - 0x42, 0xc0, 0x02, 0x26, 0x6f, 0x49, 0x7e, 0xa7, 0xdf, 0xbe, 0x1e, 0x30, 0x16, 0xc4, 0x94, 0x78, - 0x69, 0x44, 0xbc, 0x24, 0x61, 0x42, 0x86, 0x70, 0xfd, 0xf5, 0xfd, 0x09, 0xac, 0xbd, 0x98, 0xf9, - 0x9b, 0xdd, 0x90, 0x7a, 0x7d, 0x9a, 0x75, 0x07, 0x34, 0x8b, 0x1e, 0x44, 0xbe, 0x59, 0xf3, 0xc6, - 0x84, 0x78, 0xf9, 0xa9, 0xcb, 0x85, 0x27, 0xa8, 0x8e, 0x28, 0x73, 0x27, 0xdd, 0x0c, 0x48, 0x9a, - 0x31, 0xf6, 0x80, 0xeb, 0x8b, 0x5e, 0x3b, 0xaf, 0xd5, 0x6f, 0xf1, 0x80, 0x0c, 0x5a, 0xf9, 0x45, - 0x7d, 0xb0, 0xfb, 0x60, 0xdd, 0xcb, 0xcd, 0x58, 0x8d, 0xe3, 0xb5, 0x9c, 0xf0, 0x8e, 0x04, 0x74, - 0xe9, 0xc3, 0x6d, 0xca, 0x05, 0xfe, 0x04, 0xa0, 0x30, 0x67, 0x01, 0x2d, 0xa1, 0xeb, 0xf5, 0xf6, - 0x9b, 0x8e, 0xca, 0xe5, 0xe4, 0x4e, 0x3a, 0x6a, 0x0b, 0xb4, 0x93, 0xce, 0x86, 0x17, 0x50, 0x1d, - 0xeb, 0x1a, 0x91, 0xf6, 0x2f, 0x08, 0x5e, 0x2b, 0x2d, 0xc3, 0x53, 0x96, 0x70, 0x8a, 0x3f, 0x87, - 0x73, 0xa6, 0x3f, 0x7c, 0x01, 0x2d, 0xcd, 0x5e, 0xaf, 0xb7, 0x9b, 0x4e, 0xc9, 0xa6, 0xa6, 0x9b, - 0x92, 0x3d, 0xd7, 0x66, 0xa4, 0x5a, 0x3b, 0xf9, 0xec, 0xef, 0xc5, 0x19, 0x77, 0xae, 0x57, 0xbc, - 0xe2, 0x78, 0x7d, 0x0f, 0xfe, 0x09, 0x89, 0x7f, 0x6d, 0x22, 0xbe, 0x62, 0xda, 0xc3, 0x7f, 0x5b, - 0xbb, 0xb4, 0x4e, 0x45, 0x89, 0x4b, 0x97, 0x01, 0x34, 0xbd, 0xc7, 0x43, 0xe9, 0xd2, 0x9c, 0x7b, - 0x56, 0x81, 0x78, 0x3c, 0xb4, 0x63, 0xad, 0x7d, 0x7f, 0xb0, 0xd6, 0xfe, 0x19, 0xcc, 0x99, 0xda, - 0xb5, 0xcb, 0x47, 0x90, 0xee, 0xd6, 0x0d, 0xd1, 0xb6, 0x0f, 0x97, 0x46, 0x4e, 0x7f, 0x94, 0x47, - 0xdf, 0xcf, 0x3b, 0xe6, 0xb8, 0xf7, 0xf3, 0x29, 0x2a, 0xda, 0xc6, 0xac, 0xa2, 0x25, 0xdd, 0x83, - 0xba, 0xd1, 0xae, 0x87, 0x6d, 0xa6, 0xd1, 0xe1, 0x4e, 0x91, 0x48, 0x6f, 0x26, 0xf8, 0x2f, 0xde, - 0x1c, 0xdf, 0x56, 0xde, 0xd2, 0xfe, 0xac, 0x53, 0x71, 0xd0, 0x9f, 0x4b, 0x70, 0x46, 0x81, 0x47, - 0x7d, 0xe9, 0xce, 0xac, 0x7b, 0x5a, 0x3e, 0xdf, 0xed, 0xdb, 0x51, 0xd1, 0x02, 0x25, 0x8a, 0x3f, - 0xdd, 0xaf, 0x18, 0x1d, 0x4d, 0xb1, 0xa9, 0x35, 0xff, 0xb5, 0xbc, 0x2a, 0x6b, 0x6d, 0x64, 0x6c, - 0x50, 0x81, 0x0d, 0xcf, 0xc3, 0x69, 0x31, 0x54, 0xdd, 0x97, 0x3b, 0x73, 0xd6, 0x3d, 0x25, 0x86, - 0x79, 0xeb, 0xe1, 0x0e, 0xd4, 0x64, 0xbf, 0x2c, 0xcc, 0x4a, 0xa0, 0x37, 0x26, 0x34, 0xd5, 0x46, - 0x7e, 0x71, 0x55, 0xc8, 0xbe, 0xae, 0x3e, 0x29, 0xf3, 0x16, 0x5d, 0x9d, 0xe3, 0x88, 0x61, 0x37, - 0x4a, 0xfa, 0x74, 0xb8, 0x50, 0x53, 0x38, 0x62, 0x78, 0x37, 0x7f, 0xb4, 0x9b, 0x80, 0x4d, 0x7c, - 0x6d, 0xd1, 0x05, 0xa8, 0x0d, 0xbc, 0x58, 0xc3, 0x9f, 0x71, 0xd5, 0x83, 0x7d, 0x15, 0xae, 0xc8, - 0xb5, 0xaa, 0x7b, 0xef, 0x6f, 0xa7, 0x29, 0xcb, 0x04, 0xed, 0x4b, 0x67, 0xb8, 0x96, 0x6e, 0xff, - 0x80, 0xc0, 0x3e, 0x6c, 0x95, 0xae, 0x90, 0xc1, 0xbc, 0x9e, 0xaf, 0x7c, 0xb4, 0xa2, 0x2b, 0xc5, - 0x8e, 0xe6, 0xc9, 0xcd, 0x49, 0x1b, 0x52, 0x96, 0x5f, 0x37, 0xe3, 0xc5, 0xb0, 0xac, 0xb6, 0x7d, - 0x05, 0x16, 0x0d, 0xb2, 0x8f, 0x13, 0xaf, 0x17, 0xef, 0xa7, 0xff, 0x16, 0xc1, 0xd2, 0xf8, 0x35, - 0x9a, 0x3d, 0x01, 0x5d, 0xa0, 0x4b, 0xd5, 0xf7, 0xe3, 0x23, 0x3f, 0x1f, 0x1e, 0xac, 0xdb, 0xfe, - 0x0d, 0xa0, 0x26, 0xa1, 0xf0, 0x53, 0x04, 0xaf, 0x18, 0xd3, 0x64, 0x35, 0x8e, 0x71, 0x67, 0x52, - 0xb5, 0xf1, 0x47, 0x86, 0x75, 0x7b, 0xaa, 0x58, 0xe5, 0x82, 0xbd, 0xf2, 0xf5, 0x1f, 0xff, 0x7e, - 0x77, 0xe2, 0x1a, 0x5e, 0x96, 0x27, 0xda, 0x8a, 0x3a, 0xdc, 0xc6, 0x9d, 0xa2, 0x1c, 0xff, 0x8a, - 0xa0, 0x6e, 0xa4, 0xa9, 0xc8, 0x5d, 0x3a, 0xc4, 0x2b, 0x72, 0x97, 0xcf, 0x70, 0xbb, 0x23, 0xb9, - 0x6f, 0xe2, 0x76, 0x25, 0x6e, 0xf2, 0xb8, 0xf8, 0x61, 0x3d, 0xc1, 0x3f, 0x21, 0x38, 0x57, 0x0c, - 0x82, 0xdc, 0xfe, 0xf7, 0xaa, 0x5a, 0x78, 0x60, 0x80, 0x59, 0x9d, 0x69, 0x42, 0xb5, 0x88, 0xb7, - 0xa4, 0x88, 0x65, 0x7c, 0x75, 0x9c, 0x08, 0x63, 0xc2, 0xe1, 0x9f, 0x11, 0x40, 0x91, 0xa3, 0x22, - 0x72, 0xd9, 0xcc, 0xb5, 0x3a, 0xd3, 0x84, 0x6a, 0xe4, 0x5b, 0x12, 0xf9, 0x06, 0x76, 0x2a, 0x20, - 0x93, 0xc7, 0xa3, 0xf1, 0xf9, 0x04, 0x7f, 0x8f, 0xa0, 0x26, 0xa7, 0x13, 0x6e, 0x55, 0xaa, 0x6e, - 0x0e, 0x62, 0xab, 0x7d, 0x94, 0x10, 0x0d, 0xba, 0x2c, 0x41, 0x17, 0xf1, 0xe5, 0x71, 0xa0, 0xa9, - 0xa4, 0xf9, 0x13, 0xc1, 0xc5, 0xd2, 0x19, 0x87, 0x57, 0x2b, 0x15, 0x3d, 0x6c, 0x8a, 0x5a, 0x6b, - 0x2f, 0x93, 0x42, 0xeb, 0x78, 0x57, 0xea, 0x68, 0x61, 0x32, 0x4e, 0xc7, 0x98, 0x01, 0x8c, 0x7f, - 0x47, 0x70, 0xbe, 0x64, 0xfe, 0xe1, 0x0f, 0x8e, 0x00, 0x55, 0x36, 0x5d, 0xad, 0x0f, 0xa7, 0x4f, - 0xa0, 0x35, 0xbd, 0x23, 0x35, 0x11, 0xbc, 0x32, 0x41, 0xd3, 0xde, 0xc1, 0x6c, 0xd5, 0xbe, 0xfa, - 0xef, 0xc7, 0x26, 0x5a, 0xbb, 0xf3, 0x6c, 0xa7, 0x81, 0x9e, 0xef, 0x34, 0xd0, 0x3f, 0x3b, 0x0d, - 0xf4, 0xcd, 0x6e, 0x63, 0xe6, 0xf9, 0x6e, 0x63, 0xe6, 0xaf, 0xdd, 0xc6, 0xcc, 0x17, 0x4e, 0x10, - 0x89, 0x70, 0xbb, 0xe7, 0xf8, 0x6c, 0xcb, 0xcc, 0x9c, 0xb0, 0x3e, 0x25, 0xc3, 0x3d, 0x05, 0xc4, - 0xa3, 0x94, 0xf2, 0xde, 0x29, 0xf9, 0x8f, 0xfc, 0xed, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0x4e, - 0x2b, 0x58, 0x28, 0xf6, 0x0c, 0x00, 0x00, + 0x14, 0xc7, 0x33, 0x49, 0xdd, 0x1f, 0xcf, 0x29, 0x12, 0xd3, 0x56, 0x49, 0x17, 0xea, 0xa4, 0x5b, + 0x4a, 0x23, 0x4b, 0xdd, 0x89, 0x4d, 0x85, 0xa8, 0x2b, 0x7e, 0x24, 0x08, 0xd2, 0x0a, 0x21, 0xa5, + 0x5b, 0x71, 0xe1, 0x62, 0xad, 0xed, 0xe9, 0x7a, 0x95, 0xcd, 0xce, 0x76, 0x67, 0x62, 0xb9, 0x54, + 0x48, 0x88, 0x53, 0x8f, 0x08, 0x2e, 0xfd, 0x33, 0x38, 0x21, 0x24, 0x04, 0x37, 0xa4, 0x1e, 0x2b, + 0xb8, 0x20, 0x90, 0x10, 0x4a, 0x90, 0xf8, 0x37, 0xd0, 0xfc, 0x70, 0x76, 0x92, 0xac, 0xe3, 0x4d, + 0x9a, 0xd3, 0xfe, 0x9a, 0xf7, 0xde, 0xe7, 0xfb, 0x9d, 0xe7, 0x37, 0x86, 0xfa, 0x17, 0x54, 0x04, + 0xdd, 0x7e, 0x10, 0x25, 0x44, 0xdd, 0xb1, 0x8c, 0x92, 0x38, 0x0a, 0xfb, 0xa2, 0x1b, 0x47, 0x34, + 0x11, 0xe4, 0xd1, 0x16, 0xcd, 0x1e, 0x7b, 0x69, 0xc6, 0x04, 0xc3, 0xb5, 0xdd, 0xb5, 0xde, 0x68, + 0xad, 0x67, 0xad, 0x75, 0xea, 0x5d, 0xc6, 0x37, 0x19, 0x27, 0x9d, 0x80, 0x53, 0x1d, 0x48, 0x06, + 0x8d, 0x0e, 0x15, 0x41, 0x83, 0xa4, 0x41, 0x18, 0x25, 0x81, 0x88, 0x58, 0xa2, 0x73, 0x39, 0x17, + 0x43, 0x16, 0x32, 0x75, 0x4b, 0xe4, 0x9d, 0x79, 0xfb, 0x7a, 0xc8, 0x58, 0x18, 0x53, 0x12, 0xa4, + 0x11, 0x09, 0x92, 0x84, 0x09, 0x15, 0xc2, 0xcd, 0xd7, 0xf7, 0x26, 0xb0, 0x76, 0x62, 0xd6, 0xdd, + 0x68, 0xf7, 0x69, 0xd0, 0xa3, 0x59, 0x7b, 0x40, 0xb3, 0xe8, 0x61, 0xd4, 0xb5, 0x6b, 0x2e, 0x4f, + 0x88, 0x57, 0x9f, 0xda, 0x5c, 0x04, 0x82, 0x9a, 0x88, 0x22, 0x77, 0xd2, 0x8d, 0x90, 0xa4, 0x19, + 0x63, 0x0f, 0xb9, 0xb9, 0x98, 0xb5, 0x73, 0x46, 0xfd, 0x26, 0x0f, 0xc9, 0xa0, 0x21, 0x2f, 0xfa, + 0x83, 0xdb, 0x03, 0xe7, 0xbe, 0x34, 0x63, 0x25, 0x8e, 0x57, 0x25, 0xe1, 0x5d, 0x05, 0xe8, 0xd3, + 0x47, 0x5b, 0x94, 0x0b, 0xfc, 0x31, 0x40, 0x6e, 0xce, 0x3c, 0x5a, 0x44, 0x4b, 0xd5, 0xe6, 0x9b, + 0x9e, 0xce, 0xe5, 0x49, 0x27, 0x3d, 0xbd, 0x05, 0xc6, 0x49, 0x6f, 0x3d, 0x08, 0xa9, 0x89, 0xf5, + 0xad, 0x48, 0xf7, 0x67, 0x04, 0xaf, 0x15, 0x96, 0xe1, 0x29, 0x4b, 0x38, 0xc5, 0x9f, 0xc1, 0x79, + 0xdb, 0x1f, 0x3e, 0x8f, 0x16, 0x67, 0x96, 0xaa, 0xcd, 0xba, 0x57, 0xb0, 0xa9, 0xe9, 0x86, 0x62, + 0x97, 0xda, 0xac, 0x54, 0xab, 0xa7, 0x9e, 0xff, 0xbd, 0x30, 0xe5, 0xcf, 0x76, 0xf2, 0x57, 0x1c, + 0xaf, 0xed, 0xc1, 0x9f, 0x56, 0xf8, 0x37, 0x26, 0xe2, 0x6b, 0xa6, 0x3d, 0xfc, 0x77, 0x8c, 0x4b, + 0x6b, 0x54, 0x14, 0xb8, 0x74, 0x05, 0xc0, 0xd0, 0x07, 0xbc, 0xaf, 0x5c, 0x9a, 0xf5, 0xcf, 0x69, + 0x90, 0x80, 0xf7, 0xdd, 0xd8, 0x68, 0xdf, 0x1f, 0x6c, 0xb4, 0x7f, 0x0a, 0xb3, 0xb6, 0x76, 0xe3, + 0xf2, 0x11, 0xa4, 0xfb, 0x55, 0x4b, 0xb4, 0xdb, 0x85, 0xcb, 0x23, 0xa7, 0x3f, 0x94, 0xd1, 0x0f, + 0x64, 0xc7, 0x9c, 0xf4, 0x7e, 0xfe, 0x88, 0xf2, 0xb6, 0xb1, 0xab, 0x18, 0x49, 0xf7, 0xa1, 0x6a, + 0xb5, 0xeb, 0x61, 0x9b, 0x69, 0x75, 0xb8, 0x97, 0x27, 0x32, 0x9b, 0x09, 0xdd, 0xdd, 0x37, 0x27, + 0xb7, 0x95, 0x6f, 0x1b, 0x7f, 0xd6, 0xa8, 0x38, 0xe8, 0xcf, 0x65, 0x38, 0xab, 0xc1, 0xa3, 0x9e, + 0x72, 0x67, 0xc6, 0x3f, 0xa3, 0x9e, 0xef, 0xf5, 0xdc, 0x28, 0x6f, 0x81, 0x02, 0xc5, 0x9f, 0xec, + 0x57, 0x8c, 0x8e, 0xa6, 0xd8, 0xd6, 0x2a, 0x7f, 0x2d, 0xaf, 0xaa, 0x5a, 0xeb, 0x19, 0x1b, 0x94, + 0x60, 0xc3, 0x73, 0x70, 0x46, 0x0c, 0x75, 0xf7, 0x49, 0x67, 0xce, 0xf9, 0xa7, 0xc5, 0x50, 0xb6, + 0x1e, 0x6e, 0x41, 0x45, 0xf5, 0xcb, 0xfc, 0x8c, 0x02, 0x7a, 0x63, 0x42, 0x53, 0xad, 0xcb, 0x8b, + 0xaf, 0x43, 0xf6, 0x75, 0xf5, 0x29, 0x95, 0x37, 0xef, 0x6a, 0x89, 0x23, 0x86, 0xed, 0x28, 0xe9, + 0xd1, 0xe1, 0x7c, 0x45, 0xe3, 0x88, 0xe1, 0x3d, 0xf9, 0xe8, 0xd6, 0x01, 0xdb, 0xf8, 0xc6, 0xa2, + 0x8b, 0x50, 0x19, 0x04, 0xb1, 0x81, 0x3f, 0xeb, 0xeb, 0x07, 0xf7, 0x1a, 0x5c, 0x55, 0x6b, 0x75, + 0xf7, 0x3e, 0xd8, 0x4a, 0x53, 0x96, 0x09, 0xda, 0x53, 0xce, 0x70, 0x23, 0xdd, 0x7d, 0x86, 0xc0, + 0x3d, 0x6c, 0x95, 0xa9, 0x90, 0xc1, 0x9c, 0x99, 0xaf, 0x7c, 0xb4, 0xa2, 0xad, 0xc4, 0x8e, 0xe6, + 0xc9, 0xad, 0x49, 0x1b, 0x52, 0x94, 0xdf, 0x34, 0xe3, 0xa5, 0x7e, 0x51, 0x6d, 0xf7, 0x2a, 0x2c, + 0x58, 0x64, 0x1f, 0x25, 0x41, 0x27, 0xde, 0x4f, 0xff, 0x2d, 0x82, 0xc5, 0xf1, 0x6b, 0x0c, 0x7b, + 0x02, 0xa6, 0x40, 0x9b, 0xea, 0xef, 0x27, 0x47, 0x7e, 0xa1, 0x7f, 0xb0, 0x6e, 0xf3, 0x2f, 0x80, + 0x8a, 0x82, 0xc2, 0x3f, 0x21, 0x78, 0xc5, 0x9a, 0x26, 0x2b, 0x71, 0x8c, 0x5b, 0x93, 0xaa, 0x8d, + 0x3f, 0x32, 0x9c, 0x3b, 0xc7, 0x8a, 0xd5, 0x2e, 0xb8, 0xcb, 0x5f, 0xff, 0xfe, 0xef, 0x77, 0xd3, + 0x37, 0xf0, 0x75, 0x75, 0xa2, 0xdd, 0xd4, 0x87, 0xdb, 0xb8, 0x53, 0x94, 0x3f, 0x9d, 0x46, 0xf8, + 0x57, 0x04, 0x55, 0x2b, 0x53, 0x49, 0xf4, 0xc2, 0x39, 0x5e, 0x12, 0xbd, 0x78, 0x8c, 0xbb, 0xef, + 0x2a, 0xf4, 0x5b, 0xb8, 0x59, 0x0a, 0x9d, 0x3c, 0xc9, 0x7f, 0x5b, 0x5f, 0x4a, 0x1d, 0x3f, 0x20, + 0x38, 0x9f, 0x8f, 0x03, 0xb9, 0x09, 0xb7, 0xcb, 0x1a, 0x79, 0x60, 0x8c, 0x39, 0xad, 0xe3, 0x84, + 0x1a, 0x1d, 0x9e, 0xd2, 0x71, 0x1d, 0x5f, 0x1b, 0xa7, 0xc3, 0x9a, 0x73, 0x12, 0xfc, 0x17, 0x04, + 0x90, 0xa7, 0x29, 0x49, 0x5d, 0x34, 0x7c, 0x9d, 0xd6, 0x71, 0x42, 0x0d, 0xf5, 0x6d, 0x45, 0xbd, + 0x8c, 0xbd, 0x12, 0xd4, 0xe4, 0xc9, 0x68, 0x8e, 0x2a, 0xe7, 0x9f, 0x21, 0xa8, 0xa8, 0x49, 0x85, + 0x1b, 0xa5, 0x00, 0xec, 0xa1, 0xec, 0x34, 0x8f, 0x12, 0x62, 0x58, 0x97, 0x14, 0xeb, 0x02, 0xbe, + 0x32, 0x8e, 0x35, 0x95, 0xcb, 0x25, 0xda, 0x9f, 0x08, 0x2e, 0x15, 0x8e, 0x3c, 0xbc, 0x52, 0xaa, + 0xee, 0x61, 0x43, 0xd5, 0x59, 0x7d, 0x99, 0x14, 0x46, 0x4a, 0x4b, 0x49, 0x69, 0x60, 0x32, 0x4e, + 0xca, 0x98, 0x79, 0x2c, 0xc5, 0xfd, 0x86, 0xe0, 0x42, 0xc1, 0x44, 0xc4, 0xef, 0x1f, 0x81, 0xab, + 0x68, 0xde, 0x3a, 0x1f, 0x1c, 0x3f, 0x81, 0x91, 0xf5, 0x8e, 0x92, 0x45, 0xf0, 0xcd, 0x09, 0xb2, + 0xf6, 0x8e, 0xea, 0xa7, 0xd3, 0xc8, 0xa9, 0x7c, 0xf5, 0xdf, 0xf7, 0x75, 0xb4, 0x7a, 0xf7, 0xf9, + 0x76, 0x0d, 0xbd, 0xd8, 0xae, 0xa1, 0x7f, 0xb6, 0x6b, 0xe8, 0x9b, 0x9d, 0xda, 0xd4, 0x8b, 0x9d, + 0xda, 0xd4, 0x1f, 0x3b, 0xb5, 0xa9, 0xcf, 0xbd, 0x30, 0x12, 0xfd, 0xad, 0x8e, 0xd7, 0x65, 0x9b, + 0x76, 0xf2, 0x84, 0xf5, 0x28, 0x19, 0xee, 0xa9, 0x21, 0x1e, 0xa7, 0x94, 0x77, 0x4e, 0xab, 0xbf, + 0xe9, 0x6f, 0xfd, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x83, 0xf3, 0xe5, 0xa0, 0x0b, 0x0d, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -794,6 +794,7 @@ func NewQueryClient(cc grpc1.ClientConn) QueryClient { return &queryClient{cc} } +// Deprecated: Do not use. func (c *queryClient) BlockHeaderAll(ctx context.Context, in *QueryAllBlockHeaderRequest, opts ...grpc.CallOption) (*QueryAllBlockHeaderResponse, error) { out := new(QueryAllBlockHeaderResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/BlockHeaderAll", in, out, opts...) @@ -803,6 +804,7 @@ func (c *queryClient) BlockHeaderAll(ctx context.Context, in *QueryAllBlockHeade return out, nil } +// Deprecated: Do not use. func (c *queryClient) BlockHeader(ctx context.Context, in *QueryGetBlockHeaderRequest, opts ...grpc.CallOption) (*QueryGetBlockHeaderResponse, error) { out := new(QueryGetBlockHeaderResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/BlockHeader", in, out, opts...) @@ -812,6 +814,7 @@ func (c *queryClient) BlockHeader(ctx context.Context, in *QueryGetBlockHeaderRe return out, nil } +// Deprecated: Do not use. func (c *queryClient) ChainStateAll(ctx context.Context, in *QueryAllChainStateRequest, opts ...grpc.CallOption) (*QueryAllChainStateResponse, error) { out := new(QueryAllChainStateResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/ChainStateAll", in, out, opts...) @@ -821,6 +824,7 @@ func (c *queryClient) ChainStateAll(ctx context.Context, in *QueryAllChainStateR return out, nil } +// Deprecated: Do not use. func (c *queryClient) ChainState(ctx context.Context, in *QueryGetChainStateRequest, opts ...grpc.CallOption) (*QueryGetChainStateResponse, error) { out := new(QueryGetChainStateResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/ChainState", in, out, opts...) @@ -830,6 +834,7 @@ func (c *queryClient) ChainState(ctx context.Context, in *QueryGetChainStateRequ return out, nil } +// Deprecated: Do not use. func (c *queryClient) Prove(ctx context.Context, in *QueryProveRequest, opts ...grpc.CallOption) (*QueryProveResponse, error) { out := new(QueryProveResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/Prove", in, out, opts...) @@ -839,6 +844,7 @@ func (c *queryClient) Prove(ctx context.Context, in *QueryProveRequest, opts ... return out, nil } +// Deprecated: Do not use. func (c *queryClient) HeaderSupportedChains(ctx context.Context, in *QueryHeaderSupportedChainsRequest, opts ...grpc.CallOption) (*QueryHeaderSupportedChainsResponse, error) { out := new(QueryHeaderSupportedChainsResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/HeaderSupportedChains", in, out, opts...) @@ -848,6 +854,7 @@ func (c *queryClient) HeaderSupportedChains(ctx context.Context, in *QueryHeader return out, nil } +// Deprecated: Do not use. func (c *queryClient) HeaderEnabledChains(ctx context.Context, in *QueryHeaderEnabledChainsRequest, opts ...grpc.CallOption) (*QueryHeaderEnabledChainsResponse, error) { out := new(QueryHeaderEnabledChainsResponse) err := c.cc.Invoke(ctx, "/zetachain.zetacore.lightclient.Query/HeaderEnabledChains", in, out, opts...) From 4e5c732c4d6980f8e1599928a87ef8408225481c Mon Sep 17 00:00:00 2001 From: Tanmay Date: Wed, 26 Feb 2025 14:21:23 -0500 Subject: [PATCH 18/22] test: update tss address on gateway for TSS migration test (#3597) * update tss address on gateway for TSS migration test * improve comment * add todo for solana --- cmd/zetae2e/local/tss_migration.go | 5 +++++ e2e/e2etests/test_migrate_tss.go | 8 ++++---- e2e/runner/admin_evm.go | 17 +++++++++++++++++ e2e/runner/setup_zevm.go | 2 -- .../chains/bitcoin/signer/outbound_data.go | 4 ++-- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/cmd/zetae2e/local/tss_migration.go b/cmd/zetae2e/local/tss_migration.go index 30358dc7b4..629c973f7b 100644 --- a/cmd/zetae2e/local/tss_migration.go +++ b/cmd/zetae2e/local/tss_migration.go @@ -90,7 +90,12 @@ func TSSMigration(deployerRunner *runner.E2ERunner, logger *runner.Logger, verbo logger.Print("❌ tss migration failed") os.Exit(1) } + + // Update TSS address for contracts in connected chains + // TODO : Update TSS address for other chains if necessary + // https://github.com/zeta-chain/node/issues/3599 deployerRunner.UpdateTSSAddressForConnector() deployerRunner.UpdateTSSAddressForERC20custody() + deployerRunner.UpdateTSSAddressForGateway() logger.Print("✅ migration completed in %s ", time.Since(migrationStartTime).String()) } diff --git a/e2e/e2etests/test_migrate_tss.go b/e2e/e2etests/test_migrate_tss.go index 545e4abc55..b17f9431a2 100644 --- a/e2e/e2etests/test_migrate_tss.go +++ b/e2e/e2etests/test_migrate_tss.go @@ -147,8 +147,8 @@ func TestMigrateTSS(r *runner.E2ERunner, _ []string) { btcTSSBalanceNew += utxo.Amount } - r.Logger.Info("BTC Balance Old: %f", btcTSSBalanceOld*1e8) - r.Logger.Info("BTC Balance New: %f", btcTSSBalanceNew*1e8) + r.Logger.Info("BTC TSS Balance Old: %f", btcTSSBalanceOld*1e8) + r.Logger.Info("BTC TSS Balance New: %f", btcTSSBalanceNew*1e8) r.Logger.Info("Migrator amount : %s", cctxBTC.GetCurrentOutboundParam().Amount) // btcTSSBalanceNew should be less than btcTSSBalanceOld as there is some loss of funds during migration @@ -165,8 +165,8 @@ func TestMigrateTSS(r *runner.E2ERunner, _ []string) { ethTSSBalanceNew, err := r.EVMClient.BalanceAt(context.Background(), r.TSSAddress, nil) require.NoError(r, err) - r.Logger.Info("TSS Balance Old: %s", ethTSSBalanceOld.String()) - r.Logger.Info("TSS Balance New: %s", ethTSSBalanceNew.String()) + r.Logger.Info("ETH TSS Balance Old: %s", ethTSSBalanceOld.String()) + r.Logger.Info("ETH TSS Balance New: %s", ethTSSBalanceNew.String()) r.Logger.Info("Migrator amount : %s", cctxETH.GetCurrentOutboundParam().Amount.String()) // ethTSSBalanceNew should be less than ethTSSBalanceOld as there is some loss of funds during migration diff --git a/e2e/runner/admin_evm.go b/e2e/runner/admin_evm.go index e9dad09a1d..5baf4a124d 100644 --- a/e2e/runner/admin_evm.go +++ b/e2e/runner/admin_evm.go @@ -7,6 +7,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" ) +// UpdateTSSAddressForConnector updates the TSS address for the connector contract func (r *E2ERunner) UpdateTSSAddressForConnector() { require.NoError(r, r.SetTSSAddresses()) @@ -21,6 +22,7 @@ func (r *E2ERunner) UpdateTSSAddressForConnector() { require.Equal(r, r.TSSAddress, tssAddressOnConnector) } +// UpdateTSSAddressForERC20custody updates the TSS address for the ERC20 custody contract func (r *E2ERunner) UpdateTSSAddressForERC20custody() { require.NoError(r, r.SetTSSAddresses()) @@ -35,3 +37,18 @@ func (r *E2ERunner) UpdateTSSAddressForERC20custody() { require.NoError(r, err) require.Equal(r, r.TSSAddress, tssAddressOnCustody) } + +// UpdateTSSAddressForGateway updates the TSS address for the gateway contract +func (r *E2ERunner) UpdateTSSAddressForGateway() { + require.NoError(r, r.SetTSSAddresses()) + + tx, err := r.GatewayEVM.UpdateTSSAddress(r.EVMAuth, r.TSSAddress) + require.NoError(r, err) + r.Logger.Info("TSS Gateway Address Update Tx: %s", tx.Hash().String()) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.EVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt) + + tssAddressOnGateway, err := r.GatewayEVM.TssAddress(&bind.CallOpts{Context: r.Ctx}) + require.NoError(r, err) + require.Equal(r, r.TSSAddress, tssAddressOnGateway) +} diff --git a/e2e/runner/setup_zevm.go b/e2e/runner/setup_zevm.go index f8fb26ba79..306ecc5d18 100644 --- a/e2e/runner/setup_zevm.go +++ b/e2e/runner/setup_zevm.go @@ -32,8 +32,6 @@ var EmissionsPoolFunding = big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(2e7)) // SetTSSAddresses set TSS addresses from information queried from ZetaChain func (r *E2ERunner) SetTSSAddresses() error { - r.Logger.Print("⚙️ setting up TSS address") - btcChainID, err := chains.GetBTCChainIDFromChainParams(r.BitcoinParams) if err != nil { return err diff --git a/zetaclient/chains/bitcoin/signer/outbound_data.go b/zetaclient/chains/bitcoin/signer/outbound_data.go index ed62195707..669221c2a6 100644 --- a/zetaclient/chains/bitcoin/signer/outbound_data.go +++ b/zetaclient/chains/bitcoin/signer/outbound_data.go @@ -57,8 +57,8 @@ func NewOutboundData( } params := cctx.GetCurrentOutboundParam() - // support gas token only for Bitcoin outbound - if cctx.InboundParams.CoinType != coin.CoinType_Gas { + // support coin type GAS and CMD only + if cctx.InboundParams.CoinType != coin.CoinType_Gas && cctx.InboundParams.CoinType != coin.CoinType_Cmd { return nil, fmt.Errorf("invalid coin type %s", cctx.InboundParams.CoinType.String()) } From 6e8dfeed63b338c2b2565bc1816f265b8f5b51fa Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Thu, 27 Feb 2025 16:31:58 +0100 Subject: [PATCH 19/22] test(e2e): sui deposits (#3560) * initialize test * initialize sui local testing * fix zrc20 * some testing * fix deposit test * add deposit and call * add coin parsing * fake usdc setup * setup fungible token sui * add sui token deposit tests * lint * fungible token deposit test * lint * fix cursor issue * fix tests * fix gosec * deposit tests * add sui account * add accounts in config * add address balance check * add sui token zrc20 in config * few fixes * fix payload encoding * refactor faucet function * consolidate prefix * remove log * Fix more conflicts * Drop initial cursor resolution. Fix tests * Adapt e2e gw configs * Decrease logs when events=0 * fix chain info error * refactor SUI zrc20 setup * fix liquidity cap --------- Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- changelog.md | 1 + cmd/zetae2e/config/config.go | 11 + cmd/zetae2e/config/contracts.go | 38 +++ cmd/zetae2e/config/local.yml | 4 + cmd/zetae2e/config/localnet.yml | 4 + cmd/zetae2e/local/local.go | 20 +- cmd/zetae2e/local/sui.go | 68 +++++ .../localnet/orchestrator/start-zetae2e.sh | 3 + contrib/localnet/scripts/start-zetacored.sh | 4 +- e2e/config/config.go | 17 ++ e2e/contracts/sui/bin.go | 8 + e2e/contracts/sui/fake_usdc.mv | Bin 0 -> 641 bytes e2e/e2etests/e2etests.go | 43 +++ e2e/e2etests/test_sui_deposit.go | 37 +++ e2e/e2etests/test_sui_deposit_and_call.go | 43 +++ e2e/e2etests/test_sui_token_deposit.go | 37 +++ .../test_sui_token_deposit_and_call.go | 42 +++ e2e/runner/logger.go | 4 +- e2e/runner/runner.go | 68 ++++- e2e/runner/setup_sui.go | 243 ++++++++++++++++- e2e/runner/setup_zevm.go | 21 ++ e2e/runner/solana.go | 2 +- e2e/runner/sui.go | 254 ++++++++++++++++++ e2e/txserver/authority.go | 25 ++ pkg/chains/chain.go | 3 +- pkg/coin/coin_test.go | 2 +- pkg/contracts/sui/gateway.go | 9 +- pkg/contracts/sui/gateway_test.go | 9 +- testutil/keeper/crosschain.go | 2 +- .../cctx_orchestrator_validate_inbound.go | 10 +- x/crosschain/keeper/evm_deposit_test.go | 14 +- .../keeper/msg_server_vote_inbound_tx.go | 4 +- x/crosschain/types/cctx.go | 2 +- x/crosschain/types/inbound_parsing_test.go | 4 +- x/fungible/keeper/deposits.go | 5 +- .../chains/evm/observer/outbound_test.go | 2 +- zetaclient/chains/sui/client/client.go | 2 +- zetaclient/chains/sui/observer/inbound.go | 13 +- zetaclient/chains/sui/observer/observer.go | 33 +-- .../chains/sui/observer/observer_test.go | 37 ++- 40 files changed, 1039 insertions(+), 109 deletions(-) create mode 100644 cmd/zetae2e/local/sui.go create mode 100644 e2e/contracts/sui/fake_usdc.mv create mode 100644 e2e/e2etests/test_sui_deposit.go create mode 100644 e2e/e2etests/test_sui_deposit_and_call.go create mode 100644 e2e/e2etests/test_sui_token_deposit.go create mode 100644 e2e/e2etests/test_sui_token_deposit_and_call.go create mode 100644 e2e/runner/sui.go diff --git a/changelog.md b/changelog.md index 2c3dc1b073..853b852ac2 100644 --- a/changelog.md +++ b/changelog.md @@ -41,6 +41,7 @@ * [3430](https://github.com/zeta-chain/node/pull/3430) - add simulation test for MsgWithDrawEmission * [3503](https://github.com/zeta-chain/node/pull/3503) - add check in e2e test to ensure deletion of stale ballots * [3536](https://github.com/zeta-chain/node/pull/3536) - add e2e test for upgrading solana gateway program +* [3560](https://github.com/zeta-chain/node/pull/3560) - initialize Sui E2E deposit tests ## v28.0.0 diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 9cc11d815c..96ba899544 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -58,6 +58,14 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.Solana.GatewayProgramID = config.DoubleQuotedString(r.GatewayProgram.String()) conf.Contracts.Solana.SPLAddr = config.DoubleQuotedString(r.SPLAddr.String()) + if r.SuiGateway != nil { + conf.Contracts.Sui.GatewayPackageID = config.DoubleQuotedString(r.SuiGateway.PackageID()) + conf.Contracts.Sui.GatewayObjectID = config.DoubleQuotedString(r.SuiGateway.ObjectID()) + } + + conf.Contracts.Sui.FungibleTokenCoinType = config.DoubleQuotedString(r.SuiTokenCoinType) + conf.Contracts.Sui.FungibleTokenTreasuryCap = config.DoubleQuotedString(r.SuiTokenTreasuryCap) + conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(r.ZetaEthAddr.Hex()) conf.Contracts.EVM.ConnectorEthAddr = config.DoubleQuotedString(r.ConnectorEthAddr.Hex()) conf.Contracts.EVM.CustodyAddr = config.DoubleQuotedString(r.ERC20CustodyAddr.Hex()) @@ -73,6 +81,9 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.ZEVM.SOLZRC20Addr = config.DoubleQuotedString(r.SOLZRC20Addr.Hex()) conf.Contracts.ZEVM.SPLZRC20Addr = config.DoubleQuotedString(r.SPLZRC20Addr.Hex()) conf.Contracts.ZEVM.TONZRC20Addr = config.DoubleQuotedString(r.TONZRC20Addr.Hex()) + conf.Contracts.ZEVM.SUIZRC20Addr = config.DoubleQuotedString(r.SUIZRC20Addr.Hex()) + conf.Contracts.ZEVM.SuiTokenZRC20Addr = config.DoubleQuotedString(r.SuiTokenZRC20Addr.Hex()) + conf.Contracts.ZEVM.UniswapFactoryAddr = config.DoubleQuotedString(r.UniswapV2FactoryAddr.Hex()) conf.Contracts.ZEVM.UniswapRouterAddr = config.DoubleQuotedString(r.UniswapV2RouterAddr.Hex()) conf.Contracts.ZEVM.ConnectorZEVMAddr = config.DoubleQuotedString(r.ConnectorZEVMAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 445067338c..798e21f547 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -24,6 +24,7 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/contracts/sui" "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-core/contracts/uniswapv2factory.sol" uniswapv2router "github.com/zeta-chain/node/pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol" fungibletypes "github.com/zeta-chain/node/x/fungible/types" @@ -117,6 +118,21 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { r.SPLAddr = solana.MustPublicKeyFromBase58(c.String()) } + // set Sui contracts + suiPackageID := conf.Contracts.Sui.GatewayPackageID + suiGatewayID := conf.Contracts.Sui.GatewayObjectID + + if suiPackageID != "" && suiGatewayID != "" { + r.SuiGateway = sui.NewGateway(suiPackageID.String(), suiGatewayID.String()) + } + + if c := conf.Contracts.Sui.FungibleTokenCoinType; c != "" { + r.SuiTokenCoinType = c.String() + } + if c := conf.Contracts.Sui.FungibleTokenTreasuryCap; c != "" { + r.SuiTokenTreasuryCap = c.String() + } + evmChainID, err := r.EVMClient.ChainID(r.Ctx) require.NoError(r, err, "get evm chain ID") evmChainParams := chainParamsByChainID(chainParams, evmChainID.Int64()) @@ -224,6 +240,28 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { } } + if c := conf.Contracts.ZEVM.SUIZRC20Addr; c != "" { + r.SUIZRC20Addr, err = c.AsEVMAddress() + if err != nil { + return fmt.Errorf("invalid SUIZRC20Addr: %w", err) + } + r.SUIZRC20, err = zrc20.NewZRC20(r.SUIZRC20Addr, r.ZEVMClient) + if err != nil { + return err + } + } + + if c := conf.Contracts.ZEVM.SuiTokenZRC20Addr; c != "" { + r.SuiTokenZRC20Addr, err = c.AsEVMAddress() + if err != nil { + return fmt.Errorf("invalid SuiTokenZRC20Addr: %w", err) + } + r.SuiTokenZRC20, err = zrc20.NewZRC20(r.SuiTokenZRC20Addr, r.ZEVMClient) + if err != nil { + return err + } + } + if c := conf.Contracts.ZEVM.UniswapFactoryAddr; c != "" { r.UniswapV2FactoryAddr, err = c.AsEVMAddress() if err != nil { diff --git a/cmd/zetae2e/config/local.yml b/cmd/zetae2e/config/local.yml index af9dc0039c..70ce5ad8be 100644 --- a/cmd/zetae2e/config/local.yml +++ b/cmd/zetae2e/config/local.yml @@ -36,6 +36,10 @@ additional_accounts: evm_address: "0xf67deecc3B15F9CEeF5eba3468ed601f3e0B9de2" private_key: "2b3306a8ac43dbf0e350b87876c131e7e12bd49563a16de9ce8aeb664b94d559" solana_private_key: "4yqSQxDeTBvn86BuxcN5jmZb2gaobFXrBqu8kiE9rZxNkVMe3LfXmFigRsU4sRp7vk4vVP1ZCFiejDKiXBNWvs2C" + user_sui: + bech32_address: "zeta1qluk7yfyqfejwss64lqmn7de6svudcedz94zs2" + evm_address: "0x07F96F1124027327421AAFc1B9F9B9D419C6e32d" + private_key: "e9f0e48e37fb2c2357b4fbc2b675fce434889c46052d92e8f4cabd74507a65ad" user_legacy_ether: bech32_address: "zeta134rakuus43xn63yucgxhn88ywj8ewcv6ezn2ga" evm_address: "0x8D47Db7390AC4D3D449Cc20D799ce4748F97619A" diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index 6abf3b2e18..25fbaa4444 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -34,6 +34,10 @@ additional_accounts: evm_address: "0xf67deecc3B15F9CEeF5eba3468ed601f3e0B9de2" private_key: "2b3306a8ac43dbf0e350b87876c131e7e12bd49563a16de9ce8aeb664b94d559" solana_private_key: "4yqSQxDeTBvn86BuxcN5jmZb2gaobFXrBqu8kiE9rZxNkVMe3LfXmFigRsU4sRp7vk4vVP1ZCFiejDKiXBNWvs2C" + user_sui: + bech32_address: "zeta1qluk7yfyqfejwss64lqmn7de6svudcedz94zs2" + evm_address: "0x07F96F1124027327421AAFc1B9F9B9D419C6e32d" + private_key: "e9f0e48e37fb2c2357b4fbc2b675fce434889c46052d92e8f4cabd74507a65ad" user_legacy_ether: bech32_address: "zeta134rakuus43xn63yucgxhn88ywj8ewcv6ezn2ga" evm_address: "0x8D47Db7390AC4D3D449Cc20D799ce4748F97619A" diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 38ca0363dc..7f71b7075c 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -84,7 +84,7 @@ func NewLocalCmd() *cobra.Command { cmd.Flags().Bool(flagTestTSSMigration, false, "set to true to include a migration test at the end") cmd.Flags().Bool(flagTestLegacy, false, "set to true to run legacy EVM tests") cmd.Flags().Bool(flagSkipTrackerCheck, false, "set to true to skip tracker check at the end of the tests") - cmd.Flags().Bool(flagSkipPrecompiles, false, "set to true to skip stateful precompiled contracts test") + cmd.Flags().Bool(flagSkipPrecompiles, true, "set to true to skip stateful precompiled contracts test") cmd.Flags(). Bool(flagUpgradeContracts, false, "set to true to upgrade Gateways and ERC20Custody contracts during setup for ZEVM and EVM") @@ -197,10 +197,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // monitor block production to ensure we fail fast if there are consensus failures go monitorBlockProductionCancel(ctx, cancel, conf) - if testSui && !skipSetup { - deployerRunner.SetupSui(conf.RPCs.SuiFaucet) - } - // set the authority client to the zeta tx server to be able to query message permissions deployerRunner.ZetaTxServer.SetAuthorityClient(deployerRunner.AuthorityClient) @@ -261,6 +257,10 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // Update the chain params to contains protocol contract addresses deployerRunner.UpdateProtocolContractsInChainParams() + if testSui { + deployerRunner.SetupSui(conf.RPCs.SuiFaucet) + } + logger.Print("✅ setup completed in %s", time.Since(startTime)) } @@ -423,6 +423,16 @@ func localE2ETest(cmd *cobra.Command, _ []string) { eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } + if testSui { + suiTests := []string{ + e2etests.TestSuiDepositName, + e2etests.TestSuiDepositAndCallName, + e2etests.TestSuiTokenDepositName, + e2etests.TestSuiTokenDepositAndCallName, + } + eg.Go(suiTestRoutine(conf, deployerRunner, verbose, suiTests...)) + } + if testTON { if deployerRunner.Clients.TON == nil { logger.Print("❌ TON client is nil, maybe TON lite-server config is not set") diff --git a/cmd/zetae2e/local/sui.go b/cmd/zetae2e/local/sui.go new file mode 100644 index 0000000000..bbfca0b94a --- /dev/null +++ b/cmd/zetae2e/local/sui.go @@ -0,0 +1,68 @@ +package local + +import ( + "fmt" + "time" + + "github.com/fatih/color" + + "github.com/zeta-chain/node/e2e/config" + "github.com/zeta-chain/node/e2e/e2etests" + "github.com/zeta-chain/node/e2e/runner" +) + +// suiTestRoutine runs Sui related e2e tests +func suiTestRoutine( + conf config.Config, + deployerRunner *runner.E2ERunner, + verbose bool, + testNames ...string, +) func() error { + return func() (err error) { + // initialize runner for sui test + suiRunner, err := initTestRunner( + "sui", + conf, + deployerRunner, + conf.AdditionalAccounts.UserSui, + runner.NewLogger(verbose, color.FgHiCyan, "sui"), + runner.WithZetaTxServer(deployerRunner.ZetaTxServer), + ) + if err != nil { + return err + } + + suiRunner.Logger.Print("🏃 starting Sui tests") + startTime := time.Now() + + suiRunnerSigner, err := suiRunner.Account.SuiSigner() + if err != nil { + return err + } + + // get tokens for the account + suiRunner.RequestSuiFromFaucet(conf.RPCs.SuiFaucet, suiRunnerSigner.Address()) + + // mint fungible tokens to the account + txRes := deployerRunner.SuiMintUSDC("10000000", suiRunnerSigner.Address()) + + deployerRunner.Logger.Info("Sui USDC mint tx: %s", txRes.Digest) + + // run sui test + testsToRun, err := suiRunner.GetE2ETestsToRunByName( + e2etests.AllE2ETests, + testNames..., + ) + if err != nil { + return fmt.Errorf("sui tests failed: %v", err) + } + + if err := suiRunner.RunE2ETests(testsToRun); err != nil { + return fmt.Errorf("sui tests failed: %v", err) + } + + suiRunner.Logger.Print("🍾 sui tests completed in %s", time.Since(startTime).String()) + + return err + } +} diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index b5f7d43564..989a9fb134 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -115,6 +115,9 @@ fund_eth_from_config '.additional_accounts.user_bitcoin_withdraw.evm_address' 10 # unlock solana tester accounts fund_eth_from_config '.additional_accounts.user_solana.evm_address' 10000 "solana tester" +# unlock sui tester accounts +fund_eth_from_config '.additional_accounts.user_sui.evm_address' 10000 "sui tester" + # unlock miscellaneous tests accounts fund_eth_from_config '.additional_accounts.user_misc.evm_address' 10000 "misc tester" diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index 3c7474ef11..4444238f64 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -246,7 +246,9 @@ then # solana tester address=$(yq -r '.additional_accounts.user_solana.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta -# migration tester +# sui tester + address=$(yq -r '.additional_accounts.user_sui.bech32_address' /root/config.yml) + zetacored add-genesis-account "$address" 100000000000000000000000000azeta# migration tester address=$(yq -r '.additional_accounts.user_migration.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta # precompile tester diff --git a/e2e/config/config.go b/e2e/config/config.go index a66096fd72..8e4adcd41e 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -72,6 +72,7 @@ type AdditionalAccounts struct { UserBitcoinWithdraw Account `yaml:"user_bitcoin_withdraw"` UserSolana Account `yaml:"user_solana"` UserSPL Account `yaml:"user_spl"` + UserSui Account `yaml:"user_sui"` UserMisc Account `yaml:"user_misc"` UserAdmin Account `yaml:"user_admin"` UserMigration Account `yaml:"user_migration"` // used for TSS migration, TODO: rename (https://github.com/zeta-chain/node/issues/2780) @@ -122,6 +123,7 @@ type Contracts struct { EVM EVM `yaml:"evm"` ZEVM ZEVM `yaml:"zevm"` Solana Solana `yaml:"solana"` + Sui Sui `yaml:"sui"` } // Solana contains the addresses of predeployed contracts and accounts on the Solana chain @@ -130,6 +132,14 @@ type Solana struct { SPLAddr DoubleQuotedString `yaml:"spl"` } +// Sui contains the addresses of predeployed contracts on the Sui chain +type Sui struct { + GatewayPackageID DoubleQuotedString `yaml:"gateway_package_id"` + GatewayObjectID DoubleQuotedString `yaml:"gateway_object_id"` + FungibleTokenCoinType DoubleQuotedString `yaml:"fungible_token_coin_type"` + FungibleTokenTreasuryCap DoubleQuotedString `yaml:"fungible_token_treasury_cap"` +} + // EVM contains the addresses of predeployed contracts on the EVM chain type EVM struct { ZetaEthAddr DoubleQuotedString `yaml:"zeta_eth"` @@ -151,6 +161,8 @@ type ZEVM struct { SOLZRC20Addr DoubleQuotedString `yaml:"sol_zrc20"` SPLZRC20Addr DoubleQuotedString `yaml:"spl_zrc20"` TONZRC20Addr DoubleQuotedString `yaml:"ton_zrc20"` + SUIZRC20Addr DoubleQuotedString `yaml:"sui_zrc20"` + SuiTokenZRC20Addr DoubleQuotedString `yaml:"sui_token_zrc20"` UniswapFactoryAddr DoubleQuotedString `yaml:"uniswap_factory"` UniswapRouterAddr DoubleQuotedString `yaml:"uniswap_router"` ConnectorZEVMAddr DoubleQuotedString `yaml:"connector_zevm"` @@ -243,6 +255,7 @@ func (a AdditionalAccounts) AsSlice() []Account { a.UserBitcoinDeposit, a.UserBitcoinWithdraw, a.UserSolana, + a.UserSui, a.UserSPL, a.UserLegacyEther, a.UserMisc, @@ -338,6 +351,10 @@ func (c *Config) GenerateKeys() error { if err != nil { return err } + c.AdditionalAccounts.UserSui, err = generateAccount() + if err != nil { + return err + } c.AdditionalAccounts.UserLegacyEther, err = generateAccount() if err != nil { return err diff --git a/e2e/contracts/sui/bin.go b/e2e/contracts/sui/bin.go index 3718d5e6dd..da247aab93 100644 --- a/e2e/contracts/sui/bin.go +++ b/e2e/contracts/sui/bin.go @@ -8,7 +8,15 @@ import ( //go:embed gateway.mv var gatewayBinary []byte +//go:embed fake_usdc.mv +var fakeUSDC []byte + // GatewayBytecodeBase64 gets the gateway binary encoded as base64 for deployment func GatewayBytecodeBase64() string { return base64.StdEncoding.EncodeToString(gatewayBinary) } + +// FakeUSDCBytecodeBase64 gets the fake USDC binary encoded as base64 for deployment +func FakeUSDCBytecodeBase64() string { + return base64.StdEncoding.EncodeToString(fakeUSDC) +} diff --git a/e2e/contracts/sui/fake_usdc.mv b/e2e/contracts/sui/fake_usdc.mv new file mode 100644 index 0000000000000000000000000000000000000000..4c604c26192e971d84dce45939cfbe34a6f92c82 GIT binary patch literal 641 zcmbtSy^hpC47NSL$;{o}9#>M*f}o2;g%)TKqTsGXA(zZbf!)b!b|QAK0cg6YS10T8$2|08W?!sGL7%te%OIA!ai^*G!JKiU*jh zsQ@LW#svtbqobKg+)S>9KKjYL4Y}!>+!U{0ym?h$y??pV?{>K#qPt9?*^lYCYIeot zVKu}Y4q05KjcmDY(K4Jv-Hs`R*dARsZnwvJ-G@zAteelF9`{`<``Bl>?PHd4h(Ql? z>xF::::") + packageID := "0x" + splitted[0] + + // create the tx + tx, err := r.Clients.Sui.MoveCall(r.Ctx, models.MoveCallRequest{ + Signer: signer.Address(), + PackageObjectId: packageID, + Module: "fake_usdc", + Function: "mint", + TypeArguments: []any{}, + Arguments: []any{r.SuiTokenTreasuryCap, amount, receiver}, + GasBudget: "5000000000", + }) + require.NoError(r, err) + + return r.suiExecuteTx(signer, tx) +} + +// suiExecuteDeposit executes a deposit on the SUI contract +func (r *E2ERunner) suiExecuteDeposit( + signer *sui.SignerSecp256k1, + coinType string, + coinObjectID string, + receiver ethcommon.Address, +) models.SuiTransactionBlockResponse { + // create the tx + tx, err := r.Clients.Sui.MoveCall(r.Ctx, models.MoveCallRequest{ + Signer: signer.Address(), + PackageObjectId: r.SuiGateway.PackageID(), + Module: "gateway", + Function: "deposit", + TypeArguments: []any{coinType}, + Arguments: []any{r.SuiGateway.ObjectID(), coinObjectID, receiver.Hex()}, + GasBudget: "5000000000", + }) + require.NoError(r, err) + + return r.suiExecuteTx(signer, tx) +} + +// suiExecuteDepositAndCall executes a depositAndCall on the SUI contract +func (r *E2ERunner) suiExecuteDepositAndCall( + signer *sui.SignerSecp256k1, + coinType string, + coinObjectID string, + receiver ethcommon.Address, + payload []byte, +) models.SuiTransactionBlockResponse { + // create the tx + tx, err := r.Clients.Sui.MoveCall(r.Ctx, models.MoveCallRequest{ + Signer: signer.Address(), + PackageObjectId: r.SuiGateway.PackageID(), + Module: "gateway", + Function: "deposit_and_call", + TypeArguments: []any{coinType}, + Arguments: []any{r.SuiGateway.ObjectID(), coinObjectID, receiver.Hex(), payload}, + GasBudget: "5000000000", + }) + require.NoError(r, err) + + return r.suiExecuteTx(signer, tx) +} + +// suiSplitUSDC splits USDC coin and obtain a USDC coin object with the wanted balance +func (r *E2ERunner) suiSplitUSDC(signer *sui.SignerSecp256k1, balance math.Uint) (objID string) { + // find the coin to split + originalCoin := r.suiFindCoinWithBalanceAbove(signer.Address(), balance, "0x"+r.SuiTokenCoinType) + + tx, err := r.Clients.Sui.SplitCoin(r.Ctx, models.SplitCoinRequest{ + Signer: signer.Address(), + CoinObjectId: originalCoin, + SplitAmounts: []string{balance.String()}, + GasBudget: "5000000000", + }) + + require.NoError(r, err) + r.suiExecuteTx(signer, tx) + + // find the split coin + return r.suiFindCoinWithBalance(signer.Address(), balance, "0x"+r.SuiTokenCoinType) +} + +// suiSplitSUI splits SUI coin and obtain a SUI coin object with the wanted balance +func (r *E2ERunner) suiSplitSUI(signer *sui.SignerSecp256k1, balance math.Uint) (objID string) { + // find the coin to split + originalCoin := r.suiFindCoinWithBalanceAbove(signer.Address(), balance, string(sui.SUI)) + + // split the coin using the PaySui API + tx, err := r.Clients.Sui.PaySui(r.Ctx, models.PaySuiRequest{ + Signer: signer.Address(), + SuiObjectId: []string{originalCoin}, + Recipient: []string{signer.Address()}, + Amount: []string{balance.String()}, + GasBudget: "5000000000", + }) + require.NoError(r, err) + + r.suiExecuteTx(signer, tx) + + // find the split coin + return r.suiFindCoinWithBalance(signer.Address(), balance, string(sui.SUI)) +} + +func (r *E2ERunner) suiFindCoinWithBalance( + address string, + balance math.Uint, + coinType string, +) (coinID string) { + return r.suiFindCoin(address, balance, coinType, func(a, b math.Uint) bool { + return a.Equal(b) + }) +} + +func (r *E2ERunner) suiFindCoinWithBalanceAbove( + address string, + balanceAbove math.Uint, + coinType string, +) (coinID string) { + return r.suiFindCoin(address, balanceAbove, coinType, func(a, b math.Uint) bool { + return a.GTE(b) + }) +} + +type compFunc func(a, b math.Uint) bool + +func (r *E2ERunner) suiFindCoin( + address string, + balance math.Uint, + coinType string, + comp compFunc, +) (coinID string) { + res, err := r.Clients.Sui.SuiXGetCoins(r.Ctx, models.SuiXGetCoinsRequest{ + Owner: address, + CoinType: coinType, + }) + require.NoError(r, err) + + for _, data := range res.Data { + coinBalance, err := math.ParseUint(data.Balance) + require.NoError(r, err) + + if comp(coinBalance, balance) { + return data.CoinObjectId + } + } + + require.FailNow(r, fmt.Sprintf("coin %s not found for address %s", coinType, address)) + return "" +} + +func (r *E2ERunner) suiExecuteTx( + signer *sui.SignerSecp256k1, + tx models.TxnMetaData, +) models.SuiTransactionBlockResponse { + // sign the tx + signature, err := signer.SignTxBlock(tx) + require.NoError(r, err, "sign transaction") + + resp, err := r.Clients.Sui.SuiExecuteTransactionBlock(r.Ctx, models.SuiExecuteTransactionBlockRequest{ + TxBytes: tx.TxBytes, + Signature: []string{signature}, + RequestType: "WaitForLocalExecution", + }) + require.NoError(r, err) + + return resp +} diff --git a/e2e/txserver/authority.go b/e2e/txserver/authority.go index b2e5fcf3dc..43f00543a8 100644 --- a/e2e/txserver/authority.go +++ b/e2e/txserver/authority.go @@ -4,6 +4,7 @@ import ( "fmt" e2eutils "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" authoritytypes "github.com/zeta-chain/node/x/authority/types" ) @@ -31,3 +32,27 @@ func (zts ZetaTxServer) AddAuthorization(msgURL string) error { return nil } + +// UpdateChainInfo sets the chain info in the authority module +func (zts ZetaTxServer) UpdateChainInfo(chain chains.Chain) error { + // retrieve account + accAdmin, err := zts.clientCtx.Keyring.Key(e2eutils.AdminPolicyName) + if err != nil { + return err + } + addrAdmin, err := accAdmin.GetAddress() + if err != nil { + return err + } + + // set chain info + _, err = zts.BroadcastTx(e2eutils.AdminPolicyName, authoritytypes.NewMsgUpdateChainInfo( + addrAdmin.String(), + chain, + )) + if err != nil { + return fmt.Errorf("failed to update chain info: %w", err) + } + + return nil +} diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 1ccb56600d..ca64ebc116 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -135,8 +135,9 @@ func DecodeAddressFromChainID(chainID int64, addr string, additionalChains []Cha if err != nil { return nil, fmt.Errorf("invalid TON address %q: %w", addr, err) } - return []byte(acc.ToRaw()), nil + case IsSuiChain(chainID, additionalChains): + return []byte(addr), nil default: return nil, fmt.Errorf("chain (%d) not supported", chainID) } diff --git a/pkg/coin/coin_test.go b/pkg/coin/coin_test.go index acb8f7187c..e08891503a 100644 --- a/pkg/coin/coin_test.go +++ b/pkg/coin/coin_test.go @@ -144,7 +144,7 @@ func TestCoinType_SupportsRefund(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.c.SupportsRefund(); got != tt.want { - t.Errorf("CoinType.SupportsRefund() = %v, want %v", got, tt.want) + t.Errorf("FungibleTokenCoinType.SupportsRefund() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index 190d90e783..198b75fbfc 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -1,6 +1,7 @@ package sui import ( + "encoding/base64" "fmt" "strconv" "strings" @@ -270,7 +271,13 @@ func convertPayload(data []any) ([]byte, error) { } } - return payload, nil + // Sui encode bytes in base64 + decodedPayload, err := base64.StdEncoding.DecodeString(string(payload)) + if err != nil { + return nil, errors.Wrap(err, "failed to decode payload from base64") + } + + return decodedPayload, nil } func parsePair(pair string) (string, string, error) { diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index fb723f4d3d..3a8f64346c 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -1,6 +1,7 @@ package sui import ( + "encoding/base64" "fmt" "testing" @@ -29,6 +30,12 @@ func TestParseEvent(t *testing.T) { receiverAlice := sample.EthAddress() receiverBob := sample.EthAddress() + var payload []any + payloadBytes := []byte(base64.StdEncoding.EncodeToString([]byte{0, 1, 2})) + for _, p := range payloadBytes { + payload = append(payload, float64(p)) + } + for _, tt := range []struct { name string event models.SuiEventResponse @@ -77,7 +84,7 @@ func TestParseEvent(t *testing.T) { "amount": "200", "sender": sender, "receiver": receiverBob.String(), - "payload": []any{float64(0), float64(1), float64(2)}, + "payload": payload, }, }, assert: func(t *testing.T, raw models.SuiEventResponse, out Event) { diff --git a/testutil/keeper/crosschain.go b/testutil/keeper/crosschain.go index 151c7aa6ca..7862c8549b 100644 --- a/testutil/keeper/crosschain.go +++ b/testutil/keeper/crosschain.go @@ -313,7 +313,7 @@ func MockProcessV2RevertDeposit( // inboundSender string // amount *big.Int // chainID int64 - // coinType coin.CoinType + // coinType coin.FungibleTokenCoinType // asset string // revertAddress common.Address // callOnRevert bool diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go index 3ae74fe45c..3aaacf2c24 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go @@ -1,6 +1,7 @@ package keeper import ( + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/pkg/chains" @@ -20,9 +21,8 @@ func (k Keeper) ValidateInbound( if !tssFound { return nil, types.ErrCannotFindTSSKeys } - err := k.CheckIfTSSMigrationTransfer(ctx, msg) - if err != nil { - return nil, err + if err := k.CheckIfTSSMigrationTransfer(ctx, msg); err != nil { + return nil, errors.Wrap(err, "tss migration transfer check failed") } // Do not process if inbound is disabled @@ -33,7 +33,7 @@ func (k Keeper) ValidateInbound( // create a new CCTX from the inbound message. The status of the new CCTX is set to PendingInbound. cctx, err := types.NewCCTX(ctx, *msg, tss.TssPubkey) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to create new CCTX") } // Initiate outbound, the process function manages the state commit and cctx status change. @@ -43,7 +43,7 @@ func (k Keeper) ValidateInbound( ShouldPayGas: shouldPayGas, }) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to initiate outbound") } inCctxIndex, ok := ctx.Value(InCCTXIndexKey).(string) diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index b65071a13f..6ba9017ccd 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -95,7 +95,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", mock.Anything, @@ -142,7 +142,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", mock.Anything, @@ -206,7 +206,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", mock.Anything, @@ -296,7 +296,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", @@ -342,7 +342,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", @@ -388,7 +388,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", mock.Anything, @@ -433,7 +433,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount := big.NewInt(42) // expect DepositCoinZeta to be called - // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.FungibleTokenCoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", mock.Anything, diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index 0c1b875479..ebdd507575 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -75,7 +75,7 @@ func (k msgServer) VoteInbound( msg.InboundHash, ) if err != nil { - return nil, sdkerrors.Wrap(err, voteInboundID) + return nil, sdkerrors.Wrap(err, "failed to vote on inbound ballot") } // If it is a new ballot, check if an inbound with the same hash, sender chain and event index has already been finalized @@ -102,7 +102,7 @@ func (k msgServer) VoteInbound( cctx, err := k.ValidateInbound(ctx, msg, true) if err != nil { - return nil, sdkerrors.Wrap(err, voteInboundID) + return nil, sdkerrors.Wrap(err, "failed to validate inbound") } // Save the inbound CCTX to the store. This is called irrespective of the status of the CCTX or the outcome of the process function. k.SaveObservedInboundInformation(ctx, cctx, msg.EventIndex) diff --git a/x/crosschain/types/cctx.go b/x/crosschain/types/cctx.go index b7d315d7f5..e2ca13f273 100644 --- a/x/crosschain/types/cctx.go +++ b/x/crosschain/types/cctx.go @@ -180,7 +180,7 @@ func (m *CrossChainTx) AddRevertOutbound(gasLimit uint64) error { ConfirmationMode: m.GetCurrentOutboundParam().ConfirmationMode, } - // TODO : Refactor to move CoinType field to the CCTX object directly : https://github.com/zeta-chain/node/issues/1943 + // TODO : Refactor to move FungibleTokenCoinType field to the CCTX object directly : https://github.com/zeta-chain/node/issues/1943 if m.InboundParams != nil { revertTxParams.CoinType = m.InboundParams.CoinType } diff --git a/x/crosschain/types/inbound_parsing_test.go b/x/crosschain/types/inbound_parsing_test.go index 0d030fdd3d..13ad954598 100644 --- a/x/crosschain/types/inbound_parsing_test.go +++ b/x/crosschain/types/inbound_parsing_test.go @@ -33,7 +33,7 @@ func TestNewWithdrawalInbound(t *testing.T) { // _, err := types.NewWithdrawalInbound( // ctx, // sample.EthAddress().Hex(), - // fc.CoinType, + // fc.FungibleTokenCoinType, // fc.Asset, // nil, // chains.GoerliLocalnet, @@ -73,7 +73,7 @@ func TestNewWithdrawAndCallInbound(t *testing.T) { // _, err := types.NewWithdrawAndCallInbound( // ctx, // sample.EthAddress().Hex(), - // fc.CoinType, + // fc.FungibleTokenCoinType, // fc.Asset, // nil, // chains.GoerliLocalnet, diff --git a/x/fungible/keeper/deposits.go b/x/fungible/keeper/deposits.go index 64d050bd79..eaea906976 100644 --- a/x/fungible/keeper/deposits.go +++ b/x/fungible/keeper/deposits.go @@ -260,7 +260,10 @@ func (k Keeper) getAndCheckZRC20( } else { foreignCoin, found = k.GetForeignCoinFromAsset(ctx, asset, chainID) if !found { - return ethcommon.Address{}, types.ForeignCoins{}, crosschaintypes.ErrForeignCoinNotFound + return ethcommon.Address{}, types.ForeignCoins{}, errors.Wrapf( + crosschaintypes.ErrForeignCoinNotFound, + "asset: %s, chainID %d", asset, chainID, + ) } } zrc20Contract = ethcommon.HexToAddress(foreignCoin.Zrc20ContractAddress) diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 6ca7d2a6f7..00ebee4b90 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -565,7 +565,7 @@ func Test_FilterTSSOutbound(t *testing.T) { // // load archived outbound receipt that contains ZetaReceived event // // https://etherscan.io/tx/0x81342051b8a85072d3e3771c1a57c7bdb5318e8caf37f5a687b7a91e50a7257f // nonce := uint64(9718) -// coinType := coin.CoinType(5) // unknown coin type +// coinType := coin.FungibleTokenCoinType(5) // unknown coin type // cctx, outbound, receipt := testutils.LoadEVMCctxNOutboundNReceipt( // t, // TestDataDir, diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index f6493650ca..9714265b54 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -17,7 +17,7 @@ type Client struct { sui.ISuiAPI } -const DefaultEventsLimit = 100 +const DefaultEventsLimit = 50 const filterMoveEventModule = "MoveEventModule" diff --git a/zetaclient/chains/sui/observer/inbound.go b/zetaclient/chains/sui/observer/inbound.go index f0eb368fe1..5ddec29ce4 100644 --- a/zetaclient/chains/sui/observer/inbound.go +++ b/zetaclient/chains/sui/observer/inbound.go @@ -19,9 +19,7 @@ var errTxNotFound = errors.New("no tx found") // ObserveInbound processes inbound deposit cross-chain transactions. func (ob *Observer) ObserveInbound(ctx context.Context) error { - if err := ob.ensureCursor(ctx); err != nil { - return errors.Wrap(err, "unable to ensure inbound cursor") - } + ob.ensureCursor() query := client.EventQuery{ PackageID: ob.gateway.PackageID(), @@ -36,6 +34,13 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { return errors.Wrap(err, "unable to query module events") } + if len(events) == 0 { + ob.Logger().Inbound.Debug().Msg("No inbound events found") + return nil + } + + ob.Logger().Inbound.Info().Int("events", len(events)).Msg("Processing inbound events") + for _, event := range events { // Note: we can make this concurrent if needed. // Let's revisit later @@ -46,7 +51,7 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // try again later ob.Logger().Inbound.Warn().Err(err). Str(logs.FieldTx, event.Id.TxDigest). - Msg("TX not found or unfinalized. Pausing") + Msg("TX not found or not finalized. Pausing") return nil case err != nil: // failed processing also updates the cursor diff --git a/zetaclient/chains/sui/observer/observer.go b/zetaclient/chains/sui/observer/observer.go index 47df76b0aa..b4600ceaba 100644 --- a/zetaclient/chains/sui/observer/observer.go +++ b/zetaclient/chains/sui/observer/observer.go @@ -126,45 +126,16 @@ func (ob *Observer) setLatestGasPrice(price uint64) { } // ensureCursor ensures tx scroll cursor for inbound observations -func (ob *Observer) ensureCursor(ctx context.Context) error { +func (ob *Observer) ensureCursor() { if ob.LastTxScanned() != "" { - return nil + return } // Note that this would only work for the empty chain database envValue := os.Getenv(base.EnvVarLatestTxByChain(ob.Chain())) if envValue != "" { ob.WithLastTxScanned(envValue) - return nil } - - // let's take the first tx that was ever registered for the Gateway (deployment tx) - // Note that this might have for a non-archival node - req := models.SuiGetObjectRequest{ - ObjectId: ob.gateway.PackageID(), - Options: models.SuiObjectDataOptions{ - ShowPreviousTransaction: true, - }, - } - - res, err := ob.client.SuiGetObject(ctx, req) - switch { - case err != nil: - return errors.Wrap(err, "unable to get object") - case res.Error != nil: - return errors.Errorf("get object error: %s (code %s)", res.Error.Error, res.Error.Code) - case res.Data == nil: - return errors.New("object data is empty") - case res.Data.PreviousTransaction == "": - return errors.New("previous transaction is empty") - } - - cursor := client.EncodeCursor(models.EventId{ - TxDigest: res.Data.PreviousTransaction, - EventSeq: "0", - }) - - return ob.setCursor(cursor) } func (ob *Observer) getCursor() string { return ob.LastTxScanned() } diff --git a/zetaclient/chains/sui/observer/observer_test.go b/zetaclient/chains/sui/observer/observer_test.go index c14efbef0e..1b22e83f5d 100644 --- a/zetaclient/chains/sui/observer/observer_test.go +++ b/zetaclient/chains/sui/observer/observer_test.go @@ -2,6 +2,7 @@ package observer import ( "context" + "encoding/base64" "fmt" "testing" @@ -65,30 +66,11 @@ func TestObserver(t *testing.T) { const usdc = "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN" - // Given gateway object from RPC (for "ensuring" the initial scroll cursor) - gatewayRequest := models.SuiGetObjectRequest{ - ObjectId: ts.gateway.PackageID(), - Options: models.SuiObjectDataOptions{ - ShowPreviousTransaction: true, - }, - } - - gatewayObject := models.SuiObjectResponse{ - Data: &models.SuiObjectData{ - ObjectId: ts.gateway.PackageID(), - PreviousTransaction: "ABC123_first_tx", - }, - } - - ts.suiMock. - On("SuiGetObject", mock.Anything, gatewayRequest). - Return(gatewayObject, nil) - // Given list of gateway events... expectedQuery := client.EventQuery{ PackageID: ts.gateway.PackageID(), Module: ts.gateway.Module(), - Cursor: "ABC123_first_tx,0", + Cursor: "", Limit: client.DefaultEventsLimit, } @@ -112,7 +94,7 @@ func TestObserver(t *testing.T) { "amount": "300", "sender": "SUI_ALICE", "receiver": evmAlice.String(), - "payload": []any{float64(1), float64(2), float64(3)}, + "payload": preparePayload([]byte{1, 2, 3}), }), ts.SampleEvent("TX_4_invalid_data", string(sui.DepositEvent), map[string]any{ "coin_type": string(sui.SUI), @@ -141,7 +123,7 @@ func TestObserver(t *testing.T) { assert.Equal(t, "TX_4_invalid_data,0", ts.LastTxScanned()) // Check for transactions - assert.Equal(t, 2, len(ts.inboundVotesBag)) + require.Equal(t, 2, len(ts.inboundVotesBag)) vote1 := ts.inboundVotesBag[0] assert.Equal(t, "TX_1_ok", vote1.InboundHash) @@ -503,3 +485,14 @@ func (ts *testSuite) MockOutboundTrackers(trackers []cctypes.OutboundTracker) *m On("GetAllOutboundTrackerByChain", mock.Anything, ts.Chain().ChainId, mock.Anything). Return(trackers, nil) } + +func preparePayload(payload []byte) []any { + payloadBytes := []byte(base64.StdEncoding.EncodeToString(payload)) + + var out []any + for _, p := range payloadBytes { + out = append(out, float64(p)) + } + + return out +} From ed8faa6cfd0bfc44d2fe14c712bc64041f8c39a2 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 27 Feb 2025 16:10:43 +0000 Subject: [PATCH 20/22] fix: hardcode gas limits to avoid estimate gas calls (#3602) * hardcode gas limits to avoid estimate gas calls * fix ci * use same default gas limit --- changelog.md | 1 + .../testgasconsumer/TestGasConsumer.bin | 2 +- .../testgasconsumer/TestGasConsumer.go | 2 +- .../testgasconsumer/TestGasConsumer.json | 2 +- .../testgasconsumer/TestGasConsumer.sol | 2 +- x/fungible/keeper/evm.go | 11 ++++++----- x/fungible/keeper/evm_gateway.go | 2 +- x/fungible/keeper/gas_coin_and_pool.go | 4 ++-- x/fungible/keeper/gas_price.go | 6 +++--- .../msg_server_update_system_contract.go | 6 +++--- x/fungible/keeper/system_contract.go | 8 ++++---- x/fungible/keeper/zrc20_methods.go | 18 +++++++++--------- 12 files changed, 33 insertions(+), 31 deletions(-) diff --git a/changelog.md b/changelog.md index 853b852ac2..1d32b07179 100644 --- a/changelog.md +++ b/changelog.md @@ -35,6 +35,7 @@ * [3501](https://github.com/zeta-chain/node/pull/3501) - fix E2E test failure caused by nil `ConfirmationParams` for Solana and TON * [3509](https://github.com/zeta-chain/node/pull/3509) - schedule Bitcoin TSS keysign on interval to avoid TSS keysign spam * [3517](https://github.com/zeta-chain/node/pull/3517) - remove duplicate gateway event appending to fix false positive on multiple events in same tx +* [3602](https://github.com/zeta-chain/node/pull/3602) - hardcode gas limits to avoid estimate gas calls ### Tests diff --git a/e2e/contracts/testgasconsumer/TestGasConsumer.bin b/e2e/contracts/testgasconsumer/TestGasConsumer.bin index 590b637cc1..b0aafe4254 100644 --- a/e2e/contracts/testgasconsumer/TestGasConsumer.bin +++ b/e2e/contracts/testgasconsumer/TestGasConsumer.bin @@ -1 +1 @@ -6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b6000624c4b4090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea2646970667358221220e1d03a34090a8a647a128849d9f9434831ba3b1e4d28a514d9c9dc922068351e64736f6c634300081a0033 +6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b60006216e36090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea26469706673582212208d28c81ee36a96847aff0ea97e72e0e3b384da903bf7760cf019cbe78945877e64736f6c634300081a0033 diff --git a/e2e/contracts/testgasconsumer/TestGasConsumer.go b/e2e/contracts/testgasconsumer/TestGasConsumer.go index b40770c40b..4bbb5543db 100644 --- a/e2e/contracts/testgasconsumer/TestGasConsumer.go +++ b/e2e/contracts/testgasconsumer/TestGasConsumer.go @@ -39,7 +39,7 @@ type TestGasConsumerzContext struct { // TestGasConsumerMetaData contains all meta data concerning the TestGasConsumer contract. var TestGasConsumerMetaData = &bind.MetaData{ ABI: "[{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structTestGasConsumer.zContext\",\"name\":\"_context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"_zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"}],\"name\":\"onCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", - Bin: "0x6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b6000624c4b4090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea2646970667358221220e1d03a34090a8a647a128849d9f9434831ba3b1e4d28a514d9c9dc922068351e64736f6c634300081a0033", + Bin: "0x6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b60006216e36090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea26469706673582212208d28c81ee36a96847aff0ea97e72e0e3b384da903bf7760cf019cbe78945877e64736f6c634300081a0033", } // TestGasConsumerABI is the input ABI used to generate the binding from. diff --git a/e2e/contracts/testgasconsumer/TestGasConsumer.json b/e2e/contracts/testgasconsumer/TestGasConsumer.json index 9f80d60d5f..c038cd8f16 100644 --- a/e2e/contracts/testgasconsumer/TestGasConsumer.json +++ b/e2e/contracts/testgasconsumer/TestGasConsumer.json @@ -46,5 +46,5 @@ "type": "function" } ], - "bin": "6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b6000624c4b4090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea2646970667358221220e1d03a34090a8a647a128849d9f9434831ba3b1e4d28a514d9c9dc922068351e64736f6c634300081a0033" + "bin": "6080604052348015600f57600080fd5b5061036d8061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80635bcfd61614610030575b600080fd5b61004a60048036038101906100459190610233565b61004c565b005b61005461005b565b5050505050565b60006216e36090506000614e209050600081836100789190610306565b905060005b818110156100bb576000819080600181540180825580915050600190039060005260206000200160009091909190915055808060010191505061007d565b506000806100c991906100ce565b505050565b50805460008255906000526020600020908101906100ec91906100ef565b50565b5b808211156101085760008160009055506001016100f0565b5090565b600080fd5b600080fd5b600080fd5b60006060828403121561013157610130610116565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101658261013a565b9050919050565b6101758161015a565b811461018057600080fd5b50565b6000813590506101928161016c565b92915050565b6000819050919050565b6101ab81610198565b81146101b657600080fd5b50565b6000813590506101c8816101a2565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126101f3576101f26101ce565b5b8235905067ffffffffffffffff8111156102105761020f6101d3565b5b60208301915083600182028301111561022c5761022b6101d8565b5b9250929050565b60008060008060006080868803121561024f5761024e61010c565b5b600086013567ffffffffffffffff81111561026d5761026c610111565b5b6102798882890161011b565b955050602061028a88828901610183565b945050604061029b888289016101b9565b935050606086013567ffffffffffffffff8111156102bc576102bb610111565b5b6102c8888289016101dd565b92509250509295509295909350565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061031182610198565b915061031c83610198565b92508261032c5761032b6102d7565b5b82820490509291505056fea26469706673582212208d28c81ee36a96847aff0ea97e72e0e3b384da903bf7760cf019cbe78945877e64736f6c634300081a0033" } diff --git a/e2e/contracts/testgasconsumer/TestGasConsumer.sol b/e2e/contracts/testgasconsumer/TestGasConsumer.sol index 4d1d322cd2..c44c134eee 100644 --- a/e2e/contracts/testgasconsumer/TestGasConsumer.sol +++ b/e2e/contracts/testgasconsumer/TestGasConsumer.sol @@ -26,7 +26,7 @@ contract TestGasConsumer { function consumeGas() internal { // Approximate target gas consumption - uint256 targetGas = 5000000; + uint256 targetGas = 1500000; // Approximate gas cost for a single storage write uint256 storageWriteGasCost = 20000; uint256 iterations = targetGas / storageWriteGasCost; diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index d920f336e5..c027e4c5b8 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -40,8 +40,9 @@ import ( var ( BigIntZero = big.NewInt(0) - ZEVMGasLimitDepositAndCall = big.NewInt(1_000_000) - ZEVMGasLimitConnectorCall = big.NewInt(1_000_000) + DefaultGasLimit = big.NewInt(200_000) + ZEVMGasLimitDepositAndCall = big.NewInt(1_500_000) + ZEVMGasLimitConnectorCall = big.NewInt(1_500_000) ) // DeployContract deploys a new contract in the ZEVM @@ -258,7 +259,7 @@ func (k Keeper) DepositZRC20( types.ModuleAddressEVM, contract, BigIntZero, - nil, + DefaultGasLimit, true, false, "deposit", @@ -283,7 +284,7 @@ func (k Keeper) UpdateZRC20ProtocolFlatFee( types.ModuleAddressEVM, zrc20Addr, BigIntZero, - nil, + DefaultGasLimit, true, false, "updateProtocolFlatFee", @@ -307,7 +308,7 @@ func (k Keeper) UpdateZRC20GasLimit( types.ModuleAddressEVM, zrc20Addr, BigIntZero, - nil, + DefaultGasLimit, true, false, "updateGasLimit", diff --git a/x/fungible/keeper/evm_gateway.go b/x/fungible/keeper/evm_gateway.go index e7e21901a2..b689578cc6 100644 --- a/x/fungible/keeper/evm_gateway.go +++ b/x/fungible/keeper/evm_gateway.go @@ -16,7 +16,7 @@ import ( ) // gatewayGasLimit is the gas limit for the gateway functions -var gatewayGasLimit = big.NewInt(1_000_000) +var gatewayGasLimit = big.NewInt(1_500_000) // CallUpdateGatewayAddress calls the updateGatewayAddress function on the ZRC20 contract // function updateGatewayAddress(address addr) diff --git a/x/fungible/keeper/gas_coin_and_pool.go b/x/fungible/keeper/gas_coin_and_pool.go index 25794ea401..701e87e121 100644 --- a/x/fungible/keeper/gas_coin_and_pool.go +++ b/x/fungible/keeper/gas_coin_and_pool.go @@ -111,7 +111,7 @@ func (k Keeper) SetupChainGasCoinAndPool( types.ModuleAddressEVM, systemContractAddress, BigIntZero, - nil, + DefaultGasLimit, true, false, "setGasZetaPool", @@ -146,7 +146,7 @@ func (k Keeper) SetupChainGasCoinAndPool( types.ModuleAddressEVM, zrc20Addr, BigIntZero, - nil, + DefaultGasLimit, true, false, "approve", diff --git a/x/fungible/keeper/gas_price.go b/x/fungible/keeper/gas_price.go index 3d58624ad5..4aaa59dc17 100644 --- a/x/fungible/keeper/gas_price.go +++ b/x/fungible/keeper/gas_price.go @@ -34,7 +34,7 @@ func (k Keeper) SetGasPrice(ctx sdk.Context, chainid *big.Int, gasPrice *big.Int types.ModuleAddressEVM, oracle, BigIntZero, - big.NewInt(50_000), + DefaultGasLimit, true, false, "setGasPrice", @@ -70,7 +70,7 @@ func (k Keeper) SetGasCoin(ctx sdk.Context, chainid *big.Int, address ethcommon. types.ModuleAddressEVM, oracle, BigIntZero, - nil, + DefaultGasLimit, true, false, "setGasCoinZRC20", @@ -106,7 +106,7 @@ func (k Keeper) SetGasZetaPool(ctx sdk.Context, chainid *big.Int, pool ethcommon types.ModuleAddressEVM, oracle, BigIntZero, - nil, + DefaultGasLimit, true, false, "setGasZetaPool", diff --git a/x/fungible/keeper/msg_server_update_system_contract.go b/x/fungible/keeper/msg_server_update_system_contract.go index ea9f988a85..b754451683 100644 --- a/x/fungible/keeper/msg_server_update_system_contract.go +++ b/x/fungible/keeper/msg_server_update_system_contract.go @@ -58,7 +58,7 @@ func (k msgServer) UpdateSystemContract( types.ModuleAddressEVM, zrc20Addr, BigIntZero, - nil, + DefaultGasLimit, true, false, "updateSystemContractAddress", @@ -78,7 +78,7 @@ func (k msgServer) UpdateSystemContract( types.ModuleAddressEVM, newSystemContractAddr, BigIntZero, - nil, + DefaultGasLimit, true, false, "setGasCoinZRC20", @@ -98,7 +98,7 @@ func (k msgServer) UpdateSystemContract( types.ModuleAddressEVM, newSystemContractAddr, BigIntZero, - nil, + DefaultGasLimit, true, false, "setGasZetaPool", diff --git a/x/fungible/keeper/system_contract.go b/x/fungible/keeper/system_contract.go index 99c485f69e..20328c49c3 100644 --- a/x/fungible/keeper/system_contract.go +++ b/x/fungible/keeper/system_contract.go @@ -374,7 +374,7 @@ func (k *Keeper) CallUniswapV2RouterSwapExactTokensForTokens( sender, routerAddress, BigIntZero, - big.NewInt(1000_000), + big.NewInt(1_000_000), true, noEthereumTxEvent, "swapExactTokensForTokens", @@ -732,7 +732,7 @@ func (k *Keeper) CallZRC20Burn( sender, zrc20address, big.NewInt(0), - big.NewInt(100_000), + DefaultGasLimit, true, noEthereumTxEvent, "burn", @@ -764,7 +764,7 @@ func (k *Keeper) CallZRC20Deposit( sender, zrc20address, big.NewInt(0), - big.NewInt(100_000), + DefaultGasLimit, true, false, "deposit", @@ -797,7 +797,7 @@ func (k *Keeper) CallZRC20Approve( owner, zrc20address, BigIntZero, - nil, + DefaultGasLimit, true, noEthereumTxEvent, "approve", diff --git a/x/fungible/keeper/zrc20_methods.go b/x/fungible/keeper/zrc20_methods.go index 142ba012a7..21a11bd561 100644 --- a/x/fungible/keeper/zrc20_methods.go +++ b/x/fungible/keeper/zrc20_methods.go @@ -43,7 +43,7 @@ func (k Keeper) ZRC20SetName( fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), - big.NewInt(1_000_000), + DefaultGasLimit, true, true, setName, @@ -77,7 +77,7 @@ func (k Keeper) ZRC20SetSymbol( fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), - big.NewInt(1_000_000), + DefaultGasLimit, true, true, setSymbol, @@ -110,7 +110,7 @@ func (k Keeper) ZRC20Name( fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), - big.NewInt(1_000_000), + nil, false, true, name, @@ -157,7 +157,7 @@ func (k Keeper) ZRC20Symbol( fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), - big.NewInt(1_000_000), + nil, false, true, symbol, @@ -215,7 +215,7 @@ func (k Keeper) ZRC20Allowance( zrc20Address, big.NewInt(0), nil, - true, + false, true, allowance, args..., @@ -271,7 +271,7 @@ func (k Keeper) ZRC20BalanceOf( zrc20Address, big.NewInt(0), nil, - true, + false, true, balanceOf, owner, @@ -323,7 +323,7 @@ func (k Keeper) ZRC20TotalSupply( zrc20Address, big.NewInt(0), nil, - true, + false, true, totalSupply, ) @@ -379,7 +379,7 @@ func (k Keeper) ZRC20Transfer( from, zrc20Address, big.NewInt(0), - nil, + DefaultGasLimit, true, true, transfer, @@ -439,7 +439,7 @@ func (k Keeper) ZRC20TransferFrom( spender, zrc20Address, big.NewInt(0), - nil, + DefaultGasLimit, true, true, transferFrom, From 3b6ca1a375367359e124b7c0523009fb61c8dc4c Mon Sep 17 00:00:00 2001 From: 0xM3R Date: Thu, 27 Feb 2025 18:23:08 +0100 Subject: [PATCH 21/22] feat: zetachain-sec-checks (#3603) This adds zetachain specific protocol security checks implemented as CodeQL custom rules Co-authored-by: Christopher Fuka <97121270+CryptoFewka@users.noreply.github.com> --- .github/workflows/zetachain-sec-checks.yml | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/zetachain-sec-checks.yml diff --git a/.github/workflows/zetachain-sec-checks.yml b/.github/workflows/zetachain-sec-checks.yml new file mode 100644 index 0000000000..47aba1fe2e --- /dev/null +++ b/.github/workflows/zetachain-sec-checks.yml @@ -0,0 +1,26 @@ +name: "CodeQL - ZetaChain Custom checks " + +on: [push, pull_request] + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + packs: zeta-chain/protocol-security-codeql + - name: Analyze + uses: github/codeql-action/analyze@v3 From 06182c96e7dfdb770072f85ddef7cf03df90f521 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Thu, 27 Feb 2025 10:59:55 -0800 Subject: [PATCH 22/22] feat(zetaclient): add dedicated restricted addresses config file (#3600) * feat(zetaclient): add dedicated config file restricted addresses * use errors * use bg. errors should not be fatal to the main prodcess * changelog --- changelog.md | 1 + cmd/zetaclientd/start.go | 12 +- contrib/localnet/scripts/start-zetaclientd.sh | 3 + .../chains/bitcoin/observer/event_test.go | 4 +- .../bitcoin/signer/outbound_data_test.go | 2 +- .../chains/evm/observer/inbound_test.go | 14 +- .../chains/evm/observer/outbound_test.go | 2 +- .../chains/solana/observer/inbound_test.go | 4 +- zetaclient/compliance/compliance_test.go | 10 +- zetaclient/config/config.go | 120 ++++++++++++++++-- zetaclient/config/types.go | 3 +- zetaclient/types/event_test.go | 2 +- 12 files changed, 148 insertions(+), 29 deletions(-) diff --git a/changelog.md b/changelog.md index 1d32b07179..b7e2586b27 100644 --- a/changelog.md +++ b/changelog.md @@ -24,6 +24,7 @@ * [3522](https://github.com/zeta-chain/node/pull/3522) - add `MsgDisableFastConfirmation` to disable fast confirmation. This message can be triggered by the emergency policy. * [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. ### Refactor diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index eab0d134a7..d98a198e72 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/graceful" @@ -58,6 +59,15 @@ func Start(_ *cobra.Command, _ []string) error { appContext := zctx.New(cfg, passes.relayerKeys(), logger.Std) ctx := zctx.WithAppContext(context.Background(), appContext) + err = config.LoadRestrictedAddressesConfig(cfg, globalOpts.ZetacoreHome) + if err != nil { + logger.Std.Err(err).Msg("loading restricted addresses config") + } else { + bg.Work(ctx, func(ctx context.Context) error { + return config.WatchRestrictedAddressesConfig(ctx, cfg, globalOpts.ZetacoreHome, logger.Std) + }, bg.WithName("watch_restricted_addresses_config"), bg.WithLogger(logger.Std)) + } + telemetry, err := startTelemetry(ctx, cfg) if err != nil { return errors.Wrap(err, "unable to start telemetry") @@ -77,7 +87,7 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to update app context") } - log.Info().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) + log.Debug().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) granteePubKeyBech32, err := resolveObserverPubKeyBech32(cfg, passes.hotkey) if err != nil { diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 7d1e41d5ec..b4a71b0e18 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -116,4 +116,7 @@ if [[ -f /root/zetaclient-config-overlay.json ]]; then mv /tmp/merged_config.json /root/.zetacored/config/zetaclient_config.json fi +# ensure restricted addresses config is initialized to avoid log spam +echo "[]" > ~/.zetacored/config/zetaclient_restricted_addresses.json + zetaclientd-supervisor start < /root/password.file \ No newline at end of file diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index b6a0c3cba4..e73b0eef1a 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -47,7 +47,7 @@ func Test_Category(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { @@ -315,7 +315,7 @@ func Test_IsEventProcessable(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { diff --git a/zetaclient/chains/bitcoin/signer/outbound_data_test.go b/zetaclient/chains/bitcoin/signer/outbound_data_test.go index faf94920f1..31d7788e05 100644 --- a/zetaclient/chains/bitcoin/signer/outbound_data_test.go +++ b/zetaclient/chains/bitcoin/signer/outbound_data_test.go @@ -25,7 +25,7 @@ func Test_NewOutboundData(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 2bbd57e673..2eed26bfad 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -296,21 +296,21 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { t.Run("should return nil msg if sender is restricted", func(t *testing.T) { sender := event.ZetaTxSenderAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.DestinationAddress) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if txOrigin is restricted", func(t *testing.T) { txOrigin := event.SourceTxOriginAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{txOrigin} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) @@ -343,14 +343,14 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{sender.Hex()} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.Recipient) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) @@ -400,7 +400,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{tx.From} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForTokenSentToTSS( tx, ethcommon.HexToAddress(tx.From), @@ -414,7 +414,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { message := hex.EncodeToString(ethcommon.HexToAddress(testutils.OtherAddress1).Bytes()) txCopy.Input = message // use other address as receiver cfg.ComplianceConfig.RestrictedAddresses = []string{testutils.OtherAddress1} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.buildInboundVoteMsgForTokenSentToTSS( txCopy, ethcommon.HexToAddress(txCopy.From), diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 00ebee4b90..0bc109964a 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -70,7 +70,7 @@ func Test_IsOutboundProcessed(t *testing.T) { ComplianceConfig: config.ComplianceConfig{}, } cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // post outbound vote continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 8c96410bd4..20827c6e3e 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -153,7 +153,7 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { // restrict sender cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) @@ -173,7 +173,7 @@ func Test_IsEventProcessable(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index fd86cab2ea..9bb12a1a3e 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -23,29 +23,29 @@ func TestCctxRestricted(t *testing.T) { t.Run("should return true if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return true if receiver is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.GetCurrentOutboundParam().Receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return false if sender and receiver are not restricted", func(t *testing.T) { // restrict other address cfg.ComplianceConfig.RestrictedAddresses = []string{"0x27104b8dB4aEdDb054fCed87c346C0758Ff5dFB1"} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.False(t, IsCctxRestricted(cctx)) }) t.Run("should be able to restrict coinbase address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{ethcommon.Address{}.String()} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) cctx.InboundParams.Sender = ethcommon.Address{}.String() require.True(t, IsCctxRestricted(cctx)) }) t.Run("should ignore empty address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{""} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) cctx.InboundParams.Sender = "" require.False(t, IsCctxRestricted(cctx)) }) diff --git a/zetaclient/config/config.go b/zetaclient/config/config.go index 8bd3e9eff9..3714627d33 100644 --- a/zetaclient/config/config.go +++ b/zetaclient/config/config.go @@ -2,17 +2,24 @@ package config import ( + "context" "encoding/json" "fmt" "os" "path/filepath" "strings" + "sync" + "github.com/fsnotify/fsnotify" "github.com/pkg/errors" + "github.com/rs/zerolog" ) // restrictedAddressBook is a map of restricted addresses var restrictedAddressBook = map[string]bool{} +var restrictedAddressBookLock sync.RWMutex + +const restrictedAddressesPath string = "zetaclient_restricted_addresses.json" // filename is config file name for ZetaClient const filename string = "zetaclient_config.json" @@ -45,9 +52,9 @@ func Save(config *Config, path string) error { } // Load loads ZetaClient config from a filepath -func Load(path string) (Config, error) { +func Load(basePath string) (Config, error) { // retrieve file - file := filepath.Join(path, folder, filename) + file := filepath.Join(basePath, folder, filename) file, err := filepath.Abs(file) if err != nil { return Config{}, err @@ -76,19 +83,114 @@ func Load(path string) (Config, error) { // fields sanitization cfg.TssPath = GetPath(cfg.TssPath) cfg.PreParamsPath = GetPath(cfg.PreParamsPath) - cfg.ZetaCoreHome = path - - // load compliance config - LoadComplianceConfig(cfg) + cfg.ZetaCoreHome = basePath return cfg, nil } -// LoadComplianceConfig loads compliance data (restricted addresses) from config -func LoadComplianceConfig(cfg Config) { +// SetRestrictedAddressesFromConfig loads compliance data (restricted addresses) from config. +func SetRestrictedAddressesFromConfig(cfg Config) { restrictedAddressBook = cfg.GetRestrictedAddressBook() } +func getRestrictedAddressAbsPath(basePath string) (string, error) { + file := filepath.Join(basePath, folder, restrictedAddressesPath) + file, err := filepath.Abs(file) + if err != nil { + return "", errors.Wrapf(err, "absolute path conversion for %s", file) + } + return file, nil +} + +func loadRestrictedAddressesConfig(cfg Config, file string) error { + input, err := os.ReadFile(file) // #nosec G304 + if err != nil { + return errors.Wrapf(err, "reading file %s", file) + } + addresses := []string{} + err = json.Unmarshal(input, &addresses) + if err != nil { + return errors.Wrap(err, "invalid json") + } + + restrictedAddressBookLock.Lock() + defer restrictedAddressBookLock.Unlock() + + // Clear the existing map, load addresses from main config, then load addresses + // from dedicated config file + SetRestrictedAddressesFromConfig(cfg) + for _, addr := range cfg.ComplianceConfig.RestrictedAddresses { + restrictedAddressBook[strings.ToLower(addr)] = true + } + return nil +} + +// LoadRestrictedAddressesConfig loads the restricted addresses from the config file +func LoadRestrictedAddressesConfig(cfg Config, basePath string) error { + file, err := getRestrictedAddressAbsPath(basePath) + if err != nil { + return errors.Wrap(err, "getting restricted address path") + } + return loadRestrictedAddressesConfig(cfg, file) +} + +// WatchRestrictedAddressesConfig monitors the restricted addresses config file +// for changes and reloads it when necessary +func WatchRestrictedAddressesConfig(ctx context.Context, cfg Config, basePath string, logger zerolog.Logger) error { + file, err := getRestrictedAddressAbsPath(basePath) + if err != nil { + return errors.Wrap(err, "getting restricted address path") + } + watcher, err := fsnotify.NewWatcher() + if err != nil { + return errors.Wrap(err, "creating file watcher") + } + defer watcher.Close() + + // Watch the config directory + // If you only watch the file, the watch will be disconnected if/when + // the config is recreated. + dir := filepath.Dir(file) + err = watcher.Add(dir) + if err != nil { + return errors.Wrapf(err, "watching directory %s", dir) + } + + for { + select { + case <-ctx.Done(): + return nil + + case event, ok := <-watcher.Events: + if !ok { + return nil + } + + if event.Name != file { + continue + } + + // only reload on create or write + if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { + continue + } + + logger.Info().Msg("restricted addresses config updated") + + err := loadRestrictedAddressesConfig(cfg, file) + if err != nil { + logger.Err(err).Msg("load restricted addresses config") + } + + case err, ok := <-watcher.Errors: + if !ok { + return nil + } + return errors.Wrap(err, "watcher error") + } + } +} + // GetPath returns the absolute path of the input path func GetPath(inputPath string) string { path := strings.Split(inputPath, "/") @@ -109,6 +211,8 @@ func GetPath(inputPath string) string { // ContainRestrictedAddress returns true if any one of the addresses is restricted // Note: the addrs can contains both ETH and BTC addresses func ContainRestrictedAddress(addrs ...string) bool { + restrictedAddressBookLock.RLock() + defer restrictedAddressBookLock.RUnlock() for _, addr := range addrs { if addr != "" && restrictedAddressBook[strings.ToLower(addr)] { return true diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index ed27bc9e33..375c83aa47 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -68,7 +68,8 @@ type TONConfig struct { // ComplianceConfig is the config for compliance type ComplianceConfig struct { - LogPath string `json:"LogPath"` + LogPath string `json:"LogPath"` + // Deprecated: use the separate restricted addresses config RestrictedAddresses []string `json:"RestrictedAddresses" mask:"zero"` } diff --git a/zetaclient/types/event_test.go b/zetaclient/types/event_test.go index 87e95943ec..7eb81407ef 100644 --- a/zetaclient/types/event_test.go +++ b/zetaclient/types/event_test.go @@ -63,7 +63,7 @@ func Test_Catetory(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct {

+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%0DHh*M%*n#|Rk9u5K$F=t={~cp_S-a{@ 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