diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 11b0a2090d7..0d3e73fa2f3 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -247,9 +247,17 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types RPCClient: node.Client(), } + signerInfo, err := signer.Info() + require.NoError(t, err) + + acc, _, err := cli.QueryAccount(signerInfo.GetAddress()) + require.NoError(t, err) + txcfg := gnoclient.BaseTxCfg{ - GasFee: ugnot.ValueString(1000000), // Gas fee - GasWanted: 2_000_000, // Gas wanted + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 2_000_000, // Gas wanted + AccountNumber: acc.AccountNumber, + SequenceNumber: acc.Sequence, } // Set Caller in the msgs diff --git a/docs/gno-tooling/cli/gnokey/state-changing-calls.md b/docs/gno-tooling/cli/gnokey/state-changing-calls.md index 79a777cca51..e707d6170bb 100644 --- a/docs/gno-tooling/cli/gnokey/state-changing-calls.md +++ b/docs/gno-tooling/cli/gnokey/state-changing-calls.md @@ -88,6 +88,7 @@ The `addpkg` subcommmand uses the following flags and arguments: - `-gas-fee` - amount of GNOTs to pay per gas unit - `-chain-id` - id of the chain that we are sending the transaction to - `-remote` - specifies the remote node RPC listener address +- `-sponsor` - bech32 address of the sponsor who pay gas fee for the transaction The `-pkgpath` and `-pkgdir` flags are unique to the `addpkg` subcommand, while `-broadcast`,`-send`, `-gas-wanted`, `-gas-fee`, `-chain-id`, and `-remote` are diff --git a/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_existed_acc.txtar b/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_existed_acc.txtar new file mode 100644 index 00000000000..51ef9bc9f3f --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_existed_acc.txtar @@ -0,0 +1,107 @@ +# Test for performing a sponsor addpkg transaction for an existed on-chain account (sponsoree) + +# Add new sponsor account +adduser sponsor + +# Add new sponsoree account +adduser sponsoree + +# start a new node +gnoland start + +# Query sponsor's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Sponsoree creates the transaction (deploy nft realm) +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/${USER_ADDR_sponsoree}/nft -gas-fee 1000000ugnot -gas-wanted 20000000 -chainid=tendermint_test -sponsor=${USER_ADDR_sponsor} sponsoree +cp stdout sponsor.tx + +# Sponsoree signs the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsoree +cmpenv stdout sign.stdout.golden + +# Sponsor countersigns the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsor +cmpenv stdout sign.stdout.golden + +# Sponsor broadcasts the transaction +gnokey broadcast $WORK/sponsor.tx + +# Compare output to ensure the transaction was successful +stdout OK! +stdout 'GAS WANTED: 20000000' +stdout 'GAS USED: \d+' +stdout 'HEIGHT: \d+' +stdout 'EVENTS: \[\]' +stdout 'TX HASH: ' + +# Query sponsor's account after the transaction to see gas deduction +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9000000ugnot",' # 1000000 ugnot gas deducted from sponsor's balance +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account after the transaction +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' # Sponsoree's balance remains the same (sponsor paid the fees) +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +-- nft.gno -- +package nft + +func Mint() string { + return "Minted NFT successful" +} + +-- sign.stdout.golden -- + +Tx successfully signed and saved to $WORK/sponsor.tx diff --git a/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_nonexisted_acc.txtar b/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_nonexisted_acc.txtar new file mode 100644 index 00000000000..e1ad166c57b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/sponsor_addpkg_for_nonexisted_acc.txtar @@ -0,0 +1,98 @@ +# Test for performing a sponsor transaction for an non-existed on-chain account (sponsoree) + +# Add new sponsor account +adduser sponsor + +# Create a sponsoree key (only the key, without creating the on-chain account) +addkey sponsoree + +# start a new node +gnoland start + +# Query sponsoree's account before +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsor's account before +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: null' + +# Sponsoree make a call transaction +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/${USER_ADDR_sponsoree}/nft -gas-fee 1000000ugnot -gas-wanted 20000000 -chainid=tendermint_test -sponsor=${USER_ADDR_sponsor} sponsoree +cp stdout sponsor.tx + +# Sponsoree signs the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsoree +cmpenv stdout sign.stdout.golden + +# Sponsor countersigns the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsor +cmpenv stdout sign.stdout.golden + +# Sponsor broadcasts the transaction +gnokey broadcast $WORK/sponsor.tx + +# Compare output to ensure the transaction was successful +stdout OK! +stdout 'GAS WANTED: 20000000' +stdout 'GAS USED: \d+' +stdout 'HEIGHT: \d+' +stdout 'EVENTS: \[\]' +stdout 'TX HASH: ' + +# Query sponsor's account after the transaction to see gas deduction +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9000000ugnot",' # 1000000 ugnot gas deducted from sponsor's balance +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account after the transaction +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "",' # Sponsoree has no coins (sponsor paid the fees) +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' # Sponsoree's new account number +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +-- nft.gno -- +package nft + +func Mint() string { + return "Minted NFT successful" +} + +-- sign.stdout.golden -- + +Tx successfully signed and saved to $WORK/sponsor.tx \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/sponsor_call_for_existed_acc.txtar b/gno.land/cmd/gnoland/testdata/sponsor_call_for_existed_acc.txtar new file mode 100644 index 00000000000..6e6bb4af6a5 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/sponsor_call_for_existed_acc.txtar @@ -0,0 +1,111 @@ +# Test for performing a sponsor call transaction for an existed on-chain account (sponsoree) + +# Load the package from $WORK directory +loadpkg gno.land/r/demo/nft $WORK + +# Add new sponsor account +adduser sponsor + +# Add new sponsoree account +adduser sponsoree + +# start a new node +gnoland start + +# Query sponsor's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +# Sponsoree creates the transaction (call to Mint in nft realm) +# Sponsor address is provided for sponsorship +gnokey maketx call -pkgpath gno.land/r/demo/nft -chainid=tendermint_test -func Mint -gas-fee 1000000ugnot -gas-wanted 2000000 -sponsor=${USER_ADDR_sponsor} sponsoree +cp stdout sponsor.tx + +# Sponsoree signs the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsoree +cmpenv stdout sign.stdout.golden + +# Sponsor countersigns the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsor +cmpenv stdout sign.stdout.golden + +# Sponsor broadcasts the transaction +gnokey broadcast $WORK/sponsor.tx + +# Compare output to ensure the transaction was successful +stdout OK! +stdout 'GAS WANTED: 2000000' +stdout 'GAS USED: \d+' +stdout 'HEIGHT: \d+' +stdout 'EVENTS: \[\]' +stdout 'TX HASH: ' + +# Query sponsor's account after the transaction to see gas deduction +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9000000ugnot",' # 1000000 ugnot gas deducted from sponsor's balance +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account after the transaction +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' # Sponsoree's balance remains the same (sponsor paid the fees) +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +-- nft.gno -- +package nft + +func Mint() string { + return "Minted NFT successful" +} + +-- sign.stdout.golden -- + +Tx successfully signed and saved to $WORK/sponsor.tx diff --git a/gno.land/cmd/gnoland/testdata/sponsor_call_for_nonexisted_acc.txtar b/gno.land/cmd/gnoland/testdata/sponsor_call_for_nonexisted_acc.txtar new file mode 100644 index 00000000000..54555d406a0 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/sponsor_call_for_nonexisted_acc.txtar @@ -0,0 +1,102 @@ +# Test for performing a sponsor call transaction for an non-existed on-chain account (sponsoree) + +# Load the package from $WORK directory +loadpkg gno.land/r/demo/nft $WORK + +# Add new sponsor account +adduser sponsor + +# Create a sponsoree key (only the key, without creating the on-chain account) +addkey sponsoree + +# start a new node +gnoland start + +# Query sponsor's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account before the transaction (should not exist yet) +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: null' + +# Sponsoree creates the transaction (call to Mint in nft realm) +# Sponsor address is provided for sponsorship +gnokey maketx call -pkgpath gno.land/r/demo/nft -chainid=tendermint_test -func Mint -gas-fee 1000000ugnot -gas-wanted 2000000 -sponsor=${USER_ADDR_sponsor} sponsoree +cp stdout sponsor.tx + +# Sponsoree signs the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsoree +cmpenv stdout sign.stdout.golden + +# Sponsor countersigns the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsor +cmpenv stdout sign.stdout.golden + +# Sponsor broadcasts the transaction +gnokey broadcast $WORK/sponsor.tx + +# Compare output to ensure the transaction was successful +stdout OK! +stdout 'GAS WANTED: 2000000' +stdout 'GAS USED: \d+' +stdout 'HEIGHT: \d+' +stdout 'EVENTS: \[\]' +stdout 'TX HASH: ' + +# Query sponsor's account after the transaction to see gas deduction +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9000000ugnot",' # 1000000 ugnot gas deducted from sponsor's balance +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account after the transaction (now created) +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "",' # Sponsoree has no coins (sponsor paid the fees) +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' # Sponsoree's new account number +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +-- nft.gno -- +package nft + +func Mint() string { + return "Minted NFT successful" +} + +-- sign.stdout.golden -- + +Tx successfully signed and saved to $WORK/sponsor.tx diff --git a/gno.land/cmd/gnoland/testdata/sponsor_send_coins.txtar b/gno.land/cmd/gnoland/testdata/sponsor_send_coins.txtar new file mode 100644 index 00000000000..5f2132f8402 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/sponsor_send_coins.txtar @@ -0,0 +1,134 @@ +# Test for performing a sponsor send transaction without the sponsoree (sender) paying gas + +# Add new sponsor account +adduser sponsor + +# Add new sponsoree account (sender) +adduser sponsoree + +# Add new sponsoree account (receipent) +adduser receipent + +# start a new node +gnoland start + +# Query sponsor's account before the transaction to check initial state +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account before the transaction +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query receipent's account before the transaction +gnokey query auth/accounts/${USER_ADDR_receipent} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000000ugnot",' +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +# Sponsoree creates the send transaction (sending 1 ugnot to the recipient) +# Sponsor address is provided for sponsorship +gnokey maketx send -send "1ugnot" -to ${USER_ADDR_receipent} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test -sponsor=${USER_ADDR_sponsor} sponsoree +cp stdout sponsor.tx + +# Sponsoree signs the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsoree +cmpenv stdout sign.stdout.golden + +# Sponsor countersigns the transaction +gnokey sign -tx-path $WORK/sponsor.tx -chainid "tendermint_test" -fetch-account-info=true sponsor +cmpenv stdout sign.stdout.golden + +# Sponsor broadcasts the transaction +gnokey broadcast $WORK/sponsor.tx + +# Compare output to ensure the transaction was successful +stdout OK! +stdout 'GAS WANTED: 10000000' +stdout 'GAS USED: \d+' +stdout 'HEIGHT: \d+' +stdout 'EVENTS: \[\]' +stdout 'TX HASH: ' + +# Query sponsor's account after the transaction to see gas deduction +gnokey query auth/accounts/${USER_ADDR_sponsor} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9999999ugnot",' # 1 ugnot gas deducted from sponsor's balance +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query sponsoree's account after the transaction +gnokey query auth/accounts/${USER_ADDR_sponsoree} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "9999999ugnot",' # Sponsoree sent 1 ugnot, so balance reduced +stdout ' "public_key": {' +stdout ' "@type": "/tm.PubKeySecp256k1",' +stdout ' "value": ' +stdout ' },' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "1"' # Sequence incremented after transaction +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + +# Query receipent's account after the transaction +gnokey query auth/accounts/${USER_ADDR_receipent} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": ' +stdout ' "coins": "10000001ugnot",' # Recipient's balance increased by 1 ugnot +stdout ' "public_key": null,' +stdout ' "account_number": "\d+",' +stdout ' "sequence": "0"' # Sequence remains unchanged + +stdout ' }' +stdout '}' +! stderr '.+' # Ensure no errors + + +-- sign.stdout.golden -- + +Tx successfully signed and saved to $WORK/sponsor.tx diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index af57440d61e..847526f4df5 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -1,7 +1,12 @@ package gnoclient import ( + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" ) // Client provides an interface for interacting with the blockchain. @@ -10,11 +15,50 @@ type Client struct { RPCClient rpcclient.Client // RPC client for blockchain communication } +// Public Client's interface +type IClient interface { + Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) + QueryAccount(addr crypto.Address) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) + QueryAppVersion() (string, *ctypes.ResultABCIQuery, error) + Render(pkgPath string, args string) (string, *ctypes.ResultABCIQuery, error) + QEval(pkgPath string, expression string) (string, *ctypes.ResultABCIQuery, error) + Block(height int64) (*ctypes.ResultBlock, error) + BlockResult(height int64) (*ctypes.ResultBlockResults, error) + LatestBlockHeight() (int64, error) + + Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) + Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) + Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) + AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) + + SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) + BroadcastTx(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) + + NewSponsorTransaction(cfg SponsorTxCfg, msgs ...std.Msg) (*std.Tx, error) + ExecuteSponsorTransaction(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) +} + +var _ IClient = (*Client)(nil) + +// validate checks that the Client's fields are correctly configured. +func (c *Client) validate() error { + if err := c.validateSigner(); err != nil { + return err + } + + if err := c.validateRPCClient(); err != nil { + return err + } + + return nil +} + // validateSigner checks that the signer is correctly configured. func (c *Client) validateSigner() error { if c.Signer == nil { return ErrMissingSigner } + return nil } @@ -23,5 +67,6 @@ func (c *Client) validateRPCClient() error { if c.RPCClient == nil { return ErrMissingRPCClient } + return nil } diff --git a/gno.land/pkg/gnoclient/client_queries_test.go b/gno.land/pkg/gnoclient/client_queries_test.go new file mode 100644 index 00000000000..5119500842f --- /dev/null +++ b/gno.land/pkg/gnoclient/client_queries_test.go @@ -0,0 +1,165 @@ +package gnoclient + +import ( + "testing" + + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBlockResults(t *testing.T) { + t.Parallel() + + height := int64(5) + client := &Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{ + blockResults: func(height *int64) (*ctypes.ResultBlockResults, error) { + return &ctypes.ResultBlockResults{ + Height: *height, + Results: nil, + }, nil + }, + }, + } + + blockResult, err := client.BlockResult(height) + require.NoError(t, err) + assert.Equal(t, height, blockResult.Height) +} + +func TestLatestBlockHeight(t *testing.T) { + t.Parallel() + + latestHeight := int64(5) + + client := &Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{ + status: func() (*ctypes.ResultStatus, error) { + return &ctypes.ResultStatus{ + SyncInfo: ctypes.SyncInfo{ + LatestBlockHeight: latestHeight, + }, + }, nil + }, + }, + } + + head, err := client.LatestBlockHeight() + require.NoError(t, err) + assert.Equal(t, latestHeight, head) +} + +func TestBlockErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + height int64 + expectedError error + }{ + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + height: 1, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid height", + client: Client{ + &mockSigner{}, + &mockRPCClient{}, + }, + height: 0, + expectedError: ErrInvalidBlockHeight, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.Block(tc.height) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +func TestBlockResultErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + height int64 + expectedError error + }{ + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + height: 1, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid height", + client: Client{ + &mockSigner{}, + &mockRPCClient{}, + }, + height: 0, + expectedError: ErrInvalidBlockHeight, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.BlockResult(tc.height) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +func TestLatestBlockHeightErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + expectedError error + }{ + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + expectedError: ErrMissingRPCClient, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.LatestBlockHeight() + assert.Equal(t, int64(0), res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go deleted file mode 100644 index b7eb21837a7..00000000000 --- a/gno.land/pkg/gnoclient/client_test.go +++ /dev/null @@ -1,1410 +0,0 @@ -package gnoclient - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - "github.com/gnolang/gno/tm2/pkg/std" -) - -var testGasFee = ugnot.ValueString(10000) - -func TestRender(t *testing.T) { - t.Parallel() - testRealmPath := "gno.land/r/demo/deep/very/deep" - expectedRender := []byte("it works!") - - client := Client{ - Signer: &mockSigner{ - sign: func(cfg SignCfg) (*std.Tx, error) { - return &std.Tx{}, nil - }, - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{ - abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - res := &ctypes.ResultABCIQuery{ - Response: abci.ResponseQuery{ - ResponseBase: abci.ResponseBase{ - Data: expectedRender, - }, - }, - } - return res, nil - }, - }, - } - - res, data, err := client.Render(testRealmPath, "") - assert.NoError(t, err) - assert.NotEmpty(t, data.Response.Data) - assert.NotEmpty(t, res) - assert.Equal(t, data.Response.Data, expectedRender) -} - -// Call tests -func TestCallSingle(t *testing.T) { - t.Parallel() - - client := Client{ - Signer: &mockSigner{ - sign: func(cfg SignCfg) (*std.Tx, error) { - return &std.Tx{}, nil - }, - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{ - broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res := &ctypes.ResultBroadcastTxCommit{ - DeliverTx: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Data: []byte("it works!"), - }, - }, - } - return res, nil - }, - }, - } - - cfg := BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - } - - caller, err := client.Signer.Info() - require.NoError(t, err) - - msg := []vm.MsgCall{ - { - Caller: caller.GetAddress(), - PkgPath: "gno.land/r/demo/deep/very/deep", - Func: "Render", - Args: []string{""}, - Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}}, - }, - } - - res, err := client.Call(cfg, msg...) - assert.NoError(t, err) - require.NotNil(t, res) - expected := "it works!" - assert.Equal(t, string(res.DeliverTx.Data), expected) - - res, err = callSigningSeparately(t, client, cfg, msg...) - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, string(res.DeliverTx.Data), expected) -} - -func TestCallMultiple(t *testing.T) { - t.Parallel() - - client := Client{ - Signer: &mockSigner{ - sign: func(cfg SignCfg) (*std.Tx, error) { - return &std.Tx{}, nil - }, - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{ - broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res := &ctypes.ResultBroadcastTxCommit{ - CheckTx: abci.ResponseCheckTx{ - ResponseBase: abci.ResponseBase{ - Error: nil, - Data: nil, - Events: nil, - Log: "", - Info: "", - }, - }, - } - - return res, nil - }, - }, - } - - cfg := BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - } - - caller, err := client.Signer.Info() - require.NoError(t, err) - - msg := []vm.MsgCall{ - { - Caller: caller.GetAddress(), - PkgPath: "gno.land/r/demo/deep/very/deep", - Func: "Render", - Args: []string{""}, - Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}}, - }, - { - Caller: caller.GetAddress(), - PkgPath: "gno.land/r/demo/wugnot", - Func: "Deposit", - Args: []string{""}, - Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(1000)}}, - }, - { - Caller: caller.GetAddress(), - PkgPath: "gno.land/r/demo/tamagotchi", - Func: "Feed", - Args: []string{""}, - Send: nil, - }, - } - - res, err := client.Call(cfg, msg...) - assert.NoError(t, err) - assert.NotNil(t, res) - - res, err = callSigningSeparately(t, client, cfg, msg...) - assert.NoError(t, err) - assert.NotNil(t, res) -} - -func TestCallErrors(t *testing.T) { - t.Parallel() - - // These tests don't actually sign - mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - - testCases := []struct { - name string - client Client - cfg BaseTxCfg - msgs []vm.MsgCall - expectedError string - }{ - { - name: "Invalid Signer", - client: Client{ - Signer: nil, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "RandomName", - Send: nil, - Args: []string{}, - }, - }, - expectedError: ErrMissingSigner.Error(), - }, - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "RandomName", - Send: nil, - Args: []string{}, - }, - }, - expectedError: ErrMissingRPCClient.Error(), - }, - { - name: "Invalid Gas Fee", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: "", - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "RandomName", - }, - }, - expectedError: ErrInvalidGasFee.Error(), - }, - { - name: "Negative Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: -1, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "RandomName", - Send: nil, - Args: []string{}, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "0 Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 0, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "RandomName", - Send: nil, - Args: []string{}, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "Invalid PkgPath", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "", - Func: "RandomName", - Send: nil, - Args: []string{}, - }, - }, - expectedError: vm.InvalidPkgPathError{}.Error(), - }, - { - name: "Invalid FuncName", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgCall{ - { - Caller: mockAddress, - PkgPath: "gno.land/r/random/path", - Func: "", - Send: nil, - Args: []string{}, - }, - }, - expectedError: vm.InvalidExprError{}.Error(), - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.Call(tc.cfg, tc.msgs...) - assert.Nil(t, res) - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} - -func TestClient_Send_Errors(t *testing.T) { - t.Parallel() - - // These tests don't actually sign - mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - - toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") - testCases := []struct { - name string - client Client - cfg BaseTxCfg - msgs []bank.MsgSend - expectedError string - }{ - { - name: "Invalid Signer", - client: Client{ - Signer: nil, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: ErrMissingSigner.Error(), - }, - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: ErrMissingRPCClient.Error(), - }, - { - name: "Invalid Gas Fee", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: "", - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: ErrInvalidGasFee.Error(), - }, - { - name: "Negative Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: -1, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "0 Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 0, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "Invalid To Address", - client: Client{ - Signer: &mockSigner{ - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: crypto.Address{}, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, - }, - }, - expectedError: std.InvalidAddressError{}.Error(), - }, - { - name: "Invalid Send Coins", - client: Client{ - Signer: &mockSigner{ - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []bank.MsgSend{ - { - FromAddress: mockAddress, - ToAddress: toAddress, - Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(-1)}}, - }, - }, - expectedError: std.InvalidCoinsError{}.Error(), - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.Send(tc.cfg, tc.msgs...) - assert.Nil(t, res) - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} - -// Run tests -func TestRunSingle(t *testing.T) { - t.Parallel() - - client := Client{ - Signer: &mockSigner{ - sign: func(cfg SignCfg) (*std.Tx, error) { - return &std.Tx{}, nil - }, - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{ - broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res := &ctypes.ResultBroadcastTxCommit{ - DeliverTx: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Data: []byte("hi gnoclient!\n"), - }, - }, - } - return res, nil - }, - }, - } - - cfg := BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - } - - fileBody := `package main -import ( - "std" - "gno.land/p/demo/ufmt" - "gno.land/r/demo/deep/very/deep" -) -func main() { - println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) -}` - - caller, err := client.Signer.Info() - require.NoError(t, err) - - msg := vm.MsgRun{ - Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ - { - Name: "main.gno", - Body: fileBody, - }, - }, - }, - Send: nil, - } - - res, err := client.Run(cfg, msg) - assert.NoError(t, err) - require.NotNil(t, res) - expected := "hi gnoclient!\n" - assert.Equal(t, expected, string(res.DeliverTx.Data)) - - res, err = runSigningSeparately(t, client, cfg, msg) - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, expected, string(res.DeliverTx.Data)) -} - -func TestRunMultiple(t *testing.T) { - t.Parallel() - - client := Client{ - Signer: &mockSigner{ - sign: func(cfg SignCfg) (*std.Tx, error) { - return &std.Tx{}, nil - }, - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{ - broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res := &ctypes.ResultBroadcastTxCommit{ - DeliverTx: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Data: []byte("hi gnoclient!\nhi gnoclient!\n"), - }, - }, - } - return res, nil - }, - }, - } - - cfg := BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - } - - fileBody := `package main -import ( - "std" - "gno.land/p/demo/ufmt" - "gno.land/r/demo/deep/very/deep" -) -func main() { - println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) -}` - - caller, err := client.Signer.Info() - require.NoError(t, err) - - msg1 := vm.MsgRun{ - Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ - { - Name: "main1.gno", - Body: fileBody, - }, - }, - }, - Send: nil, - } - - msg2 := vm.MsgRun{ - Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ - { - Name: "main2.gno", - Body: fileBody, - }, - }, - }, - Send: nil, - } - - res, err := client.Run(cfg, msg1, msg2) - assert.NoError(t, err) - require.NotNil(t, res) - expected := "hi gnoclient!\nhi gnoclient!\n" - assert.Equal(t, expected, string(res.DeliverTx.Data)) - - res, err = runSigningSeparately(t, client, cfg, msg1, msg2) - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, expected, string(res.DeliverTx.Data)) -} - -func TestRunErrors(t *testing.T) { - t.Parallel() - - // These tests don't actually sign - mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - - testCases := []struct { - name string - client Client - cfg BaseTxCfg - msgs []vm.MsgRun - expectedError string - }{ - { - name: "Invalid Signer", - client: Client{ - Signer: nil, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{ - { - Caller: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Send: nil, - }, - }, - expectedError: ErrMissingSigner.Error(), - }, - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{}, - expectedError: ErrMissingRPCClient.Error(), - }, - { - name: "Invalid Gas Fee", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: "", - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{ - { - Caller: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Send: nil, - }, - }, - expectedError: ErrInvalidGasFee.Error(), - }, - { - name: "Negative Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: -1, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{ - { - Caller: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Send: nil, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "0 Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 0, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{ - { - Caller: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Send: nil, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "Invalid Empty Package", - client: Client{ - Signer: &mockSigner{ - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgRun{ - { - Caller: mockAddress, - Package: &std.MemPackage{Name: "", Path: " "}, - Send: nil, - }, - }, - expectedError: vm.InvalidPkgPathError{}.Error(), - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.Run(tc.cfg, tc.msgs...) - assert.Nil(t, res) - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} - -// AddPackage tests -func TestAddPackageErrors(t *testing.T) { - t.Parallel() - - // These tests don't actually sign - mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - - testCases := []struct { - name string - client Client - cfg BaseTxCfg - msgs []vm.MsgAddPackage - expectedError string - }{ - { - name: "Invalid Signer", - client: Client{ - Signer: nil, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{ - { - Creator: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Deposit: nil, - }, - }, - expectedError: ErrMissingSigner.Error(), - }, - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{}, - expectedError: ErrMissingRPCClient.Error(), - }, - { - name: "Invalid Gas Fee", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: "", - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{ - { - Creator: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Deposit: nil, - }, - }, - expectedError: ErrInvalidGasFee.Error(), - }, - { - name: "Negative Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: -1, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{ - { - Creator: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Deposit: nil, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "0 Gas Wanted", - client: Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 0, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{ - { - Creator: mockAddress, - Package: &std.MemPackage{ - Name: "", - Path: "", - Files: []*std.MemFile{ - { - Name: "file1.gno", - Body: "", - }, - }, - }, - Deposit: nil, - }, - }, - expectedError: ErrInvalidGasWanted.Error(), - }, - { - name: "Invalid Empty Package", - client: Client{ - Signer: &mockSigner{ - info: func() (keys.Info, error) { - return &mockKeysInfo{ - getAddress: func() crypto.Address { - adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - return adr - }, - }, nil - }, - }, - RPCClient: &mockRPCClient{}, - }, - cfg: BaseTxCfg{ - GasWanted: 100000, - GasFee: testGasFee, - AccountNumber: 1, - SequenceNumber: 1, - Memo: "Test memo", - }, - msgs: []vm.MsgAddPackage{ - { - Creator: mockAddress, - Package: &std.MemPackage{Name: "", Path: ""}, - Deposit: nil, - }, - }, - expectedError: vm.InvalidPkgPathError{}.Error(), - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.AddPackage(tc.cfg, tc.msgs...) - assert.Nil(t, res) - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} - -// Block tests -func TestBlock(t *testing.T) { - t.Parallel() - - height := int64(5) - client := &Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{ - block: func(height *int64) (*ctypes.ResultBlock, error) { - return &ctypes.ResultBlock{ - BlockMeta: &types.BlockMeta{ - BlockID: types.BlockID{}, - Header: types.Header{}, - }, - Block: &types.Block{ - Header: types.Header{ - Height: *height, - }, - Data: types.Data{}, - LastCommit: nil, - }, - }, nil - }, - }, - } - - block, err := client.Block(height) - require.NoError(t, err) - assert.Equal(t, height, block.Block.GetHeight()) -} - -func TestBlockResults(t *testing.T) { - t.Parallel() - - height := int64(5) - client := &Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{ - blockResults: func(height *int64) (*ctypes.ResultBlockResults, error) { - return &ctypes.ResultBlockResults{ - Height: *height, - Results: nil, - }, nil - }, - }, - } - - blockResult, err := client.BlockResult(height) - require.NoError(t, err) - assert.Equal(t, height, blockResult.Height) -} - -func TestLatestBlockHeight(t *testing.T) { - t.Parallel() - - latestHeight := int64(5) - - client := &Client{ - Signer: &mockSigner{}, - RPCClient: &mockRPCClient{ - status: func() (*ctypes.ResultStatus, error) { - return &ctypes.ResultStatus{ - SyncInfo: ctypes.SyncInfo{ - LatestBlockHeight: latestHeight, - }, - }, nil - }, - }, - } - - head, err := client.LatestBlockHeight() - require.NoError(t, err) - assert.Equal(t, latestHeight, head) -} - -func TestBlockErrors(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - client Client - height int64 - expectedError error - }{ - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - height: 1, - expectedError: ErrMissingRPCClient, - }, - { - name: "Invalid height", - client: Client{ - &mockSigner{}, - &mockRPCClient{}, - }, - height: 0, - expectedError: ErrInvalidBlockHeight, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.Block(tc.height) - assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) - }) - } -} - -func TestBlockResultErrors(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - client Client - height int64 - expectedError error - }{ - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - height: 1, - expectedError: ErrMissingRPCClient, - }, - { - name: "Invalid height", - client: Client{ - &mockSigner{}, - &mockRPCClient{}, - }, - height: 0, - expectedError: ErrInvalidBlockHeight, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.BlockResult(tc.height) - assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) - }) - } -} - -func TestLatestBlockHeightErrors(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - client Client - expectedError error - }{ - { - name: "Invalid RPCClient", - client: Client{ - &mockSigner{}, - nil, - }, - expectedError: ErrMissingRPCClient, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - res, err := tc.client.LatestBlockHeight() - assert.Equal(t, int64(0), res) - assert.ErrorIs(t, err, tc.expectedError) - }) - } -} - -// The same as client.Call, but test signing separately -func callSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { - t.Helper() - tx, err := NewCallTx(cfg, msgs...) - assert.NoError(t, err) - require.NotNil(t, tx) - signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) - assert.NoError(t, err) - require.NotNil(t, signedTx) - res, err := client.BroadcastTxCommit(signedTx) - assert.NoError(t, err) - require.NotNil(t, res) - return res, nil -} - -// The same as client.Run, but test signing separately -func runSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { - t.Helper() - tx, err := NewRunTx(cfg, msgs...) - assert.NoError(t, err) - require.NotNil(t, tx) - signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) - assert.NoError(t, err) - require.NotNil(t, signedTx) - res, err := client.BroadcastTxCommit(signedTx) - assert.NoError(t, err) - require.NotNil(t, res) - return res, nil -} - -// The same as client.Send, but test signing separately -func sendSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { - t.Helper() - tx, err := NewSendTx(cfg, msgs...) - assert.NoError(t, err) - require.NotNil(t, tx) - signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) - assert.NoError(t, err) - require.NotNil(t, signedTx) - res, err := client.BroadcastTxCommit(signedTx) - assert.NoError(t, err) - require.NotNil(t, res) - return res, nil -} - -// The same as client.AddPackage, but test signing separately -func addPackageSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { - t.Helper() - tx, err := NewAddPackageTx(cfg, msgs...) - assert.NoError(t, err) - require.NotNil(t, tx) - signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) - assert.NoError(t, err) - require.NotNil(t, signedTx) - res, err := client.BroadcastTxCommit(signedTx) - assert.NoError(t, err) - require.NotNil(t, res) - return res, nil -} diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index 9d3dbde22ae..55965c55f1a 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -9,29 +9,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -var ( - ErrInvalidGasWanted = errors.New("invalid gas wanted") - ErrInvalidGasFee = errors.New("invalid gas fee") - ErrMissingSigner = errors.New("missing Signer") - ErrMissingRPCClient = errors.New("missing RPCClient") -) - -// BaseTxCfg defines the base transaction configuration, shared by all message types -type BaseTxCfg struct { - GasFee string // Gas fee - GasWanted int64 // Gas wanted - AccountNumber uint64 // Account number - SequenceNumber uint64 // Sequence number - Memo string // Memo -} - // Call executes one or more MsgCall calls on the blockchain func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, err - } - if err := c.validateRPCClient(); err != nil { + if err := c.validate(); err != nil { return nil, err } @@ -39,6 +20,7 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas if err != nil { return nil, err } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) } @@ -46,7 +28,7 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas // The Caller field must be set. func NewCallTx(cfg BaseTxCfg, msgs ...vm.MsgCall) (*std.Tx, error) { // Validate base transaction config - if err := cfg.validateBaseTxConfig(); err != nil { + if err := cfg.validate(); err != nil { return nil, err } @@ -78,10 +60,7 @@ func NewCallTx(cfg BaseTxCfg, msgs ...vm.MsgCall) (*std.Tx, error) { // Run executes one or more MsgRun calls on the blockchain func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, err - } - if err := c.validateRPCClient(); err != nil { + if err := c.validate(); err != nil { return nil, err } @@ -89,6 +68,7 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT if err != nil { return nil, err } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) } @@ -96,7 +76,7 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT // The Caller field must be set. func NewRunTx(cfg BaseTxCfg, msgs ...vm.MsgRun) (*std.Tx, error) { // Validate base transaction config - if err := cfg.validateBaseTxConfig(); err != nil { + if err := cfg.validate(); err != nil { return nil, err } @@ -128,10 +108,7 @@ func NewRunTx(cfg BaseTxCfg, msgs ...vm.MsgRun) (*std.Tx, error) { // Send executes one or more MsgSend calls on the blockchain func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, err - } - if err := c.validateRPCClient(); err != nil { + if err := c.validate(); err != nil { return nil, err } @@ -139,6 +116,7 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc if err != nil { return nil, err } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) } @@ -146,7 +124,7 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc // The FromAddress field must be set. func NewSendTx(cfg BaseTxCfg, msgs ...bank.MsgSend) (*std.Tx, error) { // Validate base transaction config - if err := cfg.validateBaseTxConfig(); err != nil { + if err := cfg.validate(); err != nil { return nil, err } @@ -178,10 +156,7 @@ func NewSendTx(cfg BaseTxCfg, msgs ...bank.MsgSend) (*std.Tx, error) { // AddPackage executes one or more AddPackage calls on the blockchain func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, err - } - if err := c.validateRPCClient(); err != nil { + if err := c.validate(); err != nil { return nil, err } @@ -189,6 +164,7 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re if err != nil { return nil, err } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) } @@ -196,7 +172,7 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re // The Creator field must be set. func NewAddPackageTx(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*std.Tx, error) { // Validate base transaction config - if err := cfg.validateBaseTxConfig(); err != nil { + if err := cfg.validate(); err != nil { return nil, err } @@ -225,50 +201,114 @@ func NewAddPackageTx(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*std.Tx, error) { }, nil } -// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result -func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { - signedTx, err := c.SignTx(tx, accountNumber, sequenceNumber) - if err != nil { +// CreateTx creates an signed transaction for various types of messages which used for sponsorship +func (c *Client) NewSponsorTransaction(cfg SponsorTxCfg, msgs ...std.Msg) (*std.Tx, error) { + // validate required client fields + if err := c.validate(); err != nil { return nil, err } - return c.BroadcastTxCommit(signedTx) -} -// SignTx signs a transaction and returns a signed tx ready for broadcasting. -// If accountNumber or sequenceNumber is 0 then query the blockchain for the value. -func (c *Client) SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) { - if err := c.validateSigner(); err != nil { + // Validate sponsor transaction config + if err := cfg.validate(); err != nil { return nil, err } - caller, err := c.Signer.Info() + + // Ensure at least one message is provided + if len(msgs) == 0 { + return nil, ErrNoMessages + } + + // Determine the type of the first user-provided message + firstMsgType := msgs[0].Type() + + vmMsgs := make([]std.Msg, 0, len(msgs)+1) + + // First msg in list must be MsgNoop + vmMsgs = append(vmMsgs, vm.MsgNoop{ + Caller: cfg.SponsorAddress, + }) + + for _, msg := range msgs { + // Check if all messages are of the same type + if msg.Type() != firstMsgType { + return nil, ErrMixedMessageTypes + } + + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + vmMsgs = append(vmMsgs, msg) + } + + // Parse gas fee + gasFeeCoins, err := std.ParseCoin(cfg.GasFee) if err != nil { return nil, err } - if sequenceNumber == 0 || accountNumber == 0 { - account, _, err := c.QueryAccount(caller.GetAddress()) - if err != nil { - return nil, errors.Wrap(err, "query account") - } - accountNumber = account.AccountNumber - sequenceNumber = account.Sequence + // Pack transaction + tx := &std.Tx{ + Msgs: vmMsgs, + Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), + Signatures: nil, + Memo: cfg.Memo, } + return tx, nil +} + +// SignTx signs a transaction using the client's signer +func (c *Client) SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) { + // Ensure sequence number and account number are provided signCfg := SignCfg{ - UnsignedTX: tx, + Tx: tx, SequenceNumber: sequenceNumber, AccountNumber: accountNumber, } + signedTx, err := c.Signer.Sign(signCfg) if err != nil { return nil, errors.Wrap(err, "sign") } + return signedTx, nil } -// BroadcastTxCommit marshals and broadcasts the signed transaction, returning the result. +// ExecuteSponsorTransaction allows broadcasting a pre-signed transaction (represented by `sponsorTx`) +// using the signer's account to pay transaction fees. The `sponsoree` account who signed `the sponsorTx“ before benefits +// from this transaction without incurring any gas costs +func (c *Client) ExecuteSponsorTransaction(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + // Validate required client fields + if err := c.validate(); err != nil { + return nil, err + } + + // Validate basic transaction + if err := tx.ValidateBasic(); err != nil { + return nil, err + } + + // Ensure tx is a sponsor transaction + if !tx.IsSponsorTx() { + return nil, ErrInvalidSponsorTx + } + + return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) +} + +// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result +func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + signedTx, err := c.SignTx(tx, accountNumber, sequenceNumber) + if err != nil { + return nil, err + } + return c.BroadcastTx(signedTx) +} + +// BroadcastTx marshals and broadcasts the signed transaction, returning the result. // If the result has a delivery error, then return a wrapped error. -func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) BroadcastTx(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) { if err := c.validateRPCClient(); err != nil { return nil, err } @@ -291,5 +331,3 @@ func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxC return bres, nil } - -// TODO: Add more functionality, examples, and unit tests. diff --git a/gno.land/pkg/gnoclient/client_txs_test.go b/gno.land/pkg/gnoclient/client_txs_test.go new file mode 100644 index 00000000000..38c479131fb --- /dev/null +++ b/gno.land/pkg/gnoclient/client_txs_test.go @@ -0,0 +1,2528 @@ +package gnoclient + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var ( + addr1 = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + addr2 = crypto.MustAddressFromString("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +) + +func TestRender(t *testing.T) { + t.Parallel() + testRealmPath := "gno.land/r/demo/deep/very/deep" + + expectedRender := []byte("hi gnoclient!\n") + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + res := &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + ResponseBase: abci.ResponseBase{ + Data: expectedRender, + }, + }, + } + return res, nil + }, + }, + } + + res, data, err := client.Render(testRealmPath, "") + assert.NoError(t, err) + assert.NotEmpty(t, data.Response.Data) + assert.NotEmpty(t, res) + assert.Equal(t, data.Response.Data, expectedRender) +} + +// Call tests +func TestCallSingle(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := []vm.MsgCall{ + { + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + } + + res, err := client.Call(cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestCallSingle_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + tx, err := client.NewSponsorTransaction(cfg, msg) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestCallMultiple(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := []vm.MsgCall{ + { + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + { + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/wugnot", + Func: "Deposit", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + { + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/tamagotchi", + Func: "Feed", + Args: []string{""}, + Send: nil, + }, + } + + res, err := client.Call(cfg, msg...) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestCallMultiple_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + msg2 := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/wugnot", + Func: "Deposit", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + msg3 := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/tamagotchi", + Func: "Feed", + Args: []string{""}, + Send: nil, + } + + tx, err := client.NewSponsorTransaction(cfg, msg1, msg2, msg3) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestCallErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg BaseTxCfg + msgs []vm.MsgCall + expectedError error + }{ + { + name: "Invalid Signer", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, + }, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, + }, + }, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid Gas Fee", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + PkgPath: "random/path", + Func: "RandomName", + }, + }, + expectedError: ErrInvalidGasFee, + }, + { + name: "Negative Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "0 Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 0, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "Invalid PkgPath", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "", + Func: "RandomName", + Send: nil, + Args: []string{}, + }, + }, + expectedError: vm.InvalidPkgPathError{}, + }, + { + name: "Invalid FuncName", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgCall{ + { + Caller: addr1, + PkgPath: "gno.land/r/random/path", + Func: "", + Send: nil, + Args: []string{}, + }, + }, + expectedError: vm.InvalidExprError{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.Call(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +// Send tests +func TestSendSingle(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + receiver, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") + + msg := []bank.MsgSend{ + { + FromAddress: caller.GetAddress(), + ToAddress: receiver, + Amount: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + } + + res, err := client.Send(cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = sendSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestSendSingle_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + receiver, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") + + msg := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: receiver, + Amount: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + tx, err := client.NewSponsorTransaction(cfg, msg) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestSendMultiple(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + msg2 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 200}}, + } + + res, err := client.Send(cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = sendSigningSeparately(t, client, cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestSendMultiple_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + receiver, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") + + msg1 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: receiver, + Amount: std.Coins{{Denom: "ugnot", Amount: 100}}, + } + + msg2 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: receiver, + Amount: std.Coins{{Denom: "ugnot", Amount: 200}}, + } + + tx, err := client.NewSponsorTransaction(cfg, msg1, msg2) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestSendErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg BaseTxCfg + msgs []bank.MsgSend + expectedError error + }{ + { + name: "Invalid Signer", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid Gas Fee", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: ErrInvalidGasFee, + }, + { + name: "Negative Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "0 Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 0, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "Invalid To Address", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: crypto.Address{}, + Amount: std.Coins{{Denom: "ugnot", Amount: 1}}, + }, + }, + expectedError: std.InvalidAddressError{}, + }, + { + name: "Invalid Send Coins", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []bank.MsgSend{ + { + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: -1}}, + }, + }, + expectedError: std.InvalidCoinsError{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.Send(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +// Run tests +func TestRunSingle(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + res, err := client.Run(cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestRunSingle_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte("hi gnoclient!\n"), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + tx, err := client.NewSponsorTransaction(cfg, msg) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestRunMultiple(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main1.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + msg2 := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main2.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + res, err := client.Run(cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestRunMultiple_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main1.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + msg2 := vm.MsgRun{ + Caller: caller.GetAddress(), + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main2.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + tx, err := client.NewSponsorTransaction(cfg, msg1, msg2) + assert.NoError(t, err) + + presignedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*presignedTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestRunErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg BaseTxCfg + msgs []vm.MsgRun + expectedError error + }{ + { + name: "Invalid Signer", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{ + { + Caller: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: nil, + }, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{}, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid Gas Fee", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{ + { + Caller: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: nil, + }, + }, + expectedError: ErrInvalidGasFee, + }, + { + name: "Negative Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{ + { + Caller: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: nil, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "0 Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 0, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{ + { + Caller: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: nil, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "Invalid Empty Package", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgRun{ + { + Caller: addr1, + Package: &std.MemPackage{ + Name: "", + Path: " ", + }, + Send: nil, + }, + }, + expectedError: vm.InvalidPkgPathError{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.Run(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +// AddPackage tests +func TestAddPackageSingle(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "hello", + Path: "gno.land/p/demo/hello", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + } + + res, err := client.AddPackage(cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = addPackageSigningSeparately(t, client, cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestAddPackageSingle_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "hello", + Path: "gno.land/p/demo/hello", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + } + + tx, err := client.NewSponsorTransaction(cfg, msg) + assert.NoError(t, err) + + sponsorTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*sponsorTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestAddPackageMultiple(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msgs := []vm.MsgAddPackage{ + { + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "hello", + Path: "gno.land/p/demo/hello", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + { + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "goodbye", + Path: "gno.land/p/demo/goodbye", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + } + + res, err := client.AddPackage(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = addPackageSigningSeparately(t, client, cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestAddPackageMultiple_Sponsor(t *testing.T) { + t.Parallel() + + expected := "hi gnoclient!\n" + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + cfg.Tx.Signatures = make([]std.Signature, 2) + return &cfg.Tx, nil + }, + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte(expected), + }, + }, + } + return res, nil + }, + }, + } + + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + } + + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgAddPackage{ + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "hello", + Path: "gno.land/p/demo/hello", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + } + + msg2 := vm.MsgAddPackage{ + Creator: caller.GetAddress(), + Package: &std.MemPackage{ + Name: "goodbye", + Path: "gno.land/p/demo/goodbye", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + } + + tx, err := client.NewSponsorTransaction(cfg, msg1, msg2) + assert.NoError(t, err) + + sponsorTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + res, err := client.ExecuteSponsorTransaction(*sponsorTx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestAddPackageErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg BaseTxCfg + msgs []vm.MsgAddPackage + expectedError error + }{ + { + name: "Invalid Signer", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{ + { + Creator: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{}, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid Gas Fee", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{ + { + Creator: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + }, + expectedError: ErrInvalidGasFee, + }, + { + name: "Negative Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{ + { + Creator: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "0 Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 0, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{ + { + Creator: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Deposit: nil, + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "Invalid Empty Package", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []vm.MsgAddPackage{ + { + Creator: addr1, + Package: &std.MemPackage{ + Name: "", + Path: "", + }, + Deposit: nil, + }, + }, + expectedError: vm.InvalidPkgPathError{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.AddPackage(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} + +func TestNewSponsorTransaction(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg SponsorTxCfg + msgs []std.Msg + expectedError error + }{ + { + name: "Invalid Client", + client: Client{ + Signer: nil, // invalid signer + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid SponsorTxCfg", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: crypto.Address{}, // invalid sponsor address + }, + expectedError: ErrInvalidSponsorAddress, + }, + { + name: "Empty message list", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + + msgs: []std.Msg{}, // no messages provided + + expectedError: ErrNoMessages, + }, + { + name: "Signer not found", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return nil, errors.New("failed to get signer info") // signer not found + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + + msgs: []std.Msg{}, // no messages provided + + expectedError: ErrNoMessages, + }, + { + name: "All messages aren't the same type", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + + // MixedMessage is invalid + msgs: []std.Msg{ + vm.MsgCall{ + Caller: addr1, + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + bank.MsgSend{ + FromAddress: addr1, + ToAddress: addr2, + Amount: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + }, + expectedError: ErrMixedMessageTypes, + }, + { + name: "At least one invalid message", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + msgs: []std.Msg{ + // invalid message send + bank.MsgSend{ + FromAddress: addr1, + ToAddress: crypto.Address{}, + Amount: std.Coins{{Denom: "ugnot", Amount: 10000}}, + }, + }, + expectedError: std.InvalidAddressError{}, + }, + { + name: "Failed to parse gas fee", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "xxx", // invalid gas fee + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + SponsorAddress: addr2, + }, + msgs: []std.Msg{ + vm.MsgCall{ + Caller: addr1, + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: "ugnot", Amount: 100}}, + }, + }, + expectedError: errors.New("invalid coin expression: xxx"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.NewSponsorTransaction(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.Equal(t, tc.expectedError.Error(), err.Error()) + }) + } +} + +func TestSignTx(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + tx std.Tx + expectedError error + }{ + { + name: "Failed to sign transaction", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + sign: func(cfg SignCfg) (*std.Tx, error) { + return nil, errors.New("failed to sign transaction") + }, + }, + RPCClient: &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + acc := std.NewBaseAccount(addr1, nil, nil, 0, 0) + accData, _ := amino.MarshalJSON(acc) + + return &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + ResponseBase: abci.ResponseBase{ + Data: accData, + }, + }, + }, nil + }, + }, + }, + tx: std.Tx{}, + expectedError: errors.New("failed to sign transaction"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.SignTx(tc.tx, 0, 0) + assert.Nil(t, res) + assert.Equal(t, tc.expectedError.Error(), err.Error()) + }) + } +} + +func TestExecuteSponsorTransaction(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + tx std.Tx + expectedError error + }{ + { + name: "Invalid Client", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + tx: std.Tx{}, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid transaction", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + tx: std.Tx{ + Fee: std.NewFee(1000, std.NewCoin("ugnot", 10)), + Msgs: []std.Msg{ + vm.MsgCall{ + Caller: addr1, + }, + }, + Signatures: []std.Signature{}, // no signatures provided + }, + expectedError: errors.New("no signatures error"), + }, + { + name: "tx is not a sponsor transaction", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + tx: std.Tx{ + Fee: std.NewFee(1000, std.NewCoin("ugnot", 10)), + Msgs: []std.Msg{ // missing noop msg + bank.MsgSend{ + FromAddress: addr1, + ToAddress: addr2, + Amount: std.NewCoins(std.NewCoin("gnot", 1000)), + }, + }, + Signatures: []std.Signature{ + { + PubKey: nil, + Signature: nil, + }, + }, + }, + expectedError: ErrInvalidSponsorTx, + }, + { + name: "signAndBroadcastTxCommit error", + client: Client{ + Signer: &mockSigner{ + info: func() (keys.Info, error) { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + return addr1 + }, + }, nil + }, + sign: func(cfg SignCfg) (*std.Tx, error) { + return nil, errors.New("failed to sign tx") // failed to sign tx + }, + }, + RPCClient: &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + acc := std.NewBaseAccount(addr1, std.NewCoins(), nil, 0, 0) + accData, _ := amino.MarshalJSON(acc) + + return &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + ResponseBase: abci.ResponseBase{ + Data: accData, + }, + }, + }, nil + }, + }, + }, + tx: std.Tx{ + Fee: std.NewFee(1000, std.NewCoin("ugnot", 10)), + Msgs: []std.Msg{ + vm.MsgNoop{ + Caller: addr2, + }, + bank.MsgSend{ + FromAddress: addr1, + ToAddress: addr2, + Amount: std.NewCoins(std.NewCoin("gnot", 1000)), + }, + }, + Signatures: []std.Signature{ + { + PubKey: nil, + Signature: nil, + }, + { + PubKey: nil, + Signature: nil, + }, + }, + }, + expectedError: errors.New("failed to sign tx"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.ExecuteSponsorTransaction(tc.tx, 0, 0) + assert.Nil(t, res) + assert.Equal(t, tc.expectedError.Error(), err.Error()) + }) + } +} + +// The same as client.Call, but test signing separately +func callSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewCallTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTx(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Run, but test signing separately +func runSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewRunTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTx(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Send, but test signing separately +func sendSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewSendTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTx(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.AddPackage, but test signing separately +func addPackageSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewAddPackageTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTx(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index ea068e0680b..4869d4811c7 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -14,12 +14,14 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Call tests func TestCallSingle_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) @@ -27,7 +29,9 @@ func TestCallSingle_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -46,7 +50,7 @@ func TestCallSingle_Integration(t *testing.T) { Memo: "", } - caller, err := client.Signer.Info() + caller, err := signer.Info() require.NoError(t, err) // Make Msg config @@ -66,11 +70,103 @@ func TestCallSingle_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) +} + +func TestCallSingle_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") + + sponsorInfo, err := sponsor.Info() + require.NoError(t, err) + + sponsoreeInfo, err := sponsoree.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + sponsoreeClient := Client{ + Signer: sponsoree, + RPCClient: rpcClient, + } + + // Fetch sponsoree account information before the transaction + var sponsoreeAccountNumber uint64 = 0 + var sponsoreeSequence uint64 = 0 + + sponsoreeBefore, _, _ := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + if sponsoreeBefore != nil { + sponsoreeAccountNumber = sponsoreeBefore.AccountNumber + sponsoreeSequence = sponsoreeBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: ugnot.ValueString(10000), + Memo: "Test memo", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + // Create the message for the transaction + msg := vm.MsgCall{ + Caller: sponsoreeInfo.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"test argument"}, + } + + // Sponsoree creates a new sponsor transaction + tx, err := sponsoreeClient.NewSponsorTransaction(cfg, msg) + require.NoError(t, err) + + // Sponsoree signs the transaction + sponsorTx, err := sponsoreeClient.SignTx(*tx, sponsoreeAccountNumber, sponsoreeSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) - res, err = callSigningSeparately(t, client, baseCfg, msg) + // Sponsor executes the transaction which received from sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) require.NoError(t, err) - got = string(res.DeliverTx.Data) + + // Check the result of the transaction execution + expected := "(\"hi test argument\" string)\n\n" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) assert.Equal(t, expected, got) + + // Query sponsoree's balance after the transaction + sponsoreeAfter, _, err := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + require.NoError(t, err) + assert.Equal(t, std.Coins(nil), sponsoreeAfter.GetCoins()) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) } func TestCallMultiple_Integration(t *testing.T) { @@ -80,7 +176,8 @@ func TestCallMultiple_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -99,7 +196,7 @@ func TestCallMultiple_Integration(t *testing.T) { Memo: "", } - caller, err := client.Signer.Info() + caller, err := signer.Info() require.NoError(t, err) // Make Msg configs @@ -128,11 +225,152 @@ func TestCallMultiple_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) +} + +func TestCallMultiple_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for the sponsor and 2 sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree1 := newInMemorySigner(t, keybase, generateMnemonic(t), "sponsoree1") + sponsoree2 := newInMemorySigner(t, keybase, generateMnemonic(t), "sponsoree2") + + sponsorInfo, err := sponsor.Info() + require.NoError(t, err) + + sponsoree1Info, err := sponsoree1.Info() + require.NoError(t, err) + + sponsoree2Info, err := sponsoree2.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize the sponsor and 2 sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + sponsoree1Client := Client{ + Signer: sponsoree1, + RPCClient: rpcClient, + } + + sponsoree2Client := Client{ + Signer: sponsoree2, + RPCClient: rpcClient, + } + + // Fetch sponsoree1 account information before the transaction + var sponsoree1AccountNumber uint64 = 0 + var sponsoree1Sequence uint64 = 0 + + sponsoree1Before, _, _ := sponsoree1Client.QueryAccount(sponsoree1Info.GetAddress()) + if sponsoree1Before != nil { + sponsoree1AccountNumber = sponsoree1Before.AccountNumber + sponsoree1Sequence = sponsoree1Before.Sequence + } + + // Fetch sponsoree2 account information before the transaction + var sponsoree2AccountNumber uint64 = 0 + var sponsoree2Sequence uint64 = 0 + + sponsoree2Before, _, _ := sponsoree2Client.QueryAccount(sponsoree2Info.GetAddress()) + if sponsoree2Before != nil { + sponsoree2AccountNumber = sponsoree2Before.AccountNumber + sponsoree2Sequence = sponsoree2Before.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(10000), + Memo: "Test memo", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + // Create the messages for the transaction + msg1 := vm.MsgCall{ + Caller: sponsoree1Info.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"sponsoree1"}, + } + + msg2 := vm.MsgCall{ + Caller: sponsoree1Info.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"sponsoree1 again"}, + } + + msg3 := vm.MsgCall{ + Caller: sponsoree2Info.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"sponsoree2"}, + } + + msg4 := vm.MsgCall{ + Caller: sponsoree2Info.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"sponsoree2 again"}, + } + + // Sponsoree1 creates a new sponsor transaction + tx, err := sponsoree1Client.NewSponsorTransaction(cfg, msg1, msg2, msg3, msg4) + require.NoError(t, err) + + // Sponsoree1 signs the transaction + sponsorTx, err := sponsoree1Client.SignTx(*tx, sponsoree1AccountNumber, sponsoree1Sequence) + require.NoError(t, err) + + // Sponsoree2 signs the transaction + sponsorTx, err = sponsoree2Client.SignTx(*sponsorTx, sponsoree2AccountNumber, sponsoree2Sequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) - res, err = callSigningSeparately(t, client, baseCfg, msg1, msg2) + // Sponsor executes the transaction which received from the sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) require.NoError(t, err) - got = string(res.DeliverTx.Data) + + // Check the result of the transaction execution + expected := "(\"hi sponsoree1\" string)\n\n(\"hi sponsoree1 again\" string)\n\n(\"hi sponsoree2\" string)\n\n(\"hi sponsoree2 again\" string)\n\n" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) assert.Equal(t, expected, got) + + // Query sponsoree1's balance after the transaction + sponsoree1After, _, err := sponsoree1Client.QueryAccount(sponsoree1Info.GetAddress()) + require.NoError(t, err) + assert.Equal(t, std.Coins(nil), sponsoree1After.GetCoins()) + + // Query sponsoree2's balance after the transaction + sponsoree2After, _, err := sponsoree2Client.QueryAccount(sponsoree2Info.GetAddress()) + require.NoError(t, err) + assert.Equal(t, std.Coins(nil), sponsoree2After.GetCoins()) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) } func TestSendSingle_Integration(t *testing.T) { @@ -142,7 +380,8 @@ func TestSendSingle_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -186,17 +425,123 @@ func TestSendSingle_Integration(t *testing.T) { got := account.GetCoins() assert.Equal(t, expected, got) +} + +func TestSendSingle_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() - res, err = sendSigningSeparately(t, client, baseCfg, msg) + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sender := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") + + sponsorInfo, err := sponsor.Info() require.NoError(t, err) - assert.Equal(t, "", string(res.DeliverTx.Data)) - // Get the new account balance - account, _, err = client.QueryAccount(toAddress) + senderInfo, err := sender.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + senderClient := Client{ + Signer: sender, + RPCClient: rpcClient, + } + + // Ensure sender has enough money to make msg send + _, err = sponsorClient.Send(BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, bank.MsgSend{ + FromAddress: sponsorInfo.GetAddress(), + ToAddress: senderInfo.GetAddress(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100000)), + }) + require.NoError(t, err) + + // Fetch sender account information before the transaction + var senderAccountNumber uint64 = 0 + var senderSequence uint64 = 0 + + senderBefore, _, _ := senderClient.QueryAccount(senderInfo.GetAddress()) + if senderBefore != nil { + senderAccountNumber = senderBefore.AccountNumber + senderSequence = senderBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") + + // Create the message for the transaction + msg := bank.MsgSend{ + FromAddress: senderInfo.GetAddress(), + ToAddress: toAddress, + Amount: std.NewCoins(std.NewCoin("ugnot", 10000)), + } + + // sender creates a new sponsor transaction + tx, err := senderClient.NewSponsorTransaction(cfg, msg) + require.NoError(t, err) + + // sender signs the transaction + sponsorTx, err := senderClient.SignTx(*tx, senderAccountNumber, senderSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sender + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) + + // Query sender's balance after the transaction + senderAfter, _, err := senderClient.QueryAccount(senderInfo.GetAddress()) + require.NoError(t, err) + expectedSenderAfter := senderBefore.GetCoins().Sub(msg.Amount) + assert.Equal(t, expectedSenderAfter, senderAfter.GetCoins()) + + // Query to's balance after the transaction + toAfter, _, err := sponsorClient.QueryAccount(toAddress) require.NoError(t, err) - expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * amount)}} - got = account.GetCoins() - assert.Equal(t, expected2, got) + expectedToAfter := msg.Amount + assert.Equal(t, expectedToAfter, toAfter.GetCoins()) } func TestSendMultiple_Integration(t *testing.T) { @@ -206,7 +551,8 @@ func TestSendMultiple_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -258,17 +604,131 @@ func TestSendMultiple_Integration(t *testing.T) { got := account.GetCoins() assert.Equal(t, expected, got) +} + +func TestSendMultiple_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sender := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") - res, err = sendSigningSeparately(t, client, baseCfg, msg1, msg2) + sponsorInfo, err := sponsor.Info() require.NoError(t, err) - assert.Equal(t, "", string(res.DeliverTx.Data)) - // Get the new account balance - account, _, err = client.QueryAccount(toAddress) + senderInfo, err := sender.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + senderClient := Client{ + Signer: sender, + RPCClient: rpcClient, + } + + // Ensure sender has enough money to make msg send + _, err = sponsorClient.Send(BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, bank.MsgSend{ + FromAddress: sponsorInfo.GetAddress(), + ToAddress: senderInfo.GetAddress(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100000)), + }) + require.NoError(t, err) + + // Fetch sender account information before the transaction + var senderAccountNumber uint64 = 0 + var senderSequence uint64 = 0 + + senderBefore, _, _ := senderClient.QueryAccount(senderInfo.GetAddress()) + if senderBefore != nil { + senderAccountNumber = senderBefore.AccountNumber + senderSequence = senderBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") + + // Create the messages for the transaction + var amount1 int64 = 20000 + msg1 := bank.MsgSend{ + FromAddress: senderInfo.GetAddress(), + ToAddress: toAddress, + Amount: std.NewCoins(std.NewCoin("ugnot", amount1)), + } + + var amount2 int64 = 20000 + msg2 := bank.MsgSend{ + FromAddress: senderInfo.GetAddress(), + ToAddress: toAddress, + Amount: std.NewCoins(std.NewCoin("ugnot", amount2)), + } + + // sender creates a new sponsor transaction + tx, err := senderClient.NewSponsorTransaction(cfg, msg1, msg2) + require.NoError(t, err) + + // sender signs the transaction + sponsorTx, err := senderClient.SignTx(*tx, senderAccountNumber, senderSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sender + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Query sender's balance after the transaction + senderAfter, _, err := senderClient.QueryAccount(senderInfo.GetAddress()) + require.NoError(t, err) + expectSenderAfter := senderBefore.GetCoins().Sub(std.NewCoins(std.NewCoin("ugnot", amount1+amount2))) + assert.Equal(t, expectSenderAfter, senderAfter.GetCoins()) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) + + // Query to's balance after the transaction + toAfter, _, err := sponsorClient.QueryAccount(toAddress) require.NoError(t, err) - expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * (amount1 + amount2))}} - got = account.GetCoins() - assert.Equal(t, expected2, got) + expectToAfter := std.NewCoins(std.NewCoin("ugnot", amount1+amount2)) + assert.Equal(t, expectToAfter, toAfter.GetCoins()) } // Run tests @@ -279,7 +739,8 @@ func TestRunSingle_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -332,48 +793,160 @@ func main() { assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, string(res.DeliverTx.Data), "- before: 0\n- after: 10\n") - - res, err = runSigningSeparately(t, client, baseCfg, msg) - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, string(res.DeliverTx.Data), "- before: 10\n- after: 20\n") } -// Run tests -func TestRunMultiple_Integration(t *testing.T) { - // Set up in-memory node +func TestRunSingle_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() - // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") + + sponsorInfo, err := sponsor.Info() + require.NoError(t, err) + + sponsoreeInfo, err := sponsoree.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) - client := Client{ - Signer: signer, + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, RPCClient: rpcClient, } - // Make Tx config - baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, - AccountNumber: 0, - SequenceNumber: 0, - Memo: "", + sponsoreeClient := Client{ + Signer: sponsoree, + RPCClient: rpcClient, } - fileBody1 := `package main -import ( - "gno.land/p/demo/ufmt" - "gno.land/r/demo/tests" -) -func main() { - println(ufmt.Sprintf("- before: %d", tests.Counter())) - for i := 0; i < 10; i++ { - tests.IncCounter() + // Fetch sponsoree account information before the transaction + var sponsoreeAccountNumber uint64 = 0 + var sponsoreeSequence uint64 = 0 + + sponsoreeBefore, _, _ := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + if sponsoreeBefore != nil { + sponsoreeAccountNumber = sponsoreeBefore.AccountNumber + sponsoreeSequence = sponsoreeBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasFee: ugnot.ValueString(10000), + GasWanted: 8000000, + Memo: "", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + fileBody := `package main + import ( + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests" + ) + func main() { + println(ufmt.Sprintf("- before: %d", tests.Counter())) + for i := 0; i < 10; i++ { + tests.IncCounter() + } + println(ufmt.Sprintf("- after: %d", tests.Counter())) + }` + + // Create the message for the transaction + msg := vm.MsgRun{ + Caller: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "main", + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody, + }, + }, + }, + Send: nil, + } + + // Sponsoree creates a new sponsor transaction + tx, err := sponsoreeClient.NewSponsorTransaction(cfg, msg) + require.NoError(t, err) + + // Sponsoree signs the transaction + sponsorTx, err := sponsoreeClient.SignTx(*tx, sponsoreeAccountNumber, sponsoreeSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "- before: 0\n- after: 10\n" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Query sponsoree's balance after the transaction + sponsoreeAfter, _, err := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + require.NoError(t, err) + assert.Equal(t, std.Coins(nil), sponsoreeAfter.GetCoins()) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) +} + +func TestRunMultiple_Integration(t *testing.T) { + // Set up in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Init Signer & RPCClient + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + client := Client{ + Signer: signer, + RPCClient: rpcClient, + } + + // Make Tx config + baseCfg := BaseTxCfg{ + GasFee: ugnot.ValueString(10000), + GasWanted: 8000000, + AccountNumber: 0, + SequenceNumber: 0, + Memo: "", + } + + fileBody1 := `package main +import ( + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests" +) +func main() { + println(ufmt.Sprintf("- before: %d", tests.Counter())) + for i := 0; i < 10; i++ { + tests.IncCounter() } println(ufmt.Sprintf("- after: %d", tests.Counter())) }` @@ -424,14 +997,148 @@ func main() { assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +func TestRunMultiple_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") - res, err = runSigningSeparately(t, client, baseCfg, msg1, msg2) + sponsorInfo, err := sponsor.Info() require.NoError(t, err) - require.NotNil(t, res) - expected2 := "- before: 10\n- after: 20\nhi gnoclient!\n" - assert.Equal(t, expected2, string(res.DeliverTx.Data)) + + sponsoreeInfo, err := sponsoree.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + sponsoreeClient := Client{ + Signer: sponsoree, + RPCClient: rpcClient, + } + + // Fetch sponsoree account information before the transaction + var sponsoreeAccountNumber uint64 = 0 + var sponsoreeSequence uint64 = 0 + + sponsoreeBefore, _, _ := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + if sponsoreeBefore != nil { + sponsoreeAccountNumber = sponsoreeBefore.AccountNumber + sponsoreeSequence = sponsoreeBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasFee: ugnot.ValueString(10000), + GasWanted: 8000000, + Memo: "", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + fileBody1 := `package main + import ( + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests" + ) + func main() { + println(ufmt.Sprintf("- before: %d", tests.Counter())) + for i := 0; i < 10; i++ { + tests.IncCounter() + } + println(ufmt.Sprintf("- after: %d", tests.Counter())) + }` + + fileBody2 := `package main + import ( + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" + ) + func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) + }` + + // Make Msg configs + msg1 := vm.MsgRun{ + Caller: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "main", + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody1, + }, + }, + }, + Send: nil, + } + msg2 := vm.MsgRun{ + Caller: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "main", + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody2, + }, + }, + }, + Send: nil, + } + + // Sponsoree creates a new sponsor transaction + tx, err := sponsoreeClient.NewSponsorTransaction(cfg, msg1, msg2) + require.NoError(t, err) + + // Sponsoree signs the transaction + sponsorTx, err := sponsoreeClient.SignTx(*tx, sponsoreeAccountNumber, sponsoreeSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "- before: 0\n- after: 10\nhi gnoclient!\n" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Query sponsoree's balance after the transaction + sponsoreeAfter, _, err := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + require.NoError(t, err) + assert.Equal(t, std.Coins(nil), sponsoreeAfter.GetCoins()) + + // Query sponsor's balance after the transaction + sponsorAfter, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + expectedSponsorAfter := sponsorBefore.GetCoins().Sub(std.MustParseCoins(cfg.BaseTxCfg.GasFee)) + assert.Equal(t, expectedSponsorAfter, sponsorAfter.GetCoins()) } +// AddPackage tests func TestAddPackageSingle_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) @@ -439,7 +1146,8 @@ func TestAddPackageSingle_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -503,18 +1211,135 @@ func Echo(str string) string { baseAcc, _, err := client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath)) require.NoError(t, err) assert.Equal(t, baseAcc.GetCoins(), deposit) +} - // Test signing separately (using a different deployment path) - deploymentPathB := "gno.land/p/demo/integration/test/echo2" - msg.Package.Path = deploymentPathB - _, err = addPackageSigningSeparately(t, client, baseCfg, msg) - assert.NoError(t, err) - query, err = client.Query(QueryCfg{ +func TestAddPackageSingle_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") + + sponsorInfo, err := sponsor.Info() + require.NoError(t, err) + + sponsoreeInfo, err := sponsoree.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + sponsoreeClient := Client{ + Signer: sponsoree, + RPCClient: rpcClient, + } + + // Ensure sponsoree has enough money to make msg addpackage + _, err = sponsorClient.Send(BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, bank.MsgSend{ + FromAddress: sponsorInfo.GetAddress(), + ToAddress: sponsoreeInfo.GetAddress(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100000)), + }) + require.NoError(t, err) + + // Fetch sponsoree account information before the transaction + var sponsoreeAccountNumber uint64 = 0 + var sponsoreeSequence uint64 = 0 + + sponsoreeBefore, _, _ := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + if sponsoreeBefore != nil { + sponsoreeAccountNumber = sponsoreeBefore.AccountNumber + sponsoreeSequence = sponsoreeBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasFee: ugnot.ValueString(10000), + GasWanted: 8000000, + Memo: "", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + body := `package echo + +func Echo(str string) string { + return str +}` + + fileName := "echo.gno" + deploymentPath := "gno.land/p/demo/integration/test/echo" + deposit := std.NewCoins(std.NewCoin("ugnot", 100)) + + // Make Msg config + msg := vm.MsgAddPackage{ + Creator: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "echo", + Path: deploymentPath, + Files: []*std.MemFile{ + { + Name: fileName, + Body: body, + }, + }, + }, + Deposit: deposit, + } + + // Sponsoree creates a new sponsor transaction + tx, err := sponsoreeClient.NewSponsorTransaction(cfg, msg) + require.NoError(t, err) + + // Sponsoree signs the transaction + sponsorTx, err := sponsoreeClient.SignTx(*tx, sponsoreeAccountNumber, sponsoreeSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Check for deployed file on the node + query, err := sponsorClient.Query(QueryCfg{ Path: "vm/qfile", - Data: []byte(deploymentPathB), + Data: []byte(deploymentPath), }) require.NoError(t, err) assert.Equal(t, string(query.Response.Data), fileName) + + // Query package's balance to validate the deposit amount + baseAcc, _, err := sponsorClient.QueryAccount(gnolang.DerivePkgAddr(deploymentPath)) + require.NoError(t, err) + assert.Equal(t, baseAcc.GetCoins(), deposit) } func TestAddPackageMultiple_Integration(t *testing.T) { @@ -524,7 +1349,8 @@ func TestAddPackageMultiple_Integration(t *testing.T) { defer node.Stop() // Init Signer & RPCClient - signer := newInMemorySigner(t, "tendermint_test") + keybase := keys.NewInMemory() + signer := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) require.NoError(t, err) @@ -556,7 +1382,7 @@ func Echo(str string) string { body2 := `package hello func Hello(str string) string { - return "Hello " + str + "!" + return "Hello " + str + "!" }` caller, err := client.Signer.Info() @@ -626,47 +1452,197 @@ func Hello(str string) string { baseAcc, _, err = client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath2)) require.NoError(t, err) assert.Equal(t, baseAcc.GetCoins(), deposit) +} - // Test signing separately (using a different deployment path) - deploymentPath1B := "gno.land/p/demo/integration/test/echo2" - deploymentPath2B := "gno.land/p/demo/integration/test/hello2" - msg1.Package.Path = deploymentPath1B - msg2.Package.Path = deploymentPath2B - _, err = addPackageSigningSeparately(t, client, baseCfg, msg1, msg2) - assert.NoError(t, err) - query, err = client.Query(QueryCfg{ +func TestAddPackageMultiple_Sponsor_Integration(t *testing.T) { + // Set up an in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Initialize in-memory key storage + keybase := keys.NewInMemory() + + // Create signer accounts for sponsor and sponsoree + sponsor := newInMemorySigner(t, keybase, integration.DefaultAccount_Seed, integration.DefaultAccount_Name) + sponsoree := newInMemorySigner(t, keybase, generateMnemonic(t), "test2") + + sponsorInfo, err := sponsor.Info() + require.NoError(t, err) + + sponsoreeInfo, err := sponsoree.Info() + require.NoError(t, err) + + // Set up an RPC client to interact with the in-memory node + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) + + // Initialize sponsor and sponsoree clients with their respective signers and RPC client + sponsorClient := Client{ + Signer: sponsor, + RPCClient: rpcClient, + } + + sponsoreeClient := Client{ + Signer: sponsoree, + RPCClient: rpcClient, + } + + // Ensure sponsoree has enough money to make msg addpackage + _, err = sponsorClient.Send(BaseTxCfg{ + GasWanted: 1000000, + GasFee: ugnot.ValueString(100000), + Memo: "Test memo", + }, bank.MsgSend{ + FromAddress: sponsorInfo.GetAddress(), + ToAddress: sponsoreeInfo.GetAddress(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100000)), + }) + require.NoError(t, err) + + // Fetch sponsoree account information before the transaction + var sponsoreeAccountNumber uint64 = 0 + var sponsoreeSequence uint64 = 0 + + sponsoreeBefore, _, _ := sponsoreeClient.QueryAccount(sponsoreeInfo.GetAddress()) + if sponsoreeBefore != nil { + sponsoreeAccountNumber = sponsoreeBefore.AccountNumber + sponsoreeSequence = sponsoreeBefore.Sequence + } + + // Configure the transaction to be sponsored + cfg := SponsorTxCfg{ + BaseTxCfg: BaseTxCfg{ + GasFee: ugnot.ValueString(10000), + GasWanted: 8000000, + Memo: "", + }, + SponsorAddress: sponsorInfo.GetAddress(), + } + + deposit := std.NewCoins(std.NewCoin("ugnot", 100)) + deploymentPath1 := "gno.land/p/demo/integration/test/echo" + + body1 := `package echo + +func Echo(str string) string { + return str +}` + + deploymentPath2 := "gno.land/p/demo/integration/test/hello" + body2 := `package hello + +func Hello(str string) string { + return "Hello " + str + "!" +}` + + msg1 := vm.MsgAddPackage{ + Creator: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "echo", + Path: deploymentPath1, + Files: []*std.MemFile{ + { + Name: "echo.gno", + Body: body1, + }, + }, + }, + Deposit: nil, + } + + msg2 := vm.MsgAddPackage{ + Creator: sponsoreeInfo.GetAddress(), + Package: &std.MemPackage{ + Name: "hello", + Path: deploymentPath2, + Files: []*std.MemFile{ + { + Name: "gno.mod", + Body: "module gno.land/p/demo/integration/test/hello", + }, + { + Name: "hello.gno", + Body: body2, + }, + }, + }, + Deposit: deposit, + } + + // Sponsoree creates a new sponsor transaction + tx, err := sponsoreeClient.NewSponsorTransaction(cfg, msg1, msg2) + require.NoError(t, err) + + // Sponsoree signs the transaction + sponsorTx, err := sponsoreeClient.SignTx(*tx, sponsoreeAccountNumber, sponsoreeSequence) + require.NoError(t, err) + + // Fetch sponsor account information before the transaction + sponsorBefore, _, err := sponsorClient.QueryAccount(sponsorInfo.GetAddress()) + require.NoError(t, err) + + // Sponsor executes the transaction which received from sponsoree + res, err := sponsorClient.ExecuteSponsorTransaction(*sponsorTx, sponsorBefore.AccountNumber, sponsorBefore.Sequence) + require.NoError(t, err) + + // Check the result of the transaction execution + expected := "" + got := string(res.DeliverTx.Data) + + assert.Nil(t, err) + assert.Equal(t, expected, got) + + // Check Package #1 + query, err := sponsorClient.Query(QueryCfg{ Path: "vm/qfile", - Data: []byte(deploymentPath1B), + Data: []byte(deploymentPath1), }) require.NoError(t, err) assert.Equal(t, string(query.Response.Data), "echo.gno") - query, err = client.Query(QueryCfg{ + + // Query package's balance to validate the deposit amount + baseAcc, _, err := sponsorClient.QueryAccount(gnolang.DerivePkgAddr(deploymentPath1)) + require.NoError(t, err) + assert.Equal(t, baseAcc.GetCoins().String(), "") + + // Check Package #2 + query, err = sponsorClient.Query(QueryCfg{ Path: "vm/qfile", - Data: []byte(deploymentPath2B), + Data: []byte(deploymentPath2), }) require.NoError(t, err) assert.Contains(t, string(query.Response.Data), "hello.gno") assert.Contains(t, string(query.Response.Data), "gno.mod") -} -// todo add more integration tests: -// MsgCall with Send field populated (single/multiple) -// MsgRun with Send field populated (single/multiple) + // Query package's balance to validate the deposit amount + baseAcc, _, err = sponsorClient.QueryAccount(gnolang.DerivePkgAddr(deploymentPath2)) + require.NoError(t, err) + assert.Equal(t, baseAcc.GetCoins(), deposit) +} -func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase { +func newInMemorySigner(t *testing.T, kb keys.Keybase, mnemonic, accName string) *SignerFromKeybase { t.Helper() - mnemonic := integration.DefaultAccount_Seed - name := integration.DefaultAccount_Name - - kb := keys.NewInMemory() - _, err := kb.CreateAccount(name, mnemonic, "", "", uint32(0), uint32(0)) + _, err := kb.CreateAccount(accName, mnemonic, "", "", uint32(0), uint32(0)) require.NoError(t, err) return &SignerFromKeybase{ - Keybase: kb, // Stores keys in memory or on disk - Account: name, // Account name or bech32 format - Password: "", // Password for encryption - ChainID: chainid, // Chain ID for transaction signing + Keybase: kb, // Stores keys in memory or on disk + Account: accName, // Account name or bech32 format + Password: "", // Password for encryption + ChainID: "tendermint_test", // Chain ID for transaction signing } } + +func generateMnemonic(t *testing.T) string { + t.Helper() + + entropy, err := bip39.NewEntropy(256) + require.NoError(t, err) + + mnemonic, err := bip39.NewMnemonic(entropy) + require.NoError(t, err) + + return mnemonic +} diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index 6e652080c72..51b38286012 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -25,6 +25,9 @@ type SignerFromKeybase struct { ChainID string // Chain ID for transaction signing } +// Ensure SignerFromKeybase implements the Signer interface. +var _ Signer = (*SignerFromKeybase)(nil) + // Validate checks if the signer is properly configured. func (s SignerFromKeybase) Validate() error { if s.ChainID == "" { @@ -46,7 +49,7 @@ func (s SignerFromKeybase) Validate() error { Caller: caller.GetAddress(), } signCfg := SignCfg{ - UnsignedTX: std.Tx{ + Tx: std.Tx{ Msgs: []std.Msg{msg}, Fee: std.NewFee(0, std.NewCoin(ugnot.Denom, 1000000)), }, @@ -70,14 +73,14 @@ func (s SignerFromKeybase) Info() (keys.Info, error) { // SignCfg provides the signing configuration, containing: // unsigned transaction data, account number, and account sequence. type SignCfg struct { - UnsignedTX std.Tx + Tx std.Tx SequenceNumber uint64 AccountNumber uint64 } // Sign implements the Signer interface for SignerFromKeybase. func (s SignerFromKeybase) Sign(cfg SignCfg) (*std.Tx, error) { - tx := cfg.UnsignedTX + tx := cfg.Tx chainID := s.ChainID accountNumber := cfg.AccountNumber sequenceNumber := cfg.SequenceNumber @@ -111,7 +114,9 @@ func (s SignerFromKeybase) Sign(cfg SignCfg) (*std.Tx, error) { if err != nil { return nil, err } + addr := pub.Address() + found := false for i := range tx.Signatures { if signers[i] == addr { diff --git a/gno.land/pkg/gnoclient/signer_test.go b/gno.land/pkg/gnoclient/signer_test.go index 3b4cbb757ad..bf744ed84c4 100644 --- a/gno.land/pkg/gnoclient/signer_test.go +++ b/gno.land/pkg/gnoclient/signer_test.go @@ -1 +1,161 @@ package gnoclient + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSignerFromBip39 tests the SignerFromBip39 function. +func TestSignerFromBip39(t *testing.T) { + t.Parallel() + + chainID := "test-chain-id" + passphrase := "" + account := uint32(0) + index := uint32(0) + + // Define test cases with mnemonic and expected outcomes. + testcases := []struct { + name string + mnemonic string + expectedError bool + }{ + { + name: "Valid mnemonic", + mnemonic: "index brass unknown lecture autumn provide royal shrimp elegant wink now zebra discover swarm act ill you bullet entire outdoor tilt usage gap multiply", + expectedError: false, + }, + { + name: "Invalid mnemonic", + mnemonic: "invalid mnemonic", + expectedError: true, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Create a signer from mnemonic + signer, err := SignerFromBip39(tc.mnemonic, chainID, passphrase, account, index) + + // Check if an error was expected + if tc.expectedError { + assert.Error(t, err) + assert.Nil(t, signer) + } else { + require.NoError(t, err) + require.NotNil(t, signer) + + // Validate the signer + err = signer.Validate() + assert.NoError(t, err) + } + }) + } +} + +// TestSignerFromKeybase tests the SignerFromKeybase struct. +func TestSignerFromKeybase(t *testing.T) { + t.Parallel() + + chainID := "test-chain-id" + passphrase := "" + account := uint32(0) + index := uint32(0) + + mnemonic := "index brass unknown lecture autumn provide royal shrimp elegant wink now zebra discover swarm act ill you bullet entire outdoor tilt usage gap multiply" + + // Define test cases for different scenarios of the signer + tests := []struct { + name string + account string + password string + expectedError bool + validateOnly bool + }{ + { + name: "Valid signer", + account: "default", + password: "", + expectedError: false, + }, + { + name: "Missing ChainID", + account: "default", + password: "", + expectedError: true, + validateOnly: true, + }, + { + name: "Incorrect password", + account: "default", + password: "wrong-password", + expectedError: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() // Run tests in parallel + + // Initialize in-memory keybase and create account + kb := keys.NewInMemory() + name := "default" + password := "" + + _, err := kb.CreateAccount(name, mnemonic, passphrase, password, account, index) + require.NoError(t, err) + + // Create a signer from the keybase + signer := SignerFromKeybase{ + Keybase: kb, + Account: tc.account, + Password: tc.password, + ChainID: chainID, + } + + signerInfo, err := signer.Info() + require.NoError(t, err) + + // Test for missing ChainID scenario + if tc.validateOnly { + signer.ChainID = "" + err := signer.Validate() + assert.Error(t, err) + assert.Equal(t, "missing ChainID", err.Error()) + } else { + // Prepare a sign configuration + signCfg := SignCfg{ + Tx: std.Tx{ + Msgs: []std.Msg{ + vm.MsgCall{ + Caller: signerInfo.GetAddress(), + }, + }, + Fee: std.NewFee(0, std.NewCoin("ugnot", 1000000)), + }, + } + + // Try to sign the transaction + signedTx, err := signer.Sign(signCfg) + + // Check if an error was expected + if tc.expectedError { + assert.Error(t, err) + assert.Nil(t, signedTx) + } else { + assert.NoError(t, err) + assert.NotNil(t, signedTx) + } + } + }) + } +} diff --git a/gno.land/pkg/gnoclient/util.go b/gno.land/pkg/gnoclient/util.go index 50099eb4bd8..aa7c816eefc 100644 --- a/gno.land/pkg/gnoclient/util.go +++ b/gno.land/pkg/gnoclient/util.go @@ -1,12 +1,53 @@ package gnoclient -func (cfg BaseTxCfg) validateBaseTxConfig() error { +import ( + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/errors" +) + +var ( + ErrInvalidGasWanted = errors.New("invalid gas wanted") + ErrInvalidGasFee = errors.New("invalid gas fee") + ErrMissingSigner = errors.New("missing Signer") + ErrMissingRPCClient = errors.New("missing RPCClient") + ErrNoMessages = errors.New("no messages provided") + ErrMixedMessageTypes = errors.New("mixed message types not allowed") + + ErrInvalidSponsorAddress = errors.New("invalid sponsor address") + ErrInvalidSponsorTx = errors.New("invalid sponsor tx") +) + +// BaseTxCfg defines the base transaction configuration shared by all message types. +type BaseTxCfg struct { + GasFee string // Gas fee + GasWanted int64 // Gas wanted + AccountNumber uint64 // Account number + SequenceNumber uint64 // Sequence number + Memo string // Memo +} + +// validate validates the base transaction configuration. +func (cfg BaseTxCfg) validate() error { if cfg.GasWanted <= 0 { return ErrInvalidGasWanted } if cfg.GasFee == "" { return ErrInvalidGasFee } - return nil } + +// SponsorTxCfg represents the configuration for a sponsor transaction. +type SponsorTxCfg struct { + BaseTxCfg + SponsorAddress crypto.Address +} + +// validate validates the sponsor transaction configuration. +func (cfg SponsorTxCfg) validate() error { + if cfg.SponsorAddress.IsZero() { + return ErrInvalidSponsorAddress + } + + return cfg.BaseTxCfg.validate() +} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index d3f55cfadf7..291553f2f13 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -305,6 +305,27 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { err = cmd.ParseAndRun(context.Background(), args) tsValidateError(ts, "gnokey", neg, err) }, + // addkey command must be executed before starting the node; it errors out otherwise. + "addkey": func(ts *testscript.TestScript, neg bool, args []string) { + if nodeIsRunning(nodes, getNodeSID(ts)) { + tsValidateError(ts, "addkey", neg, errors.New("addkey must be used before starting node")) + return + } + + if len(args) == 0 { + ts.Fatalf("new key name required") + } + + kb, err := keys.NewKeyBaseFromDir(gnoHomeDir) + if err != nil { + ts.Fatalf("unable to get keybase") + } + + _, err = createAccountKey(ts, kb, args[0]) + if err != nil { + ts.Fatalf("error creating accountKey %s: %s", args[0], err) + } + }, // adduser command must be executed before starting the node; it errors out otherwise. "adduser": func(ts *testscript.TestScript, neg bool, args []string) { if nodeIsRunning(nodes, getNodeSID(ts)) { @@ -583,30 +604,39 @@ type envSetter interface { Setenv(key, value string) } -// createAccount creates a new account with the given name and adds it to the keybase. -func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland.Balance, error) { - var balance gnoland.Balance +// createAccountKey only creates the account within KeyBase. +func createAccountKey(env envSetter, kb keys.Keybase, accountName string) (keys.Info, error) { entropy, err := bip39.NewEntropy(256) if err != nil { - return balance, fmt.Errorf("error creating entropy: %w", err) + return nil, fmt.Errorf("error creating entropy: %w", err) } mnemonic, err := bip39.NewMnemonic(entropy) if err != nil { - return balance, fmt.Errorf("error generating mnemonic: %w", err) + return nil, fmt.Errorf("error generating mnemonic: %w", err) } var keyInfo keys.Info if keyInfo, err = kb.CreateAccount(accountName, mnemonic, "", "", 0, 0); err != nil { - return balance, fmt.Errorf("unable to create account: %w", err) + return nil, fmt.Errorf("unable to create account: %w", err) } address := keyInfo.GetAddress() env.Setenv("USER_SEED_"+accountName, mnemonic) env.Setenv("USER_ADDR_"+accountName, address.String()) + return keyInfo, nil +} + +// createAccount creates a new account with the given name and adds it to the keybase. +func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland.Balance, error) { + keyInfo, err := createAccountKey(env, kb, accountName) + if err != nil { + return gnoland.Balance{}, err + } + return gnoland.Balance{ - Address: address, + Address: keyInfo.GetAddress(), Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)}, }, nil } diff --git a/gno.land/pkg/keyscli/addpkg.go b/gno.land/pkg/keyscli/addpkg.go index 37463d13b5c..a33e97e94de 100644 --- a/gno.land/pkg/keyscli/addpkg.go +++ b/gno.land/pkg/keyscli/addpkg.go @@ -9,6 +9,7 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/errors" @@ -107,12 +108,35 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error { if err != nil { panic(err) } - // construct msg & tx and marshal. + msg := vm.MsgAddPackage{ Creator: creator, Package: memPkg, Deposit: deposit, } + + // if a sponsor onchain address is specified + if cfg.RootCfg.Sponsor != "" { + sponsorAddress, err := crypto.AddressFromBech32(cfg.RootCfg.Sponsor) + if err != nil { + return errors.Wrap(err, "invalid sponsor address") + } + + tx := std.Tx{ + Msgs: []std.Msg{ + vm.NewMsgNoop(sponsorAddress), + msg, + }, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.RootCfg.Memo, + } + + io.Println(string(amino.MustMarshalJSON(tx))) + + return nil + } + tx := std.Tx{ Msgs: []std.Msg{msg}, Fee: std.NewFee(gaswanted, gasfee), diff --git a/gno.land/pkg/keyscli/call.go b/gno.land/pkg/keyscli/call.go index a7085bbfeb7..256b331e58e 100644 --- a/gno.land/pkg/keyscli/call.go +++ b/gno.land/pkg/keyscli/call.go @@ -7,6 +7,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/errors" @@ -115,7 +116,6 @@ func execMakeCall(cfg *MakeCallCfg, args []string, io commands.IO) error { return errors.Wrap(err, "parsing gas fee coin") } - // construct msg & tx and marshal. msg := vm.MsgCall{ Caller: caller, Send: send, @@ -123,6 +123,29 @@ func execMakeCall(cfg *MakeCallCfg, args []string, io commands.IO) error { Func: fnc, Args: cfg.Args, } + + // if a sponsor onchain address is specified + if cfg.RootCfg.Sponsor != "" { + sponsorAddress, err := crypto.AddressFromBech32(cfg.RootCfg.Sponsor) + if err != nil { + return errors.Wrap(err, "invalid sponsor address") + } + + tx := std.Tx{ + Msgs: []std.Msg{ + vm.NewMsgNoop(sponsorAddress), + msg, + }, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.RootCfg.Memo, + } + + io.Println(string(amino.MustMarshalJSON(tx))) + + return nil + } + tx := std.Tx{ Msgs: []std.Msg{msg}, Fee: std.NewFee(gaswanted, gasfee), diff --git a/gno.land/pkg/keyscli/maketx.go b/gno.land/pkg/keyscli/maketx.go index 117a9bb3c3d..b4b338dd46d 100644 --- a/gno.land/pkg/keyscli/maketx.go +++ b/gno.land/pkg/keyscli/maketx.go @@ -16,6 +16,9 @@ type MakeTxCfg struct { Broadcast bool ChainID string + + // Optional + Sponsoree string } func NewMakeTxCmd(rootCfg *client.BaseCfg, io commands.IO) *commands.Command { @@ -80,4 +83,11 @@ func (c *MakeTxCfg) RegisterFlags(fs *flag.FlagSet) { "dev", "chainid to sign for (only useful if --broadcast)", ) + + fs.StringVar( + &c.Sponsoree, + "sponsoree", + "", + "address of sponsoree", + ) } diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go index aa0ee298201..74881ab452d 100644 --- a/gno.land/pkg/keyscli/run.go +++ b/gno.land/pkg/keyscli/run.go @@ -11,6 +11,7 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/errors" @@ -113,11 +114,33 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error { // Set to empty; this will be automatically set by the VM keeper. memPkg.Path = "" - // construct msg & tx and marshal. msg := vm.MsgRun{ Caller: caller, Package: memPkg, } + + // if a sponsor onchain address is specified + if cfg.RootCfg.Sponsor != "" { + sponsorAddress, err := crypto.AddressFromBech32(cfg.RootCfg.Sponsor) + if err != nil { + return errors.Wrap(err, "invalid sponsor address") + } + + tx := std.Tx{ + Msgs: []std.Msg{ + vm.NewMsgNoop(sponsorAddress), + msg, + }, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.RootCfg.Memo, + } + + cmdio.Println(string(amino.MustMarshalJSON(tx))) + + return nil + } + tx := std.Tx{ Msgs: []std.Msg{msg}, Fee: std.NewFee(gaswanted, gasfee), diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index 7b26265f35d..a19ed5f5c65 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -33,6 +33,8 @@ func (vh vmHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { return vh.handleMsgCall(ctx, msg) case MsgRun: return vh.handleMsgRun(ctx, msg) + case MsgNoop: + return sdk.Result{} default: errMsg := fmt.Sprintf("unrecognized vm message type: %T", msg) return abciResult(std.ErrUnknownRequest(errMsg)) diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index d650c23f382..274ff5b68a3 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -207,3 +207,44 @@ func (msg MsgRun) GetSigners() []crypto.Address { func (msg MsgRun) GetReceived() std.Coins { return msg.Send } + +//---------------------------------------- +// MsgNoop + +// MsgNoop - executes nothing +type MsgNoop struct { + Caller crypto.Address `json:"caller" yaml:"caller"` +} + +var _ std.Msg = MsgNoop{} + +func NewMsgNoop(caller crypto.Address) MsgNoop { + return MsgNoop{ + Caller: caller, + } +} + +// Implements Msg. +func (msg MsgNoop) Route() string { return RouterKey } + +// Implements Msg. +func (msg MsgNoop) Type() string { return "no_op" } + +// Implements Msg. +func (msg MsgNoop) ValidateBasic() error { + if msg.Caller.IsZero() { + return std.ErrInvalidAddress("missing caller address") + } + + return nil +} + +// Implements Msg. +func (msg MsgNoop) GetSignBytes() []byte { + return std.MustSortJSON(amino.MustMarshalJSON(msg)) +} + +// Implements Msg. +func (msg MsgNoop) GetSigners() []crypto.Address { + return []crypto.Address{msg.Caller} +} diff --git a/gno.land/pkg/sdk/vm/msgs_test.go b/gno.land/pkg/sdk/vm/msgs_test.go new file mode 100644 index 00000000000..f8026c1371d --- /dev/null +++ b/gno.land/pkg/sdk/vm/msgs_test.go @@ -0,0 +1,120 @@ +package vm + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/require" +) + +func TestMsgAddPackage(t *testing.T) { + creator := crypto.AddressFromPreimage([]byte("addr1")) + pkgPath := "gno.land/r/namespace/test" + files := []*std.MemFile{ + { + Name: "test.gno", + Body: `package test + func Echo() string {return "hello world"}`, + }, + } + + msg := NewMsgAddPackage(creator, pkgPath, files) + + // Validate Basic + err := msg.ValidateBasic() + require.NoError(t, err) + + // Check if package name is set correctly + require.Equal(t, msg.Package.Name, "test") + + // Test invalid address + msg.Creator = crypto.Address{} + err = msg.ValidateBasic() + require.Error(t, err) + + // Test invalid package path + msg.Creator = creator + msg.Package.Path = "" + err = msg.ValidateBasic() + require.Error(t, err) +} + +func TestMsgCall(t *testing.T) { + caller := crypto.AddressFromPreimage([]byte("addr1")) + pkgPath := "gno.land/r/namespace/mypkg" + funcName := "MyFunction" + args := []string{"arg1", "arg2"} + + msg := NewMsgCall(caller, std.Coins{}, pkgPath, funcName, args) + + // Validate Basic + err := msg.ValidateBasic() + require.NoError(t, err) + + // Test invalid caller address + msg.Caller = crypto.Address{} + err = msg.ValidateBasic() + require.Error(t, err) + + // Test invalid package path + msg.Caller = caller + msg.PkgPath = "" + err = msg.ValidateBasic() + require.Error(t, err) + + // Test invalid function name + msg.PkgPath = pkgPath + msg.Func = "" + err = msg.ValidateBasic() + require.Error(t, err) +} + +func TestMsgRun(t *testing.T) { + caller := crypto.AddressFromPreimage([]byte("addr1")) + files := []*std.MemFile{ + { + Name: "test.gno", + Body: `package main + func Echo() string {return "hello world"}`, + }, + } + + msg := NewMsgRun(caller, std.Coins{}, files) + + // Validate Basic + err := msg.ValidateBasic() + require.NoError(t, err) + + // Test invalid caller address + msg.Caller = crypto.Address{} + err = msg.ValidateBasic() + require.Error(t, err) + + // Test invalid package name + files = []*std.MemFile{ + { + Name: "test.gno", + Body: `package test + func Echo() string {return "hello world"}`, + }, + } + require.Panics(t, func() { + NewMsgRun(caller, std.Coins{}, files) + }) +} + +func TestMsgNoop(t *testing.T) { + caller := crypto.AddressFromPreimage([]byte("addr1")) + + msg := NewMsgNoop(caller) + + // Validate Basic + err := msg.ValidateBasic() + require.NoError(t, err) + + // Test invalid caller address + msg.Caller = crypto.Address{} + err = msg.ValidateBasic() + require.Error(t, err) +} diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 30dd116d4e3..fd7db63b3b0 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -15,6 +15,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( MsgCall{}, "m_call", MsgRun{}, "m_run", MsgAddPackage{}, "m_addpkg", // TODO rename both to MsgAddPkg? + MsgNoop{}, "m_noop", // errors InvalidPkgPathError{}, "InvalidPkgPathError", diff --git a/gno.land/pkg/sdk/vm/vm.proto b/gno.land/pkg/sdk/vm/vm.proto index e02226e1ae4..16cfe104c16 100644 --- a/gno.land/pkg/sdk/vm/vm.proto +++ b/gno.land/pkg/sdk/vm/vm.proto @@ -27,6 +27,10 @@ message m_addpkg { string deposit = 3; } +message m_noop { + string caller = 1; +} + message InvalidPkgPathError { } diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index 7e67392ebe7..18e0b78f487 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -24,6 +24,9 @@ type MakeTxCfg struct { // Valid options are SimulateTest, SimulateSkip or SimulateOnly. Simulate string ChainID string + + // Optional + Sponsor string } // These are the valid options for MakeTxConfig.Simulate. @@ -109,6 +112,13 @@ func (c *MakeTxCfg) RegisterFlags(fs *flag.FlagSet) { "dev", "chainid to sign for (only useful with --broadcast)", ) + + fs.StringVar( + &c.Sponsor, + "sponsor", + "", + "onchain address of the sponsor", + ) } func SignAndBroadcastHandler( @@ -131,24 +141,32 @@ func SignAndBroadcastHandler( } accountAddr := info.GetAddress() + var accountNumber uint64 + var sequence uint64 + qopts := &QueryCfg{ RootCfg: baseopts, Path: fmt.Sprintf("auth/accounts/%s", accountAddr), } qres, err := QueryHandler(qopts) if err != nil { - return nil, errors.Wrap(err, "query account") - } - var qret struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &qret) - if err != nil { - return nil, err + if !tx.IsSponsorTx() { + return nil, errors.Wrap(err, "query account") + } + } else { + var qret struct { + BaseAccount std.BaseAccount + } + + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return nil, err + } + accountNumber = qret.BaseAccount.AccountNumber + sequence = qret.BaseAccount.Sequence } // sign tx - accountNumber := qret.BaseAccount.AccountNumber - sequence := qret.BaseAccount.Sequence - sOpts := signOpts{ chainID: txopts.ChainID, accountSequence: sequence, diff --git a/tm2/pkg/crypto/keys/client/send.go b/tm2/pkg/crypto/keys/client/send.go index 28016889548..5af337b031e 100644 --- a/tm2/pkg/crypto/keys/client/send.go +++ b/tm2/pkg/crypto/keys/client/send.go @@ -4,6 +4,7 @@ import ( "context" "flag" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -104,12 +105,34 @@ func execMakeSend(cfg *MakeSendCfg, args []string, io commands.IO) error { return errors.Wrap(err, "parsing gas fee coin") } - // construct msg & tx and marshal. msg := bank.MsgSend{ FromAddress: fromAddr, ToAddress: toAddr, Amount: send, } + + // if a sponsor onchain address is specified + if cfg.RootCfg.Sponsor != "" { + sponsorAddress, err := crypto.AddressFromBech32(cfg.RootCfg.Sponsor) + if err != nil { + return errors.Wrap(err, "invalid sponsor address") + } + + tx := std.Tx{ + Msgs: []std.Msg{ + vm.NewMsgNoop(sponsorAddress), // sponsored noop msg + msg, // original msg + }, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.RootCfg.Memo, + } + + io.Println(string(amino.MustMarshalJSON(tx))) + + return nil + } + tx := std.Tx{ Msgs: []std.Msg{msg}, Fee: std.NewFee(gaswanted, gasfee), @@ -125,5 +148,6 @@ func execMakeSend(cfg *MakeSendCfg, args []string, io commands.IO) error { } else { io.Println(string(amino.MustMarshalJSON(tx))) } + return nil } diff --git a/tm2/pkg/crypto/keys/client/sign.go b/tm2/pkg/crypto/keys/client/sign.go index 87833165063..f935830a184 100644 --- a/tm2/pkg/crypto/keys/client/sign.go +++ b/tm2/pkg/crypto/keys/client/sign.go @@ -29,11 +29,12 @@ type keyOpts struct { type SignCfg struct { RootCfg *BaseCfg - TxPath string - ChainID string - AccountNumber uint64 - Sequence uint64 - NameOrBech32 string + TxPath string + ChainID string + AccountNumber uint64 + Sequence uint64 + NameOrBech32 string + FetchAccountInfo bool } func NewSignCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { @@ -82,6 +83,13 @@ func (c *SignCfg) RegisterFlags(fs *flag.FlagSet) { 0, "account sequence to sign with", ) + + fs.BoolVar( + &c.FetchAccountInfo, + "fetch-account-info", + false, + "fetch account info from the blockchain if account number or sequence is not provided", + ) } func execSign(cfg *SignCfg, args []string, io commands.IO) error { @@ -157,6 +165,24 @@ func execSign(cfg *SignCfg, args []string, io commands.IO) error { } } + // Check if we need to fetch account information based on the new flag + if cfg.FetchAccountInfo && (cfg.AccountNumber == 0 || cfg.Sequence == 0) { + accountAddr := info.GetAddress() + + qopts := &QueryCfg{ + RootCfg: cfg.RootCfg, + Path: fmt.Sprintf("auth/accounts/%s", accountAddr), + } + qres, err := QueryHandler(qopts) + if err == nil { + var qret struct{ BaseAccount std.BaseAccount } + if err := amino.UnmarshalJSON(qres.Response.Data, &qret); err == nil { + cfg.AccountNumber = qret.BaseAccount.AccountNumber + cfg.Sequence = qret.BaseAccount.Sequence + } + } + } + // Prepare the signature ops sOpts := signOpts{ chainID: cfg.ChainID, @@ -185,6 +211,22 @@ func signTx( signOpts signOpts, keyOpts keyOpts, ) error { + // Save the signature + signers := tx.GetSigners() + if tx.Signatures == nil { + for range signers { + tx.Signatures = append(tx.Signatures, std.Signature{ + PubKey: nil, // Zero signature + Signature: nil, // Zero signature + }) + } + } + + // Validate the tx after signing + if err := tx.ValidateBasic(); err != nil { + return fmt.Errorf("unable to validate transaction, %w", err) + } + signBytes, err := tx.GetSignBytes( signOpts.chainID, signOpts.accountNumber, @@ -204,38 +246,21 @@ func signTx( return fmt.Errorf("unable to sign transaction bytes, %w", err) } - // Save the signature - if tx.Signatures == nil { - tx.Signatures = make([]std.Signature, 0, 1) - } - - // Check if the signature needs to be overwritten - for index, signature := range tx.Signatures { - if !signature.PubKey.Equals(pub) { - continue - } + addr := pub.Address() - // Save the signature - tx.Signatures[index] = std.Signature{ - PubKey: pub, - Signature: sig, + found := false + for i := range tx.Signatures { + if signers[i] == addr { + found = true + tx.Signatures[i] = std.Signature{ + PubKey: pub, + Signature: sig, + } } - - return nil } - // Append the signature, since it wasn't - // present before - tx.Signatures = append( - tx.Signatures, std.Signature{ - PubKey: pub, - Signature: sig, - }, - ) - - // Validate the tx after signing - if err := tx.ValidateBasic(); err != nil { - return fmt.Errorf("unable to validate transaction, %w", err) + if !found { + return fmt.Errorf("address %v (%s) not in signer set", addr, keyOpts.keyName) } return nil diff --git a/tm2/pkg/crypto/keys/client/sign_test.go b/tm2/pkg/crypto/keys/client/sign_test.go index eb30dc17162..772e0163358 100644 --- a/tm2/pkg/crypto/keys/client/sign_test.go +++ b/tm2/pkg/crypto/keys/client/sign_test.go @@ -403,7 +403,7 @@ func TestSign_SignTx(t *testing.T) { assert.True(t, savedTx.Signatures[0].PubKey.Equals(info.GetPubKey())) }) - t.Run("existing signature list", func(t *testing.T) { + t.Run("sign an already signed transaction", func(t *testing.T) { t.Parallel() var ( @@ -415,10 +415,10 @@ func TestSign_SignTx(t *testing.T) { } mnemonic = generateTestMnemonic(t) - keyName = "generated-key" + key1 = "generated-key" encryptPassword = "encrypt" - anotherKey = "another-key" + key2 = "another-key" tx = std.Tx{ Fee: std.Fee{ @@ -436,21 +436,25 @@ func TestSign_SignTx(t *testing.T) { require.NoError(t, err) // Create an initial account - info, err := kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) + info1, err := kb.CreateAccount(key1, mnemonic, "", encryptPassword, 0, 0) require.NoError(t, err) // Create a new account - anotherKeyInfo, err := kb.CreateAccount(anotherKey, mnemonic, "", encryptPassword, 0, 1) + info2, err := kb.CreateAccount(key2, mnemonic, "", encryptPassword, 0, 1) require.NoError(t, err) // Generate the signature signBytes, err := tx.GetSignBytes("id", 1, 0) require.NoError(t, err) - signature, pubKey, err := kb.Sign(anotherKey, encryptPassword, signBytes) + signature, pubKey, err := kb.Sign(key2, encryptPassword, signBytes) require.NoError(t, err) tx.Signatures = []std.Signature{ + { + PubKey: nil, + Signature: nil, + }, { PubKey: pubKey, Signature: signature, @@ -461,10 +465,10 @@ func TestSign_SignTx(t *testing.T) { // for validation to complete tx.Msgs = []std.Msg{ bank.MsgSend{ - FromAddress: info.GetAddress(), + FromAddress: info1.GetAddress(), }, bank.MsgSend{ - FromAddress: anotherKeyInfo.GetAddress(), + FromAddress: info2.GetAddress(), }, } @@ -504,7 +508,8 @@ func TestSign_SignTx(t *testing.T) { kbHome, "--tx-path", txFile.Name(), - keyName, + "--fetch-account-info", + key1, } // Run the command @@ -518,11 +523,124 @@ func TestSign_SignTx(t *testing.T) { require.NoError(t, amino.UnmarshalJSON(savedTxRaw, &savedTx)) require.Len(t, savedTx.Signatures, 2) - assert.True(t, savedTx.Signatures[0].PubKey.Equals(anotherKeyInfo.GetPubKey())) - assert.True(t, savedTx.Signatures[1].PubKey.Equals(info.GetPubKey())) + assert.True(t, savedTx.Signatures[0].PubKey.Equals(info1.GetPubKey())) + assert.True(t, savedTx.Signatures[1].PubKey.Equals(info2.GetPubKey())) assert.NotEqual(t, savedTx.Signatures[0].Signature, savedTx.Signatures[1].Signature) }) + t.Run("Message signer and signature count mismatch", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + Quiet: true, + } + + mnemonic = generateTestMnemonic(t) + key1 = "generated-key" + encryptPassword = "encrypt" + + key2 = "another-key" + + tx = std.Tx{ + Fee: std.Fee{ + GasWanted: 10, + GasFee: std.Coin{ + Amount: 10, + Denom: "ugnot", + }, + }, + } + ) + + // Generate a key in the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + // Create an initial account + info1, err := kb.CreateAccount(key1, mnemonic, "", encryptPassword, 0, 0) + require.NoError(t, err) + + // Create a new account + info2, err := kb.CreateAccount(key2, mnemonic, "", encryptPassword, 0, 1) + require.NoError(t, err) + + // Generate the signature + signBytes, err := tx.GetSignBytes("id", 1, 0) + require.NoError(t, err) + + signature, pubKey, err := kb.Sign(key2, encryptPassword, signBytes) + require.NoError(t, err) + + // Add only one signature to the transaction (from key2), simulating a mismatch + tx.Signatures = []std.Signature{ + { + PubKey: pubKey, + Signature: signature, + }, + } + + // We need to prepare the message signers as well + // for validation to complete + tx.Msgs = []std.Msg{ + bank.MsgSend{ + FromAddress: info1.GetAddress(), + }, + bank.MsgSend{ + FromAddress: info2.GetAddress(), + }, + } + + // Create an empty tx file + txFile, err := os.CreateTemp("", "") + require.NoError(t, err) + + // Marshal the tx and write it to the file + encodedTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + _, err = txFile.Write(encodedTx) + require.NoError(t, err) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + // Create the command IO + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n%s\n", + encryptPassword, + encryptPassword, + ), + ), + ) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "sign", + "--insecure-password-stdin", + "--home", + kbHome, + "--tx-path", + txFile.Name(), + "--fetch-account-info", + key1, + } + + // Run the command + err = cmd.ParseAndRun(ctx, args) + + require.Error(t, err) + require.Contains(t, err.Error(), "unable to sign transaction, unable to validate transaction, unauthorized error") + }) + t.Run("overwrite existing signature", func(t *testing.T) { t.Parallel() diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index 49662b47a55..7bce05ebf0c 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -139,11 +139,19 @@ func NewAnteHandler(ak AccountKeeper, bank BankKeeperI, sigGasConsumer Signature stdSigs := tx.GetSignatures() for i := 0; i < len(stdSigs); i++ { + var isNewAccount bool // skip the fee payer, account is cached and fees were deducted already if i != 0 { signerAccs[i], res = GetSignerAcc(newCtx, ak, signerAddrs[i]) + // only create new account when tx is a sponsor transaction if !res.IsOK() { - return newCtx, res, true + if tx.IsSponsorTx() { + isNewAccount = true + signerAccs[i] = ak.NewAccountWithAddress(newCtx, signerAddrs[i]) + signerAccs[i].SetPubKey(stdSigs[i].PubKey) + } else { + return newCtx, res, true + } } } @@ -152,8 +160,11 @@ func NewAnteHandler(ak AccountKeeper, bank BankKeeperI, sigGasConsumer Signature if isGenesis && !opts.VerifyGenesisSignatures { // No signatures are needed for genesis. } else { + // Check if the account is being created for the first time on-chain + // e.g., during genesis or as a new account created indirectly through a sponsor transaction + newAccount := isGenesis || isNewAccount // Check signature - signBytes, err := GetSignBytes(newCtx.ChainID(), tx, sacc, isGenesis) + signBytes, err := GetSignBytes(newCtx.ChainID(), tx, sacc, newAccount) if err != nil { return newCtx, res, true } @@ -433,9 +444,9 @@ func SetGasMeter(simulate bool, ctx sdk.Context, gasLimit int64) sdk.Context { // GetSignBytes returns a slice of bytes to sign over for a given transaction // and an account. -func GetSignBytes(chainID string, tx std.Tx, acc std.Account, genesis bool) ([]byte, error) { +func GetSignBytes(chainID string, tx std.Tx, acc std.Account, isNewAccount bool) ([]byte, error) { var accNum uint64 - if !genesis { + if !isNewAccount { accNum = acc.GetAccountNumber() } diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index be4167a6238..f9a25988762 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -99,12 +99,6 @@ func TestAnteHandlerSigErrors(t *testing.T) { privs, accNums, seqs = []crypto.PrivKey{priv1, priv2, priv3}, []uint64{0, 1, 2}, []uint64{0, 0, 0} tx = tu.NewTestTx(t, ctx.ChainID(), msgs, privs, accNums, seqs, fee) checkInvalidTx(t, anteHandler, ctx, tx, false, std.UnknownAddressError{}) - - // save the first account, but second is still unrecognized - acc1 := env.acck.NewAccountWithAddress(ctx, addr1) - acc1.SetCoins(std.Coins{fee.GasFee}) - env.acck.SetAccount(ctx, acc1) - checkInvalidTx(t, anteHandler, ctx, tx, false, std.UnknownAddressError{}) } // Test logic around account number checking with one signer and many signers. @@ -586,6 +580,132 @@ func TestAnteHandlerSetPubKey(t *testing.T) { require.Nil(t, acc2.GetPubKey()) } +func TestAnteHandlerSponsorTx(t *testing.T) { + t.Parallel() + + // setup + env := setupTestEnv() + anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) + ctx := env.ctx + + // keys and addresses + priv1, _, addr1 := tu.KeyTestPubAddr() + priv2, _, addr2 := tu.KeyTestPubAddr() + + // Only create and set up account acc1 (addr1) + acc1 := env.acck.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(tu.NewTestCoins()) + require.NoError(t, acc1.SetAccountNumber(0)) + env.acck.SetAccount(ctx, acc1) + + t.Run("TestRegularTransaction", func(t *testing.T) { + // Create a regular transaction with addr2, which doesn't exist yet + msg1 := tu.NewTestMsg(addr2) + msg2 := tu.NewTestMsg(addr2) + msgs := []std.Msg{msg1, msg2} + + privs, accnums, seqs := []crypto.PrivKey{priv2}, []uint64{0}, []uint64{0} + fee := tu.NewTestFee() + tx := tu.NewTestTx(t, ctx.ChainID(), msgs, privs, accnums, seqs, fee) + + // Regular transactions will fail if addr2 has no account on-chain + checkInvalidTx(t, anteHandler, ctx, tx, false, std.UnknownAddressError{}) + + // Ensure addr2 account does not exist + acc2 := env.acck.GetAccount(ctx, addr2) + require.Nil(t, acc2) + }) + + t.Run("TestSponsorTransactionTwoSigners", func(t *testing.T) { + t.Parallel() + // Create a sponsor transaction with addr1 (sponsor) and addr2 (sponsoree) + msg1 := tu.NewTestMsg(addr1) // First message by addr1 (sponsor) + msg2 := tu.NewTestMsg(addr2) // Second message by addr2 (sponsoree) + msgs := []std.Msg{msg1, msg2} + + privs := []crypto.PrivKey{priv1, priv2} + accnums := []uint64{acc1.GetAccountNumber(), 0} + seqs := []uint64{acc1.GetSequence(), 0} + + fee := tu.NewTestFee() + tx := tu.NewTestTx(t, ctx.ChainID(), msgs, privs, accnums, seqs, fee) + + // Sponsor transaction will work, creating addr2 if it doesn't exist + checkValidTx(t, anteHandler, ctx, tx, false) + + // Check account states + acc1 = env.acck.GetAccount(ctx, addr1) + require.Equal(t, acc1.GetPubKey(), priv1.PubKey()) + require.Equal(t, acc1.GetAccountNumber(), uint64(0)) + require.Equal(t, acc1.GetSequence(), uint64(1)) + + acc2 := env.acck.GetAccount(ctx, addr2) + require.NotNil(t, acc2, "Expected addr2 account to be created by sponsor transaction") + require.Equal(t, acc2.GetPubKey(), priv2.PubKey()) + require.Equal(t, acc2.GetAccountNumber(), uint64(2)) + require.Equal(t, acc2.GetSequence(), uint64(1)) + }) +} + +func TestAnteHandlerSponsorTxMultipleSigners(t *testing.T) { + t.Parallel() + + // setup + env := setupTestEnv() + anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) + ctx := env.ctx + + // keys and addresses + priv1, _, addr1 := tu.KeyTestPubAddr() + priv2, _, addr2 := tu.KeyTestPubAddr() + priv3, _, addr3 := tu.KeyTestPubAddr() + + // Only create and set up account acc1 (addr1) + acc1 := env.acck.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(tu.NewTestCoins()) + require.NoError(t, acc1.SetAccountNumber(0)) + env.acck.SetAccount(ctx, acc1) + + t.Run("TestSponsorTransactionMultipleSigners", func(t *testing.T) { + t.Parallel() + + // Create multiple messages for multiple signers + msg1 := tu.NewTestMsg(addr1) // First message by addr1 (sponsor) + msg2 := tu.NewTestMsg(addr2) // Second message by addr2 + msg3 := tu.NewTestMsg(addr2) // Another message by addr2 + msg4 := tu.NewTestMsg(addr3) // Message by addr3 + msg5 := tu.NewTestMsg(addr3) // Another message by addr3 + msgs := []std.Msg{msg1, msg2, msg3, msg4, msg5} + + privs := []crypto.PrivKey{priv1, priv2, priv3} + accnums := []uint64{acc1.GetAccountNumber(), 0, 0} + seqs := []uint64{acc1.GetSequence(), 0, 0} + fee := tu.NewTestFee() + tx := tu.NewTestTx(t, ctx.ChainID(), msgs, privs, accnums, seqs, fee) + + // Check sponsor transaction with multiple signers + checkValidTx(t, anteHandler, ctx, tx, false) + + // Check the state of accounts + acc1 = env.acck.GetAccount(ctx, addr1) + require.Equal(t, acc1.GetPubKey(), priv1.PubKey()) + require.Equal(t, acc1.GetAccountNumber(), uint64(0)) + require.Equal(t, acc1.GetSequence(), uint64(1)) + + acc2 := env.acck.GetAccount(ctx, addr2) + require.NotNil(t, acc2, "Expected addr2 account to be created by sponsor transaction") + require.Equal(t, acc2.GetPubKey(), priv2.PubKey()) + require.Equal(t, acc2.GetAccountNumber(), uint64(2)) + require.Equal(t, acc2.GetSequence(), uint64(1)) + + acc3 := env.acck.GetAccount(ctx, addr3) + require.NotNil(t, acc3, "Expected addr3 account to be created by sponsor transaction") + require.Equal(t, acc3.GetPubKey(), priv3.PubKey()) + require.Equal(t, acc3.GetAccountNumber(), uint64(3)) + require.Equal(t, acc3.GetSequence(), uint64(1)) + }) +} + func TestProcessPubKey(t *testing.T) { t.Parallel() diff --git a/tm2/pkg/std/package.go b/tm2/pkg/std/package.go index 76e1f9fc4ad..8191bf616c4 100644 --- a/tm2/pkg/std/package.go +++ b/tm2/pkg/std/package.go @@ -13,6 +13,9 @@ var Package = amino.RegisterPackage(amino.NewPackage( // Account &BaseAccount{}, "BaseAccount", + // Fee + &Fee{}, "Fee", + // MemFile/MemPackage MemFile{}, "MemFile", MemPackage{}, "MemPackage", diff --git a/tm2/pkg/std/tx.go b/tm2/pkg/std/tx.go index 4fd7af4a641..837f4157e7c 100644 --- a/tm2/pkg/std/tx.go +++ b/tm2/pkg/std/tx.go @@ -60,6 +60,29 @@ func (tx Tx) ValidateBasic() error { return nil } +// IsSponsorTx checks if the transaction is a valid sponsor transaction +func (tx Tx) IsSponsorTx() bool { + // At least two message in the transaction + if len(tx.Msgs) <= 1 { + return false + } + + // More than one signer + signers := tx.GetSigners() + if len(signers) <= 1 { + return false + } + + // The first signer is different from all other signers + for i := 1; i < len(signers); i++ { + if signers[0] == signers[i] { + return false + } + } + + return true +} + // CountSubKeys counts the total number of keys for a multi-sig public key. func CountSubKeys(pub crypto.PubKey) int { v, ok := pub.(multisig.PubKeyMultisigThreshold) diff --git a/tm2/pkg/std/tx_test.go b/tm2/pkg/std/tx_test.go new file mode 100644 index 00000000000..18cc7ec314e --- /dev/null +++ b/tm2/pkg/std/tx_test.go @@ -0,0 +1,319 @@ +package std + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/crypto/multisig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Mock implementations of Msg interfaces +type mockMsg struct { + caller crypto.Address + msgType string +} + +func (m mockMsg) ValidateBasic() error { + return nil +} + +func (m mockMsg) GetSignBytes() []byte { + return nil +} + +func (m mockMsg) GetSigners() []crypto.Address { + return []crypto.Address{m.caller} +} + +func (m mockMsg) Route() string { + return "" +} + +func (m mockMsg) Type() string { + return m.msgType +} + +func TestNewTx(t *testing.T) { + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + }, + } + + fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) + + sigs := []Signature{ + { + Signature: []byte{0x00}, + }, + } + + memo := "test memo" + + tx := NewTx(msgs, fee, sigs, memo) + require.Equal(t, msgs, tx.GetMsgs()) + require.Equal(t, fee, tx.Fee) + require.Equal(t, sigs, tx.GetSignatures()) + require.Equal(t, memo, tx.GetMemo()) +} + +func TestValidateBasic(t *testing.T) { + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + }, + } + + fee := NewFee(maxGasWanted, Coin{Denom: "atom", Amount: 10}) + sigs := []Signature{ + { + Signature: []byte{0x00}, + }, + } + + tx := NewTx(msgs, fee, sigs, "test memo") + + // Valid case + require.NoError(t, tx.ValidateBasic()) + + // Invalid gas case + invalidFee := NewFee(maxGasWanted+1, Coin{Denom: "atom", Amount: 10}) + txInvalidGas := NewTx(msgs, invalidFee, sigs, "test memo") + require.Error(t, txInvalidGas.ValidateBasic(), "expected gas overflow error") + + // Invalid fee case + invalidFeeAmount := NewFee(1000, Coin{Denom: "atom", Amount: -10}) + txInvalidFee := NewTx(msgs, invalidFeeAmount, sigs, "test memo") + require.Error(t, txInvalidFee.ValidateBasic(), "expected insufficient fee error") + + // No signatures case + txNoSigs := NewTx(msgs, fee, []Signature{}, "test memo") + require.Error(t, txNoSigs.ValidateBasic(), "expected no signatures error") + + // Wrong number of signers case + wrongSigs := []Signature{ + { + Signature: []byte{0x00}, + }, + { + Signature: []byte{0x01}, + }, + } + txWrongSigs := NewTx(msgs, fee, wrongSigs, "test memo") + require.Error(t, txWrongSigs.ValidateBasic(), "expected wrong number of signers error") +} + +func TestCountSubKeys(t *testing.T) { + // Single key case + pubKey := ed25519.GenPrivKey().PubKey() + require.Equal(t, 1, CountSubKeys(pubKey)) + + // Multi-sig case + // Assuming multisig.PubKeyMultisigThreshold is correctly implemented for testing purposes + pubKeys := []crypto.PubKey{ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey()} + multisigPubKey := multisig.NewPubKeyMultisigThreshold(2, pubKeys) + require.Equal(t, len(pubKeys), CountSubKeys(multisigPubKey)) +} + +func TestGetSigners(t *testing.T) { + // Single signer case + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + msgType: "call", + }, + } + tx := NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) + + // Duplicate signers case + msgs = []Msg{ + mockMsg{ + caller: addr, + msgType: "send", + }, + mockMsg{ + caller: addr, + msgType: "send", + }, + } + + tx = NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) + + // Multiple unique signers case + addr2, _ := crypto.AddressFromBech32("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + msgs = []Msg{ + mockMsg{ + caller: addr, + msgType: "call", + }, + mockMsg{ + caller: addr2, + msgType: "run", + }, + } + tx = NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr, addr2}, tx.GetSigners()) +} + +func TestGetSignBytes(t *testing.T) { + msgs := []Msg{} + fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) + sigs := []Signature{} + tx := NewTx(msgs, fee, sigs, "test memo") + chainID := "test-chain" + accountNumber := uint64(1) + sequence := uint64(1) + + signBytes, err := tx.GetSignBytes(chainID, accountNumber, sequence) + require.NoError(t, err) + require.NotEmpty(t, signBytes) +} + +func TestIsSponsorTx(t *testing.T) { + addr1, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + addr2, _ := crypto.AddressFromBech32("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + addr3, _ := crypto.AddressFromBech32("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") + + tests := []struct { + name string + msgs []Msg + expected bool + }{ + { + name: "message with different signers", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "send", + }, + mockMsg{ + caller: addr2, + msgType: "call", + }, + }, + expected: true, + }, + { + name: "single message", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "send", + }, + }, + expected: false, + }, + { + name: "messages with same signer", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "send", + }, + mockMsg{ + caller: addr1, + msgType: "call", + }, + }, + expected: false, + }, + { + name: "different message types with different signers", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "call", + }, + mockMsg{ + caller: addr2, + msgType: "send", + }, + }, + expected: true, + }, + { + name: "same message type with different signers", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "call", + }, + mockMsg{ + caller: addr2, + msgType: "call", + }, + }, + expected: true, + }, + { + name: "empty messages", + msgs: []Msg{}, + expected: false, + }, + { + name: "multiple messages with at least two having the same signer", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "send", + }, + mockMsg{ + caller: addr2, + msgType: "call", + }, + mockMsg{ + caller: addr2, + msgType: "send", + }, + }, + expected: true, + }, + { + name: "multiple messages with all different signers", + msgs: []Msg{ + mockMsg{ + caller: addr1, + msgType: "send", + }, + mockMsg{ + caller: addr2, + msgType: "call", + }, + mockMsg{ + caller: addr3, + msgType: "send", + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := Tx{ + Msgs: tt.msgs, + } + result := tx.IsSponsorTx() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFee(t *testing.T) { + fee := Fee{ + GasWanted: 1000, + GasFee: Coin{Denom: "ugnot", Amount: 10}, + } + + expectedBytes := []byte(`{"gas_wanted":"1000","gas_fee":"10ugnot"}`) + + require.Equal(t, expectedBytes, fee.Bytes(), "Bytes should return the correct JSON representation") +}