From dc76d03534cc84b5af0c5795a3fc0291418b9f62 Mon Sep 17 00:00:00 2001 From: lumtis Date: Tue, 8 Apr 2025 16:33:44 +0200 Subject: [PATCH 01/49] add example contract --- e2e/contracts/sui/example/.gitignore | 1 + e2e/contracts/sui/example/Move.lock | 58 ++++++++++++++++++ e2e/contracts/sui/example/Move.toml | 36 +++++++++++ .../sui/example/sources/example.move | 61 +++++++++++++++++++ .../sui/example/sources/token.go.move | 29 +++++++++ 5 files changed, 185 insertions(+) create mode 100644 e2e/contracts/sui/example/.gitignore create mode 100644 e2e/contracts/sui/example/Move.lock create mode 100644 e2e/contracts/sui/example/Move.toml create mode 100644 e2e/contracts/sui/example/sources/example.move create mode 100644 e2e/contracts/sui/example/sources/token.go.move diff --git a/e2e/contracts/sui/example/.gitignore b/e2e/contracts/sui/example/.gitignore new file mode 100644 index 0000000000..a007feab07 --- /dev/null +++ b/e2e/contracts/sui/example/.gitignore @@ -0,0 +1 @@ +build/* diff --git a/e2e/contracts/sui/example/Move.lock b/e2e/contracts/sui/example/Move.lock new file mode 100644 index 0000000000..8593c3c4da --- /dev/null +++ b/e2e/contracts/sui/example/Move.lock @@ -0,0 +1,58 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "795BC4DE1AC42C1015B85547951A89D7F9AD63EA50982960E095E815C560BFCC" +deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" +dependencies = [ + { id = "Bridge", name = "Bridge" }, + { id = "DeepBook", name = "DeepBook" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "Bridge" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/bridge" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "DeepBook" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/deepbook" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "SuiSystem" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-system" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, +] + +[move.toolchain-version] +compiler-version = "1.45.0" +edition = "2024.beta" +flavor = "sui" diff --git a/e2e/contracts/sui/example/Move.toml b/e2e/contracts/sui/example/Move.toml new file mode 100644 index 0000000000..cd022bc446 --- /dev/null +++ b/e2e/contracts/sui/example/Move.toml @@ -0,0 +1,36 @@ +[package] +name = "example" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.xwremote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +example = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/e2e/contracts/sui/example/sources/example.move b/e2e/contracts/sui/example/sources/example.move new file mode 100644 index 0000000000..5397cd9e6f --- /dev/null +++ b/e2e/contracts/sui/example/sources/example.move @@ -0,0 +1,61 @@ +module example::example; + +use sui::address::from_bytes; +use sui::coin::Coin; + +// stub for shared objects +public struct GlobalConfig has key { + id: UID, +} + +public struct Partner has key { + id: UID, +} + +public struct Clock has key { + id: UID, +} + +public struct Pool has key { + id: UID, +} + +// share objects +fun init(ctx: &mut TxContext) { + let global_config = GlobalConfig { + id: object::new(ctx), + }; + let pool = Pool { + id: object::new(ctx), + }; + let partner = Partner { + id: object::new(ctx), + }; + let clock = Clock { + id: object::new(ctx), + }; + + transfer::share_object(global_config); + transfer::share_object(pool); + transfer::share_object(partner); + transfer::share_object(clock); +} + +public entry fun on_call( + in_coins: Coin, + _cetus_config: &GlobalConfig, + _pool: &mut Pool, + _cetus_partner: &mut Partner, + _clock: &Clock, + data: vector, + _ctx: &mut TxContext, +) { + let receiver = decode_receiver(data); + + // transfer the coins to the provided address + transfer::public_transfer(in_coins, receiver) +} + +fun decode_receiver(data: vector): address { + from_bytes(data) +} \ No newline at end of file diff --git a/e2e/contracts/sui/example/sources/token.go.move b/e2e/contracts/sui/example/sources/token.go.move new file mode 100644 index 0000000000..ac872f6916 --- /dev/null +++ b/e2e/contracts/sui/example/sources/token.go.move @@ -0,0 +1,29 @@ +module example::token; + +use sui::coin::{Self, TreasuryCap}; + +public struct TOKEN has drop {} + +fun init(witness: TOKEN, ctx: &mut TxContext) { + let (treasury, metadata) = coin::create_currency( + witness, + 6, + b"TOKEN", + b"", + b"", + option::none(), + ctx, + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury, ctx.sender()) +} + +public entry fun mint( + treasury_cap: &mut TreasuryCap, + amount: u64, + recipient: address, + ctx: &mut TxContext, +) { + let coin = coin::mint(treasury_cap, amount, ctx); + transfer::public_transfer(coin, recipient) +} \ No newline at end of file From 5075844b9b063bf531c6dd95a9a997569fbc6c66 Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 9 Apr 2025 16:23:46 +0200 Subject: [PATCH 02/49] add example --- e2e/contracts/sui/example.mv | Bin 0 -> 613 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 e2e/contracts/sui/example.mv diff --git a/e2e/contracts/sui/example.mv b/e2e/contracts/sui/example.mv new file mode 100644 index 0000000000000000000000000000000000000000..c2a84a891176d937e3ac8f207390abcb0cccf8d6 GIT binary patch literal 613 zcmbtROODhq5UnbIV|%8%huF&GpwbQE09dM5<$8Y4mg~!dUtR-%LzvV2kvMxOOQ*JP%s2ds_Op26zKc`;Ls0lB zRuJGWX6SH%ckmvb0$BjT0UXj2ARuZ2fq^S&B`8H;3<1oTgA+n7!3zm=#31LOLEkz? z4ElD4XtmW&dmYpf8f(FLg&F9n5R^g2P1KegF^P^)@tqbdGHZ>MtpAgNW&gTtOt`mV z6Q!1Ispl*HtXMv)W0mSy^s$P~huE9AE3dBG=r(=2D%PLII8al|rfWvg#g96z-^Xe+sVl0o zZD-dv>$a(iu`jz}6Z`yxGSo|OZX4}XAuw{TaA09J&FUG z$yVKWu#hJZXv&Wv$o(FJ@Kc`xddk7eoPRhck2#Bxhso1gP%s|6$t6vL#z`Rvegl4k BR!RT> literal 0 HcmV?d00001 From f3d230cfe9d9900acebe8712132202d2d37a8268 Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 9 Apr 2025 16:24:08 +0200 Subject: [PATCH 03/49] add ptb building for withdraw and call --- go.mod | 4 + go.sum | 9 + zetaclient/chains/sui/signer/signer_tx.go | 28 +- .../chains/sui/signer/withdraw_and_call.go | 269 ++++++++++++++++++ 4 files changed, 297 insertions(+), 13 deletions(-) create mode 100644 zetaclient/chains/sui/signer/withdraw_and_call.go diff --git a/go.mod b/go.mod index a5d9a59211..f149241c47 100644 --- a/go.mod +++ b/go.mod @@ -307,8 +307,10 @@ require ( github.com/bnb-chain/tss-lib v1.5.0 github.com/cosmos/cosmos-db v1.1.1 github.com/cosmos/ibc-go/modules/capability v1.0.1 + github.com/fardream/go-bcs v0.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/montanaflynn/stats v0.7.1 + github.com/pattonkan/sui-go v0.1.0 github.com/showa-93/go-mask v0.6.2 github.com/tonkeeper/tongo v1.14.8 github.com/zeta-chain/go-tss v0.4.0 @@ -323,6 +325,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/aead/siphash v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/btcsuite/btcutil v1.0.2 // indirect github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect @@ -349,6 +352,7 @@ require ( github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.2.2 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect diff --git a/go.sum b/go.sum index 10578af5ce..81ebe6f6f2 100644 --- a/go.sum +++ b/go.sum @@ -325,6 +325,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -528,6 +530,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/fardream/go-bcs v0.7.0 h1:4YIiCXrtUFiRT86TsvUx+tIennZBRXQCzrgt8xC2g0c= +github.com/fardream/go-bcs v0.7.0/go.mod h1:UsoxhIoe2GsVexX0s5NDLIChxeb/JUbjw7IWzzgF3Xk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -1039,6 +1043,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -1166,6 +1172,8 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pattonkan/sui-go v0.1.0 h1:95re846OafM6erXSqk53UASESQocavRT/g418ic198E= +github.com/pattonkan/sui-go v0.1.0/go.mod h1:E07Cqy27cBNcef90eXnfi/1T5t4Hyn6RxxeK3+NxQ2A= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -1494,6 +1502,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 2ac1e3cac7..7e54d70b8d 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -59,7 +59,7 @@ func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx // build tx depending on the type of transaction if cctx.IsWithdrawAndCall() { - return s.buildWithdrawAndCallTx(ctx, params, coinType, gasBudget, withdrawCapID, cctx.RelayedMessage) + return s.buildWithdrawAndCallTx(params, coinType, gasBudget, withdrawCapID, cctx.RelayedMessage) } return s.buildWithdrawTx(ctx, params, coinType, gasBudget, withdrawCapID) } @@ -94,7 +94,6 @@ func (s *Signer) buildWithdrawTx( // buildWithdrawAndCallTx builds unsigned withdrawAndCall // a withdrawAndCall is a PTB transaction that contains a withdraw_impl call and a on_call call func (s *Signer) buildWithdrawAndCallTx( - ctx context.Context, params *cctypes.OutboundParams, coinType, gasBudget, @@ -120,20 +119,23 @@ func (s *Signer) buildWithdrawAndCallTx( cp.Message, ) - // keep lint quiet without using _ in params - _ = ctx - _ = params - _ = coinType - _ = gasBudget - _ = withdrawCapID - // TODO: check all object IDs are share object here // https://github.com/zeta-chain/node/issues/3755 - // TODO: build PTB here - // https://github.com/zeta-chain/node/issues/3741 - - return models.TxnMetaData{}, errors.New("not implemented") + // build the PTB transaction + return withdrawAndCallPTB( + s.TSS().PubKey().AddressSui(), + s.gateway.PackageID(), + s.gateway.Module(), + s.gateway.ObjectID(), + withdrawCapID, + coinType, + params.Amount.String(), + strconv.FormatUint(params.TssNonce, 10), + gasBudget, + params.Receiver, + cp, + ) } // broadcast attaches signature to tx and broadcasts it to Sui network. Returns tx digest. diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go new file mode 100644 index 0000000000..e455df0d01 --- /dev/null +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -0,0 +1,269 @@ +package signer + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/fardream/go-bcs/bcs" + "github.com/pattonkan/sui-go/sui" + "github.com/pattonkan/sui-go/sui/suiptb" + "github.com/pattonkan/sui-go/suiclient" + + zetasui "github.com/zeta-chain/node/pkg/contracts/sui" +) + +const ( + typeSeparator = "::" + funcWithdrawImpl = "withdraw_impl" + funcOnCall = "on_call" + moduleConnected = "connected" +) + +// withdrawAndCallPTB builds unsigned withdraw and call PTB Sui transaction +// it chains the following calls: +// 1. withdraw_impl on gateway +// 2. gas budget coin transfer to TSS +// 3. on_call on target contract +// The function returns a TxnMetaData object with tx bytes, the other fields are ignored +func withdrawAndCallPTB( + signerAddrStr, + gatewayPackageIDStr, + gatewayModule, + gatewayObjectIDStr, + withdrawCapIDStr, + coinTypeStr, + amountStr, + nonceStr, + gasBudgetStr, + receiver string, + cp zetasui.CallPayload, +) (models.TxnMetaData, error) { + ptb := suiptb.NewTransactionDataTransactionBuilder() + + // Parse arguments + packageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse package ID: %w", err) + } + + coinType, err := parseTypeString(coinTypeStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse coin type: %w", err) + } + + gatewayObjectID, err := sui.ObjectIdFromHex(gatewayObjectIDStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse gateway object ID: %w", err) + } + gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ + SharedObject: &suiptb.SharedObjectArg{ + Id: gatewayObjectID, + InitialSharedVersion: 0, + Mutable: true, + }, + }) + + amountUint64, err := strconv.ParseUint(amountStr, 10, 64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse amount: %w", err) + } + amount, err := ptb.Pure(amountUint64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + } + + nonceUint64, err := strconv.ParseUint(nonceStr, 10, 64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse nonce: %w", err) + } + nonce, err := ptb.Pure(nonceUint64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + } + + gasBudgetUint64, err := strconv.ParseUint(gasBudgetStr, 10, 64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse gas budget: %w", err) + } + gasBudget, err := ptb.Pure(gasBudgetUint64) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + } + + withdrawCapID, err := sui.ObjectIdFromHex(withdrawCapIDStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse withdraw cap ID: %w", err) + } + withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ + ImmOrOwnedObject: &sui.ObjectRef{ + ObjectId: withdrawCapID, + Version: 0, + Digest: nil, + }, + }) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + } + + // Move call for withdraw_impl and get its command index + cmdIndex := uint16(len(ptb.Commands)) + ptb.Command(suiptb.Command{ + MoveCall: &suiptb.ProgrammableMoveCall{ + Package: packageID, + Module: gatewayModule, + Function: funcWithdrawImpl, + TypeArguments: []sui.TypeTag{ + {Struct: coinType}, + }, + Arguments: []suiptb.Argument{ + gatewayObject, + amount, + nonce, + gasBudget, + withdrawCap, + }, + }, + }) + + // Create arguments to access the two results from the withdraw_impl call + withdrawnCoinsArg := suiptb.Argument{ + NestedResult: &suiptb.NestedResult{ + Cmd: cmdIndex, + Result: 0, // First result (main coins) + }, + } + + budgetCoinsArg := suiptb.Argument{ + NestedResult: &suiptb.NestedResult{ + Cmd: cmdIndex, + Result: 1, // Second result (budget coins) + }, + } + + // Transfer the budget coins to the TSS address + tssAddrArg, err := ptb.Pure(signerAddrStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create pure address argument: %w", err) + } + + // Transfer budget coins to the TSS address + ptb.Command(suiptb.Command{ + TransferObjects: &suiptb.ProgrammableTransferObjects{ + Objects: []suiptb.Argument{budgetCoinsArg}, + Address: tssAddrArg, + }, + }) + + // Extract argument for on_call + + // The receiver in the cctx is used as target package ID + targetPackageID, err := sui.PackageIdFromHex(receiver) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse target package ID: %w", err) + } + + // Convert call payload type arguments in addition to the withdrawn coin type + onCallTypeArgs := make([]sui.TypeTag, 0, len(cp.TypeArgs)+1) + onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: coinType}) + for _, typeArg := range cp.TypeArgs { + typeStruct, err := parseTypeString(typeArg) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse type argument: %w", err) + } + onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: typeStruct}) + } + + // Build the args for on_call, contains withdrawns coins + payload objects + message + onCallArgs := make([]suiptb.Argument, 0, len(cp.ObjectIDs)+1) + onCallArgs = append(onCallArgs, withdrawnCoinsArg) + + // Add the payload objects, objects are all shared + for _, objectID := range cp.ObjectIDs { + objectIDParsed, err := sui.ObjectIdFromHex(objectID) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse object ID: %w", err) + } + objectArg, err := ptb.Obj(suiptb.ObjectArg{ + SharedObject: &suiptb.SharedObjectArg{ + Id: objectIDParsed, + InitialSharedVersion: 0, + Mutable: true, + }, + }) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + } + onCallArgs = append(onCallArgs, objectArg) + } + + // Add any additional message arguments + messageArg, err := ptb.Pure(cp.Message) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create pure message argument: %w", err) + } + onCallArgs = append(onCallArgs, messageArg) + + // Call the target contract on_call + ptb.Command(suiptb.Command{ + MoveCall: &suiptb.ProgrammableMoveCall{ + Package: targetPackageID, + Module: moduleConnected, + Function: funcOnCall, + TypeArguments: onCallTypeArgs, + Arguments: onCallArgs, + }, + }) + + // Finish building the PTB + pt := ptb.Finish() + + // Get the signer address + signerAddr, err := sui.AddressFromHex(signerAddrStr) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to parse signer address: %w", err) + } + + // TODO: get coin object for gas payment + + txData := suiptb.NewTransactionData( + signerAddr, + pt, + []*sui.ObjectRef{}, + suiclient.DefaultGasBudget, + suiclient.DefaultGasPrice, + ) + + txBytes, err := bcs.Marshal(txData) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to marshal transaction data: %w", err) + } + + // Encode the transaction bytes to base64 + return models.TxnMetaData{ + TxBytes: base64.StdEncoding.EncodeToString(txBytes), + }, nil +} + +func parseTypeString(t string) (*sui.StructTag, error) { + parts := strings.Split(t, typeSeparator) + if len(parts) != 3 { + return nil, fmt.Errorf("invalid type string: %s", t) + } + + address, err := sui.AddressFromHex(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid address: %s", parts[0]) + } + + module := parts[1] + name := parts[2] + + return &sui.StructTag{ + Address: address, + Module: module, + Name: name, + }, nil +} From df06a9b43df18bef0eef234e7d05056f1fcfb519 Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 9 Apr 2025 16:27:51 +0200 Subject: [PATCH 04/49] lint --- zetaclient/chains/sui/signer/withdraw_and_call.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index e455df0d01..18addf93d5 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -65,6 +65,9 @@ func withdrawAndCallPTB( Mutable: true, }, }) + if err != nil { + return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + } amountUint64, err := strconv.ParseUint(amountStr, 10, 64) if err != nil { From ff0e8594c5b51241764be3b7c39bebf5697bfde7 Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 9 Apr 2025 16:33:32 +0200 Subject: [PATCH 05/49] changelogs --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 03cc13b187..ce24580c7f 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,7 @@ * [3750](https://github.com/zeta-chain/node/pull/3750) - support simple call from solana * [3764](https://github.com/zeta-chain/node/pull/3764) - add payload parsing for Sui WithdrawAndCall * [3756](https://github.com/zeta-chain/node/pull/3756) - parse revert options in solana inbounds +* [3793](https://github.com/zeta-chain/node/pull/3793) - add Sui PTB transaction building for withdrawAndCall ### Refactor From a6732de0f654298f3e2aa84887f62fa2ca706d3b Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 9 Apr 2025 19:47:04 +0200 Subject: [PATCH 06/49] add todos --- .../chains/sui/signer/withdraw_and_call.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 18addf93d5..ddc2bc93c2 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -61,8 +61,8 @@ func withdrawAndCallPTB( gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjectID, - InitialSharedVersion: 0, - Mutable: true, + InitialSharedVersion: 0, // TODO: get coin object for gas payment + Mutable: true, // TODO: get coin object for gas payment }, }) if err != nil { @@ -103,8 +103,8 @@ func withdrawAndCallPTB( withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ ImmOrOwnedObject: &sui.ObjectRef{ ObjectId: withdrawCapID, - Version: 0, - Digest: nil, + Version: 0, // TODO: get coin object for gas payment + Digest: nil, // TODO: get coin object for gas payment }, }) if err != nil { @@ -192,8 +192,8 @@ func withdrawAndCallPTB( objectArg, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: objectIDParsed, - InitialSharedVersion: 0, - Mutable: true, + InitialSharedVersion: 0, // TODO: get the correct value by querying the object + Mutable: true, // TODO: get coin object for gas payment }, }) if err != nil { @@ -229,12 +229,10 @@ func withdrawAndCallPTB( return models.TxnMetaData{}, fmt.Errorf("failed to parse signer address: %w", err) } - // TODO: get coin object for gas payment - txData := suiptb.NewTransactionData( signerAddr, pt, - []*sui.ObjectRef{}, + []*sui.ObjectRef{}, // TODO: get coin object for gas payment - retrieve a coin object owned by the signer suiclient.DefaultGasBudget, suiclient.DefaultGasPrice, ) From c20b6e303466058ceb168f57e8204ea2dbb06f74 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 14 Apr 2025 16:54:46 -0500 Subject: [PATCH 07/49] make e2e sui setup cleaner; deploy example package --- e2e/contracts/sui/bin.go | 16 +++ e2e/contracts/sui/example/Move.toml | 1 + e2e/contracts/sui/token.mv | Bin 0 -> 629 bytes e2e/runner/runner.go | 3 +- e2e/runner/setup_sui.go | 176 ++++++++++++++-------------- 5 files changed, 105 insertions(+), 91 deletions(-) create mode 100644 e2e/contracts/sui/token.mv diff --git a/e2e/contracts/sui/bin.go b/e2e/contracts/sui/bin.go index a83929915d..09a2e85819 100644 --- a/e2e/contracts/sui/bin.go +++ b/e2e/contracts/sui/bin.go @@ -14,6 +14,12 @@ var fakeUSDC []byte //go:embed evm.mv var evmBinary []byte +//go:embed token.mv +var tokenBinary []byte + +//go:embed example.mv +var exampleBinary []byte + // GatewayBytecodeBase64 gets the gateway binary encoded as base64 for deployment func GatewayBytecodeBase64() string { return base64.StdEncoding.EncodeToString(gatewayBinary) @@ -28,3 +34,13 @@ func FakeUSDCBytecodeBase64() string { func EVMBytecodeBase64() string { return base64.StdEncoding.EncodeToString(evmBinary) } + +// TokenBytecodeBase64 gets the token binary encoded as base64 for deployment +func TokenBytecodeBase64() string { + return base64.StdEncoding.EncodeToString(tokenBinary) +} + +// ExampleBytecodeBase64 gets the example binary encoded as base64 for deployment +func ExampleBytecodeBase64() string { + return base64.StdEncoding.EncodeToString(exampleBinary) +} diff --git a/e2e/contracts/sui/example/Move.toml b/e2e/contracts/sui/example/Move.toml index cd022bc446..98d43121ba 100644 --- a/e2e/contracts/sui/example/Move.toml +++ b/e2e/contracts/sui/example/Move.toml @@ -5,6 +5,7 @@ edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] [dependencies] +#Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet-v1.41.1" } # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. # Revision can be a branch, a tag, and a commit hash. diff --git a/e2e/contracts/sui/token.mv b/e2e/contracts/sui/token.mv new file mode 100644 index 0000000000000000000000000000000000000000..de339780214e484ce6f1e4b5a4309f3a235ccad2 GIT binary patch literal 629 zcmbtSy>1gh5T2R+z1wvhCn;0Xf}lHz(xO6;#wnxYa$ZXZ_MWxg6E-hE!xPZ)8axd> zZ$QP|k+f;pl}0n)H!~W|H$Sd_I}HFOf+zn>ynHV2yy`xiU-%R4ckx005wr5ERQ{WM z4GWx+p$odCD-Z-o1_1~>vWt_Iqm_z4X~SKCc}EO+R0w2^MItvpM-XQvq8IZsw({J0 zAStve^h|2KAPdGSkj6>0AQT7W1iMp=R-;2KfB{ngmD6X$>M2n&#o5eHHKU`g;sK^= zDnN;`aRGwUk;fY|iJQq~-$g%~cOf_HCO7(KpSwP))y>;C@7*ee<}jqwve_4_ Date: Tue, 15 Apr 2025 15:38:25 -0500 Subject: [PATCH 08/49] add sui Example package information to E2E runner --- cmd/zetae2e/config/config.go | 6 ++++++ cmd/zetae2e/config/contracts.go | 19 +++++++++++++++++++ e2e/config/config.go | 6 ++++++ e2e/runner/runner.go | 7 +++++-- e2e/runner/setup_sui.go | 32 ++++++++++++++++++++++++++------ e2e/runner/sui.go | 25 +++++++++++++++++++++++++ go.mod | 3 +++ go.sum | 8 ++++---- testutil/sample/crypto.go | 14 ++++++++++++++ 9 files changed, 108 insertions(+), 12 deletions(-) diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 92a655f2c2..3093174530 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -66,6 +66,12 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.Sui.FungibleTokenCoinType = config.DoubleQuotedString(r.SuiTokenCoinType) conf.Contracts.Sui.FungibleTokenTreasuryCap = config.DoubleQuotedString(r.SuiTokenTreasuryCap) + conf.Contracts.Sui.ExamplePackageID = config.DoubleQuotedString(r.SuiExample.PackageID) + conf.Contracts.Sui.ExampleTokenType = config.DoubleQuotedString(r.SuiExample.TokenType) + conf.Contracts.Sui.ExampleGlobalConfigID = config.DoubleQuotedString(r.SuiExample.GlobalConfigID) + conf.Contracts.Sui.ExamplePartnerID = config.DoubleQuotedString(r.SuiExample.PartnerID) + conf.Contracts.Sui.ExampleClockID = config.DoubleQuotedString(r.SuiExample.ClockID) + conf.Contracts.Sui.ExamplePoolID = config.DoubleQuotedString(r.SuiExample.PoolID) conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(r.ZetaEthAddr.Hex()) conf.Contracts.EVM.ConnectorEthAddr = config.DoubleQuotedString(r.ConnectorEthAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 6b94982e15..bf8d8e4366 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -138,6 +138,25 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { r.SuiTokenTreasuryCap = c.String() } + if c := conf.Contracts.Sui.ExamplePackageID; c != "" { + r.SuiExample.PackageID = c.String() + } + if c := conf.Contracts.Sui.ExampleTokenType; c != "" { + r.SuiExample.TokenType = c.String() + } + if c := conf.Contracts.Sui.ExampleGlobalConfigID; c != "" { + r.SuiExample.GlobalConfigID = c.String() + } + if c := conf.Contracts.Sui.ExamplePartnerID; c != "" { + r.SuiExample.PartnerID = c.String() + } + if c := conf.Contracts.Sui.ExampleClockID; c != "" { + r.SuiExample.ClockID = c.String() + } + if c := conf.Contracts.Sui.ExamplePoolID; c != "" { + r.SuiExample.PoolID = c.String() + } + evmChainID, err := r.EVMClient.ChainID(r.Ctx) require.NoError(r, err, "get evm chain ID") evmChainParams := chainParamsByChainID(chainParams, evmChainID.Int64()) diff --git a/e2e/config/config.go b/e2e/config/config.go index afa954dc52..7530e72ae3 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -150,6 +150,12 @@ type Sui struct { GatewayObjectID DoubleQuotedString `yaml:"gateway_object_id"` FungibleTokenCoinType DoubleQuotedString `yaml:"fungible_token_coin_type"` FungibleTokenTreasuryCap DoubleQuotedString `yaml:"fungible_token_treasury_cap"` + ExamplePackageID DoubleQuotedString `yaml:"example_package_id"` + ExampleTokenType DoubleQuotedString `yaml:"example_token_type"` + ExampleGlobalConfigID DoubleQuotedString `yaml:"example_global_config_id"` + ExamplePartnerID DoubleQuotedString `yaml:"example_partner_id"` + ExampleClockID DoubleQuotedString `yaml:"example_clock_id"` + ExamplePoolID DoubleQuotedString `yaml:"example_pool_id"` } // EVM contains the addresses of predeployed contracts on the EVM chain diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index dcd1b3ecfd..4a03dcb72d 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -125,8 +125,7 @@ type E2ERunner struct { TONGateway ton.AccountID // contract Sui - SuiGateway *sui.Gateway - SuiExamplePackageID string + SuiGateway *sui.Gateway // SuiTokenCoinType is the coin type identifying the fungible token for SUI SuiTokenCoinType string @@ -134,6 +133,9 @@ type E2ERunner struct { // SuiTokenTreasuryCap is the treasury cap for the SUI token that allows minting, only using in local tests SuiTokenTreasuryCap string + // SuiExample is the example contract for Sui + SuiExample Example + // contracts evm ZetaEthAddr ethcommon.Address ZetaEth *zetaeth.ZetaEth @@ -289,6 +291,7 @@ func (r *E2ERunner) CopyAddressesFrom(other *E2ERunner) (err error) { r.SuiGateway = other.SuiGateway r.SuiTokenCoinType = other.SuiTokenCoinType r.SuiTokenTreasuryCap = other.SuiTokenTreasuryCap + r.SuiExample = other.SuiExample // create instances of contracts r.ZetaEth, err = zetaeth.NewZetaEth(r.ZetaEthAddr, r.EVMClient) diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index f8d5b7bc7b..4403c16e52 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -84,13 +84,13 @@ func (r *E2ERunner) deploySUIGateway() (whitelistCapID, withdrawCapID string) { ) gatewayID, ok := objectIDs[filterGatewayType] - require.True(r, ok, "gateway not found") + require.True(r, ok, "gateway object not found") whitelistCapID, ok = objectIDs[filterWhitelistCapType] - require.True(r, ok, "whitelistCap not found") + require.True(r, ok, "whitelistCap object not found") withdrawCapID, ok = objectIDs[filterWithdrawCapType] - require.True(r, ok, "withdrawCap not found") + require.True(r, ok, "withdrawCap object not found") // set sui gateway r.SuiGateway = zetasui.NewGateway(packageID, gatewayID) @@ -142,13 +142,33 @@ func (r *E2ERunner) deploySuiFakeUSDC() string { // deploySuiExample deploys the example package on Sui func (r *E2ERunner) deploySuiExample() { - packageID, _ := r.deploySuiPackage( + const ( + filterGlobalConfigType = "example::GlobalConfig" + filterPartnerType = "example::Partner" + filterClockType = "example::Clock" + filterPoolType = "example::Pool" + ) + + objectTypeFilters := []string{filterGlobalConfigType, filterPartnerType, filterClockType, filterPoolType} + packageID, objectIDs := r.deploySuiPackage( []string{suicontract.TokenBytecodeBase64(), suicontract.ExampleBytecodeBase64()}, - nil, + objectTypeFilters, ) r.Logger.Info("deployed example package with packageID: %s", packageID) - r.SuiExamplePackageID = packageID + globalConfigID, ok := objectIDs[filterGlobalConfigType] + require.True(r, ok, "globalConfig object not found") + + partnerID, ok := objectIDs[filterPartnerType] + require.True(r, ok, "partner object not found") + + clockID, ok := objectIDs[filterClockType] + require.True(r, ok, "clock object not found") + + poolID, ok := objectIDs[filterPoolType] + require.True(r, ok, "pool object not found") + + r.SuiExample = NewExample(packageID, globalConfigID, partnerID, clockID, poolID) } // deploySuiPackage is a helper function that deploys a package on Sui diff --git a/e2e/runner/sui.go b/e2e/runner/sui.go index eb34a09a4f..b8185bad0a 100644 --- a/e2e/runner/sui.go +++ b/e2e/runner/sui.go @@ -17,6 +17,31 @@ import ( "github.com/zeta-chain/node/pkg/contracts/sui" ) +// Example is the struct containing the object IDs of the Example package +type Example struct { + PackageID string + TokenType string + GlobalConfigID string + PartnerID string + ClockID string + PoolID string +} + +// NewExample creates a new Example struct +func NewExample(packageID, globalConfigID, partnerID, clockID, poolID string) Example { + // token type is the packageID + ::token::TOKEN + tokenType := packageID + "::token::TOKEN" + + return Example{ + PackageID: packageID, + TokenType: tokenType, + GlobalConfigID: globalConfigID, + PartnerID: partnerID, + ClockID: clockID, + PoolID: poolID, + } +} + // SuiGetSUIBalance returns the SUI balance of an address func (r *E2ERunner) SuiGetSUIBalance(addr string) uint64 { resp, err := r.Clients.Sui.SuiXGetBalance(r.Ctx, models.SuiXGetBalanceRequest{ diff --git a/go.mod b/go.mod index 4f734f3c43..05313b10b3 100644 --- a/go.mod +++ b/go.mod @@ -317,6 +317,8 @@ require ( github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250320221859-9eabc2d8eba8 ) +require github.com/coming-chat/go-aptos v0.0.0-20221013022715-39f91035c785 // indirect + require ( cloud.google.com/go/auth v0.6.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect @@ -327,6 +329,7 @@ require ( github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcutil v1.0.2 // indirect github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect + github.com/coming-chat/go-sui/v2 v2.0.1 github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect diff --git a/go.sum b/go.sum index 7169666165..7230eebea2 100644 --- a/go.sum +++ b/go.sum @@ -401,6 +401,10 @@ github.com/cometbft/cometbft v0.38.17 h1:FkrQNbAjiFqXydeAO81FUzriL4Bz0abYxN/eOHr github.com/cometbft/cometbft v0.38.17/go.mod h1:5l0SkgeLRXi6bBfQuevXjKqML1jjfJJlvI1Ulp02/o4= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/coming-chat/go-aptos v0.0.0-20221013022715-39f91035c785 h1:xIOXIW3uXakffHoVqA6qkyUgYYuhJWLPohIyR1tBS38= +github.com/coming-chat/go-aptos v0.0.0-20221013022715-39f91035c785/go.mod h1:HaGBPmQOlKzxkbGancRSX8wcwDxvj9Zs173CSla43vE= +github.com/coming-chat/go-sui/v2 v2.0.1 h1:Mi7IGUvKd8OLP5zA3YhfDN/L5AJTXHsSsJnLb9WX9+4= +github.com/coming-chat/go-sui/v2 v2.0.1/go.mod h1:0/cgsi6HcHEfPFC05mY/ovzWuxxpmKxiY0NIEFgMP4g= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= @@ -1408,10 +1412,6 @@ github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc h1:FVOt 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= github.com/zeta-chain/go-libp2p v0.0.0-20240710192637-567fbaacc2b4/go.mod h1:TBv5NY/CqWYIfUstXO1fDWrt4bDoqgCw79yihqBspg8= -github.com/zeta-chain/go-tss v0.4.0 h1:SeV1/bOsQa6JBI7dk4STU1T8j6CiBdHCvrAxci84l6Q= -github.com/zeta-chain/go-tss v0.4.0/go.mod h1:9i7wLXr09Guc4JaO4cLhNkGzZkqmvjQxWx2aMTdzkXM= -github.com/zeta-chain/go-tss v0.4.1-0.20250326144024-94b1d39b9c2d h1:cjCi8b4FLFL892uMt3EpRxZEEApy0ZiA8VnVgNrrhAk= -github.com/zeta-chain/go-tss v0.4.1-0.20250326144024-94b1d39b9c2d/go.mod h1:xLssidNiAP/fcdcw+cUPA2VS7Td2bnPMS/8x0jnde8w= github.com/zeta-chain/go-tss v0.5.0 h1:vXFEXPC5fSDhlcsU515/vUYGKQWAFj4PfwjrZnbtAEg= github.com/zeta-chain/go-tss v0.5.0/go.mod h1:xLssidNiAP/fcdcw+cUPA2VS7Td2bnPMS/8x0jnde8w= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250318094825-429d8390b7fc h1:oXw/b55v4KbX5KV7CELt5IRNDwu6w6g/P6jY2ARYzUQ= diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index b631520c4e..b72037b914 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -13,6 +13,8 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/coming-chat/go-sui/v2/account" + "github.com/coming-chat/go-sui/v2/sui_types" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -161,6 +163,18 @@ func SolanaSignature(t *testing.T) solana.Signature { return signature } +// SuiAddress returns a sample sui address +func SuiAddress(t require.TestingT) string { + privateKey := ed25519.GenPrivKey() + + // create a new account with ed25519 scheme + scheme, err := sui_types.NewSignatureScheme(0) + require.NoError(t, err) + acc := account.NewAccount(scheme, privateKey.GetKey().Seed()) + + return acc.Address +} + // Hash returns a sample hash func Hash() ethcommon.Hash { return ethcommon.BytesToHash(EthAddress().Bytes()) From eeee8cfea5a8f11b8bbf76d2e5f8fcaa3ead232e Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 16 Apr 2025 15:52:07 -0500 Subject: [PATCH 09/49] sui withdraw and call e2e using example contract; refactor sui signer withdraw and call logic to use correct object information --- e2e/e2etests/test_sui_withdraw_and_call.go | 27 ++-- pkg/contracts/sui/coin.go | 17 +++ pkg/contracts/sui/gateway.go | 6 - zetaclient/chains/sui/signer/signer.go | 3 + zetaclient/chains/sui/signer/signer_tx.go | 32 +++- .../chains/sui/signer/withdraw_and_call.go | 138 ++++++++++++++---- 6 files changed, 172 insertions(+), 51 deletions(-) create mode 100644 pkg/contracts/sui/coin.go diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index f51a9e7839..f0ebf57735 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -8,40 +8,49 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/contracts/sui" + "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) + // ARRANGE + // Given a signer and an amount signer, err := r.Account.SuiSigner() require.NoError(r, err, "get deployer signer") - amount := utils.ParseBigInt(r, args[0]) - r.ApproveSUIZRC20(r.GatewayZEVMAddr) - + // Given example contract in Sui network // sample withdrawAndCall payload // TODO: use real contract // https://github.com/zeta-chain/node/issues/3742 argumentTypes := []string{ - "0xb112f370bc8e3ba6e45ad1a954660099fc3e6de2a203df9d26e11aa0d870f635::token::TOKEN", + r.SuiExample.TokenType, } objects := []string{ - "0x57dd7b5841300199ac87b420ddeb48229523e76af423b4fce37da0cb78604408", - "0xbab1a2d90ea585eab574932e1b3467ff1d5d3f2aee55fed304f963ca2b9209eb", - "0xee6f1f44d24a8bf7268d82425d6e7bd8b9c48d11b2119b20756ee150c8e24ac3", - "0x039ce62b538a0d0fca21c3c3a5b99adf519d55e534c536568fbcca40ee61fb7e", + r.SuiExample.GlobalConfigID, + r.SuiExample.PoolID, + r.SuiExample.PartnerID, + r.SuiExample.ClockID, } - message, err := hex.DecodeString("3573924024f4a7ff8e6755cb2d9fdeef69bdb65329f081d21b0b6ab37a265d06") + + // assemble payload with random Sui receiver address + suiAddress := sample.SuiAddress(r) + message, err := hex.DecodeString(suiAddress[2:]) // remove 0x prefix require.NoError(r, err) payload := sui.NewCallPayload(argumentTypes, objects, message) + // ACT + // approve SUI ZRC20 token + r.ApproveSUIZRC20(r.GatewayZEVMAddr) + // perform the withdraw and call tx := r.SuiWithdrawAndCallSUI(signer.Address(), amount, payload) r.Logger.EVMTransaction(*tx, "withdraw_and_call") + // ASSERT // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "withdraw") diff --git a/pkg/contracts/sui/coin.go b/pkg/contracts/sui/coin.go new file mode 100644 index 0000000000..c5fa7de026 --- /dev/null +++ b/pkg/contracts/sui/coin.go @@ -0,0 +1,17 @@ +package sui + +// CoinType represents the coin type for the inbound +type CoinType string + +const ( + // SUI is the coin type for SUI, native gas token + SUI CoinType = "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" + + // SUIShort is the short coin type for SUI + SUIShort CoinType = "0x2::sui::SUI" +) + +// IsSUIType returns true if the given coin type is SUI +func IsSUIType(coinType CoinType) bool { + return coinType == SUI || coinType == SUIShort +} diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index f4fb428a58..e17bda19d4 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -12,9 +12,6 @@ import ( "github.com/pkg/errors" ) -// CoinType represents the coin type for the inbound -type CoinType string - // EventType represents Gateway event type (both inbound & outbound) type EventType string @@ -38,9 +35,6 @@ type OutboundEventContent interface { TxNonce() uint64 } -// SUI is the coin type for SUI, native gas token -const SUI CoinType = "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" - // Event types const ( DepositEvent EventType = "DepositEvent" diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 197b1c8e9f..63bb479112 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -30,6 +30,9 @@ type Signer struct { type RPC interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) + SuiGetObject(ctx context.Context, req models.SuiGetObjectRequest) (models.SuiObjectResponse, error) + SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) + SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) SuiExecuteTransactionBlock( diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 80763763bc..389b30ed67 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -88,17 +88,17 @@ func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx gasBudget := strconv.FormatUint(gasPrice*params.CallOptions.GasLimit, 10) // Retrieve withdraw cap ID - withdrawCapID, err := s.getWithdrawCapIDCached(ctx) + withdrawCapIDStr, err := s.getWithdrawCapIDCached(ctx) if err != nil { return tx, errors.Wrap(err, "unable to get withdraw cap ID") } // build tx depending on the type of transaction if cctx.IsWithdrawAndCall() { - return s.buildWithdrawAndCallTx(params, coinType, gasBudget, withdrawCapID, cctx.RelayedMessage) + return s.buildWithdrawAndCallTx(ctx, params, coinType, gasBudget, withdrawCapIDStr, cctx.RelayedMessage) } - return s.buildWithdrawTx(ctx, params, coinType, gasBudget, withdrawCapID) + return s.buildWithdrawTx(ctx, params, coinType, gasBudget, withdrawCapIDStr) } // buildWithdrawTx builds unsigned withdraw transaction @@ -129,10 +129,11 @@ func (s *Signer) buildWithdrawTx( // buildWithdrawAndCallTx builds unsigned withdrawAndCall // a withdrawAndCall is a PTB transaction that contains a withdraw_impl call and a on_call call func (s *Signer) buildWithdrawAndCallTx( + ctx context.Context, params *cctypes.OutboundParams, coinType, gasBudget, - withdrawCapID, + withdrawCapIDStr, payload string, ) (models.TxnMetaData, error) { // decode and parse the payload to object the on_call arguments @@ -146,6 +147,23 @@ func (s *Signer) buildWithdrawAndCallTx( return models.TxnMetaData{}, errors.Wrap(err, "unable to parse withdrawAndCall payload") } + // get latest TSS SUI coin object ref for gas payment + suiCoinObjRef, err := s.getTSSSuiCoinObjectRef(ctx) + if err != nil { + return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") + } + + // get all other object references: [gateway, withdrawCap, ...] + objectIDStrs := append([]string{s.gateway.ObjectID(), withdrawCapIDStr}, cp.ObjectIDs...) + objectRefs, err := s.getSuiObjectRefs(ctx, objectIDStrs...) + if err != nil { + return models.TxnMetaData{}, errors.Wrap(err, "unable to get objects") + } + + gatewayObjRef := objectRefs[0] + withdrawCapObjRef := objectRefs[1] + onCallObjectRefs := objectRefs[2:] + // Note: logs not formatted in standard, it's a temporary log s.Logger().Std.Info().Msgf( "WithdrawAndCall called with type arguments %v, object IDs %v, message %v", @@ -162,8 +180,10 @@ func (s *Signer) buildWithdrawAndCallTx( s.TSS().PubKey().AddressSui(), s.gateway.PackageID(), s.gateway.Module(), - s.gateway.ObjectID(), - withdrawCapID, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + onCallObjectRefs, coinType, params.Amount.String(), strconv.FormatUint(params.TssNonce, 10), diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index ddc2bc93c2..fb61311820 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -1,6 +1,7 @@ package signer import ( + "context" "encoding/base64" "fmt" "strconv" @@ -11,6 +12,7 @@ import ( "github.com/pattonkan/sui-go/sui" "github.com/pattonkan/sui-go/sui/suiptb" "github.com/pattonkan/sui-go/suiclient" + "github.com/pkg/errors" zetasui "github.com/zeta-chain/node/pkg/contracts/sui" ) @@ -31,9 +33,11 @@ const ( func withdrawAndCallPTB( signerAddrStr, gatewayPackageIDStr, - gatewayModule, - gatewayObjectIDStr, - withdrawCapIDStr, + gatewayModule string, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef sui.ObjectRef, + onCallObjectRefs []sui.ObjectRef, coinTypeStr, amountStr, nonceStr, @@ -54,15 +58,11 @@ func withdrawAndCallPTB( return models.TxnMetaData{}, fmt.Errorf("failed to parse coin type: %w", err) } - gatewayObjectID, err := sui.ObjectIdFromHex(gatewayObjectIDStr) - if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse gateway object ID: %w", err) - } gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ - Id: gatewayObjectID, - InitialSharedVersion: 0, // TODO: get coin object for gas payment - Mutable: true, // TODO: get coin object for gas payment + Id: gatewayObjRef.ObjectId, + InitialSharedVersion: gatewayObjRef.Version, // TODO: get coin object for gas payment + Mutable: true, }, }) if err != nil { @@ -96,17 +96,7 @@ func withdrawAndCallPTB( return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) } - withdrawCapID, err := sui.ObjectIdFromHex(withdrawCapIDStr) - if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse withdraw cap ID: %w", err) - } - withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ - ImmOrOwnedObject: &sui.ObjectRef{ - ObjectId: withdrawCapID, - Version: 0, // TODO: get coin object for gas payment - Digest: nil, // TODO: get coin object for gas payment - }, - }) + withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) if err != nil { return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) } @@ -184,16 +174,12 @@ func withdrawAndCallPTB( onCallArgs = append(onCallArgs, withdrawnCoinsArg) // Add the payload objects, objects are all shared - for _, objectID := range cp.ObjectIDs { - objectIDParsed, err := sui.ObjectIdFromHex(objectID) - if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse object ID: %w", err) - } + for _, onCallObjectRef := range onCallObjectRefs { objectArg, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ - Id: objectIDParsed, - InitialSharedVersion: 0, // TODO: get the correct value by querying the object - Mutable: true, // TODO: get coin object for gas payment + Id: onCallObjectRef.ObjectId, + InitialSharedVersion: onCallObjectRef.Version, // TODO: get the correct value by querying the object + Mutable: true, // TODO: get coin object for gas payment }, }) if err != nil { @@ -232,7 +218,9 @@ func withdrawAndCallPTB( txData := suiptb.NewTransactionData( signerAddr, pt, - []*sui.ObjectRef{}, // TODO: get coin object for gas payment - retrieve a coin object owned by the signer + []*sui.ObjectRef{ + &suiCoinObjRef, + }, // TODO: get coin object for gas payment - retrieve a coin object owned by the signer suiclient.DefaultGasBudget, suiclient.DefaultGasPrice, ) @@ -242,6 +230,8 @@ func withdrawAndCallPTB( return models.TxnMetaData{}, fmt.Errorf("failed to marshal transaction data: %w", err) } + fmt.Println("withdrawAndCallPTB success") + // Encode the transaction bytes to base64 return models.TxnMetaData{ TxBytes: base64.StdEncoding.EncodeToString(txBytes), @@ -268,3 +258,91 @@ func parseTypeString(t string) (*sui.StructTag, error) { Name: name, }, nil } + +// getSuiObjectRefs returns the latest SUI object references for the given object IDs +// Note: the SUI object may change over time, so we need to get the latest object +func (s *Signer) getSuiObjectRefs(ctx context.Context, objectIDStrs ...string) ([]sui.ObjectRef, error) { + if len(objectIDStrs) == 0 { + return nil, errors.New("object ID is required") + } + + // query objects in batch + suiObjects, err := s.client.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ + ObjectIds: objectIDStrs, + Options: models.SuiObjectDataOptions{}, + }) + if err != nil { + return nil, errors.Wrapf(err, "unable to get SUI objects for %v", objectIDStrs) + } + + // convert object data to object references + objectRefs := make([]sui.ObjectRef, 0, len(objectIDStrs)) + + for _, object := range suiObjects { + objectID, err := sui.ObjectIdFromHex(object.Data.ObjectId) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse SUI object ID for %s", object.Data.ObjectId) + } + objectVersion, err := strconv.ParseUint(object.Data.Version, 10, 64) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse SUI object version for %s", object.Data.ObjectId) + } + objectDigest, err := sui.NewBase58(object.Data.Digest) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse SUI object digest for %s", object.Data.ObjectId) + } + + fmt.Printf("object: %s, version: %d, digest: %s\n", objectID.String(), objectVersion, objectDigest.String()) + + objectRefs = append(objectRefs, sui.ObjectRef{ + ObjectId: objectID, + Version: objectVersion, + Digest: objectDigest, + }) + } + + return objectRefs, nil +} + +// getTSSSuiCoinObjectRef returns the latest SUI coin object reference for the TSS address +// Note: the SUI object may change over time, so we need to get the latest object +func (s *Signer) getTSSSuiCoinObjectRef(ctx context.Context) (sui.ObjectRef, error) { + coins, err := s.client.SuiXGetAllCoins(ctx, models.SuiXGetAllCoinsRequest{ + Owner: s.TSS().PubKey().AddressSui(), + }) + if err != nil { + return sui.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") + } + + // locate the SUI coin object under TSS account + var suiCoin *models.CoinData + for _, coin := range coins.Data { + if zetasui.IsSUIType(zetasui.CoinType(coin.CoinType)) { + suiCoin = &coin + break + } + } + if suiCoin == nil { + return sui.ObjectRef{}, errors.New("SUI coin not found") + } + + // convert coin data to object ref + suiCoinID, err := sui.ObjectIdFromHex(suiCoin.CoinObjectId) + if err != nil { + return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin ID: %w", err) + } + suiCoinVersion, err := strconv.ParseUint(suiCoin.Version, 10, 64) + if err != nil { + return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin version: %w", err) + } + suiCoinDigest, err := sui.NewBase58(suiCoin.Digest) + if err != nil { + return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin digest: %w", err) + } + + return sui.ObjectRef{ + ObjectId: suiCoinID, + Version: suiCoinVersion, + Digest: suiCoinDigest, + }, nil +} From 10827dc633c7f0ba19b52760e0918e274a2e8423 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 16 Apr 2025 21:54:57 -0500 Subject: [PATCH 10/49] use example package id as receiver address in e2e; fix missing logger in the sui signer context --- cmd/zetae2e/local/local.go | 16 +++--- e2e/e2etests/test_sui_withdraw_and_call.go | 12 ++--- e2e/runner/setup_sui.go | 9 ++++ zetaclient/chains/sui/observer/outbound.go | 4 +- zetaclient/chains/sui/signer/signer.go | 8 +++ .../chains/sui/signer/signer_tracker.go | 5 +- zetaclient/chains/sui/signer/signer_tx.go | 2 +- .../chains/sui/signer/withdraw_and_call.go | 6 +-- .../sui/signer/withdraw_and_call_test.go | 54 +++++++++++++++++++ 9 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 zetaclient/chains/sui/signer/withdraw_and_call_test.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 6857cd84a7..42d33f878c 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -461,20 +461,20 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testSui { suiTests := []string{ e2etests.TestSuiDepositName, - e2etests.TestSuiDepositAndCallRevertName, - e2etests.TestSuiDepositAndCallName, + // e2etests.TestSuiDepositAndCallRevertName, + // e2etests.TestSuiDepositAndCallName, e2etests.TestSuiTokenDepositName, - e2etests.TestSuiTokenDepositAndCallName, - e2etests.TestSuiTokenDepositAndCallRevertName, + // e2etests.TestSuiTokenDepositAndCallName, + // e2etests.TestSuiTokenDepositAndCallRevertName, e2etests.TestSuiWithdrawName, - e2etests.TestSuiWithdrawRevertWithCallName, + // e2etests.TestSuiWithdrawRevertWithCallName, e2etests.TestSuiTokenWithdrawName, - e2etests.TestSuiDepositRestrictedName, - e2etests.TestSuiWithdrawRestrictedName, + // e2etests.TestSuiDepositRestrictedName, + // e2etests.TestSuiWithdrawRestrictedName, // TODO: enable withdraw and call test // https://github.com/zeta-chain/node/issues/3742 - //e2etests.TestSuiWithdrawAndCallName, + e2etests.TestSuiWithdrawAndCallName, } eg.Go(suiTestRoutine(conf, deployerRunner, verbose, suiTests...)) } diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index f0ebf57735..2400b32973 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -16,12 +16,11 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // ARRANGE - // Given a signer and an amount - signer, err := r.Account.SuiSigner() - require.NoError(r, err, "get deployer signer") + // Given target package ID (example package) and an SUI amount + targetPackageID := r.SuiExample.PackageID amount := utils.ParseBigInt(r, args[0]) - // Given example contract in Sui network + // Given example contract on_call function arguments // sample withdrawAndCall payload // TODO: use real contract // https://github.com/zeta-chain/node/issues/3742 @@ -35,7 +34,8 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { r.SuiExample.ClockID, } - // assemble payload with random Sui receiver address + // create a random Sui address and use it for on_call payload message + // the example contract will just forward the withdrawn SUI token to this address suiAddress := sample.SuiAddress(r) message, err := hex.DecodeString(suiAddress[2:]) // remove 0x prefix require.NoError(r, err) @@ -47,7 +47,7 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { r.ApproveSUIZRC20(r.GatewayZEVMAddr) // perform the withdraw and call - tx := r.SuiWithdrawAndCallSUI(signer.Address(), amount, payload) + tx := r.SuiWithdrawAndCallSUI(targetPackageID, amount, payload) r.Logger.EVMTransaction(*tx, "withdraw_and_call") // ASSERT diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index 4403c16e52..bc5f8b16a1 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -92,6 +92,13 @@ func (r *E2ERunner) deploySUIGateway() (whitelistCapID, withdrawCapID string) { withdrawCapID, ok = objectIDs[filterWithdrawCapType] require.True(r, ok, "withdrawCap object not found") + gatewayObj, err := r.Clients.Sui.SuiGetObject(r.Ctx, models.SuiGetObjectRequest{ + ObjectId: gatewayID, + Options: models.SuiObjectDataOptions{}, + }) + require.NoError(r, err) + r.Logger.Print("gateway object Data: %v", gatewayObj.Data) + // set sui gateway r.SuiGateway = zetasui.NewGateway(packageID, gatewayID) @@ -207,6 +214,8 @@ func (r *E2ERunner) deploySuiPackage(bytecodeBase64s []string, objectTypeFilters }) require.NoError(r, err) + fmt.Printf("deploySuiPackage resp: \n%v\n", resp) + // find packageID var packageID string for _, change := range resp.ObjectChanges { diff --git a/zetaclient/chains/sui/observer/outbound.go b/zetaclient/chains/sui/observer/outbound.go index f4044ae76e..9a985ca760 100644 --- a/zetaclient/chains/sui/observer/outbound.go +++ b/zetaclient/chains/sui/observer/outbound.go @@ -151,9 +151,6 @@ func (ob *Observer) VoteOutbound(ctx context.Context, cctx *cctypes.CrossChainTx 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") } @@ -238,6 +235,7 @@ func (ob *Observer) postVoteOutbound(ctx context.Context, msg *cctypes.MsgVoteOu return errors.Wrap(err, "unable to post vote outbound") case zetaTxHash != "": ob.Logger().Outbound.Info(). + Str(logs.FieldTx, msg.ObservedOutboundHash). Str(logs.FieldZetaTx, zetaTxHash). Str(logs.FieldBallot, ballot). Msg("PostVoteOutbound: posted outbound vote successfully") diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 63bb479112..e442956ebd 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -14,6 +14,7 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" + "github.com/zeta-chain/node/zetaclient/logs" ) // Signer Sui outbound transaction signer. @@ -87,6 +88,13 @@ func (s *Signer) ProcessCCTX(ctx context.Context, cctx *cctypes.CrossChainTx, ze return errors.Wrap(err, "unable to create cancel tx builder") } + // prepare logger + logger := s.Logger().Std.With(). + Int64(logs.FieldChain, s.Chain().ChainId). + Uint64(logs.FieldNonce, nonce). + Logger() + ctx = logger.WithContext(ctx) + var txDigest string // broadcast tx according to compliance check result diff --git a/zetaclient/chains/sui/signer/signer_tracker.go b/zetaclient/chains/sui/signer/signer_tracker.go index ae606f3974..5e0efdd8a4 100644 --- a/zetaclient/chains/sui/signer/signer_tracker.go +++ b/zetaclient/chains/sui/signer/signer_tracker.go @@ -6,6 +6,7 @@ import ( "github.com/block-vision/sui-go-sdk/models" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/zeta-chain/node/zetaclient/chains/sui/client" "github.com/zeta-chain/node/zetaclient/logs" @@ -21,10 +22,8 @@ func (s *Signer) reportOutboundTracker(ctx context.Context, nonce uint64, digest const maxTimeout = time.Minute // prepare logger - logger := s.Logger().Std.With(). + logger := zerolog.Ctx(ctx).With(). Str(logs.FieldMethod, "reportOutboundTracker"). - Int64(logs.FieldChain, s.Chain().ChainId). - Uint64(logs.FieldNonce, nonce). Str(logs.FieldTx, digest). Logger() diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 389b30ed67..68415e2b08 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -306,7 +306,7 @@ func (s *Signer) broadcastWithdrawalWithFallback( // broadcastCancelTx broadcasts a cancel tx and returns the tx digest func (s *Signer) broadcastCancelTx(ctx context.Context, cancelTxBuilder txBuilder) (string, error) { - logger := zerolog.Ctx(ctx) + logger := zerolog.Ctx(ctx).With().Str(logs.FieldMethod, "broadcastCancelTx").Logger() // build cancel tx txCancel, sigCancel, err := cancelTxBuilder(ctx) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index fb61311820..f73dba9974 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -48,7 +48,7 @@ func withdrawAndCallPTB( ptb := suiptb.NewTransactionDataTransactionBuilder() // Parse arguments - packageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) + gatewayPackageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) if err != nil { return models.TxnMetaData{}, fmt.Errorf("failed to parse package ID: %w", err) } @@ -61,7 +61,7 @@ func withdrawAndCallPTB( gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjRef.ObjectId, - InitialSharedVersion: gatewayObjRef.Version, // TODO: get coin object for gas payment + InitialSharedVersion: gatewayObjRef.Version, // TODO: use initial version Mutable: true, }, }) @@ -105,7 +105,7 @@ func withdrawAndCallPTB( cmdIndex := uint16(len(ptb.Commands)) ptb.Command(suiptb.Command{ MoveCall: &suiptb.ProgrammableMoveCall{ - Package: packageID, + Package: gatewayPackageID, Module: gatewayModule, Function: funcWithdrawImpl, TypeArguments: []sui.TypeTag{ diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go new file mode 100644 index 0000000000..fec5f6e598 --- /dev/null +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -0,0 +1,54 @@ +package signer + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/contracts/sui" +) + +func Test_withdrawAndCallPTB(t *testing.T) { + // ARRANGE + signerAddrStr := "0x19c693d9b288073f5fd9572dd696604844cabab4c1637436338e023a5d67bc7a" + gatewayPackageIDStr := "0x68efbe57639ca63db3bb6d37410f2963a81f9ba0cb4cfda205cff0b5ebd134ad" + gatewayModule := "gateway" + gatewayObjectIDStr := "0x71e93003b3b18a19bee11047778c4dcde65111cf27c0d76b42833704cf848d4b" + withdrawCapIDStr := "0x0e5e4e15b982a46c40e33c22d0a528af4b61fa404a849938b2afa02f552dce20" + coinTypeStr := string(sui.SUI) + amountStr := "1000000" + nonceStr := "1" + gasBudgetStr := "20000000" + receiver := "0xdfe280f111900991daa5c25d73a5ae134022b81616d3325945ac157b5394b598" + message, err := hex.DecodeString("0ee37c85c59aecc644a3114a4182eacdf3e49435e588978b702191c531c397d6") + require.NoError(t, err) + encodedMessage := hex.EncodeToString(message) + require.Equal(t, "0ee37c85c59aecc644a3114a4182eacdf3e49435e588978b702191c531c397d6", encodedMessage) + cp := sui.CallPayload{ + TypeArgs: []string{"0x10bb172f3bd83b6ecd9861aeb843d4dc0549d3ca2085492d6810c5d768dfe880::token::TOKEN"}, + ObjectIDs: []string{ + "0xcf63b648079a11f00e6b738ad0b2226e49444d04fa6f6d585dbb636f551f323f", + "0x382b6d92ed22bf13d61a6786ef9af505608e24c3572cadaad8eefb66e1366fc2", + "0x2d4eb032bda2aaac27bac5a72c0b821fda8c3c50be90869f024959f2b350b053", + "0x398a4b74e3ee229bb3deaf9facd2e435bcb8e216292ffe6e9616f77611718094", + }, + Message: message, + } + + tx, err := withdrawAndCallPTB( + signerAddrStr, + gatewayPackageIDStr, + gatewayModule, + gatewayObjectIDStr, + withdrawCapIDStr, + coinTypeStr, + amountStr, + nonceStr, + gasBudgetStr, + receiver, + cp, + ) + require.NoError(t, err) + fmt.Printf("tx: %v\n", tx) +} From b0f2762f62a602cb11ba7c490045862f881a1b4b Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 16 Apr 2025 22:00:19 -0500 Subject: [PATCH 11/49] uncomment sui e2e tests; remove redundant debug log print --- cmd/zetae2e/local/local.go | 14 ++--- e2e/runner/setup_sui.go | 7 --- .../sui/signer/withdraw_and_call_test.go | 54 ------------------- 3 files changed, 7 insertions(+), 68 deletions(-) delete mode 100644 zetaclient/chains/sui/signer/withdraw_and_call_test.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 42d33f878c..c9a66beeb0 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -461,16 +461,16 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testSui { suiTests := []string{ e2etests.TestSuiDepositName, - // e2etests.TestSuiDepositAndCallRevertName, - // e2etests.TestSuiDepositAndCallName, + e2etests.TestSuiDepositAndCallRevertName, + e2etests.TestSuiDepositAndCallName, e2etests.TestSuiTokenDepositName, - // e2etests.TestSuiTokenDepositAndCallName, - // e2etests.TestSuiTokenDepositAndCallRevertName, + e2etests.TestSuiTokenDepositAndCallName, + e2etests.TestSuiTokenDepositAndCallRevertName, e2etests.TestSuiWithdrawName, - // e2etests.TestSuiWithdrawRevertWithCallName, + e2etests.TestSuiWithdrawRevertWithCallName, e2etests.TestSuiTokenWithdrawName, - // e2etests.TestSuiDepositRestrictedName, - // e2etests.TestSuiWithdrawRestrictedName, + e2etests.TestSuiDepositRestrictedName, + e2etests.TestSuiWithdrawRestrictedName, // TODO: enable withdraw and call test // https://github.com/zeta-chain/node/issues/3742 diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index bc5f8b16a1..969506ebda 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -92,13 +92,6 @@ func (r *E2ERunner) deploySUIGateway() (whitelistCapID, withdrawCapID string) { withdrawCapID, ok = objectIDs[filterWithdrawCapType] require.True(r, ok, "withdrawCap object not found") - gatewayObj, err := r.Clients.Sui.SuiGetObject(r.Ctx, models.SuiGetObjectRequest{ - ObjectId: gatewayID, - Options: models.SuiObjectDataOptions{}, - }) - require.NoError(r, err) - r.Logger.Print("gateway object Data: %v", gatewayObj.Data) - // set sui gateway r.SuiGateway = zetasui.NewGateway(packageID, gatewayID) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go deleted file mode 100644 index fec5f6e598..0000000000 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package signer - -import ( - "encoding/hex" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/contracts/sui" -) - -func Test_withdrawAndCallPTB(t *testing.T) { - // ARRANGE - signerAddrStr := "0x19c693d9b288073f5fd9572dd696604844cabab4c1637436338e023a5d67bc7a" - gatewayPackageIDStr := "0x68efbe57639ca63db3bb6d37410f2963a81f9ba0cb4cfda205cff0b5ebd134ad" - gatewayModule := "gateway" - gatewayObjectIDStr := "0x71e93003b3b18a19bee11047778c4dcde65111cf27c0d76b42833704cf848d4b" - withdrawCapIDStr := "0x0e5e4e15b982a46c40e33c22d0a528af4b61fa404a849938b2afa02f552dce20" - coinTypeStr := string(sui.SUI) - amountStr := "1000000" - nonceStr := "1" - gasBudgetStr := "20000000" - receiver := "0xdfe280f111900991daa5c25d73a5ae134022b81616d3325945ac157b5394b598" - message, err := hex.DecodeString("0ee37c85c59aecc644a3114a4182eacdf3e49435e588978b702191c531c397d6") - require.NoError(t, err) - encodedMessage := hex.EncodeToString(message) - require.Equal(t, "0ee37c85c59aecc644a3114a4182eacdf3e49435e588978b702191c531c397d6", encodedMessage) - cp := sui.CallPayload{ - TypeArgs: []string{"0x10bb172f3bd83b6ecd9861aeb843d4dc0549d3ca2085492d6810c5d768dfe880::token::TOKEN"}, - ObjectIDs: []string{ - "0xcf63b648079a11f00e6b738ad0b2226e49444d04fa6f6d585dbb636f551f323f", - "0x382b6d92ed22bf13d61a6786ef9af505608e24c3572cadaad8eefb66e1366fc2", - "0x2d4eb032bda2aaac27bac5a72c0b821fda8c3c50be90869f024959f2b350b053", - "0x398a4b74e3ee229bb3deaf9facd2e435bcb8e216292ffe6e9616f77611718094", - }, - Message: message, - } - - tx, err := withdrawAndCallPTB( - signerAddrStr, - gatewayPackageIDStr, - gatewayModule, - gatewayObjectIDStr, - withdrawCapIDStr, - coinTypeStr, - amountStr, - nonceStr, - gasBudgetStr, - receiver, - cp, - ) - require.NoError(t, err) - fmt.Printf("tx: %v\n", tx) -} From d964e45078082ee49e31fd05ef788ec128241afe Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 16 Apr 2025 23:31:54 -0500 Subject: [PATCH 12/49] remove redundant log print; use hardcoded version to replicate InvalidBCSBytes error --- e2e/runner/setup_sui.go | 2 -- zetaclient/chains/sui/signer/withdraw_and_call.go | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index 969506ebda..4403c16e52 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -207,8 +207,6 @@ func (r *E2ERunner) deploySuiPackage(bytecodeBase64s []string, objectTypeFilters }) require.NoError(r, err) - fmt.Printf("deploySuiPackage resp: \n%v\n", resp) - // find packageID var packageID string for _, change := range resp.ObjectChanges { diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index f73dba9974..a9ed8bec88 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -61,7 +61,7 @@ func withdrawAndCallPTB( gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjRef.ObjectId, - InitialSharedVersion: gatewayObjRef.Version, // TODO: use initial version + InitialSharedVersion: 3, // TODO: get the correct initial version by querying the object Mutable: true, }, }) @@ -178,8 +178,8 @@ func withdrawAndCallPTB( objectArg, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: onCallObjectRef.ObjectId, - InitialSharedVersion: onCallObjectRef.Version, // TODO: get the correct value by querying the object - Mutable: true, // TODO: get coin object for gas payment + InitialSharedVersion: 6, // TODO: get the correct initial version by querying the object + Mutable: true, // TODO: get coin object for gas payment }, }) if err != nil { From 0e376eff6ea3269c0c56bd28afd0d4ff1022d8cc Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 17 Apr 2025 14:35:43 -0500 Subject: [PATCH 13/49] fix PTB withdraw and call InvalidBCSBytes and other execution errors --- cmd/zetae2e/local/local.go | 2 +- e2e/contracts/sui/bin.go | 14 ++++++------ .../sui/{example.mv => connected.mv} | Bin 613 -> 615 bytes .../sui/example/sources/example.move | 2 +- .../sources/{token.go.move => token.move} | 0 e2e/runner/setup_sui.go | 10 ++++----- .../chains/sui/signer/withdraw_and_call.go | 20 ++++++++---------- 7 files changed, 23 insertions(+), 25 deletions(-) rename e2e/contracts/sui/{example.mv => connected.mv} (79%) rename e2e/contracts/sui/example/sources/{token.go.move => token.move} (100%) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index c9a66beeb0..6857cd84a7 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -474,7 +474,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // TODO: enable withdraw and call test // https://github.com/zeta-chain/node/issues/3742 - e2etests.TestSuiWithdrawAndCallName, + //e2etests.TestSuiWithdrawAndCallName, } eg.Go(suiTestRoutine(conf, deployerRunner, verbose, suiTests...)) } diff --git a/e2e/contracts/sui/bin.go b/e2e/contracts/sui/bin.go index 09a2e85819..b13d13948e 100644 --- a/e2e/contracts/sui/bin.go +++ b/e2e/contracts/sui/bin.go @@ -17,8 +17,8 @@ var evmBinary []byte //go:embed token.mv var tokenBinary []byte -//go:embed example.mv -var exampleBinary []byte +//go:embed connected.mv +var connectedBinary []byte // GatewayBytecodeBase64 gets the gateway binary encoded as base64 for deployment func GatewayBytecodeBase64() string { @@ -35,12 +35,12 @@ func EVMBytecodeBase64() string { return base64.StdEncoding.EncodeToString(evmBinary) } -// TokenBytecodeBase64 gets the token binary encoded as base64 for deployment -func TokenBytecodeBase64() string { +// ExampleTokenBytecodeBase64 gets the token binary encoded as base64 for deployment +func ExampleTokenBytecodeBase64() string { return base64.StdEncoding.EncodeToString(tokenBinary) } -// ExampleBytecodeBase64 gets the example binary encoded as base64 for deployment -func ExampleBytecodeBase64() string { - return base64.StdEncoding.EncodeToString(exampleBinary) +// ExampleConnectedBytecodeBase64 gets the connected binary encoded as base64 for deployment +func ExampleConnectedBytecodeBase64() string { + return base64.StdEncoding.EncodeToString(connectedBinary) } diff --git a/e2e/contracts/sui/example.mv b/e2e/contracts/sui/connected.mv similarity index 79% rename from e2e/contracts/sui/example.mv rename to e2e/contracts/sui/connected.mv index c2a84a891176d937e3ac8f207390abcb0cccf8d6..f69d65c38be6065fd429b5686ddef8f56cb1c710 100644 GIT binary patch delta 67 zcmaFL@|e-eHNGe{IW@B^wP+z*(Ucpfo_GVn~)cV* Date: Fri, 18 Apr 2025 18:26:14 -0500 Subject: [PATCH 14/49] make Sui withdraw and call E2E test working --- cmd/zetae2e/local/local.go | 2 +- pkg/contracts/sui/gateway.go | 38 +++ pkg/contracts/sui/withdraw_and_call_ptb.go | 221 ++++++++++++++++++ zetaclient/chains/sui/signer/signer_tx.go | 24 +- .../chains/sui/signer/withdraw_and_call.go | 102 ++++---- 5 files changed, 327 insertions(+), 60 deletions(-) create mode 100644 pkg/contracts/sui/withdraw_and_call_ptb.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 6857cd84a7..c9a66beeb0 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -474,7 +474,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // TODO: enable withdraw and call test // https://github.com/zeta-chain/node/issues/3742 - //e2etests.TestSuiWithdrawAndCallName, + e2etests.TestSuiWithdrawAndCallName, } eg.Go(suiTestRoutine(conf, deployerRunner, verbose, suiTests...)) } diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index e17bda19d4..30ce47a7f6 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -40,6 +40,10 @@ const ( DepositEvent EventType = "DepositEvent" DepositAndCallEvent EventType = "DepositAndCallEvent" WithdrawEvent EventType = "WithdrawEvent" + + // this event does not exist on gateway, this is to make the logic consistent + WithdrawAndCallPTBEvent EventType = "WithdrawAndCallPTBEvent" + // the gateway.move uses name "NonceIncreaseEvent", but here uses a more descriptive name CancelTxEvent EventType = "NonceIncreaseEvent" ) @@ -221,6 +225,12 @@ func (gw *Gateway) ParseEvent(event models.SuiEventResponse) (Event, error) { func (gw *Gateway) ParseOutboundEvent( res models.SuiTransactionBlockResponse, ) (event Event, content OutboundEventContent, err error) { + // normal withdraw contains only 1 command, if it contains 3 commands, + // try passing the transaction as a withdraw and call with PTB + if len(res.Transaction.Data.Transaction.Transactions) == ptbWithdrawAndCallCmdCount { + return gw.parseWithdrawAndCallPTB(res) + } + if len(res.Events) == 0 { return event, nil, errors.New("missing events") } @@ -306,6 +316,34 @@ func extractStr(kv map[string]any, key string) (string, error) { return v, nil } +func extractUint64(kv map[string]any, key string) (uint64, error) { + if _, ok := kv[key]; !ok { + return 0, errors.Errorf("missing %s", key) + } + + v, ok := kv[key].(float64) + if !ok { + return 0, errors.Errorf("invalid %s", key) + } + + // #nosec G115 always in range + return uint64(v), nil +} + +func extractInt(kv map[string]any, key string) (int, error) { + if _, ok := kv[key]; !ok { + return 0, errors.Errorf("missing %s", key) + } + + v, ok := kv[key].(float64) + if !ok { + return 0, errors.Errorf("invalid %s", key) + } + + // #nosec G115 always in range + return int(v), nil +} + func convertPayload(data []any) ([]byte, error) { payload := make([]byte, len(data)) diff --git a/pkg/contracts/sui/withdraw_and_call_ptb.go b/pkg/contracts/sui/withdraw_and_call_ptb.go new file mode 100644 index 0000000000..f3080644a1 --- /dev/null +++ b/pkg/contracts/sui/withdraw_and_call_ptb.go @@ -0,0 +1,221 @@ +package sui + +import ( + "fmt" + "slices" + "strconv" + + "cosmossdk.io/math" + "github.com/block-vision/sui-go-sdk/models" + "github.com/pkg/errors" +) + +const ( + // FuncWithdrawImpl is the gateway function name withdraw_impl + FuncWithdrawImpl = "withdraw_impl" + + // ModuleConnected is the Sui connected module name + ModuleConnected = "connected" + + // FuncOnCall is the Sui connected module function name on_call + FuncOnCall = "on_call" + + // TypeSeparator is the separator for Sui package and module + TypeSeparator = "::" + + // ptbWithdrawAndCallCmdCount is the number of commands in the PTB withdraw and call + // the three commands are: [withdraw_impl, transfer_objects, on_call] + ptbWithdrawAndCallCmdCount = 3 + + // ptbWithdrawImplInputCount is the number of inputs in the withdraw_impl command + // the inputs are: [gatewayObject, amount, nonce, gasBudget, withdrawCap] + ptbWithdrawImplInputCount = 5 +) + +// ptbWithdrawImplArgIndexes is the indexes of the inputs for the withdraw_impl command +// these are the corresponding indexes for arguments: [gatewayObject, amount, nonce, gasBudget, withdrawCap] +var ptbWithdrawImplArgIndexes = []int{0, 1, 2, 3, 4} + +// WithdrawAndCallPTB represents data for a Sui withdraw and call event +type WithdrawAndCallPTB struct { + PackageID string + Module string + Function string + + Amount math.Uint + Nonce uint64 +} + +// TokenAmount returns the amount of the withdraw and call +func (d WithdrawAndCallPTB) TokenAmount() math.Uint { + return d.Amount +} + +// TxNonce returns the nonce of the withdraw and call +func (d WithdrawAndCallPTB) TxNonce() uint64 { + return d.Nonce +} + +// ExtractInitialSharedVersion extracts the object initial shared version from the object data +func ExtractInitialSharedVersion(objData models.SuiObjectData) (uint64, error) { + owner, ok := objData.Owner.(map[string]any) + if !ok { + return 0, fmt.Errorf("invalid object owner type %T", objData.Owner) + } + + shared, ok := owner["Shared"] + if !ok { + return 0, fmt.Errorf("missing shared object") + } + + sharedMap, ok := shared.(map[string]any) + if !ok { + return 0, fmt.Errorf("invalid shared object type %T", shared) + } + + return extractUint64(sharedMap, "initial_shared_version") +} + +// parseWithdrawAndCallPTB parses withdraw and call with PTB. +// There is no actual event on gateway for withdraw and call, so we construct our own event to make the logic consistent. +func (gw *Gateway) parseWithdrawAndCallPTB( + res models.SuiTransactionBlockResponse, +) (event Event, content OutboundEventContent, err error) { + tx := res.Transaction + + // the number of PTB commands should be 3 + if len(tx.Data.Transaction.Transactions) != ptbWithdrawAndCallCmdCount { + return event, nil, errors.Wrapf( + ErrParseEvent, + "invalid number of commands(%d) in the PTB", + len(tx.Data.Transaction.Transactions), + ) + } + + // the number of PTB inputs should be >= 5 + if len(tx.Data.Transaction.Inputs) < ptbWithdrawImplInputCount { + return event, nil, errors.Wrapf( + ErrParseEvent, + "invalid number of inputs(%d) in the PTB", + len(tx.Data.Transaction.Inputs), + ) + } + + // parse withdraw_impl at command 0 + packageID, module, function, argIndexes, err := extractMoveCall(tx.Data.Transaction.Transactions[0]) + if err != nil { + return event, nil, errors.Wrap(ErrParseEvent, "unable to parse withdraw_impl command in the PTB") + } + + if packageID != gw.packageID { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid package id %s in the PTB", packageID) + } + + if module != moduleName { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid module name %s in the PTB", module) + } + + if function != FuncWithdrawImpl { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid function name %s in the PTB", function) + } + + // ensure the argument indexes are matching the expected indexes + if !slices.Equal(argIndexes, ptbWithdrawImplArgIndexes) { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid argument indexes %v", argIndexes) + } + + // parse withdraw_impl arguments + // argument1: amount + amountStr, err := extractStr(tx.Data.Transaction.Inputs[1], "value") + if err != nil { + return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to extract amount") + } + amount, err := math.ParseUint(amountStr) + if err != nil { + return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to parse amount") + } + + // argument2: nonce + nonceStr, err := extractStr(tx.Data.Transaction.Inputs[2], "value") + if err != nil { + return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to extract nonce") + } + nonce, err := strconv.ParseUint(nonceStr, 10, 64) + if err != nil { + return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to parse nonce") + } + + content = WithdrawAndCallPTB{ + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + Amount: amount, + Nonce: nonce, + } + + event = Event{ + TxHash: res.Digest, + EventIndex: 0, + EventType: WithdrawAndCallPTBEvent, + content: content, + } + + return event, content, nil +} + +// extractMoveCall extracts the MoveCall information from the PTB transaction command +func extractMoveCall(transaction any) (packageID, module, function string, argIndexes []int, err error) { + commands, ok := transaction.(map[string]any) + if !ok { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "invalid command type") + } + + // parse MoveCall info + moveCall, ok := commands["MoveCall"].(map[string]any) + if !ok { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing MoveCall") + } + + packageID, err = extractStr(moveCall, "package") + if err != nil { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing package ID") + } + + module, err = extractStr(moveCall, "module") + if err != nil { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing module name") + } + + function, err = extractStr(moveCall, "function") + if err != nil { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing function name") + } + + // parse MoveCall data + data, ok := moveCall["arguments"] + if !ok { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing arguments") + } + + arguments, ok := data.([]any) + if !ok { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "arguments should be of slice type") + } + + // extract MoveCall argument indexes + argIndexes = make([]int, len(arguments)) + for i, arg := range arguments { + indexes, ok := arg.(map[string]any) + if !ok { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "invalid argument type") + } + + index, err := extractInt(indexes, "Input") + if err != nil { + return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing argument index") + } + argIndexes[i] = index + } + + return packageID, module, function, argIndexes, nil +} diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 68415e2b08..8366469140 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -153,9 +153,8 @@ func (s *Signer) buildWithdrawAndCallTx( return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") } - // get all other object references: [gateway, withdrawCap, ...] - objectIDStrs := append([]string{s.gateway.ObjectID(), withdrawCapIDStr}, cp.ObjectIDs...) - objectRefs, err := s.getSuiObjectRefs(ctx, objectIDStrs...) + // get all other object references: [gateway, withdrawCap, onCallObjects] + objectRefs, err := s.getWithdrawAndCallObjectRefs(ctx, s.gateway.ObjectID(), withdrawCapIDStr, cp.ObjectIDs) if err != nil { return models.TxnMetaData{}, errors.Wrap(err, "unable to get objects") } @@ -164,13 +163,18 @@ func (s *Signer) buildWithdrawAndCallTx( withdrawCapObjRef := objectRefs[1] onCallObjectRefs := objectRefs[2:] - // Note: logs not formatted in standard, it's a temporary log - s.Logger().Std.Info().Msgf( - "WithdrawAndCall called with type arguments %v, object IDs %v, message %v", - cp.TypeArgs, - cp.ObjectIDs, - cp.Message, - ) + // print PTB transaction parameters + s.Logger().Std.Info(). + Str(logs.FieldMethod, "buildWithdrawAndCallTx"). + Str(logs.FieldCoinType, coinType). + Str("amount", params.Amount.String()). + Uint64(logs.FieldNonce, params.TssNonce). + Str("receiver", params.Receiver). + Str("gas_budget", gasBudget). + Any("type_args", cp.TypeArgs). + Any("object_ids", cp.ObjectIDs). + Hex("message", cp.Message). + Msg("calling withdrawAndCallPTB") // TODO: check all object IDs are share object here // https://github.com/zeta-chain/node/issues/3755 diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index c6e4398359..5a6ce35831 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -17,13 +17,6 @@ import ( zetasui "github.com/zeta-chain/node/pkg/contracts/sui" ) -const ( - typeSeparator = "::" - funcWithdrawImpl = "withdraw_impl" - funcOnCall = "on_call" - moduleConnected = "connected" -) - // withdrawAndCallPTB builds unsigned withdraw and call PTB Sui transaction // it chains the following calls: // 1. withdraw_impl on gateway @@ -44,75 +37,75 @@ func withdrawAndCallPTB( gasBudgetStr, receiver string, cp zetasui.CallPayload, -) (models.TxnMetaData, error) { +) (tx models.TxnMetaData, err error) { ptb := suiptb.NewTransactionDataTransactionBuilder() // Parse arguments signerAddr, err := sui.AddressFromHex(signerAddrStr) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse signer address: %w", err) + return tx, errors.Wrapf(err, "failed to parse signer address %s", signerAddrStr) } gatewayPackageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse package ID: %w", err) + return tx, errors.Wrapf(err, "failed to parse package ID %s", gatewayPackageIDStr) } coinType, err := parseTypeString(coinTypeStr) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse coin type: %w", err) + return tx, errors.Wrapf(err, "failed to parse coin type %s", coinTypeStr) } gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjRef.ObjectId, - InitialSharedVersion: 3, // TODO: get the correct initial version by querying the object + InitialSharedVersion: gatewayObjRef.Version, Mutable: true, }, }) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + return tx, errors.Wrap(err, "failed to create gateway object argument") } amountUint64, err := strconv.ParseUint(amountStr, 10, 64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse amount: %w", err) + return tx, errors.Wrapf(err, "failed to parse amount %s", amountStr) } amount, err := ptb.Pure(amountUint64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + return tx, errors.Wrapf(err, "failed to create amount argument") } nonceUint64, err := strconv.ParseUint(nonceStr, 10, 64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse nonce: %w", err) + return tx, errors.Wrapf(err, "failed to parse nonce %s", nonceStr) } nonce, err := ptb.Pure(nonceUint64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + return tx, errors.Wrapf(err, "failed to create nonce argument") } gasBudgetUint64, err := strconv.ParseUint(gasBudgetStr, 10, 64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse gas budget: %w", err) + return tx, errors.Wrapf(err, "failed to parse gas budget %s", gasBudgetStr) } gasBudget, err := ptb.Pure(gasBudgetUint64) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create pure argument: %w", err) + return tx, errors.Wrapf(err, "failed to create gas budget argument") } withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + return tx, errors.Wrapf(err, "failed to create withdraw cap object argument") } - // Move call for withdraw_impl and get its command index + // Move call for withdraw_impl and get its command index (0) cmdIndex := uint16(len(ptb.Commands)) ptb.Command(suiptb.Command{ MoveCall: &suiptb.ProgrammableMoveCall{ Package: gatewayPackageID, Module: gatewayModule, - Function: funcWithdrawImpl, + Function: zetasui.FuncWithdrawImpl, TypeArguments: []sui.TypeTag{ {Struct: coinType}, }, @@ -141,10 +134,10 @@ func withdrawAndCallPTB( }, } - // Transfer budget coins to the TSS address + // Transfer gas budget coins to the TSS address tssAddrArg, err := ptb.Pure(signerAddr) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create pure address argument: %w", err) + return tx, errors.Wrapf(err, "failed to create tss address argument") } ptb.Command(suiptb.Command{ @@ -158,7 +151,7 @@ func withdrawAndCallPTB( // The receiver in the cctx is used as target package ID targetPackageID, err := sui.PackageIdFromHex(receiver) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse target package ID: %w", err) + return tx, errors.Wrapf(err, "failed to parse target package ID %s", receiver) } // Build the type arguments for on_call in order: [withdrawn coin type, ... payload type arguments] @@ -167,7 +160,7 @@ func withdrawAndCallPTB( for _, typeArg := range cp.TypeArgs { typeStruct, err := parseTypeString(typeArg) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to parse type argument: %w", err) + return tx, errors.Wrapf(err, "failed to parse type argument %s", typeArg) } onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: typeStruct}) } @@ -181,12 +174,12 @@ func withdrawAndCallPTB( objectArg, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: onCallObjectRef.ObjectId, - InitialSharedVersion: 6, // TODO: get the correct initial version by querying the object - Mutable: true, // TODO: get coin object for gas payment + InitialSharedVersion: onCallObjectRef.Version, + Mutable: true, }, }) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create object argument: %w", err) + return tx, errors.Wrapf(err, "failed to create object argument: %v", onCallObjectRef) } onCallArgs = append(onCallArgs, objectArg) } @@ -194,7 +187,7 @@ func withdrawAndCallPTB( // Add any additional message arguments messageArg, err := ptb.Pure(cp.Message) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to create pure message argument: %w", err) + return tx, errors.Wrapf(err, "failed to create message argument: %x", cp.Message) } onCallArgs = append(onCallArgs, messageArg) @@ -202,8 +195,8 @@ func withdrawAndCallPTB( ptb.Command(suiptb.Command{ MoveCall: &suiptb.ProgrammableMoveCall{ Package: targetPackageID, - Module: moduleConnected, - Function: funcOnCall, + Module: zetasui.ModuleConnected, + Function: zetasui.FuncOnCall, TypeArguments: onCallTypeArgs, Arguments: onCallArgs, }, @@ -218,18 +211,16 @@ func withdrawAndCallPTB( pt, []*sui.ObjectRef{ &suiCoinObjRef, - }, // TODO: get coin object for gas payment - retrieve a coin object owned by the signer + }, suiclient.DefaultGasBudget, suiclient.DefaultGasPrice, ) txBytes, err := bcs.Marshal(txData) if err != nil { - return models.TxnMetaData{}, fmt.Errorf("failed to marshal transaction data: %w", err) + return tx, errors.Wrapf(err, "failed to marshal transaction data: %v", txData) } - fmt.Println("withdrawAndCallPTB success") - // Encode the transaction bytes to base64 return models.TxnMetaData{ TxBytes: base64.StdEncoding.EncodeToString(txBytes), @@ -237,7 +228,7 @@ func withdrawAndCallPTB( } func parseTypeString(t string) (*sui.StructTag, error) { - parts := strings.Split(t, typeSeparator) + parts := strings.Split(t, zetasui.TypeSeparator) if len(parts) != 3 { return nil, fmt.Errorf("invalid type string: %s", t) } @@ -257,41 +248,54 @@ func parseTypeString(t string) (*sui.StructTag, error) { }, nil } -// getSuiObjectRefs returns the latest SUI object references for the given object IDs -// Note: the SUI object may change over time, so we need to get the latest object -func (s *Signer) getSuiObjectRefs(ctx context.Context, objectIDStrs ...string) ([]sui.ObjectRef, error) { - if len(objectIDStrs) == 0 { - return nil, errors.New("object ID is required") - } +// getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call +func (s *Signer) getWithdrawAndCallObjectRefs( + ctx context.Context, + gatewayID, withdrawCapID string, + onCallObjectIDs []string, +) ([]sui.ObjectRef, error) { + objectIDs := append([]string{gatewayID, withdrawCapID}, onCallObjectIDs...) // query objects in batch suiObjects, err := s.client.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ - ObjectIds: objectIDStrs, - Options: models.SuiObjectDataOptions{}, + ObjectIds: objectIDs, + Options: models.SuiObjectDataOptions{ + // show owner info in order to retrieve object initial shared version + ShowOwner: true, + }, }) if err != nil { - return nil, errors.Wrapf(err, "unable to get SUI objects for %v", objectIDStrs) + return nil, errors.Wrapf(err, "unable to get SUI objects for %v", objectIDs) } // convert object data to object references - objectRefs := make([]sui.ObjectRef, 0, len(objectIDStrs)) + objectRefs := make([]sui.ObjectRef, 0, len(objectIDs)) for _, object := range suiObjects { objectID, err := sui.ObjectIdFromHex(object.Data.ObjectId) if err != nil { return nil, errors.Wrapf(err, "failed to parse SUI object ID for %s", object.Data.ObjectId) } + objectVersion, err := strconv.ParseUint(object.Data.Version, 10, 64) if err != nil { return nil, errors.Wrapf(err, "failed to parse SUI object version for %s", object.Data.ObjectId) } + + // must use initial version for shared object, not the current version + // withdraw cap is not a shared object, so we must use current version + if object.Data.ObjectId != withdrawCapID { + objectVersion, err = zetasui.ExtractInitialSharedVersion(*object.Data) + if err != nil { + return nil, errors.Wrapf(err, "failed to extract initial shared version for %s", object.Data.ObjectId) + } + } + objectDigest, err := sui.NewBase58(object.Data.Digest) if err != nil { return nil, errors.Wrapf(err, "failed to parse SUI object digest for %s", object.Data.ObjectId) } - fmt.Printf("object: %s, version: %d, digest: %s\n", objectID.String(), objectVersion, objectDigest.String()) - objectRefs = append(objectRefs, sui.ObjectRef{ ObjectId: objectID, Version: objectVersion, From dfb2c3838c5af1fb223589a4ef26906f888a8f67 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 21 Apr 2025 13:53:13 -0500 Subject: [PATCH 15/49] improve sui withdraw and call e2e test --- changelog.md | 2 +- e2e/e2etests/test_sui_withdraw_and_call.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index c366b6bf0b..1c82e3d0a1 100644 --- a/changelog.md +++ b/changelog.md @@ -18,7 +18,7 @@ * [3756](https://github.com/zeta-chain/node/pull/3756) - parse revert options in solana inbounds * [3765](https://github.com/zeta-chain/node/pull/3765) - support cancelling Sui rejected withdrawal * [3792](https://github.com/zeta-chain/node/pull/3792) - add compliance check for Sui inbound and outbound -* [3793](https://github.com/zeta-chain/node/pull/3793) - add Sui PTB transaction building for withdrawAndCall +* [3793](https://github.com/zeta-chain/node/pull/3793) - support Sui withdrawAndCall using the PTB transaction ### Refactor diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 2400b32973..7cc009b634 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -16,7 +16,7 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // ARRANGE - // Given target package ID (example package) and an SUI amount + // Given target package ID (example package) and a SUI amount targetPackageID := r.SuiExample.PackageID amount := utils.ParseBigInt(r, args[0]) @@ -39,7 +39,9 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { suiAddress := sample.SuiAddress(r) message, err := hex.DecodeString(suiAddress[2:]) // remove 0x prefix require.NoError(r, err) + balanceBefore := r.SuiGetSUIBalance(suiAddress) + // create the payload payload := sui.NewCallPayload(argumentTypes, objects, message) // ACT @@ -55,4 +57,8 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "withdraw") require.EqualValues(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + + // balance after + balanceAfter := r.SuiGetSUIBalance(suiAddress) + require.Equal(r, balanceBefore+amount.Uint64(), balanceAfter) } From 351f6075a41e4782ede4456d44699f2f7c665837 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 21 Apr 2025 14:31:23 -0500 Subject: [PATCH 16/49] simplify json integer extraction; fix code generate; improve comment --- pkg/contracts/sui/coin.go | 6 +- pkg/contracts/sui/gateway.go | 29 +++------ pkg/contracts/sui/withdraw_and_call_ptb.go | 6 +- zetaclient/chains/sui/observer/observer.go | 1 - zetaclient/chains/sui/signer/signer.go | 1 - .../chains/sui/signer/withdraw_and_call.go | 2 +- zetaclient/testutils/mocks/sui_client.go | 62 ++++++++++++++----- zetaclient/testutils/mocks/sui_gen.go | 3 +- 8 files changed, 64 insertions(+), 46 deletions(-) diff --git a/pkg/contracts/sui/coin.go b/pkg/contracts/sui/coin.go index c5fa7de026..0b4a019f0e 100644 --- a/pkg/contracts/sui/coin.go +++ b/pkg/contracts/sui/coin.go @@ -1,6 +1,6 @@ package sui -// CoinType represents the coin type for the inbound +// CoinType represents the coin type for the SUI token type CoinType string const ( @@ -11,7 +11,7 @@ const ( SUIShort CoinType = "0x2::sui::SUI" ) -// IsSUIType returns true if the given coin type is SUI -func IsSUIType(coinType CoinType) bool { +// IsSUICoinType returns true if the given coin type is SUI +func IsSUICoinType(coinType CoinType) bool { return coinType == SUI || coinType == SUIShort } diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index 30ce47a7f6..233e3d67b8 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -10,6 +10,7 @@ import ( "cosmossdk.io/math" "github.com/block-vision/sui-go-sdk/models" "github.com/pkg/errors" + "golang.org/x/exp/constraints" ) // EventType represents Gateway event type (both inbound & outbound) @@ -41,7 +42,7 @@ const ( DepositAndCallEvent EventType = "DepositAndCallEvent" WithdrawEvent EventType = "WithdrawEvent" - // this event does not exist on gateway, this is to make the logic consistent + // this event does not exist on gateway, we define it to make the outbound processing consistent WithdrawAndCallPTBEvent EventType = "WithdrawAndCallPTBEvent" // the gateway.move uses name "NonceIncreaseEvent", but here uses a more descriptive name @@ -225,8 +226,8 @@ func (gw *Gateway) ParseEvent(event models.SuiEventResponse) (Event, error) { func (gw *Gateway) ParseOutboundEvent( res models.SuiTransactionBlockResponse, ) (event Event, content OutboundEventContent, err error) { - // normal withdraw contains only 1 command, if it contains 3 commands, - // try passing the transaction as a withdraw and call with PTB + // a simple withdraw contains one single command, if it contains 3 commands, + // we try passing the transaction as a withdraw and call with PTB if len(res.Transaction.Data.Transaction.Transactions) == ptbWithdrawAndCallCmdCount { return gw.parseWithdrawAndCallPTB(res) } @@ -316,32 +317,20 @@ func extractStr(kv map[string]any, key string) (string, error) { return v, nil } -func extractUint64(kv map[string]any, key string) (uint64, error) { - if _, ok := kv[key]; !ok { - return 0, errors.Errorf("missing %s", key) - } - - v, ok := kv[key].(float64) +// extractInteger extracts a float64 value from a map and converts it to any integer type +func extractInteger[T constraints.Integer](kv map[string]any, key string) (T, error) { + rawValue, ok := kv[key] if !ok { - return 0, errors.Errorf("invalid %s", key) - } - - // #nosec G115 always in range - return uint64(v), nil -} - -func extractInt(kv map[string]any, key string) (int, error) { - if _, ok := kv[key]; !ok { return 0, errors.Errorf("missing %s", key) } - v, ok := kv[key].(float64) + v, ok := rawValue.(float64) if !ok { return 0, errors.Errorf("invalid %s", key) } // #nosec G115 always in range - return int(v), nil + return T(v), nil } func convertPayload(data []any) ([]byte, error) { diff --git a/pkg/contracts/sui/withdraw_and_call_ptb.go b/pkg/contracts/sui/withdraw_and_call_ptb.go index f3080644a1..f73fdc7e1e 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb.go +++ b/pkg/contracts/sui/withdraw_and_call_ptb.go @@ -73,11 +73,11 @@ func ExtractInitialSharedVersion(objData models.SuiObjectData) (uint64, error) { return 0, fmt.Errorf("invalid shared object type %T", shared) } - return extractUint64(sharedMap, "initial_shared_version") + return extractInteger[uint64](sharedMap, "initial_shared_version") } // parseWithdrawAndCallPTB parses withdraw and call with PTB. -// There is no actual event on gateway for withdraw and call, so we construct our own event to make the logic consistent. +// There is no actual event on gateway for withdraw and call, but we construct our own event to make the logic consistent. func (gw *Gateway) parseWithdrawAndCallPTB( res models.SuiTransactionBlockResponse, ) (event Event, content OutboundEventContent, err error) { @@ -210,7 +210,7 @@ func extractMoveCall(transaction any) (packageID, module, function string, argIn return "", "", "", nil, errors.Wrap(ErrParseEvent, "invalid argument type") } - index, err := extractInt(indexes, "Input") + index, err := extractInteger[int](indexes, "Input") if err != nil { return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing argument index") } diff --git a/zetaclient/chains/sui/observer/observer.go b/zetaclient/chains/sui/observer/observer.go index ca009a51bf..9075d3dfa9 100644 --- a/zetaclient/chains/sui/observer/observer.go +++ b/zetaclient/chains/sui/observer/observer.go @@ -37,7 +37,6 @@ type RPC interface { QueryModuleEvents(ctx context.Context, q client.EventQuery) ([]models.SuiEventResponse, string, error) SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) - SuiGetObject(ctx context.Context, req models.SuiGetObjectRequest) (models.SuiObjectResponse, error) SuiGetTransactionBlock( ctx context.Context, req models.SuiGetTransactionBlockRequest, diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index e442956ebd..1c1f588b32 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -31,7 +31,6 @@ type Signer struct { type RPC interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) - SuiGetObject(ctx context.Context, req models.SuiGetObjectRequest) (models.SuiObjectResponse, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 5a6ce35831..650028c171 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -319,7 +319,7 @@ func (s *Signer) getTSSSuiCoinObjectRef(ctx context.Context) (sui.ObjectRef, err // locate the SUI coin object under TSS account var suiCoin *models.CoinData for _, coin := range coins.Data { - if zetasui.IsSUIType(zetasui.CoinType(coin.CoinType)) { + if zetasui.IsSUICoinType(zetasui.CoinType(coin.CoinType)) { suiCoin = &coin break } diff --git a/zetaclient/testutils/mocks/sui_client.go b/zetaclient/testutils/mocks/sui_client.go index 493f03f3bf..b2cd428e7f 100644 --- a/zetaclient/testutils/mocks/sui_client.go +++ b/zetaclient/testutils/mocks/sui_client.go @@ -224,26 +224,26 @@ func (_m *SuiClient) SuiExecuteTransactionBlock(ctx context.Context, req models. 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) { +// SuiGetTransactionBlock provides a mock function with given fields: ctx, req +func (_m *SuiClient) SuiGetTransactionBlock(ctx context.Context, req models.SuiGetTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { ret := _m.Called(ctx, req) if len(ret) == 0 { - panic("no return value specified for SuiGetObject") + panic("no return value specified for SuiGetTransactionBlock") } - var r0 models.SuiObjectResponse + var r0 models.SuiTransactionBlockResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetObjectRequest) (models.SuiObjectResponse, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetTransactionBlockRequest) (models.SuiTransactionBlockResponse, error)); ok { return rf(ctx, req) } - if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetObjectRequest) models.SuiObjectResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetTransactionBlockRequest) models.SuiTransactionBlockResponse); ok { r0 = rf(ctx, req) } else { - r0 = ret.Get(0).(models.SuiObjectResponse) + r0 = ret.Get(0).(models.SuiTransactionBlockResponse) } - if rf, ok := ret.Get(1).(func(context.Context, models.SuiGetObjectRequest) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, models.SuiGetTransactionBlockRequest) error); ok { r1 = rf(ctx, req) } else { r1 = ret.Error(1) @@ -252,26 +252,56 @@ func (_m *SuiClient) SuiGetObject(ctx context.Context, req models.SuiGetObjectRe return r0, r1 } -// SuiGetTransactionBlock provides a mock function with given fields: ctx, req -func (_m *SuiClient) SuiGetTransactionBlock(ctx context.Context, req models.SuiGetTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { +// SuiMultiGetObjects provides a mock function with given fields: ctx, req +func (_m *SuiClient) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) { ret := _m.Called(ctx, req) if len(ret) == 0 { - panic("no return value specified for SuiGetTransactionBlock") + panic("no return value specified for SuiMultiGetObjects") } - var r0 models.SuiTransactionBlockResponse + var r0 []*models.SuiObjectResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetTransactionBlockRequest) (models.SuiTransactionBlockResponse, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error)); ok { return rf(ctx, req) } - if rf, ok := ret.Get(0).(func(context.Context, models.SuiGetTransactionBlockRequest) models.SuiTransactionBlockResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiMultiGetObjectsRequest) []*models.SuiObjectResponse); ok { r0 = rf(ctx, req) } else { - r0 = ret.Get(0).(models.SuiTransactionBlockResponse) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SuiObjectResponse) + } } - if rf, ok := ret.Get(1).(func(context.Context, models.SuiGetTransactionBlockRequest) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, models.SuiMultiGetObjectsRequest) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SuiXGetAllCoins provides a mock function with given fields: ctx, req +func (_m *SuiClient) SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for SuiXGetAllCoins") + } + + var r0 models.PaginatedCoinsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetAllCoinsRequest) models.PaginatedCoinsResponse); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Get(0).(models.PaginatedCoinsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, models.SuiXGetAllCoinsRequest) error); ok { r1 = rf(ctx, req) } else { r1 = ret.Error(1) diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index a168554605..6b156f7475 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -23,7 +23,8 @@ type suiClient interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) SuiXQueryEvents(ctx context.Context, req models.SuiXQueryEventsRequest) (models.PaginatedEventsResponse, error) - SuiGetObject(ctx context.Context, req models.SuiGetObjectRequest) (models.SuiObjectResponse, error) + SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) + SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) SuiGetTransactionBlock( ctx context.Context, req models.SuiGetTransactionBlockRequest, From 3b165a80d236921bb812e42a85bb60ff83c8a084 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 21 Apr 2025 15:18:02 -0500 Subject: [PATCH 17/49] improve readability of function getWithdrawAndCallObjectRefs --- pkg/contracts/sui/withdraw_and_call_ptb.go | 1 + zetaclient/chains/sui/signer/signer_tx.go | 11 +++--- .../chains/sui/signer/withdraw_and_call.go | 38 +++++++++++++------ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/pkg/contracts/sui/withdraw_and_call_ptb.go b/pkg/contracts/sui/withdraw_and_call_ptb.go index f73fdc7e1e..5979290606 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb.go +++ b/pkg/contracts/sui/withdraw_and_call_ptb.go @@ -34,6 +34,7 @@ const ( // ptbWithdrawImplArgIndexes is the indexes of the inputs for the withdraw_impl command // these are the corresponding indexes for arguments: [gatewayObject, amount, nonce, gasBudget, withdrawCap] +// the withdraw_impl command is the first command in the PTB, so the indexes will always be [0, 1, 2, 3, 4] var ptbWithdrawImplArgIndexes = []int{0, 1, 2, 3, 4} // WithdrawAndCallPTB represents data for a Sui withdraw and call event diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 8366469140..479c01dd1e 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -154,15 +154,16 @@ func (s *Signer) buildWithdrawAndCallTx( } // get all other object references: [gateway, withdrawCap, onCallObjects] - objectRefs, err := s.getWithdrawAndCallObjectRefs(ctx, s.gateway.ObjectID(), withdrawCapIDStr, cp.ObjectIDs) + gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := s.getWithdrawAndCallObjectRefs( + ctx, + s.gateway.ObjectID(), + withdrawCapIDStr, + cp.ObjectIDs, + ) if err != nil { return models.TxnMetaData{}, errors.Wrap(err, "unable to get objects") } - gatewayObjRef := objectRefs[0] - withdrawCapObjRef := objectRefs[1] - onCallObjectRefs := objectRefs[2:] - // print PTB transaction parameters s.Logger().Std.Info(). Str(logs.FieldMethod, "buildWithdrawAndCallTx"). diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 650028c171..eba67d40d7 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -253,7 +253,7 @@ func (s *Signer) getWithdrawAndCallObjectRefs( ctx context.Context, gatewayID, withdrawCapID string, onCallObjectIDs []string, -) ([]sui.ObjectRef, error) { +) (gatewayObjRef, withdrawCapObjRef sui.ObjectRef, onCallObjectRefs []sui.ObjectRef, err error) { objectIDs := append([]string{gatewayID, withdrawCapID}, onCallObjectIDs...) // query objects in batch @@ -265,21 +265,29 @@ func (s *Signer) getWithdrawAndCallObjectRefs( }, }) if err != nil { - return nil, errors.Wrapf(err, "unable to get SUI objects for %v", objectIDs) + return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf(err, "failed to get SUI objects for %v", objectIDs) } // convert object data to object references - objectRefs := make([]sui.ObjectRef, 0, len(objectIDs)) + objectRefs := make([]sui.ObjectRef, len(objectIDs)) - for _, object := range suiObjects { + for i, object := range suiObjects { objectID, err := sui.ObjectIdFromHex(object.Data.ObjectId) if err != nil { - return nil, errors.Wrapf(err, "failed to parse SUI object ID for %s", object.Data.ObjectId) + return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( + err, + "failed to parse object ID %s", + object.Data.ObjectId, + ) } objectVersion, err := strconv.ParseUint(object.Data.Version, 10, 64) if err != nil { - return nil, errors.Wrapf(err, "failed to parse SUI object version for %s", object.Data.ObjectId) + return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( + err, + "failed to parse object version %s", + object.Data.Version, + ) } // must use initial version for shared object, not the current version @@ -287,23 +295,31 @@ func (s *Signer) getWithdrawAndCallObjectRefs( if object.Data.ObjectId != withdrawCapID { objectVersion, err = zetasui.ExtractInitialSharedVersion(*object.Data) if err != nil { - return nil, errors.Wrapf(err, "failed to extract initial shared version for %s", object.Data.ObjectId) + return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( + err, + "failed to extract initial shared version for object %s", + object.Data.ObjectId, + ) } } objectDigest, err := sui.NewBase58(object.Data.Digest) if err != nil { - return nil, errors.Wrapf(err, "failed to parse SUI object digest for %s", object.Data.ObjectId) + return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( + err, + "failed to parse object digest %s", + object.Data.Digest, + ) } - objectRefs = append(objectRefs, sui.ObjectRef{ + objectRefs[i] = sui.ObjectRef{ ObjectId: objectID, Version: objectVersion, Digest: objectDigest, - }) + } } - return objectRefs, nil + return objectRefs[0], objectRefs[1], objectRefs[2:], nil } // getTSSSuiCoinObjectRef returns the latest SUI coin object reference for the TSS address From 8c58f1559c720fa1b65d4a46584603291cdf09ec Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 21 Apr 2025 16:32:15 -0500 Subject: [PATCH 18/49] move PTB argument related function to pkg contracts --- pkg/contracts/sui/ptb_argument.go | 55 +++++++++++++++++++ zetaclient/chains/sui/signer/signer_tx.go | 2 +- .../chains/sui/signer/withdraw_and_call.go | 44 ++------------- 3 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 pkg/contracts/sui/ptb_argument.go diff --git a/pkg/contracts/sui/ptb_argument.go b/pkg/contracts/sui/ptb_argument.go new file mode 100644 index 0000000000..d920530aaa --- /dev/null +++ b/pkg/contracts/sui/ptb_argument.go @@ -0,0 +1,55 @@ +package sui + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pattonkan/sui-go/sui" + "github.com/pattonkan/sui-go/sui/suiptb" + "github.com/pkg/errors" +) + +// PureUint64ArgFromStr converts a string to a uint64 PTB pure argument +func PureUint64FromString(ptb *suiptb.ProgrammableTransactionBuilder, integerStr string) (suiptb.Argument, error) { + valueUint64, err := strconv.ParseUint(integerStr, 10, 64) + if err != nil { + return suiptb.Argument{}, errors.Wrapf(err, "failed to parse amount %s", integerStr) + } + + arg, err := ptb.Pure(valueUint64) + if err != nil { + return suiptb.Argument{}, errors.Wrapf(err, "failed to create amount argument") + } + + return arg, nil +} + +// ParseTypeTagFromStr parses a PTB type argument StructTag from a type string +// Example: "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" -> +// +// &sui.StructTag{ +// Address: "0x0000000000000000000000000000000000000000000000000000000000000002", +// Module: "sui", +// Name: "SUI", +// } +func ParseTypeTagFromString(t string) (*sui.StructTag, error) { + parts := strings.Split(t, TypeSeparator) + if len(parts) != 3 { + return nil, fmt.Errorf("invalid type string: %s", t) + } + + address, err := sui.AddressFromHex(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid address: %s", parts[0]) + } + + module := parts[1] + name := parts[2] + + return &sui.StructTag{ + Address: address, + Module: module, + Name: name, + }, nil +} diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 479c01dd1e..2785e03f81 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -161,7 +161,7 @@ func (s *Signer) buildWithdrawAndCallTx( cp.ObjectIDs, ) if err != nil { - return models.TxnMetaData{}, errors.Wrap(err, "unable to get objects") + return models.TxnMetaData{}, errors.Wrap(err, "unable to get object references") } // print PTB transaction parameters diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index eba67d40d7..f4603f7819 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "fmt" "strconv" - "strings" "github.com/block-vision/sui-go-sdk/models" "github.com/fardream/go-bcs/bcs" @@ -51,7 +50,7 @@ func withdrawAndCallPTB( return tx, errors.Wrapf(err, "failed to parse package ID %s", gatewayPackageIDStr) } - coinType, err := parseTypeString(coinTypeStr) + coinType, err := zetasui.ParseTypeTagFromString(coinTypeStr) if err != nil { return tx, errors.Wrapf(err, "failed to parse coin type %s", coinTypeStr) } @@ -67,29 +66,17 @@ func withdrawAndCallPTB( return tx, errors.Wrap(err, "failed to create gateway object argument") } - amountUint64, err := strconv.ParseUint(amountStr, 10, 64) - if err != nil { - return tx, errors.Wrapf(err, "failed to parse amount %s", amountStr) - } - amount, err := ptb.Pure(amountUint64) + amount, err := zetasui.PureUint64FromString(ptb, amountStr) if err != nil { return tx, errors.Wrapf(err, "failed to create amount argument") } - nonceUint64, err := strconv.ParseUint(nonceStr, 10, 64) - if err != nil { - return tx, errors.Wrapf(err, "failed to parse nonce %s", nonceStr) - } - nonce, err := ptb.Pure(nonceUint64) + nonce, err := zetasui.PureUint64FromString(ptb, nonceStr) if err != nil { return tx, errors.Wrapf(err, "failed to create nonce argument") } - gasBudgetUint64, err := strconv.ParseUint(gasBudgetStr, 10, 64) - if err != nil { - return tx, errors.Wrapf(err, "failed to parse gas budget %s", gasBudgetStr) - } - gasBudget, err := ptb.Pure(gasBudgetUint64) + gasBudget, err := zetasui.PureUint64FromString(ptb, gasBudgetStr) if err != nil { return tx, errors.Wrapf(err, "failed to create gas budget argument") } @@ -158,7 +145,7 @@ func withdrawAndCallPTB( onCallTypeArgs := make([]sui.TypeTag, 0, len(cp.TypeArgs)+1) onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: coinType}) for _, typeArg := range cp.TypeArgs { - typeStruct, err := parseTypeString(typeArg) + typeStruct, err := zetasui.ParseTypeTagFromString(typeArg) if err != nil { return tx, errors.Wrapf(err, "failed to parse type argument %s", typeArg) } @@ -227,27 +214,6 @@ func withdrawAndCallPTB( }, nil } -func parseTypeString(t string) (*sui.StructTag, error) { - parts := strings.Split(t, zetasui.TypeSeparator) - if len(parts) != 3 { - return nil, fmt.Errorf("invalid type string: %s", t) - } - - address, err := sui.AddressFromHex(parts[0]) - if err != nil { - return nil, fmt.Errorf("invalid address: %s", parts[0]) - } - - module := parts[1] - name := parts[2] - - return &sui.StructTag{ - Address: address, - Module: module, - Name: name, - }, nil -} - // getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call func (s *Signer) getWithdrawAndCallObjectRefs( ctx context.Context, From 6189872ac7b58dfa609b8c418fb58f3ad4b4ac8b Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 00:02:08 -0500 Subject: [PATCH 19/49] add pkg contracts for sui withdraw and call --- pkg/contracts/sui/coin_test.go | 38 +++ pkg/contracts/sui/ptb_argument_test.go | 69 +++++ pkg/contracts/sui/withdraw_and_call_ptb.go | 16 +- .../sui/withdraw_and_call_ptb_test.go | 290 ++++++++++++++++++ .../chains/sui/signer/withdraw_and_call.go | 2 + 5 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 pkg/contracts/sui/coin_test.go create mode 100644 pkg/contracts/sui/ptb_argument_test.go create mode 100644 pkg/contracts/sui/withdraw_and_call_ptb_test.go diff --git a/pkg/contracts/sui/coin_test.go b/pkg/contracts/sui/coin_test.go new file mode 100644 index 0000000000..a5db06ec18 --- /dev/null +++ b/pkg/contracts/sui/coin_test.go @@ -0,0 +1,38 @@ +package sui + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsSUICoinType(t *testing.T) { + tests := []struct { + name string + coinType CoinType + want bool + }{ + { + name: "SUI coin type", + coinType: SUI, + want: true, + }, + { + name: "SUI short coin type", + coinType: SUIShort, + want: true, + }, + { + name: "not SUI coin type", + coinType: "0xae330284eefb31f37777c78007fc3bc0e88ca69b1be267e1b803d37c9ea52dc6", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := IsSUICoinType(test.coinType) + require.Equal(t, test.want, got) + }) + } +} diff --git a/pkg/contracts/sui/ptb_argument_test.go b/pkg/contracts/sui/ptb_argument_test.go new file mode 100644 index 0000000000..2798797014 --- /dev/null +++ b/pkg/contracts/sui/ptb_argument_test.go @@ -0,0 +1,69 @@ +package sui + +import ( + "testing" + + "github.com/pattonkan/sui-go/sui" + "github.com/stretchr/testify/require" +) + +func TestParseTypeTagFromString(t *testing.T) { + suiAddr, err := sui.AddressFromHex("0000000000000000000000000000000000000000000000000000000000000002") + require.NoError(t, err) + + otherAddrStr := "0xae330284eefb31f37777c78007fc3bc0e88ca69b1be267e1b803d37c9ea52dc6" + otherAddr, err := sui.AddressFromHex(otherAddrStr) + require.NoError(t, err) + + tests := []struct { + name string + coinType CoinType + want *sui.StructTag + errMsg string + }{ + { + name: "SUI coin type", + coinType: SUI, + want: &sui.StructTag{ + Address: suiAddr, + Module: "sui", + Name: "SUI", + }, + }, + { + name: "some other coin type", + coinType: CoinType(otherAddrStr + "::other::TOKEN"), + want: &sui.StructTag{ + Address: otherAddr, + Module: "other", + Name: "TOKEN", + }, + }, + { + name: "invalid type string", + coinType: CoinType(otherAddrStr), + want: nil, + errMsg: "invalid type string", + }, + { + name: "invalid address", + coinType: "invalid::sui::SUI", + want: nil, + errMsg: "invalid address", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ParseTypeTagFromString(string(test.coinType)) + if test.errMsg != "" { + require.Error(t, err) + require.ErrorContains(t, err, test.errMsg) + return + } + + require.NoError(t, err) + require.Equal(t, test.want, got) + }) + } +} diff --git a/pkg/contracts/sui/withdraw_and_call_ptb.go b/pkg/contracts/sui/withdraw_and_call_ptb.go index 5979290606..180cebbbf4 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb.go +++ b/pkg/contracts/sui/withdraw_and_call_ptb.go @@ -82,28 +82,28 @@ func ExtractInitialSharedVersion(objData models.SuiObjectData) (uint64, error) { func (gw *Gateway) parseWithdrawAndCallPTB( res models.SuiTransactionBlockResponse, ) (event Event, content OutboundEventContent, err error) { - tx := res.Transaction + tx := res.Transaction.Data.Transaction // the number of PTB commands should be 3 - if len(tx.Data.Transaction.Transactions) != ptbWithdrawAndCallCmdCount { + if len(tx.Transactions) != ptbWithdrawAndCallCmdCount { return event, nil, errors.Wrapf( ErrParseEvent, "invalid number of commands(%d) in the PTB", - len(tx.Data.Transaction.Transactions), + len(tx.Transactions), ) } // the number of PTB inputs should be >= 5 - if len(tx.Data.Transaction.Inputs) < ptbWithdrawImplInputCount { + if len(tx.Inputs) < ptbWithdrawImplInputCount { return event, nil, errors.Wrapf( ErrParseEvent, "invalid number of inputs(%d) in the PTB", - len(tx.Data.Transaction.Inputs), + len(tx.Inputs), ) } // parse withdraw_impl at command 0 - packageID, module, function, argIndexes, err := extractMoveCall(tx.Data.Transaction.Transactions[0]) + packageID, module, function, argIndexes, err := extractMoveCall(tx.Transactions[0]) if err != nil { return event, nil, errors.Wrap(ErrParseEvent, "unable to parse withdraw_impl command in the PTB") } @@ -127,7 +127,7 @@ func (gw *Gateway) parseWithdrawAndCallPTB( // parse withdraw_impl arguments // argument1: amount - amountStr, err := extractStr(tx.Data.Transaction.Inputs[1], "value") + amountStr, err := extractStr(tx.Inputs[1], "value") if err != nil { return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to extract amount") } @@ -137,7 +137,7 @@ func (gw *Gateway) parseWithdrawAndCallPTB( } // argument2: nonce - nonceStr, err := extractStr(tx.Data.Transaction.Inputs[2], "value") + nonceStr, err := extractStr(tx.Inputs[2], "value") if err != nil { return Event{}, nil, errors.Wrap(ErrParseEvent, "unable to extract nonce") } diff --git a/pkg/contracts/sui/withdraw_and_call_ptb_test.go b/pkg/contracts/sui/withdraw_and_call_ptb_test.go new file mode 100644 index 0000000000..c3379ac460 --- /dev/null +++ b/pkg/contracts/sui/withdraw_and_call_ptb_test.go @@ -0,0 +1,290 @@ +package sui + +import ( + "testing" + + "cosmossdk.io/math" + "github.com/block-vision/sui-go-sdk/models" + "github.com/stretchr/testify/require" +) + +func Test_WithdrawAndCallPTB_TokenAmount(t *testing.T) { + event := WithdrawAndCallPTB{ + Amount: math.NewUint(100), + Nonce: 1, + } + require.Equal(t, math.NewUint(100), event.TokenAmount()) +} + +func Test_WithdrawAndCallPTB_TxNonce(t *testing.T) { + event := WithdrawAndCallPTB{ + Amount: math.NewUint(100), + Nonce: 1, + } + require.Equal(t, uint64(1), event.TxNonce()) +} + +func Test_ExtractInitialSharedVersion(t *testing.T) { + tests := []struct { + name string + objData models.SuiObjectData + wantVersion uint64 + errMsg string + }{ + { + name: "successful extraction", + objData: models.SuiObjectData{ + Owner: map[string]any{ + "Shared": map[string]any{ + "initial_shared_version": float64(3), + }, + }, + }, + wantVersion: 3, + }, + { + name: "invalid owner type", + objData: models.SuiObjectData{ + Owner: "invalid", + }, + wantVersion: 0, + errMsg: "invalid object owner type string", + }, + { + name: "missing shared object", + objData: models.SuiObjectData{ + Owner: map[string]any{ + "Owned": map[string]any{}, + }, + }, + wantVersion: 0, + errMsg: "missing shared object", + }, + { + name: "invalid shared object type", + objData: models.SuiObjectData{ + Owner: map[string]any{ + "Shared": "invalid", + }, + }, + wantVersion: 0, + errMsg: "invalid shared object type string", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + version, err := ExtractInitialSharedVersion(tt.objData) + if tt.errMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errMsg) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantVersion, version) + }) + } +} + +func Test_parseWithdrawAndCallPTB(t *testing.T) { + const ( + txHash = "AvpjaWZmHEJqj6AewiyK3bziGE2RPH5URub9nMx1sNL7" + packageID = "0xa0464ffe0ffb12b2a474e0669e15aeb0b1e2b31a1865cca83f47b42c4707a550" + amountStr = "100" + nonceStr = "2" + ) + + gw := &Gateway{ + packageID: packageID, + } + + tests := []struct { + name string + response models.SuiTransactionBlockResponse + want WithdrawAndCallPTB + errMsg string + }{ + { + name: "valid transaction block", + response: createValidTxResponse(txHash, packageID, amountStr, nonceStr), + want: WithdrawAndCallPTB{ + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + Amount: math.NewUint(100), + Nonce: 2, + }, + }, + { + name: "invalid number of commands", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res.Transaction.Data.Transaction.Transactions = res.Transaction.Data.Transaction.Transactions[:2] + return res + }(), + errMsg: "invalid number of commands", + }, + { + name: "invalid number of inputs", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res.Transaction.Data.Transaction.Inputs = res.Transaction.Data.Transaction.Inputs[:4] + return res + }(), + errMsg: "invalid number of inputs", + }, + { + name: "unable to parse withdraw_impl", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res.Transaction.Data.Transaction.Transactions[0] = "invalid" + return res + }(), + errMsg: "unable to parse withdraw_impl command", + }, + { + name: "invalid package ID", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) + moveCall["package"] = "wrong_package" + return res + }(), + errMsg: "invalid package id", + }, + { + name: "invalid module name", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) + moveCall["module"] = "wrong_module" + return res + }(), + errMsg: "invalid module name", + }, + { + name: "invalid function name", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) + moveCall["function"] = "wrong_function" + return res + }(), + errMsg: "invalid function name", + }, + { + name: "invalid argument indexes", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) + arguments := moveCall["arguments"].([]any) + arguments[0] = map[string]any{"Input": float64(5)} // Change index to make it invalid + return res + }(), + errMsg: "invalid argument indexes", + }, + { + name: "invalid amount format", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res.Transaction.Data.Transaction.Inputs[1] = models.SuiCallArg{ + "value": "invalid_number", + } + return res + }(), + errMsg: "unable to parse amount", + }, + { + name: "invalid nonce format", + response: func() models.SuiTransactionBlockResponse { + res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res.Transaction.Data.Transaction.Inputs[2] = models.SuiCallArg{ + "value": "invalid_nonce", + } + return res + }(), + errMsg: "unable to parse nonce", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + event, content, err := gw.parseWithdrawAndCallPTB(test.response) + if test.errMsg != "" { + require.Contains(t, err.Error(), test.errMsg) + return + } + + require.NoError(t, err) + require.Equal(t, txHash, event.TxHash) + require.Zero(t, event.EventIndex) + require.Equal(t, WithdrawAndCallPTBEvent, event.EventType) + + withdrawCallPTB, ok := content.(WithdrawAndCallPTB) + require.True(t, ok) + require.Equal(t, test.want, withdrawCallPTB) + }) + } +} + +func createValidTxResponse(txHash, packageID, amount, nonce string) models.SuiTransactionBlockResponse { + return models.SuiTransactionBlockResponse{ + Digest: txHash, + Transaction: models.SuiTransactionBlock{ + Data: models.SuiTransactionBlockData{ + Transaction: models.SuiTransactionBlockKind{ + Inputs: []models.SuiCallArg{ + { + "initialSharedVersion": "3", + "objectId": "0xb3630c3eba7b1211c12604a4ceade7a5c0811c4a5eb55af227f9943fcef0e24c", + }, + { + "type": "pure", + "value": amount, + }, + { + "type": "pure", + "value": nonce, + }, + { + "type": "pure", + "value": "1000", + }, + { + "digest": "26kdbzHiCt4nBFbMA6DjaazjRw98d6BRbe5gy81Wr1Aj", + "objectId": "0x48a52371089644d30703f726ca5c30cf76e85347b263549e092ccf22ac059c6c", + }, + }, + Transactions: []any{ + map[string]any{ + "MoveCall": map[string]any{ + "arguments": []any{ + map[string]any{"Input": float64(0)}, + map[string]any{"Input": float64(1)}, + map[string]any{"Input": float64(2)}, + map[string]any{"Input": float64(3)}, + map[string]any{"Input": float64(4)}, + }, + "function": FuncWithdrawImpl, + "module": moduleName, + "package": packageID, + }, + }, + map[string]any{ + "TransferObjects": map[string]any{}, + }, + map[string]any{ + "MoveCall": map[string]any{ + "arguments": []any{}, + "function": FuncOnCall, + "module": ModuleConnected, + "package": "target_package_id", + }, + }, + }, + }, + }, + }, + } +} diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index f4603f7819..27074ca24d 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -215,6 +215,8 @@ func withdrawAndCallPTB( } // getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call +// - Initial shared version will be used for shared objects +// - Current version will be used for non-shared objects, e.g. withdraw cap func (s *Signer) getWithdrawAndCallObjectRefs( ctx context.Context, gatewayID, withdrawCapID string, From 57ff9aec8e801d6a6cb28472cf6df94852a505b8 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 00:12:17 -0500 Subject: [PATCH 20/49] fix gosec --- zetaclient/chains/sui/signer/withdraw_and_call.go | 1 + 1 file changed, 1 insertion(+) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 27074ca24d..deb18bf9c8 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -87,6 +87,7 @@ func withdrawAndCallPTB( } // Move call for withdraw_impl and get its command index (0) + // #nosec G115 always in range cmdIndex := uint16(len(ptb.Commands)) ptb.Command(suiptb.Command{ MoveCall: &suiptb.ProgrammableMoveCall{ From a4123deaf91b4becea123654b35c955b0227d681 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 10:25:49 -0500 Subject: [PATCH 21/49] simplify the code for sui contract fields assignment --- cmd/zetae2e/config/contracts.go | 35 ++++++++++++--------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index bf8d8e4366..5312caa2c3 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -131,30 +131,21 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { 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() + otherSuiFields := map[string]*string{ + conf.Contracts.Sui.FungibleTokenCoinType.String(): &r.SuiTokenCoinType, + conf.Contracts.Sui.FungibleTokenTreasuryCap.String(): &r.SuiTokenTreasuryCap, + conf.Contracts.Sui.ExamplePackageID.String(): &r.SuiExample.PackageID, + conf.Contracts.Sui.ExampleTokenType.String(): &r.SuiExample.TokenType, + conf.Contracts.Sui.ExampleGlobalConfigID.String(): &r.SuiExample.GlobalConfigID, + conf.Contracts.Sui.ExamplePartnerID.String(): &r.SuiExample.PartnerID, + conf.Contracts.Sui.ExampleClockID.String(): &r.SuiExample.ClockID, + conf.Contracts.Sui.ExamplePoolID.String(): &r.SuiExample.PoolID, } - if c := conf.Contracts.Sui.ExamplePackageID; c != "" { - r.SuiExample.PackageID = c.String() - } - if c := conf.Contracts.Sui.ExampleTokenType; c != "" { - r.SuiExample.TokenType = c.String() - } - if c := conf.Contracts.Sui.ExampleGlobalConfigID; c != "" { - r.SuiExample.GlobalConfigID = c.String() - } - if c := conf.Contracts.Sui.ExamplePartnerID; c != "" { - r.SuiExample.PartnerID = c.String() - } - if c := conf.Contracts.Sui.ExampleClockID; c != "" { - r.SuiExample.ClockID = c.String() - } - if c := conf.Contracts.Sui.ExamplePoolID; c != "" { - r.SuiExample.PoolID = c.String() + for source, target := range otherSuiFields { + if source != "" { + *target = source + } } evmChainID, err := r.EVMClient.ChainID(r.Ctx) From 385b6abacc9f220698a93cd90b983a347d6e698d Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 12:55:43 -0500 Subject: [PATCH 22/49] use actual gas budget in the CCTX for PTB transaction --- pkg/contracts/sui/ptb_argument.go | 17 ++++++---- .../chains/sui/signer/withdraw_and_call.go | 34 +++++++++---------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pkg/contracts/sui/ptb_argument.go b/pkg/contracts/sui/ptb_argument.go index d920530aaa..6eefae9980 100644 --- a/pkg/contracts/sui/ptb_argument.go +++ b/pkg/contracts/sui/ptb_argument.go @@ -10,19 +10,22 @@ import ( "github.com/pkg/errors" ) -// PureUint64ArgFromStr converts a string to a uint64 PTB pure argument -func PureUint64FromString(ptb *suiptb.ProgrammableTransactionBuilder, integerStr string) (suiptb.Argument, error) { - valueUint64, err := strconv.ParseUint(integerStr, 10, 64) +// PureUint64ArgFromStr converts a string to a uint64 and creates a PTB pure argument +func PureUint64FromString( + ptb *suiptb.ProgrammableTransactionBuilder, + integerStr string, +) (arg suiptb.Argument, value uint64, err error) { + value, err = strconv.ParseUint(integerStr, 10, 64) if err != nil { - return suiptb.Argument{}, errors.Wrapf(err, "failed to parse amount %s", integerStr) + return suiptb.Argument{}, 0, errors.Wrapf(err, "failed to parse amount %s", integerStr) } - arg, err := ptb.Pure(valueUint64) + arg, err = ptb.Pure(value) if err != nil { - return suiptb.Argument{}, errors.Wrapf(err, "failed to create amount argument") + return suiptb.Argument{}, 0, errors.Wrapf(err, "failed to create amount argument") } - return arg, nil + return arg, value, nil } // ParseTypeTagFromStr parses a PTB type argument StructTag from a type string diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index deb18bf9c8..0f62a6fd96 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -55,7 +55,7 @@ func withdrawAndCallPTB( return tx, errors.Wrapf(err, "failed to parse coin type %s", coinTypeStr) } - gatewayObject, err := ptb.Obj(suiptb.ObjectArg{ + argGatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjRef.ObjectId, InitialSharedVersion: gatewayObjRef.Version, @@ -66,22 +66,22 @@ func withdrawAndCallPTB( return tx, errors.Wrap(err, "failed to create gateway object argument") } - amount, err := zetasui.PureUint64FromString(ptb, amountStr) + argAmount, _, err := zetasui.PureUint64FromString(ptb, amountStr) if err != nil { return tx, errors.Wrapf(err, "failed to create amount argument") } - nonce, err := zetasui.PureUint64FromString(ptb, nonceStr) + argNonce, _, err := zetasui.PureUint64FromString(ptb, nonceStr) if err != nil { return tx, errors.Wrapf(err, "failed to create nonce argument") } - gasBudget, err := zetasui.PureUint64FromString(ptb, gasBudgetStr) + argGasBudget, gasBudgetUint, err := zetasui.PureUint64FromString(ptb, gasBudgetStr) if err != nil { return tx, errors.Wrapf(err, "failed to create gas budget argument") } - withdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) + argWithdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) if err != nil { return tx, errors.Wrapf(err, "failed to create withdraw cap object argument") } @@ -98,24 +98,24 @@ func withdrawAndCallPTB( {Struct: coinType}, }, Arguments: []suiptb.Argument{ - gatewayObject, - amount, - nonce, - gasBudget, - withdrawCap, + argGatewayObject, + argAmount, + argNonce, + argGasBudget, + argWithdrawCap, }, }, }) // Create arguments to access the two results from the withdraw_impl call - withdrawnCoinsArg := suiptb.Argument{ + argWithdrawnCoins := suiptb.Argument{ NestedResult: &suiptb.NestedResult{ Cmd: cmdIndex, Result: 0, // First result (main coins) }, } - budgetCoinsArg := suiptb.Argument{ + argBudgetCoins := suiptb.Argument{ NestedResult: &suiptb.NestedResult{ Cmd: cmdIndex, Result: 1, // Second result (budget coins) @@ -123,15 +123,15 @@ func withdrawAndCallPTB( } // Transfer gas budget coins to the TSS address - tssAddrArg, err := ptb.Pure(signerAddr) + argTSSAddr, err := ptb.Pure(signerAddr) if err != nil { return tx, errors.Wrapf(err, "failed to create tss address argument") } ptb.Command(suiptb.Command{ TransferObjects: &suiptb.ProgrammableTransferObjects{ - Objects: []suiptb.Argument{budgetCoinsArg}, - Address: tssAddrArg, + Objects: []suiptb.Argument{argBudgetCoins}, + Address: argTSSAddr, }, }) @@ -155,7 +155,7 @@ func withdrawAndCallPTB( // Build the args for on_call: [withdrawns coins + payload objects + message] onCallArgs := make([]suiptb.Argument, 0, len(cp.ObjectIDs)+1) - onCallArgs = append(onCallArgs, withdrawnCoinsArg) + onCallArgs = append(onCallArgs, argWithdrawnCoins) // Add the payload objects, objects are all shared for _, onCallObjectRef := range onCallObjectRefs { @@ -200,7 +200,7 @@ func withdrawAndCallPTB( []*sui.ObjectRef{ &suiCoinObjRef, }, - suiclient.DefaultGasBudget, + gasBudgetUint, suiclient.DefaultGasPrice, ) From 1cfda65ac30a686c2e1634c8c35efa0859310627 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 13:16:59 -0500 Subject: [PATCH 23/49] get latest version of TSS SUI coin object for gas payment --- zetaclient/chains/sui/signer/signer.go | 2 +- .../chains/sui/signer/withdraw_and_call.go | 27 ++++++++++++------- zetaclient/testutils/mocks/sui_client.go | 12 ++++----- zetaclient/testutils/mocks/sui_gen.go | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 1c1f588b32..308e19d3a2 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -32,7 +32,7 @@ type RPC interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) - SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) + SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) SuiExecuteTransactionBlock( diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 0f62a6fd96..eab1c37b00 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -294,19 +294,29 @@ func (s *Signer) getWithdrawAndCallObjectRefs( // getTSSSuiCoinObjectRef returns the latest SUI coin object reference for the TSS address // Note: the SUI object may change over time, so we need to get the latest object func (s *Signer) getTSSSuiCoinObjectRef(ctx context.Context) (sui.ObjectRef, error) { - coins, err := s.client.SuiXGetAllCoins(ctx, models.SuiXGetAllCoinsRequest{ - Owner: s.TSS().PubKey().AddressSui(), + coins, err := s.client.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ + Owner: s.TSS().PubKey().AddressSui(), + CoinType: string(zetasui.SUI), }) if err != nil { return sui.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") } - // locate the SUI coin object under TSS account - var suiCoin *models.CoinData + var ( + suiCoin *models.CoinData + suiCoinVersion uint64 + ) + + // locate the latest version of SUI coin object under TSS account for _, coin := range coins.Data { - if zetasui.IsSUICoinType(zetasui.CoinType(coin.CoinType)) { + if !zetasui.IsSUICoinType(zetasui.CoinType(coin.CoinType)) { + continue + } + + version, _ := strconv.ParseUint(coin.Version, 10, 64) + if version > suiCoinVersion { suiCoin = &coin - break + suiCoinVersion = version } } if suiCoin == nil { @@ -318,10 +328,7 @@ func (s *Signer) getTSSSuiCoinObjectRef(ctx context.Context) (sui.ObjectRef, err if err != nil { return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin ID: %w", err) } - suiCoinVersion, err := strconv.ParseUint(suiCoin.Version, 10, 64) - if err != nil { - return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin version: %w", err) - } + suiCoinDigest, err := sui.NewBase58(suiCoin.Digest) if err != nil { return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin digest: %w", err) diff --git a/zetaclient/testutils/mocks/sui_client.go b/zetaclient/testutils/mocks/sui_client.go index b2cd428e7f..da2e061601 100644 --- a/zetaclient/testutils/mocks/sui_client.go +++ b/zetaclient/testutils/mocks/sui_client.go @@ -282,26 +282,26 @@ func (_m *SuiClient) SuiMultiGetObjects(ctx context.Context, req models.SuiMulti return r0, r1 } -// SuiXGetAllCoins provides a mock function with given fields: ctx, req -func (_m *SuiClient) SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) { +// SuiXGetCoins provides a mock function with given fields: ctx, req +func (_m *SuiClient) SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) { ret := _m.Called(ctx, req) if len(ret) == 0 { - panic("no return value specified for SuiXGetAllCoins") + panic("no return value specified for SuiXGetCoins") } var r0 models.PaginatedCoinsResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error)); ok { return rf(ctx, req) } - if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetAllCoinsRequest) models.PaginatedCoinsResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetCoinsRequest) models.PaginatedCoinsResponse); ok { r0 = rf(ctx, req) } else { r0 = ret.Get(0).(models.PaginatedCoinsResponse) } - if rf, ok := ret.Get(1).(func(context.Context, models.SuiXGetAllCoinsRequest) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, models.SuiXGetCoinsRequest) error); ok { r1 = rf(ctx, req) } else { r1 = ret.Error(1) diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index 6b156f7475..c5160e9129 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -24,7 +24,7 @@ type suiClient interface { SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) SuiXQueryEvents(ctx context.Context, req models.SuiXQueryEventsRequest) (models.PaginatedEventsResponse, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) - SuiXGetAllCoins(ctx context.Context, req models.SuiXGetAllCoinsRequest) (models.PaginatedCoinsResponse, error) + SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) SuiGetTransactionBlock( ctx context.Context, req models.SuiGetTransactionBlockRequest, From ee2268c5fc003bc7741ca6f7cbafbe413eb3ccb6 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 14:19:29 -0500 Subject: [PATCH 24/49] improve naming and add additional sui contract pkg unit test --- pkg/contracts/sui/gateway_test.go | 16 ++++++++++++++ pkg/contracts/sui/ptb_argument.go | 4 ++-- pkg/contracts/sui/ptb_argument_test.go | 4 ++-- .../sui/withdraw_and_call_ptb_test.go | 22 +++++++++---------- .../chains/sui/signer/withdraw_and_call.go | 4 ++-- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index f7d1d0cc9f..42dfd24703 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -377,6 +377,22 @@ func Test_ParseOutboundEvent(t *testing.T) { }, }, }, + { + name: "withdrawAndCall with PTB", + response: createPTBResponse(txHash, packageID, "200", "123"), + wantEvent: Event{ + TxHash: txHash, + EventIndex: 0, + EventType: WithdrawAndCallPTBEvent, + content: WithdrawAndCallPTB{ + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + Amount: math.NewUint(200), + Nonce: 123, + }, + }, + }, { name: "cancelTx", response: models.SuiTransactionBlockResponse{ diff --git a/pkg/contracts/sui/ptb_argument.go b/pkg/contracts/sui/ptb_argument.go index 6eefae9980..43729e94d0 100644 --- a/pkg/contracts/sui/ptb_argument.go +++ b/pkg/contracts/sui/ptb_argument.go @@ -28,7 +28,7 @@ func PureUint64FromString( return arg, value, nil } -// ParseTypeTagFromStr parses a PTB type argument StructTag from a type string +// TypeTagFromString creates a PTB type argument StructTag from a type string // Example: "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" -> // // &sui.StructTag{ @@ -36,7 +36,7 @@ func PureUint64FromString( // Module: "sui", // Name: "SUI", // } -func ParseTypeTagFromString(t string) (*sui.StructTag, error) { +func TypeTagFromString(t string) (*sui.StructTag, error) { parts := strings.Split(t, TypeSeparator) if len(parts) != 3 { return nil, fmt.Errorf("invalid type string: %s", t) diff --git a/pkg/contracts/sui/ptb_argument_test.go b/pkg/contracts/sui/ptb_argument_test.go index 2798797014..d368b6bff0 100644 --- a/pkg/contracts/sui/ptb_argument_test.go +++ b/pkg/contracts/sui/ptb_argument_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseTypeTagFromString(t *testing.T) { +func Test_TypeTagFromString(t *testing.T) { suiAddr, err := sui.AddressFromHex("0000000000000000000000000000000000000000000000000000000000000002") require.NoError(t, err) @@ -55,7 +55,7 @@ func TestParseTypeTagFromString(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := ParseTypeTagFromString(string(test.coinType)) + got, err := TypeTagFromString(string(test.coinType)) if test.errMsg != "" { require.Error(t, err) require.ErrorContains(t, err, test.errMsg) diff --git a/pkg/contracts/sui/withdraw_and_call_ptb_test.go b/pkg/contracts/sui/withdraw_and_call_ptb_test.go index c3379ac460..27ee31d87a 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb_test.go +++ b/pkg/contracts/sui/withdraw_and_call_ptb_test.go @@ -107,7 +107,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { }{ { name: "valid transaction block", - response: createValidTxResponse(txHash, packageID, amountStr, nonceStr), + response: createPTBResponse(txHash, packageID, amountStr, nonceStr), want: WithdrawAndCallPTB{ PackageID: packageID, Module: moduleName, @@ -119,7 +119,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid number of commands", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) res.Transaction.Data.Transaction.Transactions = res.Transaction.Data.Transaction.Transactions[:2] return res }(), @@ -128,7 +128,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid number of inputs", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) res.Transaction.Data.Transaction.Inputs = res.Transaction.Data.Transaction.Inputs[:4] return res }(), @@ -137,7 +137,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "unable to parse withdraw_impl", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) res.Transaction.Data.Transaction.Transactions[0] = "invalid" return res }(), @@ -146,7 +146,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid package ID", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) moveCall["package"] = "wrong_package" return res @@ -156,7 +156,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid module name", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) moveCall["module"] = "wrong_module" return res @@ -166,7 +166,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid function name", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) moveCall["function"] = "wrong_function" return res @@ -176,7 +176,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid argument indexes", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) moveCall := res.Transaction.Data.Transaction.Transactions[0].(map[string]any)["MoveCall"].(map[string]any) arguments := moveCall["arguments"].([]any) arguments[0] = map[string]any{"Input": float64(5)} // Change index to make it invalid @@ -187,7 +187,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid amount format", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) res.Transaction.Data.Transaction.Inputs[1] = models.SuiCallArg{ "value": "invalid_number", } @@ -198,7 +198,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { { name: "invalid nonce format", response: func() models.SuiTransactionBlockResponse { - res := createValidTxResponse(txHash, packageID, amountStr, nonceStr) + res := createPTBResponse(txHash, packageID, amountStr, nonceStr) res.Transaction.Data.Transaction.Inputs[2] = models.SuiCallArg{ "value": "invalid_nonce", } @@ -228,7 +228,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { } } -func createValidTxResponse(txHash, packageID, amount, nonce string) models.SuiTransactionBlockResponse { +func createPTBResponse(txHash, packageID, amount, nonce string) models.SuiTransactionBlockResponse { return models.SuiTransactionBlockResponse{ Digest: txHash, Transaction: models.SuiTransactionBlock{ diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index eab1c37b00..3bec800ea1 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -50,7 +50,7 @@ func withdrawAndCallPTB( return tx, errors.Wrapf(err, "failed to parse package ID %s", gatewayPackageIDStr) } - coinType, err := zetasui.ParseTypeTagFromString(coinTypeStr) + coinType, err := zetasui.TypeTagFromString(coinTypeStr) if err != nil { return tx, errors.Wrapf(err, "failed to parse coin type %s", coinTypeStr) } @@ -146,7 +146,7 @@ func withdrawAndCallPTB( onCallTypeArgs := make([]sui.TypeTag, 0, len(cp.TypeArgs)+1) onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: coinType}) for _, typeArg := range cp.TypeArgs { - typeStruct, err := zetasui.ParseTypeTagFromString(typeArg) + typeStruct, err := zetasui.TypeTagFromString(typeArg) if err != nil { return tx, errors.Wrapf(err, "failed to parse type argument %s", typeArg) } From b047c7a5346169c2b743f46f319d2f9b89124353 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 17:41:22 -0500 Subject: [PATCH 25/49] add unit test to ptb tx builder; split ptb tx builder into small functions; move pure RPC method to sui client package --- .../sui/withdraw_and_call_ptb_test.go | 2 +- testutil/sample/crypto.go | 10 + zetaclient/chains/sui/client/client.go | 53 ++++ zetaclient/chains/sui/signer/signer.go | 3 +- zetaclient/chains/sui/signer/signer_tx.go | 5 +- .../chains/sui/signer/withdraw_and_call.go | 251 ++++++++++-------- .../sui/signer/withdraw_and_call_test.go | 248 +++++++++++++++++ zetaclient/testutils/mocks/sui_client.go | 28 -- zetaclient/testutils/mocks/sui_gen.go | 1 - 9 files changed, 459 insertions(+), 142 deletions(-) create mode 100644 zetaclient/chains/sui/signer/withdraw_and_call_test.go diff --git a/pkg/contracts/sui/withdraw_and_call_ptb_test.go b/pkg/contracts/sui/withdraw_and_call_ptb_test.go index 27ee31d87a..90fd929dea 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb_test.go +++ b/pkg/contracts/sui/withdraw_and_call_ptb_test.go @@ -212,7 +212,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { t.Run(test.name, func(t *testing.T) { event, content, err := gw.parseWithdrawAndCallPTB(test.response) if test.errMsg != "" { - require.Contains(t, err.Error(), test.errMsg) + require.ErrorContains(t, err, test.errMsg) return } diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index b72037b914..18e90d84d0 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -23,6 +23,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/gagliardetto/solana-go" + "github.com/mr-tron/base58" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/cosmos" @@ -175,6 +176,15 @@ func SuiAddress(t require.TestingT) string { return acc.Address } +// SuiDigest returns a sample sui digest +func SuiDigest(t *testing.T) string { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + require.NoError(t, err) + + return base58.Encode(randomBytes) +} + // Hash returns a sample hash func Hash() ethcommon.Hash { return ethcommon.BytesToHash(EthAddress().Bytes()) diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 1e597d3cc1..4e12ad135a 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -10,7 +10,10 @@ import ( "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/sui" + patsui "github.com/pattonkan/sui-go/sui" "github.com/pkg/errors" + + zetasui "github.com/zeta-chain/node/pkg/contracts/sui" ) // Client Sui client. @@ -247,6 +250,56 @@ func (c *Client) SuiExecuteTransactionBlock( return parseRPCResponse[models.SuiTransactionBlockResponse]([]byte(resString)) } +// GetSuiCoinObjectRef returns the latest SUI coin object reference for given owner address +// Note: the SUI object may change over time, so we need to get the latest object +func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) { + coins, err := c.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ + Owner: owner, + CoinType: string(zetasui.SUI), + }) + if err != nil { + return patsui.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") + } + + var ( + suiCoin *models.CoinData + suiCoinVersion uint64 + ) + + // locate the latest version of SUI coin object of given owner + for _, coin := range coins.Data { + if !zetasui.IsSUICoinType(zetasui.CoinType(coin.CoinType)) { + continue + } + + version, _ := strconv.ParseUint(coin.Version, 10, 64) + if version > suiCoinVersion { + suiCoin = &coin + suiCoinVersion = version + } + } + if suiCoin == nil { + return patsui.ObjectRef{}, errors.New("SUI coin not found") + } + + // convert coin data to object ref + suiCoinID, err := patsui.ObjectIdFromHex(suiCoin.CoinObjectId) + if err != nil { + return patsui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin ID: %w", err) + } + + suiCoinDigest, err := patsui.NewBase58(suiCoin.Digest) + if err != nil { + return patsui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin digest: %w", err) + } + + return patsui.ObjectRef{ + ObjectId: suiCoinID, + Version: suiCoinVersion, + Digest: suiCoinDigest, + }, nil +} + // EncodeCursor encodes event ID into cursor. func EncodeCursor(id models.EventId) string { return fmt.Sprintf("%s,%s", id.TxDigest, id.EventSeq) diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 308e19d3a2..4176aec656 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/block-vision/sui-go-sdk/models" + patsui "github.com/pattonkan/sui-go/sui" "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/bg" @@ -32,7 +33,7 @@ type RPC interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) - SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) + GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) SuiExecuteTransactionBlock( diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 2785e03f81..64e890c864 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -148,14 +148,15 @@ func (s *Signer) buildWithdrawAndCallTx( } // get latest TSS SUI coin object ref for gas payment - suiCoinObjRef, err := s.getTSSSuiCoinObjectRef(ctx) + suiCoinObjRef, err := s.client.GetSuiCoinObjectRef(ctx, s.TSS().PubKey().AddressSui()) if err != nil { return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") } // get all other object references: [gateway, withdrawCap, onCallObjects] - gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := s.getWithdrawAndCallObjectRefs( + gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := getWithdrawAndCallObjectRefs( ctx, + s.client, s.gateway.ObjectID(), withdrawCapIDStr, cp.ObjectIDs, diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 3bec800ea1..942233ae96 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -3,7 +3,6 @@ package signer import ( "context" "encoding/base64" - "fmt" "strconv" "github.com/block-vision/sui-go-sdk/models" @@ -39,22 +38,113 @@ func withdrawAndCallPTB( ) (tx models.TxnMetaData, err error) { ptb := suiptb.NewTransactionDataTransactionBuilder() - // Parse arguments + // Parse signer address signerAddr, err := sui.AddressFromHex(signerAddrStr) if err != nil { - return tx, errors.Wrapf(err, "failed to parse signer address %s", signerAddrStr) + return tx, errors.Wrapf(err, "invalid signer address %s", signerAddrStr) } + // Add withdraw_impl command and get its command index + gasBudgetUint, err := addPTBCmdWithdrawImpl( + ptb, + gatewayPackageIDStr, + gatewayModule, + gatewayObjRef, + withdrawCapObjRef, + coinTypeStr, + amountStr, + nonceStr, + gasBudgetStr, + ) + if err != nil { + return tx, err + } + + // Create arguments to access the two results from the withdraw_impl call + cmdIndex := uint16(0) + argWithdrawnCoins := suiptb.Argument{ + NestedResult: &suiptb.NestedResult{ + Cmd: cmdIndex, + Result: 0, // First result (main coins) + }, + } + + argBudgetCoins := suiptb.Argument{ + NestedResult: &suiptb.NestedResult{ + Cmd: cmdIndex, + Result: 1, // Second result (budget coins) + }, + } + + // Add gas budget transfer command + err = addPTBCmdGasBudgetTransfer(ptb, argBudgetCoins, *signerAddr) + if err != nil { + return tx, err + } + + // Add on_call command + err = addPTBCmdOnCall( + ptb, + receiver, + coinTypeStr, + argWithdrawnCoins, + onCallObjectRefs, + cp, + ) + if err != nil { + return tx, err + } + + // Finish building the PTB + pt := ptb.Finish() + + // Wrap the PTB into a transaction data + txData := suiptb.NewTransactionData( + signerAddr, + pt, + []*sui.ObjectRef{ + &suiCoinObjRef, + }, + gasBudgetUint, + suiclient.DefaultGasPrice, + ) + + txBytes, err := bcs.Marshal(txData) + if err != nil { + return tx, errors.Wrapf(err, "failed to marshal transaction data: %v", txData) + } + + // Encode the transaction bytes to base64 + return models.TxnMetaData{ + TxBytes: base64.StdEncoding.EncodeToString(txBytes), + }, nil +} + +// addPTBCmdWithdrawImpl adds the withdraw_impl command to the PTB and returns the gas budget value +func addPTBCmdWithdrawImpl( + ptb *suiptb.ProgrammableTransactionBuilder, + gatewayPackageIDStr string, + gatewayModule string, + gatewayObjRef sui.ObjectRef, + withdrawCapObjRef sui.ObjectRef, + coinTypeStr string, + amountStr string, + nonceStr string, + gasBudgetStr string, +) (uint64, error) { + // Parse gateway package ID gatewayPackageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) if err != nil { - return tx, errors.Wrapf(err, "failed to parse package ID %s", gatewayPackageIDStr) + return 0, errors.Wrapf(err, "invalid gateway package ID %s", gatewayPackageIDStr) } + // Parse coin type coinType, err := zetasui.TypeTagFromString(coinTypeStr) if err != nil { - return tx, errors.Wrapf(err, "failed to parse coin type %s", coinTypeStr) + return 0, errors.Wrapf(err, "invalid coin type %s", coinTypeStr) } + // Create gateway object argument argGatewayObject, err := ptb.Obj(suiptb.ObjectArg{ SharedObject: &suiptb.SharedObjectArg{ Id: gatewayObjRef.ObjectId, @@ -63,32 +153,35 @@ func withdrawAndCallPTB( }, }) if err != nil { - return tx, errors.Wrap(err, "failed to create gateway object argument") + return 0, errors.Wrap(err, "unable to create gateway object argument") } + // Create amount argument argAmount, _, err := zetasui.PureUint64FromString(ptb, amountStr) if err != nil { - return tx, errors.Wrapf(err, "failed to create amount argument") + return 0, errors.Wrapf(err, "unable to create amount argument") } + // Create nonce argument argNonce, _, err := zetasui.PureUint64FromString(ptb, nonceStr) if err != nil { - return tx, errors.Wrapf(err, "failed to create nonce argument") + return 0, errors.Wrapf(err, "unable to create nonce argument") } + // Create gas budget argument argGasBudget, gasBudgetUint, err := zetasui.PureUint64FromString(ptb, gasBudgetStr) if err != nil { - return tx, errors.Wrapf(err, "failed to create gas budget argument") + return 0, errors.Wrapf(err, "unable to create gas budget argument") } + // Create withdraw cap argument argWithdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) if err != nil { - return tx, errors.Wrapf(err, "failed to create withdraw cap object argument") + return 0, errors.Wrapf(err, "unable to create withdraw cap object argument") } - // Move call for withdraw_impl and get its command index (0) + // add Move call for withdraw_impl // #nosec G115 always in range - cmdIndex := uint16(len(ptb.Commands)) ptb.Command(suiptb.Command{ MoveCall: &suiptb.ProgrammableMoveCall{ Package: gatewayPackageID, @@ -107,25 +200,19 @@ func withdrawAndCallPTB( }, }) - // Create arguments to access the two results from the withdraw_impl call - argWithdrawnCoins := suiptb.Argument{ - NestedResult: &suiptb.NestedResult{ - Cmd: cmdIndex, - Result: 0, // First result (main coins) - }, - } - - argBudgetCoins := suiptb.Argument{ - NestedResult: &suiptb.NestedResult{ - Cmd: cmdIndex, - Result: 1, // Second result (budget coins) - }, - } + return gasBudgetUint, nil +} - // Transfer gas budget coins to the TSS address +// addPTBCmdGasBudgetTransfer adds the gas budget transfer command to the PTB +func addPTBCmdGasBudgetTransfer( + ptb *suiptb.ProgrammableTransactionBuilder, + argBudgetCoins suiptb.Argument, + signerAddr sui.Address, +) error { + // create TSS address argument argTSSAddr, err := ptb.Pure(signerAddr) if err != nil { - return tx, errors.Wrapf(err, "failed to create tss address argument") + return errors.Wrapf(err, "unable to create tss address argument") } ptb.Command(suiptb.Command{ @@ -135,11 +222,28 @@ func withdrawAndCallPTB( }, }) - // Extract argument for on_call - // The receiver in the cctx is used as target package ID + return nil +} + +// addPTBCmdOnCall adds the on_call command to the PTB +func addPTBCmdOnCall( + ptb *suiptb.ProgrammableTransactionBuilder, + receiver string, + coinTypeStr string, + argWithdrawnCoins suiptb.Argument, + onCallObjectRefs []sui.ObjectRef, + cp zetasui.CallPayload, +) error { + // Parse target package ID targetPackageID, err := sui.PackageIdFromHex(receiver) if err != nil { - return tx, errors.Wrapf(err, "failed to parse target package ID %s", receiver) + return errors.Wrapf(err, "invalid target package ID %s", receiver) + } + + // Parse coin type + coinType, err := zetasui.TypeTagFromString(coinTypeStr) + if err != nil { + return errors.Wrapf(err, "invalid coin type %s", coinTypeStr) } // Build the type arguments for on_call in order: [withdrawn coin type, ... payload type arguments] @@ -148,7 +252,7 @@ func withdrawAndCallPTB( for _, typeArg := range cp.TypeArgs { typeStruct, err := zetasui.TypeTagFromString(typeArg) if err != nil { - return tx, errors.Wrapf(err, "failed to parse type argument %s", typeArg) + return errors.Wrapf(err, "invalid type argument %s", typeArg) } onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: typeStruct}) } @@ -167,7 +271,7 @@ func withdrawAndCallPTB( }, }) if err != nil { - return tx, errors.Wrapf(err, "failed to create object argument: %v", onCallObjectRef) + return errors.Wrapf(err, "unable to create object argument: %v", onCallObjectRef) } onCallArgs = append(onCallArgs, objectArg) } @@ -175,7 +279,7 @@ func withdrawAndCallPTB( // Add any additional message arguments messageArg, err := ptb.Pure(cp.Message) if err != nil { - return tx, errors.Wrapf(err, "failed to create message argument: %x", cp.Message) + return errors.Wrapf(err, "unable to create message argument: %x", cp.Message) } onCallArgs = append(onCallArgs, messageArg) @@ -190,43 +294,22 @@ func withdrawAndCallPTB( }, }) - // Finish building the PTB - pt := ptb.Finish() - - // Get the signer address - txData := suiptb.NewTransactionData( - signerAddr, - pt, - []*sui.ObjectRef{ - &suiCoinObjRef, - }, - gasBudgetUint, - suiclient.DefaultGasPrice, - ) - - txBytes, err := bcs.Marshal(txData) - if err != nil { - return tx, errors.Wrapf(err, "failed to marshal transaction data: %v", txData) - } - - // Encode the transaction bytes to base64 - return models.TxnMetaData{ - TxBytes: base64.StdEncoding.EncodeToString(txBytes), - }, nil + return nil } // getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call // - Initial shared version will be used for shared objects // - Current version will be used for non-shared objects, e.g. withdraw cap -func (s *Signer) getWithdrawAndCallObjectRefs( +func getWithdrawAndCallObjectRefs( ctx context.Context, + rpc RPC, gatewayID, withdrawCapID string, onCallObjectIDs []string, ) (gatewayObjRef, withdrawCapObjRef sui.ObjectRef, onCallObjectRefs []sui.ObjectRef, err error) { objectIDs := append([]string{gatewayID, withdrawCapID}, onCallObjectIDs...) // query objects in batch - suiObjects, err := s.client.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ + suiObjects, err := rpc.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ ObjectIds: objectIDs, Options: models.SuiObjectDataOptions{ // show owner info in order to retrieve object initial shared version @@ -290,53 +373,3 @@ func (s *Signer) getWithdrawAndCallObjectRefs( return objectRefs[0], objectRefs[1], objectRefs[2:], nil } - -// getTSSSuiCoinObjectRef returns the latest SUI coin object reference for the TSS address -// Note: the SUI object may change over time, so we need to get the latest object -func (s *Signer) getTSSSuiCoinObjectRef(ctx context.Context) (sui.ObjectRef, error) { - coins, err := s.client.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ - Owner: s.TSS().PubKey().AddressSui(), - CoinType: string(zetasui.SUI), - }) - if err != nil { - return sui.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") - } - - var ( - suiCoin *models.CoinData - suiCoinVersion uint64 - ) - - // locate the latest version of SUI coin object under TSS account - for _, coin := range coins.Data { - if !zetasui.IsSUICoinType(zetasui.CoinType(coin.CoinType)) { - continue - } - - version, _ := strconv.ParseUint(coin.Version, 10, 64) - if version > suiCoinVersion { - suiCoin = &coin - suiCoinVersion = version - } - } - if suiCoin == nil { - return sui.ObjectRef{}, errors.New("SUI coin not found") - } - - // convert coin data to object ref - suiCoinID, err := sui.ObjectIdFromHex(suiCoin.CoinObjectId) - if err != nil { - return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin ID: %w", err) - } - - suiCoinDigest, err := sui.NewBase58(suiCoin.Digest) - if err != nil { - return sui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin digest: %w", err) - } - - return sui.ObjectRef{ - ObjectId: suiCoinID, - Version: suiCoinVersion, - Digest: suiCoinDigest, - }, nil -} diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go new file mode 100644 index 0000000000..1f8bfb98ff --- /dev/null +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -0,0 +1,248 @@ +package signer + +import ( + "testing" + + "github.com/pattonkan/sui-go/sui" + "github.com/stretchr/testify/require" + zetasui "github.com/zeta-chain/node/pkg/contracts/sui" + "github.com/zeta-chain/node/testutil/sample" +) + +// testPTBArgs holds all the arguments needed for withdrawAndCallPTB +type testPTBArgs struct { + signerAddrStr string + gatewayPackageIDStr string + gatewayModule string + gatewayObjRef sui.ObjectRef + suiCoinObjRef sui.ObjectRef + withdrawCapObjRef sui.ObjectRef + onCallObjectRefs []sui.ObjectRef + coinTypeStr string + amountStr string + nonceStr string + gasBudgetStr string + receiver string + cp zetasui.CallPayload +} + +// newTestPTBArgs creates a testArgs struct with default values +func newTestPTBArgs( + t *testing.T, + gatewayObjRef, suiCoinObjRef, withdrawCapObjRef sui.ObjectRef, + onCallObjectRefs []sui.ObjectRef, +) testPTBArgs { + return testPTBArgs{ + signerAddrStr: sample.SuiAddress(t), + gatewayPackageIDStr: sample.SuiAddress(t), + gatewayModule: "gateway", + gatewayObjRef: gatewayObjRef, + suiCoinObjRef: suiCoinObjRef, + withdrawCapObjRef: withdrawCapObjRef, + onCallObjectRefs: onCallObjectRefs, + coinTypeStr: string(zetasui.SUI), + amountStr: "1000000", + nonceStr: "1", + gasBudgetStr: "2000000", + receiver: sample.SuiAddress(t), + cp: zetasui.CallPayload{ + TypeArgs: []string{string(zetasui.SUI)}, + ObjectIDs: []string{sample.SuiAddress(t)}, + Message: []byte("test message"), + }, + } +} + +func Test_withdrawAndCallPTB(t *testing.T) { + // create test objects references + gatewayObjRef := sampleObjectRef(t) + suiCoinObjRef := sampleObjectRef(t) + withdrawCapObjRef := sampleObjectRef(t) + onCallObjRef := sampleObjectRef(t) + + tests := []struct { + name string + args testPTBArgs + errMsg string + }{ + { + name: "successful withdraw and call", + args: newTestPTBArgs(t, gatewayObjRef, suiCoinObjRef, withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}), + }, + { + name: "successful withdraw and call with empty payload", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.cp.Message = []byte{} + return args + }(), + }, + { + name: "invalid signer address", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.signerAddrStr = "invalid_address" + return args + }(), + errMsg: "invalid signer address", + }, + { + name: "invalid gateway package ID", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.gatewayPackageIDStr = "invalid_package_id" + return args + }(), + errMsg: "invalid gateway package ID", + }, + { + name: "invalid coin type", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.coinTypeStr = "invalid_coin_type" + return args + }(), + errMsg: "invalid coin type", + }, + { + name: "unable to create amount argument", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.amountStr = "invalid_amount" + return args + }(), + errMsg: "unable to create amount argument", + }, + { + name: "unable to create nonce argument", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.nonceStr = "invalid_nonce" + return args + }(), + errMsg: "unable to create nonce argument", + }, + { + name: "unable to create gas budget argument", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.gasBudgetStr = "invalid_gas_budget" + return args + }(), + errMsg: "unable to create gas budget argument", + }, + { + name: "invalid target package ID", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.receiver = "invalid_target_package_id" + return args + }(), + errMsg: "invalid target package ID", + }, + { + name: "invalid type argument", + args: func() testPTBArgs { + args := newTestPTBArgs( + t, + gatewayObjRef, + suiCoinObjRef, + withdrawCapObjRef, + []sui.ObjectRef{onCallObjRef}, + ) + args.cp.TypeArgs[0] = "invalid_type_argument" + return args + }(), + errMsg: "invalid type argument", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := withdrawAndCallPTB( + tt.args.signerAddrStr, + tt.args.gatewayPackageIDStr, + tt.args.gatewayModule, + tt.args.gatewayObjRef, + tt.args.suiCoinObjRef, + tt.args.withdrawCapObjRef, + tt.args.onCallObjectRefs, + tt.args.coinTypeStr, + tt.args.amountStr, + tt.args.nonceStr, + tt.args.gasBudgetStr, + tt.args.receiver, + tt.args.cp, + ) + + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + return + } + + require.NoError(t, err) + require.NotEmpty(t, got.TxBytes) + }) + } +} + +// sampleObjectRef creates a sample Sui object reference +func sampleObjectRef(t *testing.T) sui.ObjectRef { + objectID := sui.MustObjectIdFromHex(sample.SuiAddress(t)) + digest, err := sui.NewBase58(sample.SuiDigest(t)) + require.NoError(t, err) + + return sui.ObjectRef{ + ObjectId: objectID, + Version: 1, + Digest: digest, + } +} diff --git a/zetaclient/testutils/mocks/sui_client.go b/zetaclient/testutils/mocks/sui_client.go index da2e061601..3632a9eee9 100644 --- a/zetaclient/testutils/mocks/sui_client.go +++ b/zetaclient/testutils/mocks/sui_client.go @@ -282,34 +282,6 @@ func (_m *SuiClient) SuiMultiGetObjects(ctx context.Context, req models.SuiMulti return r0, r1 } -// SuiXGetCoins provides a mock function with given fields: ctx, req -func (_m *SuiClient) SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) { - ret := _m.Called(ctx, req) - - if len(ret) == 0 { - panic("no return value specified for SuiXGetCoins") - } - - var r0 models.PaginatedCoinsResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error)); ok { - return rf(ctx, req) - } - if rf, ok := ret.Get(0).(func(context.Context, models.SuiXGetCoinsRequest) models.PaginatedCoinsResponse); ok { - r0 = rf(ctx, req) - } else { - r0 = ret.Get(0).(models.PaginatedCoinsResponse) - } - - if rf, ok := ret.Get(1).(func(context.Context, models.SuiXGetCoinsRequest) error); ok { - r1 = rf(ctx, req) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // SuiXGetLatestSuiSystemState provides a mock function with given fields: ctx func (_m *SuiClient) SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) { ret := _m.Called(ctx) diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index c5160e9129..93143e6a7c 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -24,7 +24,6 @@ type suiClient interface { SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) SuiXQueryEvents(ctx context.Context, req models.SuiXQueryEventsRequest) (models.PaginatedEventsResponse, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) - SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) SuiGetTransactionBlock( ctx context.Context, req models.SuiGetTransactionBlockRequest, From 60642a29a3153ad69e1bafb9d59e57a370955863 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 19:15:45 -0500 Subject: [PATCH 26/49] fix unit test compile --- zetaclient/testutils/mocks/sui_client.go | 30 ++++++++++++++++++++++++ zetaclient/testutils/mocks/sui_gen.go | 2 ++ 2 files changed, 32 insertions(+) diff --git a/zetaclient/testutils/mocks/sui_client.go b/zetaclient/testutils/mocks/sui_client.go index 3632a9eee9..7f6617266f 100644 --- a/zetaclient/testutils/mocks/sui_client.go +++ b/zetaclient/testutils/mocks/sui_client.go @@ -11,6 +11,8 @@ import ( models "github.com/block-vision/sui-go-sdk/models" + sui "github.com/pattonkan/sui-go/sui" + time "time" ) @@ -75,6 +77,34 @@ func (_m *SuiClient) GetOwnedObjectID(ctx context.Context, ownerAddress string, return r0, r1 } +// GetSuiCoinObjectRef provides a mock function with given fields: ctx, owner +func (_m *SuiClient) GetSuiCoinObjectRef(ctx context.Context, owner string) (sui.ObjectRef, error) { + ret := _m.Called(ctx, owner) + + if len(ret) == 0 { + panic("no return value specified for GetSuiCoinObjectRef") + } + + var r0 sui.ObjectRef + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (sui.ObjectRef, error)); ok { + return rf(ctx, owner) + } + if rf, ok := ret.Get(0).(func(context.Context, string) sui.ObjectRef); ok { + r0 = rf(ctx, owner) + } else { + r0 = ret.Get(0).(sui.ObjectRef) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, owner) + } 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) diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index 93143e6a7c..20f87edff5 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -5,6 +5,7 @@ import ( time "time" models "github.com/block-vision/sui-go-sdk/models" + patsui "github.com/pattonkan/sui-go/sui" "github.com/zeta-chain/node/zetaclient/chains/sui/client" ) @@ -19,6 +20,7 @@ type suiClient interface { 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) + GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) From c032afa2f3cb978b33c18e513ba3620464d9f3e7 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 22 Apr 2025 20:42:31 -0500 Subject: [PATCH 27/49] add withdraw and call signer test --- .../sui/signer/withdraw_and_call_test.go | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 1f8bfb98ff..d894e14c2f 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -1,9 +1,12 @@ package signer import ( + "context" "testing" + "github.com/block-vision/sui-go-sdk/models" "github.com/pattonkan/sui-go/sui" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" zetasui "github.com/zeta-chain/node/pkg/contracts/sui" "github.com/zeta-chain/node/testutil/sample" @@ -234,6 +237,185 @@ func Test_withdrawAndCallPTB(t *testing.T) { } } +func Test_getWithdrawAndCallObjectRefs(t *testing.T) { + // create test objects references + gatewayID, err := sui.ObjectIdFromHex(sample.SuiAddress(t)) + require.NoError(t, err) + withdrawCapID, err := sui.ObjectIdFromHex(sample.SuiAddress(t)) + require.NoError(t, err) + onCallObjectID, err := sui.ObjectIdFromHex(sample.SuiAddress(t)) + require.NoError(t, err) + + // create test object digests + digest1, err := sui.NewBase58(sample.SuiDigest(t)) + require.NoError(t, err) + digest2, err := sui.NewBase58(sample.SuiDigest(t)) + require.NoError(t, err) + digest3, err := sui.NewBase58(sample.SuiDigest(t)) + require.NoError(t, err) + + tests := []struct { + name string + gatewayID string + withdrawCapID string + onCallObjectIDs []string + mockObjects []*models.SuiObjectResponse + mockError error + expected []sui.ObjectRef + errMsg string + }{ + { + name: "successful get object refs", + gatewayID: gatewayID.String(), + withdrawCapID: withdrawCapID.String(), + onCallObjectIDs: []string{onCallObjectID.String()}, + mockObjects: []*models.SuiObjectResponse{ + { + Data: &models.SuiObjectData{ + ObjectId: gatewayID.String(), + Version: "3", + Digest: digest1.String(), + Owner: map[string]any{ + "Shared": map[string]any{ + "initial_shared_version": float64(1), + }, + }, + }, + }, + { + Data: &models.SuiObjectData{ + ObjectId: withdrawCapID.String(), + Version: "2", + Digest: digest2.String(), + }, + }, + { + Data: &models.SuiObjectData{ + ObjectId: onCallObjectID.String(), + Version: "3", + Digest: digest3.String(), + Owner: map[string]any{ + "Shared": map[string]any{ + "initial_shared_version": float64(1), + }, + }, + }, + }, + }, + expected: []sui.ObjectRef{ + { + ObjectId: gatewayID, + Version: 1, + Digest: digest1, + }, + { + ObjectId: withdrawCapID, + Version: 2, + Digest: digest2, + }, + { + ObjectId: onCallObjectID, + Version: 1, + Digest: digest3, + }, + }, + }, + { + name: "rpc call fails", + gatewayID: gatewayID.String(), + withdrawCapID: withdrawCapID.String(), + onCallObjectIDs: []string{onCallObjectID.String()}, + mockError: sample.ErrSample, + errMsg: "failed to get SUI objects", + }, + { + name: "invalid object ID", + gatewayID: gatewayID.String(), + withdrawCapID: withdrawCapID.String(), + onCallObjectIDs: []string{onCallObjectID.String()}, + mockObjects: []*models.SuiObjectResponse{ + { + Data: &models.SuiObjectData{ + ObjectId: "invalid_id", + Version: "1", + Digest: digest1.String(), + }, + }, + }, + errMsg: "failed to parse object ID", + }, + { + name: "invalid object version", + gatewayID: gatewayID.String(), + withdrawCapID: withdrawCapID.String(), + onCallObjectIDs: []string{onCallObjectID.String()}, + mockObjects: []*models.SuiObjectResponse{ + { + Data: &models.SuiObjectData{ + ObjectId: gatewayID.String(), + Version: "invalid_version", + Digest: digest1.String(), + }, + }, + }, + errMsg: "failed to parse object version", + }, + { + name: "invalid initial shared version", + gatewayID: gatewayID.String(), + withdrawCapID: withdrawCapID.String(), + onCallObjectIDs: []string{onCallObjectID.String()}, + mockObjects: []*models.SuiObjectResponse{ + { + Data: &models.SuiObjectData{ + ObjectId: gatewayID.String(), + Version: "1", + Digest: digest1.String(), + Owner: map[string]any{ + "Shared": map[string]any{ + "initial_shared_version": "invalid_version", + }, + }, + }, + }, + }, + errMsg: "failed to extract initial shared version", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // setup mock + ctx := context.Background() + ts.SuiMock.On("SuiMultiGetObjects", ctx, mock.Anything).Return(tt.mockObjects, tt.mockError) + + // ACT + gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := getWithdrawAndCallObjectRefs( + ctx, + ts.SuiMock, + tt.gatewayID, + tt.withdrawCapID, + tt.onCallObjectIDs, + ) + + // ASSERT + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + return + } + + require.NoError(t, err) + require.EqualValues(t, tt.expected[0], gatewayObjRef) + require.EqualValues(t, tt.expected[1], withdrawCapObjRef) + require.EqualValues(t, tt.expected[2:], onCallObjectRefs) + require.Len(t, onCallObjectRefs, len(tt.onCallObjectIDs)) + }) + } +} + // sampleObjectRef creates a sample Sui object reference func sampleObjectRef(t *testing.T) sui.ObjectRef { objectID := sui.MustObjectIdFromHex(sample.SuiAddress(t)) From b77955d27adfcdfa82f5a14a04a982bdd8c8c71e Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 11:40:22 -0500 Subject: [PATCH 28/49] use a struct to hold the config of Sui example package; a name revert --- cmd/zetae2e/config/config.go | 7 +----- cmd/zetae2e/config/contracts.go | 21 ++++++------------ e2e/config/config.go | 17 +++++++++------ e2e/e2etests/test_sui_withdraw_and_call.go | 12 +++++------ e2e/runner/runner.go | 4 ++-- e2e/runner/setup_sui.go | 10 ++++++++- e2e/runner/sui.go | 25 ---------------------- zetaclient/chains/sui/signer/signer_tx.go | 10 ++++----- 8 files changed, 40 insertions(+), 66 deletions(-) diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 3093174530..93ba238af6 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -66,12 +66,7 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.Sui.FungibleTokenCoinType = config.DoubleQuotedString(r.SuiTokenCoinType) conf.Contracts.Sui.FungibleTokenTreasuryCap = config.DoubleQuotedString(r.SuiTokenTreasuryCap) - conf.Contracts.Sui.ExamplePackageID = config.DoubleQuotedString(r.SuiExample.PackageID) - conf.Contracts.Sui.ExampleTokenType = config.DoubleQuotedString(r.SuiExample.TokenType) - conf.Contracts.Sui.ExampleGlobalConfigID = config.DoubleQuotedString(r.SuiExample.GlobalConfigID) - conf.Contracts.Sui.ExamplePartnerID = config.DoubleQuotedString(r.SuiExample.PartnerID) - conf.Contracts.Sui.ExampleClockID = config.DoubleQuotedString(r.SuiExample.ClockID) - conf.Contracts.Sui.ExamplePoolID = config.DoubleQuotedString(r.SuiExample.PoolID) + conf.Contracts.Sui.Example = r.SuiExample conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(r.ZetaEthAddr.Hex()) conf.Contracts.EVM.ConnectorEthAddr = config.DoubleQuotedString(r.ConnectorEthAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 5312caa2c3..5cf108cd1a 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -130,24 +130,15 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { if suiPackageID != "" && suiGatewayID != "" { r.SuiGateway = sui.NewGateway(suiPackageID.String(), suiGatewayID.String()) } - - otherSuiFields := map[string]*string{ - conf.Contracts.Sui.FungibleTokenCoinType.String(): &r.SuiTokenCoinType, - conf.Contracts.Sui.FungibleTokenTreasuryCap.String(): &r.SuiTokenTreasuryCap, - conf.Contracts.Sui.ExamplePackageID.String(): &r.SuiExample.PackageID, - conf.Contracts.Sui.ExampleTokenType.String(): &r.SuiExample.TokenType, - conf.Contracts.Sui.ExampleGlobalConfigID.String(): &r.SuiExample.GlobalConfigID, - conf.Contracts.Sui.ExamplePartnerID.String(): &r.SuiExample.PartnerID, - conf.Contracts.Sui.ExampleClockID.String(): &r.SuiExample.ClockID, - conf.Contracts.Sui.ExamplePoolID.String(): &r.SuiExample.PoolID, + if c := conf.Contracts.Sui.FungibleTokenCoinType; c != "" { + r.SuiTokenCoinType = c.String() } - - for source, target := range otherSuiFields { - if source != "" { - *target = source - } + if c := conf.Contracts.Sui.FungibleTokenTreasuryCap; c != "" { + r.SuiTokenTreasuryCap = c.String() } + r.SuiExample = conf.Contracts.Sui.Example + // set EVM contracts evmChainID, err := r.EVMClient.ChainID(r.Ctx) require.NoError(r, err, "get evm chain ID") evmChainParams := chainParamsByChainID(chainParams, evmChainID.Int64()) diff --git a/e2e/config/config.go b/e2e/config/config.go index afd44560f3..5662fedd20 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -144,18 +144,23 @@ type TON struct { GatewayAccountID DoubleQuotedString `yaml:"gateway_account_id"` } +// SuiExample contains the object IDs in the example package +type SuiExample struct { + PackageID DoubleQuotedString `yaml:"package_id"` + TokenType DoubleQuotedString `yaml:"token_type"` + GlobalConfigID DoubleQuotedString `yaml:"global_config_id"` + PartnerID DoubleQuotedString `yaml:"partner_id"` + ClockID DoubleQuotedString `yaml:"clock_id"` + PoolID DoubleQuotedString `yaml:"pool_id"` +} + // 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"` - ExamplePackageID DoubleQuotedString `yaml:"example_package_id"` - ExampleTokenType DoubleQuotedString `yaml:"example_token_type"` - ExampleGlobalConfigID DoubleQuotedString `yaml:"example_global_config_id"` - ExamplePartnerID DoubleQuotedString `yaml:"example_partner_id"` - ExampleClockID DoubleQuotedString `yaml:"example_clock_id"` - ExamplePoolID DoubleQuotedString `yaml:"example_pool_id"` + Example SuiExample `yaml:"example"` } // EVM contains the addresses of predeployed contracts on the EVM chain diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 7cc009b634..49c4b585d6 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -17,7 +17,7 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { // ARRANGE // Given target package ID (example package) and a SUI amount - targetPackageID := r.SuiExample.PackageID + targetPackageID := r.SuiExample.PackageID.String() amount := utils.ParseBigInt(r, args[0]) // Given example contract on_call function arguments @@ -25,13 +25,13 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { // TODO: use real contract // https://github.com/zeta-chain/node/issues/3742 argumentTypes := []string{ - r.SuiExample.TokenType, + r.SuiExample.TokenType.String(), } objects := []string{ - r.SuiExample.GlobalConfigID, - r.SuiExample.PoolID, - r.SuiExample.PartnerID, - r.SuiExample.ClockID, + r.SuiExample.GlobalConfigID.String(), + r.SuiExample.PoolID.String(), + r.SuiExample.PartnerID.String(), + r.SuiExample.ClockID.String(), } // create a random Sui address and use it for on_call payload message diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 4a03dcb72d..eae03218d1 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -133,8 +133,8 @@ type E2ERunner struct { // SuiTokenTreasuryCap is the treasury cap for the SUI token that allows minting, only using in local tests SuiTokenTreasuryCap string - // SuiExample is the example contract for Sui - SuiExample Example + // SuiExample contains the example package information for Sui + SuiExample config.SuiExample // contracts evm ZetaEthAddr ethcommon.Address diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index a389ecf949..76ae08935c 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/protocol-contracts/pkg/zrc20.sol" + "github.com/zeta-chain/node/e2e/config" suicontract "github.com/zeta-chain/node/e2e/contracts/sui" "github.com/zeta-chain/node/e2e/txserver" "github.com/zeta-chain/node/e2e/utils" @@ -168,7 +169,14 @@ func (r *E2ERunner) deploySuiExample() { poolID, ok := objectIDs[filterPoolType] require.True(r, ok, "pool object not found") - r.SuiExample = NewExample(packageID, globalConfigID, partnerID, clockID, poolID) + r.SuiExample = config.SuiExample{ + PackageID: config.DoubleQuotedString(packageID), + TokenType: config.DoubleQuotedString(packageID + "::token::TOKEN"), + GlobalConfigID: config.DoubleQuotedString(globalConfigID), + PartnerID: config.DoubleQuotedString(partnerID), + ClockID: config.DoubleQuotedString(clockID), + PoolID: config.DoubleQuotedString(poolID), + } } // deploySuiPackage is a helper function that deploys a package on Sui diff --git a/e2e/runner/sui.go b/e2e/runner/sui.go index b8185bad0a..eb34a09a4f 100644 --- a/e2e/runner/sui.go +++ b/e2e/runner/sui.go @@ -17,31 +17,6 @@ import ( "github.com/zeta-chain/node/pkg/contracts/sui" ) -// Example is the struct containing the object IDs of the Example package -type Example struct { - PackageID string - TokenType string - GlobalConfigID string - PartnerID string - ClockID string - PoolID string -} - -// NewExample creates a new Example struct -func NewExample(packageID, globalConfigID, partnerID, clockID, poolID string) Example { - // token type is the packageID + ::token::TOKEN - tokenType := packageID + "::token::TOKEN" - - return Example{ - PackageID: packageID, - TokenType: tokenType, - GlobalConfigID: globalConfigID, - PartnerID: partnerID, - ClockID: clockID, - PoolID: poolID, - } -} - // SuiGetSUIBalance returns the SUI balance of an address func (r *E2ERunner) SuiGetSUIBalance(addr string) uint64 { resp, err := r.Clients.Sui.SuiXGetBalance(r.Ctx, models.SuiXGetBalanceRequest{ diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 64e890c864..15b2f7f07c 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -88,17 +88,17 @@ func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx gasBudget := strconv.FormatUint(gasPrice*params.CallOptions.GasLimit, 10) // Retrieve withdraw cap ID - withdrawCapIDStr, err := s.getWithdrawCapIDCached(ctx) + withdrawCapID, err := s.getWithdrawCapIDCached(ctx) if err != nil { return tx, errors.Wrap(err, "unable to get withdraw cap ID") } // build tx depending on the type of transaction if cctx.IsWithdrawAndCall() { - return s.buildWithdrawAndCallTx(ctx, params, coinType, gasBudget, withdrawCapIDStr, cctx.RelayedMessage) + return s.buildWithdrawAndCallTx(ctx, params, coinType, gasBudget, withdrawCapID, cctx.RelayedMessage) } - return s.buildWithdrawTx(ctx, params, coinType, gasBudget, withdrawCapIDStr) + return s.buildWithdrawTx(ctx, params, coinType, gasBudget, withdrawCapID) } // buildWithdrawTx builds unsigned withdraw transaction @@ -133,7 +133,7 @@ func (s *Signer) buildWithdrawAndCallTx( params *cctypes.OutboundParams, coinType, gasBudget, - withdrawCapIDStr, + withdrawCapID, payload string, ) (models.TxnMetaData, error) { // decode and parse the payload to object the on_call arguments @@ -158,7 +158,7 @@ func (s *Signer) buildWithdrawAndCallTx( ctx, s.client, s.gateway.ObjectID(), - withdrawCapIDStr, + withdrawCapID, cp.ObjectIDs, ) if err != nil { From 06e75d6ac39e433ce871fdb060ffa07bdb6de601 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 13:23:06 -0500 Subject: [PATCH 29/49] use the r.chain.method pattern for SUI contract deployment related functions --- e2e/contracts/sui/bin.go | 6 +++--- e2e/runner/setup_sui.go | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/e2e/contracts/sui/bin.go b/e2e/contracts/sui/bin.go index b13d13948e..34b2b0be80 100644 --- a/e2e/contracts/sui/bin.go +++ b/e2e/contracts/sui/bin.go @@ -35,12 +35,12 @@ func EVMBytecodeBase64() string { return base64.StdEncoding.EncodeToString(evmBinary) } -// ExampleTokenBytecodeBase64 gets the token binary encoded as base64 for deployment -func ExampleTokenBytecodeBase64() string { +// ExampleFungibleTokenBytecodeBase64 gets the example package's fungible token binary encoded as base64 for deployment +func ExampleFungibleTokenBytecodeBase64() string { return base64.StdEncoding.EncodeToString(tokenBinary) } -// ExampleConnectedBytecodeBase64 gets the connected binary encoded as base64 for deployment +// ExampleConnectedBytecodeBase64 gets the example package's connected binary encoded as base64 for deployment func ExampleConnectedBytecodeBase64() string { return base64.StdEncoding.EncodeToString(connectedBinary) } diff --git a/e2e/runner/setup_sui.go b/e2e/runner/setup_sui.go index 76ae08935c..ab3fd614e1 100644 --- a/e2e/runner/setup_sui.go +++ b/e2e/runner/setup_sui.go @@ -50,17 +50,17 @@ func (r *E2ERunner) SetupSui(faucetURL string) { r.RequestSuiFromFaucet(faucetURL, r.SuiTSSAddress) // deploy gateway package - whitelistCapID, withdrawCapID := r.deploySUIGateway() + whitelistCapID, withdrawCapID := r.suiDeployGateway() // deploy SUI zrc20 r.deploySUIZRC20() // deploy fake USDC and whitelist it - fakeUSDCCoinType := r.deploySuiFakeUSDC() + fakeUSDCCoinType := r.suiDeployFakeUSDC() r.whitelistSuiFakeUSDC(deployerSigner, fakeUSDCCoinType, whitelistCapID) // deploy example contract with on_call function - r.deploySuiExample() + r.suiDeployExample() // send withdraw cap to TSS r.suiSendWithdrawCapToTSS(deployerSigner, withdrawCapID) @@ -70,8 +70,8 @@ func (r *E2ERunner) SetupSui(faucetURL string) { require.NoError(r, err) } -// deploySUIGateway deploys the SUI gateway package on Sui -func (r *E2ERunner) deploySUIGateway() (whitelistCapID, withdrawCapID string) { +// suiDeployGateway deploys the SUI gateway package on Sui +func (r *E2ERunner) suiDeployGateway() (whitelistCapID, withdrawCapID string) { const ( filterGatewayType = "gateway::Gateway" filterWithdrawCapType = "gateway::WithdrawCap" @@ -79,7 +79,7 @@ func (r *E2ERunner) deploySUIGateway() (whitelistCapID, withdrawCapID string) { ) objectTypeFilters := []string{filterGatewayType, filterWhitelistCapType, filterWithdrawCapType} - packageID, objectIDs := r.deploySuiPackage( + packageID, objectIDs := r.suiDeployPackage( []string{suicontract.GatewayBytecodeBase64(), suicontract.EVMBytecodeBase64()}, objectTypeFilters, ) @@ -121,10 +121,10 @@ func (r *E2ERunner) deploySUIZRC20() { r.SetupSUIZRC20() } -// deploySuiFakeUSDC deploys the FakeUSDC contract on Sui +// suiDeployFakeUSDC deploys the FakeUSDC contract on Sui // it returns the treasuryCap object ID that allows to mint tokens -func (r *E2ERunner) deploySuiFakeUSDC() string { - packageID, objectIDs := r.deploySuiPackage([]string{suicontract.FakeUSDCBytecodeBase64()}, []string{"TreasuryCap"}) +func (r *E2ERunner) suiDeployFakeUSDC() string { + packageID, objectIDs := r.suiDeployPackage([]string{suicontract.FakeUSDCBytecodeBase64()}, []string{"TreasuryCap"}) treasuryCap, ok := objectIDs["TreasuryCap"] require.True(r, ok, "treasuryCap not found") @@ -141,8 +141,8 @@ func (r *E2ERunner) deploySuiFakeUSDC() string { return coinType } -// deploySuiExample deploys the example package on Sui -func (r *E2ERunner) deploySuiExample() { +// suiDeployExample deploys the example package on Sui +func (r *E2ERunner) suiDeployExample() { const ( filterGlobalConfigType = "connected::GlobalConfig" filterPartnerType = "connected::Partner" @@ -151,8 +151,8 @@ func (r *E2ERunner) deploySuiExample() { ) objectTypeFilters := []string{filterGlobalConfigType, filterPartnerType, filterClockType, filterPoolType} - packageID, objectIDs := r.deploySuiPackage( - []string{suicontract.ExampleTokenBytecodeBase64(), suicontract.ExampleConnectedBytecodeBase64()}, + packageID, objectIDs := r.suiDeployPackage( + []string{suicontract.ExampleFungibleTokenBytecodeBase64(), suicontract.ExampleConnectedBytecodeBase64()}, objectTypeFilters, ) r.Logger.Info("deployed example package with packageID: %s", packageID) @@ -179,9 +179,9 @@ func (r *E2ERunner) deploySuiExample() { } } -// deploySuiPackage is a helper function that deploys a package on Sui +// suiDeployPackage is a helper function that deploys a package on Sui // It returns the packageID and a map of object types to their IDs -func (r *E2ERunner) deploySuiPackage(bytecodeBase64s []string, objectTypeFilters []string) (string, map[string]string) { +func (r *E2ERunner) suiDeployPackage(bytecodeBase64s []string, objectTypeFilters []string) (string, map[string]string) { client := r.Clients.Sui deployerSigner, err := r.Account.SuiSigner() From 65086b3cdda90751056a6a22f074014baaaacca7 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 14:15:11 -0500 Subject: [PATCH 30/49] use hardcoded address in the E2E test to receive withdawAndCall funds --- e2e/e2etests/test_sui_withdraw_and_call.go | 5 ++--- zetaclient/chains/sui/signer/signer_tx.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 49c4b585d6..05d27ab757 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -8,7 +8,6 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/contracts/sui" - "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) @@ -34,9 +33,9 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { r.SuiExample.ClockID.String(), } - // create a random Sui address and use it for on_call payload message + // define a deterministic address and use it for on_call payload message // the example contract will just forward the withdrawn SUI token to this address - suiAddress := sample.SuiAddress(r) + suiAddress := "0x34a30aaee833d649d7313ddfe4ff5b6a9bac48803236b919369e6636fe93392e" message, err := hex.DecodeString(suiAddress[2:]) // remove 0x prefix require.NoError(r, err) balanceBefore := r.SuiGetSUIBalance(suiAddress) diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 15b2f7f07c..55a9bfba9b 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -175,7 +175,7 @@ func (s *Signer) buildWithdrawAndCallTx( Str("gas_budget", gasBudget). Any("type_args", cp.TypeArgs). Any("object_ids", cp.ObjectIDs). - Hex("message", cp.Message). + Hex("payload", cp.Message). Msg("calling withdrawAndCallPTB") // TODO: check all object IDs are share object here From 0adb35add36c77339e7e1ede53f6c561a7dc6f45 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 14:35:38 -0500 Subject: [PATCH 31/49] add unit test for gateway pure method extractInteger --- pkg/contracts/sui/gateway.go | 2 +- pkg/contracts/sui/gateway_test.go | 117 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index 233e3d67b8..fe0e1d5288 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -326,7 +326,7 @@ func extractInteger[T constraints.Integer](kv map[string]any, key string) (T, er v, ok := rawValue.(float64) if !ok { - return 0, errors.Errorf("invalid %s", key) + return 0, errors.Errorf("want float64, got %T for %s", rawValue, key) } // #nosec G115 always in range diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index 42dfd24703..20ced7c8b1 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -472,3 +472,120 @@ func Test_ParseOutboundEvent(t *testing.T) { }) } } + +func Test_extractInteger(t *testing.T) { + tests := []struct { + name string + kv map[string]any + key string + want any + errMsg string + }{ + { + name: "valid int8", + kv: map[string]any{"key": float64(42)}, + key: "key", + want: int8(42), + }, + { + name: "valid int16", + kv: map[string]any{"key": float64(1000)}, + key: "key", + want: int16(1000), + }, + { + name: "valid int32", + kv: map[string]any{"key": float64(100000)}, + key: "key", + want: int32(100000), + }, + { + name: "valid int64", + kv: map[string]any{"key": float64(1000000000)}, + key: "key", + want: int64(1000000000), + }, + { + name: "valid uint8", + kv: map[string]any{"key": float64(42)}, + key: "key", + want: uint8(42), + }, + { + name: "valid uint16", + kv: map[string]any{"key": float64(1000)}, + key: "key", + want: uint16(1000), + }, + { + name: "valid uint32", + kv: map[string]any{"key": float64(100000)}, + key: "key", + want: uint32(100000), + }, + { + name: "valid uint64", + kv: map[string]any{"key": float64(1000000000)}, + key: "key", + want: uint64(1000000000), + }, + { + name: "missing key", + kv: map[string]any{}, + key: "key", + errMsg: "missing key", + }, + { + name: "invalid value type", + kv: map[string]any{"key": "not a number"}, + key: "key", + errMsg: "want float64, got string for key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch tt.name { + case "valid int8": + got, err := extractInteger[int8](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid int16": + got, err := extractInteger[int16](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid int32": + got, err := extractInteger[int32](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid int64": + got, err := extractInteger[int64](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid uint8": + got, err := extractInteger[uint8](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid uint16": + got, err := extractInteger[uint16](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid uint32": + got, err := extractInteger[uint32](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + case "valid uint64": + got, err := extractInteger[uint64](tt.kv, tt.key) + require.NoError(t, err) + require.Equal(t, tt.want, got) + default: + // Test error cases for all types + if tt.errMsg != "" { + // Test with int64 as an example + _, err := extractInteger[int64](tt.kv, tt.key) + require.ErrorContains(t, err, tt.errMsg) + } + } + }) + } +} From edc687f35edb4abe448647a6adbda8ed4699ddd0 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 15:24:52 -0500 Subject: [PATCH 32/49] return error if SUI coin object version is invalid --- zetaclient/chains/sui/client/client.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 4e12ad135a..287ce0b5d0 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -272,7 +272,11 @@ func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui. continue } - version, _ := strconv.ParseUint(coin.Version, 10, 64) + version, err := strconv.ParseUint(coin.Version, 10, 64) + if err != nil { + return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin version %s", coin.Version) + } + if version > suiCoinVersion { suiCoin = &coin suiCoinVersion = version From c61476e49da44dd81fc7926a0a2e3ecf5a19e1fe Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 15:45:46 -0500 Subject: [PATCH 33/49] use error wrapping; remove TODO --- cmd/zetae2e/local/local.go | 2 -- zetaclient/chains/sui/client/client.go | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 844a4414c8..1cfb4159ec 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -502,8 +502,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSuiTokenWithdrawName, e2etests.TestSuiDepositRestrictedName, e2etests.TestSuiWithdrawRestrictedName, - - // TODO: enable withdraw and call test // https://github.com/zeta-chain/node/issues/3742 e2etests.TestSuiWithdrawAndCallName, } diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 287ce0b5d0..6228833479 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -289,12 +289,12 @@ func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui. // convert coin data to object ref suiCoinID, err := patsui.ObjectIdFromHex(suiCoin.CoinObjectId) if err != nil { - return patsui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin ID: %w", err) + return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin ID: %s", suiCoin.CoinObjectId) } suiCoinDigest, err := patsui.NewBase58(suiCoin.Digest) if err != nil { - return patsui.ObjectRef{}, fmt.Errorf("failed to parse SUI coin digest: %w", err) + return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin digest: %s", suiCoin.Digest) } return patsui.ObjectRef{ From eb06d0aaaa81bc5fbce51dc5d4698c83bbf6bfb2 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 15:48:18 -0500 Subject: [PATCH 34/49] removed TODO in sui e2e withdraw and call test --- e2e/e2etests/test_sui_withdraw_and_call.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 05d27ab757..3381e87dbf 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -21,8 +21,6 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { // Given example contract on_call function arguments // sample withdrawAndCall payload - // TODO: use real contract - // https://github.com/zeta-chain/node/issues/3742 argumentTypes := []string{ r.SuiExample.TokenType.String(), } From 003d155c0d4d41ff7a80e43e697d2256ebc89bb9 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 15:58:43 -0500 Subject: [PATCH 35/49] clean redundant comment; use name suiptb for pattonkan sui go library; rename WithdrawAndCallPTBEvent as WithdrawAndCallEvent --- e2e/e2etests/test_sui_withdraw_and_call.go | 1 - pkg/contracts/sui/gateway.go | 2 +- pkg/contracts/sui/gateway_test.go | 2 +- ...w_and_call_ptb.go => withdraw_and_call.go} | 2 +- ..._ptb_test.go => withdraw_and_call_test.go} | 2 +- zetaclient/chains/sui/client/client.go | 20 +++++++++---------- zetaclient/chains/sui/signer/signer.go | 4 ++-- zetaclient/testutils/mocks/sui_gen.go | 4 ++-- 8 files changed, 18 insertions(+), 19 deletions(-) rename pkg/contracts/sui/{withdraw_and_call_ptb.go => withdraw_and_call.go} (99%) rename pkg/contracts/sui/{withdraw_and_call_ptb_test.go => withdraw_and_call_test.go} (99%) diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 3381e87dbf..5a2d796af7 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -20,7 +20,6 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { amount := utils.ParseBigInt(r, args[0]) // Given example contract on_call function arguments - // sample withdrawAndCall payload argumentTypes := []string{ r.SuiExample.TokenType.String(), } diff --git a/pkg/contracts/sui/gateway.go b/pkg/contracts/sui/gateway.go index fe0e1d5288..48fcf394d1 100644 --- a/pkg/contracts/sui/gateway.go +++ b/pkg/contracts/sui/gateway.go @@ -43,7 +43,7 @@ const ( WithdrawEvent EventType = "WithdrawEvent" // this event does not exist on gateway, we define it to make the outbound processing consistent - WithdrawAndCallPTBEvent EventType = "WithdrawAndCallPTBEvent" + WithdrawAndCallEvent EventType = "WithdrawAndCallEvent" // the gateway.move uses name "NonceIncreaseEvent", but here uses a more descriptive name CancelTxEvent EventType = "NonceIncreaseEvent" diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index 20ced7c8b1..f010600794 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -383,7 +383,7 @@ func Test_ParseOutboundEvent(t *testing.T) { wantEvent: Event{ TxHash: txHash, EventIndex: 0, - EventType: WithdrawAndCallPTBEvent, + EventType: WithdrawAndCallEvent, content: WithdrawAndCallPTB{ PackageID: packageID, Module: moduleName, diff --git a/pkg/contracts/sui/withdraw_and_call_ptb.go b/pkg/contracts/sui/withdraw_and_call.go similarity index 99% rename from pkg/contracts/sui/withdraw_and_call_ptb.go rename to pkg/contracts/sui/withdraw_and_call.go index 180cebbbf4..3465c12af0 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb.go +++ b/pkg/contracts/sui/withdraw_and_call.go @@ -157,7 +157,7 @@ func (gw *Gateway) parseWithdrawAndCallPTB( event = Event{ TxHash: res.Digest, EventIndex: 0, - EventType: WithdrawAndCallPTBEvent, + EventType: WithdrawAndCallEvent, content: content, } diff --git a/pkg/contracts/sui/withdraw_and_call_ptb_test.go b/pkg/contracts/sui/withdraw_and_call_test.go similarity index 99% rename from pkg/contracts/sui/withdraw_and_call_ptb_test.go rename to pkg/contracts/sui/withdraw_and_call_test.go index 90fd929dea..f87fb89826 100644 --- a/pkg/contracts/sui/withdraw_and_call_ptb_test.go +++ b/pkg/contracts/sui/withdraw_and_call_test.go @@ -219,7 +219,7 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { require.NoError(t, err) require.Equal(t, txHash, event.TxHash) require.Zero(t, event.EventIndex) - require.Equal(t, WithdrawAndCallPTBEvent, event.EventType) + require.Equal(t, WithdrawAndCallEvent, event.EventType) withdrawCallPTB, ok := content.(WithdrawAndCallPTB) require.True(t, ok) diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 6228833479..865085162b 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -10,7 +10,7 @@ import ( "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/sui" - patsui "github.com/pattonkan/sui-go/sui" + suiptb "github.com/pattonkan/sui-go/sui" "github.com/pkg/errors" zetasui "github.com/zeta-chain/node/pkg/contracts/sui" @@ -252,13 +252,13 @@ func (c *Client) SuiExecuteTransactionBlock( // GetSuiCoinObjectRef returns the latest SUI coin object reference for given owner address // Note: the SUI object may change over time, so we need to get the latest object -func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) { +func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (suiptb.ObjectRef, error) { coins, err := c.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ Owner: owner, CoinType: string(zetasui.SUI), }) if err != nil { - return patsui.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") + return suiptb.ObjectRef{}, errors.Wrap(err, "unable to get TSS coins") } var ( @@ -274,7 +274,7 @@ func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui. version, err := strconv.ParseUint(coin.Version, 10, 64) if err != nil { - return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin version %s", coin.Version) + return suiptb.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin version %s", coin.Version) } if version > suiCoinVersion { @@ -283,21 +283,21 @@ func (c *Client) GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui. } } if suiCoin == nil { - return patsui.ObjectRef{}, errors.New("SUI coin not found") + return suiptb.ObjectRef{}, errors.New("SUI coin not found") } // convert coin data to object ref - suiCoinID, err := patsui.ObjectIdFromHex(suiCoin.CoinObjectId) + suiCoinID, err := suiptb.ObjectIdFromHex(suiCoin.CoinObjectId) if err != nil { - return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin ID: %s", suiCoin.CoinObjectId) + return suiptb.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin ID: %s", suiCoin.CoinObjectId) } - suiCoinDigest, err := patsui.NewBase58(suiCoin.Digest) + suiCoinDigest, err := suiptb.NewBase58(suiCoin.Digest) if err != nil { - return patsui.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin digest: %s", suiCoin.Digest) + return suiptb.ObjectRef{}, errors.Wrapf(err, "failed to parse SUI coin digest: %s", suiCoin.Digest) } - return patsui.ObjectRef{ + return suiptb.ObjectRef{ ObjectId: suiCoinID, Version: suiCoinVersion, Digest: suiCoinDigest, diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 4176aec656..78a5001535 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/block-vision/sui-go-sdk/models" - patsui "github.com/pattonkan/sui-go/sui" + suiptb "github.com/pattonkan/sui-go/sui" "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/bg" @@ -33,7 +33,7 @@ type RPC interface { SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) SuiMultiGetObjects(ctx context.Context, req models.SuiMultiGetObjectsRequest) ([]*models.SuiObjectResponse, error) - GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) + GetSuiCoinObjectRef(ctx context.Context, owner string) (suiptb.ObjectRef, error) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) SuiExecuteTransactionBlock( diff --git a/zetaclient/testutils/mocks/sui_gen.go b/zetaclient/testutils/mocks/sui_gen.go index 20f87edff5..aa6ac25605 100644 --- a/zetaclient/testutils/mocks/sui_gen.go +++ b/zetaclient/testutils/mocks/sui_gen.go @@ -5,7 +5,7 @@ import ( time "time" models "github.com/block-vision/sui-go-sdk/models" - patsui "github.com/pattonkan/sui-go/sui" + suiptb "github.com/pattonkan/sui-go/sui" "github.com/zeta-chain/node/zetaclient/chains/sui/client" ) @@ -20,7 +20,7 @@ type suiClient interface { 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) - GetSuiCoinObjectRef(ctx context.Context, owner string) (patsui.ObjectRef, error) + GetSuiCoinObjectRef(ctx context.Context, owner string) (suiptb.ObjectRef, error) SuiXGetLatestSuiSystemState(ctx context.Context) (models.SuiSystemStateSummary, error) SuiXGetReferenceGasPrice(ctx context.Context) (uint64, error) From 551618572f01a1d718ea62a944a79b049ff02360 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 16:34:29 -0500 Subject: [PATCH 36/49] unexport typeSeparator; switch return type from pointer to value; remove redundant logger field chain id --- pkg/contracts/sui/ptb_argument.go | 10 +++++----- pkg/contracts/sui/ptb_argument_test.go | 12 ++++++------ pkg/contracts/sui/withdraw_and_call.go | 8 ++++++-- zetaclient/chains/sui/signer/signer.go | 5 +---- zetaclient/chains/sui/signer/withdraw_and_call.go | 6 +++--- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pkg/contracts/sui/ptb_argument.go b/pkg/contracts/sui/ptb_argument.go index 43729e94d0..fa650c6968 100644 --- a/pkg/contracts/sui/ptb_argument.go +++ b/pkg/contracts/sui/ptb_argument.go @@ -36,21 +36,21 @@ func PureUint64FromString( // Module: "sui", // Name: "SUI", // } -func TypeTagFromString(t string) (*sui.StructTag, error) { - parts := strings.Split(t, TypeSeparator) +func TypeTagFromString(t string) (tag sui.StructTag, err error) { + parts := strings.Split(t, typeSeparator) if len(parts) != 3 { - return nil, fmt.Errorf("invalid type string: %s", t) + return tag, fmt.Errorf("invalid type string: %s", t) } address, err := sui.AddressFromHex(parts[0]) if err != nil { - return nil, fmt.Errorf("invalid address: %s", parts[0]) + return tag, errors.Wrapf(err, "invalid address: %s", parts[0]) } module := parts[1] name := parts[2] - return &sui.StructTag{ + return sui.StructTag{ Address: address, Module: module, Name: name, diff --git a/pkg/contracts/sui/ptb_argument_test.go b/pkg/contracts/sui/ptb_argument_test.go index d368b6bff0..2577f4386f 100644 --- a/pkg/contracts/sui/ptb_argument_test.go +++ b/pkg/contracts/sui/ptb_argument_test.go @@ -18,13 +18,13 @@ func Test_TypeTagFromString(t *testing.T) { tests := []struct { name string coinType CoinType - want *sui.StructTag + want sui.StructTag errMsg string }{ { name: "SUI coin type", coinType: SUI, - want: &sui.StructTag{ + want: sui.StructTag{ Address: suiAddr, Module: "sui", Name: "SUI", @@ -33,7 +33,7 @@ func Test_TypeTagFromString(t *testing.T) { { name: "some other coin type", coinType: CoinType(otherAddrStr + "::other::TOKEN"), - want: &sui.StructTag{ + want: sui.StructTag{ Address: otherAddr, Module: "other", Name: "TOKEN", @@ -42,13 +42,13 @@ func Test_TypeTagFromString(t *testing.T) { { name: "invalid type string", coinType: CoinType(otherAddrStr), - want: nil, + want: sui.StructTag{}, errMsg: "invalid type string", }, { name: "invalid address", coinType: "invalid::sui::SUI", - want: nil, + want: sui.StructTag{}, errMsg: "invalid address", }, } @@ -57,7 +57,7 @@ func Test_TypeTagFromString(t *testing.T) { t.Run(test.name, func(t *testing.T) { got, err := TypeTagFromString(string(test.coinType)) if test.errMsg != "" { - require.Error(t, err) + require.Empty(t, got) require.ErrorContains(t, err, test.errMsg) return } diff --git a/pkg/contracts/sui/withdraw_and_call.go b/pkg/contracts/sui/withdraw_and_call.go index 3465c12af0..640b6d5000 100644 --- a/pkg/contracts/sui/withdraw_and_call.go +++ b/pkg/contracts/sui/withdraw_and_call.go @@ -20,8 +20,8 @@ const ( // FuncOnCall is the Sui connected module function name on_call FuncOnCall = "on_call" - // TypeSeparator is the separator for Sui package and module - TypeSeparator = "::" + // typeSeparator is the separator for Sui package and module + typeSeparator = "::" // ptbWithdrawAndCallCmdCount is the number of commands in the PTB withdraw and call // the three commands are: [withdraw_impl, transfer_objects, on_call] @@ -58,6 +58,10 @@ func (d WithdrawAndCallPTB) TxNonce() uint64 { } // ExtractInitialSharedVersion extracts the object initial shared version from the object data +// +// Objects referenced for on_call are shared objects, initial shared version is required to build +// the withdraw and call using PTB. +// see: https://docs.sui.io/concepts/transactions/prog-txn-blocks#inputs func ExtractInitialSharedVersion(objData models.SuiObjectData) (uint64, error) { owner, ok := objData.Owner.(map[string]any) if !ok { diff --git a/zetaclient/chains/sui/signer/signer.go b/zetaclient/chains/sui/signer/signer.go index 78a5001535..99c6804221 100644 --- a/zetaclient/chains/sui/signer/signer.go +++ b/zetaclient/chains/sui/signer/signer.go @@ -89,10 +89,7 @@ func (s *Signer) ProcessCCTX(ctx context.Context, cctx *cctypes.CrossChainTx, ze } // prepare logger - logger := s.Logger().Std.With(). - Int64(logs.FieldChain, s.Chain().ChainId). - Uint64(logs.FieldNonce, nonce). - Logger() + logger := s.Logger().Std.With().Uint64(logs.FieldNonce, nonce).Logger() ctx = logger.WithContext(ctx) var txDigest string diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 942233ae96..f5221bca76 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -188,7 +188,7 @@ func addPTBCmdWithdrawImpl( Module: gatewayModule, Function: zetasui.FuncWithdrawImpl, TypeArguments: []sui.TypeTag{ - {Struct: coinType}, + {Struct: &coinType}, }, Arguments: []suiptb.Argument{ argGatewayObject, @@ -248,13 +248,13 @@ func addPTBCmdOnCall( // Build the type arguments for on_call in order: [withdrawn coin type, ... payload type arguments] onCallTypeArgs := make([]sui.TypeTag, 0, len(cp.TypeArgs)+1) - onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: coinType}) + onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: &coinType}) for _, typeArg := range cp.TypeArgs { typeStruct, err := zetasui.TypeTagFromString(typeArg) if err != nil { return errors.Wrapf(err, "invalid type argument %s", typeArg) } - onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: typeStruct}) + onCallTypeArgs = append(onCallTypeArgs, sui.TypeTag{Struct: &typeStruct}) } // Build the args for on_call: [withdrawns coins + payload objects + message] From 8a9ce1300467e68d48d131a35990fe0f2d2565fa Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 21:51:04 -0500 Subject: [PATCH 37/49] create a MoveCall struct to hold parsed packageID, module and function names --- pkg/contracts/sui/gateway_test.go | 12 ++-- pkg/contracts/sui/withdraw_and_call.go | 74 ++++++++++++--------- pkg/contracts/sui/withdraw_and_call_test.go | 12 ++-- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index f010600794..7f6aba7f9b 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -385,11 +385,13 @@ func Test_ParseOutboundEvent(t *testing.T) { EventIndex: 0, EventType: WithdrawAndCallEvent, content: WithdrawAndCallPTB{ - PackageID: packageID, - Module: moduleName, - Function: FuncWithdrawImpl, - Amount: math.NewUint(200), - Nonce: 123, + MoveCall: MoveCall{ + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + }, + Amount: math.NewUint(200), + Nonce: 123, }, }, }, diff --git a/pkg/contracts/sui/withdraw_and_call.go b/pkg/contracts/sui/withdraw_and_call.go index 640b6d5000..6877aeefa2 100644 --- a/pkg/contracts/sui/withdraw_and_call.go +++ b/pkg/contracts/sui/withdraw_and_call.go @@ -37,12 +37,17 @@ const ( // the withdraw_impl command is the first command in the PTB, so the indexes will always be [0, 1, 2, 3, 4] var ptbWithdrawImplArgIndexes = []int{0, 1, 2, 3, 4} +// MoveCall represents a Sui Move call with package ID, module and function +type MoveCall struct { + PackageID string + Module string + Function string + ArgIndexes []int +} + // WithdrawAndCallPTB represents data for a Sui withdraw and call event type WithdrawAndCallPTB struct { - PackageID string - Module string - Function string - + MoveCall Amount math.Uint Nonce uint64 } @@ -107,26 +112,26 @@ func (gw *Gateway) parseWithdrawAndCallPTB( } // parse withdraw_impl at command 0 - packageID, module, function, argIndexes, err := extractMoveCall(tx.Transactions[0]) + moveCall, err := extractMoveCall(tx.Transactions[0]) if err != nil { return event, nil, errors.Wrap(ErrParseEvent, "unable to parse withdraw_impl command in the PTB") } - if packageID != gw.packageID { - return event, nil, errors.Wrapf(ErrParseEvent, "invalid package id %s in the PTB", packageID) + if moveCall.PackageID != gw.packageID { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid package id %s in the PTB", moveCall.PackageID) } - if module != moduleName { - return event, nil, errors.Wrapf(ErrParseEvent, "invalid module name %s in the PTB", module) + if moveCall.Module != moduleName { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid module name %s in the PTB", moveCall.Module) } - if function != FuncWithdrawImpl { - return event, nil, errors.Wrapf(ErrParseEvent, "invalid function name %s in the PTB", function) + if moveCall.Function != FuncWithdrawImpl { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid function name %s in the PTB", moveCall.Function) } // ensure the argument indexes are matching the expected indexes - if !slices.Equal(argIndexes, ptbWithdrawImplArgIndexes) { - return event, nil, errors.Wrapf(ErrParseEvent, "invalid argument indexes %v", argIndexes) + if !slices.Equal(moveCall.ArgIndexes, ptbWithdrawImplArgIndexes) { + return event, nil, errors.Wrapf(ErrParseEvent, "invalid argument indexes %v", moveCall.ArgIndexes) } // parse withdraw_impl arguments @@ -151,11 +156,9 @@ func (gw *Gateway) parseWithdrawAndCallPTB( } content = WithdrawAndCallPTB{ - PackageID: packageID, - Module: moduleName, - Function: FuncWithdrawImpl, - Amount: amount, - Nonce: nonce, + MoveCall: moveCall, + Amount: amount, + Nonce: nonce, } event = Event{ @@ -169,58 +172,63 @@ func (gw *Gateway) parseWithdrawAndCallPTB( } // extractMoveCall extracts the MoveCall information from the PTB transaction command -func extractMoveCall(transaction any) (packageID, module, function string, argIndexes []int, err error) { +func extractMoveCall(transaction any) (MoveCall, error) { commands, ok := transaction.(map[string]any) if !ok { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "invalid command type") + return MoveCall{}, errors.Wrap(ErrParseEvent, "invalid command type") } // parse MoveCall info moveCall, ok := commands["MoveCall"].(map[string]any) if !ok { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing MoveCall") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing MoveCall") } - packageID, err = extractStr(moveCall, "package") + packageID, err := extractStr(moveCall, "package") if err != nil { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing package ID") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing package ID") } - module, err = extractStr(moveCall, "module") + module, err := extractStr(moveCall, "module") if err != nil { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing module name") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing module name") } - function, err = extractStr(moveCall, "function") + function, err := extractStr(moveCall, "function") if err != nil { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing function name") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing function name") } // parse MoveCall data data, ok := moveCall["arguments"] if !ok { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing arguments") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing arguments") } arguments, ok := data.([]any) if !ok { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "arguments should be of slice type") + return MoveCall{}, errors.Wrap(ErrParseEvent, "arguments should be of slice type") } // extract MoveCall argument indexes - argIndexes = make([]int, len(arguments)) + argIndexes := make([]int, len(arguments)) for i, arg := range arguments { indexes, ok := arg.(map[string]any) if !ok { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "invalid argument type") + return MoveCall{}, errors.Wrap(ErrParseEvent, "invalid argument type") } index, err := extractInteger[int](indexes, "Input") if err != nil { - return "", "", "", nil, errors.Wrap(ErrParseEvent, "missing argument index") + return MoveCall{}, errors.Wrap(ErrParseEvent, "missing argument index") } argIndexes[i] = index } - return packageID, module, function, argIndexes, nil + return MoveCall{ + PackageID: packageID, + Module: module, + Function: function, + ArgIndexes: argIndexes, + }, nil } diff --git a/pkg/contracts/sui/withdraw_and_call_test.go b/pkg/contracts/sui/withdraw_and_call_test.go index f87fb89826..3f6e5d20d5 100644 --- a/pkg/contracts/sui/withdraw_and_call_test.go +++ b/pkg/contracts/sui/withdraw_and_call_test.go @@ -109,11 +109,13 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { name: "valid transaction block", response: createPTBResponse(txHash, packageID, amountStr, nonceStr), want: WithdrawAndCallPTB{ - PackageID: packageID, - Module: moduleName, - Function: FuncWithdrawImpl, - Amount: math.NewUint(100), - Nonce: 2, + MoveCall: MoveCall{ + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + }, + Amount: math.NewUint(100), + Nonce: 2, }, }, { From 31a92f2d144365d2651f7058445d030902d298a6 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 22:32:34 -0500 Subject: [PATCH 38/49] use ptb as prefix for tx building functions; pass gasBudget param as type uint64 --- zetaclient/chains/sui/signer/signer_tx.go | 31 ++++++----- .../chains/sui/signer/withdraw_and_call.go | 51 +++++++++---------- .../sui/signer/withdraw_and_call_test.go | 21 ++------ 3 files changed, 45 insertions(+), 58 deletions(-) diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 55a9bfba9b..19d49446a8 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -85,7 +85,7 @@ func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx if err != nil { return tx, errors.Wrap(err, "unable to parse gas price") } - gasBudget := strconv.FormatUint(gasPrice*params.CallOptions.GasLimit, 10) + gasBudget := gasPrice * params.CallOptions.GasLimit // Retrieve withdraw cap ID withdrawCapID, err := s.getWithdrawCapIDCached(ctx) @@ -105,12 +105,15 @@ func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx func (s *Signer) buildWithdrawTx( ctx context.Context, params *cctypes.OutboundParams, - coinType, gasBudget, withdrawCapID string, + coinType string, + gasBudget uint64, + withdrawCapID string, ) (models.TxnMetaData, error) { var ( - nonce = strconv.FormatUint(params.TssNonce, 10) - recipient = params.Receiver - amount = params.Amount.String() + nonce = strconv.FormatUint(params.TssNonce, 10) + recipient = params.Receiver + amount = params.Amount.String() + gasBudgetStr = strconv.FormatUint(gasBudget, 10) ) req := models.MoveCallRequest{ @@ -119,8 +122,8 @@ func (s *Signer) buildWithdrawTx( Module: s.gateway.Module(), Function: funcWithdraw, TypeArguments: []any{coinType}, - Arguments: []any{s.gateway.ObjectID(), amount, nonce, recipient, gasBudget, withdrawCapID}, - GasBudget: gasBudget, + Arguments: []any{s.gateway.ObjectID(), amount, nonce, recipient, gasBudgetStr, withdrawCapID}, + GasBudget: gasBudgetStr, } return s.client.MoveCall(ctx, req) @@ -131,13 +134,13 @@ func (s *Signer) buildWithdrawTx( func (s *Signer) buildWithdrawAndCallTx( ctx context.Context, params *cctypes.OutboundParams, - coinType, - gasBudget, - withdrawCapID, - payload string, + coinType string, + gasBudget uint64, + withdrawCapID string, + payloadHex string, ) (models.TxnMetaData, error) { - // decode and parse the payload to object the on_call arguments - payloadBytes, err := hex.DecodeString(payload) + // decode and parse the payload into object IDs and on_call arguments + payloadBytes, err := hex.DecodeString(payloadHex) if err != nil { return models.TxnMetaData{}, errors.Wrap(err, "unable to decode payload hex bytes") } @@ -172,7 +175,7 @@ func (s *Signer) buildWithdrawAndCallTx( Str("amount", params.Amount.String()). Uint64(logs.FieldNonce, params.TssNonce). Str("receiver", params.Receiver). - Str("gas_budget", gasBudget). + Uint64("gas_budget", gasBudget). Any("type_args", cp.TypeArgs). Any("object_ids", cp.ObjectIDs). Hex("payload", cp.Message). diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index f5221bca76..70a6f9d2db 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -31,8 +31,8 @@ func withdrawAndCallPTB( onCallObjectRefs []sui.ObjectRef, coinTypeStr, amountStr, - nonceStr, - gasBudgetStr, + nonceStr string, + gasBudget uint64, receiver string, cp zetasui.CallPayload, ) (tx models.TxnMetaData, err error) { @@ -45,7 +45,7 @@ func withdrawAndCallPTB( } // Add withdraw_impl command and get its command index - gasBudgetUint, err := addPTBCmdWithdrawImpl( + if err := ptbAddCmdWithdrawImpl( ptb, gatewayPackageIDStr, gatewayModule, @@ -54,9 +54,8 @@ func withdrawAndCallPTB( coinTypeStr, amountStr, nonceStr, - gasBudgetStr, - ) - if err != nil { + gasBudget, + ); err != nil { return tx, err } @@ -77,13 +76,13 @@ func withdrawAndCallPTB( } // Add gas budget transfer command - err = addPTBCmdGasBudgetTransfer(ptb, argBudgetCoins, *signerAddr) + err = ptbAddCmdGasBudgetTransfer(ptb, argBudgetCoins, *signerAddr) if err != nil { return tx, err } // Add on_call command - err = addPTBCmdOnCall( + err = ptbAddCmdOnCall( ptb, receiver, coinTypeStr, @@ -105,7 +104,7 @@ func withdrawAndCallPTB( []*sui.ObjectRef{ &suiCoinObjRef, }, - gasBudgetUint, + gasBudget, suiclient.DefaultGasPrice, ) @@ -120,8 +119,8 @@ func withdrawAndCallPTB( }, nil } -// addPTBCmdWithdrawImpl adds the withdraw_impl command to the PTB and returns the gas budget value -func addPTBCmdWithdrawImpl( +// ptbAddCmdWithdrawImpl adds the withdraw_impl command to the PTB +func ptbAddCmdWithdrawImpl( ptb *suiptb.ProgrammableTransactionBuilder, gatewayPackageIDStr string, gatewayModule string, @@ -130,18 +129,18 @@ func addPTBCmdWithdrawImpl( coinTypeStr string, amountStr string, nonceStr string, - gasBudgetStr string, -) (uint64, error) { + gasBudget uint64, +) error { // Parse gateway package ID gatewayPackageID, err := sui.PackageIdFromHex(gatewayPackageIDStr) if err != nil { - return 0, errors.Wrapf(err, "invalid gateway package ID %s", gatewayPackageIDStr) + return errors.Wrapf(err, "invalid gateway package ID %s", gatewayPackageIDStr) } // Parse coin type coinType, err := zetasui.TypeTagFromString(coinTypeStr) if err != nil { - return 0, errors.Wrapf(err, "invalid coin type %s", coinTypeStr) + return errors.Wrapf(err, "invalid coin type %s", coinTypeStr) } // Create gateway object argument @@ -153,31 +152,31 @@ func addPTBCmdWithdrawImpl( }, }) if err != nil { - return 0, errors.Wrap(err, "unable to create gateway object argument") + return errors.Wrap(err, "unable to create gateway object argument") } // Create amount argument argAmount, _, err := zetasui.PureUint64FromString(ptb, amountStr) if err != nil { - return 0, errors.Wrapf(err, "unable to create amount argument") + return errors.Wrapf(err, "unable to create amount argument") } // Create nonce argument argNonce, _, err := zetasui.PureUint64FromString(ptb, nonceStr) if err != nil { - return 0, errors.Wrapf(err, "unable to create nonce argument") + return errors.Wrapf(err, "unable to create nonce argument") } // Create gas budget argument - argGasBudget, gasBudgetUint, err := zetasui.PureUint64FromString(ptb, gasBudgetStr) + argGasBudget, err := ptb.Pure(gasBudget) if err != nil { - return 0, errors.Wrapf(err, "unable to create gas budget argument") + return errors.Wrapf(err, "unable to create gas budget argument") } // Create withdraw cap argument argWithdrawCap, err := ptb.Obj(suiptb.ObjectArg{ImmOrOwnedObject: &withdrawCapObjRef}) if err != nil { - return 0, errors.Wrapf(err, "unable to create withdraw cap object argument") + return errors.Wrapf(err, "unable to create withdraw cap object argument") } // add Move call for withdraw_impl @@ -200,11 +199,11 @@ func addPTBCmdWithdrawImpl( }, }) - return gasBudgetUint, nil + return nil } -// addPTBCmdGasBudgetTransfer adds the gas budget transfer command to the PTB -func addPTBCmdGasBudgetTransfer( +// ptbAddCmdGasBudgetTransfer adds the gas budget transfer command to the PTB +func ptbAddCmdGasBudgetTransfer( ptb *suiptb.ProgrammableTransactionBuilder, argBudgetCoins suiptb.Argument, signerAddr sui.Address, @@ -225,8 +224,8 @@ func addPTBCmdGasBudgetTransfer( return nil } -// addPTBCmdOnCall adds the on_call command to the PTB -func addPTBCmdOnCall( +// ptbAddCmdOnCall adds the on_call command to the PTB +func ptbAddCmdOnCall( ptb *suiptb.ProgrammableTransactionBuilder, receiver string, coinTypeStr string, diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index d894e14c2f..7d9bf19a30 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -24,7 +24,7 @@ type testPTBArgs struct { coinTypeStr string amountStr string nonceStr string - gasBudgetStr string + gasBudget uint64 receiver string cp zetasui.CallPayload } @@ -46,7 +46,7 @@ func newTestPTBArgs( coinTypeStr: string(zetasui.SUI), amountStr: "1000000", nonceStr: "1", - gasBudgetStr: "2000000", + gasBudget: 2000000, receiver: sample.SuiAddress(t), cp: zetasui.CallPayload{ TypeArgs: []string{string(zetasui.SUI)}, @@ -161,21 +161,6 @@ func Test_withdrawAndCallPTB(t *testing.T) { }(), errMsg: "unable to create nonce argument", }, - { - name: "unable to create gas budget argument", - args: func() testPTBArgs { - args := newTestPTBArgs( - t, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - []sui.ObjectRef{onCallObjRef}, - ) - args.gasBudgetStr = "invalid_gas_budget" - return args - }(), - errMsg: "unable to create gas budget argument", - }, { name: "invalid target package ID", args: func() testPTBArgs { @@ -221,7 +206,7 @@ func Test_withdrawAndCallPTB(t *testing.T) { tt.args.coinTypeStr, tt.args.amountStr, tt.args.nonceStr, - tt.args.gasBudgetStr, + tt.args.gasBudget, tt.args.receiver, tt.args.cp, ) From dc0951b4ad6e6c15871d602063f8cf87624338a6 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 23 Apr 2025 23:43:31 -0500 Subject: [PATCH 39/49] move withdrawAndCallPTB as Signer method; group withdrawAndCallPTB arguments into a struct; pass amount and nonce as integers --- zetaclient/chains/sui/signer/signer_tx.go | 43 +++-- .../chains/sui/signer/withdraw_and_call.go | 83 +++++----- .../sui/signer/withdraw_and_call_test.go | 147 ++++-------------- 3 files changed, 93 insertions(+), 180 deletions(-) diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 19d49446a8..de2479533e 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -168,38 +168,37 @@ func (s *Signer) buildWithdrawAndCallTx( return models.TxnMetaData{}, errors.Wrap(err, "unable to get object references") } + args := withdrawAndCallPTBArgs{ + gatewayObjRef: gatewayObjRef, + suiCoinObjRef: suiCoinObjRef, + withdrawCapObjRef: withdrawCapObjRef, + onCallObjectRefs: onCallObjectRefs, + coinType: coinType, + amount: params.Amount.Uint64(), + nonce: params.TssNonce, + gasBudget: gasBudget, + receiver: params.Receiver, + cp: cp, + } + // print PTB transaction parameters s.Logger().Std.Info(). Str(logs.FieldMethod, "buildWithdrawAndCallTx"). + Uint64(logs.FieldNonce, args.nonce). Str(logs.FieldCoinType, coinType). - Str("amount", params.Amount.String()). - Uint64(logs.FieldNonce, params.TssNonce). - Str("receiver", params.Receiver). - Uint64("gas_budget", gasBudget). - Any("type_args", cp.TypeArgs). - Any("object_ids", cp.ObjectIDs). - Hex("payload", cp.Message). + Uint64("amount", args.amount). + Str("receiver", args.receiver). + Uint64("gas_budget", args.gasBudget). + Any("type_args", args.cp.TypeArgs). + Any("object_ids", args.cp.ObjectIDs). + Hex("payload", args.cp.Message). Msg("calling withdrawAndCallPTB") // TODO: check all object IDs are share object here // https://github.com/zeta-chain/node/issues/3755 // build the PTB transaction - return withdrawAndCallPTB( - s.TSS().PubKey().AddressSui(), - s.gateway.PackageID(), - s.gateway.Module(), - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - onCallObjectRefs, - coinType, - params.Amount.String(), - strconv.FormatUint(params.TssNonce, 10), - gasBudget, - params.Receiver, - cp, - ) + return s.withdrawAndCallPTB(args) } // createCancelTxBuilder creates a cancel tx builder for given CCTX diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 70a6f9d2db..ce477366f5 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -15,46 +15,51 @@ import ( zetasui "github.com/zeta-chain/node/pkg/contracts/sui" ) +// withdrawAndCallPTBArgs holds all the arguments needed for withdrawAndCallPTB +type withdrawAndCallPTBArgs struct { + gatewayObjRef sui.ObjectRef + suiCoinObjRef sui.ObjectRef + withdrawCapObjRef sui.ObjectRef + onCallObjectRefs []sui.ObjectRef + coinType string + amount uint64 + nonce uint64 + gasBudget uint64 + receiver string + cp zetasui.CallPayload +} + // withdrawAndCallPTB builds unsigned withdraw and call PTB Sui transaction // it chains the following calls: // 1. withdraw_impl on gateway // 2. gas budget coin transfer to TSS // 3. on_call on target contract // The function returns a TxnMetaData object with tx bytes, the other fields are ignored -func withdrawAndCallPTB( - signerAddrStr, - gatewayPackageIDStr, - gatewayModule string, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef sui.ObjectRef, - onCallObjectRefs []sui.ObjectRef, - coinTypeStr, - amountStr, - nonceStr string, - gasBudget uint64, - receiver string, - cp zetasui.CallPayload, -) (tx models.TxnMetaData, err error) { - ptb := suiptb.NewTransactionDataTransactionBuilder() +func (s *Signer) withdrawAndCallPTB(args withdrawAndCallPTBArgs) (tx models.TxnMetaData, err error) { + var ( + tssAddress = s.TSS().PubKey().AddressSui() + gatewayPackageID = s.gateway.PackageID() + gatewayModule = s.gateway.Module() + ptb = suiptb.NewTransactionDataTransactionBuilder() + ) // Parse signer address - signerAddr, err := sui.AddressFromHex(signerAddrStr) + signerAddr, err := sui.AddressFromHex(tssAddress) if err != nil { - return tx, errors.Wrapf(err, "invalid signer address %s", signerAddrStr) + return tx, errors.Wrapf(err, "invalid signer address %s", tssAddress) } // Add withdraw_impl command and get its command index if err := ptbAddCmdWithdrawImpl( ptb, - gatewayPackageIDStr, + gatewayPackageID, gatewayModule, - gatewayObjRef, - withdrawCapObjRef, - coinTypeStr, - amountStr, - nonceStr, - gasBudget, + args.gatewayObjRef, + args.withdrawCapObjRef, + args.coinType, + args.amount, + args.nonce, + args.gasBudget, ); err != nil { return tx, err } @@ -84,11 +89,11 @@ func withdrawAndCallPTB( // Add on_call command err = ptbAddCmdOnCall( ptb, - receiver, - coinTypeStr, + args.receiver, + args.coinType, argWithdrawnCoins, - onCallObjectRefs, - cp, + args.onCallObjectRefs, + args.cp, ) if err != nil { return tx, err @@ -102,9 +107,9 @@ func withdrawAndCallPTB( signerAddr, pt, []*sui.ObjectRef{ - &suiCoinObjRef, + &args.suiCoinObjRef, }, - gasBudget, + args.gasBudget, suiclient.DefaultGasPrice, ) @@ -126,9 +131,9 @@ func ptbAddCmdWithdrawImpl( gatewayModule string, gatewayObjRef sui.ObjectRef, withdrawCapObjRef sui.ObjectRef, - coinTypeStr string, - amountStr string, - nonceStr string, + coinType string, + amount uint64, + nonce uint64, gasBudget uint64, ) error { // Parse gateway package ID @@ -138,9 +143,9 @@ func ptbAddCmdWithdrawImpl( } // Parse coin type - coinType, err := zetasui.TypeTagFromString(coinTypeStr) + tagCoinType, err := zetasui.TypeTagFromString(coinType) if err != nil { - return errors.Wrapf(err, "invalid coin type %s", coinTypeStr) + return errors.Wrapf(err, "invalid coin type %s", coinType) } // Create gateway object argument @@ -156,13 +161,13 @@ func ptbAddCmdWithdrawImpl( } // Create amount argument - argAmount, _, err := zetasui.PureUint64FromString(ptb, amountStr) + argAmount, err := ptb.Pure(amount) if err != nil { return errors.Wrapf(err, "unable to create amount argument") } // Create nonce argument - argNonce, _, err := zetasui.PureUint64FromString(ptb, nonceStr) + argNonce, err := ptb.Pure(nonce) if err != nil { return errors.Wrapf(err, "unable to create nonce argument") } @@ -187,7 +192,7 @@ func ptbAddCmdWithdrawImpl( Module: gatewayModule, Function: zetasui.FuncWithdrawImpl, TypeArguments: []sui.TypeTag{ - {Struct: &coinType}, + {Struct: &tagCoinType}, }, Arguments: []suiptb.Argument{ argGatewayObject, diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 7d9bf19a30..6ad0b90d63 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -12,42 +12,22 @@ import ( "github.com/zeta-chain/node/testutil/sample" ) -// testPTBArgs holds all the arguments needed for withdrawAndCallPTB -type testPTBArgs struct { - signerAddrStr string - gatewayPackageIDStr string - gatewayModule string - gatewayObjRef sui.ObjectRef - suiCoinObjRef sui.ObjectRef - withdrawCapObjRef sui.ObjectRef - onCallObjectRefs []sui.ObjectRef - coinTypeStr string - amountStr string - nonceStr string - gasBudget uint64 - receiver string - cp zetasui.CallPayload -} - -// newTestPTBArgs creates a testArgs struct with default values -func newTestPTBArgs( +// newTestWACPTBArgs creates a withdrawAndCallPTBArgs struct for testing +func newTestWACPTBArgs( t *testing.T, gatewayObjRef, suiCoinObjRef, withdrawCapObjRef sui.ObjectRef, onCallObjectRefs []sui.ObjectRef, -) testPTBArgs { - return testPTBArgs{ - signerAddrStr: sample.SuiAddress(t), - gatewayPackageIDStr: sample.SuiAddress(t), - gatewayModule: "gateway", - gatewayObjRef: gatewayObjRef, - suiCoinObjRef: suiCoinObjRef, - withdrawCapObjRef: withdrawCapObjRef, - onCallObjectRefs: onCallObjectRefs, - coinTypeStr: string(zetasui.SUI), - amountStr: "1000000", - nonceStr: "1", - gasBudget: 2000000, - receiver: sample.SuiAddress(t), +) withdrawAndCallPTBArgs { + return withdrawAndCallPTBArgs{ + gatewayObjRef: gatewayObjRef, + suiCoinObjRef: suiCoinObjRef, + withdrawCapObjRef: withdrawCapObjRef, + onCallObjectRefs: onCallObjectRefs, + coinType: string(zetasui.SUI), + amount: 1000000, + nonce: 1, + gasBudget: 2000000, + receiver: sample.SuiAddress(t), cp: zetasui.CallPayload{ TypeArgs: []string{string(zetasui.SUI)}, ObjectIDs: []string{sample.SuiAddress(t)}, @@ -57,6 +37,9 @@ func newTestPTBArgs( } func Test_withdrawAndCallPTB(t *testing.T) { + // Create a test suite + ts := newTestSuite(t) + // create test objects references gatewayObjRef := sampleObjectRef(t) suiCoinObjRef := sampleObjectRef(t) @@ -65,17 +48,17 @@ func Test_withdrawAndCallPTB(t *testing.T) { tests := []struct { name string - args testPTBArgs + args withdrawAndCallPTBArgs errMsg string }{ { name: "successful withdraw and call", - args: newTestPTBArgs(t, gatewayObjRef, suiCoinObjRef, withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}), + args: newTestWACPTBArgs(t, gatewayObjRef, suiCoinObjRef, withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}), }, { name: "successful withdraw and call with empty payload", - args: func() testPTBArgs { - args := newTestPTBArgs( + args: func() withdrawAndCallPTBArgs { + args := newTestWACPTBArgs( t, gatewayObjRef, suiCoinObjRef, @@ -86,85 +69,25 @@ func Test_withdrawAndCallPTB(t *testing.T) { return args }(), }, - { - name: "invalid signer address", - args: func() testPTBArgs { - args := newTestPTBArgs( - t, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - []sui.ObjectRef{onCallObjRef}, - ) - args.signerAddrStr = "invalid_address" - return args - }(), - errMsg: "invalid signer address", - }, - { - name: "invalid gateway package ID", - args: func() testPTBArgs { - args := newTestPTBArgs( - t, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - []sui.ObjectRef{onCallObjRef}, - ) - args.gatewayPackageIDStr = "invalid_package_id" - return args - }(), - errMsg: "invalid gateway package ID", - }, { name: "invalid coin type", - args: func() testPTBArgs { - args := newTestPTBArgs( + args: func() withdrawAndCallPTBArgs { + args := newTestWACPTBArgs( t, gatewayObjRef, suiCoinObjRef, withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}, ) - args.coinTypeStr = "invalid_coin_type" + args.coinType = "invalid_coin_type" return args }(), errMsg: "invalid coin type", }, - { - name: "unable to create amount argument", - args: func() testPTBArgs { - args := newTestPTBArgs( - t, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - []sui.ObjectRef{onCallObjRef}, - ) - args.amountStr = "invalid_amount" - return args - }(), - errMsg: "unable to create amount argument", - }, - { - name: "unable to create nonce argument", - args: func() testPTBArgs { - args := newTestPTBArgs( - t, - gatewayObjRef, - suiCoinObjRef, - withdrawCapObjRef, - []sui.ObjectRef{onCallObjRef}, - ) - args.nonceStr = "invalid_nonce" - return args - }(), - errMsg: "unable to create nonce argument", - }, { name: "invalid target package ID", - args: func() testPTBArgs { - args := newTestPTBArgs( + args: func() withdrawAndCallPTBArgs { + args := newTestWACPTBArgs( t, gatewayObjRef, suiCoinObjRef, @@ -178,8 +101,8 @@ func Test_withdrawAndCallPTB(t *testing.T) { }, { name: "invalid type argument", - args: func() testPTBArgs { - args := newTestPTBArgs( + args: func() withdrawAndCallPTBArgs { + args := newTestWACPTBArgs( t, gatewayObjRef, suiCoinObjRef, @@ -195,21 +118,7 @@ func Test_withdrawAndCallPTB(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := withdrawAndCallPTB( - tt.args.signerAddrStr, - tt.args.gatewayPackageIDStr, - tt.args.gatewayModule, - tt.args.gatewayObjRef, - tt.args.suiCoinObjRef, - tt.args.withdrawCapObjRef, - tt.args.onCallObjectRefs, - tt.args.coinTypeStr, - tt.args.amountStr, - tt.args.nonceStr, - tt.args.gasBudget, - tt.args.receiver, - tt.args.cp, - ) + got, err := ts.Signer.withdrawAndCallPTB(tt.args) if tt.errMsg != "" { require.ErrorContains(t, err, tt.errMsg) From 63e4bd670584313029bed0f18b1458b3bce7af35 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 00:47:06 -0500 Subject: [PATCH 40/49] move getWithdrawAndCallObjectRefs to a Signer method --- zetaclient/chains/sui/signer/signer_tx.go | 39 ++-- .../chains/sui/signer/withdraw_and_call.go | 168 +++++++++--------- .../sui/signer/withdraw_and_call_test.go | 53 +++--- 3 files changed, 122 insertions(+), 138 deletions(-) diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index de2479533e..8a42b8de9e 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -150,42 +150,35 @@ func (s *Signer) buildWithdrawAndCallTx( return models.TxnMetaData{}, errors.Wrap(err, "unable to parse withdrawAndCall payload") } - // get latest TSS SUI coin object ref for gas payment - suiCoinObjRef, err := s.client.GetSuiCoinObjectRef(ctx, s.TSS().PubKey().AddressSui()) + // get all needed object references + wacRefs, err := s.getWithdrawAndCallObjectRefs(ctx, withdrawCapID, cp.ObjectIDs) if err != nil { - return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") + return models.TxnMetaData{}, errors.Wrap(err, "unable to get object references") } - // get all other object references: [gateway, withdrawCap, onCallObjects] - gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := getWithdrawAndCallObjectRefs( - ctx, - s.client, - s.gateway.ObjectID(), - withdrawCapID, - cp.ObjectIDs, - ) + // get latest TSS SUI coin object ref for gas payment + suiCoinObjRef, err := s.client.GetSuiCoinObjectRef(ctx, s.TSS().PubKey().AddressSui()) if err != nil { - return models.TxnMetaData{}, errors.Wrap(err, "unable to get object references") + return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") } + wacRefs.suiCoinObjRef = suiCoinObjRef + // all PTB arguments args := withdrawAndCallPTBArgs{ - gatewayObjRef: gatewayObjRef, - suiCoinObjRef: suiCoinObjRef, - withdrawCapObjRef: withdrawCapObjRef, - onCallObjectRefs: onCallObjectRefs, - coinType: coinType, - amount: params.Amount.Uint64(), - nonce: params.TssNonce, - gasBudget: gasBudget, - receiver: params.Receiver, - cp: cp, + withdrawAndCallObjRefs: wacRefs, + coinType: coinType, + amount: params.Amount.Uint64(), + nonce: params.TssNonce, + gasBudget: gasBudget, + receiver: params.Receiver, + cp: cp, } // print PTB transaction parameters s.Logger().Std.Info(). Str(logs.FieldMethod, "buildWithdrawAndCallTx"). Uint64(logs.FieldNonce, args.nonce). - Str(logs.FieldCoinType, coinType). + Str(logs.FieldCoinType, args.coinType). Uint64("amount", args.amount). Str("receiver", args.receiver). Uint64("gas_budget", args.gasBudget). diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index ce477366f5..1c6a9a4aa4 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -15,18 +15,23 @@ import ( zetasui "github.com/zeta-chain/node/pkg/contracts/sui" ) -// withdrawAndCallPTBArgs holds all the arguments needed for withdrawAndCallPTB -type withdrawAndCallPTBArgs struct { +// withdrawAndCallObjRefs contains all the object references needed for withdraw and call +type withdrawAndCallObjRefs struct { gatewayObjRef sui.ObjectRef - suiCoinObjRef sui.ObjectRef withdrawCapObjRef sui.ObjectRef onCallObjectRefs []sui.ObjectRef - coinType string - amount uint64 - nonce uint64 - gasBudget uint64 - receiver string - cp zetasui.CallPayload + suiCoinObjRef sui.ObjectRef +} + +// withdrawAndCallPTBArgs contains all the arguments needed for withdraw and call +type withdrawAndCallPTBArgs struct { + withdrawAndCallObjRefs + coinType string + amount uint64 + nonce uint64 + gasBudget uint64 + receiver string + cp zetasui.CallPayload } // withdrawAndCallPTB builds unsigned withdraw and call PTB Sui transaction @@ -124,6 +129,74 @@ func (s *Signer) withdrawAndCallPTB(args withdrawAndCallPTBArgs) (tx models.TxnM }, nil } +// getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call +// - Initial shared version will be used for shared objects +// - Current version will be used for non-shared objects, e.g. withdraw cap +func (s *Signer) getWithdrawAndCallObjectRefs( + ctx context.Context, + withdrawCapID string, + onCallObjectIDs []string, +) (withdrawAndCallObjRefs, error) { + objectIDs := append([]string{s.gateway.ObjectID(), withdrawCapID}, onCallObjectIDs...) + + // query objects in batch + suiObjects, err := s.client.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ + ObjectIds: objectIDs, + Options: models.SuiObjectDataOptions{ + // show owner info in order to retrieve object initial shared version + ShowOwner: true, + }, + }) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf(err, "failed to get objects for %v", objectIDs) + } + + // convert object data to object references + objectRefs := make([]sui.ObjectRef, len(objectIDs)) + + for i, object := range suiObjects { + objectID, err := sui.ObjectIdFromHex(object.Data.ObjectId) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf(err, "failed to parse object ID %s", object.Data.ObjectId) + } + + objectVersion, err := strconv.ParseUint(object.Data.Version, 10, 64) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf(err, "failed to parse object version %s", object.Data.Version) + } + + // must use initial version for shared object, not the current version + // withdraw cap is not a shared object, so we must use current version + if object.Data.ObjectId != withdrawCapID { + objectVersion, err = zetasui.ExtractInitialSharedVersion(*object.Data) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf( + err, + "failed to extract initial shared version for object %s", + object.Data.ObjectId, + ) + } + } + + objectDigest, err := sui.NewBase58(object.Data.Digest) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf(err, "failed to parse object digest %s", object.Data.Digest) + } + + objectRefs[i] = sui.ObjectRef{ + ObjectId: objectID, + Version: objectVersion, + Digest: objectDigest, + } + } + + return withdrawAndCallObjRefs{ + gatewayObjRef: objectRefs[0], + withdrawCapObjRef: objectRefs[1], + onCallObjectRefs: objectRefs[2:], + }, nil +} + // ptbAddCmdWithdrawImpl adds the withdraw_impl command to the PTB func ptbAddCmdWithdrawImpl( ptb *suiptb.ProgrammableTransactionBuilder, @@ -300,80 +373,3 @@ func ptbAddCmdOnCall( return nil } - -// getWithdrawAndCallObjectRefs returns the SUI object references for withdraw and call -// - Initial shared version will be used for shared objects -// - Current version will be used for non-shared objects, e.g. withdraw cap -func getWithdrawAndCallObjectRefs( - ctx context.Context, - rpc RPC, - gatewayID, withdrawCapID string, - onCallObjectIDs []string, -) (gatewayObjRef, withdrawCapObjRef sui.ObjectRef, onCallObjectRefs []sui.ObjectRef, err error) { - objectIDs := append([]string{gatewayID, withdrawCapID}, onCallObjectIDs...) - - // query objects in batch - suiObjects, err := rpc.SuiMultiGetObjects(ctx, models.SuiMultiGetObjectsRequest{ - ObjectIds: objectIDs, - Options: models.SuiObjectDataOptions{ - // show owner info in order to retrieve object initial shared version - ShowOwner: true, - }, - }) - if err != nil { - return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf(err, "failed to get SUI objects for %v", objectIDs) - } - - // convert object data to object references - objectRefs := make([]sui.ObjectRef, len(objectIDs)) - - for i, object := range suiObjects { - objectID, err := sui.ObjectIdFromHex(object.Data.ObjectId) - if err != nil { - return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( - err, - "failed to parse object ID %s", - object.Data.ObjectId, - ) - } - - objectVersion, err := strconv.ParseUint(object.Data.Version, 10, 64) - if err != nil { - return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( - err, - "failed to parse object version %s", - object.Data.Version, - ) - } - - // must use initial version for shared object, not the current version - // withdraw cap is not a shared object, so we must use current version - if object.Data.ObjectId != withdrawCapID { - objectVersion, err = zetasui.ExtractInitialSharedVersion(*object.Data) - if err != nil { - return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( - err, - "failed to extract initial shared version for object %s", - object.Data.ObjectId, - ) - } - } - - objectDigest, err := sui.NewBase58(object.Data.Digest) - if err != nil { - return gatewayObjRef, withdrawCapObjRef, nil, errors.Wrapf( - err, - "failed to parse object digest %s", - object.Data.Digest, - ) - } - - objectRefs[i] = sui.ObjectRef{ - ObjectId: objectID, - Version: objectVersion, - Digest: objectDigest, - } - } - - return objectRefs[0], objectRefs[1], objectRefs[2:], nil -} diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 6ad0b90d63..62fbf4dc2b 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -19,15 +19,17 @@ func newTestWACPTBArgs( onCallObjectRefs []sui.ObjectRef, ) withdrawAndCallPTBArgs { return withdrawAndCallPTBArgs{ - gatewayObjRef: gatewayObjRef, - suiCoinObjRef: suiCoinObjRef, - withdrawCapObjRef: withdrawCapObjRef, - onCallObjectRefs: onCallObjectRefs, - coinType: string(zetasui.SUI), - amount: 1000000, - nonce: 1, - gasBudget: 2000000, - receiver: sample.SuiAddress(t), + withdrawAndCallObjRefs: withdrawAndCallObjRefs{ + gatewayObjRef: gatewayObjRef, + withdrawCapObjRef: withdrawCapObjRef, + onCallObjectRefs: onCallObjectRefs, + suiCoinObjRef: suiCoinObjRef, + }, + coinType: string(zetasui.SUI), + amount: 1000000, + nonce: 1, + gasBudget: 2000000, + receiver: sample.SuiAddress(t), cp: zetasui.CallPayload{ TypeArgs: []string{string(zetasui.SUI)}, ObjectIDs: []string{sample.SuiAddress(t)}, @@ -155,7 +157,7 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { onCallObjectIDs []string mockObjects []*models.SuiObjectResponse mockError error - expected []sui.ObjectRef + expected withdrawAndCallObjRefs errMsg string }{ { @@ -196,21 +198,23 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { }, }, }, - expected: []sui.ObjectRef{ - { + expected: withdrawAndCallObjRefs{ + gatewayObjRef: sui.ObjectRef{ ObjectId: gatewayID, Version: 1, Digest: digest1, }, - { + withdrawCapObjRef: sui.ObjectRef{ ObjectId: withdrawCapID, Version: 2, Digest: digest2, }, - { - ObjectId: onCallObjectID, - Version: 1, - Digest: digest3, + onCallObjectRefs: []sui.ObjectRef{ + { + ObjectId: onCallObjectID, + Version: 1, + Digest: digest3, + }, }, }, }, @@ -220,7 +224,7 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { withdrawCapID: withdrawCapID.String(), onCallObjectIDs: []string{onCallObjectID.String()}, mockError: sample.ErrSample, - errMsg: "failed to get SUI objects", + errMsg: "failed to get objects", }, { name: "invalid object ID", @@ -287,13 +291,7 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { ts.SuiMock.On("SuiMultiGetObjects", ctx, mock.Anything).Return(tt.mockObjects, tt.mockError) // ACT - gatewayObjRef, withdrawCapObjRef, onCallObjectRefs, err := getWithdrawAndCallObjectRefs( - ctx, - ts.SuiMock, - tt.gatewayID, - tt.withdrawCapID, - tt.onCallObjectIDs, - ) + got, err := ts.Signer.getWithdrawAndCallObjectRefs(ctx, tt.withdrawCapID, tt.onCallObjectIDs) // ASSERT if tt.errMsg != "" { @@ -302,10 +300,7 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { } require.NoError(t, err) - require.EqualValues(t, tt.expected[0], gatewayObjRef) - require.EqualValues(t, tt.expected[1], withdrawCapObjRef) - require.EqualValues(t, tt.expected[2:], onCallObjectRefs) - require.Len(t, onCallObjectRefs, len(tt.onCallObjectIDs)) + require.Equal(t, tt.expected, got) }) } } From fcb377f8a422ba04068e7e1dc7ab2aabc25a5f98 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 10:35:41 -0500 Subject: [PATCH 41/49] add Makefile to rebuild example package and update compiled mv files --- e2e/contracts/sui/example/Makefile | 22 ++++++++++++++++++ e2e/contracts/sui/example/connected.mv | Bin 0 -> 665 bytes .../sui/example/sources/example.move | 9 +++++-- e2e/contracts/sui/example/token.mv | Bin 0 -> 629 bytes 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 e2e/contracts/sui/example/Makefile create mode 100644 e2e/contracts/sui/example/connected.mv create mode 100644 e2e/contracts/sui/example/token.mv diff --git a/e2e/contracts/sui/example/Makefile b/e2e/contracts/sui/example/Makefile new file mode 100644 index 0000000000..6fb133a100 --- /dev/null +++ b/e2e/contracts/sui/example/Makefile @@ -0,0 +1,22 @@ +.PHONY: clean build all + +# Default target +all: clean build + +# Clean build directory +clean: + rm -rf build/ + +# Build the package and generate bytecode +build: + sui move build + cp build/example/bytecode_modules/token.mv . + cp build/example/bytecode_modules/connected.mv . + +# Help target +help: + @echo "Available targets:" + @echo " all - Clean and build everything (default)" + @echo " clean - Remove build directory" + @echo " build - Build the package and generate bytecode" + @echo " help - Show this help message" \ No newline at end of file diff --git a/e2e/contracts/sui/example/connected.mv b/e2e/contracts/sui/example/connected.mv new file mode 100644 index 0000000000000000000000000000000000000000..9923fca1ff048f2501a9f4caafc750ecfda85b91 GIT binary patch literal 665 zcmbtSOODh)47FX~q^hSs%nUGMkHiTYR!9v(BP1A!nGKsLom4~UP=%5-G$&!rVYmPr zPQZ#4;sQ85AP#^ff8=LBmt6jQ=ZDh(;1FgsyC)vpm1VBBFU=SHiuRLun12&z?x!IC zOLUX(lEA}F%+TQiPw^Js2C@Kx6GZx7y_6v1~&*fgo%(qM{MLA zH0a|aM6XXWM60dNwbNcr&{zw`Da^o(fuIaBPNKGy6N8u&DxPVhn=ZXA)*pw^Q&Y<( zHiL-agHG!=TyIh=MzZO<-MVe6V(7})Z$jt$*JT%q>mt$b8a1SCi1sg@4||?|co?tr ze}sT&%8M6B$es>XTw5~|tFs*Da=es-EWp#4xDQ^Aet}-N(UV6yDrOa^#$C|I has key { fun init(ctx: &mut TxContext) { let global_config = GlobalConfig { id: object::new(ctx), + called_count: 0, }; let pool = Pool { id: object::new(ctx), @@ -43,7 +45,7 @@ fun init(ctx: &mut TxContext) { public entry fun on_call( in_coins: Coin, - _cetus_config: &GlobalConfig, + cetus_config: &mut GlobalConfig, _pool: &mut Pool, _cetus_partner: &mut Partner, _clock: &Clock, @@ -53,7 +55,10 @@ public entry fun on_call( let receiver = decode_receiver(data); // transfer the coins to the provided address - transfer::public_transfer(in_coins, receiver) + transfer::public_transfer(in_coins, receiver); + + // increment the called count + cetus_config.called_count = cetus_config.called_count + 1; } fun decode_receiver(data: vector): address { diff --git a/e2e/contracts/sui/example/token.mv b/e2e/contracts/sui/example/token.mv new file mode 100644 index 0000000000000000000000000000000000000000..de339780214e484ce6f1e4b5a4309f3a235ccad2 GIT binary patch literal 629 zcmbtSy>1gh5T2R+z1wvhCn;0Xf}lHz(xO6;#wnxYa$ZXZ_MWxg6E-hE!xPZ)8axd> zZ$QP|k+f;pl}0n)H!~W|H$Sd_I}HFOf+zn>ynHV2yy`xiU-%R4ckx005wr5ERQ{WM z4GWx+p$odCD-Z-o1_1~>vWt_Iqm_z4X~SKCc}EO+R0w2^MItvpM-XQvq8IZsw({J0 zAStve^h|2KAPdGSkj6>0AQT7W1iMp=R-;2KfB{ngmD6X$>M2n&#o5eHHKU`g;sK^= zDnN;`aRGwUk;fY|iJQq~-$g%~cOf_HCO7(KpSwP))y>;C@7*ee<}jqwve_4_ Date: Thu, 24 Apr 2025 11:50:40 -0500 Subject: [PATCH 42/49] ensure the on_call method gets called in withdraw and call E2E test --- e2e/contracts/sui/connected.mv | Bin 615 -> 665 bytes e2e/e2etests/test_sui_withdraw_and_call.go | 7 +++++ e2e/runner/sui.go | 29 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/e2e/contracts/sui/connected.mv b/e2e/contracts/sui/connected.mv index f69d65c38be6065fd429b5686ddef8f56cb1c710..9923fca1ff048f2501a9f4caafc750ecfda85b91 100644 GIT binary patch delta 254 zcmXAjy-EX75QXQ=+`qkZH@mAz)Mzgl1WBWag1&~0glJ_@5Pguag;)wciQo&^_yl5M zCB8sz)>C|^8RmT7=GmUV?EI)BA_di4@5%A5nMQka;otFr@FZvPQ%3qLoc`p#{4z|5 zHc&vZh9NewwKyH0)C8h+2Iohm7~)=rX~>c5_kiRE1rS_t(Wy(egb#tdS`_3LuWgxT z)A{`R>T-7Xc>B;E;%%R+d@Nw(=8p1DpC`zUA~j~E-KE{;$w8&I;pCOnADM`n`Yua# Q87e9o@X8LN)CgzaC15EVRR910 delta 182 zcmXBMF>b;@6h+bZ{vXen8OPWJ5*tyuK?)rWQ6wbjvVk<|sM!fcB+FGwI@S@%O_qU> zDA+`7(wy$z@91jQ$Js4d0G`Msd(pSK+4*Sy=~}jo54!QcT7jtRnpd{rg`U2#Wc2*Ub(CPx~@i4dgP{A$#UvK SluP0Qf_pX6zo`Ma3jP3e%@oxD diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 5a2d796af7..92dba1c987 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -37,6 +37,9 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { require.NoError(r, err) balanceBefore := r.SuiGetSUIBalance(suiAddress) + // query the called_count before withdraw and call + calledCountBefore := r.SuiGetConnectedCalledCount() + // create the payload payload := sui.NewCallPayload(argumentTypes, objects, message) @@ -57,4 +60,8 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { // balance after balanceAfter := r.SuiGetSUIBalance(suiAddress) require.Equal(r, balanceBefore+amount.Uint64(), balanceAfter) + + // verify the called_count increased by 1 + calledCountAfter := r.SuiGetConnectedCalledCount() + require.Equal(r, calledCountBefore+1, calledCountAfter) } diff --git a/e2e/runner/sui.go b/e2e/runner/sui.go index eb34a09a4f..38bd72ad46 100644 --- a/e2e/runner/sui.go +++ b/e2e/runner/sui.go @@ -207,6 +207,35 @@ func (r *E2ERunner) SuiMintUSDC( return r.suiExecuteTx(signer, tx) } +// SuiGetConnectedCalledCount reads the called_count from the GlobalConfig object in connected module +func (r *E2ERunner) SuiGetConnectedCalledCount() uint64 { + // Get object data + resp, err := r.Clients.Sui.SuiGetObject(r.Ctx, models.SuiGetObjectRequest{ + ObjectId: r.SuiExample.GlobalConfigID.String(), + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + + require.NoError(r, err) + require.Nil(r, resp.Error) + require.NotNil(r, resp.Data) + require.NotNil(r, resp.Data.Content) + + fields := resp.Data.Content.Fields + + // Extract called_count field from the object content + rawValue, ok := fields["called_count"] + require.True(r, ok, "missing called_count field") + + v, ok := rawValue.(string) + require.True(r, ok, "want string, got %T for called_count", rawValue) + + // #nosec G115 always in range + calledCount, err := strconv.ParseUint(v, 10, 64) + require.NoError(r, err) + + return calledCount +} + // suiExecuteDeposit executes a deposit on the SUI contract func (r *E2ERunner) suiExecuteDeposit( signer *sui.SignerSecp256k1, From 2376ca94a094605cabd68cbb17a2be4849762532 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 12:52:55 -0500 Subject: [PATCH 43/49] cancel withdrawAndCall if on_call object is not shared or immutable --- pkg/contracts/sui/errors.go | 3 ++ zetaclient/chains/sui/client/client.go | 15 +++++---- zetaclient/chains/sui/client/client_test.go | 4 +-- zetaclient/chains/sui/signer/signer_tx.go | 10 +++--- .../chains/sui/signer/withdraw_and_call.go | 12 +++++++ .../sui/signer/withdraw_and_call_test.go | 32 +++++++++++++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/pkg/contracts/sui/errors.go b/pkg/contracts/sui/errors.go index d3e1bbcbf7..a7ef696394 100644 --- a/pkg/contracts/sui/errors.go +++ b/pkg/contracts/sui/errors.go @@ -26,6 +26,9 @@ var ( // ErrParseEvent event parse error ErrParseEvent = errors.New("event parse error") + // ErrObjectOwnership is the error returned when a wrong object ownership is used in withdraw_and_call + ErrObjectOwnership = errors.New("wrong object ownership") + // retryableOutboundErrCodes are the outbound execution (if failed) error codes that are retryable. // The list is used to determine if a withdraw_and_call should fallback if rejected by the network. // Note: keep this list in sync with the actual implementation in `gateway.move` diff --git a/zetaclient/chains/sui/client/client.go b/zetaclient/chains/sui/client/client.go index 865085162b..256eb79d25 100644 --- a/zetaclient/chains/sui/client/client.go +++ b/zetaclient/chains/sui/client/client.go @@ -374,28 +374,29 @@ func (c *Client) CheckObjectIDsShared(ctx context.Context, objectIDs []string) e return fmt.Errorf("expected %d objects, but got %d", len(objectIDs), len(res)) } - return checkContainOwnedObject(res) + return CheckContainOwnedObject(res) } -func checkContainOwnedObject(res []*models.SuiObjectResponse) error { +// CheckContainOwnedObject checks if the provided object list represents Sui shared or immmutable objects +func CheckContainOwnedObject(res []*models.SuiObjectResponse) error { for i, obj := range res { if obj.Data == nil { - return fmt.Errorf("object %d is missing data", i) + return errors.Wrapf(zetasui.ErrObjectOwnership, "object %d is missing data", i) } switch owner := obj.Data.Owner.(type) { case string: if owner != immutableOwner { - return fmt.Errorf("object %d has unexpected string owner: %s", i, owner) + return errors.Wrapf(zetasui.ErrObjectOwnership, "object %d has unexpected string owner: %s", i, owner) } // Immutable is valid, continue - case map[string]interface{}: + case map[string]any: if _, isShared := owner[sharedOwner]; !isShared { - return fmt.Errorf("object %d is not shared or immutable: owner = %+v", i, owner) + return errors.Wrapf(zetasui.ErrObjectOwnership, "object %d is not shared or immutable: owner = %+v", i, owner) } // Shared is valid, continue default: - return fmt.Errorf("object %d has unknown owner type: %+v", i, obj.Data.Owner) + return errors.Wrapf(zetasui.ErrObjectOwnership, "object %d has unknown owner type: %+v", i, obj.Data.Owner) } } diff --git a/zetaclient/chains/sui/client/client_test.go b/zetaclient/chains/sui/client/client_test.go index cb5fa09583..75046f465d 100644 --- a/zetaclient/chains/sui/client/client_test.go +++ b/zetaclient/chains/sui/client/client_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestContainsOwnedObject(t *testing.T) { +func TestCheckContainOwnedObject(t *testing.T) { tests := []struct { name string input []*models.SuiObjectResponse @@ -68,7 +68,7 @@ func TestContainsOwnedObject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := checkContainOwnedObject(tt.input) + err := CheckContainOwnedObject(tt.input) if tt.wantErr { require.Error(t, err) } else { diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 8a42b8de9e..a8f3d034c8 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -187,9 +187,6 @@ func (s *Signer) buildWithdrawAndCallTx( Hex("payload", args.cp.Message). Msg("calling withdrawAndCallPTB") - // TODO: check all object IDs are share object here - // https://github.com/zeta-chain/node/issues/3755 - // build the PTB transaction return s.withdrawAndCallPTB(args) } @@ -259,7 +256,12 @@ func (s *Signer) broadcastWithdrawalWithFallback( } tx, sig, err := withdrawTxBuilder(ctx) - if err != nil { + + // we should cancel withdrawAndCall if user provided objects are not shared or immutable + if errors.Is(err, sui.ErrObjectOwnership) { + logger.Info().Any("Err", err).Msg("cancelling tx due to wrong object ownership") + return s.broadcastCancelTx(ctx, cancelTxBuilder) + } else if err != nil { return "", errors.Wrap(err, "unable to build withdraw tx") } diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 1c6a9a4aa4..2407ccbd16 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -3,6 +3,7 @@ package signer import ( "context" "encoding/base64" + "fmt" "strconv" "github.com/block-vision/sui-go-sdk/models" @@ -13,6 +14,7 @@ import ( "github.com/pkg/errors" zetasui "github.com/zeta-chain/node/pkg/contracts/sui" + "github.com/zeta-chain/node/zetaclient/chains/sui/client" ) // withdrawAndCallObjRefs contains all the object references needed for withdraw and call @@ -151,6 +153,16 @@ func (s *Signer) getWithdrawAndCallObjectRefs( return withdrawAndCallObjRefs{}, errors.Wrapf(err, "failed to get objects for %v", objectIDs) } + // should never mismatch, just a sanity check + if len(suiObjects) != len(objectIDs) { + return withdrawAndCallObjRefs{}, fmt.Errorf("expected %d objects, but got %d", len(objectIDs), len(suiObjects)) + } + + // ensure no owned objects are used for on_call + if err := client.CheckContainOwnedObject(suiObjects[2:]); err != nil { + return withdrawAndCallObjRefs{}, errors.Wrapf(err, "objects used for on_call must be shared") + } + // convert object data to object references objectRefs := make([]sui.ObjectRef, len(objectIDs)) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 62fbf4dc2b..441e755145 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -239,6 +239,12 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { Digest: digest1.String(), }, }, + { + Data: sampleSharedObjectData(t), + }, + { + Data: sampleSharedObjectData(t), + }, }, errMsg: "failed to parse object ID", }, @@ -255,6 +261,12 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { Digest: digest1.String(), }, }, + { + Data: sampleSharedObjectData(t), + }, + { + Data: sampleSharedObjectData(t), + }, }, errMsg: "failed to parse object version", }, @@ -276,6 +288,12 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { }, }, }, + { + Data: sampleSharedObjectData(t), + }, + { + Data: sampleSharedObjectData(t), + }, }, errMsg: "failed to extract initial shared version", }, @@ -317,3 +335,17 @@ func sampleObjectRef(t *testing.T) sui.ObjectRef { Digest: digest, } } + +// sampleSharedObjectData creates a sample Sui object data for a shared object +func sampleSharedObjectData(t *testing.T) *models.SuiObjectData { + return &models.SuiObjectData{ + ObjectId: sample.SuiAddress(t), + Version: "1", + Digest: sample.SuiDigest(t), + Owner: map[string]any{ + "Shared": map[string]any{ + "initial_shared_version": float64(1), + }, + }, + } +} From bd2f5da453f682276b107488752645729d244ca8 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 13:34:33 -0500 Subject: [PATCH 44/49] add sui withdrawAndCall revert E2E test --- cmd/zetae2e/local/local.go | 5 +- e2e/e2etests/e2etests.go | 34 ++++--- e2e/e2etests/test_sui_withdraw_and_call.go | 9 +- ..._sui_withdraw_and_call_revert_with_call.go | 85 ++++++++-------- .../test_sui_withdraw_revert_with_call.go | 98 +++++++++++++++++++ e2e/runner/sui.go | 3 +- 6 files changed, 174 insertions(+), 60 deletions(-) create mode 100644 e2e/e2etests/test_sui_withdraw_revert_with_call.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 1cfb4159ec..2367acf5cb 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -500,10 +500,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSuiWithdrawName, e2etests.TestSuiWithdrawRevertWithCallName, e2etests.TestSuiTokenWithdrawName, - e2etests.TestSuiDepositRestrictedName, - e2etests.TestSuiWithdrawRestrictedName, // https://github.com/zeta-chain/node/issues/3742 e2etests.TestSuiWithdrawAndCallName, + e2etests.TestSuiWithdrawAndCallRevertWithCallName, + e2etests.TestSuiDepositRestrictedName, + e2etests.TestSuiWithdrawRestrictedName, } eg.Go(suiTestRoutine(conf, deployerRunner, verbose, suiTests...)) } diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index cba7a3d026..9be38f5b6d 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -95,18 +95,19 @@ const ( /* Sui tests */ - TestSuiDepositName = "sui_deposit" - TestSuiDepositAndCallName = "sui_deposit_and_call" - TestSuiDepositAndCallRevertName = "sui_deposit_and_call_revert" - TestSuiTokenDepositName = "sui_token_deposit" // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestSuiTokenDepositAndCallName = "sui_token_deposit_and_call" // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestSuiTokenDepositAndCallRevertName = "sui_token_deposit_and_call_revert" // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestSuiWithdrawName = "sui_withdraw" - TestSuiTokenWithdrawName = "sui_token_withdraw" // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestSuiWithdrawAndCallName = "sui_withdraw_and_call" - TestSuiWithdrawRevertWithCallName = "sui_withdraw_revert_with_call" // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestSuiDepositRestrictedName = "sui_deposit_restricted" - TestSuiWithdrawRestrictedName = "sui_withdraw_restricted" + TestSuiDepositName = "sui_deposit" + TestSuiDepositAndCallName = "sui_deposit_and_call" + TestSuiDepositAndCallRevertName = "sui_deposit_and_call_revert" + TestSuiTokenDepositName = "sui_token_deposit" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiTokenDepositAndCallName = "sui_token_deposit_and_call" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiTokenDepositAndCallRevertName = "sui_token_deposit_and_call_revert" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiWithdrawName = "sui_withdraw" + TestSuiTokenWithdrawName = "sui_token_withdraw" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiWithdrawAndCallName = "sui_withdraw_and_call" + TestSuiWithdrawRevertWithCallName = "sui_withdraw_revert_with_call" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiWithdrawAndCallRevertWithCallName = "sui_withdraw_and_call_revert_with_call" // #nosec G101: Potential hardcoded credentials (gosec), not a credential + TestSuiDepositRestrictedName = "sui_deposit_restricted" + TestSuiWithdrawRestrictedName = "sui_withdraw_restricted" /* Bitcoin tests @@ -879,6 +880,15 @@ var AllE2ETests = []runner.E2ETest{ TestSuiWithdrawRevertWithCall, runner.WithMinimumVersion("v30.0.0"), ), + runner.NewE2ETest( + TestSuiWithdrawAndCallRevertWithCallName, + "withdraw SUI from ZEVM and call a contract that reverts with a onRevert call", + []runner.ArgDefinition{ + {Description: "amount in mist", DefaultValue: "1000000"}, + }, + TestSuiWithdrawAndCallRevertWithCall, + runner.WithMinimumVersion("v30.0.0"), + ), runner.NewE2ETest( TestSuiTokenWithdrawName, "withdraw fungible token from ZEVM", diff --git a/e2e/e2etests/test_sui_withdraw_and_call.go b/e2e/e2etests/test_sui_withdraw_and_call.go index 92dba1c987..95fbc94f5e 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call.go @@ -2,8 +2,10 @@ package e2etests import ( "encoding/hex" + "math/big" "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" @@ -48,7 +50,12 @@ func TestSuiWithdrawAndCall(r *runner.E2ERunner, args []string) { r.ApproveSUIZRC20(r.GatewayZEVMAddr) // perform the withdraw and call - tx := r.SuiWithdrawAndCallSUI(targetPackageID, amount, payload) + tx := r.SuiWithdrawAndCallSUI( + targetPackageID, + amount, + payload, + gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, + ) r.Logger.EVMTransaction(*tx, "withdraw_and_call") // ASSERT diff --git a/e2e/e2etests/test_sui_withdraw_and_call_revert_with_call.go b/e2e/e2etests/test_sui_withdraw_and_call_revert_with_call.go index 4244fa05e7..87d00f15e3 100644 --- a/e2e/e2etests/test_sui_withdraw_and_call_revert_with_call.go +++ b/e2e/e2etests/test_sui_withdraw_and_call_revert_with_call.go @@ -1,98 +1,95 @@ package e2etests import ( + "encoding/hex" "math/big" - "time" - "cosmossdk.io/math" "github.com/ethereum/go-ethereum/accounts/abi/bind" "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" + "github.com/zeta-chain/node/pkg/contracts/sui" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -// TestSuiWithdrawRevertWithCall executes withdraw on zevm gateway. -// The outbound is rejected by Sui network, and 'nonce_increase' is called instead to cancel the tx. -func TestSuiWithdrawRevertWithCall(r *runner.E2ERunner, args []string) { +// TestSuiWithdrawAndCallRevertWithCall executes withdraw and call on zevm gateway. +// The outbound is rejected by the connected module due to invalid payload (invalid address), +// and 'onRevert' is called instead to handle the revert. +func TestSuiWithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) - amount := utils.ParseBigInt(r, args[0]) // ARRANGE - // given signer - signer, err := r.Account.SuiSigner() - require.NoError(r, err, "get deployer signer") - signerBalanceBefore := r.SuiGetSUIBalance(signer.Address()) + // Given target package ID (example package) and a SUI amount + targetPackageID := r.SuiExample.PackageID.String() + amount := utils.ParseBigInt(r, args[0]) + + // Given example contract on_call function arguments + argumentTypes := []string{ + r.SuiExample.TokenType.String(), + } + objects := []string{ + r.SuiExample.GlobalConfigID.String(), + r.SuiExample.PoolID.String(), + r.SuiExample.PartnerID.String(), + r.SuiExample.ClockID.String(), + } + + // define an invalid address to cause 'on_call' failure + invalidAddress := "8f569597ebca884b784d32678a6f" + message, err := hex.DecodeString(invalidAddress) + require.NoError(r, err) // given ZEVM revert address (the dApp) dAppAddress := r.TestDAppV2ZEVMAddr dAppBalanceBefore, err := r.SUIZRC20.BalanceOf(&bind.CallOpts{}, dAppAddress) require.NoError(r, err) - // given random payload - payload := randomPayload(r) - r.AssertTestDAppEVMCalled(false, payload, amount) - - // retrieve current zrc20 gas limit - oldGasLimit, err := r.SUIZRC20.GASLIMIT(&bind.CallOpts{}) - require.NoError(r, err) - r.Logger.Info("current gas limit: %s", oldGasLimit.String()) - - // set a low ZRC20 gas limit so gasBudget will be low: "1000000" - // withdraw tx will be rejected due to execution error "InsufficientGas" - lowGasLimit := math.NewUintFromBigInt(big.NewInt(1000)) - _, err = r.ZetaTxServer.UpdateZRC20GasLimit(r.SUIZRC20Addr, lowGasLimit) - require.NoError(r, err) + // given random payload for 'onRevert' + payloadOnRevert := randomPayload(r) + r.AssertTestDAppEVMCalled(false, payloadOnRevert, amount) - // wait for the new gas limit to take effect - utils.WaitForZetaBlocks(r.Ctx, r, r.ZEVMClient, 1, 10*time.Second) + // create the payload for 'on_call' + payloadOnCall := sui.NewCallPayload(argumentTypes, objects, message) // ACT - // approve the ZRC20 + // approve SUI ZRC20 token r.ApproveSUIZRC20(r.GatewayZEVMAddr) - // perform the withdraw with revert options - tx := r.SuiWithdrawSUI( - signer.Address(), + // perform the withdraw and call with revert options + tx := r.SuiWithdrawAndCallSUI( + targetPackageID, amount, + payloadOnCall, gatewayzevm.RevertOptions{ CallOnRevert: true, RevertAddress: dAppAddress, - RevertMessage: []byte(payload), + RevertMessage: []byte(payloadOnRevert), OnRevertGasLimit: big.NewInt(0), }, ) - r.Logger.EVMTransaction(*tx, "withdraw") + r.Logger.EVMTransaction(*tx, "withdraw_and_call") // ASSERT - // wait for the CCTX to be mined + // wait for the cctx to be reverted cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "withdraw") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted) // should have called 'onRevert' - r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) + r.AssertTestDAppZEVMCalled(true, payloadOnRevert, big.NewInt(0)) // sender and message should match sender, err := r.TestDAppV2ZEVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payload), + []byte(payloadOnRevert), ) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, sender) - // signer balance should remain unchanged in Sui chain - signerBalanceAfter := r.SuiGetSUIBalance(signer.Address()) - require.Equal(r, signerBalanceBefore, signerBalanceAfter) - // the dApp address should get reverted amount dAppBalanceAfter, err := r.SUIZRC20.BalanceOf(&bind.CallOpts{}, dAppAddress) require.NoError(r, err) require.Equal(r, amount.Int64(), dAppBalanceAfter.Int64()-dAppBalanceBefore.Int64()) - - // TEARDOWN - // restore old gas limit - _, err = r.ZetaTxServer.UpdateZRC20GasLimit(r.SUIZRC20Addr, math.NewUintFromBigInt(oldGasLimit)) - require.NoError(r, err, "failed to restore gas limit") } diff --git a/e2e/e2etests/test_sui_withdraw_revert_with_call.go b/e2e/e2etests/test_sui_withdraw_revert_with_call.go new file mode 100644 index 0000000000..4244fa05e7 --- /dev/null +++ b/e2e/e2etests/test_sui_withdraw_revert_with_call.go @@ -0,0 +1,98 @@ +package e2etests + +import ( + "math/big" + "time" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "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" +) + +// TestSuiWithdrawRevertWithCall executes withdraw on zevm gateway. +// The outbound is rejected by Sui network, and 'nonce_increase' is called instead to cancel the tx. +func TestSuiWithdrawRevertWithCall(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + amount := utils.ParseBigInt(r, args[0]) + + // ARRANGE + // given signer + signer, err := r.Account.SuiSigner() + require.NoError(r, err, "get deployer signer") + signerBalanceBefore := r.SuiGetSUIBalance(signer.Address()) + + // given ZEVM revert address (the dApp) + dAppAddress := r.TestDAppV2ZEVMAddr + dAppBalanceBefore, err := r.SUIZRC20.BalanceOf(&bind.CallOpts{}, dAppAddress) + require.NoError(r, err) + + // given random payload + payload := randomPayload(r) + r.AssertTestDAppEVMCalled(false, payload, amount) + + // retrieve current zrc20 gas limit + oldGasLimit, err := r.SUIZRC20.GASLIMIT(&bind.CallOpts{}) + require.NoError(r, err) + r.Logger.Info("current gas limit: %s", oldGasLimit.String()) + + // set a low ZRC20 gas limit so gasBudget will be low: "1000000" + // withdraw tx will be rejected due to execution error "InsufficientGas" + lowGasLimit := math.NewUintFromBigInt(big.NewInt(1000)) + _, err = r.ZetaTxServer.UpdateZRC20GasLimit(r.SUIZRC20Addr, lowGasLimit) + require.NoError(r, err) + + // wait for the new gas limit to take effect + utils.WaitForZetaBlocks(r.Ctx, r, r.ZEVMClient, 1, 10*time.Second) + + // ACT + // approve the ZRC20 + r.ApproveSUIZRC20(r.GatewayZEVMAddr) + + // perform the withdraw with revert options + tx := r.SuiWithdrawSUI( + signer.Address(), + amount, + gatewayzevm.RevertOptions{ + CallOnRevert: true, + RevertAddress: dAppAddress, + RevertMessage: []byte(payload), + OnRevertGasLimit: big.NewInt(0), + }, + ) + r.Logger.EVMTransaction(*tx, "withdraw") + + // ASSERT + // 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) + + // should have called 'onRevert' + r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) + + // sender and message should match + sender, err := r.TestDAppV2ZEVM.SenderWithMessage( + &bind.CallOpts{}, + []byte(payload), + ) + require.NoError(r, err) + require.Equal(r, r.ZEVMAuth.From, sender) + + // signer balance should remain unchanged in Sui chain + signerBalanceAfter := r.SuiGetSUIBalance(signer.Address()) + require.Equal(r, signerBalanceBefore, signerBalanceAfter) + + // the dApp address should get reverted amount + dAppBalanceAfter, err := r.SUIZRC20.BalanceOf(&bind.CallOpts{}, dAppAddress) + require.NoError(r, err) + require.Equal(r, amount.Int64(), dAppBalanceAfter.Int64()-dAppBalanceBefore.Int64()) + + // TEARDOWN + // restore old gas limit + _, err = r.ZetaTxServer.UpdateZRC20GasLimit(r.SUIZRC20Addr, math.NewUintFromBigInt(oldGasLimit)) + require.NoError(r, err, "failed to restore gas limit") +} diff --git a/e2e/runner/sui.go b/e2e/runner/sui.go index 38bd72ad46..ffcf04d635 100644 --- a/e2e/runner/sui.go +++ b/e2e/runner/sui.go @@ -71,6 +71,7 @@ func (r *E2ERunner) SuiWithdrawAndCallSUI( receiver string, amount *big.Int, payload sui.CallPayload, + revertOptions gatewayzevm.RevertOptions, ) *ethtypes.Transaction { receiverBytes, err := hex.DecodeString(receiver[2:]) require.NoError(r, err, "receiver: "+receiver[2:]) @@ -89,7 +90,7 @@ func (r *E2ERunner) SuiWithdrawAndCallSUI( IsArbitraryCall: false, GasLimit: big.NewInt(20000), }, - gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, + revertOptions, ) require.NoError(r, err) From b96c4d4cd5f8dfc4b8d5dfedce7533d3e9437d7d Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 13:53:15 -0500 Subject: [PATCH 45/49] add readme file to briefly describe how sui withdrawAndCall works using PTB --- e2e/contracts/sui/example/README.md | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 e2e/contracts/sui/example/README.md diff --git a/e2e/contracts/sui/example/README.md b/e2e/contracts/sui/example/README.md new file mode 100644 index 0000000000..ddbdfb9205 --- /dev/null +++ b/e2e/contracts/sui/example/README.md @@ -0,0 +1,67 @@ +# SUI WithdrawAndCall with PTB Transactions + +This document explains how the SUI `withdrawAndCall` functionality works using Programmable Transaction Blocks (PTB) in the ZetaChain protocol. + +## Overview + +The `withdrawAndCall` operation in ZetaChain allows users to withdraw tokens from ZEVM to the Sui blockchain and simultaneously calls a `on_call` function in the `connected` module on the Sui side. + +This is implemented as a single atomic transaction using Sui's Programmable Transaction Blocks (PTB). + +## Transaction Flow + +1. **User Initiates Withdrawal**: A user initiates a withdrawal from ZEVM to Sui with a `on_call` payload. + +2. **ZEVM Processing**: The ZEVM gateway processes the withdrawal request and prepares the transaction. + +3. **PTB Construction**: A Programmable Transaction Block is constructed with the following steps: + - **Withdraw**: The first command in the PTB is the `withdraw_impl` function call, which: + - Verifies the withdrawal parameters + - Withdraw and returns two coin objects: the main withdrawn coins and the gas budget coins + - **Gas Budget Transfer**: The second command transfers the gas budget coins to the TSS address to cover transaction fees. + - **Connected Module Call**: The third command calls the `on_call` function in the connected module, passing: + - The withdrawn coins + - The call payload from the user + - Any additional parameters required by the connected module + +4. **Transaction Execution**: The entire PTB is executed atomically on the Sui blockchain. + +## PTB Structure + +The PTB for a `withdrawAndCall` transaction consists of three main commands: + +``` +PTB { + // Command 0: Withdraw Implementation + MoveCall { + package: gateway_package_id, + module: gateway_module, + function: withdraw_impl, + arguments: [ + gateway_object_ref, + withdraw_cap_object_ref, + coin_type, + amount, + nonce, + gas_budget + ] + } + + // Command 1: Gas Budget Transfer + TransferObjects { + from: withdraw_impl_result[1], // Gas budget coins + to: tss_address + } + + // Command 2: Connected Module Call + MoveCall { + package: target_package_id, + module: connected_module, + function: on_call, + arguments: [ + withdraw_impl_result[0], // Main withdrawn coins + on_call_payload + ] + } +} +``` \ No newline at end of file From e2a9995cbc3b8a7742d4d5dfd26833529803ef92 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 24 Apr 2025 14:41:23 -0500 Subject: [PATCH 46/49] fix CI unit test failure --- pkg/contracts/sui/gateway_test.go | 7 ++++--- pkg/contracts/sui/withdraw_and_call_test.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/contracts/sui/gateway_test.go b/pkg/contracts/sui/gateway_test.go index 7f6aba7f9b..bc26bc4b45 100644 --- a/pkg/contracts/sui/gateway_test.go +++ b/pkg/contracts/sui/gateway_test.go @@ -386,9 +386,10 @@ func Test_ParseOutboundEvent(t *testing.T) { EventType: WithdrawAndCallEvent, content: WithdrawAndCallPTB{ MoveCall: MoveCall{ - PackageID: packageID, - Module: moduleName, - Function: FuncWithdrawImpl, + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + ArgIndexes: ptbWithdrawImplArgIndexes, }, Amount: math.NewUint(200), Nonce: 123, diff --git a/pkg/contracts/sui/withdraw_and_call_test.go b/pkg/contracts/sui/withdraw_and_call_test.go index 3f6e5d20d5..44c657c10d 100644 --- a/pkg/contracts/sui/withdraw_and_call_test.go +++ b/pkg/contracts/sui/withdraw_and_call_test.go @@ -110,9 +110,10 @@ func Test_parseWithdrawAndCallPTB(t *testing.T) { response: createPTBResponse(txHash, packageID, amountStr, nonceStr), want: WithdrawAndCallPTB{ MoveCall: MoveCall{ - PackageID: packageID, - Module: moduleName, - Function: FuncWithdrawImpl, + PackageID: packageID, + Module: moduleName, + Function: FuncWithdrawImpl, + ArgIndexes: ptbWithdrawImplArgIndexes, }, Amount: math.NewUint(100), Nonce: 2, From 177b255444130b8963d8d05fa805894840ef5120 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 25 Apr 2025 11:17:33 -0500 Subject: [PATCH 47/49] name and log improvement --- zetaclient/chains/sui/signer/signer_tx.go | 23 ++++++++-------- .../chains/sui/signer/withdraw_and_call.go | 26 +++++++++---------- .../sui/signer/withdraw_and_call_test.go | 20 +++++++------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index a8f3d034c8..7c582a59bf 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -161,7 +161,7 @@ func (s *Signer) buildWithdrawAndCallTx( if err != nil { return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") } - wacRefs.suiCoinObjRef = suiCoinObjRef + wacRefs.suiCoin = suiCoinObjRef // all PTB arguments args := withdrawAndCallPTBArgs{ @@ -171,7 +171,7 @@ func (s *Signer) buildWithdrawAndCallTx( nonce: params.TssNonce, gasBudget: gasBudget, receiver: params.Receiver, - cp: cp, + payload: cp, } // print PTB transaction parameters @@ -179,12 +179,12 @@ func (s *Signer) buildWithdrawAndCallTx( Str(logs.FieldMethod, "buildWithdrawAndCallTx"). Uint64(logs.FieldNonce, args.nonce). Str(logs.FieldCoinType, args.coinType). - Uint64("amount", args.amount). - Str("receiver", args.receiver). - Uint64("gas_budget", args.gasBudget). - Any("type_args", args.cp.TypeArgs). - Any("object_ids", args.cp.ObjectIDs). - Hex("payload", args.cp.Message). + Uint64("tx.amount", args.amount). + Str("tx.receiver", args.receiver). + Uint64("tx.gas_budget", args.gasBudget). + Strs("tx.type_args", args.payload.TypeArgs). + Strs("tx.object_ids", args.payload.ObjectIDs). + Hex("tx.payload", args.payload.Message). Msg("calling withdrawAndCallPTB") // build the PTB transaction @@ -258,10 +258,11 @@ func (s *Signer) broadcastWithdrawalWithFallback( tx, sig, err := withdrawTxBuilder(ctx) // we should cancel withdrawAndCall if user provided objects are not shared or immutable - if errors.Is(err, sui.ErrObjectOwnership) { - logger.Info().Any("Err", err).Msg("cancelling tx due to wrong object ownership") + switch { + case errors.Is(err, sui.ErrObjectOwnership): + logger.Info().Err(err).Msg("cancelling tx due to wrong object ownership") return s.broadcastCancelTx(ctx, cancelTxBuilder) - } else if err != nil { + case err != nil: return "", errors.Wrap(err, "unable to build withdraw tx") } diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 2407ccbd16..6575a932e9 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -19,10 +19,10 @@ import ( // withdrawAndCallObjRefs contains all the object references needed for withdraw and call type withdrawAndCallObjRefs struct { - gatewayObjRef sui.ObjectRef - withdrawCapObjRef sui.ObjectRef - onCallObjectRefs []sui.ObjectRef - suiCoinObjRef sui.ObjectRef + gateway sui.ObjectRef + withdrawCap sui.ObjectRef + onCall []sui.ObjectRef + suiCoin sui.ObjectRef } // withdrawAndCallPTBArgs contains all the arguments needed for withdraw and call @@ -33,7 +33,7 @@ type withdrawAndCallPTBArgs struct { nonce uint64 gasBudget uint64 receiver string - cp zetasui.CallPayload + payload zetasui.CallPayload } // withdrawAndCallPTB builds unsigned withdraw and call PTB Sui transaction @@ -61,8 +61,8 @@ func (s *Signer) withdrawAndCallPTB(args withdrawAndCallPTBArgs) (tx models.TxnM ptb, gatewayPackageID, gatewayModule, - args.gatewayObjRef, - args.withdrawCapObjRef, + args.gateway, + args.withdrawCap, args.coinType, args.amount, args.nonce, @@ -99,8 +99,8 @@ func (s *Signer) withdrawAndCallPTB(args withdrawAndCallPTBArgs) (tx models.TxnM args.receiver, args.coinType, argWithdrawnCoins, - args.onCallObjectRefs, - args.cp, + args.onCall, + args.payload, ) if err != nil { return tx, err @@ -114,7 +114,7 @@ func (s *Signer) withdrawAndCallPTB(args withdrawAndCallPTBArgs) (tx models.TxnM signerAddr, pt, []*sui.ObjectRef{ - &args.suiCoinObjRef, + &args.suiCoin, }, args.gasBudget, suiclient.DefaultGasPrice, @@ -203,9 +203,9 @@ func (s *Signer) getWithdrawAndCallObjectRefs( } return withdrawAndCallObjRefs{ - gatewayObjRef: objectRefs[0], - withdrawCapObjRef: objectRefs[1], - onCallObjectRefs: objectRefs[2:], + gateway: objectRefs[0], + withdrawCap: objectRefs[1], + onCall: objectRefs[2:], }, nil } diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 441e755145..9c4c7e17d2 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -20,17 +20,17 @@ func newTestWACPTBArgs( ) withdrawAndCallPTBArgs { return withdrawAndCallPTBArgs{ withdrawAndCallObjRefs: withdrawAndCallObjRefs{ - gatewayObjRef: gatewayObjRef, - withdrawCapObjRef: withdrawCapObjRef, - onCallObjectRefs: onCallObjectRefs, - suiCoinObjRef: suiCoinObjRef, + gateway: gatewayObjRef, + withdrawCap: withdrawCapObjRef, + onCall: onCallObjectRefs, + suiCoin: suiCoinObjRef, }, coinType: string(zetasui.SUI), amount: 1000000, nonce: 1, gasBudget: 2000000, receiver: sample.SuiAddress(t), - cp: zetasui.CallPayload{ + payload: zetasui.CallPayload{ TypeArgs: []string{string(zetasui.SUI)}, ObjectIDs: []string{sample.SuiAddress(t)}, Message: []byte("test message"), @@ -67,7 +67,7 @@ func Test_withdrawAndCallPTB(t *testing.T) { withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}, ) - args.cp.Message = []byte{} + args.payload.Message = []byte{} return args }(), }, @@ -111,7 +111,7 @@ func Test_withdrawAndCallPTB(t *testing.T) { withdrawCapObjRef, []sui.ObjectRef{onCallObjRef}, ) - args.cp.TypeArgs[0] = "invalid_type_argument" + args.payload.TypeArgs[0] = "invalid_type_argument" return args }(), errMsg: "invalid type argument", @@ -199,17 +199,17 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { }, }, expected: withdrawAndCallObjRefs{ - gatewayObjRef: sui.ObjectRef{ + gateway: sui.ObjectRef{ ObjectId: gatewayID, Version: 1, Digest: digest1, }, - withdrawCapObjRef: sui.ObjectRef{ + withdrawCap: sui.ObjectRef{ ObjectId: withdrawCapID, Version: 2, Digest: digest2, }, - onCallObjectRefs: []sui.ObjectRef{ + onCall: []sui.ObjectRef{ { ObjectId: onCallObjectID, Version: 1, From d55769230f3dedc0e3cef8269b4ef4487f30974e Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 25 Apr 2025 12:24:19 -0500 Subject: [PATCH 48/49] move GetSuiCoinObjectRef RPC call into getWithdrawAndCallObjectRefs; add more description on PTB gas budget transfer command --- e2e/contracts/sui/example/README.md | 2 ++ zetaclient/chains/sui/signer/signer_tx.go | 7 ------- zetaclient/chains/sui/signer/withdraw_and_call.go | 7 +++++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/e2e/contracts/sui/example/README.md b/e2e/contracts/sui/example/README.md index ddbdfb9205..c3666cb965 100644 --- a/e2e/contracts/sui/example/README.md +++ b/e2e/contracts/sui/example/README.md @@ -19,6 +19,8 @@ This is implemented as a single atomic transaction using Sui's Programmable Tran - Verifies the withdrawal parameters - Withdraw and returns two coin objects: the main withdrawn coins and the gas budget coins - **Gas Budget Transfer**: The second command transfers the gas budget coins to the TSS address to cover transaction fees. + - The gas budget is the SUI coin withdrawn from sui vault, together with withdrawn CCTX's coinType. + - The gas budget needs to be forwarded to TSS address to cover the transaction fee. - **Connected Module Call**: The third command calls the `on_call` function in the connected module, passing: - The withdrawn coins - The call payload from the user diff --git a/zetaclient/chains/sui/signer/signer_tx.go b/zetaclient/chains/sui/signer/signer_tx.go index 7c582a59bf..9203d49132 100644 --- a/zetaclient/chains/sui/signer/signer_tx.go +++ b/zetaclient/chains/sui/signer/signer_tx.go @@ -156,13 +156,6 @@ func (s *Signer) buildWithdrawAndCallTx( return models.TxnMetaData{}, errors.Wrap(err, "unable to get object references") } - // get latest TSS SUI coin object ref for gas payment - suiCoinObjRef, err := s.client.GetSuiCoinObjectRef(ctx, s.TSS().PubKey().AddressSui()) - if err != nil { - return models.TxnMetaData{}, errors.Wrap(err, "unable to get TSS SUI coin object") - } - wacRefs.suiCoin = suiCoinObjRef - // all PTB arguments args := withdrawAndCallPTBArgs{ withdrawAndCallObjRefs: wacRefs, diff --git a/zetaclient/chains/sui/signer/withdraw_and_call.go b/zetaclient/chains/sui/signer/withdraw_and_call.go index 6575a932e9..0bcf38badf 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call.go @@ -202,10 +202,17 @@ func (s *Signer) getWithdrawAndCallObjectRefs( } } + // get latest TSS SUI coin object ref for gas payment + suiCoinObjRef, err := s.client.GetSuiCoinObjectRef(ctx, s.TSS().PubKey().AddressSui()) + if err != nil { + return withdrawAndCallObjRefs{}, errors.Wrap(err, "unable to get TSS SUI coin object") + } + return withdrawAndCallObjRefs{ gateway: objectRefs[0], withdrawCap: objectRefs[1], onCall: objectRefs[2:], + suiCoin: suiCoinObjRef, }, nil } From 85449be5fb2175fca13cff1ac146cb05955acc98 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 25 Apr 2025 12:54:48 -0500 Subject: [PATCH 49/49] fix CI unit test --- .../chains/sui/signer/withdraw_and_call_test.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/sui/signer/withdraw_and_call_test.go b/zetaclient/chains/sui/signer/withdraw_and_call_test.go index 9c4c7e17d2..27360e2bb5 100644 --- a/zetaclient/chains/sui/signer/withdraw_and_call_test.go +++ b/zetaclient/chains/sui/signer/withdraw_and_call_test.go @@ -141,6 +141,8 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { require.NoError(t, err) onCallObjectID, err := sui.ObjectIdFromHex(sample.SuiAddress(t)) require.NoError(t, err) + suiCoinID, err := sui.ObjectIdFromHex(sample.SuiAddress(t)) + require.NoError(t, err) // create test object digests digest1, err := sui.NewBase58(sample.SuiDigest(t)) @@ -149,6 +151,15 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { require.NoError(t, err) digest3, err := sui.NewBase58(sample.SuiDigest(t)) require.NoError(t, err) + digest4, err := sui.NewBase58(sample.SuiDigest(t)) + require.NoError(t, err) + + // create SUI coin object reference + suiCoinObjRef := sui.ObjectRef{ + ObjectId: suiCoinID, + Version: 1, + Digest: digest4, + } tests := []struct { name string @@ -216,6 +227,7 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { Digest: digest3, }, }, + suiCoin: suiCoinObjRef, }, }, { @@ -304,9 +316,10 @@ func Test_getWithdrawAndCallObjectRefs(t *testing.T) { // ARRANGE ts := newTestSuite(t) - // setup mock + // setup RPC mock ctx := context.Background() ts.SuiMock.On("SuiMultiGetObjects", ctx, mock.Anything).Return(tt.mockObjects, tt.mockError) + ts.SuiMock.On("GetSuiCoinObjectRef", ctx, mock.Anything).Maybe().Return(suiCoinObjRef, nil) // ACT got, err := ts.Signer.getWithdrawAndCallObjectRefs(ctx, tt.withdrawCapID, tt.onCallObjectIDs)