Skip to content

Commit

Permalink
Add support for broadcasting arbitrary messages as test users (#186)
Browse files Browse the repository at this point in the history
* wip: Adding broadcast interface and cosmos chain broadcaster

* chore: added additional docstrings

* chore: cleaning up NewLocalKeyringFromDockerContainer function

* chore: added test for broadcast

* chore: reverted default values

* chore: update keyring to use mapping instead of single instance

* chore: added wait in the create user tests

* fix: addressing PR feedback and updating to use official docker client

* chore: use buf.Reset()

* addressing PR feedback

* chore: addressing PR comments

* chore: updating test to verify target account received funds
  • Loading branch information
chatton authored Jul 6, 2022
1 parent 76314a6 commit c709d57
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 1 deletion.
40 changes: 40 additions & 0 deletions broadcast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ibctest

import (
"context"

"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/strangelove-ventures/ibctest/broadcast"
)

// BroadcastTx uses the provided Broadcaster to broadcast all the provided messages which will be signed
// by the User provided. The sdk.TxResponse and an error are returned.
func BroadcastTx(ctx context.Context, broadcaster broadcast.Broadcaster, broadcastingUser broadcast.User, msgs ...sdk.Msg) (sdk.TxResponse, error) {
for _, msg := range msgs {
if err := msg.ValidateBasic(); err != nil {
return sdk.TxResponse{}, err
}
}

f, err := broadcaster.GetFactory(ctx, broadcastingUser)
if err != nil {
return sdk.TxResponse{}, err
}

cc, err := broadcaster.GetClientContext(ctx, broadcastingUser)
if err != nil {
return sdk.TxResponse{}, err
}

if err := tx.BroadcastTx(cc, f, msgs...); err != nil {
return sdk.TxResponse{}, err
}

txBytes, err := broadcaster.GetTxResponseBytes(ctx, broadcastingUser)
if err != nil {
return sdk.TxResponse{}, err
}

return broadcaster.UnmarshalTxResponseBytes(ctx, txBytes)
}
28 changes: 28 additions & 0 deletions broadcast/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package broadcast

import (
"context"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
)

type ClientContextOpt func(clientContext client.Context) client.Context

type FactoryOpt func(factory tx.Factory) tx.Factory

type User interface {
GetKeyName() string
Bech32Address(bech32Prefix string) string
}

// Broadcaster implementations can broadcast messages as the provided user.
type Broadcaster interface {
ConfigureFactoryOptions(opts ...FactoryOpt)
ConfigureClientContextOptions(opts ...ClientContextOpt)
GetFactory(ctx context.Context, user User) (tx.Factory, error)
GetClientContext(ctx context.Context, user User) (client.Context, error)
GetTxResponseBytes(ctx context.Context, user User) ([]byte, error)
UnmarshalTxResponseBytes(ctx context.Context, bytes []byte) (sdk.TxResponse, error)
}
173 changes: 173 additions & 0 deletions chain/cosmos/broadcaster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cosmos

import (
"bytes"
"context"
"fmt"
"path/filepath"
"testing"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/strangelove-ventures/ibctest/broadcast"
"github.com/strangelove-ventures/ibctest/internal/dockerutil"
)

var _ broadcast.Broadcaster = &Broadcaster{}

type Broadcaster struct {
// buf stores the output sdk.TxResponse when broadcast.Tx is invoked.
buf *bytes.Buffer
// keyrings is a mapping of keyrings which point to a temporary test directory. The contents
// of this directory are copied from the node container for the specific user.
keyrings map[broadcast.User]keyring.Keyring

// chain is a reference to the CosmosChain instance which will be the target of the messages.
chain *CosmosChain
// t is the testing.T for the current test.
t *testing.T

// factoryOptions is a slice of broadcast.FactoryOpt which enables arbitrary configuration of the tx.Factory.
factoryOptions []broadcast.FactoryOpt
// clientContextOptions is a slice of broadcast.ClientContextOpt which enables arbitrary configuration of the client.Context.
clientContextOptions []broadcast.ClientContextOpt
}

// NewBroadcaster returns a instance of Broadcaster which can be used with broadcast.Tx to
// broadcast messages sdk messages.
func NewBroadcaster(t *testing.T, chain *CosmosChain) *Broadcaster {
return &Broadcaster{
t: t,
chain: chain,
buf: &bytes.Buffer{},
keyrings: map[broadcast.User]keyring.Keyring{},
}
}

// ConfigureFactoryOptions ensure the given configuration functions are run when calling GetFactory
// after all default options have been applied.
func (b *Broadcaster) ConfigureFactoryOptions(opts ...broadcast.FactoryOpt) {
b.factoryOptions = append(b.factoryOptions, opts...)
}

// ConfigureClientContextOptions ensure the given configuration functions are run when calling GetClientContext
// after all default options have been applied.
func (b *Broadcaster) ConfigureClientContextOptions(opts ...broadcast.ClientContextOpt) {
b.clientContextOptions = append(b.clientContextOptions, opts...)
}

// GetFactory returns an instance of tx.Factory that is configured with this Broadcaster's CosmosChain
// and the provided user. ConfigureFactoryOptions can be used to specify arbitrary options to configure the returned
// factory.
func (b *Broadcaster) GetFactory(ctx context.Context, user broadcast.User) (tx.Factory, error) {
clientContext, err := b.GetClientContext(ctx, user)
if err != nil {
return tx.Factory{}, err
}

sdkAdd, err := sdk.AccAddressFromBech32(user.Bech32Address(b.chain.Config().Bech32Prefix))
if err != nil {
return tx.Factory{}, err
}

accNumber, err := clientContext.AccountRetriever.GetAccount(clientContext, sdkAdd)
if err != nil {
return tx.Factory{}, err
}

f := b.defaultTxFactory(clientContext, accNumber.GetAccountNumber())
for _, opt := range b.factoryOptions {
f = opt(f)
}
return f, nil
}

// GetClientContext returns a client context that is configured with this Broadcaster's CosmosChain and
// the provided user. ConfigureClientContextOptions can be used to configure arbitrary options to configure the returned
// client.Context.
func (b *Broadcaster) GetClientContext(ctx context.Context, user broadcast.User) (client.Context, error) {
chain := b.chain
cn := chain.getFullNode()

_, ok := b.keyrings[user]
if !ok {
localDir := b.t.TempDir()
containerKeyringDir := filepath.Join(cn.HomeDir(), "keyring-test")
kr, err := dockerutil.NewLocalKeyringFromDockerContainer(ctx, cn.DockerClient, localDir, containerKeyringDir, cn.containerID)
if err != nil {
return client.Context{}, err
}
b.keyrings[user] = kr
}

sdkAdd, err := sdk.AccAddressFromBech32(user.Bech32Address(chain.Config().Bech32Prefix))
if err != nil {
return client.Context{}, err
}

clientContext := b.defaultClientContext(user, sdkAdd)
for _, opt := range b.clientContextOptions {
clientContext = opt(clientContext)
}
return clientContext, nil
}

// GetTxResponseBytes returns the sdk.TxResponse bytes which returned from broadcast.Tx.
func (b *Broadcaster) GetTxResponseBytes(ctx context.Context, user broadcast.User) ([]byte, error) {
if b.buf == nil || b.buf.Len() == 0 {
return nil, fmt.Errorf("empty buffer, transaction has not been executed yet")
}
return b.buf.Bytes(), nil
}

// UnmarshalTxResponseBytes accepts the sdk.TxResponse bytes and unmarshalls them into an
// instance of sdk.TxResponse.
func (b *Broadcaster) UnmarshalTxResponseBytes(ctx context.Context, bytes []byte) (sdk.TxResponse, error) {
resp := sdk.TxResponse{}
if err := defaultEncoding.Marshaler.UnmarshalJSON(bytes, &resp); err != nil {
return sdk.TxResponse{}, err
}
return resp, nil
}

// defaultClientContext returns a default client context configured with the user as the sender.
func (b *Broadcaster) defaultClientContext(fromUser broadcast.User, sdkAdd sdk.AccAddress) client.Context {
// initialize a clean buffer each time
b.buf.Reset()
kr := b.keyrings[fromUser]
cn := b.chain.getFullNode()
return cn.CliContext().
WithOutput(b.buf).
WithFrom(fromUser.Bech32Address(b.chain.Config().Bech32Prefix)).
WithFromAddress(sdkAdd).
WithFromName(fromUser.GetKeyName()).
WithSkipConfirmation(true).
WithAccountRetriever(authtypes.AccountRetriever{}).
WithKeyring(kr).
WithBroadcastMode(flags.BroadcastBlock).
WithCodec(defaultEncoding.Marshaler).
WithHomeDir(cn.Home)

}

// defaultTxFactory creates a new Factory with default configuration.
func (b *Broadcaster) defaultTxFactory(clientCtx client.Context, accountNumber uint64) tx.Factory {
chainConfig := b.chain.Config()
return tx.Factory{}.
WithAccountNumber(accountNumber).
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT).
WithGasAdjustment(chainConfig.GasAdjustment).
WithGas(flags.DefaultGasLimit).
WithGasPrices(chainConfig.GasPrices).
WithMemo("ibctest").
WithTxConfig(clientCtx.TxConfig).
WithAccountRetriever(clientCtx.AccountRetriever).
WithKeybase(clientCtx.Keyring).
WithChainID(clientCtx.ChainID).
WithSimulateAndExecute(false)
}
97 changes: 96 additions & 1 deletion interchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import (
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/strangelove-ventures/ibctest"
"github.com/strangelove-ventures/ibctest/chain/cosmos"
"github.com/strangelove-ventures/ibctest/ibc"
"github.com/strangelove-ventures/ibctest/relayer/rly"
"github.com/strangelove-ventures/ibctest/test"
"github.com/strangelove-ventures/ibctest/testreporter"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"

transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
)

