From b7fed900f807da31146f53bfa30b2bb2ad36dc6c Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Wed, 25 Jun 2025 15:10:22 +0300 Subject: [PATCH 1/9] initial commit --- clients/chain/chain.go | 3 + clients/errors.go | 21 + clients/ethereum/client.go | 16 +- clients/ethereum/client_test.go | 18 +- clients/ethereum/errors.go | 19 +- .../ethereum/wrappers/ethereumChainWrapper.go | 2 +- clients/sui/client.go | 433 ++++++++++ clients/sui/dtos/contractStructs.go | 19 + clients/sui/dtos/sharedOwner.go | 7 + .../dtos/transactionBlockResponseResult.go | 12 + clients/sui/errors.go | 17 + clients/sui/interface.go | 47 ++ clients/sui/suiClientDataGetter.go | 780 ++++++++++++++++++ clients/sui/testdata/nok-sui-seed | 1 + clients/sui/testdata/ok-sui-seed | 2 + clients/sui/transactionHandler.go | 39 + config/config.go | 7 + core/batchProcessor/batchProcessor.go | 13 +- go.mod | 25 +- go.sum | 59 +- testsCommon/addressGenerators.go | 24 + 21 files changed, 1501 insertions(+), 63 deletions(-) create mode 100644 clients/sui/client.go create mode 100644 clients/sui/dtos/contractStructs.go create mode 100644 clients/sui/dtos/sharedOwner.go create mode 100644 clients/sui/dtos/transactionBlockResponseResult.go create mode 100644 clients/sui/errors.go create mode 100644 clients/sui/interface.go create mode 100644 clients/sui/suiClientDataGetter.go create mode 100644 clients/sui/testdata/nok-sui-seed create mode 100644 clients/sui/testdata/ok-sui-seed create mode 100644 clients/sui/transactionHandler.go diff --git a/clients/chain/chain.go b/clients/chain/chain.go index 0e6e8981..9a1aa3da 100644 --- a/clients/chain/chain.go +++ b/clients/chain/chain.go @@ -32,6 +32,9 @@ const ( // Polygon is the string representation of the Polygon chain Polygon Chain = "Polygon" + + // Sui is the string representation of the Sui chain + Sui Chain = "Sui" ) // ToLower returns the lowercase string of chain diff --git a/clients/errors.go b/clients/errors.go index c4652e8c..b1b06b4f 100644 --- a/clients/errors.go +++ b/clients/errors.go @@ -38,4 +38,25 @@ var ( // ErrNilCryptoHandler signals that a nil crypto handler was provided ErrNilCryptoHandler = errors.New("nil crypto handler") + + // ErrInvalidGasLimit signals that gas limit is invalid + ErrInvalidGasLimit = errors.New("invalid gas limit") + + // ErrNilClientWrapper signals that a nil client wrapper was provided + ErrNilClientWrapper = errors.New("nil client wrapper") + + // ErrDepositsAndBatchDepositsCountDiffer signals that the deposits count and batch deposits count differ + ErrDepositsAndBatchDepositsCountDiffer = errors.New("deposits and batch.DepositsCount differs") + + // ErrStatusIsNotFinal signals that the status is not final + ErrStatusIsNotFinal = errors.New("status is not final") + + // ErrQuorumNotReached signals that the quorum was not reached + ErrQuorumNotReached = errors.New("quorum not reached") + + // ErrNilBroadcaster signals that a nil broadcaster was provided + ErrNilBroadcaster = errors.New("nil broadcaster") + + // ErrNilSignaturesHolder signals that a nil signature holder was provided + ErrNilSignaturesHolder = errors.New("nil signatures holder") ) diff --git a/clients/ethereum/client.go b/clients/ethereum/client.go index fc45358f..efbccf35 100644 --- a/clients/ethereum/client.go +++ b/clients/ethereum/client.go @@ -100,7 +100,7 @@ func NewEthereumClient(args ArgsEthereumClient) (*client, error) { func checkArgs(args ArgsEthereumClient) error { if check.IfNil(args.ClientWrapper) { - return errNilClientWrapper + return clients.ErrNilClientWrapper } if check.IfNil(args.Erc20ContractsHandler) { return errNilERC20ContractsHandler @@ -112,7 +112,7 @@ func checkArgs(args ArgsEthereumClient) error { return clients.ErrNilAddressConverter } if check.IfNil(args.Broadcaster) { - return errNilBroadcaster + return clients.ErrNilBroadcaster } if check.IfNil(args.CryptoHandler) { return clients.ErrNilCryptoHandler @@ -121,16 +121,16 @@ func checkArgs(args ArgsEthereumClient) error { return clients.ErrNilTokensMapper } if check.IfNil(args.SignatureHolder) { - return errNilSignaturesHolder + return clients.ErrNilSignaturesHolder } if check.IfNil(args.GasHandler) { return errNilGasHandler } if args.TransferGasLimitBase == 0 { - return errInvalidGasLimit + return clients.ErrInvalidGasLimit } if args.TransferGasLimitForEach == 0 { - return errInvalidGasLimit + return clients.ErrInvalidGasLimit } if args.ClientAvailabilityAllowDelta < minClientAvailabilityAllowDelta { return fmt.Errorf("%w for args.AllowedDelta, got: %d, minimum: %d", @@ -157,7 +157,7 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf } if int(batch.DepositsCount) != len(deposits) { return nil, false, fmt.Errorf("%w, batch.DepositsCount: %d, fetched deposits len: %d", - errDepositsAndBatchDepositsCountDiffer, batch.DepositsCount, len(deposits)) + clients.ErrDepositsAndBatchDepositsCountDiffer, batch.DepositsCount, len(deposits)) } transferBatch := &bridgeCore.TransferBatch{ @@ -359,7 +359,7 @@ func (c *client) ExecuteTransfer( signatures := c.signatureHolder.Signatures(msgHash.Bytes()) if len(signatures) < quorum { - return "", fmt.Errorf("%w num signatures: %d, quorum: %d", errQuorumNotReached, len(signatures), quorum) + return "", fmt.Errorf("%w num signatures: %d, quorum: %d", clients.ErrQuorumNotReached, len(signatures), quorum) } if len(signatures) > quorum { c.log.Debug("reducing the size of the signatures set", @@ -524,7 +524,7 @@ func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([ return nil, err } if !isFinal { - return nil, errStatusIsNotFinal + return nil, clients.ErrStatusIsNotFinal } return buff, nil diff --git a/clients/ethereum/client_test.go b/clients/ethereum/client_test.go index a13922c7..a975ff8e 100644 --- a/clients/ethereum/client_test.go +++ b/clients/ethereum/client_test.go @@ -99,7 +99,7 @@ func TestNewEthereumClient(t *testing.T) { args.ClientWrapper = nil c, err := NewEthereumClient(args) - assert.Equal(t, errNilClientWrapper, err) + assert.Equal(t, clients.ErrNilClientWrapper, err) assert.True(t, check.IfNil(c)) }) t.Run("nil erc20 contracts handler", func(t *testing.T) { @@ -131,7 +131,7 @@ func TestNewEthereumClient(t *testing.T) { args.Broadcaster = nil c, err := NewEthereumClient(args) - assert.Equal(t, errNilBroadcaster, err) + assert.Equal(t, clients.ErrNilBroadcaster, err) assert.True(t, check.IfNil(c)) }) t.Run("nil crypto handler", func(t *testing.T) { @@ -155,7 +155,7 @@ func TestNewEthereumClient(t *testing.T) { args.SignatureHolder = nil c, err := NewEthereumClient(args) - assert.Equal(t, errNilSignaturesHolder, err) + assert.Equal(t, clients.ErrNilSignaturesHolder, err) assert.True(t, check.IfNil(c)) }) t.Run("nil gas handler", func(t *testing.T) { @@ -171,7 +171,7 @@ func TestNewEthereumClient(t *testing.T) { args.TransferGasLimitBase = 0 c, err := NewEthereumClient(args) - assert.Equal(t, errInvalidGasLimit, err) + assert.Equal(t, clients.ErrInvalidGasLimit, err) assert.True(t, check.IfNil(c)) }) t.Run("0 transfer gas limit for each", func(t *testing.T) { @@ -179,7 +179,7 @@ func TestNewEthereumClient(t *testing.T) { args.TransferGasLimitForEach = 0 c, err := NewEthereumClient(args) - assert.Equal(t, errInvalidGasLimit, err) + assert.Equal(t, clients.ErrInvalidGasLimit, err) assert.True(t, check.IfNil(c)) }) t.Run("invalid ClientAvailabilityAllowDelta should error", func(t *testing.T) { @@ -266,7 +266,7 @@ func TestClient_GetBatch(t *testing.T) { } batch, isFinal, err := c.GetBatch(context.Background(), 1) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errDepositsAndBatchDepositsCountDiffer)) + assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 2, fetched deposits len: 0")) assert.False(t, isFinal) }) @@ -288,7 +288,7 @@ func TestClient_GetBatch(t *testing.T) { } batch, isFinal, err := c.GetBatch(context.Background(), 1) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errDepositsAndBatchDepositsCountDiffer)) + assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 2, fetched deposits len: 1")) assert.False(t, isFinal) }) @@ -655,7 +655,7 @@ func TestClient_ExecuteTransfer(t *testing.T) { } hash, err := c.ExecuteTransfer(context.Background(), common.Hash{}, argLists, batch.ID, 10) assert.Equal(t, "", hash) - assert.True(t, errors.Is(err, errQuorumNotReached)) + assert.True(t, errors.Is(err, clients.ErrQuorumNotReached)) assert.True(t, strings.Contains(err.Error(), "num signatures: 9, quorum: 10")) }) t.Run("not enough balance for fees", func(t *testing.T) { @@ -1055,7 +1055,7 @@ func TestClient_GetTransactionsStatuses(t *testing.T) { c, _ := NewEthereumClient(args) statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) assert.Nil(t, statuses) - assert.Equal(t, errStatusIsNotFinal, err) + assert.Equal(t, clients.ErrStatusIsNotFinal, err) }) t.Run("should work", func(t *testing.T) { t.Parallel() diff --git a/clients/ethereum/errors.go b/clients/ethereum/errors.go index 002d341f..bced280a 100644 --- a/clients/ethereum/errors.go +++ b/clients/ethereum/errors.go @@ -3,17 +3,10 @@ package ethereum import "errors" var ( - errQuorumNotReached = errors.New("quorum not reached") - errInsufficientErc20Balance = errors.New("insufficient ERC20 balance") - errInsufficientBalance = errors.New("insufficient balance") - errPublicKeyCast = errors.New("error casting public key to ECDSA") - errNilClientWrapper = errors.New("nil client wrapper") - errNilERC20ContractsHandler = errors.New("nil ERC20 contracts handler") - errNilBroadcaster = errors.New("nil broadcaster") - errNilSignaturesHolder = errors.New("nil signatures holder") - errNilGasHandler = errors.New("nil gas handler") - errInvalidGasLimit = errors.New("invalid gas limit") - errNilEthClient = errors.New("nil eth client") - errDepositsAndBatchDepositsCountDiffer = errors.New("deposits and batch.DepositsCount differs") - errStatusIsNotFinal = errors.New("status is not final") + errInsufficientErc20Balance = errors.New("insufficient ERC20 balance") + errInsufficientBalance = errors.New("insufficient balance") + errPublicKeyCast = errors.New("error casting public key to ECDSA") + errNilERC20ContractsHandler = errors.New("nil ERC20 contracts handler") + errNilGasHandler = errors.New("nil gas handler") + errNilEthClient = errors.New("nil eth client") ) diff --git a/clients/ethereum/wrappers/ethereumChainWrapper.go b/clients/ethereum/wrappers/ethereumChainWrapper.go index f5ca77e4..89648585 100644 --- a/clients/ethereum/wrappers/ethereumChainWrapper.go +++ b/clients/ethereum/wrappers/ethereumChainWrapper.go @@ -171,7 +171,7 @@ func (wrapper *ethereumChainWrapper) NativeTokens(ctx context.Context, token com return wrapper.safeContract.NativeTokens(&bind.CallOpts{Context: ctx}, token) } -// WhitelistedTokens returns true if the token is a native token +// WhitelistedTokens returns true if the token is a whitelisted func (wrapper *ethereumChainWrapper) WhitelistedTokens(ctx context.Context, token common.Address) (bool, error) { wrapper.AddIntMetric(core.MetricNumEthClientRequests, 1) return wrapper.safeContract.WhitelistedTokens(&bind.CallOpts{Context: ctx}, token) diff --git a/clients/sui/client.go b/clients/sui/client.go new file mode 100644 index 00000000..1a2fd040 --- /dev/null +++ b/clients/sui/client.go @@ -0,0 +1,433 @@ +package sui + +import ( + "context" + "crypto/ed25519" + "encoding/hex" + "fmt" + "github.com/block-vision/sui-go-sdk/common/keypair" + signer "github.com/block-vision/sui-go-sdk/signer" + "github.com/block-vision/sui-go-sdk/sui" + "github.com/ethereum/go-ethereum/common" + "github.com/multiversx/mx-bridge-eth-go/clients" + bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" + "github.com/multiversx/mx-bridge-eth-go/core/converters" + chainCore "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-core-go/core/check" + "golang.org/x/crypto/blake2b" + "math/big" + "sync" +) + +const ( + minQuorumValue = uint64(1) + minClientAvailabilityAllowDelta = 1 +) + +type ArgsSuiClient struct { + suiClient sui.ISuiAPI + Log chainCore.Logger + RelayerPrivateKey ed25519.PrivateKey + SafeContractAddress string + BridgeContractAddress string + TokensMapper TokensMapper + StatusHandler bridgeCore.StatusHandler + Broadcaster Broadcaster + SignatureHolder SignaturesHolder + + ClientAvailabilityAllowDelta uint64 +} + +type client struct { + *suiClientDataGetter + txHandler txHandler + tokensMapper TokensMapper + relayerPublicKey ed25519.PublicKey + relayerAddress string + safeContractAddress string + bridgeContractAddress string + log chainCore.Logger + addressConverter bridgeCore.AddressConverter + statusHandler bridgeCore.StatusHandler + broadcaster Broadcaster + signatureHolder SignaturesHolder + + lastCheckpoint uint64 + retriesAvailabilityCheck uint64 + clientAvailabilityAllowDelta uint64 + mut sync.RWMutex +} + +func NewSuiClient(args ArgsSuiClient) (*client, error) { + err := checkArgs(args) + if err != nil { + return nil, err + } + + relayerPubKey, relayerAddress := generateRelayerPubKeyAndAddress(args.RelayerPrivateKey) + signerRelayer := &signer.Signer{ + PriKey: args.RelayerPrivateKey, + PubKey: relayerPubKey, + Address: relayerAddress, + } + + argsSuiClientDataGetter := ArgsSuiClientDataGetter{ + BridgeContractAddress: args.BridgeContractAddress, + SafeContractAddress: args.SafeContractAddress, + RelayerAddress: signerRelayer, + Client: args.suiClient, + Log: args.Log, + } + getter, err := NewSuiClientDataGetter(argsSuiClientDataGetter) + if err != nil { + return nil, err + } + + addressConverter, err := converters.NewAddressConverter() + if err != nil { + return nil, clients.ErrNilAddressConverter + } + + c := &client{ + txHandler: &transactionHandler{ + client: args.suiClient, + relayerAddress: relayerAddress, + relayerPrivateKey: args.RelayerPrivateKey, + bridgeContractAddress: args.BridgeContractAddress, + }, + suiClientDataGetter: getter, + relayerPublicKey: relayerPubKey, + relayerAddress: relayerAddress, + bridgeContractAddress: args.BridgeContractAddress, + safeContractAddress: args.SafeContractAddress, + log: args.Log, + addressConverter: addressConverter, + broadcaster: args.Broadcaster, + tokensMapper: args.TokensMapper, + statusHandler: args.StatusHandler, + signatureHolder: args.SignatureHolder, + clientAvailabilityAllowDelta: args.ClientAvailabilityAllowDelta, + } + + c.log.Info("NewSuiClient") + + return c, err +} + +func checkArgs(args ArgsSuiClient) error { + if check.IfNil(args.suiClient) { + return errNilClient + } + if len(args.RelayerPrivateKey) == 0 { + return clients.ErrNilPrivateKey + } + if args.BridgeContractAddress == "" { + return fmt.Errorf("%w for the MultisigContractAddress argument", errNilBridgeContract) + } + if args.SafeContractAddress == "" { + return fmt.Errorf("%w for the SafeContractAddress argument", errNilSafeContract) + } + if check.IfNil(args.Log) { + return clients.ErrNilLogger + } + if check.IfNil(args.TokensMapper) { + return clients.ErrNilTokensMapper + } + if check.IfNil(args.Broadcaster) { + return clients.ErrNilBroadcaster + } + if check.IfNil(args.SignatureHolder) { + return clients.ErrNilSignaturesHolder + } + if check.IfNil(args.StatusHandler) { + return clients.ErrNilStatusHandler + } + if args.ClientAvailabilityAllowDelta < minClientAvailabilityAllowDelta { + return fmt.Errorf("%w for args.AllowedDelta, got: %d, minimum: %d", + clients.ErrInvalidValue, args.ClientAvailabilityAllowDelta, minClientAvailabilityAllowDelta) + } + + return nil +} + +func generateRelayerPubKeyAndAddress(priKey ed25519.PrivateKey) (ed25519.PublicKey, string) { + pubKey := priKey.Public().(ed25519.PublicKey) + + tmp := []byte{byte(keypair.Ed25519Flag)} + tmp = append(tmp, pubKey...) + addrBytes := blake2b.Sum256(tmp) + addr := "0x" + hex.EncodeToString(addrBytes[:])[:64] + + return pubKey, addr +} + +func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.TransferBatch, bool, error) { + c.log.Info("Getting batch", "nonce", nonce) + nonceAsBigInt := big.NewInt(0).SetUint64(nonce) + batch, isFinalBatch, err := c.clientWrapper.GetBatch(ctx, nonceAsBigInt) + if err != nil { + return nil, false, err + } + deposits, areFinalDeposits, err := c.clientWrapper.GetBatchDeposits(ctx, nonceAsBigInt) + if err != nil { + return nil, false, err + } + if int(batch.DepositsCount) != len(deposits) { + return nil, false, fmt.Errorf("%w, batch.DepositsCount: %d, fetched deposits len: %d", + clients.ErrDepositsAndBatchDepositsCountDiffer, batch.DepositsCount, len(deposits)) + } + + transferBatch := &bridgeCore.TransferBatch{ + ID: batch.Nonce.Uint64(), + BlockNumber: batch.BlockNumber, + Deposits: make([]*bridgeCore.DepositTransfer, 0, batch.DepositsCount), + } + cachedTokens := make(map[string][]byte) + for i := range deposits { + deposit := deposits[i] + toBytes := deposit.Recipient[:] + fromBytes := deposit.Depositor[:] + tokenId := deposit.TokenAddress + + depositTransfer := &bridgeCore.DepositTransfer{ + Nonce: deposit.Nonce.Uint64(), + ToBytes: toBytes, + DisplayableTo: c.addressConverter.ToBech32StringSilent(toBytes), + FromBytes: fromBytes, + DisplayableFrom: c.addressConverter.ToHexString(fromBytes), + SourceTokenBytes: []byte(tokenId), + DisplayableToken: tokenId, + Amount: big.NewInt(0).Set(deposit.Amount), + } + storedConvertedTokenBytes, exists := cachedTokens[depositTransfer.DisplayableToken] + if !exists { + depositTransfer.DestinationTokenBytes, err = c.tokensMapper.ConvertToken(ctx, depositTransfer.SourceTokenBytes) + if err != nil { + return nil, false, err + } + cachedTokens[depositTransfer.DisplayableToken] = depositTransfer.DestinationTokenBytes + } else { + depositTransfer.DestinationTokenBytes = storedConvertedTokenBytes + } + + transferBatch.Deposits = append(transferBatch.Deposits, depositTransfer) + } + + transferBatch.Statuses = make([]byte, len(transferBatch.Deposits)) + + return transferBatch, isFinalBatch && areFinalDeposits, nil +} + +// WasExecuted returns true if the MultiversX batch ID was executed +func (c *client) WasExecuted(ctx context.Context, mvxBatchID uint64) (bool, error) { + return c.clientWrapper.WasBatchExecuted(ctx, big.NewInt(0).SetUint64(mvxBatchID)) +} + +// BroadcastSignatureForMessageHash will send the signature for the provided message hash +func (c *client) BroadcastSignatureForMessageHash(msgHash []byte) { + signature, err := c.cryptoHandler.Sign(msgHash) + if err != nil { + c.log.Error("error generating signature", "msh hash", msgHash, "error", err) + return + } + + c.broadcaster.BroadcastSignature(signature, msgHash) +} + +// GenerateMessageHash will generate the message hash based on the provided batch +func (c *client) GenerateMessageHash(batch *batchProcessor.ArgListsBatch, batchId uint64) (common.Hash, error) { + panic("not implemented yet") +} + +// ExecuteTransfer will initiate and send the transaction from the transfer batch struct +func (c *client) ExecuteTransfer( + ctx context.Context, + msgHash []byte, + argLists *batchProcessor.ArgListsBatchSui, + batchId uint64, + quorum int, +) (string, error) { + if argLists == nil { + return "", clients.ErrNilBatch + } + + isPaused, err := c.clientWrapper.IsPaused(ctx) + if err != nil { + return "", fmt.Errorf("%w in client.ExecuteTransfer", err) + } + if isPaused { + return "", fmt.Errorf("%w in client.ExecuteTransfer", clients.ErrMultisigContractPaused) + } + + signatures := c.signatureHolder.Signatures(msgHash) + if len(signatures) < quorum { + return "", fmt.Errorf("%w num signatures: %d, quorum: %d", clients.ErrQuorumNotReached, len(signatures), quorum) + } + if len(signatures) > quorum { + c.log.Debug("reducing the size of the signatures set", + "quorum", quorum, "total signatures", len(signatures)) + signatures = signatures[:quorum] + } + + //minimumForFee := big.NewInt(int64(auth.GasLimit)) + //minimumForFee.Mul(minimumForFee, auth.GasPrice) + //err = c.checkRelayerFundsForFee(ctx, minimumForFee) + //if err != nil { + // return "", err + //} + + batchID := big.NewInt(0).SetUint64(batchId) + tx, err := c.clientWrapper.ExecuteTransfer(argLists.SuiTokens, argLists.Recipients, argLists.Amounts, argLists.Nonces, batchID, signatures) + if err != nil { + return "", err + } + + txHash := tx.Hash().String() + c.log.Info("Executed transfer transaction", "batchID", batchID, "hash", txHash) + + return txHash, err +} + +func (c *client) CheckClientAvailability(ctx context.Context) error { + c.mut.Lock() + defer c.mut.Unlock() + + currentCheckpoint, err := c.clientWrapper.GetLatestCheckpoint(ctx) + if err != nil { + c.setStatusForAvailabilityCheck(bridgeCore.Unavailable, err.Error(), currentCheckpoint) + + return err + } + + if currentCheckpoint != c.lastCheckpoint { + c.retriesAvailabilityCheck = 0 + c.lastCheckpoint = currentCheckpoint + } + + // if we reached this point we will need to increment the retries counter + defer c.incrementRetriesAvailabilityCheck() + + if c.retriesAvailabilityCheck > c.clientAvailabilityAllowDelta { + message := fmt.Sprintf("block %d fetched for %d times in a row", currentCheckpoint, c.retriesAvailabilityCheck) + c.setStatusForAvailabilityCheck(bridgeCore.Unavailable, message, currentCheckpoint) + + return nil + } + + c.setStatusForAvailabilityCheck(bridgeCore.Available, "", currentCheckpoint) + + return nil +} + +func (c *client) setStatusForAvailabilityCheck(status bridgeCore.ClientStatus, message string, nonce uint64) { + c.clientWrapper.SetStringMetric(bridgeCore.MetricMultiversXClientStatus, status.String()) + c.clientWrapper.SetStringMetric(bridgeCore.MetricLastMultiversXClientError, message) + c.clientWrapper.SetIntMetric(bridgeCore.MetricLastBlockNonce, int(nonce)) +} + +func (c *client) incrementRetriesAvailabilityCheck() { + c.retriesAvailabilityCheck++ +} + +// CheckRequiredBalance will check if the safe has enough balance for the transfer +func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, value *big.Int) error { + isMintBurn, err := c.MintBurnTokens(ctx, coinType) + if err != nil { + return err + } + + if isMintBurn { + return nil + } + existingBalance, err := c.clientWrapper.GetBalance(ctx, c.safeContractId, coinType) + if err != nil { + return fmt.Errorf("%w for owner %s for coin %s", err, c.safeContractId, coinType) + } + + totalExistingBalanceStr := existingBalance.TotalBalance + totalExistingBalance := new(big.Int) + _, ok := totalExistingBalance.SetString(totalExistingBalanceStr, 10) + if !ok { + return fmt.Errorf("invalid balance string: %s", totalExistingBalanceStr) + } + if value.Cmp(totalExistingBalance) > 0 { + return fmt.Errorf("%w, existing: %s, required: %s for coin %s and owner %s", + errInsufficientCoinBalance, totalExistingBalanceStr, value.String(), coinType, c.safeContractId) + } + + c.log.Debug("checked coin balance", + "Coin type", coinType, + "owner address", c.safeContractId, + "existing balance", totalExistingBalanceStr, + "needed", value.String()) + + return nil +} + +// TotalBalances returns the total balance of the given token +func (c *client) TotalBalances(ctx context.Context, token string) (*big.Int, error) { + return c.clientWrapper.TotalBalances(ctx, token) +} + +// MintBalances returns the mint balance of the given token +func (c *client) MintBalances(ctx context.Context, token string) (*big.Int, error) { + return c.clientWrapper.MintBalances(ctx, token) +} + +// BurnBalances returns the burn balance of the given token +func (c *client) BurnBalances(ctx context.Context, token string) (*big.Int, error) { + return c.clientWrapper.BurnBalances(ctx, token) +} + +// MintBurnTokens returns true if the token is mintBurn token +func (c *client) MintBurnTokens(ctx context.Context, token string) (bool, error) { + return c.clientWrapper.MintBurnTokens(ctx, token) +} + +// NativeTokens returns true if the token is native +func (c *client) NativeTokens(ctx context.Context, token string) (bool, error) { + return c.clientWrapper.NativeTokens(ctx, token) +} + +// WhitelistedTokens returns true if the token is whitelisted +func (c *client) WhitelistedTokens(ctx context.Context, token string) (bool, error) { + return c.clientWrapper.WhitelistedTokens(ctx, token) +} + +// GetTransactionsStatuses will return the transactions statuses from the batch +func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]byte, error) { + buff, isFinal, err := c.clientWrapper.GetStatusesAfterExecution(ctx, big.NewInt(0).SetUint64(batchId)) + if err != nil { + return nil, err + } + if !isFinal { + return nil, clients.ErrStatusIsNotFinal + } + + return buff, nil +} + +// GetQuorumSize returns the size of the quorum +func (c *client) GetQuorumSize(ctx context.Context) (*big.Int, error) { + return c.clientWrapper.Quorum(ctx) +} + +// IsQuorumReached returns true if the number of signatures is at least the size of quorum +func (c *client) IsQuorumReached(ctx context.Context, msg []byte) (bool, error) { + signatures := c.signatureHolder.Signatures(msg) + quorum, err := c.clientWrapper.Quorum(ctx) + if err != nil { + return false, fmt.Errorf("%w in IsQuorumReached, Quorum call", err) + } + if quorum.Uint64() < minQuorumValue { + return false, fmt.Errorf("%w in IsQuorumReached, minQuorum %d, got: %s", clients.ErrInvalidValue, minQuorumValue, quorum.String()) + } + + return len(signatures) >= int(quorum.Int64()), nil +} + +// IsInterfaceNil returns true if there is no value under the interface +func (c *client) IsInterfaceNil() bool { + return c == nil +} diff --git a/clients/sui/dtos/contractStructs.go b/clients/sui/dtos/contractStructs.go new file mode 100644 index 00000000..1229c4ad --- /dev/null +++ b/clients/sui/dtos/contractStructs.go @@ -0,0 +1,19 @@ +package dtos + +import "math/big" + +type Batch struct { + Nonce uint64 + BlockNumber uint64 + LastUpdatedBlockNumber uint64 + DepositsCount uint16 +} + +type Deposit struct { + Nonce *big.Int + TokenAddress string + Amount *big.Int + Depositor [32]byte + Recipient [32]byte + Status uint8 +} diff --git a/clients/sui/dtos/sharedOwner.go b/clients/sui/dtos/sharedOwner.go new file mode 100644 index 00000000..ddceb16c --- /dev/null +++ b/clients/sui/dtos/sharedOwner.go @@ -0,0 +1,7 @@ +package dtos + +type SharedOwner struct { + Shared struct { + InitialSharedVersion uint64 `json:"initial_shared_version"` + } `json:"Shared"` +} diff --git a/clients/sui/dtos/transactionBlockResponseResult.go b/clients/sui/dtos/transactionBlockResponseResult.go new file mode 100644 index 00000000..d8150eaf --- /dev/null +++ b/clients/sui/dtos/transactionBlockResponseResult.go @@ -0,0 +1,12 @@ +package dtos + +// InspectResult represents the result of inspecting a transaction block +type InspectResult struct { + ReturnValues [][]interface{} `json:"returnValues"` +} + +// ReturnValue represents a single return value with its type +type ReturnValue struct { + Value interface{} + Type string +} diff --git a/clients/sui/errors.go b/clients/sui/errors.go new file mode 100644 index 00000000..30e000bf --- /dev/null +++ b/clients/sui/errors.go @@ -0,0 +1,17 @@ +package sui + +import "errors" + +var ( + errInsufficientCoinBalance = errors.New("insufficient coin balance") + errEmptyAddress = errors.New("empty address") + errNilRelayerSigner = errors.New("nil relayer signer") + errNilClient = errors.New("nil client") + errGetCoinObjectsFailed = errors.New("get coin objects failed") + errNoGasObjects = errors.New("no gas objects found") + errInvalidCoinType = errors.New("invalid coin type format") + + errNilBlockchainClient = errors.New("nil blockchain client") + errNilSafeContract = errors.New("nil safe contract") + errNilBridgeContract = errors.New("nil bridge contract") +) diff --git a/clients/sui/interface.go b/clients/sui/interface.go new file mode 100644 index 00000000..bf292329 --- /dev/null +++ b/clients/sui/interface.go @@ -0,0 +1,47 @@ +package sui + +import ( + "context" + "github.com/block-vision/sui-go-sdk/models" +) + +// SuiClient defines the operations for a component that can interact with the Sui blockchain +type SuiClient interface { + SuiGetLatestCheckpointSequenceNumber(ctx context.Context) (uint64, error) + SuiGetChainIdentifier(ctx context.Context) (string, error) + SuiXGetBalance(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) + MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) + SignAndExecuteTransactionBlock(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) + SuiDryRunTransactionBlock(ctx context.Context, req models.SuiDryRunTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) + SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) + IsInterfaceNil() bool +} + +// TokensMapper can convert a token bytes from one chain to another +type TokensMapper interface { + ConvertToken(ctx context.Context, sourceBytes []byte) ([]byte, error) + IsInterfaceNil() bool +} + +// SignaturesHolder defines the operations for a component that can hold and manage signatures +type SignaturesHolder interface { + Signatures(messageHash []byte) [][]byte + ClearStoredSignatures() + IsInterfaceNil() bool +} + +// Broadcaster defines the operations for a component used for communication with other peers +type Broadcaster interface { + BroadcastSignature(signature []byte, messageHash []byte) + IsInterfaceNil() bool +} + +// CryptoHandler defines the operations for a component that expose some crypto primitives +type CryptoHandler interface { + Sign(msg []byte) ([]byte, error) + IsInterfaceNil() bool +} + +type txHandler interface { + SendTransaction(ctx context.Context, moveCallRequest models.MoveCallRequest, gasLimit uint64) (string, error) +} diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go new file mode 100644 index 00000000..ad165a13 --- /dev/null +++ b/clients/sui/suiClientDataGetter.go @@ -0,0 +1,780 @@ +package sui + +import ( + "context" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/mystenbcs" + "github.com/block-vision/sui-go-sdk/signer" + "github.com/block-vision/sui-go-sdk/sui" + "github.com/block-vision/sui-go-sdk/transaction" + "github.com/multiversx/mx-bridge-eth-go/clients" + "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" + chainCore "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-core-go/core/check" +) + +const suiCoinType = "0x2::sui::SUI" + +// ArgsSuiClientDataGetter is the arguments DTO used in the NewSuiClientDataGetter constructor +type ArgsSuiClientDataGetter struct { + SafeContractAddress string + BridgeContractAddress string + Relayer *signer.Signer + Client sui.ISuiAPI + Log chainCore.Logger +} + +type suiClientDataGetter struct { + safeContractAddress string + safeObjectIdBytes models.SuiAddressBytes + + bridgeContractAddress string + bridgeObjectIdBytes models.SuiAddressBytes + relayer *signer.Signer + client *sui.Client + initialSharedVersionForObject sync.Map // to cache the initial shared version for objects + log chainCore.Logger + mtx sync.Mutex +} + +// NewSuiClientDataGetter creates a new instance of type suiClientDataGetter +func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, error) { + if check.IfNil(args.Log) { + return nil, clients.ErrNilLogger + } + if args.Client == nil { + return nil, errNilClient + } + if args.Relayer == nil { + return nil, errNilRelayerSigner + } + if args.BridgeContractAddress == "" { + return nil, fmt.Errorf("%w for the BridgeContractAddress argument", errEmptyAddress) + } + if args.SafeContractAddress == "" { + return nil, fmt.Errorf("%w for the SafeContractAddress argument", errEmptyAddress) + } + + var suiClient, ok = args.Client.(*sui.Client) + if !ok { + panic("not sui client") + } + + safeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.SafeContractAddress)) + if err != nil { + return nil, fmt.Errorf("failed to convert address: %w", err) + } + + bridgeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.BridgeContractAddress)) + if err != nil { + return nil, fmt.Errorf("failed to convert address: %w", err) + } + + return &suiClientDataGetter{ + safeContractAddress: args.SafeContractAddress, + safeObjectIdBytes: *safeObjectIdBytes, + bridgeContractAddress: args.BridgeContractAddress, + bridgeObjectIdBytes: *bridgeObjectIdBytes, + relayer: args.Relayer, + client: suiClient, + log: args.Log, + }, nil + +} + +func (getter *suiClientDataGetter) getGasObject(ctx context.Context) (*transaction.SuiObjectRef, error) { + gasObjects, err := getter.client.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ + Owner: getter.relayer.Address, + CoinType: suiCoinType, + Limit: 1, + }) + if err != nil { + return nil, errGetCoinObjectsFailed + } + + if len(gasObjects.Data) == 0 { + fmt.Println("NU ai obiecte de gas!") + return nil, fmt.Errorf("%w for address %s", errNoGasObjects, getter.relayer.Address) + } + + gasCoin, err := transaction.NewSuiObjectRef( + models.SuiAddress(gasObjects.Data[0].CoinObjectId), + gasObjects.Data[0].Version, + models.ObjectDigest(gasObjects.Data[0].Digest), + ) + if err != nil { + return nil, fmt.Errorf("failed to create gas object ref: %w", err) + } + + return gasCoin, nil +} + +func (getter *suiClientDataGetter) getNewTransaction(ctx context.Context) (*transaction.Transaction, error) { + gasCoin, err := getter.getGasObject(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get gas object: %w", err) + } + + tx := transaction.NewTransaction() + tx.SetSuiClient(getter.client). + SetSigner(getter.relayer). + SetSender(models.SuiAddress(getter.relayer.Address)). + SetGasPrice(1000). + SetGasBudget(50000000). + SetGasPayment([]transaction.SuiObjectRef{*gasCoin}). + SetGasOwner(models.SuiAddress(getter.relayer.Address)) + + return tx, nil +} + +func (getter *suiClientDataGetter) getTxBytes(tx *transaction.Transaction) (string, error) { + bcsEncodedMsg, err := tx.Data.V1.Kind.Marshal() + if err != nil { + return "", fmt.Errorf("failed to marshal transaction data: %w", err) + } + txBytes := mystenbcs.ToBase64(bcsEncodedMsg) + + return txBytes, nil +} + +func (getter *suiClientDataGetter) sendTxGetBlockResponse(ctx context.Context, tx *transaction.Transaction) (models.SuiTransactionBlockResponse, error) { + txBytes, err := getter.getTxBytes(tx) + if err != nil { + return models.SuiTransactionBlockResponse{}, err + } + + return getter.client.SuiDevInspectTransactionBlock(ctx, models.SuiDevInspectTransactionBlockRequest{ + Sender: getter.relayer.Address, + TxBytes: txBytes, + }) +} + +func (getter *suiClientDataGetter) getInitialSharedVersionForObject(ctx context.Context, objectId string) (uint64, error) { + if value, exists := getter.initialSharedVersionForObject.Load(objectId); exists { + return value.(uint64), nil + } + + // If not found, make the network call + rsp, err := getter.client.SuiGetObject(ctx, models.SuiGetObjectRequest{ + ObjectId: objectId, + Options: models.SuiObjectDataOptions{ + ShowOwner: true, + }, + }) + + if err != nil { + return 0, fmt.Errorf("failed to get object %s: %w", objectId, err) + } + + data := rsp.Data + ownerBytes, err := json.Marshal(data.Owner) + if err != nil { + return 0, fmt.Errorf("failed to marshal owner: %w", err) + } + + var sharedOwner dtos.SharedOwner + if err = json.Unmarshal(ownerBytes, &sharedOwner); err != nil { + return 0, fmt.Errorf("failed to unmarshal to SharedOwner: %w", err) + } + + initialSharedVersion := sharedOwner.Shared.InitialSharedVersion + + if actualValue, loaded := getter.initialSharedVersionForObject.LoadOrStore(objectId, initialSharedVersion); loaded { + // if another goroutine stored it first, return that value + return actualValue.(uint64), nil + } + + return initialSharedVersion, nil +} + +// GetBatch returns the batch of transactions by providing the batch nonce +func (getter *suiClientDataGetter) GetBatch(ctx context.Context, batchNonce uint64) (dtos.Batch, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return dtos.Batch{}, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return dtos.Batch{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_batch", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + tx.Pure(batchNonce), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return dtos.Batch{}, fmt.Errorf("failed to get batch: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return dtos.Batch{}, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + } + + var batch *dtos.Batch + result := txBlockResp.Results + returnValues, err := ExtractReturnValues(result) + if err != nil { + return dtos.Batch{}, fmt.Errorf("failed to extract return values: %w", err) + } + if len(returnValues) > 0 && strings.HasSuffix(returnValues[0].Type, "::Batch") { + batch, err = DecodeStructFromExtracted(returnValues[0], BatchDecoder) + if err != nil { + return dtos.Batch{}, fmt.Errorf("failed to decode batch: %w", err) + } + } else { + return dtos.Batch{}, fmt.Errorf("wrong expected return value, got %s", returnValues[0].Type) + } + + return *batch, nil +} + +// GetBatchDeposits returns the transactions of a batch by providing the batch nonce +func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNonce uint64) ([]dtos.Deposit, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return []dtos.Deposit{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_batch_deposits", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + tx.Pure(batchNonce), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return nil, fmt.Errorf("failed to get batch deposits: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return nil, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return []dtos.Deposit{}, nil +} + +// GetRelayers returns all whitelisted sui addresses +func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.SuiAddress, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return []models.SuiAddress{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_relayers", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return nil, fmt.Errorf("failed to get batch deposits: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return nil, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return []models.SuiAddress{}, nil +} + +// WasBatchExecuted returns true if the batch was executed +func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNonce uint64) (bool, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return false, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_relayers", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + tx.Pure(batchNonce), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return false, fmt.Errorf("failed to get batch deposits: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return false, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return false, nil +} + +// IsPaused returns true if the bridge contract is paused +func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return false, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.safeContractAddress), + "bridge", + "get_pause", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return false, fmt.Errorf("failed to get get_pause: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return false, fmt.Errorf("get_pause transaction failed: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return false, nil +} + +// Quorum returns the current set quorum value +func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return 0, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_quorum", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return 0, fmt.Errorf("failed to get quorum: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return 0, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return 0, nil +} + +// GetStatusesAfterExecution returns the statuses of the last executed transfer +func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) (uint64, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return 0, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) + if err != nil { + return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + } + + tx.MoveCall( + models.SuiAddress(getter.bridgeContractAddress), + "bridge", + "get_statuses_after_execution", + nil, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.bridgeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + tx.Pure(batchNonce), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return 0, fmt.Errorf("failed to get quorum: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return 0, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return 0, nil +} + +// TotalBalances returns the total balance of the given token +func (getter *suiClientDataGetter) TotalBalances(ctx context.Context, coinType string) (uint64, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return 0, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) + if err != nil { + return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) + } + + coinParts, err := ParseCoinType(coinType) + if err != nil { + return 0, fmt.Errorf("failed to parse coin type %s: %w", coinType, err) + } + + coinIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(coinParts[0])) + if err != nil { + return 0, fmt.Errorf("failed to convert coin type %s: %w", coinType, err) + } + + tx.MoveCall( + models.SuiAddress(getter.safeContractAddress), + "safe", + "get_stored_coin_balance", + []transaction.TypeTag{ + { + Struct: &transaction.StructTag{ + Address: *coinIdBytes, + Module: coinParts[1], + Name: coinParts[2], + }, + }, + }, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.safeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return 0, fmt.Errorf("failed to get total balances: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return 0, fmt.Errorf("get total balances transaction failed: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return 0, nil +} + +// WhitelistedTokens returns true if the token is whitelisted +func (getter *suiClientDataGetter) WhitelistedTokens(ctx context.Context, coinType string) (bool, error) { + tx, err := getter.getNewTransaction(ctx) + if err != nil { + return false, fmt.Errorf("failed to create new transaction: %w", err) + } + + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) + if err != nil { + return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) + } + + coinParts, err := ParseCoinType(coinType) + if err != nil { + return false, fmt.Errorf("failed to parse coin type %s: %w", coinType, err) + } + + coinIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(coinParts[0])) + if err != nil { + return false, fmt.Errorf("failed to convert coin type %s: %w", coinType, err) + } + + tx.MoveCall( + models.SuiAddress(getter.safeContractAddress), + "safe", + "is_token_whitelisted", + []transaction.TypeTag{ + { + Struct: &transaction.StructTag{ + Address: *coinIdBytes, + Module: coinParts[1], + Name: coinParts[2], + }, + }, + }, + []transaction.Argument{ + tx.Object( + transaction.CallArg{ + Object: &transaction.ObjectArg{ + SharedObject: &transaction.SharedObjectRef{ + ObjectId: getter.safeObjectIdBytes, + InitialSharedVersion: initialSharedVersion, + Mutable: true, + }, + }, + }, + ), + }, + ) + + txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) + if err != nil { + return false, fmt.Errorf("failed to get total balances: %w", err) + } + + if txBlockResp.Effects.Status.Status != "success" { + return false, fmt.Errorf("get total balances transaction failed: %s", txBlockResp.Effects.Status.Error) + } + + // TODO: decode result + return false, nil +} + +// GetLatestCheckpoint returns the latest checkpoint sequence number +func (getter *suiClientDataGetter) GetLatestCheckpoint(ctx context.Context) (uint64, error) { + return getter.client.SuiGetLatestCheckpointSequenceNumber(ctx) +} + +// GetBalance returns the sui balance of the given account +func (getter *suiClientDataGetter) GetBalance(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { + return getter.client.SuiXGetBalance(ctx, models.SuiXGetBalanceRequest{ + Owner: account, + CoinType: coinType, + }) +} + +// ParseCoinType parses a coin type string in the format "package::module::struct" and returns its components. +func ParseCoinType(coinType string) ([3]string, error) { + parts := strings.Split(coinType, "::") + + if len(parts) != 3 { + return [3]string{}, errInvalidCoinType + } + + packageAddr := strings.TrimSpace(parts[0]) + module := strings.TrimSpace(parts[1]) + structName := strings.TrimSpace(parts[2]) + + // Basic validation + if packageAddr == "" || module == "" || structName == "" { + return [3]string{}, errors.New("coin type parts cannot be empty") + } + + return [3]string{packageAddr, module, structName}, nil +} + +// ExtractReturnValues extracts and parses return values from json.RawMessage +func ExtractReturnValues(rawResults json.RawMessage) ([]dtos.ReturnValue, error) { + var returnValues []dtos.ReturnValue + + if len(rawResults) == 0 { + return returnValues, nil + } + + var results []dtos.InspectResult + if err := json.Unmarshal(rawResults, &results); err != nil { + return nil, fmt.Errorf("failed to unmarshal results: %w", err) + } + + for _, result := range results { + for _, rv := range result.ReturnValues { + if len(rv) >= 2 { + returnValue := dtos.ReturnValue{ + Value: rv[0], + Type: fmt.Sprintf("%v", rv[1]), + } + returnValues = append(returnValues, returnValue) + } + } + } + + return returnValues, nil +} + +// DecodeStructFromExtracted decodes a struct from the extracted return value. +func DecodeStructFromExtracted[T any](rv dtos.ReturnValue, decoder func([]interface{}) (*T, error)) (*T, error) { + fields, ok := rv.Value.([]interface{}) + if !ok { + return nil, fmt.Errorf("expected struct fields as []interface{}, got %T", rv.Value) + } + return decoder(fields) +} + +func BatchDecoder(fields []interface{}) (*dtos.Batch, error) { + if len(fields) != 26 { + return nil, fmt.Errorf("expected 26 bytes for serialized Sword, got %d", len(fields)) + } + + bytes := make([]byte, 26) + for i, v := range fields { + f, ok := v.(float64) + if !ok { + return nil, fmt.Errorf("expected float64 byte at index %d, got %T", i, v) + } + bytes[i] = byte(f) + } + + nonce, err := parseU64(bytes[0:8]) + if err != nil { + return nil, fmt.Errorf("failed to decode nonce: %v", err) + } + + blockNumber, err := parseU64(bytes[8:16]) + if err != nil { + return nil, fmt.Errorf("failed to decode block_number: %v", err) + } + + lastUpdatedBlock, err := parseU64(bytes[16:24]) + if err != nil { + return nil, fmt.Errorf("failed to decode last_updated_block: %v", err) + } + + depositsCount, err := parseU16(bytes[24:26]) + if err != nil { + return nil, fmt.Errorf("failed to decode deposits_count: %v", err) + } + + return &dtos.Batch{ + Nonce: nonce, + BlockNumber: blockNumber, + LastUpdatedBlockNumber: lastUpdatedBlock, + DepositsCount: depositsCount, + }, nil +} + +func parseU64(bytes []byte) (uint64, error) { + if len(bytes) != 8 { + return 0, fmt.Errorf("expected 8 bytes for u64, got %d", len(bytes)) + } + return binary.LittleEndian.Uint64(bytes), nil +} + +func parseU16(bytes []byte) (uint16, error) { + if len(bytes) != 2 { + return 0, fmt.Errorf("expected 2 bytes for u16, got %d", len(bytes)) + } + return binary.LittleEndian.Uint16(bytes), nil +} diff --git a/clients/sui/testdata/nok-sui-seed b/clients/sui/testdata/nok-sui-seed new file mode 100644 index 00000000..3a6260b0 --- /dev/null +++ b/clients/sui/testdata/nok-sui-seed @@ -0,0 +1 @@ +vrsݩ`^vn \ No newline at end of file diff --git a/clients/sui/testdata/ok-sui-seed b/clients/sui/testdata/ok-sui-seed new file mode 100644 index 00000000..26289944 --- /dev/null +++ b/clients/sui/testdata/ok-sui-seed @@ -0,0 +1,2 @@ +qyk +C}g 6r;_/0IS \ No newline at end of file diff --git a/clients/sui/transactionHandler.go b/clients/sui/transactionHandler.go new file mode 100644 index 00000000..71d30d03 --- /dev/null +++ b/clients/sui/transactionHandler.go @@ -0,0 +1,39 @@ +package sui + +import ( + "context" + "crypto/ed25519" + "github.com/block-vision/sui-go-sdk/models" +) + +type transactionHandler struct { + client SuiClient + relayerAddress string + relayerPrivateKey ed25519.PrivateKey + bridgeContractAddress string +} + +func (txHandler *transactionHandler) SendTransaction(ctx context.Context, moveCallRequest models.MoveCallRequest, gasLimit uint64) (string, error) { + moveCallRequest.Signer = txHandler.relayerAddress + moveCallRequest.PackageObjectId = txHandler.bridgeContractAddress + + txnMetaData, err := txHandler.client.MoveCall(ctx, moveCallRequest) + if err != nil { + return "", err + } + + txBlockResponse, err := txHandler.client.SignAndExecuteTransactionBlock(ctx, models.SignAndExecuteTransactionBlockRequest{ + TxnMetaData: txnMetaData, + PriKey: txHandler.relayerPrivateKey, + Options: models.SuiTransactionBlockOptions{ + ShowInput: true, + ShowRawInput: true, + }, + RequestType: "WaitForLocalExecution", + }) + if err != nil { + return "", err + } + + return txBlockResponse.Digest, nil +} diff --git a/config/config.go b/config/config.go index cce4ce7d..34ad4a03 100644 --- a/config/config.go +++ b/config/config.go @@ -17,6 +17,7 @@ type Configs struct { type Config struct { Eth EthereumConfig MultiversX MultiversXConfig + Sui SuiConfig P2P ConfigP2P StateMachine map[string]ConfigStateMachine Relayer ConfigRelayer @@ -43,6 +44,12 @@ type EthereumConfig struct { EventsBlockRangeTo int64 } +// TODO: complete this +type SuiConfig struct { + Chain chain.Chain + NetworkAddress string +} + // GasStationConfig represents the configuration for the gas station handler type GasStationConfig struct { Enabled bool diff --git a/core/batchProcessor/batchProcessor.go b/core/batchProcessor/batchProcessor.go index a8ec2bcd..d0621ddb 100644 --- a/core/batchProcessor/batchProcessor.go +++ b/core/batchProcessor/batchProcessor.go @@ -1,6 +1,7 @@ package batchProcessor import ( + "github.com/block-vision/sui-go-sdk/models" "math/big" "github.com/ethereum/go-ethereum/common" @@ -17,7 +18,7 @@ const ( ToMultiversX Direction = "ToMultiversX" ) -// ArgListsBatch is a struct that contains the batch data in a format that is easy to use +// ArgListsBatch is a struct that contains the batch data in a format that is easy to use for Eth type ArgListsBatch struct { EthTokens []common.Address Recipients []common.Address @@ -27,6 +28,16 @@ type ArgListsBatch struct { Direction Direction } +// ArgListsBatchSui is a struct that contains the batch data in a format that is easy to use for Sui +type ArgListsBatchSui struct { + SuiTokens []string + Recipients []models.SuiAddress + MvxTokenBytes [][]byte + Amounts []*big.Int + Nonces []*big.Int + Direction Direction +} + // ExtractListMvxToEth will extract the batch data into a format that is easy to use // The transfer is from MultiversX to Ethereum func ExtractListMvxToEth(batch *bridgeCore.TransferBatch) *ArgListsBatch { diff --git a/go.mod b/go.mod index 34960fd0..f364203f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ module github.com/multiversx/mx-bridge-eth-go -go 1.20 +go 1.21 + +toolchain go1.22.12 require ( + github.com/block-vision/sui-go-sdk v1.0.8 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 github.com/ethereum/go-ethereum v1.13.15 github.com/gin-contrib/cors v1.4.0 @@ -15,8 +18,9 @@ require ( github.com/multiversx/mx-chain-logger-go v1.0.14 github.com/multiversx/mx-sdk-go v1.4.1 github.com/pelletier/go-toml v1.9.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.10 + golang.org/x/crypto v0.23.0 ) require ( @@ -190,7 +194,7 @@ require ( github.com/status-im/keycard-go v0.2.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect - github.com/tidwall/gjson v1.14.0 // indirect + github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -212,15 +216,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.15.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect diff --git a/go.sum b/go.sum index 2baca09c..5d63c728 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= @@ -42,6 +43,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/block-vision/sui-go-sdk v1.0.8 h1:2EJ4TuBSg77NnZXf7JAdWdXf3mA1Yiz+BAccBRZub2U= +github.com/block-vision/sui-go-sdk v1.0.8/go.mod h1:tf1o9oSxBa8h+CaUyPDW8LbqfW4YPjd4fL22yO4QXMo= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= @@ -73,6 +76,7 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -86,6 +90,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= @@ -134,6 +139,7 @@ github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6 github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= @@ -173,6 +179,7 @@ github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwU github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -213,6 +220,7 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -250,6 +258,7 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -284,11 +293,13 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gops v0.3.18 h1:my259V+172PVFmduS2RAsq4FKH+HjKqdh7pLr17Ot8c= @@ -308,6 +319,7 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -352,6 +364,7 @@ github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= @@ -414,9 +427,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -439,6 +454,7 @@ github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhr github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= @@ -691,6 +707,7 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU= +github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= @@ -721,16 +738,17 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= -github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -803,6 +821,7 @@ go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -832,8 +851,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= @@ -850,8 +869,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -883,8 +902,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -900,8 +919,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -948,8 +967,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -964,12 +983,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -993,8 +1012,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/testsCommon/addressGenerators.go b/testsCommon/addressGenerators.go index e0d8c55d..5f500a95 100644 --- a/testsCommon/addressGenerators.go +++ b/testsCommon/addressGenerators.go @@ -2,6 +2,8 @@ package testsCommon import ( "crypto/rand" + "encoding/hex" + "fmt" "github.com/ethereum/go-ethereum/common" sdkCore "github.com/multiversx/mx-sdk-go/core" @@ -33,3 +35,25 @@ func CreateRandomMultiversXSCAddress() sdkCore.AddressHandler { return data.NewAddressFromBytes(append(firstPart, buff...)) } + +// CreateRandomSuiAddressBytes will create a random Sui smart contract (object) or user address bytes +func CreateRandomSuiAddressBytes() [32]byte { + buff := make([]byte, 32) + _, _ = rand.Read(buff) + + return [32]byte(buff) +} + +// CreateRandomSuiAddress will create a random Sui smart contract (object) or user address +func CreateRandomSuiAddress() string { + buff := make([]byte, 32) + _, _ = rand.Read(buff) + + return fmt.Sprintf("0x%s", hex.EncodeToString(buff)) +} + +// CreateRandomCoinId will create a random Sui coin ID +func CreateRandomCoinId() string { + addrBytes := CreateRandomSuiAddressBytes() + return fmt.Sprintf("0x%s::coin::COIN", hex.EncodeToString(addrBytes[:])) +} From 6e1723f5e612f8b7775ae3bd200c90a4f96fef86 Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Thu, 3 Jul 2025 11:43:19 +0300 Subject: [PATCH 2/9] add sui client functionalities --- clients/sui/client.go | 223 ++++++++------- clients/sui/errors.go | 8 +- clients/sui/interface.go | 9 +- clients/sui/suiClientDataGetter.go | 422 +++++++++++------------------ clients/sui/transactionHandler.go | 35 ++- 5 files changed, 301 insertions(+), 396 deletions(-) diff --git a/clients/sui/client.go b/clients/sui/client.go index 1a2fd040..f7b436db 100644 --- a/clients/sui/client.go +++ b/clients/sui/client.go @@ -6,10 +6,11 @@ import ( "encoding/hex" "fmt" "github.com/block-vision/sui-go-sdk/common/keypair" + "github.com/block-vision/sui-go-sdk/models" signer "github.com/block-vision/sui-go-sdk/signer" "github.com/block-vision/sui-go-sdk/sui" - "github.com/ethereum/go-ethereum/common" "github.com/multiversx/mx-bridge-eth-go/clients" + "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" "github.com/multiversx/mx-bridge-eth-go/core/converters" @@ -26,32 +27,37 @@ const ( ) type ArgsSuiClient struct { - suiClient sui.ISuiAPI - Log chainCore.Logger - RelayerPrivateKey ed25519.PrivateKey - SafeContractAddress string - BridgeContractAddress string - TokensMapper TokensMapper - StatusHandler bridgeCore.StatusHandler - Broadcaster Broadcaster - SignatureHolder SignaturesHolder + SuiClient sui.ISuiAPI + Log chainCore.Logger + RelayerPrivateKey ed25519.PrivateKey + SafePackageId string + SafeObjectId string + BridgePackageId string + BridgeObjectId string + RelayerCapacityId string + TokensMapper TokensMapper + StatusHandler bridgeCore.StatusHandler + Broadcaster Broadcaster + SignatureHolder SignaturesHolder ClientAvailabilityAllowDelta uint64 } type client struct { *suiClientDataGetter - txHandler txHandler - tokensMapper TokensMapper - relayerPublicKey ed25519.PublicKey - relayerAddress string - safeContractAddress string - bridgeContractAddress string - log chainCore.Logger - addressConverter bridgeCore.AddressConverter - statusHandler bridgeCore.StatusHandler - broadcaster Broadcaster - signatureHolder SignaturesHolder + txHandler txHandler + tokensMapper TokensMapper + relayerPublicKey ed25519.PublicKey + relayerAddress string + safePackageId string + safeObjectId string + bridgePackageId string + bridgeObjectId string + log chainCore.Logger + addressConverter bridgeCore.AddressConverter + statusHandler bridgeCore.StatusHandler + broadcaster Broadcaster + signatureHolder SignaturesHolder lastCheckpoint uint64 retriesAvailabilityCheck uint64 @@ -65,18 +71,18 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { return nil, err } - relayerPubKey, relayerAddress := generateRelayerPubKeyAndAddress(args.RelayerPrivateKey) - signerRelayer := &signer.Signer{ + relayerPubKey, relayerAddress := generatePubKeyAndAddressFromPriKey(args.RelayerPrivateKey) + relayerSigner := &signer.Signer{ PriKey: args.RelayerPrivateKey, PubKey: relayerPubKey, Address: relayerAddress, } argsSuiClientDataGetter := ArgsSuiClientDataGetter{ - BridgeContractAddress: args.BridgeContractAddress, - SafeContractAddress: args.SafeContractAddress, - RelayerAddress: signerRelayer, - Client: args.suiClient, + SafeContractAddress: args.SafePackageId, + BridgeContractAddress: args.BridgePackageId, + RelayerAddress: relayerAddress, + Client: args.SuiClient, Log: args.Log, } getter, err := NewSuiClientDataGetter(argsSuiClientDataGetter) @@ -91,16 +97,16 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { c := &client{ txHandler: &transactionHandler{ - client: args.suiClient, - relayerAddress: relayerAddress, - relayerPrivateKey: args.RelayerPrivateKey, - bridgeContractAddress: args.BridgeContractAddress, + client: args.SuiClient, + relayerSigner: relayerSigner, }, suiClientDataGetter: getter, relayerPublicKey: relayerPubKey, relayerAddress: relayerAddress, - bridgeContractAddress: args.BridgeContractAddress, - safeContractAddress: args.SafeContractAddress, + safePackageId: args.SafePackageId, + safeObjectId: args.SafeObjectId, + bridgePackageId: args.BridgePackageId, + bridgeObjectId: args.BridgeObjectId, log: args.Log, addressConverter: addressConverter, broadcaster: args.Broadcaster, @@ -111,22 +117,32 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { } c.log.Info("NewSuiClient") + c.log.Info("NewSuiClient", + "relayer address", relayerAddress, + "bridge contract address", c.bridgeContractAddress, + "safe contract address", c.safeContractAddress) return c, err } func checkArgs(args ArgsSuiClient) error { - if check.IfNil(args.suiClient) { + if args.SuiClient == nil { return errNilClient } if len(args.RelayerPrivateKey) == 0 { return clients.ErrNilPrivateKey } - if args.BridgeContractAddress == "" { - return fmt.Errorf("%w for the MultisigContractAddress argument", errNilBridgeContract) + if args.BridgePackageId == "" { + return fmt.Errorf("%w for the BridgePackageId argument", errNilPackageId) } - if args.SafeContractAddress == "" { - return fmt.Errorf("%w for the SafeContractAddress argument", errNilSafeContract) + if args.BridgeObjectId == "" { + return fmt.Errorf("%w for the BridgeObjectId argument", errNilObjectId) + } + if args.SafePackageId == "" { + return fmt.Errorf("%w for the SafePackageId argument", errNilPackageId) + } + if args.SafeObjectId == "" { + return fmt.Errorf("%w for the SafeObjectId argument", errNilObjectId) } if check.IfNil(args.Log) { return clients.ErrNilLogger @@ -151,7 +167,7 @@ func checkArgs(args ArgsSuiClient) error { return nil } -func generateRelayerPubKeyAndAddress(priKey ed25519.PrivateKey) (ed25519.PublicKey, string) { +func generatePubKeyAndAddressFromPriKey(priKey ed25519.PrivateKey) (ed25519.PublicKey, string) { pubKey := priKey.Public().(ed25519.PublicKey) tmp := []byte{byte(keypair.Ed25519Flag)} @@ -162,14 +178,14 @@ func generateRelayerPubKeyAndAddress(priKey ed25519.PrivateKey) (ed25519.PublicK return pubKey, addr } +// GetBatch returns the transfer batch by providing the nonce func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.TransferBatch, bool, error) { c.log.Info("Getting batch", "nonce", nonce) - nonceAsBigInt := big.NewInt(0).SetUint64(nonce) - batch, isFinalBatch, err := c.clientWrapper.GetBatch(ctx, nonceAsBigInt) + batch, isFinalBatch, err := c.GetBatchByNonce(ctx, nonce) if err != nil { return nil, false, err } - deposits, areFinalDeposits, err := c.clientWrapper.GetBatchDeposits(ctx, nonceAsBigInt) + deposits, areFinalDeposits, err := c.GetBatchDeposits(ctx, nonce) if err != nil { return nil, false, err } @@ -179,7 +195,7 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf } transferBatch := &bridgeCore.TransferBatch{ - ID: batch.Nonce.Uint64(), + ID: batch.Nonce, BlockNumber: batch.BlockNumber, Deposits: make([]*bridgeCore.DepositTransfer, 0, batch.DepositsCount), } @@ -191,7 +207,7 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf tokenId := deposit.TokenAddress depositTransfer := &bridgeCore.DepositTransfer{ - Nonce: deposit.Nonce.Uint64(), + Nonce: deposit.Nonce, ToBytes: toBytes, DisplayableTo: c.addressConverter.ToBech32StringSilent(toBytes), FromBytes: fromBytes, @@ -221,12 +237,12 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf // WasExecuted returns true if the MultiversX batch ID was executed func (c *client) WasExecuted(ctx context.Context, mvxBatchID uint64) (bool, error) { - return c.clientWrapper.WasBatchExecuted(ctx, big.NewInt(0).SetUint64(mvxBatchID)) + return c.WasBatchExecuted(ctx, mvxBatchID) } // BroadcastSignatureForMessageHash will send the signature for the provided message hash func (c *client) BroadcastSignatureForMessageHash(msgHash []byte) { - signature, err := c.cryptoHandler.Sign(msgHash) + signature, err := c.txHandler.Sign(msgHash) if err != nil { c.log.Error("error generating signature", "msh hash", msgHash, "error", err) return @@ -236,8 +252,12 @@ func (c *client) BroadcastSignatureForMessageHash(msgHash []byte) { } // GenerateMessageHash will generate the message hash based on the provided batch -func (c *client) GenerateMessageHash(batch *batchProcessor.ArgListsBatch, batchId uint64) (common.Hash, error) { - panic("not implemented yet") +func (c *client) GenerateMessageHash(batch *batchProcessor.ArgListsBatchSui, batchId uint64) (string, error) { + if batch == nil { + return "", clients.ErrNilBatch + } + + //args, err := generateTransferArgs() } // ExecuteTransfer will initiate and send the transaction from the transfer batch struct @@ -252,7 +272,7 @@ func (c *client) ExecuteTransfer( return "", clients.ErrNilBatch } - isPaused, err := c.clientWrapper.IsPaused(ctx) + isPaused, err := c.IsPaused(ctx) if err != nil { return "", fmt.Errorf("%w in client.ExecuteTransfer", err) } @@ -270,30 +290,40 @@ func (c *client) ExecuteTransfer( signatures = signatures[:quorum] } - //minimumForFee := big.NewInt(int64(auth.GasLimit)) - //minimumForFee.Mul(minimumForFee, auth.GasPrice) - //err = c.checkRelayerFundsForFee(ctx, minimumForFee) - //if err != nil { - // return "", err - //} + moveCallReq := models.MoveCallRequest{ + Signer: c.relayerAddress, + PackageObjectId: c.bridgeContractAddress, + Module: "bridge", + Function: "execute_transfer", + TypeArguments: []interface{}{}, + Arguments: []interface{}{ + c.bridgeObjectId, + c.safeObjectId, + argLists.SuiTokens, + argLists.Recipients, + argLists.Amounts, + argLists.Nonces, + batchId, + signatures, + }, + GasBudget: "100000000", // TODO + } - batchID := big.NewInt(0).SetUint64(batchId) - tx, err := c.clientWrapper.ExecuteTransfer(argLists.SuiTokens, argLists.Recipients, argLists.Amounts, argLists.Nonces, batchID, signatures) + hash, err := c.txHandler.SendTransactionReturnHash(ctx, moveCallReq) if err != nil { return "", err } - txHash := tx.Hash().String() - c.log.Info("Executed transfer transaction", "batchID", batchID, "hash", txHash) + c.log.Info("Executed transfer transaction", "batchID", batchId, "hash", hash) - return txHash, err + return hash, err } func (c *client) CheckClientAvailability(ctx context.Context) error { c.mut.Lock() defer c.mut.Unlock() - currentCheckpoint, err := c.clientWrapper.GetLatestCheckpoint(ctx) + currentCheckpoint, err := c.GetLatestCheckpoint(ctx) if err != nil { c.setStatusForAvailabilityCheck(bridgeCore.Unavailable, err.Error(), currentCheckpoint) @@ -321,9 +351,9 @@ func (c *client) CheckClientAvailability(ctx context.Context) error { } func (c *client) setStatusForAvailabilityCheck(status bridgeCore.ClientStatus, message string, nonce uint64) { - c.clientWrapper.SetStringMetric(bridgeCore.MetricMultiversXClientStatus, status.String()) - c.clientWrapper.SetStringMetric(bridgeCore.MetricLastMultiversXClientError, message) - c.clientWrapper.SetIntMetric(bridgeCore.MetricLastBlockNonce, int(nonce)) + c.statusHandler.SetStringMetric(bridgeCore.MetricMultiversXClientStatus, status.String()) + c.statusHandler.SetStringMetric(bridgeCore.MetricLastMultiversXClientError, message) + c.statusHandler.SetIntMetric(bridgeCore.MetricLastBlockNonce, int(nonce)) } func (c *client) incrementRetriesAvailabilityCheck() { @@ -332,17 +362,9 @@ func (c *client) incrementRetriesAvailabilityCheck() { // CheckRequiredBalance will check if the safe has enough balance for the transfer func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, value *big.Int) error { - isMintBurn, err := c.MintBurnTokens(ctx, coinType) - if err != nil { - return err - } - - if isMintBurn { - return nil - } - existingBalance, err := c.clientWrapper.GetBalance(ctx, c.safeContractId, coinType) + existingBalance, err := c.GetBalance(ctx, c.safeContractAddress, coinType) if err != nil { - return fmt.Errorf("%w for owner %s for coin %s", err, c.safeContractId, coinType) + return fmt.Errorf("%w for owner %s for coin %s", err, c.safeContractAddress, coinType) } totalExistingBalanceStr := existingBalance.TotalBalance @@ -353,12 +375,12 @@ func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, valu } if value.Cmp(totalExistingBalance) > 0 { return fmt.Errorf("%w, existing: %s, required: %s for coin %s and owner %s", - errInsufficientCoinBalance, totalExistingBalanceStr, value.String(), coinType, c.safeContractId) + errInsufficientCoinBalance, totalExistingBalanceStr, value.String(), coinType, c.safeContractAddress) } c.log.Debug("checked coin balance", "Coin type", coinType, - "owner address", c.safeContractId, + "owner address", c.safeContractAddress, "existing balance", totalExistingBalanceStr, "needed", value.String()) @@ -367,37 +389,26 @@ func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, valu // TotalBalances returns the total balance of the given token func (c *client) TotalBalances(ctx context.Context, token string) (*big.Int, error) { - return c.clientWrapper.TotalBalances(ctx, token) -} - -// MintBalances returns the mint balance of the given token -func (c *client) MintBalances(ctx context.Context, token string) (*big.Int, error) { - return c.clientWrapper.MintBalances(ctx, token) -} - -// BurnBalances returns the burn balance of the given token -func (c *client) BurnBalances(ctx context.Context, token string) (*big.Int, error) { - return c.clientWrapper.BurnBalances(ctx, token) -} - -// MintBurnTokens returns true if the token is mintBurn token -func (c *client) MintBurnTokens(ctx context.Context, token string) (bool, error) { - return c.clientWrapper.MintBurnTokens(ctx, token) + balance, err := c.GetTotalBalanceFromSafe(ctx, token) + if err != nil { + return nil, err + } + return big.NewInt(0).SetUint64(balance), nil } -// NativeTokens returns true if the token is native -func (c *client) NativeTokens(ctx context.Context, token string) (bool, error) { - return c.clientWrapper.NativeTokens(ctx, token) -} +//// NativeTokens returns true if the token is native +//func (c *client) NativeTokens(ctx context.Context, token string) (bool, error) { +// return c.NativeTokens(ctx, token) +//} // WhitelistedTokens returns true if the token is whitelisted func (c *client) WhitelistedTokens(ctx context.Context, token string) (bool, error) { - return c.clientWrapper.WhitelistedTokens(ctx, token) + return c.IsTokenWhitelisted(ctx, token) } // GetTransactionsStatuses will return the transactions statuses from the batch -func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]byte, error) { - buff, isFinal, err := c.clientWrapper.GetStatusesAfterExecution(ctx, big.NewInt(0).SetUint64(batchId)) +func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]dtos.DepositStatus, error) { + buff, isFinal, err := c.GetStatusesAfterExecution(ctx, batchId) if err != nil { return nil, err } @@ -410,21 +421,25 @@ func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([ // GetQuorumSize returns the size of the quorum func (c *client) GetQuorumSize(ctx context.Context) (*big.Int, error) { - return c.clientWrapper.Quorum(ctx) + quorum, err := c.Quorum(ctx) + if err != nil { + return nil, err + } + return big.NewInt(0).SetUint64(quorum), nil } // IsQuorumReached returns true if the number of signatures is at least the size of quorum func (c *client) IsQuorumReached(ctx context.Context, msg []byte) (bool, error) { signatures := c.signatureHolder.Signatures(msg) - quorum, err := c.clientWrapper.Quorum(ctx) + quorum, err := c.Quorum(ctx) if err != nil { return false, fmt.Errorf("%w in IsQuorumReached, Quorum call", err) } - if quorum.Uint64() < minQuorumValue { - return false, fmt.Errorf("%w in IsQuorumReached, minQuorum %d, got: %s", clients.ErrInvalidValue, minQuorumValue, quorum.String()) + if quorum < minQuorumValue { + return false, fmt.Errorf("%w in IsQuorumReached, minQuorum %d, got: %s", clients.ErrInvalidValue, minQuorumValue, quorum) } - return len(signatures) >= int(quorum.Int64()), nil + return len(signatures) >= int(quorum), nil } // IsInterfaceNil returns true if there is no value under the interface diff --git a/clients/sui/errors.go b/clients/sui/errors.go index 30e000bf..9e9a49c5 100644 --- a/clients/sui/errors.go +++ b/clients/sui/errors.go @@ -4,14 +4,12 @@ import "errors" var ( errInsufficientCoinBalance = errors.New("insufficient coin balance") - errEmptyAddress = errors.New("empty address") + errNilObjectId = errors.New("nil object id") + errNilPackageId = errors.New("nil package id") errNilRelayerSigner = errors.New("nil relayer signer") errNilClient = errors.New("nil client") errGetCoinObjectsFailed = errors.New("get coin objects failed") errNoGasObjects = errors.New("no gas objects found") errInvalidCoinType = errors.New("invalid coin type format") - - errNilBlockchainClient = errors.New("nil blockchain client") - errNilSafeContract = errors.New("nil safe contract") - errNilBridgeContract = errors.New("nil bridge contract") + errNilBlockchainClient = errors.New("nil blockchain client") ) diff --git a/clients/sui/interface.go b/clients/sui/interface.go index bf292329..0b90e8c5 100644 --- a/clients/sui/interface.go +++ b/clients/sui/interface.go @@ -36,12 +36,7 @@ type Broadcaster interface { IsInterfaceNil() bool } -// CryptoHandler defines the operations for a component that expose some crypto primitives -type CryptoHandler interface { - Sign(msg []byte) ([]byte, error) - IsInterfaceNil() bool -} - type txHandler interface { - SendTransaction(ctx context.Context, moveCallRequest models.MoveCallRequest, gasLimit uint64) (string, error) + SendTransactionReturnHash(ctx context.Context, moveCallRequest models.MoveCallRequest) (string, error) + Sign(message []byte) ([]byte, error) } diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go index ad165a13..4125f1f8 100644 --- a/clients/sui/suiClientDataGetter.go +++ b/clients/sui/suiClientDataGetter.go @@ -2,7 +2,6 @@ package sui import ( "context" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -11,7 +10,6 @@ import ( "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/mystenbcs" - "github.com/block-vision/sui-go-sdk/signer" "github.com/block-vision/sui-go-sdk/sui" "github.com/block-vision/sui-go-sdk/transaction" "github.com/multiversx/mx-bridge-eth-go/clients" @@ -20,13 +18,11 @@ import ( "github.com/multiversx/mx-chain-core-go/core/check" ) -const suiCoinType = "0x2::sui::SUI" - // ArgsSuiClientDataGetter is the arguments DTO used in the NewSuiClientDataGetter constructor type ArgsSuiClientDataGetter struct { SafeContractAddress string BridgeContractAddress string - Relayer *signer.Signer + RelayerAddress string Client sui.ISuiAPI Log chainCore.Logger } @@ -37,8 +33,8 @@ type suiClientDataGetter struct { bridgeContractAddress string bridgeObjectIdBytes models.SuiAddressBytes - relayer *signer.Signer - client *sui.Client + relayerAddress string + client sui.ISuiAPI initialSharedVersionForObject sync.Map // to cache the initial shared version for objects log chainCore.Logger mtx sync.Mutex @@ -52,19 +48,14 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, if args.Client == nil { return nil, errNilClient } - if args.Relayer == nil { - return nil, errNilRelayerSigner + if args.RelayerAddress == "" { + return nil, fmt.Errorf("%w for the signer address argument", errEmptyAddress) } if args.BridgeContractAddress == "" { - return nil, fmt.Errorf("%w for the BridgeContractAddress argument", errEmptyAddress) + return nil, fmt.Errorf("%w for the BridgePackageId argument", errEmptyAddress) } if args.SafeContractAddress == "" { - return nil, fmt.Errorf("%w for the SafeContractAddress argument", errEmptyAddress) - } - - var suiClient, ok = args.Client.(*sui.Client) - if !ok { - panic("not sui client") + return nil, fmt.Errorf("%w for the SafePackageId argument", errEmptyAddress) } safeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.SafeContractAddress)) @@ -82,128 +73,19 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, safeObjectIdBytes: *safeObjectIdBytes, bridgeContractAddress: args.BridgeContractAddress, bridgeObjectIdBytes: *bridgeObjectIdBytes, - relayer: args.Relayer, - client: suiClient, + relayerAddress: args.RelayerAddress, + client: args.Client, log: args.Log, }, nil - -} - -func (getter *suiClientDataGetter) getGasObject(ctx context.Context) (*transaction.SuiObjectRef, error) { - gasObjects, err := getter.client.SuiXGetCoins(ctx, models.SuiXGetCoinsRequest{ - Owner: getter.relayer.Address, - CoinType: suiCoinType, - Limit: 1, - }) - if err != nil { - return nil, errGetCoinObjectsFailed - } - - if len(gasObjects.Data) == 0 { - fmt.Println("NU ai obiecte de gas!") - return nil, fmt.Errorf("%w for address %s", errNoGasObjects, getter.relayer.Address) - } - - gasCoin, err := transaction.NewSuiObjectRef( - models.SuiAddress(gasObjects.Data[0].CoinObjectId), - gasObjects.Data[0].Version, - models.ObjectDigest(gasObjects.Data[0].Digest), - ) - if err != nil { - return nil, fmt.Errorf("failed to create gas object ref: %w", err) - } - - return gasCoin, nil } -func (getter *suiClientDataGetter) getNewTransaction(ctx context.Context) (*transaction.Transaction, error) { - gasCoin, err := getter.getGasObject(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get gas object: %w", err) - } - +// GetBatchByNonce returns the batch of transactions by providing the batch nonce +func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNonce uint64) (dtos.Batch, bool, error) { tx := transaction.NewTransaction() - tx.SetSuiClient(getter.client). - SetSigner(getter.relayer). - SetSender(models.SuiAddress(getter.relayer.Address)). - SetGasPrice(1000). - SetGasBudget(50000000). - SetGasPayment([]transaction.SuiObjectRef{*gasCoin}). - SetGasOwner(models.SuiAddress(getter.relayer.Address)) - - return tx, nil -} - -func (getter *suiClientDataGetter) getTxBytes(tx *transaction.Transaction) (string, error) { - bcsEncodedMsg, err := tx.Data.V1.Kind.Marshal() - if err != nil { - return "", fmt.Errorf("failed to marshal transaction data: %w", err) - } - txBytes := mystenbcs.ToBase64(bcsEncodedMsg) - - return txBytes, nil -} - -func (getter *suiClientDataGetter) sendTxGetBlockResponse(ctx context.Context, tx *transaction.Transaction) (models.SuiTransactionBlockResponse, error) { - txBytes, err := getter.getTxBytes(tx) - if err != nil { - return models.SuiTransactionBlockResponse{}, err - } - - return getter.client.SuiDevInspectTransactionBlock(ctx, models.SuiDevInspectTransactionBlockRequest{ - Sender: getter.relayer.Address, - TxBytes: txBytes, - }) -} - -func (getter *suiClientDataGetter) getInitialSharedVersionForObject(ctx context.Context, objectId string) (uint64, error) { - if value, exists := getter.initialSharedVersionForObject.Load(objectId); exists { - return value.(uint64), nil - } - - // If not found, make the network call - rsp, err := getter.client.SuiGetObject(ctx, models.SuiGetObjectRequest{ - ObjectId: objectId, - Options: models.SuiObjectDataOptions{ - ShowOwner: true, - }, - }) - - if err != nil { - return 0, fmt.Errorf("failed to get object %s: %w", objectId, err) - } - - data := rsp.Data - ownerBytes, err := json.Marshal(data.Owner) - if err != nil { - return 0, fmt.Errorf("failed to marshal owner: %w", err) - } - - var sharedOwner dtos.SharedOwner - if err = json.Unmarshal(ownerBytes, &sharedOwner); err != nil { - return 0, fmt.Errorf("failed to unmarshal to SharedOwner: %w", err) - } - - initialSharedVersion := sharedOwner.Shared.InitialSharedVersion - - if actualValue, loaded := getter.initialSharedVersionForObject.LoadOrStore(objectId, initialSharedVersion); loaded { - // if another goroutine stored it first, return that value - return actualValue.(uint64), nil - } - - return initialSharedVersion, nil -} - -// GetBatch returns the batch of transactions by providing the batch nonce -func (getter *suiClientDataGetter) GetBatch(ctx context.Context, batchNonce uint64) (dtos.Batch, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return dtos.Batch{}, fmt.Errorf("failed to create new transaction: %w", err) - } initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { - return dtos.Batch{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + return dtos.Batch{}, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) } tx.MoveCall( @@ -229,41 +111,30 @@ func (getter *suiClientDataGetter) GetBatch(ctx context.Context, batchNonce uint txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) if err != nil { - return dtos.Batch{}, fmt.Errorf("failed to get batch: %w", err) + return dtos.Batch{}, false, fmt.Errorf("failed to get batch: %w", err) } if txBlockResp.Effects.Status.Status != "success" { - return dtos.Batch{}, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + return dtos.Batch{}, false, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) } - var batch *dtos.Batch - result := txBlockResp.Results - returnValues, err := ExtractReturnValues(result) + var batch dtos.Batch + var isFinalBatch bool + err = DecodeReturnValues(txBlockResp.Results, &batch, &isFinalBatch) if err != nil { - return dtos.Batch{}, fmt.Errorf("failed to extract return values: %w", err) - } - if len(returnValues) > 0 && strings.HasSuffix(returnValues[0].Type, "::Batch") { - batch, err = DecodeStructFromExtracted(returnValues[0], BatchDecoder) - if err != nil { - return dtos.Batch{}, fmt.Errorf("failed to decode batch: %w", err) - } - } else { - return dtos.Batch{}, fmt.Errorf("wrong expected return value, got %s", returnValues[0].Type) + return dtos.Batch{}, false, fmt.Errorf("failed to decode return value: %w", err) } - return *batch, nil + return batch, isFinalBatch, nil } // GetBatchDeposits returns the transactions of a batch by providing the batch nonce -func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNonce uint64) ([]dtos.Deposit, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return nil, fmt.Errorf("failed to create new transaction: %w", err) - } +func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNonce uint64) ([]dtos.Deposit, bool, error) { + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { - return []dtos.Deposit{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + return nil, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) } tx.MoveCall( @@ -289,23 +160,26 @@ func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNo txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) if err != nil { - return nil, fmt.Errorf("failed to get batch deposits: %w", err) + return nil, false, fmt.Errorf("failed to get batch deposits: %w", err) } if txBlockResp.Effects.Status.Status != "success" { - return nil, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) + return nil, false, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return []dtos.Deposit{}, nil + var depositsList []dtos.Deposit + var areFinalDeposits bool + err = DecodeReturnValues(txBlockResp.Results, &depositsList, &areFinalDeposits) + if err != nil { + return nil, false, fmt.Errorf("failed to decode return value: %w", err) + } + + return depositsList, areFinalDeposits, nil } // GetRelayers returns all whitelisted sui addresses func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.SuiAddress, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return nil, fmt.Errorf("failed to create new transaction: %w", err) - } + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { @@ -341,16 +215,18 @@ func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.Su return nil, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return []models.SuiAddress{}, nil + var relayersAddresses []models.SuiAddress + err = DecodeReturnValues(txBlockResp.Results, &relayersAddresses) + if err != nil { + return []models.SuiAddress{}, fmt.Errorf("failed to decode return value: %w", err) + } + + return relayersAddresses, nil } // WasBatchExecuted returns true if the batch was executed func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNonce uint64) (bool, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return false, fmt.Errorf("failed to create new transaction: %w", err) - } + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { @@ -387,16 +263,18 @@ func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNo return false, fmt.Errorf("failed to get batch: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return false, nil + var wasBatchExecuted bool + err = DecodeReturnValues(txBlockResp.Results, &wasBatchExecuted) + if err != nil { + return false, fmt.Errorf("failed to decode return value: %w", err) + } + + return wasBatchExecuted, nil } // IsPaused returns true if the bridge contract is paused func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return false, fmt.Errorf("failed to create new transaction: %w", err) - } + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { @@ -432,16 +310,18 @@ func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { return false, fmt.Errorf("get_pause transaction failed: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return false, nil + var isPaused bool + err = DecodeReturnValues(txBlockResp.Results, &isPaused) + if err != nil { + return false, fmt.Errorf("failed to decode return value: %w", err) + } + + return isPaused, nil } // Quorum returns the current set quorum value func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return 0, fmt.Errorf("failed to create new transaction: %w", err) - } + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { @@ -477,20 +357,22 @@ func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { return 0, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return 0, nil + var quorum uint64 + err = DecodeReturnValues(txBlockResp.Results, &quorum) + if err != nil { + return 0, fmt.Errorf("failed to decode return value: %w", err) + } + + return quorum, nil } // GetStatusesAfterExecution returns the statuses of the last executed transfer -func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) (uint64, error) { - tx, err := getter.getNewTransaction(ctx) - if err != nil { - return 0, fmt.Errorf("failed to create new transaction: %w", err) - } +func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) ([]dtos.DepositStatus, bool, error) { + tx := transaction.NewTransaction() initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) if err != nil { - return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) + return []dtos.DepositStatus{}, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) } tx.MoveCall( @@ -516,24 +398,27 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) if err != nil { - return 0, fmt.Errorf("failed to get quorum: %w", err) + return []dtos.DepositStatus{}, false, fmt.Errorf("failed to get quorum: %w", err) } if txBlockResp.Effects.Status.Status != "success" { - return 0, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) + return []dtos.DepositStatus{}, false, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return 0, nil -} - -// TotalBalances returns the total balance of the given token -func (getter *suiClientDataGetter) TotalBalances(ctx context.Context, coinType string) (uint64, error) { - tx, err := getter.getNewTransaction(ctx) + var depositStatuses []dtos.DepositStatus + var isFinal bool + err = DecodeReturnValues(txBlockResp.Results, &depositStatuses, &isFinal) if err != nil { - return 0, fmt.Errorf("failed to create new transaction: %w", err) + return []dtos.DepositStatus{}, false, fmt.Errorf("failed to decode return value: %w", err) } + return depositStatuses, isFinal, nil +} + +// GetTotalBalanceFromSafe returns the total balance of the given token +func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, coinType string) (uint64, error) { + tx := transaction.NewTransaction() + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) if err != nil { return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) @@ -586,17 +471,19 @@ func (getter *suiClientDataGetter) TotalBalances(ctx context.Context, coinType s return 0, fmt.Errorf("get total balances transaction failed: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return 0, nil -} - -// WhitelistedTokens returns true if the token is whitelisted -func (getter *suiClientDataGetter) WhitelistedTokens(ctx context.Context, coinType string) (bool, error) { - tx, err := getter.getNewTransaction(ctx) + var totalBalance uint64 + err = DecodeReturnValues(txBlockResp.Results, &totalBalance) if err != nil { - return false, fmt.Errorf("failed to create new transaction: %w", err) + return 0, fmt.Errorf("failed to decode return value: %w", err) } + return totalBalance, nil +} + +// IsTokenWhitelisted returns true if the token is whitelisted +func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinType string) (bool, error) { + tx := transaction.NewTransaction() + initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) if err != nil { return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) @@ -649,8 +536,13 @@ func (getter *suiClientDataGetter) WhitelistedTokens(ctx context.Context, coinTy return false, fmt.Errorf("get total balances transaction failed: %s", txBlockResp.Effects.Status.Error) } - // TODO: decode result - return false, nil + var isTokenWhitelisted bool + err = DecodeReturnValues(txBlockResp.Results, &isTokenWhitelisted) + if err != nil { + return false, fmt.Errorf("failed to decode return value: %w", err) + } + + return isTokenWhitelisted, nil } // GetLatestCheckpoint returns the latest checkpoint sequence number @@ -686,95 +578,87 @@ func ParseCoinType(coinType string) ([3]string, error) { return [3]string{packageAddr, module, structName}, nil } -// ExtractReturnValues extracts and parses return values from json.RawMessage -func ExtractReturnValues(rawResults json.RawMessage) ([]dtos.ReturnValue, error) { - var returnValues []dtos.ReturnValue +// DecodeReturnValues decodes the return value from a transaction block response. +func DecodeReturnValues(data json.RawMessage, out ...interface{}) error { + var results []dtos.InspectResult + if err := json.Unmarshal(data, &results); err != nil { + return fmt.Errorf("decode dev inspect results: %w", err) + } - if len(rawResults) == 0 { - return returnValues, nil + if len(results) == 0 { + return fmt.Errorf("no dev inspect results") } - var results []dtos.InspectResult - if err := json.Unmarshal(rawResults, &results); err != nil { - return nil, fmt.Errorf("failed to unmarshal results: %w", err) - } - - for _, result := range results { - for _, rv := range result.ReturnValues { - if len(rv) >= 2 { - returnValue := dtos.ReturnValue{ - Value: rv[0], - Type: fmt.Sprintf("%v", rv[1]), - } - returnValues = append(returnValues, returnValue) - } + returnValues := results[0].ReturnValues + if len(returnValues) < len(out) { + return fmt.Errorf("expected at least %d return values, got %d", len(out), len(returnValues)) + } + + for i := range out { + if _, err := mystenbcs.Unmarshal(returnValues[i].Bytes, out[i]); err != nil { + return fmt.Errorf("unmarshal return value %d: %w", i, err) } } - return returnValues, nil + return nil } -// DecodeStructFromExtracted decodes a struct from the extracted return value. -func DecodeStructFromExtracted[T any](rv dtos.ReturnValue, decoder func([]interface{}) (*T, error)) (*T, error) { - fields, ok := rv.Value.([]interface{}) - if !ok { - return nil, fmt.Errorf("expected struct fields as []interface{}, got %T", rv.Value) +func (getter *suiClientDataGetter) getTxBytes(tx *transaction.Transaction) (string, error) { + bcsEncodedMsg, err := tx.Data.V1.Kind.Marshal() + if err != nil { + return "", fmt.Errorf("failed to marshal transaction data: %w", err) } - return decoder(fields) + txBytes := mystenbcs.ToBase64(bcsEncodedMsg) + + return txBytes, nil } -func BatchDecoder(fields []interface{}) (*dtos.Batch, error) { - if len(fields) != 26 { - return nil, fmt.Errorf("expected 26 bytes for serialized Sword, got %d", len(fields)) +func (getter *suiClientDataGetter) sendTxGetBlockResponse(ctx context.Context, tx *transaction.Transaction) (models.SuiTransactionBlockResponse, error) { + txBytes, err := getter.getTxBytes(tx) + if err != nil { + return models.SuiTransactionBlockResponse{}, err } - bytes := make([]byte, 26) - for i, v := range fields { - f, ok := v.(float64) - if !ok { - return nil, fmt.Errorf("expected float64 byte at index %d, got %T", i, v) - } - bytes[i] = byte(f) - } + return getter.client.SuiDevInspectTransactionBlock(ctx, models.SuiDevInspectTransactionBlockRequest{ + Sender: getter.relayerAddress, + TxBytes: txBytes, + }) +} - nonce, err := parseU64(bytes[0:8]) - if err != nil { - return nil, fmt.Errorf("failed to decode nonce: %v", err) +func (getter *suiClientDataGetter) getInitialSharedVersionForObject(ctx context.Context, objectId string) (uint64, error) { + if value, exists := getter.initialSharedVersionForObject.Load(objectId); exists { + return value.(uint64), nil } - blockNumber, err := parseU64(bytes[8:16]) + // If not found, make the network call + rsp, err := getter.client.SuiGetObject(ctx, models.SuiGetObjectRequest{ + ObjectId: objectId, + Options: models.SuiObjectDataOptions{ + ShowOwner: true, + }, + }) + if err != nil { - return nil, fmt.Errorf("failed to decode block_number: %v", err) + return 0, fmt.Errorf("failed to get object %s: %w", objectId, err) } - lastUpdatedBlock, err := parseU64(bytes[16:24]) + data := rsp.Data + ownerBytes, err := json.Marshal(data.Owner) if err != nil { - return nil, fmt.Errorf("failed to decode last_updated_block: %v", err) + return 0, fmt.Errorf("failed to marshal owner: %w", err) } - depositsCount, err := parseU16(bytes[24:26]) - if err != nil { - return nil, fmt.Errorf("failed to decode deposits_count: %v", err) + var sharedOwner dtos.SharedOwner + if err = json.Unmarshal(ownerBytes, &sharedOwner); err != nil { + return 0, fmt.Errorf("failed to unmarshal to SharedOwner: %w", err) } - return &dtos.Batch{ - Nonce: nonce, - BlockNumber: blockNumber, - LastUpdatedBlockNumber: lastUpdatedBlock, - DepositsCount: depositsCount, - }, nil -} + initialSharedVersion := sharedOwner.Shared.InitialSharedVersion -func parseU64(bytes []byte) (uint64, error) { - if len(bytes) != 8 { - return 0, fmt.Errorf("expected 8 bytes for u64, got %d", len(bytes)) + if actualValue, loaded := getter.initialSharedVersionForObject.LoadOrStore(objectId, initialSharedVersion); loaded { + // if another goroutine stored it first, return that value + return actualValue.(uint64), nil } - return binary.LittleEndian.Uint64(bytes), nil -} -func parseU16(bytes []byte) (uint16, error) { - if len(bytes) != 2 { - return 0, fmt.Errorf("expected 2 bytes for u16, got %d", len(bytes)) - } - return binary.LittleEndian.Uint16(bytes), nil + return initialSharedVersion, nil } diff --git a/clients/sui/transactionHandler.go b/clients/sui/transactionHandler.go index 71d30d03..24955cf5 100644 --- a/clients/sui/transactionHandler.go +++ b/clients/sui/transactionHandler.go @@ -2,20 +2,20 @@ package sui import ( "context" - "crypto/ed25519" + "encoding/base64" + "errors" "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/signer" + "github.com/block-vision/sui-go-sdk/sui" ) type transactionHandler struct { - client SuiClient - relayerAddress string - relayerPrivateKey ed25519.PrivateKey - bridgeContractAddress string + client sui.ISuiAPI + relayerSigner *signer.Signer } -func (txHandler *transactionHandler) SendTransaction(ctx context.Context, moveCallRequest models.MoveCallRequest, gasLimit uint64) (string, error) { - moveCallRequest.Signer = txHandler.relayerAddress - moveCallRequest.PackageObjectId = txHandler.bridgeContractAddress +func (txHandler *transactionHandler) SendTransactionReturnHash(ctx context.Context, moveCallRequest models.MoveCallRequest) (string, error) { + moveCallRequest.Signer = txHandler.relayerSigner.Address txnMetaData, err := txHandler.client.MoveCall(ctx, moveCallRequest) if err != nil { @@ -24,10 +24,11 @@ func (txHandler *transactionHandler) SendTransaction(ctx context.Context, moveCa txBlockResponse, err := txHandler.client.SignAndExecuteTransactionBlock(ctx, models.SignAndExecuteTransactionBlockRequest{ TxnMetaData: txnMetaData, - PriKey: txHandler.relayerPrivateKey, + PriKey: txHandler.relayerSigner.PriKey, Options: models.SuiTransactionBlockOptions{ - ShowInput: true, - ShowRawInput: true, + ShowInput: true, + ShowEffects: true, + ShowObjectChanges: true, }, RequestType: "WaitForLocalExecution", }) @@ -35,5 +36,17 @@ func (txHandler *transactionHandler) SendTransaction(ctx context.Context, moveCa return "", err } + if txBlockResponse.Effects.Status.Status != "success" { + return "", errors.New(txBlockResponse.Effects.Status.Error) + } + return txBlockResponse.Digest, nil } + +func (txHandler *transactionHandler) Sign(message []byte) ([]byte, error) { + signedMessageSerializedSig, err := txHandler.relayerSigner.SignPersonalMessage(string(message)) + if err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(signedMessageSerializedSig.Signature) +} From ec68ca7fdd3ea0fc52b64db4489d704ccc015158 Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Thu, 3 Jul 2025 13:21:08 +0300 Subject: [PATCH 3/9] add all functionalities for sui client --- clients/sui/client.go | 20 ++++++++++-- clients/sui/dtos/contractStructs.go | 15 ++++++++- .../dtos/transactionBlockResponseResult.go | 32 +++++++++++++++++-- core/batchProcessor/batchProcessor.go | 15 +++++++-- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/clients/sui/client.go b/clients/sui/client.go index f7b436db..bdd4aab9 100644 --- a/clients/sui/client.go +++ b/clients/sui/client.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/block-vision/sui-go-sdk/common/keypair" "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/mystenbcs" signer "github.com/block-vision/sui-go-sdk/signer" "github.com/block-vision/sui-go-sdk/sui" "github.com/multiversx/mx-bridge-eth-go/clients" @@ -252,12 +253,25 @@ func (c *client) BroadcastSignatureForMessageHash(msgHash []byte) { } // GenerateMessageHash will generate the message hash based on the provided batch -func (c *client) GenerateMessageHash(batch *batchProcessor.ArgListsBatchSui, batchId uint64) (string, error) { +func (c *client) GenerateMessageHash(batch *batchProcessor.ArgListsBatchSui, batchId uint64) ([]byte, error) { if batch == nil { - return "", clients.ErrNilBatch + return nil, clients.ErrNilBatch + } + + transferData := batchProcessor.SuiTransferData{ + Recipients: batch.Recipients, + SuiTokens: batch.SuiTokens, + Amounts: batch.Amounts, + Nonces: batch.Nonces, + BatchId: batchId, } - //args, err := generateTransferArgs() + transferDataBytes, err := mystenbcs.Marshal(transferData) + if err != nil { + return nil, fmt.Errorf("error marshaling transfer data: %v", err) + } + hash := blake2b.Sum256(transferDataBytes) + return hash[:], nil } // ExecuteTransfer will initiate and send the transaction from the transfer batch struct diff --git a/clients/sui/dtos/contractStructs.go b/clients/sui/dtos/contractStructs.go index 1229c4ad..c413e14b 100644 --- a/clients/sui/dtos/contractStructs.go +++ b/clients/sui/dtos/contractStructs.go @@ -2,6 +2,7 @@ package dtos import "math/big" +// Batch represents a batch of deposits from safe contract type Batch struct { Nonce uint64 BlockNumber uint64 @@ -9,11 +10,23 @@ type Batch struct { DepositsCount uint16 } +// Deposit represents a single deposit in the batch type Deposit struct { - Nonce *big.Int + Nonce uint64 TokenAddress string Amount *big.Int Depositor [32]byte Recipient [32]byte Status uint8 } + +// DepositStatus represents the status of a deposit +type DepositStatus byte + +const ( + None DepositStatus = iota + Pending + InProgress + Executed + Rejected +) diff --git a/clients/sui/dtos/transactionBlockResponseResult.go b/clients/sui/dtos/transactionBlockResponseResult.go index d8150eaf..dc27d889 100644 --- a/clients/sui/dtos/transactionBlockResponseResult.go +++ b/clients/sui/dtos/transactionBlockResponseResult.go @@ -1,12 +1,40 @@ package dtos +import ( + "encoding/json" + "fmt" +) + // InspectResult represents the result of inspecting a transaction block type InspectResult struct { - ReturnValues [][]interface{} `json:"returnValues"` + ReturnValues []ReturnValue `json:"returnValues"` } // ReturnValue represents a single return value with its type type ReturnValue struct { - Value interface{} + Bytes []byte Type string } + +func (rv *ReturnValue) UnmarshalJSON(data []byte) error { + var parts []json.RawMessage + err := json.Unmarshal(data, &parts) + if err != nil { + return fmt.Errorf("split into parts: %w", err) + } + if len(parts) != 2 { + return fmt.Errorf("expected 2 elements, got %d", len(parts)) + } + + err = json.Unmarshal(parts[0], &rv.Bytes) + if err != nil { + return fmt.Errorf("decode byte array: %w", err) + } + + err = json.Unmarshal(parts[1], &rv.Type) + if err != nil { + return fmt.Errorf("decode type string: %w", err) + } + + return nil +} diff --git a/core/batchProcessor/batchProcessor.go b/core/batchProcessor/batchProcessor.go index d0621ddb..cb3e66d5 100644 --- a/core/batchProcessor/batchProcessor.go +++ b/core/batchProcessor/batchProcessor.go @@ -30,14 +30,23 @@ type ArgListsBatch struct { // ArgListsBatchSui is a struct that contains the batch data in a format that is easy to use for Sui type ArgListsBatchSui struct { - SuiTokens []string + SuiTokens [][]byte Recipients []models.SuiAddress MvxTokenBytes [][]byte - Amounts []*big.Int - Nonces []*big.Int + Amounts []uint64 + Nonces []uint64 Direction Direction } +// SuiTransferData is a struct that contains the transfer data to be signed +type SuiTransferData struct { + Recipients []models.SuiAddress + SuiTokens [][]byte + Amounts []uint64 + Nonces []uint64 + BatchId uint64 +} + // ExtractListMvxToEth will extract the batch data into a format that is easy to use // The transfer is from MultiversX to Ethereum func ExtractListMvxToEth(batch *bridgeCore.TransferBatch) *ArgListsBatch { From 90acb934995c16bdcf7da38d51d03ebb8a5aff2c Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Thu, 3 Jul 2025 14:15:27 +0300 Subject: [PATCH 4/9] client tests --- clients/sui/client_test.go | 938 +++++++++++++++++++++++++++++++++++++ 1 file changed, 938 insertions(+) create mode 100644 clients/sui/client_test.go diff --git a/clients/sui/client_test.go b/clients/sui/client_test.go new file mode 100644 index 00000000..1170972a --- /dev/null +++ b/clients/sui/client_test.go @@ -0,0 +1,938 @@ +package sui + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/multiversx/mx-bridge-eth-go/clients" + "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/core/converters" + "github.com/multiversx/mx-bridge-eth-go/testsCommon" + bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" + "github.com/multiversx/mx-chain-core-go/core/check" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/stretchr/testify/assert" +) + +func createMockSuiClientArgs() ArgsSuiClient { + addressConverter, err := converters.NewAddressConverter() + if err != nil { + panic(err) + } + + return ArgsSuiClient{ + ClientWrapper: &bridgeTests.SuiClientWrapperStub{}, + Log: logger.GetOrCreate("test"), + AddressConverter: addressConverter, + Broadcaster: &testsCommon.BroadcasterStub{}, + TokensMapper: &bridgeTests.TokensMapperStub{ + ConvertTokenCalled: func(ctx context.Context, sourceBytes []byte) ([]byte, error) { + return append([]byte("SUI"), sourceBytes...), nil + }, + }, + CryptoHandler: &bridgeTests.SuiCryptoHandlerStub{}, + SignatureHolder: &testsCommon.SignaturesHolderStub{}, + SafeContractId: testsCommon.CreateRandomSuiAddress(), + TransferGasLimitBase: 50, + TransferGasLimitForEach: 20, + ClientAvailabilityAllowDelta: 5, + EventsBlockRangeFrom: -100, + EventsBlockRangeTo: 400, + } +} + +func TestNewSuiClient(t *testing.T) { + t.Parallel() + + t.Run("nil client wrapper", func(t *testing.T) { + args := createMockSuiClientArgs() + args.ClientWrapper = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilClientWrapper, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil logger", func(t *testing.T) { + args := createMockSuiClientArgs() + args.Log = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilLogger, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil address converter", func(t *testing.T) { + args := createMockSuiClientArgs() + args.AddressConverter = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilAddressConverter, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil broadcaster", func(t *testing.T) { + args := createMockSuiClientArgs() + args.Broadcaster = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilBroadcaster, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil crypto handler", func(t *testing.T) { + args := createMockSuiClientArgs() + args.CryptoHandler = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilCryptoHandler, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil tokens mapper", func(t *testing.T) { + args := createMockSuiClientArgs() + args.TokensMapper = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilTokensMapper, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("nil signature holder", func(t *testing.T) { + args := createMockSuiClientArgs() + args.SignatureHolder = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilSignaturesHolder, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("0 transfer gas limit base", func(t *testing.T) { + args := createMockSuiClientArgs() + args.TransferGasLimitBase = 0 + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrInvalidGasLimit, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("0 transfer gas limit for each", func(t *testing.T) { + args := createMockSuiClientArgs() + args.TransferGasLimitForEach = 0 + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrInvalidGasLimit, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("invalid ClientAvailabilityAllowDelta should error", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientAvailabilityAllowDelta = 0 + + c, err := NewSuiClient(args) + + assert.True(t, check.IfNil(c)) + assert.True(t, errors.Is(err, clients.ErrInvalidValue)) + assert.True(t, strings.Contains(err.Error(), "for args.AllowedDelta")) + }) + t.Run("invalid events block range from should error", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.EventsBlockRangeFrom = 100 + args.EventsBlockRangeTo = 50 + + c, err := NewSuiClient(args) + + assert.True(t, check.IfNil(c)) + assert.True(t, errors.Is(err, clients.ErrInvalidValue)) + assert.True(t, strings.Contains(err.Error(), "args.EventsBlockRangeFrom")) + assert.True(t, strings.Contains(err.Error(), "args.EventsBlockRangeTo")) + }) + t.Run("nil crypto handler", func(t *testing.T) { + args := createMockSuiClientArgs() + args.CryptoHandler = nil + c, err := NewSuiClient(args) + + assert.Equal(t, clients.ErrNilCryptoHandler, err) + assert.True(t, check.IfNil(c)) + }) + t.Run("should work", func(t *testing.T) { + args := createMockSuiClientArgs() + c, err := NewSuiClient(args) + + assert.Nil(t, err) + assert.False(t, check.IfNil(c)) + }) +} + +func TestClient_GetBatch(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + c, _ := NewSuiClient(args) + expectedErr := errors.New("expected error") + + t.Run("error while getting batch", func(t *testing.T) { + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{}, false, expectedErr + }, + } + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Nil(t, batch) + assert.Equal(t, expectedErr, err) + assert.False(t, isFinal) + }) + t.Run("error while getting deposits", func(t *testing.T) { + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{ + Nonce: batchNonce, + DepositsCount: 3, + }, true, nil + }, + GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { + return nil, false, expectedErr + }, + } + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Nil(t, batch) + assert.Equal(t, expectedErr, err) + assert.False(t, isFinal) + }) + t.Run("deposits mismatch - with 0", func(t *testing.T) { + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{ + Nonce: batchNonce, + DepositsCount: 3, + }, true, nil + }, + GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { + return make([]core.Deposit, 0), true, nil + }, + } + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Nil(t, batch) + assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) + assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 3, fetched deposits len: 0")) + assert.False(t, isFinal) + }) + t.Run("deposits mismatch - with non zero value", func(t *testing.T) { + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{ + Nonce: batchNonce, + DepositsCount: 3, + }, true, nil + }, + GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { + return make([]core.Deposit, 4), true, nil + }, + } + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Nil(t, batch) + assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) + assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 3, fetched deposits len: 4")) + assert.False(t, isFinal) + }) + t.Run("returns batch should work", func(t *testing.T) { + from1 := testsCommon.CreateRandomSuiAddressBytes() + token1 := testsCommon.CreateRandomCoinId() + recipient1 := testsCommon.CreateRandomMultiversXAddress() + + from2 := testsCommon.CreateRandomSuiAddressBytes() + token2 := testsCommon.CreateRandomCoinId() + recipient2 := testsCommon.CreateRandomMultiversXAddress() + + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{ + Nonce: big.NewInt(537), + BlockNumber: 0, + LastUpdatedBlockNumber: 0, + DepositsCount: 2, + }, true, nil + }, + GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { + return []core.Deposit{ + { + Nonce: big.NewInt(10), + TokenAddress: token1, + Amount: big.NewInt(20), + Depositor: from1, + Recipient: recipient1.AddressSlice(), + }, + { + Nonce: big.NewInt(30), + TokenAddress: token2, + Amount: big.NewInt(40), + Depositor: from2, + Recipient: recipient2.AddressSlice(), + }, + }, true, nil + }, + } + + bech32Recipient1Address, _ := recipient1.AddressAsBech32String() + bech32Recipient2Address, _ := recipient2.AddressAsBech32String() + expectedBatch := &core.TransferBatch{ + ID: 537, + Deposits: []*core.DepositTransfer{ + { + Nonce: 10, + ToBytes: recipient1.AddressBytes(), + DisplayableTo: bech32Recipient1Address, + FromBytes: from1[:], + DisplayableFrom: hex.EncodeToString(from1[:]), + SourceTokenBytes: []byte(token1), + DisplayableToken: token1, + Amount: big.NewInt(20), + DestinationTokenBytes: append([]byte("SUI"), token1[:]...), + }, + { + Nonce: 30, + ToBytes: recipient2.AddressBytes(), + DisplayableTo: bech32Recipient2Address, + FromBytes: from2[:], + DisplayableFrom: hex.EncodeToString(from2[:]), + SourceTokenBytes: []byte(token2), + DisplayableToken: token2, + Amount: big.NewInt(40), + DestinationTokenBytes: append([]byte("SUI"), token2[:]...), + }, + }, + Statuses: make([]byte, 2), + } + + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Equal(t, expectedBatch, batch) + assert.Nil(t, err) + assert.True(t, isFinal) + }) + t.Run("returns non final batch should work", func(t *testing.T) { + from1 := testsCommon.CreateRandomSuiAddressBytes() + token1 := testsCommon.CreateRandomCoinId() + recipient1 := testsCommon.CreateRandomMultiversXAddress() + + from2 := testsCommon.CreateRandomSuiAddressBytes() + token2 := testsCommon.CreateRandomCoinId() + recipient2 := testsCommon.CreateRandomMultiversXAddress() + + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { + return core.Batch{ + Nonce: big.NewInt(98765), + BlockNumber: 0, + LastUpdatedBlockNumber: 0, + DepositsCount: 2, + }, false, nil + }, + GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { + return []core.Deposit{ + { + Nonce: big.NewInt(10), + TokenAddress: token1, + Amount: big.NewInt(20), + Depositor: from1, + Recipient: recipient1.AddressSlice(), + }, + { + Nonce: big.NewInt(30), + TokenAddress: token2, + Amount: big.NewInt(40), + Depositor: from2, + Recipient: recipient2.AddressSlice(), + }, + }, false, nil + }, + } + + bech32Recipient1Address, _ := recipient1.AddressAsBech32String() + bech32Recipient2Address, _ := recipient2.AddressAsBech32String() + expectedBatch := &core.TransferBatch{ + ID: 98765, + Deposits: []*core.DepositTransfer{ + { + Nonce: 10, + ToBytes: recipient1.AddressBytes(), + DisplayableTo: bech32Recipient1Address, + FromBytes: from1[:], + DisplayableFrom: hex.EncodeToString(from1[:]), + SourceTokenBytes: []byte(token1), + DisplayableToken: token1, + Amount: big.NewInt(20), + DestinationTokenBytes: append([]byte("SUI"), token1[:]...), + }, + { + Nonce: 30, + ToBytes: recipient2.AddressBytes(), + DisplayableTo: bech32Recipient2Address, + FromBytes: from2[:], + DisplayableFrom: hex.EncodeToString(from2[:]), + SourceTokenBytes: []byte(token2), + DisplayableToken: token2[:], + Amount: big.NewInt(40), + DestinationTokenBytes: append([]byte("SUI"), token2[:]...), + }, + }, + Statuses: make([]byte, 2), + } + + batch, isFinal, err := c.GetBatch(context.Background(), 1) + assert.Equal(t, expectedBatch, batch) + assert.Nil(t, err) + assert.False(t, isFinal) + }) +} + +func TestClient_BroadcastSignatureForMessageHash(t *testing.T) { + t.Parallel() + + t.Run("sign failed should not broadcast", func(t *testing.T) { + t.Parallel() + + expectedError := errors.New("expected error") + msgToSign := []byte("message to sign") + args := createMockSuiClientArgs() + args.Broadcaster = &testsCommon.BroadcasterStub{ + BroadcastSignatureCalled: func(signature []byte, messageHash []byte) { + assert.Fail(t, "should have not called broadcast") + }, + } + args.CryptoHandler = &bridgeTests.SuiCryptoHandlerStub{ + SignCalled: func(msg []byte) ([]byte, error) { + assert.Equal(t, msgToSign, msg) + return nil, expectedError + }, + } + + c, _ := NewSuiClient(args) + c.BroadcastSignatureForMessageHash(msgToSign) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + expectedSig := "expected sig" + broadcastCalled := false + + msgToSign := []byte("message to sign") + args := createMockSuiClientArgs() + args.Broadcaster = &testsCommon.BroadcasterStub{ + BroadcastSignatureCalled: func(signature []byte, message []byte) { + assert.Equal(t, msgToSign, message) + assert.Equal(t, expectedSig, string(signature)) + broadcastCalled = true + }, + } + args.CryptoHandler = &bridgeTests.SuiCryptoHandlerStub{ + SignCalled: func(msg []byte) ([]byte, error) { + assert.Equal(t, msgToSign, msg) + return []byte(expectedSig), nil + }, + } + + c, _ := NewSuiClient(args) + c.BroadcastSignatureForMessageHash(msgToSign) + + assert.True(t, broadcastCalled) + }) +} + +func TestClient_WasExecuted(t *testing.T) { + t.Parallel() + + wasCalled := false + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + WasBatchExecutedCalled: func(ctx context.Context, batchNonce *big.Int) (bool, error) { + wasCalled = true + return true, nil + }, + } + c, _ := NewSuiClient(args) + wasExecuted, err := c.WasExecuted(context.Background(), 1) + + assert.True(t, wasExecuted) + assert.True(t, wasCalled) + assert.Nil(t, err) +} + +func TestClient_CheckRequiredBalance(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + coinType := "0x0::coin" + balance := big.NewInt(1000000) + + t.Run("not enough coins", func(t *testing.T) { + c, _ := NewSuiClient(args) + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { + assert.Equal(t, c.safeContractId, account) + + return models.CoinBalanceResponse{ + TotalBalance: balance.String(), + }, nil + }, + } + err := c.CheckRequiredBalance(context.Background(), coinType, big.NewInt(0).Add(balance, big.NewInt(1))) + assert.True(t, errors.Is(err, errInsufficientCoinBalance)) + }) + t.Run("erc20 balance of errors", func(t *testing.T) { + expectedErr := errors.New("expected error erc20 balance of") + c, _ := NewSuiClient(args) + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { + return models.CoinBalanceResponse{}, expectedErr + }, + } + err := c.CheckRequiredBalance(context.Background(), coinType, balance) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + c, _ := NewSuiClient(args) + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { + return models.CoinBalanceResponse{ + TotalBalance: balance.String(), + }, nil + }, + } + err := c.CheckRequiredBalance(context.Background(), coinType, balance) + assert.Nil(t, err) + }) +} + +func TestClient_TotalBalances(t *testing.T) { + t.Parallel() + + t.Run("error while getting total balances", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + TotalBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return nil, expectedErr + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.TotalBalances(context.Background(), "wrong coin type") + assert.Nil(t, balances) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + providedBalance := big.NewInt(100) + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + TotalBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return providedBalance, nil + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.TotalBalances(context.Background(), "0x0::coin::Coin") + assert.Nil(t, err) + assert.Equal(t, providedBalance, balances) + }) +} + +func TestClient_MintBalances(t *testing.T) { + t.Parallel() + + t.Run("error while getting mint balances", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + MintBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return nil, expectedErr + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.MintBalances(context.Background(), "wrong coin type") + assert.Nil(t, balances) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + providedBalance := big.NewInt(100) + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + MintBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return providedBalance, nil + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.MintBalances(context.Background(), "0x0::coin::Coin") + assert.Nil(t, err) + assert.Equal(t, providedBalance, balances) + }) +} + +func TestClient_BurnBalances(t *testing.T) { + t.Parallel() + + t.Run("error while getting burn balances", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + BurnBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return nil, expectedErr + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.BurnBalances(context.Background(), "wrong coin type") + assert.Nil(t, balances) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + providedBalance := big.NewInt(100) + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + BurnBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { + return providedBalance, nil + }, + } + c, _ := NewSuiClient(args) + + balances, err := c.BurnBalances(context.Background(), "0x0::coin::Coin") + assert.Nil(t, err) + assert.Equal(t, providedBalance, balances) + }) +} + +func TestClient_MintBurnTokens(t *testing.T) { + t.Parallel() + + t.Run("error while getting mint burn tokens", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + MintBurnTokensCalled: func(ctx context.Context, token string) (bool, error) { + return false, expectedErr + }, + } + c, _ := NewSuiClient(args) + + isMintBurn, err := c.MintBurnTokens(context.Background(), "wrong coin type") + assert.False(t, isMintBurn) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + MintBurnTokensCalled: func(ctx context.Context, token string) (bool, error) { + return true, nil + }, + } + c, _ := NewSuiClient(args) + + isMintBurn, err := c.MintBurnTokens(context.Background(), "0x0::coin::Coin") + assert.Nil(t, err) + assert.True(t, isMintBurn) + }) +} + +func TestClient_NativeTokens(t *testing.T) { + t.Parallel() + + t.Run("error while getting native tokens", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + NativeTokensCalled: func(ctx context.Context, token string) (bool, error) { + return false, expectedErr + }, + } + c, _ := NewSuiClient(args) + + isNative, err := c.NativeTokens(context.Background(), "wrong coin type") + assert.False(t, isNative) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + NativeTokensCalled: func(ctx context.Context, token string) (bool, error) { + return true, nil + }, + } + c, _ := NewSuiClient(args) + + isNative, err := c.NativeTokens(context.Background(), "0x0::coin::Coin") + assert.Nil(t, err) + assert.True(t, isNative) + }) +} + +func TestClient_GetTransactionsStatuses(t *testing.T) { + t.Parallel() + + expectedStatuses := []byte{1, 2, 3} + expectedBatchID := big.NewInt(2232) + expectedErr := errors.New("expected error") + t.Run("operation error, should error", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { + assert.Equal(t, expectedBatchID, batchID) + return nil, false, expectedErr + }, + } + + c, _ := NewSuiClient(args) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Nil(t, statuses) + assert.Equal(t, expectedErr, err) + }) + t.Run("statuses are not final, should error", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { + assert.Equal(t, expectedBatchID, batchID) + return []byte("dummy"), false, nil + }, + } + + c, _ := NewSuiClient(args) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Nil(t, statuses) + assert.Equal(t, clients.ErrStatusIsNotFinal, err) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { + assert.Equal(t, expectedBatchID, batchID) + return expectedStatuses, true, nil + }, + } + + c, _ := NewSuiClient(args) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Equal(t, expectedStatuses, statuses) + assert.Nil(t, err) + }) +} + +func TestClient_GetQuorumSize(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + providedValue := big.NewInt(6453) + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return providedValue, nil + }, + } + c, _ := NewSuiClient(args) + + quorum, err := c.GetQuorumSize(context.Background()) + assert.Nil(t, err) + assert.Equal(t, providedValue, quorum) +} + +func TestClient_IsQuorumReached(t *testing.T) { + t.Parallel() + msg := []byte("message") + + t.Run("quorum errors", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return nil, expectedErr + }, + } + c, _ := NewSuiClient(args) + + isReached, err := c.IsQuorumReached(context.Background(), msg) + assert.False(t, isReached) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("quorum returns less than minimum allowed", func(t *testing.T) { + t.Parallel() + + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil + }, + } + c, _ := NewSuiClient(args) + + isReached, err := c.IsQuorumReached(context.Background(), msg) + assert.False(t, isReached) + assert.True(t, errors.Is(err, clients.ErrInvalidValue)) + assert.True(t, strings.Contains(err.Error(), "in IsQuorumReached, minQuorum")) + }) + t.Run("quorum values comparison", func(t *testing.T) { + t.Parallel() + + signatures := make([][]byte, 0) + args := createMockSuiClientArgs() + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + QuorumCalled: func(ctx context.Context) (*big.Int, error) { + return big.NewInt(3), nil + }, + } + args.SignatureHolder = &testsCommon.SignaturesHolderStub{ + SignaturesCalled: func(messageHash []byte) [][]byte { + return signatures + }, + } + c, _ := NewSuiClient(args) + + isReached, err := c.IsQuorumReached(context.Background(), msg) + assert.False(t, isReached) + assert.Nil(t, err) + + signatures = append(signatures, []byte("sig")) + signatures = append(signatures, []byte("sig")) + isReached, err = c.IsQuorumReached(context.Background(), msg) + assert.False(t, isReached) + assert.Nil(t, err) + + signatures = append(signatures, []byte("sig")) + isReached, err = c.IsQuorumReached(context.Background(), msg) + assert.True(t, isReached) + assert.Nil(t, err) + + signatures = append(signatures, []byte("sig")) + isReached, err = c.IsQuorumReached(context.Background(), msg) + assert.True(t, isReached) + assert.Nil(t, err) + }) +} + +func TestClient_CheckClientAvailability(t *testing.T) { + t.Parallel() + + currentCheckpoint := uint64(0) + incrementor := uint64(1) + args := createMockSuiClientArgs() + statusHandler := testsCommon.NewStatusHandlerMock("test") + args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ + StatusHandler: statusHandler, + GetLatestCheckpointCalled: func(ctx context.Context) (uint64, error) { + currentCheckpoint += incrementor + return currentCheckpoint, nil + }, + } + expectedErr := errors.New("expected error") + + c, _ := NewSuiClient(args) + t.Run("different current checkpoint should update - 10 times", func(t *testing.T) { + resetClient(c) + for i := 0; i < 10; i++ { + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Available, "") + } + assert.True(t, statusHandler.GetIntMetric(core.MetricLastBlockNonce) > 0) + }) + t.Run("same current checkpoint should error after a while", func(t *testing.T) { + resetClient(c) + _ = c.CheckClientAvailability(context.Background()) + + incrementor = 0 + + // place a random message as to test it is reset + statusHandler.SetStringMetric(core.MetricMultiversXClientStatus, core.ClientStatus(3).String()) + statusHandler.SetStringMetric(core.MetricLastMultiversXClientError, "random") + + // this will just increment the retry counter + for i := 0; i < int(args.ClientAvailabilityAllowDelta); i++ { + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Available, "") + } + + for i := 0; i < 10; i++ { + message := fmt.Sprintf("block %d fetched for %d times in a row", currentCheckpoint, args.ClientAvailabilityAllowDelta+uint64(i+1)) + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Unavailable, message) + } + }) + t.Run("same current checkpoint should error after a while and then recovers", func(t *testing.T) { + resetClient(c) + _ = c.CheckClientAvailability(context.Background()) + + incrementor = 0 + + // this will just increment the retry counter + for i := 0; i < int(args.ClientAvailabilityAllowDelta); i++ { + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Available, "") + } + + for i := 0; i < 10; i++ { + message := fmt.Sprintf("block %d fetched for %d times in a row", currentCheckpoint, args.ClientAvailabilityAllowDelta+uint64(i+1)) + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Unavailable, message) + } + + incrementor = 1 + err := c.CheckClientAvailability(context.Background()) + assert.Nil(t, err) + checkStatusHandler(t, statusHandler, core.Available, "") + }) + t.Run("get current checkpoint errors", func(t *testing.T) { + resetClient(c) + c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ + StatusHandler: statusHandler, + GetLatestCheckpointCalled: func(ctx context.Context) (uint64, error) { + return 0, expectedErr + }, + } + + err := c.CheckClientAvailability(context.Background()) + checkStatusHandler(t, statusHandler, core.Unavailable, expectedErr.Error()) + assert.Equal(t, expectedErr, err) + }) +} + +func resetClient(c *client) { + c.mut.Lock() + c.retriesAvailabilityCheck = 0 + c.mut.Unlock() + c.clientWrapper.SetStringMetric(core.MetricMultiversXClientStatus, "") + c.clientWrapper.SetStringMetric(core.MetricLastMultiversXClientError, "") +} + +func checkStatusHandler(t *testing.T, statusHandler *testsCommon.StatusHandlerMock, status core.ClientStatus, message string) { + assert.Equal(t, status.String(), statusHandler.GetStringMetric(core.MetricMultiversXClientStatus)) + assert.Equal(t, message, statusHandler.GetStringMetric(core.MetricLastMultiversXClientError)) +} From a2ef965d37bd8d4b1f5b927c6dd4f72cfdefb89b Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Fri, 11 Jul 2025 18:28:31 +0300 Subject: [PATCH 5/9] fixes for sui client --- clients/sui/client.go | 105 +++++++----- clients/sui/dtos/sharedOwner.go | 7 - clients/sui/errors.go | 16 +- clients/sui/interface.go | 9 +- clients/sui/suiClientDataGetter.go | 264 +++++++++++------------------ clients/sui/transactionHandler.go | 7 +- 6 files changed, 170 insertions(+), 238 deletions(-) delete mode 100644 clients/sui/dtos/sharedOwner.go diff --git a/clients/sui/client.go b/clients/sui/client.go index bdd4aab9..e8062da1 100644 --- a/clients/sui/client.go +++ b/clients/sui/client.go @@ -5,11 +5,11 @@ import ( "crypto/ed25519" "encoding/hex" "fmt" + "github.com/block-vision/sui-go-sdk/common/keypair" "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/mystenbcs" - signer "github.com/block-vision/sui-go-sdk/signer" - "github.com/block-vision/sui-go-sdk/sui" + "github.com/block-vision/sui-go-sdk/signer" "github.com/multiversx/mx-bridge-eth-go/clients" "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" @@ -28,37 +28,40 @@ const ( ) type ArgsSuiClient struct { - SuiClient sui.ISuiAPI - Log chainCore.Logger - RelayerPrivateKey ed25519.PrivateKey - SafePackageId string - SafeObjectId string - BridgePackageId string - BridgeObjectId string - RelayerCapacityId string - TokensMapper TokensMapper - StatusHandler bridgeCore.StatusHandler - Broadcaster Broadcaster - SignatureHolder SignaturesHolder + Proxy Proxy + Log chainCore.Logger + RelayerPrivateKey ed25519.PrivateKey + SafePackageId string + SafeObjectId string + SafeInitialSharedVersion uint64 + BridgePackageId string + BridgeObjectId string + BridgeInitialSharedVersion uint64 + RelayerCapacityId string + TokensMapper TokensMapper + StatusHandler bridgeCore.StatusHandler + Broadcaster Broadcaster + SignatureHolder SignaturesHolder ClientAvailabilityAllowDelta uint64 } type client struct { *suiClientDataGetter - txHandler txHandler - tokensMapper TokensMapper - relayerPublicKey ed25519.PublicKey - relayerAddress string - safePackageId string - safeObjectId string - bridgePackageId string - bridgeObjectId string - log chainCore.Logger - addressConverter bridgeCore.AddressConverter - statusHandler bridgeCore.StatusHandler - broadcaster Broadcaster - signatureHolder SignaturesHolder + txHandler txHandler + tokensMapper TokensMapper + relayerPublicKey ed25519.PublicKey + relayerAddress string + safePackageId string + safeObjectId string + bridgePackageId string + bridgeObjectId string + relayerCapacityId uint64 // TODO: I think this can be removed + log chainCore.Logger + addressConverter bridgeCore.AddressConverter + statusHandler bridgeCore.StatusHandler + broadcaster Broadcaster + signatureHolder SignaturesHolder lastCheckpoint uint64 retriesAvailabilityCheck uint64 @@ -80,11 +83,15 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { } argsSuiClientDataGetter := ArgsSuiClientDataGetter{ - SafeContractAddress: args.SafePackageId, - BridgeContractAddress: args.BridgePackageId, - RelayerAddress: relayerAddress, - Client: args.SuiClient, - Log: args.Log, + SafePackageId: args.SafePackageId, + SafeObjectId: args.SafeObjectId, + SafeInitialSharedVersion: args.SafeInitialSharedVersion, + BridgePackageId: args.BridgePackageId, + BridgeObjectId: args.BridgeObjectId, + BridgeInitialSharedVersion: args.BridgeInitialSharedVersion, + RelayerAddress: relayerAddress, + Proxy: args.Proxy, + Log: args.Log, } getter, err := NewSuiClientDataGetter(argsSuiClientDataGetter) if err != nil { @@ -98,7 +105,7 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { c := &client{ txHandler: &transactionHandler{ - client: args.SuiClient, + proxy: args.Proxy, relayerSigner: relayerSigner, }, suiClientDataGetter: getter, @@ -120,31 +127,34 @@ func NewSuiClient(args ArgsSuiClient) (*client, error) { c.log.Info("NewSuiClient") c.log.Info("NewSuiClient", "relayer address", relayerAddress, - "bridge contract address", c.bridgeContractAddress, - "safe contract address", c.safeContractAddress) + "bridge package ID", c.bridgePackageId, + "safe package ID", c.safePackageId) return c, err } func checkArgs(args ArgsSuiClient) error { - if args.SuiClient == nil { - return errNilClient + if args.Proxy == nil { + return errNilProxy } if len(args.RelayerPrivateKey) == 0 { return clients.ErrNilPrivateKey } - if args.BridgePackageId == "" { + if len(args.BridgePackageId) == 0 { return fmt.Errorf("%w for the BridgePackageId argument", errNilPackageId) } - if args.BridgeObjectId == "" { + if len(args.BridgeObjectId) == 0 { return fmt.Errorf("%w for the BridgeObjectId argument", errNilObjectId) } - if args.SafePackageId == "" { + if len(args.SafePackageId) == 0 { return fmt.Errorf("%w for the SafePackageId argument", errNilPackageId) } - if args.SafeObjectId == "" { + if len(args.SafeObjectId) == 0 { return fmt.Errorf("%w for the SafeObjectId argument", errNilObjectId) } + if len(args.RelayerCapacityId) == 0 { + return fmt.Errorf("%w for the RelayerCapacityId argument", errNilObjectId) + } if check.IfNil(args.Log) { return clients.ErrNilLogger } @@ -306,19 +316,20 @@ func (c *client) ExecuteTransfer( moveCallReq := models.MoveCallRequest{ Signer: c.relayerAddress, - PackageObjectId: c.bridgeContractAddress, + PackageObjectId: c.bridgePackageId, Module: "bridge", Function: "execute_transfer", TypeArguments: []interface{}{}, Arguments: []interface{}{ c.bridgeObjectId, c.safeObjectId, - argLists.SuiTokens, + argLists.SuiTokens, // TODO: in smart contract argLists.Recipients, argLists.Amounts, argLists.Nonces, batchId, signatures, + // TODO: clock? }, GasBudget: "100000000", // TODO } @@ -376,9 +387,9 @@ func (c *client) incrementRetriesAvailabilityCheck() { // CheckRequiredBalance will check if the safe has enough balance for the transfer func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, value *big.Int) error { - existingBalance, err := c.GetBalance(ctx, c.safeContractAddress, coinType) + existingBalance, err := c.GetBalance(ctx, c.safePackageId, coinType) // TODO: package or object id? if err != nil { - return fmt.Errorf("%w for owner %s for coin %s", err, c.safeContractAddress, coinType) + return fmt.Errorf("%w for owner %s for coin %s", err, c.safePackageId, coinType) } totalExistingBalanceStr := existingBalance.TotalBalance @@ -389,12 +400,12 @@ func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, valu } if value.Cmp(totalExistingBalance) > 0 { return fmt.Errorf("%w, existing: %s, required: %s for coin %s and owner %s", - errInsufficientCoinBalance, totalExistingBalanceStr, value.String(), coinType, c.safeContractAddress) + errInsufficientCoinBalance, totalExistingBalanceStr, value.String(), coinType, c.safePackageId) } c.log.Debug("checked coin balance", "Coin type", coinType, - "owner address", c.safeContractAddress, + "owner address", c.safePackageId, "existing balance", totalExistingBalanceStr, "needed", value.String()) diff --git a/clients/sui/dtos/sharedOwner.go b/clients/sui/dtos/sharedOwner.go deleted file mode 100644 index ddceb16c..00000000 --- a/clients/sui/dtos/sharedOwner.go +++ /dev/null @@ -1,7 +0,0 @@ -package dtos - -type SharedOwner struct { - Shared struct { - InitialSharedVersion uint64 `json:"initial_shared_version"` - } `json:"Shared"` -} diff --git a/clients/sui/errors.go b/clients/sui/errors.go index 9e9a49c5..bf93b5e6 100644 --- a/clients/sui/errors.go +++ b/clients/sui/errors.go @@ -3,13 +3,11 @@ package sui import "errors" var ( - errInsufficientCoinBalance = errors.New("insufficient coin balance") - errNilObjectId = errors.New("nil object id") - errNilPackageId = errors.New("nil package id") - errNilRelayerSigner = errors.New("nil relayer signer") - errNilClient = errors.New("nil client") - errGetCoinObjectsFailed = errors.New("get coin objects failed") - errNoGasObjects = errors.New("no gas objects found") - errInvalidCoinType = errors.New("invalid coin type format") - errNilBlockchainClient = errors.New("nil blockchain client") + errInsufficientCoinBalance = errors.New("insufficient coin balance") + errNilObjectId = errors.New("nil object id") + errNilPackageId = errors.New("nil package id") + errNilAddress = errors.New("nil address") + errNilProxy = errors.New("nil proxy") + errInvalidCoinType = errors.New("invalid coin type format") + errInvalidInitialSharedVersion = errors.New("invalid initial shared version") ) diff --git a/clients/sui/interface.go b/clients/sui/interface.go index 0b90e8c5..01317e5d 100644 --- a/clients/sui/interface.go +++ b/clients/sui/interface.go @@ -5,16 +5,13 @@ import ( "github.com/block-vision/sui-go-sdk/models" ) -// SuiClient defines the operations for a component that can interact with the Sui blockchain -type SuiClient interface { +// Proxy defines the operations for a component that can act as a proxy to interact with the Sui blockchain +type Proxy interface { SuiGetLatestCheckpointSequenceNumber(ctx context.Context) (uint64, error) - SuiGetChainIdentifier(ctx context.Context) (string, error) SuiXGetBalance(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) + SuiDevInspectTransactionBlock(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) SignAndExecuteTransactionBlock(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) - SuiDryRunTransactionBlock(ctx context.Context, req models.SuiDryRunTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) - SuiXGetCoins(ctx context.Context, req models.SuiXGetCoinsRequest) (models.PaginatedCoinsResponse, error) - IsInterfaceNil() bool } // TokensMapper can convert a token bytes from one chain to another diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go index 4125f1f8..e573f056 100644 --- a/clients/sui/suiClientDataGetter.go +++ b/clients/sui/suiClientDataGetter.go @@ -10,7 +10,6 @@ import ( "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/mystenbcs" - "github.com/block-vision/sui-go-sdk/sui" "github.com/block-vision/sui-go-sdk/transaction" "github.com/multiversx/mx-bridge-eth-go/clients" "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" @@ -20,24 +19,28 @@ import ( // ArgsSuiClientDataGetter is the arguments DTO used in the NewSuiClientDataGetter constructor type ArgsSuiClientDataGetter struct { - SafeContractAddress string - BridgeContractAddress string - RelayerAddress string - Client sui.ISuiAPI - Log chainCore.Logger + SafePackageId string + SafeObjectId string + SafeInitialSharedVersion uint64 + BridgePackageId string + BridgeObjectId string + BridgeInitialSharedVersion uint64 + RelayerAddress string + Proxy Proxy + Log chainCore.Logger } type suiClientDataGetter struct { - safeContractAddress string - safeObjectIdBytes models.SuiAddressBytes - - bridgeContractAddress string - bridgeObjectIdBytes models.SuiAddressBytes - relayerAddress string - client sui.ISuiAPI - initialSharedVersionForObject sync.Map // to cache the initial shared version for objects - log chainCore.Logger - mtx sync.Mutex + safePackageId string + safeObjectIdBytes models.SuiAddressBytes + safeInitialSharedVersion uint64 + bridgePackageId string + bridgeObjectIdBytes models.SuiAddressBytes + bridgeInitialSharedVersion uint64 + relayerAddress string + proxy Proxy + log chainCore.Logger + mtx sync.Mutex } // NewSuiClientDataGetter creates a new instance of type suiClientDataGetter @@ -45,37 +48,51 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, if check.IfNil(args.Log) { return nil, clients.ErrNilLogger } - if args.Client == nil { - return nil, errNilClient + if args.Proxy == nil { + return nil, errNilProxy } - if args.RelayerAddress == "" { - return nil, fmt.Errorf("%w for the signer address argument", errEmptyAddress) + if len(args.RelayerAddress) == 0 { + return nil, fmt.Errorf("%w for the signer address argument", errNilAddress) } - if args.BridgeContractAddress == "" { - return nil, fmt.Errorf("%w for the BridgePackageId argument", errEmptyAddress) + if len(args.BridgePackageId) == 0 { + return nil, fmt.Errorf("%w for the BridgePackageId argument", errNilPackageId) } - if args.SafeContractAddress == "" { - return nil, fmt.Errorf("%w for the SafePackageId argument", errEmptyAddress) + if len(args.BridgeObjectId) == 0 { + return nil, fmt.Errorf("%w for the BridgeObjectId argument", errNilObjectId) + } + if args.BridgeInitialSharedVersion < 0 { + return nil, errInvalidInitialSharedVersion + } + if len(args.SafePackageId) == 0 { + return nil, fmt.Errorf("%w for the SafePackageId argument", errNilPackageId) + } + if len(args.SafeObjectId) == 0 { + return nil, fmt.Errorf("%w for the SafeObjectId argument", errNilObjectId) + } + if args.SafeInitialSharedVersion < 0 { + return nil, errInvalidInitialSharedVersion } - safeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.SafeContractAddress)) + bridgeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.BridgeObjectId)) if err != nil { return nil, fmt.Errorf("failed to convert address: %w", err) } - bridgeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.BridgeContractAddress)) + safeObjectIdBytes, err := transaction.ConvertSuiAddressStringToBytes(models.SuiAddress(args.SafeObjectId)) if err != nil { return nil, fmt.Errorf("failed to convert address: %w", err) } return &suiClientDataGetter{ - safeContractAddress: args.SafeContractAddress, - safeObjectIdBytes: *safeObjectIdBytes, - bridgeContractAddress: args.BridgeContractAddress, - bridgeObjectIdBytes: *bridgeObjectIdBytes, - relayerAddress: args.RelayerAddress, - client: args.Client, - log: args.Log, + safePackageId: args.SafePackageId, + safeObjectIdBytes: *safeObjectIdBytes, + safeInitialSharedVersion: args.SafeInitialSharedVersion, + bridgePackageId: args.BridgePackageId, + bridgeObjectIdBytes: *bridgeObjectIdBytes, + bridgeInitialSharedVersion: args.BridgeInitialSharedVersion, + relayerAddress: args.RelayerAddress, + proxy: args.Proxy, + log: args.Log, }, nil } @@ -83,13 +100,8 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNonce uint64) (dtos.Batch, bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return dtos.Batch{}, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_batch", nil, @@ -98,14 +110,15 @@ func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNon transaction.CallArg{ Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ - ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + ObjectId: getter.safeObjectIdBytes, + InitialSharedVersion: getter.safeInitialSharedVersion, Mutable: true, }, }, }, ), tx.Pure(batchNonce), + // TODO: clock? }, ) @@ -120,7 +133,7 @@ func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNon var batch dtos.Batch var isFinalBatch bool - err = DecodeReturnValues(txBlockResp.Results, &batch, &isFinalBatch) + err = getter.decodeReturnValues(txBlockResp.Results, &batch, &isFinalBatch) if err != nil { return dtos.Batch{}, false, fmt.Errorf("failed to decode return value: %w", err) } @@ -132,13 +145,8 @@ func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNon func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNonce uint64) ([]dtos.Deposit, bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return nil, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_batch_deposits", nil, @@ -147,14 +155,15 @@ func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNo transaction.CallArg{ Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ - ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + ObjectId: getter.safeObjectIdBytes, + InitialSharedVersion: getter.safeInitialSharedVersion, Mutable: true, }, }, }, ), tx.Pure(batchNonce), + // TODO: clock? }, ) @@ -169,7 +178,7 @@ func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNo var depositsList []dtos.Deposit var areFinalDeposits bool - err = DecodeReturnValues(txBlockResp.Results, &depositsList, &areFinalDeposits) + err = getter.decodeReturnValues(txBlockResp.Results, &depositsList, &areFinalDeposits) if err != nil { return nil, false, fmt.Errorf("failed to decode return value: %w", err) } @@ -181,13 +190,8 @@ func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNo func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.SuiAddress, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return []models.SuiAddress{}, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_relayers", nil, @@ -197,7 +201,7 @@ func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.Su Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.bridgeInitialSharedVersion, Mutable: true, }, }, @@ -216,7 +220,7 @@ func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.Su } var relayersAddresses []models.SuiAddress - err = DecodeReturnValues(txBlockResp.Results, &relayersAddresses) + err = getter.decodeReturnValues(txBlockResp.Results, &relayersAddresses) if err != nil { return []models.SuiAddress{}, fmt.Errorf("failed to decode return value: %w", err) } @@ -228,15 +232,10 @@ func (getter *suiClientDataGetter) GetRelayers(ctx context.Context) ([]models.Su func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNonce uint64) (bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", - "get_relayers", + "was_batch_executed", nil, []transaction.Argument{ tx.Object( @@ -244,7 +243,7 @@ func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNo Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.bridgeInitialSharedVersion, Mutable: true, }, }, @@ -264,7 +263,7 @@ func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNo } var wasBatchExecuted bool - err = DecodeReturnValues(txBlockResp.Results, &wasBatchExecuted) + err = getter.decodeReturnValues(txBlockResp.Results, &wasBatchExecuted) if err != nil { return false, fmt.Errorf("failed to decode return value: %w", err) } @@ -276,13 +275,8 @@ func (getter *suiClientDataGetter) WasBatchExecuted(ctx context.Context, batchNo func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.safeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_pause", nil, @@ -292,7 +286,7 @@ func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.bridgeInitialSharedVersion, Mutable: true, }, }, @@ -311,7 +305,7 @@ func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { } var isPaused bool - err = DecodeReturnValues(txBlockResp.Results, &isPaused) + err = getter.decodeReturnValues(txBlockResp.Results, &isPaused) if err != nil { return false, fmt.Errorf("failed to decode return value: %w", err) } @@ -323,13 +317,8 @@ func (getter *suiClientDataGetter) IsPaused(ctx context.Context) (bool, error) { func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_quorum", nil, @@ -339,7 +328,7 @@ func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.bridgeInitialSharedVersion, Mutable: true, }, }, @@ -358,7 +347,7 @@ func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { } var quorum uint64 - err = DecodeReturnValues(txBlockResp.Results, &quorum) + err = getter.decodeReturnValues(txBlockResp.Results, &quorum) if err != nil { return 0, fmt.Errorf("failed to decode return value: %w", err) } @@ -370,13 +359,8 @@ func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) ([]dtos.DepositStatus, bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.bridgeContractAddress) - if err != nil { - return []dtos.DepositStatus{}, false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.bridgeContractAddress, err) - } - tx.MoveCall( - models.SuiAddress(getter.bridgeContractAddress), + models.SuiAddress(getter.bridgePackageId), "bridge", "get_statuses_after_execution", nil, @@ -386,13 +370,14 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.bridgeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.bridgeInitialSharedVersion, Mutable: true, }, }, }, ), tx.Pure(batchNonce), + // TODO: clock? }, ) @@ -407,7 +392,7 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context var depositStatuses []dtos.DepositStatus var isFinal bool - err = DecodeReturnValues(txBlockResp.Results, &depositStatuses, &isFinal) + err = getter.decodeReturnValues(txBlockResp.Results, &depositStatuses, &isFinal) if err != nil { return []dtos.DepositStatus{}, false, fmt.Errorf("failed to decode return value: %w", err) } @@ -419,12 +404,7 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, coinType string) (uint64, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) - if err != nil { - return 0, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) - } - - coinParts, err := ParseCoinType(coinType) + coinParts, err := parseCoinType(coinType) if err != nil { return 0, fmt.Errorf("failed to parse coin type %s: %w", coinType, err) } @@ -435,7 +415,7 @@ func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, } tx.MoveCall( - models.SuiAddress(getter.safeContractAddress), + models.SuiAddress(getter.safePackageId), "safe", "get_stored_coin_balance", []transaction.TypeTag{ @@ -453,7 +433,7 @@ func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.safeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.safeInitialSharedVersion, Mutable: true, }, }, @@ -472,7 +452,7 @@ func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, } var totalBalance uint64 - err = DecodeReturnValues(txBlockResp.Results, &totalBalance) + err = getter.decodeReturnValues(txBlockResp.Results, &totalBalance) if err != nil { return 0, fmt.Errorf("failed to decode return value: %w", err) } @@ -484,12 +464,7 @@ func (getter *suiClientDataGetter) GetTotalBalanceFromSafe(ctx context.Context, func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinType string) (bool, error) { tx := transaction.NewTransaction() - initialSharedVersion, err := getter.getInitialSharedVersionForObject(ctx, getter.safeContractAddress) - if err != nil { - return false, fmt.Errorf("failed to get initial shared version for object %s: %w", getter.safeContractAddress, err) - } - - coinParts, err := ParseCoinType(coinType) + coinParts, err := parseCoinType(coinType) if err != nil { return false, fmt.Errorf("failed to parse coin type %s: %w", coinType, err) } @@ -500,7 +475,7 @@ func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinT } tx.MoveCall( - models.SuiAddress(getter.safeContractAddress), + models.SuiAddress(getter.safePackageId), "safe", "is_token_whitelisted", []transaction.TypeTag{ @@ -518,7 +493,7 @@ func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinT Object: &transaction.ObjectArg{ SharedObject: &transaction.SharedObjectRef{ ObjectId: getter.safeObjectIdBytes, - InitialSharedVersion: initialSharedVersion, + InitialSharedVersion: getter.safeInitialSharedVersion, Mutable: true, }, }, @@ -537,7 +512,7 @@ func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinT } var isTokenWhitelisted bool - err = DecodeReturnValues(txBlockResp.Results, &isTokenWhitelisted) + err = getter.decodeReturnValues(txBlockResp.Results, &isTokenWhitelisted) if err != nil { return false, fmt.Errorf("failed to decode return value: %w", err) } @@ -547,39 +522,18 @@ func (getter *suiClientDataGetter) IsTokenWhitelisted(ctx context.Context, coinT // GetLatestCheckpoint returns the latest checkpoint sequence number func (getter *suiClientDataGetter) GetLatestCheckpoint(ctx context.Context) (uint64, error) { - return getter.client.SuiGetLatestCheckpointSequenceNumber(ctx) + return getter.proxy.SuiGetLatestCheckpointSequenceNumber(ctx) } // GetBalance returns the sui balance of the given account func (getter *suiClientDataGetter) GetBalance(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { - return getter.client.SuiXGetBalance(ctx, models.SuiXGetBalanceRequest{ + return getter.proxy.SuiXGetBalance(ctx, models.SuiXGetBalanceRequest{ Owner: account, CoinType: coinType, }) } -// ParseCoinType parses a coin type string in the format "package::module::struct" and returns its components. -func ParseCoinType(coinType string) ([3]string, error) { - parts := strings.Split(coinType, "::") - - if len(parts) != 3 { - return [3]string{}, errInvalidCoinType - } - - packageAddr := strings.TrimSpace(parts[0]) - module := strings.TrimSpace(parts[1]) - structName := strings.TrimSpace(parts[2]) - - // Basic validation - if packageAddr == "" || module == "" || structName == "" { - return [3]string{}, errors.New("coin type parts cannot be empty") - } - - return [3]string{packageAddr, module, structName}, nil -} - -// DecodeReturnValues decodes the return value from a transaction block response. -func DecodeReturnValues(data json.RawMessage, out ...interface{}) error { +func (getter *suiClientDataGetter) decodeReturnValues(data json.RawMessage, out ...interface{}) error { var results []dtos.InspectResult if err := json.Unmarshal(data, &results); err != nil { return fmt.Errorf("decode dev inspect results: %w", err) @@ -619,46 +573,26 @@ func (getter *suiClientDataGetter) sendTxGetBlockResponse(ctx context.Context, t return models.SuiTransactionBlockResponse{}, err } - return getter.client.SuiDevInspectTransactionBlock(ctx, models.SuiDevInspectTransactionBlockRequest{ + return getter.proxy.SuiDevInspectTransactionBlock(ctx, models.SuiDevInspectTransactionBlockRequest{ Sender: getter.relayerAddress, TxBytes: txBytes, }) } -func (getter *suiClientDataGetter) getInitialSharedVersionForObject(ctx context.Context, objectId string) (uint64, error) { - if value, exists := getter.initialSharedVersionForObject.Load(objectId); exists { - return value.(uint64), nil - } - - // If not found, make the network call - rsp, err := getter.client.SuiGetObject(ctx, models.SuiGetObjectRequest{ - ObjectId: objectId, - Options: models.SuiObjectDataOptions{ - ShowOwner: true, - }, - }) - - if err != nil { - return 0, fmt.Errorf("failed to get object %s: %w", objectId, err) - } - - data := rsp.Data - ownerBytes, err := json.Marshal(data.Owner) - if err != nil { - return 0, fmt.Errorf("failed to marshal owner: %w", err) - } +func parseCoinType(coinType string) ([3]string, error) { + parts := strings.Split(coinType, "::") - var sharedOwner dtos.SharedOwner - if err = json.Unmarshal(ownerBytes, &sharedOwner); err != nil { - return 0, fmt.Errorf("failed to unmarshal to SharedOwner: %w", err) + if len(parts) != 3 { + return [3]string{}, errInvalidCoinType } - initialSharedVersion := sharedOwner.Shared.InitialSharedVersion + packageAddr := strings.TrimSpace(parts[0]) + module := strings.TrimSpace(parts[1]) + structName := strings.TrimSpace(parts[2]) - if actualValue, loaded := getter.initialSharedVersionForObject.LoadOrStore(objectId, initialSharedVersion); loaded { - // if another goroutine stored it first, return that value - return actualValue.(uint64), nil + if packageAddr == "" || module == "" || structName == "" { + return [3]string{}, errors.New("coin type parts cannot be empty") } - return initialSharedVersion, nil + return [3]string{packageAddr, module, structName}, nil } diff --git a/clients/sui/transactionHandler.go b/clients/sui/transactionHandler.go index 24955cf5..02bcc0cc 100644 --- a/clients/sui/transactionHandler.go +++ b/clients/sui/transactionHandler.go @@ -6,23 +6,22 @@ import ( "errors" "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/signer" - "github.com/block-vision/sui-go-sdk/sui" ) type transactionHandler struct { - client sui.ISuiAPI + proxy Proxy relayerSigner *signer.Signer } func (txHandler *transactionHandler) SendTransactionReturnHash(ctx context.Context, moveCallRequest models.MoveCallRequest) (string, error) { moveCallRequest.Signer = txHandler.relayerSigner.Address - txnMetaData, err := txHandler.client.MoveCall(ctx, moveCallRequest) + txnMetaData, err := txHandler.proxy.MoveCall(ctx, moveCallRequest) if err != nil { return "", err } - txBlockResponse, err := txHandler.client.SignAndExecuteTransactionBlock(ctx, models.SignAndExecuteTransactionBlockRequest{ + txBlockResponse, err := txHandler.proxy.SignAndExecuteTransactionBlock(ctx, models.SignAndExecuteTransactionBlockRequest{ TxnMetaData: txnMetaData, PriKey: txHandler.relayerSigner.PriKey, Options: models.SuiTransactionBlockOptions{ From 162f44fca48b74729e4495d7bbcdd5ca3c26b3b0 Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Mon, 14 Jul 2025 12:49:13 +0300 Subject: [PATCH 6/9] removed unused stuff --- clients/sui/client.go | 38 ++++++++++++----------------- clients/sui/dtos/contractStructs.go | 2 +- clients/sui/suiClientDataGetter.go | 3 --- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/clients/sui/client.go b/clients/sui/client.go index e8062da1..fe791612 100644 --- a/clients/sui/client.go +++ b/clients/sui/client.go @@ -5,6 +5,8 @@ import ( "crypto/ed25519" "encoding/hex" "fmt" + "math/big" + "sync" "github.com/block-vision/sui-go-sdk/common/keypair" "github.com/block-vision/sui-go-sdk/models" @@ -18,8 +20,6 @@ import ( chainCore "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" "golang.org/x/crypto/blake2b" - "math/big" - "sync" ) const ( @@ -37,7 +37,6 @@ type ArgsSuiClient struct { BridgePackageId string BridgeObjectId string BridgeInitialSharedVersion uint64 - RelayerCapacityId string TokensMapper TokensMapper StatusHandler bridgeCore.StatusHandler Broadcaster Broadcaster @@ -48,20 +47,19 @@ type ArgsSuiClient struct { type client struct { *suiClientDataGetter - txHandler txHandler - tokensMapper TokensMapper - relayerPublicKey ed25519.PublicKey - relayerAddress string - safePackageId string - safeObjectId string - bridgePackageId string - bridgeObjectId string - relayerCapacityId uint64 // TODO: I think this can be removed - log chainCore.Logger - addressConverter bridgeCore.AddressConverter - statusHandler bridgeCore.StatusHandler - broadcaster Broadcaster - signatureHolder SignaturesHolder + txHandler txHandler + tokensMapper TokensMapper + relayerPublicKey ed25519.PublicKey + relayerAddress string + safePackageId string + safeObjectId string + bridgePackageId string + bridgeObjectId string + log chainCore.Logger + addressConverter bridgeCore.AddressConverter + statusHandler bridgeCore.StatusHandler + broadcaster Broadcaster + signatureHolder SignaturesHolder lastCheckpoint uint64 retriesAvailabilityCheck uint64 @@ -152,9 +150,6 @@ func checkArgs(args ArgsSuiClient) error { if len(args.SafeObjectId) == 0 { return fmt.Errorf("%w for the SafeObjectId argument", errNilObjectId) } - if len(args.RelayerCapacityId) == 0 { - return fmt.Errorf("%w for the RelayerCapacityId argument", errNilObjectId) - } if check.IfNil(args.Log) { return clients.ErrNilLogger } @@ -329,7 +324,6 @@ func (c *client) ExecuteTransfer( argLists.Nonces, batchId, signatures, - // TODO: clock? }, GasBudget: "100000000", // TODO } @@ -387,7 +381,7 @@ func (c *client) incrementRetriesAvailabilityCheck() { // CheckRequiredBalance will check if the safe has enough balance for the transfer func (c *client) CheckRequiredBalance(ctx context.Context, coinType string, value *big.Int) error { - existingBalance, err := c.GetBalance(ctx, c.safePackageId, coinType) // TODO: package or object id? + existingBalance, err := c.GetBalance(ctx, c.safeObjectId, coinType) if err != nil { return fmt.Errorf("%w for owner %s for coin %s", err, c.safePackageId, coinType) } diff --git a/clients/sui/dtos/contractStructs.go b/clients/sui/dtos/contractStructs.go index c413e14b..9b55d0b7 100644 --- a/clients/sui/dtos/contractStructs.go +++ b/clients/sui/dtos/contractStructs.go @@ -6,7 +6,7 @@ import "math/big" type Batch struct { Nonce uint64 BlockNumber uint64 - LastUpdatedBlockNumber uint64 + LastUpdatedTimestampMs uint64 DepositsCount uint16 } diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go index e573f056..8e33f409 100644 --- a/clients/sui/suiClientDataGetter.go +++ b/clients/sui/suiClientDataGetter.go @@ -118,7 +118,6 @@ func (getter *suiClientDataGetter) GetBatchByNonce(ctx context.Context, batchNon }, ), tx.Pure(batchNonce), - // TODO: clock? }, ) @@ -163,7 +162,6 @@ func (getter *suiClientDataGetter) GetBatchDeposits(ctx context.Context, batchNo }, ), tx.Pure(batchNonce), - // TODO: clock? }, ) @@ -377,7 +375,6 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context }, ), tx.Pure(batchNonce), - // TODO: clock? }, ) From 56837af4f6aea85b22d7f37429eb17220151e701 Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Thu, 17 Jul 2025 16:30:56 +0300 Subject: [PATCH 7/9] add tests for client and its components --- clients/sui/client.go | 16 +- clients/sui/client_test.go | 838 +++++++++++++----------- clients/sui/dtos/contractStructs.go | 21 +- clients/sui/suiClientDataGetter.go | 16 +- clients/sui/suiClientDataGetter_test.go | 776 ++++++++++++++++++++++ clients/sui/testdata/nok-sui-seed | 1 - clients/sui/testdata/ok-sui-seed | 2 - clients/sui/transactionHandler_test.go | 157 +++++ go.mod | 2 + go.sum | 4 + testsCommon/bridge/suiTxHandlerStub.go | 30 + testsCommon/interactors/suiProxyStub.go | 57 ++ 12 files changed, 1483 insertions(+), 437 deletions(-) create mode 100644 clients/sui/suiClientDataGetter_test.go delete mode 100644 clients/sui/testdata/nok-sui-seed delete mode 100644 clients/sui/testdata/ok-sui-seed create mode 100644 clients/sui/transactionHandler_test.go create mode 100644 testsCommon/bridge/suiTxHandlerStub.go create mode 100644 testsCommon/interactors/suiProxyStub.go diff --git a/clients/sui/client.go b/clients/sui/client.go index fe791612..bff3837d 100644 --- a/clients/sui/client.go +++ b/clients/sui/client.go @@ -13,7 +13,6 @@ import ( "github.com/block-vision/sui-go-sdk/mystenbcs" "github.com/block-vision/sui-go-sdk/signer" "github.com/multiversx/mx-bridge-eth-go/clients" - "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" "github.com/multiversx/mx-bridge-eth-go/core/converters" @@ -202,7 +201,7 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf transferBatch := &bridgeCore.TransferBatch{ ID: batch.Nonce, - BlockNumber: batch.BlockNumber, + BlockNumber: batch.TimestampMs, Deposits: make([]*bridgeCore.DepositTransfer, 0, batch.DepositsCount), } cachedTokens := make(map[string][]byte) @@ -220,7 +219,7 @@ func (c *client) GetBatch(ctx context.Context, nonce uint64) (*bridgeCore.Transf DisplayableFrom: c.addressConverter.ToHexString(fromBytes), SourceTokenBytes: []byte(tokenId), DisplayableToken: tokenId, - Amount: big.NewInt(0).Set(deposit.Amount), + Amount: big.NewInt(0).SetUint64(deposit.Amount), } storedConvertedTokenBytes, exists := cachedTokens[depositTransfer.DisplayableToken] if !exists { @@ -318,7 +317,7 @@ func (c *client) ExecuteTransfer( Arguments: []interface{}{ c.bridgeObjectId, c.safeObjectId, - argLists.SuiTokens, // TODO: in smart contract + argLists.SuiTokens, argLists.Recipients, argLists.Amounts, argLists.Nonces, @@ -415,18 +414,13 @@ func (c *client) TotalBalances(ctx context.Context, token string) (*big.Int, err return big.NewInt(0).SetUint64(balance), nil } -//// NativeTokens returns true if the token is native -//func (c *client) NativeTokens(ctx context.Context, token string) (bool, error) { -// return c.NativeTokens(ctx, token) -//} - // WhitelistedTokens returns true if the token is whitelisted func (c *client) WhitelistedTokens(ctx context.Context, token string) (bool, error) { return c.IsTokenWhitelisted(ctx, token) } // GetTransactionsStatuses will return the transactions statuses from the batch -func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]dtos.DepositStatus, error) { +func (c *client) GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]byte, error) { buff, isFinal, err := c.GetStatusesAfterExecution(ctx, batchId) if err != nil { return nil, err @@ -455,7 +449,7 @@ func (c *client) IsQuorumReached(ctx context.Context, msg []byte) (bool, error) return false, fmt.Errorf("%w in IsQuorumReached, Quorum call", err) } if quorum < minQuorumValue { - return false, fmt.Errorf("%w in IsQuorumReached, minQuorum %d, got: %s", clients.ErrInvalidValue, minQuorumValue, quorum) + return false, fmt.Errorf("%w in IsQuorumReached, minQuorum %d, got: %d", clients.ErrInvalidValue, minQuorumValue, quorum) } return len(signatures) >= int(quorum), nil diff --git a/clients/sui/client_test.go b/clients/sui/client_test.go index 1170972a..f1b4b814 100644 --- a/clients/sui/client_test.go +++ b/clients/sui/client_test.go @@ -1,6 +1,7 @@ package sui import ( + "bytes" "context" "encoding/hex" "errors" @@ -10,52 +11,86 @@ import ( "testing" "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/mystenbcs" + "github.com/block-vision/sui-go-sdk/signer" "github.com/multiversx/mx-bridge-eth-go/clients" + "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" "github.com/multiversx/mx-bridge-eth-go/core" - "github.com/multiversx/mx-bridge-eth-go/core/converters" + "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" "github.com/multiversx/mx-bridge-eth-go/testsCommon" bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" + "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" "github.com/multiversx/mx-chain-core-go/core/check" logger "github.com/multiversx/mx-chain-logger-go" "github.com/stretchr/testify/assert" + "golang.org/x/crypto/blake2b" ) func createMockSuiClientArgs() ArgsSuiClient { - addressConverter, err := converters.NewAddressConverter() - if err != nil { - panic(err) - } + relayer := signer.NewSigner(seedBytes) return ArgsSuiClient{ - ClientWrapper: &bridgeTests.SuiClientWrapperStub{}, - Log: logger.GetOrCreate("test"), - AddressConverter: addressConverter, - Broadcaster: &testsCommon.BroadcasterStub{}, + Proxy: &interactors.SuiProxyStub{}, + Log: logger.GetOrCreate("test"), + RelayerPrivateKey: relayer.PriKey, + SafePackageId: "0x674a8fc0a6b48c8efea86ad7ed962107c5c132a78e7cc79c9c5b9391ba8b6d83", + SafeObjectId: "0x32c8ebf5853163472964ce226b194af05aa5f2e4cc47678924ebe538a1c88416", + SafeInitialSharedVersion: 425322, + BridgePackageId: "0x19ecc8df0b93999f87b5e2e531bb80a49d1ab2f4644032cd31f2c53ed624c94a", + BridgeObjectId: "0xe15513cc93d6efbfbdc7844df141b312bb677ee564a5838b7b22a891f9f05c65", + BridgeInitialSharedVersion: 982471, TokensMapper: &bridgeTests.TokensMapperStub{ ConvertTokenCalled: func(ctx context.Context, sourceBytes []byte) ([]byte, error) { return append([]byte("SUI"), sourceBytes...), nil }, }, - CryptoHandler: &bridgeTests.SuiCryptoHandlerStub{}, + Broadcaster: &testsCommon.BroadcasterStub{}, + StatusHandler: &testsCommon.StatusHandlerStub{}, SignatureHolder: &testsCommon.SignaturesHolderStub{}, - SafeContractId: testsCommon.CreateRandomSuiAddress(), - TransferGasLimitBase: 50, - TransferGasLimitForEach: 20, ClientAvailabilityAllowDelta: 5, - EventsBlockRangeFrom: -100, - EventsBlockRangeTo: 400, + } +} + +func createMockTransferBatch() *core.TransferBatch { + return &core.TransferBatch{ + ID: 332, + Deposits: []*core.DepositTransfer{ + { + Nonce: 10, + ToBytes: []byte("to1"), + DisplayableTo: "to1", + FromBytes: []byte("from1"), + DisplayableFrom: "from1", + SourceTokenBytes: []byte("source token1"), + DisplayableToken: "token1", + Amount: big.NewInt(20), + DestinationTokenBytes: []byte("SUItoken1"), + }, + { + Nonce: 30, + ToBytes: []byte("to2"), + DisplayableTo: "to2", + FromBytes: []byte("from2"), + DisplayableFrom: "from2", + SourceTokenBytes: []byte("source token2"), + DisplayableToken: "token2", + Amount: big.NewInt(40), + DestinationTokenBytes: []byte("SUItoken2"), + }, + }, + Statuses: make([]byte, 2), } } func TestNewSuiClient(t *testing.T) { t.Parallel() - t.Run("nil client wrapper", func(t *testing.T) { + t.Run("nil proxy", func(t *testing.T) { args := createMockSuiClientArgs() - args.ClientWrapper = nil + args.Proxy = nil c, err := NewSuiClient(args) - assert.Equal(t, clients.ErrNilClientWrapper, err) + assert.Equal(t, errNilProxy, err) assert.True(t, check.IfNil(c)) }) t.Run("nil logger", func(t *testing.T) { @@ -66,12 +101,12 @@ func TestNewSuiClient(t *testing.T) { assert.Equal(t, clients.ErrNilLogger, err) assert.True(t, check.IfNil(c)) }) - t.Run("nil address converter", func(t *testing.T) { + t.Run("nil tokens mapper", func(t *testing.T) { args := createMockSuiClientArgs() - args.AddressConverter = nil + args.TokensMapper = nil c, err := NewSuiClient(args) - assert.Equal(t, clients.ErrNilAddressConverter, err) + assert.Equal(t, clients.ErrNilTokensMapper, err) assert.True(t, check.IfNil(c)) }) t.Run("nil broadcaster", func(t *testing.T) { @@ -82,12 +117,12 @@ func TestNewSuiClient(t *testing.T) { assert.Equal(t, clients.ErrNilBroadcaster, err) assert.True(t, check.IfNil(c)) }) - t.Run("nil crypto handler", func(t *testing.T) { + t.Run("nil status handler", func(t *testing.T) { args := createMockSuiClientArgs() - args.CryptoHandler = nil + args.StatusHandler = nil c, err := NewSuiClient(args) - assert.Equal(t, clients.ErrNilCryptoHandler, err) + assert.Equal(t, clients.ErrNilStatusHandler, err) assert.True(t, check.IfNil(c)) }) t.Run("nil tokens mapper", func(t *testing.T) { @@ -106,22 +141,6 @@ func TestNewSuiClient(t *testing.T) { assert.Equal(t, clients.ErrNilSignaturesHolder, err) assert.True(t, check.IfNil(c)) }) - t.Run("0 transfer gas limit base", func(t *testing.T) { - args := createMockSuiClientArgs() - args.TransferGasLimitBase = 0 - c, err := NewSuiClient(args) - - assert.Equal(t, clients.ErrInvalidGasLimit, err) - assert.True(t, check.IfNil(c)) - }) - t.Run("0 transfer gas limit for each", func(t *testing.T) { - args := createMockSuiClientArgs() - args.TransferGasLimitForEach = 0 - c, err := NewSuiClient(args) - - assert.Equal(t, clients.ErrInvalidGasLimit, err) - assert.True(t, check.IfNil(c)) - }) t.Run("invalid ClientAvailabilityAllowDelta should error", func(t *testing.T) { t.Parallel() @@ -134,28 +153,6 @@ func TestNewSuiClient(t *testing.T) { assert.True(t, errors.Is(err, clients.ErrInvalidValue)) assert.True(t, strings.Contains(err.Error(), "for args.AllowedDelta")) }) - t.Run("invalid events block range from should error", func(t *testing.T) { - t.Parallel() - - args := createMockSuiClientArgs() - args.EventsBlockRangeFrom = 100 - args.EventsBlockRangeTo = 50 - - c, err := NewSuiClient(args) - - assert.True(t, check.IfNil(c)) - assert.True(t, errors.Is(err, clients.ErrInvalidValue)) - assert.True(t, strings.Contains(err.Error(), "args.EventsBlockRangeFrom")) - assert.True(t, strings.Contains(err.Error(), "args.EventsBlockRangeTo")) - }) - t.Run("nil crypto handler", func(t *testing.T) { - args := createMockSuiClientArgs() - args.CryptoHandler = nil - c, err := NewSuiClient(args) - - assert.Equal(t, clients.ErrNilCryptoHandler, err) - assert.True(t, check.IfNil(c)) - }) t.Run("should work", func(t *testing.T) { args := createMockSuiClientArgs() c, err := NewSuiClient(args) @@ -167,76 +164,176 @@ func TestNewSuiClient(t *testing.T) { func TestClient_GetBatch(t *testing.T) { t.Parallel() - - args := createMockSuiClientArgs() - c, _ := NewSuiClient(args) expectedErr := errors.New("expected error") - t.Run("error while getting batch", func(t *testing.T) { - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{}, false, expectedErr + t.Run("get batch failed should error", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + return models.SuiTransactionBlockResponse{}, expectedErr }, } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Nil(t, batch) - assert.Equal(t, expectedErr, err) assert.False(t, isFinal) + assert.ErrorIs(t, err, expectedErr) }) - t.Run("error while getting deposits", func(t *testing.T) { - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{ - Nonce: batchNonce, - DepositsCount: 3, - }, true, nil - }, - GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { - return nil, false, expectedErr + t.Run("get batch deposits failed should error", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + + getBatchCalled := false + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + // Simulate a call to get batch + if getBatchCalled == false { + getBatchCalled = true + + res, _ := createResultsRawFromValues(dtos.Batch{ + Nonce: batchNonce, + DepositsCount: 2, + TimestampMs: 100, + }, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil + } + + // Simulate a call to get batch deposits + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "failed", + }, + }, + }, expectedErr }, } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Nil(t, batch) - assert.Equal(t, expectedErr, err) assert.False(t, isFinal) + assert.ErrorIs(t, err, expectedErr) }) - t.Run("deposits mismatch - with 0", func(t *testing.T) { - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{ - Nonce: batchNonce, - DepositsCount: 3, - }, true, nil - }, - GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { - return make([]core.Deposit, 0), true, nil + t.Run("deposits count mismatch should error", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + + getBatchCalled := false + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + // Simulate a call to get batch + if getBatchCalled == false { + getBatchCalled = true + + res, _ := createResultsRawFromValues(dtos.Batch{ + Nonce: batchNonce, + DepositsCount: 2, + TimestampMs: 100, + }, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil + } + + // Simulate a call to get batch deposits + res, _ := createResultsRawFromValues(make([]dtos.Deposit, 4), true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil }, } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Nil(t, batch) - assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) - assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 3, fetched deposits len: 0")) assert.False(t, isFinal) + assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) }) - t.Run("deposits mismatch - with non zero value", func(t *testing.T) { - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{ - Nonce: batchNonce, - DepositsCount: 3, - }, true, nil + t.Run("token conversion failed should error", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + + wasGetBatchCalled := false + args.TokensMapper = &bridgeTests.TokensMapperStub{ + ConvertTokenCalled: func(ctx context.Context, sourceTokenBytes []byte) ([]byte, error) { + return nil, expectedErr }, - GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { - return make([]core.Deposit, 4), true, nil + } + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + // First call for get_batch + if wasGetBatchCalled == false { + wasGetBatchCalled = true + + res, _ := createResultsRawFromValues(dtos.Batch{ + Nonce: batchNonce, + DepositsCount: 1, + TimestampMs: 100, + }, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil + } + + // Second call for get_batch_deposits + deposits := []dtos.Deposit{ + { + Nonce: 42, + TokenAddress: "coin::Coin::0x1", + Amount: 1000, + Depositor: bytes.Repeat([]byte{0x1}, 32), + Recipient: bytes.Repeat([]byte{0x2}, 32), + Status: uint8(1), + }, + } + res, _ := createResultsRawFromValues(deposits, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil }, } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Nil(t, batch) - assert.True(t, errors.Is(err, clients.ErrDepositsAndBatchDepositsCountDiffer)) - assert.True(t, strings.Contains(err.Error(), "batch.DepositsCount: 3, fetched deposits len: 4")) assert.False(t, isFinal) + assert.Equal(t, expectedErr, err) }) t.Run("returns batch should work", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + from1 := testsCommon.CreateRandomSuiAddressBytes() token1 := testsCommon.CreateRandomCoinId() recipient1 := testsCommon.CreateRandomMultiversXAddress() @@ -245,42 +342,69 @@ func TestClient_GetBatch(t *testing.T) { token2 := testsCommon.CreateRandomCoinId() recipient2 := testsCommon.CreateRandomMultiversXAddress() - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{ - Nonce: big.NewInt(537), - BlockNumber: 0, - LastUpdatedBlockNumber: 0, - DepositsCount: 2, - }, true, nil - }, - GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { - return []core.Deposit{ + wasGetBatchCalled := false + + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + // First call for get_batch + if wasGetBatchCalled == false { + wasGetBatchCalled = true + + res, _ := createResultsRawFromValues(dtos.Batch{ + Nonce: batchNonce, + DepositsCount: 2, + LastUpdatedTimestampMs: 100, + TimestampMs: 100, + }, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil + } + + // Second call for get_batch_deposits + deposits := []dtos.Deposit{ { - Nonce: big.NewInt(10), + Nonce: 42, TokenAddress: token1, - Amount: big.NewInt(20), - Depositor: from1, - Recipient: recipient1.AddressSlice(), + Amount: 20, + Depositor: from1[:], + Recipient: recipient1.AddressBytes(), + Status: uint8(1), }, { - Nonce: big.NewInt(30), + Nonce: 43, TokenAddress: token2, - Amount: big.NewInt(40), - Depositor: from2, - Recipient: recipient2.AddressSlice(), + Amount: 40, + Depositor: from2[:], + Recipient: recipient2.AddressBytes(), + Status: uint8(2), }, - }, true, nil + } + res, _ := createResultsRawFromValues(deposits, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil }, } bech32Recipient1Address, _ := recipient1.AddressAsBech32String() bech32Recipient2Address, _ := recipient2.AddressAsBech32String() expectedBatch := &core.TransferBatch{ - ID: 537, + ID: batchNonce, + BlockNumber: 100, Deposits: []*core.DepositTransfer{ { - Nonce: 10, + Nonce: 42, ToBytes: recipient1.AddressBytes(), DisplayableTo: bech32Recipient1Address, FromBytes: from1[:], @@ -291,7 +415,7 @@ func TestClient_GetBatch(t *testing.T) { DestinationTokenBytes: append([]byte("SUI"), token1[:]...), }, { - Nonce: 30, + Nonce: 43, ToBytes: recipient2.AddressBytes(), DisplayableTo: bech32Recipient2Address, FromBytes: from2[:], @@ -305,12 +429,16 @@ func TestClient_GetBatch(t *testing.T) { Statuses: make([]byte, 2), } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Equal(t, expectedBatch, batch) - assert.Nil(t, err) assert.True(t, isFinal) + assert.Nil(t, err) }) t.Run("returns non final batch should work", func(t *testing.T) { + t.Parallel() + args := createMockSuiClientArgs() + from1 := testsCommon.CreateRandomSuiAddressBytes() token1 := testsCommon.CreateRandomCoinId() recipient1 := testsCommon.CreateRandomMultiversXAddress() @@ -319,42 +447,69 @@ func TestClient_GetBatch(t *testing.T) { token2 := testsCommon.CreateRandomCoinId() recipient2 := testsCommon.CreateRandomMultiversXAddress() - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBatchCalled: func(ctx context.Context, batchNonce *big.Int) (core.Batch, bool, error) { - return core.Batch{ - Nonce: big.NewInt(98765), - BlockNumber: 0, - LastUpdatedBlockNumber: 0, - DepositsCount: 2, - }, false, nil - }, - GetBatchDepositsCalled: func(ctx context.Context, batchNonce *big.Int) ([]core.Deposit, bool, error) { - return []core.Deposit{ + wasGetBatchCalled := false + + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + // First call for get_batch + if wasGetBatchCalled == false { + wasGetBatchCalled = true + + res, _ := createResultsRawFromValues(dtos.Batch{ + Nonce: batchNonce, + DepositsCount: 2, + LastUpdatedTimestampMs: 100, + TimestampMs: 100, + }, false) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: res, + }, nil + } + + // Second call for get_batch_deposits + deposits := []dtos.Deposit{ { - Nonce: big.NewInt(10), + Nonce: 42, TokenAddress: token1, - Amount: big.NewInt(20), - Depositor: from1, - Recipient: recipient1.AddressSlice(), + Amount: 20, + Depositor: from1[:], + Recipient: recipient1.AddressBytes(), + Status: uint8(1), }, { - Nonce: big.NewInt(30), + Nonce: 43, TokenAddress: token2, - Amount: big.NewInt(40), - Depositor: from2, - Recipient: recipient2.AddressSlice(), + Amount: 40, + Depositor: from2[:], + Recipient: recipient2.AddressBytes(), + Status: uint8(2), + }, + } + res, _ := createResultsRawFromValues(deposits, true) + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, }, - }, false, nil + Results: res, + }, nil }, } bech32Recipient1Address, _ := recipient1.AddressAsBech32String() bech32Recipient2Address, _ := recipient2.AddressAsBech32String() expectedBatch := &core.TransferBatch{ - ID: 98765, + ID: batchNonce, + BlockNumber: 100, Deposits: []*core.DepositTransfer{ { - Nonce: 10, + Nonce: 42, ToBytes: recipient1.AddressBytes(), DisplayableTo: bech32Recipient1Address, FromBytes: from1[:], @@ -365,13 +520,13 @@ func TestClient_GetBatch(t *testing.T) { DestinationTokenBytes: append([]byte("SUI"), token1[:]...), }, { - Nonce: 30, + Nonce: 43, ToBytes: recipient2.AddressBytes(), DisplayableTo: bech32Recipient2Address, FromBytes: from2[:], DisplayableFrom: hex.EncodeToString(from2[:]), SourceTokenBytes: []byte(token2), - DisplayableToken: token2[:], + DisplayableToken: token2, Amount: big.NewInt(40), DestinationTokenBytes: append([]byte("SUI"), token2[:]...), }, @@ -379,10 +534,11 @@ func TestClient_GetBatch(t *testing.T) { Statuses: make([]byte, 2), } - batch, isFinal, err := c.GetBatch(context.Background(), 1) + c, _ := NewSuiClient(args) + batch, isFinal, err := c.GetBatch(context.Background(), batchNonce) assert.Equal(t, expectedBatch, batch) - assert.Nil(t, err) assert.False(t, isFinal) + assert.Nil(t, err) }) } @@ -395,19 +551,20 @@ func TestClient_BroadcastSignatureForMessageHash(t *testing.T) { expectedError := errors.New("expected error") msgToSign := []byte("message to sign") args := createMockSuiClientArgs() - args.Broadcaster = &testsCommon.BroadcasterStub{ - BroadcastSignatureCalled: func(signature []byte, messageHash []byte) { - assert.Fail(t, "should have not called broadcast") - }, - } - args.CryptoHandler = &bridgeTests.SuiCryptoHandlerStub{ + c, _ := NewSuiClient(args) + + c.txHandler = &bridgeTests.SuiTxHandlerStub{ SignCalled: func(msg []byte) ([]byte, error) { assert.Equal(t, msgToSign, msg) return nil, expectedError }, } + c.broadcaster = &testsCommon.BroadcasterStub{ + BroadcastSignatureCalled: func(signature []byte, messageHash []byte) { + assert.Fail(t, "should have not called broadcast") + }, + } - c, _ := NewSuiClient(args) c.BroadcastSignatureForMessageHash(msgToSign) }) t.Run("should work", func(t *testing.T) { @@ -418,21 +575,22 @@ func TestClient_BroadcastSignatureForMessageHash(t *testing.T) { msgToSign := []byte("message to sign") args := createMockSuiClientArgs() - args.Broadcaster = &testsCommon.BroadcasterStub{ + c, _ := NewSuiClient(args) + + c.txHandler = &bridgeTests.SuiTxHandlerStub{ + SignCalled: func(msg []byte) ([]byte, error) { + assert.Equal(t, msgToSign, msg) + return []byte(expectedSig), nil + }, + } + c.broadcaster = &testsCommon.BroadcasterStub{ BroadcastSignatureCalled: func(signature []byte, message []byte) { assert.Equal(t, msgToSign, message) assert.Equal(t, expectedSig, string(signature)) broadcastCalled = true }, } - args.CryptoHandler = &bridgeTests.SuiCryptoHandlerStub{ - SignCalled: func(msg []byte) ([]byte, error) { - assert.Equal(t, msgToSign, msg) - return []byte(expectedSig), nil - }, - } - c, _ := NewSuiClient(args) c.BroadcastSignatureForMessageHash(msgToSign) assert.True(t, broadcastCalled) @@ -444,10 +602,19 @@ func TestClient_WasExecuted(t *testing.T) { wasCalled := false args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - WasBatchExecutedCalled: func(ctx context.Context, batchNonce *big.Int) (bool, error) { + args.Proxy = &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { wasCalled = true - return true, nil + values, _ := createResultsRawFromValues(true) + + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: values, + }, nil }, } c, _ := NewSuiClient(args) @@ -461,43 +628,46 @@ func TestClient_WasExecuted(t *testing.T) { func TestClient_CheckRequiredBalance(t *testing.T) { t.Parallel() args := createMockSuiClientArgs() - coinType := "0x0::coin" + coinType := testsCommon.CreateRandomCoinId() balance := big.NewInt(1000000) - t.Run("not enough coins", func(t *testing.T) { + t.Run("get balance fails should error", func(t *testing.T) { + expectedErr := errors.New("expected error GetBalance") c, _ := NewSuiClient(args) - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { - assert.Equal(t, c.safeContractId, account) + c.proxy = &interactors.SuiProxyStub{ + SuiXGetBalanceCalled: func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { + return models.CoinBalanceResponse{}, expectedErr + }, + } + err := c.CheckRequiredBalance(context.Background(), coinType, balance) + assert.True(t, errors.Is(err, expectedErr)) + }) + t.Run("not enough coins", func(t *testing.T) { + c, _ := NewSuiClient(args) + c.proxy = &interactors.SuiProxyStub{ + SuiXGetBalanceCalled: func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { return models.CoinBalanceResponse{ + CoinType: coinType, TotalBalance: balance.String(), }, nil }, } + err := c.CheckRequiredBalance(context.Background(), coinType, big.NewInt(0).Add(balance, big.NewInt(1))) assert.True(t, errors.Is(err, errInsufficientCoinBalance)) }) - t.Run("erc20 balance of errors", func(t *testing.T) { - expectedErr := errors.New("expected error erc20 balance of") - c, _ := NewSuiClient(args) - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { - return models.CoinBalanceResponse{}, expectedErr - }, - } - err := c.CheckRequiredBalance(context.Background(), coinType, balance) - assert.True(t, errors.Is(err, expectedErr)) - }) t.Run("should work", func(t *testing.T) { c, _ := NewSuiClient(args) - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetBalanceCalled: func(ctx context.Context, account string, coinType string) (models.CoinBalanceResponse, error) { + c.proxy = &interactors.SuiProxyStub{ + SuiXGetBalanceCalled: func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { return models.CoinBalanceResponse{ + CoinType: coinType, TotalBalance: balance.String(), }, nil }, } + err := c.CheckRequiredBalance(context.Background(), coinType, balance) assert.Nil(t, err) }) @@ -505,240 +675,124 @@ func TestClient_CheckRequiredBalance(t *testing.T) { func TestClient_TotalBalances(t *testing.T) { t.Parallel() + coinType := testsCommon.CreateRandomCoinId() t.Run("error while getting total balances", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - TotalBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return nil, expectedErr - }, - } + args.Proxy = createFailMockProxy(expectedErr) c, _ := NewSuiClient(args) - balances, err := c.TotalBalances(context.Background(), "wrong coin type") + balances, err := c.TotalBalances(context.Background(), coinType) assert.Nil(t, balances) assert.True(t, errors.Is(err, expectedErr)) }) t.Run("should work", func(t *testing.T) { t.Parallel() - providedBalance := big.NewInt(100) + providedBalance := uint64(100) + values, _ := createResultsRawFromValues(providedBalance) args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - TotalBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return providedBalance, nil - }, - } + args.Proxy = createMockProxy(values) c, _ := NewSuiClient(args) - balances, err := c.TotalBalances(context.Background(), "0x0::coin::Coin") + balances, err := c.TotalBalances(context.Background(), coinType) assert.Nil(t, err) - assert.Equal(t, providedBalance, balances) + assert.Equal(t, big.NewInt(0).SetUint64(providedBalance), balances) }) } -func TestClient_MintBalances(t *testing.T) { +func TestClient_GetTransactionsStatuses(t *testing.T) { t.Parallel() - t.Run("error while getting mint balances", func(t *testing.T) { - t.Parallel() - - expectedErr := errors.New("expected error") - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - MintBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return nil, expectedErr - }, - } - c, _ := NewSuiClient(args) + expectedStatuses := []byte{1, 2, 3} + expectedBatchID := big.NewInt(2232) + expectedErr := errors.New("expected error") - balances, err := c.MintBalances(context.Background(), "wrong coin type") - assert.Nil(t, balances) - assert.True(t, errors.Is(err, expectedErr)) - }) - t.Run("should work", func(t *testing.T) { + t.Run("operation error, should error", func(t *testing.T) { t.Parallel() - providedBalance := big.NewInt(100) args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - MintBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return providedBalance, nil - }, - } - c, _ := NewSuiClient(args) - - balances, err := c.MintBalances(context.Background(), "0x0::coin::Coin") - assert.Nil(t, err) - assert.Equal(t, providedBalance, balances) - }) -} - -func TestClient_BurnBalances(t *testing.T) { - t.Parallel() - - t.Run("error while getting burn balances", func(t *testing.T) { - t.Parallel() + args.Proxy = createFailMockProxy(expectedErr) - expectedErr := errors.New("expected error") - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - BurnBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return nil, expectedErr - }, - } c, _ := NewSuiClient(args) - - balances, err := c.BurnBalances(context.Background(), "wrong coin type") - assert.Nil(t, balances) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Nil(t, statuses) assert.True(t, errors.Is(err, expectedErr)) }) - t.Run("should work", func(t *testing.T) { + t.Run("statuses are not final, should error", func(t *testing.T) { t.Parallel() - providedBalance := big.NewInt(100) args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - BurnBalancesCalled: func(ctx context.Context, token string) (*big.Int, error) { - return providedBalance, nil - }, - } - c, _ := NewSuiClient(args) - - balances, err := c.BurnBalances(context.Background(), "0x0::coin::Coin") - assert.Nil(t, err) - assert.Equal(t, providedBalance, balances) - }) -} + values, _ := createResultsRawFromValues([]byte("dummy"), false) + args.Proxy = createMockProxy(values) -func TestClient_MintBurnTokens(t *testing.T) { - t.Parallel() - - t.Run("error while getting mint burn tokens", func(t *testing.T) { - t.Parallel() - - expectedErr := errors.New("expected error") - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - MintBurnTokensCalled: func(ctx context.Context, token string) (bool, error) { - return false, expectedErr - }, - } c, _ := NewSuiClient(args) - - isMintBurn, err := c.MintBurnTokens(context.Background(), "wrong coin type") - assert.False(t, isMintBurn) - assert.True(t, errors.Is(err, expectedErr)) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Nil(t, statuses) + assert.Equal(t, clients.ErrStatusIsNotFinal, err) }) t.Run("should work", func(t *testing.T) { t.Parallel() args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - MintBurnTokensCalled: func(ctx context.Context, token string) (bool, error) { - return true, nil - }, - } - c, _ := NewSuiClient(args) + values, _ := createResultsRawFromValues(expectedStatuses, true) + args.Proxy = createMockProxy(values) - isMintBurn, err := c.MintBurnTokens(context.Background(), "0x0::coin::Coin") + c, _ := NewSuiClient(args) + statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) + assert.Equal(t, expectedStatuses, statuses) assert.Nil(t, err) - assert.True(t, isMintBurn) }) } -func TestClient_NativeTokens(t *testing.T) { +func TestClient_GenerateMessageHash(t *testing.T) { t.Parallel() - t.Run("error while getting native tokens", func(t *testing.T) { - t.Parallel() - - expectedErr := errors.New("expected error") + t.Run("should return error when batch is nil", func(t *testing.T) { args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - NativeTokensCalled: func(ctx context.Context, token string) (bool, error) { - return false, expectedErr - }, - } c, _ := NewSuiClient(args) + hash, err := c.GenerateMessageHash(nil, 42) - isNative, err := c.NativeTokens(context.Background(), "wrong coin type") - assert.False(t, isNative) - assert.True(t, errors.Is(err, expectedErr)) + assert.Nil(t, hash) + assert.True(t, errors.Is(err, clients.ErrNilBatch)) }) - t.Run("should work", func(t *testing.T) { - t.Parallel() + t.Run("should return correct hash for valid input", func(t *testing.T) { args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - NativeTokensCalled: func(ctx context.Context, token string) (bool, error) { - return true, nil - }, - } c, _ := NewSuiClient(args) - isNative, err := c.NativeTokens(context.Background(), "0x0::coin::Coin") - assert.Nil(t, err) - assert.True(t, isNative) - }) -} - -func TestClient_GetTransactionsStatuses(t *testing.T) { - t.Parallel() - - expectedStatuses := []byte{1, 2, 3} - expectedBatchID := big.NewInt(2232) - expectedErr := errors.New("expected error") - t.Run("operation error, should error", func(t *testing.T) { - t.Parallel() - - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { - assert.Equal(t, expectedBatchID, batchID) - return nil, false, expectedErr + batch := &batchProcessor.ArgListsBatchSui{ + SuiTokens: [][]byte{[]byte("token1"), []byte("token2")}, + Recipients: []models.SuiAddress{ + "recipient1", + "recipient2", }, + MvxTokenBytes: [][]byte{[]byte("mvxToken1"), []byte("mvxToken2")}, + Amounts: []uint64{100, 200}, + Nonces: []uint64{1, 2}, + Direction: batchProcessor.FromMultiversX, } + batchID := uint64(123) - c, _ := NewSuiClient(args) - statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) - assert.Nil(t, statuses) - assert.Equal(t, expectedErr, err) - }) - t.Run("statuses are not final, should error", func(t *testing.T) { - t.Parallel() - - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { - assert.Equal(t, expectedBatchID, batchID) - return []byte("dummy"), false, nil - }, + expectedData := batchProcessor.SuiTransferData{ + Recipients: batch.Recipients, + SuiTokens: batch.SuiTokens, + Amounts: batch.Amounts, + Nonces: batch.Nonces, + BatchId: batchID, } - c, _ := NewSuiClient(args) - statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) - assert.Nil(t, statuses) - assert.Equal(t, clients.ErrStatusIsNotFinal, err) - }) - t.Run("should work", func(t *testing.T) { - t.Parallel() + expectedBytes, err := mystenbcs.Marshal(expectedData) + assert.NoError(t, err) - args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - GetStatusesAfterExecutionCalled: func(ctx context.Context, batchID *big.Int) ([]byte, bool, error) { - assert.Equal(t, expectedBatchID, batchID) - return expectedStatuses, true, nil - }, - } + expectedHash := blake2b.Sum256(expectedBytes) - c, _ := NewSuiClient(args) - statuses, err := c.GetTransactionsStatuses(context.Background(), expectedBatchID.Uint64()) - assert.Equal(t, expectedStatuses, statuses) - assert.Nil(t, err) + hash, err := c.GenerateMessageHash(batch, batchID) + assert.NoError(t, err) + assert.Equal(t, expectedHash[:], hash) }) } @@ -746,17 +800,14 @@ func TestClient_GetQuorumSize(t *testing.T) { t.Parallel() args := createMockSuiClientArgs() - providedValue := big.NewInt(6453) - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - QuorumCalled: func(ctx context.Context) (*big.Int, error) { - return providedValue, nil - }, - } - c, _ := NewSuiClient(args) + providedValue := uint64(6453) + values, _ := createResultsRawFromValues(providedValue, true) + args.Proxy = createMockProxy(values) + c, _ := NewSuiClient(args) quorum, err := c.GetQuorumSize(context.Background()) assert.Nil(t, err) - assert.Equal(t, providedValue, quorum) + assert.Equal(t, big.NewInt(0).SetUint64(providedValue), quorum) } func TestClient_IsQuorumReached(t *testing.T) { @@ -768,11 +819,7 @@ func TestClient_IsQuorumReached(t *testing.T) { expectedErr := errors.New("expected error") args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - QuorumCalled: func(ctx context.Context) (*big.Int, error) { - return nil, expectedErr - }, - } + args.Proxy = createFailMockProxy(expectedErr) c, _ := NewSuiClient(args) isReached, err := c.IsQuorumReached(context.Background(), msg) @@ -783,11 +830,8 @@ func TestClient_IsQuorumReached(t *testing.T) { t.Parallel() args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - QuorumCalled: func(ctx context.Context) (*big.Int, error) { - return big.NewInt(0), nil - }, - } + values, _ := createResultsRawFromValues(uint64(0)) + args.Proxy = createMockProxy(values) c, _ := NewSuiClient(args) isReached, err := c.IsQuorumReached(context.Background(), msg) @@ -800,11 +844,9 @@ func TestClient_IsQuorumReached(t *testing.T) { signatures := make([][]byte, 0) args := createMockSuiClientArgs() - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - QuorumCalled: func(ctx context.Context) (*big.Int, error) { - return big.NewInt(3), nil - }, - } + values, _ := createResultsRawFromValues(uint64(3)) + args.Proxy = createMockProxy(values) + args.SignatureHolder = &testsCommon.SignaturesHolderStub{ SignaturesCalled: func(messageHash []byte) [][]byte { return signatures @@ -841,16 +883,17 @@ func TestClient_CheckClientAvailability(t *testing.T) { incrementor := uint64(1) args := createMockSuiClientArgs() statusHandler := testsCommon.NewStatusHandlerMock("test") - args.ClientWrapper = &bridgeTests.SuiClientWrapperStub{ - StatusHandler: statusHandler, - GetLatestCheckpointCalled: func(ctx context.Context) (uint64, error) { + expectedErr := errors.New("expected error") + args.StatusHandler = statusHandler + args.Proxy = &interactors.SuiProxyStub{ + SuiGetLatestCheckpointSequenceNumberCalled: func(ctx context.Context) (uint64, error) { currentCheckpoint += incrementor return currentCheckpoint, nil }, } - expectedErr := errors.New("expected error") c, _ := NewSuiClient(args) + t.Run("different current checkpoint should update - 10 times", func(t *testing.T) { resetClient(c) for i := 0; i < 10; i++ { @@ -911,9 +954,8 @@ func TestClient_CheckClientAvailability(t *testing.T) { }) t.Run("get current checkpoint errors", func(t *testing.T) { resetClient(c) - c.clientWrapper = &bridgeTests.SuiClientWrapperStub{ - StatusHandler: statusHandler, - GetLatestCheckpointCalled: func(ctx context.Context) (uint64, error) { + c.proxy = &interactors.SuiProxyStub{ + SuiGetLatestCheckpointSequenceNumberCalled: func(ctx context.Context) (uint64, error) { return 0, expectedErr }, } @@ -928,8 +970,8 @@ func resetClient(c *client) { c.mut.Lock() c.retriesAvailabilityCheck = 0 c.mut.Unlock() - c.clientWrapper.SetStringMetric(core.MetricMultiversXClientStatus, "") - c.clientWrapper.SetStringMetric(core.MetricLastMultiversXClientError, "") + c.statusHandler.SetStringMetric(core.MetricMultiversXClientStatus, "") + c.statusHandler.SetStringMetric(core.MetricLastMultiversXClientError, "") } func checkStatusHandler(t *testing.T, statusHandler *testsCommon.StatusHandlerMock, status core.ClientStatus, message string) { diff --git a/clients/sui/dtos/contractStructs.go b/clients/sui/dtos/contractStructs.go index 9b55d0b7..35231487 100644 --- a/clients/sui/dtos/contractStructs.go +++ b/clients/sui/dtos/contractStructs.go @@ -1,11 +1,9 @@ package dtos -import "math/big" - // Batch represents a batch of deposits from safe contract type Batch struct { Nonce uint64 - BlockNumber uint64 + TimestampMs uint64 LastUpdatedTimestampMs uint64 DepositsCount uint16 } @@ -14,19 +12,8 @@ type Batch struct { type Deposit struct { Nonce uint64 TokenAddress string - Amount *big.Int - Depositor [32]byte - Recipient [32]byte + Amount uint64 + Depositor []byte + Recipient []byte Status uint8 } - -// DepositStatus represents the status of a deposit -type DepositStatus byte - -const ( - None DepositStatus = iota - Pending - InProgress - Executed - Rejected -) diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go index 8e33f409..4c010a35 100644 --- a/clients/sui/suiClientDataGetter.go +++ b/clients/sui/suiClientDataGetter.go @@ -52,7 +52,7 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, return nil, errNilProxy } if len(args.RelayerAddress) == 0 { - return nil, fmt.Errorf("%w for the signer address argument", errNilAddress) + return nil, fmt.Errorf("%w for the RelayerAddress argument", errNilAddress) } if len(args.BridgePackageId) == 0 { return nil, fmt.Errorf("%w for the BridgePackageId argument", errNilPackageId) @@ -60,7 +60,7 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, if len(args.BridgeObjectId) == 0 { return nil, fmt.Errorf("%w for the BridgeObjectId argument", errNilObjectId) } - if args.BridgeInitialSharedVersion < 0 { + if args.BridgeInitialSharedVersion == 0 { return nil, errInvalidInitialSharedVersion } if len(args.SafePackageId) == 0 { @@ -69,7 +69,7 @@ func NewSuiClientDataGetter(args ArgsSuiClientDataGetter) (*suiClientDataGetter, if len(args.SafeObjectId) == 0 { return nil, fmt.Errorf("%w for the SafeObjectId argument", errNilObjectId) } - if args.SafeInitialSharedVersion < 0 { + if args.SafeInitialSharedVersion == 0 { return nil, errInvalidInitialSharedVersion } @@ -354,7 +354,7 @@ func (getter *suiClientDataGetter) Quorum(ctx context.Context) (uint64, error) { } // GetStatusesAfterExecution returns the statuses of the last executed transfer -func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) ([]dtos.DepositStatus, bool, error) { +func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context, batchNonce uint64) ([]byte, bool, error) { tx := transaction.NewTransaction() tx.MoveCall( @@ -380,18 +380,18 @@ func (getter *suiClientDataGetter) GetStatusesAfterExecution(ctx context.Context txBlockResp, err := getter.sendTxGetBlockResponse(ctx, tx) if err != nil { - return []dtos.DepositStatus{}, false, fmt.Errorf("failed to get quorum: %w", err) + return nil, false, fmt.Errorf("failed to get quorum: %w", err) } if txBlockResp.Effects.Status.Status != "success" { - return []dtos.DepositStatus{}, false, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) + return nil, false, fmt.Errorf("get quorum transaction failed: %s", txBlockResp.Effects.Status.Error) } - var depositStatuses []dtos.DepositStatus + var depositStatuses []byte var isFinal bool err = getter.decodeReturnValues(txBlockResp.Results, &depositStatuses, &isFinal) if err != nil { - return []dtos.DepositStatus{}, false, fmt.Errorf("failed to decode return value: %w", err) + return nil, false, fmt.Errorf("failed to decode return value: %w", err) } return depositStatuses, isFinal, nil diff --git a/clients/sui/suiClientDataGetter_test.go b/clients/sui/suiClientDataGetter_test.go new file mode 100644 index 00000000..e570ef9d --- /dev/null +++ b/clients/sui/suiClientDataGetter_test.go @@ -0,0 +1,776 @@ +package sui + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/mystenbcs" + "github.com/multiversx/mx-bridge-eth-go/clients" + "github.com/multiversx/mx-bridge-eth-go/clients/sui/dtos" + "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/stretchr/testify/assert" +) + +var batchNonce = uint64(7) + +func createMockArgsSuiClientDataGetter() ArgsSuiClientDataGetter { + return ArgsSuiClientDataGetter{ + SafePackageId: "0x674a8fc0a6b48c8efea86ad7ed962107c5c132a78e7cc79c9c5b9391ba8b6d83", + SafeObjectId: "0x32c8ebf5853163472964ce226b194af05aa5f2e4cc47678924ebe538a1c88416", + SafeInitialSharedVersion: 1, + BridgePackageId: "0x19ecc8df0b93999f87b5e2e531bb80a49d1ab2f4644032cd31f2c53ed624c94a", + BridgeObjectId: "0xe15513cc93d6efbfbdc7844df141b312bb677ee564a5838b7b22a891f9f05c65", + BridgeInitialSharedVersion: 2, + RelayerAddress: "mock-relayer-address", + Proxy: &interactors.SuiProxyStub{}, + Log: logger.GetOrCreate("test"), + } +} + +func createMockProxy(message json.RawMessage) *interactors.SuiProxyStub { + return &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + Results: message, + }, nil + }, + } +} + +func createFailMockProxy(err error) *interactors.SuiProxyStub { + return &interactors.SuiProxyStub{ + SuiDevInspectTransactionBlockCalled: func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "failed", + }, + }, + }, err + }, + } +} + +func createResultsRawFromValues(values ...interface{}) ([]byte, error) { + var returnValues [][]interface{} + + for _, value := range values { + marshaled, err := mystenbcs.Marshal(value) + if err != nil { + return nil, fmt.Errorf("failed to marshal value: %w", err) + } + returnValues = append(returnValues, []interface{}{marshaled, "test_type"}) + } + + results := []map[string]interface{}{ + { + "returnValues": returnValues, + }, + } + return json.Marshal(results) +} + +func TestNewSuiClientDataGetter(t *testing.T) { + t.Parallel() + + t.Run("nil logger", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.Log = nil + + dg, err := NewSuiClientDataGetter(args) + assert.Equal(t, clients.ErrNilLogger, err) + assert.Nil(t, dg) + }) + t.Run("nil proxy", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.Proxy = nil + + dg, err := NewSuiClientDataGetter(args) + assert.Equal(t, errNilProxy, err) + assert.Nil(t, dg) + }) + t.Run("nil bridge package id", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.BridgePackageId = "" + + dg, err := NewSuiClientDataGetter(args) + assert.True(t, errors.Is(err, errNilPackageId)) + assert.True(t, strings.Contains(err.Error(), "BridgePackageId")) + assert.Nil(t, dg) + }) + t.Run("nil bridge object id", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.BridgeObjectId = "" + + dg, err := NewSuiClientDataGetter(args) + assert.True(t, errors.Is(err, errNilObjectId)) + assert.True(t, strings.Contains(err.Error(), "BridgeObjectId")) + assert.Nil(t, dg) + }) + t.Run("invalid bridge initial shared version", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.BridgeInitialSharedVersion = 0 + + dg, err := NewSuiClientDataGetter(args) + assert.Equal(t, errInvalidInitialSharedVersion, err) + assert.Nil(t, dg) + }) + t.Run("nil safe package id", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.SafePackageId = "" + + dg, err := NewSuiClientDataGetter(args) + assert.True(t, errors.Is(err, errNilPackageId)) + assert.True(t, strings.Contains(err.Error(), "SafePackageId")) + assert.Nil(t, dg) + }) + t.Run("nil safe object id", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.SafeObjectId = "" + + dg, err := NewSuiClientDataGetter(args) + assert.True(t, errors.Is(err, errNilObjectId)) + assert.True(t, strings.Contains(err.Error(), "SafeObjectId")) + assert.Nil(t, dg) + }) + t.Run("invalid safe initial shared version", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.SafeInitialSharedVersion = 0 + + dg, err := NewSuiClientDataGetter(args) + assert.Equal(t, errInvalidInitialSharedVersion, err) + assert.Nil(t, dg) + }) + t.Run("nil relayer address", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + args.RelayerAddress = "" + + dg, err := NewSuiClientDataGetter(args) + assert.True(t, errors.Is(err, errNilAddress)) + assert.True(t, strings.Contains(err.Error(), "RelayerAddress")) + assert.Nil(t, dg) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + dg, err := NewSuiClientDataGetter(args) + assert.Nil(t, err) + assert.NotNil(t, dg) + }) +} + +func TestSuiClientDataGetter_GetBatchByNonce(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + batch, isFinal, err := dg.GetBatchByNonce(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, dtos.Batch{}, batch) + assert.False(t, isFinal) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + batch, isFinal, err := dg.GetBatchByNonce(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.Equal(t, dtos.Batch{}, batch) + assert.False(t, isFinal) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedBatch := dtos.Batch{ + Nonce: 42, + } + expectedIsFinal := true + + resultsRaw, err := createResultsRawFromValues(expectedBatch, expectedIsFinal) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + batch, isFinal, err := dg.GetBatchByNonce(context.Background(), batchNonce) + assert.Nil(t, err) + assert.Equal(t, expectedBatch, batch) + assert.Equal(t, expectedIsFinal, isFinal) + }) +} + +func TestSuiClientDataGetter_GetBatchDeposits(t *testing.T) { + t.Parallel() + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + deposits, areFinal, err := dg.GetBatchDeposits(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Nil(t, deposits) + assert.False(t, areFinal) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + deposits, areFinal, err := dg.GetBatchDeposits(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.Nil(t, deposits) + assert.False(t, areFinal) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedDeposits := []dtos.Deposit{ + { + Nonce: 42, + TokenAddress: "coin::Coin::0x1", + Amount: 1000, + Depositor: bytes.Repeat([]byte{0x1}, 32), + Recipient: bytes.Repeat([]byte{0x2}, 32), + Status: uint8(1), + }, + { + Nonce: 43, + TokenAddress: "coin::Coin::0x2", + Amount: 2000, + Depositor: bytes.Repeat([]byte{0x3}, 32), + Recipient: bytes.Repeat([]byte{0x4}, 32), + Status: uint8(2), + }, + } + expectedAreFinal := true + + resultsRaw, err := createResultsRawFromValues(expectedDeposits, expectedAreFinal) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + deposits, areFinal, err := dg.GetBatchDeposits(context.Background(), batchNonce) + assert.Nil(t, err) + assert.Equal(t, expectedDeposits, deposits) + assert.Equal(t, expectedAreFinal, areFinal) + }) +} + +func TestSuiClientDataGetter_GetRelayers(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + addresses, err := dg.GetRelayers(context.Background()) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Nil(t, addresses) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + addresses, err := dg.GetRelayers(context.Background()) + assert.NotNil(t, err) + assert.Nil(t, addresses) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedAddresses := []models.SuiAddress{ + "0xc75ff8f7215e3dc9671ae2ef85c519414b886bff4e0f6edb5ff7b8d3d9e648fa", + "0xfe6d2075696ecea4614d9dd3ac532c84af2ac1962cee43ad99a6d84f3f7e30e0", + } + + resultsRaw, err := createResultsRawFromValues(expectedAddresses) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + addresses, err := dg.GetRelayers(context.Background()) + assert.Nil(t, err) + assert.Equal(t, expectedAddresses, addresses) + }) +} + +func TestSuiClientDataGetter_WasBatchExecuted(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + wasBatchExecuted, err := dg.WasBatchExecuted(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.False(t, wasBatchExecuted) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + wasBatchExecuted, err := dg.WasBatchExecuted(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.False(t, wasBatchExecuted) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedBool := true + + resultsRaw, err := createResultsRawFromValues(expectedBool) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + wasBatchExecuted, err := dg.WasBatchExecuted(context.Background(), batchNonce) + assert.Nil(t, err) + assert.Equal(t, expectedBool, wasBatchExecuted) + }) +} + +func TestSuiClientDataGetter_IsPaused(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + isPaused, err := dg.IsPaused(context.Background()) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.False(t, isPaused) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + isPaused, err := dg.IsPaused(context.Background()) + assert.NotNil(t, err) + assert.False(t, isPaused) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedBool := true + resultsRaw, err := createResultsRawFromValues(expectedBool) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + isPaused, err := dg.IsPaused(context.Background()) + assert.Nil(t, err) + assert.Equal(t, expectedBool, isPaused) + }) +} + +func TestSuiClientDataGetter_Quorum(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + quorum, err := dg.Quorum(context.Background()) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, uint64(0), quorum) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + quorum, err := dg.Quorum(context.Background()) + assert.NotNil(t, err) + assert.Equal(t, uint64(0), quorum) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedQuorum := uint64(5) + + resultsRaw, err := createResultsRawFromValues(expectedQuorum) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + quorum, err := dg.Quorum(context.Background()) + assert.Nil(t, err) + assert.Equal(t, expectedQuorum, quorum) + }) +} + +func TestSuiClientDataGetter_GetStatusesAfterExecution(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + statuses, isFinal, err := dg.GetStatusesAfterExecution(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Nil(t, statuses) + assert.False(t, isFinal) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + statuses, isFinal, err := dg.GetStatusesAfterExecution(context.Background(), batchNonce) + assert.NotNil(t, err) + assert.Nil(t, statuses) + assert.False(t, isFinal) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedStatuses := []uint8{1, 2, 3} + expectedIsFinal := true + + resultsRaw, err := createResultsRawFromValues(expectedStatuses, expectedIsFinal) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + statuses, isFinal, err := dg.GetStatusesAfterExecution(context.Background(), batchNonce) + assert.Nil(t, err) + assert.Equal(t, expectedStatuses, statuses) + assert.Equal(t, expectedIsFinal, isFinal) + }) +} + +func TestSuiClientDataGetter_GetTotalBalanceFromSafe(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + coinType := "0x2::sui::SUI" + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + balance, err := dg.GetTotalBalanceFromSafe(context.Background(), coinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, uint64(0), balance) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + balance, err := dg.GetTotalBalanceFromSafe(context.Background(), coinType) + assert.NotNil(t, err) + assert.Equal(t, uint64(0), balance) + }) + t.Run("invalid coin type", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + invalidCoinType := "invalid::cointype" + + balance, err := dg.GetTotalBalanceFromSafe(context.Background(), invalidCoinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "failed to parse coin type")) + assert.Equal(t, uint64(0), balance) + }) + t.Run("failed to convert coin type", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + invalidAddressCoinType := "invalid-address::sui::SUI" + + balance, err := dg.GetTotalBalanceFromSafe(context.Background(), invalidAddressCoinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "failed to convert coin type")) + assert.Equal(t, uint64(0), balance) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedBalance := uint64(1000000) + + resultsRaw, err := createResultsRawFromValues(expectedBalance) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + balance, err := dg.GetTotalBalanceFromSafe(context.Background(), coinType) + assert.Nil(t, err) + assert.Equal(t, expectedBalance, balance) + }) +} + +func TestSuiClientDataGetter_IsTokenWhitelisted(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + coinType := "0x2::sui::SUI" + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = createFailMockProxy(expectedErr) + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), coinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.False(t, isWhitelisted) + }) + t.Run("failed tx status", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + dg.proxy = createFailMockProxy(nil) + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), coinType) + assert.NotNil(t, err) + assert.False(t, isWhitelisted) + }) + t.Run("invalid coin type", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + invalidCoinType := "invalid::cointype" + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), invalidCoinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "failed to parse coin type")) + assert.False(t, isWhitelisted) + }) + t.Run("failed to convert coin type", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + invalidAddressCoinType := "invalid-address::sui::SUI" + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), invalidAddressCoinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "failed to convert coin type")) + assert.False(t, isWhitelisted) + }) + t.Run("should work - token whitelisted", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedIsWhitelisted := true + + resultsRaw, err := createResultsRawFromValues(expectedIsWhitelisted) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), coinType) + assert.Nil(t, err) + assert.Equal(t, expectedIsWhitelisted, isWhitelisted) + }) + t.Run("should work - token not whitelisted", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + + expectedIsWhitelisted := false + + resultsRaw, err := createResultsRawFromValues(expectedIsWhitelisted) + assert.NoError(t, err) + + dg.proxy = createMockProxy(resultsRaw) + + isWhitelisted, err := dg.IsTokenWhitelisted(context.Background(), coinType) + assert.Nil(t, err) + assert.Equal(t, expectedIsWhitelisted, isWhitelisted) + }) +} + +func TestSuiClientDataGetter_GetLatestCheckpoint(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = &interactors.SuiProxyStub{ + SuiGetLatestCheckpointSequenceNumberCalled: func(ctx context.Context) (uint64, error) { + return 0, expectedErr + }, + } + + checkpoint, err := dg.GetLatestCheckpoint(context.Background()) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, uint64(0), checkpoint) + }) + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedCheckpoint := uint64(12345) + dg.proxy = &interactors.SuiProxyStub{ + SuiGetLatestCheckpointSequenceNumberCalled: func(ctx context.Context) (uint64, error) { + return expectedCheckpoint, nil + }, + } + + checkpoint, err := dg.GetLatestCheckpoint(context.Background()) + assert.Nil(t, err) + assert.Equal(t, expectedCheckpoint, checkpoint) + }) +} + +func TestSuiClientDataGetter_GetBalance(t *testing.T) { + t.Parallel() + + args := createMockArgsSuiClientDataGetter() + account := "0xc75ff8f7215e3dc9671ae2ef85c519414b886bff4e0f6edb5ff7b8d3d9e648fa" + coinType := "0x2::sui::SUI" + + t.Run("proxy errors", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedErr := errors.New("expected error") + dg.proxy = &interactors.SuiProxyStub{ + SuiXGetBalanceCalled: func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { + return models.CoinBalanceResponse{}, expectedErr + }, + } + + balance, err := dg.GetBalance(context.Background(), account, coinType) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, models.CoinBalanceResponse{}, balance) + }) + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + dg, _ := NewSuiClientDataGetter(args) + expectedBalance := models.CoinBalanceResponse{ + CoinType: coinType, + CoinObjectCount: 5, + TotalBalance: "1000000000", + } + dg.proxy = &interactors.SuiProxyStub{ + SuiXGetBalanceCalled: func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { + assert.Equal(t, account, req.Owner) + assert.Equal(t, coinType, req.CoinType) + return expectedBalance, nil + }, + } + + balance, err := dg.GetBalance(context.Background(), account, coinType) + assert.Nil(t, err) + assert.Equal(t, expectedBalance, balance) + }) +} diff --git a/clients/sui/testdata/nok-sui-seed b/clients/sui/testdata/nok-sui-seed deleted file mode 100644 index 3a6260b0..00000000 --- a/clients/sui/testdata/nok-sui-seed +++ /dev/null @@ -1 +0,0 @@ -vrsݩ`^vn \ No newline at end of file diff --git a/clients/sui/testdata/ok-sui-seed b/clients/sui/testdata/ok-sui-seed deleted file mode 100644 index 26289944..00000000 --- a/clients/sui/testdata/ok-sui-seed +++ /dev/null @@ -1,2 +0,0 @@ -qyk -C}g 6r;_/0IS \ No newline at end of file diff --git a/clients/sui/transactionHandler_test.go b/clients/sui/transactionHandler_test.go new file mode 100644 index 00000000..9f1a31f6 --- /dev/null +++ b/clients/sui/transactionHandler_test.go @@ -0,0 +1,157 @@ +package sui + +import ( + "context" + "errors" + "github.com/block-vision/sui-go-sdk/models" + "github.com/block-vision/sui-go-sdk/signer" + "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +var ( + seedBytes = []byte{ + 49, 48, 50, 97, 49, 113, 122, 122, 121, 57, 120, 56, 54, 121, 101, 55, + 107, 121, 112, 116, 103, 57, 117, 51, 115, 97, 113, 117, 103, 48, 104, 100, + } + testBridgePackageId = "0x19ecc8df0b93999f87b5e2e531bb80a49d1ab2f4644032cd31f2c53ed624c94a" +) + +func createTransactionHandlerWithMockComponents() *transactionHandler { + return &transactionHandler{ + proxy: &interactors.SuiProxyStub{}, + relayerSigner: signer.NewSigner([]byte(seedBytes)), + } +} + +func TestTransactionHandler_SendTransactionReturnHash(t *testing.T) { + t.Parallel() + + moveCallRequest := models.MoveCallRequest{ + PackageObjectId: testBridgePackageId, + Module: "test_module", + Function: "test_function", + TypeArguments: []interface{}{}, + Arguments: []interface{}{}, + GasBudget: "1000000", + } + + t.Run("MoveCall proxy error", func(t *testing.T) { + t.Parallel() + + txHandlerInstance := createTransactionHandlerWithMockComponents() + expectedErr := errors.New("move call error") + txHandlerInstance.proxy = &interactors.SuiProxyStub{ + MoveCallCalled: func(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + assert.Equal(t, txHandlerInstance.relayerSigner.Address, req.Signer) + return models.TxnMetaData{}, expectedErr + }, + } + + moveCallRequest.Signer = txHandlerInstance.relayerSigner.Address + hash, err := txHandlerInstance.SendTransactionReturnHash(context.Background(), moveCallRequest) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, "", hash) + }) + + t.Run("SignAndExecuteTransactionBlock proxy error", func(t *testing.T) { + t.Parallel() + + txHandlerInstance := createTransactionHandlerWithMockComponents() + expectedErr := errors.New("sign and execute error") + mockTxnMetaData := models.TxnMetaData{ + TxBytes: "mock-tx-bytes", + } + + txHandlerInstance.proxy = &interactors.SuiProxyStub{ + MoveCallCalled: func(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + return mockTxnMetaData, nil + }, + SignAndExecuteTransactionBlockCalled: func(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + assert.Equal(t, mockTxnMetaData, req.TxnMetaData) + assert.Equal(t, txHandlerInstance.relayerSigner.PriKey, req.PriKey) + assert.Equal(t, "WaitForLocalExecution", req.RequestType) + assert.True(t, req.Options.ShowInput) + assert.True(t, req.Options.ShowEffects) + assert.True(t, req.Options.ShowObjectChanges) + return models.SuiTransactionBlockResponse{}, expectedErr + }, + } + + moveCallRequest.Signer = txHandlerInstance.relayerSigner.Address + hash, err := txHandlerInstance.SendTransactionReturnHash(context.Background(), moveCallRequest) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), expectedErr.Error())) + assert.Equal(t, "", hash) + }) + + t.Run("transaction failed status", func(t *testing.T) { + t.Parallel() + + txHandlerInstance := createTransactionHandlerWithMockComponents() + mockTxnMetaData := models.TxnMetaData{ + TxBytes: "mock-tx-bytes", + } + + txHandlerInstance.proxy = &interactors.SuiProxyStub{ + MoveCallCalled: func(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + return mockTxnMetaData, nil + }, + SignAndExecuteTransactionBlockCalled: func(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + return models.SuiTransactionBlockResponse{ + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "failure", + Error: "transaction execution failed", + }, + }, + }, nil + }, + } + + moveCallRequest.Signer = txHandlerInstance.relayerSigner.Address + hash, err := txHandlerInstance.SendTransactionReturnHash(context.Background(), moveCallRequest) + assert.NotNil(t, err) + assert.Equal(t, "transaction execution failed", err.Error()) + assert.Equal(t, "", hash) + }) + + t.Run("send transaction successfully", func(t *testing.T) { + t.Parallel() + + expectedHash := "expected hash" + mockTxnMetaData := models.TxnMetaData{ + TxBytes: "mock-tx-bytes", + } + + txHandlerInstance := createTransactionHandlerWithMockComponents() + txHandlerInstance.proxy = &interactors.SuiProxyStub{ + MoveCallCalled: func(ctx context.Context, request models.MoveCallRequest) (models.TxnMetaData, error) { + assert.Equal(t, txHandlerInstance.relayerSigner.Address, request.Signer) + return mockTxnMetaData, nil + }, + + SignAndExecuteTransactionBlockCalled: func(ctx context.Context, request models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + assert.Equal(t, mockTxnMetaData, request.TxnMetaData) + assert.Equal(t, txHandlerInstance.relayerSigner.PriKey, request.PriKey) + + return models.SuiTransactionBlockResponse{ + Digest: expectedHash, + Effects: models.SuiEffects{ + Status: models.ExecutionStatus{ + Status: "success", + }, + }, + }, nil + }, + } + + moveCallRequest.Signer = txHandlerInstance.relayerSigner.Address + hash, err := txHandlerInstance.SendTransactionReturnHash(context.Background(), moveCallRequest) + assert.Nil(t, err) + assert.Equal(t, expectedHash, hash) + }) +} diff --git a/go.mod b/go.mod index f364203f..d8c031e1 100644 --- a/go.mod +++ b/go.mod @@ -108,6 +108,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect @@ -189,6 +190,7 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/samber/lo v1.49.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/status-im/keycard-go v0.2.0 // indirect diff --git a/go.sum b/go.sum index 5d63c728..b77652c1 100644 --- a/go.sum +++ b/go.sum @@ -386,6 +386,8 @@ github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZl github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -674,6 +676,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/testsCommon/bridge/suiTxHandlerStub.go b/testsCommon/bridge/suiTxHandlerStub.go new file mode 100644 index 00000000..1c909436 --- /dev/null +++ b/testsCommon/bridge/suiTxHandlerStub.go @@ -0,0 +1,30 @@ +package bridge + +import ( + "context" + "github.com/block-vision/sui-go-sdk/models" +) + +// SuiTxHandlerStub - +type SuiTxHandlerStub struct { + SendTransactionReturnHashCalled func(ctx context.Context, moveCallRequest models.MoveCallRequest) (string, error) + SignCalled func(message []byte) ([]byte, error) +} + +// SendTransactionReturnHash - +func (t *SuiTxHandlerStub) SendTransactionReturnHash(ctx context.Context, moveCallRequest models.MoveCallRequest) (string, error) { + if t.SendTransactionReturnHashCalled != nil { + return t.SendTransactionReturnHashCalled(ctx, moveCallRequest) + } + + return "", nil +} + +// Sign - +func (t *SuiTxHandlerStub) Sign(message []byte) ([]byte, error) { + if t.SignCalled != nil { + return t.SignCalled(message) + } + + return nil, nil +} diff --git a/testsCommon/interactors/suiProxyStub.go b/testsCommon/interactors/suiProxyStub.go new file mode 100644 index 00000000..37b3d492 --- /dev/null +++ b/testsCommon/interactors/suiProxyStub.go @@ -0,0 +1,57 @@ +package interactors + +import ( + "context" + "fmt" + + "github.com/block-vision/sui-go-sdk/models" +) + +// SuiProxyStub - +type SuiProxyStub struct { + SuiGetLatestCheckpointSequenceNumberCalled func(ctx context.Context) (uint64, error) + SuiXGetBalanceCalled func(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) + SuiDevInspectTransactionBlockCalled func(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) + MoveCallCalled func(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) + SignAndExecuteTransactionBlockCalled func(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) +} + +// SuiGetLatestCheckpointSequenceNumber - +func (sps *SuiProxyStub) SuiGetLatestCheckpointSequenceNumber(ctx context.Context) (uint64, error) { + if sps.SuiGetLatestCheckpointSequenceNumberCalled != nil { + return sps.SuiGetLatestCheckpointSequenceNumberCalled(ctx) + } + return 0, fmt.Errorf("not implemented") +} + +// SuiXGetBalance - +func (sps *SuiProxyStub) SuiXGetBalance(ctx context.Context, req models.SuiXGetBalanceRequest) (models.CoinBalanceResponse, error) { + if sps.SuiXGetBalanceCalled != nil { + return sps.SuiXGetBalanceCalled(ctx, req) + } + return models.CoinBalanceResponse{}, fmt.Errorf("not implemented") +} + +// SuiDevInspectTransactionBlock - +func (sps *SuiProxyStub) SuiDevInspectTransactionBlock(ctx context.Context, req models.SuiDevInspectTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + if sps.SuiDevInspectTransactionBlockCalled != nil { + return sps.SuiDevInspectTransactionBlockCalled(ctx, req) + } + return models.SuiTransactionBlockResponse{}, fmt.Errorf("not implemented") +} + +// MoveCall - +func (sps *SuiProxyStub) MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) { + if sps.MoveCallCalled != nil { + return sps.MoveCallCalled(ctx, req) + } + return models.TxnMetaData{}, fmt.Errorf("not implemented") +} + +// SignAndExecuteTransactionBlock - +func (sps *SuiProxyStub) SignAndExecuteTransactionBlock(ctx context.Context, req models.SignAndExecuteTransactionBlockRequest) (models.SuiTransactionBlockResponse, error) { + if sps.SignAndExecuteTransactionBlockCalled != nil { + return sps.SignAndExecuteTransactionBlockCalled(ctx, req) + } + return models.SuiTransactionBlockResponse{}, fmt.Errorf("not implemented") +} From c0fc1d91da75f46a119358e8344b58150f7c07ea Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Thu, 17 Jul 2025 19:19:28 +0300 Subject: [PATCH 8/9] remove toolchain --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index d8c031e1..292e629c 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/multiversx/mx-bridge-eth-go go 1.21 -toolchain go1.22.12 require ( github.com/block-vision/sui-go-sdk v1.0.8 From 86324a368ee067a46699c1e88876435adf5ae609 Mon Sep 17 00:00:00 2001 From: cosmatudor Date: Fri, 18 Jul 2025 12:12:41 +0300 Subject: [PATCH 9/9] fixes after lint --- clients/sui/client_test.go | 31 ------------------------------ clients/sui/suiClientDataGetter.go | 2 -- go.mod | 1 - 3 files changed, 34 deletions(-) diff --git a/clients/sui/client_test.go b/clients/sui/client_test.go index f1b4b814..49809039 100644 --- a/clients/sui/client_test.go +++ b/clients/sui/client_test.go @@ -51,37 +51,6 @@ func createMockSuiClientArgs() ArgsSuiClient { } } -func createMockTransferBatch() *core.TransferBatch { - return &core.TransferBatch{ - ID: 332, - Deposits: []*core.DepositTransfer{ - { - Nonce: 10, - ToBytes: []byte("to1"), - DisplayableTo: "to1", - FromBytes: []byte("from1"), - DisplayableFrom: "from1", - SourceTokenBytes: []byte("source token1"), - DisplayableToken: "token1", - Amount: big.NewInt(20), - DestinationTokenBytes: []byte("SUItoken1"), - }, - { - Nonce: 30, - ToBytes: []byte("to2"), - DisplayableTo: "to2", - FromBytes: []byte("from2"), - DisplayableFrom: "from2", - SourceTokenBytes: []byte("source token2"), - DisplayableToken: "token2", - Amount: big.NewInt(40), - DestinationTokenBytes: []byte("SUItoken2"), - }, - }, - Statuses: make([]byte, 2), - } -} - func TestNewSuiClient(t *testing.T) { t.Parallel() diff --git a/clients/sui/suiClientDataGetter.go b/clients/sui/suiClientDataGetter.go index 4c010a35..2cf57d0d 100644 --- a/clients/sui/suiClientDataGetter.go +++ b/clients/sui/suiClientDataGetter.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "strings" - "sync" "github.com/block-vision/sui-go-sdk/models" "github.com/block-vision/sui-go-sdk/mystenbcs" @@ -40,7 +39,6 @@ type suiClientDataGetter struct { relayerAddress string proxy Proxy log chainCore.Logger - mtx sync.Mutex } // NewSuiClientDataGetter creates a new instance of type suiClientDataGetter diff --git a/go.mod b/go.mod index 292e629c..ddbb6132 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/multiversx/mx-bridge-eth-go go 1.21 - require ( github.com/block-vision/sui-go-sdk v1.0.8 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792