func TestInterchain_DuplicateChain(t *testing.T) {
Expand Down Expand Up @@ -197,7 +203,7 @@ func TestInterchain_CreateUser(t *testing.T) {
require.NotEmpty(t, mnemonic)

user := ibctest.GetAndFundTestUserWithMnemonic(t, ctx, keyName, mnemonic, 10000, gaia0)

require.NoError(t, test.WaitForBlocks(ctx, 2, gaia0))
require.NotEmpty(t, user.Address)
require.NotEmpty(t, user.KeyName)

Expand All @@ -210,6 +216,7 @@ func TestInterchain_CreateUser(t *testing.T) {
t.Run("without mnemonic", func(t *testing.T) {
keyName := "regular-user-name"
users := ibctest.GetAndFundTestUsers(t, ctx, keyName, 10000, gaia0)
require.NoError(t, test.WaitForBlocks(ctx, 2, gaia0))
require.Len(t, users, 1)
require.NotEmpty(t, users[0].Address)
require.NotEmpty(t, users[0].KeyName)
Expand All @@ -220,6 +227,84 @@ func TestInterchain_CreateUser(t *testing.T) {
})
}

func TestCosmosChain_BroadcastTx(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}

t.Parallel()

home := ibctest.TempDir(t)
client, network := ibctest.DockerSetup(t)

cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*ibctest.ChainSpec{
// Two otherwise identical chains that only differ by ChainID.
{Name: "gaia", ChainName: "g1", Version: "v7.0.1", ChainConfig: ibc.ChainConfig{ChainID: "cosmoshub-0"}},
{Name: "gaia", ChainName: "g2", Version: "v7.0.1", ChainConfig: ibc.ChainConfig{ChainID: "cosmoshub-1"}},
})

chains, err := cf.Chains(t.Name())
require.NoError(t, err)

gaia0, gaia1 := chains[0], chains[1]

r := ibctest.NewBuiltinRelayerFactory(ibc.CosmosRly, zaptest.NewLogger(t)).Build(
t, client, network, home,
)

pathName := "p"
ic := ibctest.NewInterchain().
AddChain(gaia0).
AddChain(gaia1).
AddRelayer(r, "r").
AddLink(ibctest.InterchainLink{
Chain1: gaia0,
Chain2: gaia1,
Relayer: r,
Path: pathName,
})

rep := testreporter.NewNopReporter()
eRep := rep.RelayerExecReporter(t)

ctx := context.Background()
require.NoError(t, ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{
TestName: t.Name(),
HomeDir: home,
Client: client,
NetworkID: network,
}))

testUser := ibctest.GetAndFundTestUsers(t, ctx, "gaia-user-1", 10_000_000, gaia0)[0]

sendAmount := int64(10000)

t.Run("relayer starts", func(t *testing.T) {
require.NoError(t, r.StartRelayer(ctx, eRep, pathName))
})

t.Run("broadcast success", func(t *testing.T) {
b := cosmos.NewBroadcaster(t, gaia0.(*cosmos.CosmosChain))
transferAmount := types.Coin{Denom: gaia0.Config().Denom, Amount: types.NewInt(sendAmount)}

msg := transfertypes.NewMsgTransfer("transfer", "channel-0", transferAmount, testUser.Bech32Address(gaia0.Config().Bech32Prefix), testUser.Bech32Address(gaia1.Config().Bech32Prefix), clienttypes.NewHeight(1, 1000), 0)
resp, err := ibctest.BroadcastTx(ctx, b, testUser, msg)
require.NoError(t, err)
assertTransactionIsValid(t, resp)
})

t.Run("transfer success", func(t *testing.T) {
require.NoError(t, test.WaitForBlocks(ctx, 5, gaia0, gaia1))

srcDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", "channel-0", gaia0.Config().Denom))
dstIbcDenom := srcDenomTrace.IBCDenom()

dstFinalBalance, err := gaia1.GetBalance(ctx, testUser.Bech32Address(gaia1.Config().Bech32Prefix), dstIbcDenom)
require.NoError(t, err, "failed to get balance from dest chain")
require.Equal(t, sendAmount, dstFinalBalance)
})
}

// An external package that imports ibctest may not provide a GitSha when they provide a BlockDatabaseFile.
// The GitSha field is documented as optional, so this should succeed.
func TestInterchain_OmitGitSHA(t *testing.T) {
Expand Down Expand Up @@ -332,3 +417,13 @@ func TestInterchain_AddNil(t *testing.T) {
_ = ibctest.NewInterchain().AddRelayer(nil, "r")
})
}

func assertTransactionIsValid(t *testing.T, resp sdk.TxResponse) {
require.NotNil(t, resp)
require.NotEqual(t, 0, resp.GasUsed)
require.NotEqual(t, 0, resp.GasWanted)
require.Equal(t, uint32(0), resp.Code)
require.NotEmpty(t, resp.Data)
require.NotEmpty(t, resp.TxHash)
require.NotEmpty(t, resp.Events)
}
Loading

0 comments on commit c709d57

Please sign in to comment.