Add EVM signer to e2e test SigningAccounts (#1482)

* rename cosmos-sdk specific signers

* add evm_signer util

* add utilities for converting between addresses

* rename signers

* dont include e2e tests in docker image

* add evmsigner to e2e SigningAccount

* add new whale account that is an EthAccount

* use ethsecp256k1 for e2e SigningAccounts

* wait for evm tx to be committed to block

also add example evm tx tests! 🎉

* check remainined balance is expected

* check balance via evm
This commit is contained in:
Robert Pirtle 2023-03-01 17:05:53 -08:00 committed by GitHub
parent 214393ccfd
commit f051ea3a49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 415 additions and 57 deletions

View File

@ -2,3 +2,4 @@ out/
**/node_modules/ **/node_modules/
.git/ .git/
docs/ docs/
tests/

View File

@ -291,7 +291,7 @@ test-basic: test
# run end-to-end tests (local docker container must be built, see docker-build) # run end-to-end tests (local docker container must be built, see docker-build)
test-e2e: test-e2e:
export E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC='season bone lucky dog depth pond royal decide unknown device fruit inch clock trap relief horse morning taxi bird session throw skull avocado private'; \ export E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC='tent fitness boat among census primary pipe nose dream glance cave turtle electric fabric jacket shaft easy myself genuine this sibling pulse word unfold'; \
go test -failfast -count=1 -v ./tests/e2e/... go test -failfast -count=1 -v ./tests/e2e/...
test: test:

View File

@ -8,10 +8,18 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
ethtypes "github.com/ethereum/go-ethereum/core/types"
emtypes "github.com/tharsis/ethermint/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/tests/e2e/testutil" "github.com/kava-labs/kava/tests/e2e/testutil"
"github.com/kava-labs/kava/tests/util"
)
var (
minEvmGasPrice = big.NewInt(1e10) // akava
) )
func ukava(amt int64) sdk.Coin { func ukava(amt int64) sdk.Coin {
@ -26,14 +34,18 @@ func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite)) suite.Run(t, new(IntegrationTestSuite))
} }
// example test that queries kava chain & kava's EVM // example test that queries kava via SDK and EVM
func (suite *IntegrationTestSuite) TestChainID() { func (suite *IntegrationTestSuite) TestChainID() {
// TODO: make chain agnostic, don't hardcode expected chain ids // TODO: make chain agnostic, don't hardcode expected chain ids (in testutil)
expectedEvmNetworkId, err := emtypes.ParseChainID(testutil.ChainId)
suite.NoError(err)
// EVM query
evmNetworkId, err := suite.EvmClient.NetworkID(context.Background()) evmNetworkId, err := suite.EvmClient.NetworkID(context.Background())
suite.NoError(err) suite.NoError(err)
suite.Equal(big.NewInt(8888), evmNetworkId) suite.Equal(expectedEvmNetworkId, evmNetworkId)
// SDK query
nodeInfo, err := suite.Tm.GetNodeInfo(context.Background(), &tmservice.GetNodeInfoRequest{}) nodeInfo, err := suite.Tm.GetNodeInfo(context.Background(), &tmservice.GetNodeInfoRequest{})
suite.NoError(err) suite.NoError(err)
suite.Equal(testutil.ChainId, nodeInfo.DefaultNodeInfo.Network) suite.Equal(testutil.ChainId, nodeInfo.DefaultNodeInfo.Network)
@ -43,9 +55,55 @@ func (suite *IntegrationTestSuite) TestChainID() {
func (suite *IntegrationTestSuite) TestFundedAccount() { func (suite *IntegrationTestSuite) TestFundedAccount() {
funds := ukava(1e7) funds := ukava(1e7)
acc := suite.NewFundedAccount("example-acc", sdk.NewCoins(funds)) acc := suite.NewFundedAccount("example-acc", sdk.NewCoins(funds))
// check that the sdk & evm signers are for the same account
suite.Equal(acc.SdkAddress.String(), util.EvmToSdkAddress(acc.EvmAddress).String())
suite.Equal(acc.EvmAddress.Hex(), util.SdkToEvmAddress(acc.SdkAddress).Hex())
// check balance via SDK query
res, err := suite.Bank.Balance(context.Background(), banktypes.NewQueryBalanceRequest( res, err := suite.Bank.Balance(context.Background(), banktypes.NewQueryBalanceRequest(
acc.Address, "ukava", acc.SdkAddress, "ukava",
)) ))
suite.NoError(err) suite.NoError(err)
suite.Equal(funds, *res.Balance) suite.Equal(funds, *res.Balance)
// check balance via EVM query
akavaBal, err := suite.EvmClient.BalanceAt(context.Background(), acc.EvmAddress, nil)
suite.NoError(err)
suite.Equal(funds.Amount.MulRaw(1e12).BigInt(), akavaBal)
}
// example test that signs & broadcasts an EVM tx
func (suite *IntegrationTestSuite) TestTransferOverEVM() {
// fund an account that can perform the transfer
initialFunds := ukava(1e7) // 10 KAVA
acc := suite.NewFundedAccount("evm-test-transfer", sdk.NewCoins(initialFunds))
// get a rando account to send kava to
randomAddr := app.RandomAddress()
to := util.SdkToEvmAddress(randomAddr)
// example fetching of nonce (account sequence)
nonce, err := suite.EvmClient.PendingNonceAt(context.Background(), acc.EvmAddress)
suite.NoError(err)
suite.Equal(uint64(0), nonce) // sanity check. the account should have no prior txs
// transfer kava over EVM
kavaToTransfer := big.NewInt(1e18) // 1 KAVA; akava has 18 decimals.
req := util.EvmTxRequest{
Tx: ethtypes.NewTransaction(nonce, to, kavaToTransfer, 1e5, minEvmGasPrice, nil),
Data: "any ol' data to track this through the system",
}
res := acc.SignAndBroadcastEvmTx(req)
suite.NoError(res.Err)
suite.Equal(ethtypes.ReceiptStatusSuccessful, res.Receipt.Status)
// evm txs refund unused gas. so to know the expected balance we need to know how much gas was used.
ukavaUsedForGas := sdk.NewIntFromBigInt(minEvmGasPrice).
Mul(sdk.NewIntFromUint64(res.Receipt.GasUsed)).
QuoRaw(1e12) // convert akava to ukava
// expect (9 - gas used) KAVA remaining in account.
balance := suite.QuerySdkForBalances(acc.SdkAddress)
suite.Equal(sdk.NewInt(9e6).Sub(ukavaUsedForGas), balance.AmountOf("ukava"))
} }

View File

@ -60,11 +60,11 @@ inter-block-cache = true
# ["message.sender", "message.recipient"] # ["message.sender", "message.recipient"]
index-events = [] index-events = []
# IavlCacheSize set the size of the iavl tree cache. # IavlCacheSize set the size of the iavl tree cache.
# Default cache size is 50mb. # Default cache size is 50mb.
iavl-cache-size = 781250 iavl-cache-size = 781250
trace = true trace = true
# IAVLDisableFastNode enables or disables the fast node feature of IAVL. # IAVLDisableFastNode enables or disables the fast node feature of IAVL.
# Default is true. # Default is true.
iavl-disable-fastnode = true iavl-disable-fastnode = true

View File

@ -1,5 +1,5 @@
{ {
"genesis_time": "2023-02-15T18:28:01.981711Z", "genesis_time": "2023-02-28T20:05:58.764023Z",
"chain_id": "kavalocalnet_8888-1", "chain_id": "kavalocalnet_8888-1",
"initial_height": "1", "initial_height": "1",
"consensus_params": { "consensus_params": {
@ -187,6 +187,16 @@
} }
] ]
}, },
{
"@type": "/ethermint.types.v1.EthAccount",
"base_account": {
"address": "kava1q0dkky0505r555etn6u2nz4h4kjcg5y8dg863a",
"pub_key": null,
"account_number": "0",
"sequence": "0"
},
"code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
},
{ {
"@type": "/ethermint.types.v1.EthAccount", "@type": "/ethermint.types.v1.EthAccount",
"base_account": { "base_account": {
@ -268,6 +278,47 @@
"default_send_enabled": true "default_send_enabled": true
}, },
"balances": [ "balances": [
{
"address": "kava1q0dkky0505r555etn6u2nz4h4kjcg5y8dg863a",
"coins": [
{
"denom": "bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42",
"amount": "10000000000000000"
},
{
"denom": "bnb",
"amount": "10000000000000000"
},
{
"denom": "btcb",
"amount": "10000000000000000"
},
{
"denom": "busd",
"amount": "10000000000000000"
},
{
"denom": "hard",
"amount": "1000000000000000000"
},
{
"denom": "swp",
"amount": "1000000000000000000"
},
{
"denom": "ukava",
"amount": "1000000000000"
},
{
"denom": "usdx",
"amount": "10000000000000000"
},
{
"denom": "xrpb",
"amount": "10000000000000000"
}
]
},
{ {
"address": "kava1z3ytjpr6ancl8gw80z6f47z9smug7986x29vtj", "address": "kava1z3ytjpr6ancl8gw80z6f47z9smug7986x29vtj",
"coins": [ "coins": [

View File

@ -1,28 +1,45 @@
package testutil package testutil
import ( import (
"context"
"encoding/hex"
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
"time"
"github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/go-bip39" "github.com/cosmos/go-bip39"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/tharsis/ethermint/crypto/ethsecp256k1"
emtypes "github.com/tharsis/ethermint/types"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/tests/util" "github.com/kava-labs/kava/tests/util"
) )
type SigningAccount struct { var BroadcastTimeoutErr = errors.New("timed out waiting for tx to be committed to block")
name string
mnemonic string
signer *util.Signer
requests chan<- util.MsgRequest
responses <-chan util.MsgResponse
Address sdk.AccAddress type SigningAccount struct {
name string
mnemonic string
evmSigner *util.EvmSigner
evmReqChan chan<- util.EvmTxRequest
evmResChan <-chan util.EvmTxResponse
kavaSigner *util.KavaSigner
sdkReqChan chan<- util.KavaMsgRequest
sdkResChan <-chan util.KavaMsgResponse
EvmAddress common.Address
SdkAddress sdk.AccAddress
l *log.Logger l *log.Logger
} }
@ -42,11 +59,12 @@ func (suite *E2eTestSuite) AddNewSigningAccount(name string, hdPath *hd.BIP44Par
suite.Failf("can't create signing account", "account with name %s already exists", name) suite.Failf("can't create signing account", "account with name %s already exists", name)
} }
// Kava signing account for SDK side
privKeyBytes, err := hd.Secp256k1.Derive()(mnemonic, "", hdPath.String()) privKeyBytes, err := hd.Secp256k1.Derive()(mnemonic, "", hdPath.String())
suite.NoErrorf(err, "failed to derive private key from mnemonic for %s: %s", name, err) suite.NoErrorf(err, "failed to derive private key from mnemonic for %s: %s", name, err)
privKey := &secp256k1.PrivKey{Key: privKeyBytes} privKey := &ethsecp256k1.PrivKey{Key: privKeyBytes}
signer := util.NewSigner( kavaSigner := util.NewKavaSigner(
chainId, chainId,
suite.encodingConfig, suite.encodingConfig,
suite.Auth, suite.Auth,
@ -55,36 +73,58 @@ func (suite *E2eTestSuite) AddNewSigningAccount(name string, hdPath *hd.BIP44Par
100, 100,
) )
requests := make(chan util.MsgRequest) sdkReqChan := make(chan util.KavaMsgRequest)
responses, err := signer.Run(requests) sdkResChan, err := kavaSigner.Run(sdkReqChan)
suite.NoErrorf(err, "failed to start signer for account %s: %s", name, err) suite.NoErrorf(err, "failed to start signer for account %s: %s", name, err)
// Kava signing account for EVM side
evmChainId, err := emtypes.ParseChainID(chainId)
suite.NoErrorf(err, "unable to parse ethermint-compatible chain id from %s", chainId)
ecdsaPrivKey, err := crypto.HexToECDSA(hex.EncodeToString(privKeyBytes))
suite.NoError(err, "failed to generate ECDSA private key from bytes")
evmSigner, err := util.NewEvmSigner(
suite.EvmClient,
ecdsaPrivKey,
evmChainId,
)
suite.NoErrorf(err, "failed to create evm signer")
evmReqChan := make(chan util.EvmTxRequest)
evmResChan := evmSigner.Run(evmReqChan)
logger := log.New(os.Stdout, fmt.Sprintf("[%s] ", name), log.LstdFlags) logger := log.New(os.Stdout, fmt.Sprintf("[%s] ", name), log.LstdFlags)
// TODO: authenticated eth client.
suite.accounts[name] = &SigningAccount{ suite.accounts[name] = &SigningAccount{
name: name, name: name,
mnemonic: mnemonic, mnemonic: mnemonic,
signer: signer, l: logger,
requests: requests,
responses: responses,
l: logger,
Address: signer.Address(), evmSigner: evmSigner,
evmReqChan: evmReqChan,
evmResChan: evmResChan,
kavaSigner: kavaSigner,
sdkReqChan: sdkReqChan,
sdkResChan: sdkResChan,
EvmAddress: evmSigner.Address(),
SdkAddress: kavaSigner.Address(),
} }
return suite.accounts[name] return suite.accounts[name]
} }
// SignAndBroadcastKavaTx sends a request to the signer and awaits its response. // SignAndBroadcastKavaTx sends a request to the signer and awaits its response.
func (a *SigningAccount) SignAndBroadcastKavaTx(req util.MsgRequest) util.MsgResponse { func (a *SigningAccount) SignAndBroadcastKavaTx(req util.KavaMsgRequest) util.KavaMsgResponse {
a.l.Printf("broadcasting tx %+v\n", req.Data) a.l.Printf("broadcasting sdk tx %+v\n", req.Data)
// send the request to signer // send the request to signer
a.requests <- req a.sdkReqChan <- req
// TODO: timeout awaiting the response.
// block and await response // block and await response
// response is not returned until the msg is committed to a block // response is not returned until the msg is committed to a block
res := <-a.responses res := <-a.sdkResChan
// error will be set if response is not Code 0 (success) or Code 19 (already in mempool) // error will be set if response is not Code 0 (success) or Code 19 (already in mempool)
if res.Err != nil { if res.Err != nil {
@ -96,6 +136,50 @@ func (a *SigningAccount) SignAndBroadcastKavaTx(req util.MsgRequest) util.MsgRes
return res return res
} }
// EvmTxResponse is util.EvmTxResponse that also includes the Receipt, if available
type EvmTxResponse struct {
util.EvmTxResponse
Receipt *ethtypes.Receipt
}
// SignAndBroadcastEvmTx sends a request to the signer and awaits its response.
func (a *SigningAccount) SignAndBroadcastEvmTx(req util.EvmTxRequest) EvmTxResponse {
a.l.Printf("broadcasting evm tx %+v\n", req.Data)
// send the request to signer
a.evmReqChan <- req
// block and await response
// response occurs once tx is submitted to pending tx pool.
// poll for the receipt to wait for it to be included in a block
res := <-a.evmResChan
response := EvmTxResponse{
EvmTxResponse: res,
}
// if failed during signing or broadcast, there will never be a receipt.
if res.Err != nil {
return response
}
// if we don't have a tx receipt within a given timeout, fail the request
timeout := time.After(10 * time.Second)
for {
select {
case <-timeout:
response.Err = BroadcastTimeoutErr
default:
response.Receipt, response.Err = a.evmSigner.EvmClient.TransactionReceipt(context.Background(), res.TxHash)
if errors.Is(response.Err, ethereum.NotFound) {
// tx still not committed to a block. retry!
time.Sleep(100 * time.Millisecond)
continue
}
}
break
}
return response
}
func (suite *E2eTestSuite) NewFundedAccount(name string, funds sdk.Coins) *SigningAccount { func (suite *E2eTestSuite) NewFundedAccount(name string, funds sdk.Coins) *SigningAccount {
entropy, err := bip39.NewEntropy(128) entropy, err := bip39.NewEntropy(128)
suite.NoErrorf(err, "failed to generate entropy for account %s: %s", name, err) suite.NoErrorf(err, "failed to generate entropy for account %s: %s", name, err)
@ -110,12 +194,13 @@ func (suite *E2eTestSuite) NewFundedAccount(name string, funds sdk.Coins) *Signi
) )
whale := suite.GetAccount(FundedAccountName) whale := suite.GetAccount(FundedAccountName)
whale.l.Printf("attempting to fund created account (%s=%s)\n", name, acc.SdkAddress.String())
res := whale.SignAndBroadcastKavaTx( res := whale.SignAndBroadcastKavaTx(
util.MsgRequest{ util.KavaMsgRequest{
Msgs: []sdk.Msg{ Msgs: []sdk.Msg{
banktypes.NewMsgSend(whale.Address, acc.Address, funds), banktypes.NewMsgSend(whale.SdkAddress, acc.SdkAddress, funds),
}, },
GasLimit: 1e5, GasLimit: 2e5,
FeeAmount: sdk.NewCoins(sdk.NewCoin(StakingDenom, sdk.NewInt(75000))), FeeAmount: sdk.NewCoins(sdk.NewCoin(StakingDenom, sdk.NewInt(75000))),
Data: fmt.Sprintf("initial funding of account %s", name), Data: fmt.Sprintf("initial funding of account %s", name),
}, },
@ -123,5 +208,7 @@ func (suite *E2eTestSuite) NewFundedAccount(name string, funds sdk.Coins) *Signi
suite.NoErrorf(res.Err, "failed to fund new account %s: %s", name, res.Err) suite.NoErrorf(res.Err, "failed to fund new account %s: %s", name, res.Err)
whale.l.Printf("successfully funded [%s]\n", name)
return acc return acc
} }

View File

@ -1,6 +1,7 @@
package testutil package testutil
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -10,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
"github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/hd"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx" txtypes "github.com/cosmos/cosmos-sdk/types/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
@ -25,6 +27,10 @@ const (
ChainId = "kavalocalnet_8888-1" ChainId = "kavalocalnet_8888-1"
FundedAccountName = "whale" FundedAccountName = "whale"
StakingDenom = "ukava" StakingDenom = "ukava"
// use coin type 60 so we are compatible with accounts from `kava add keys --eth <name>`
// these accounts use the ethsecp256k1 signing algorithm that allows the signing client
// to manage both sdk & evm txs.
Bip44CoinType = 60
) )
type E2eTestSuite struct { type E2eTestSuite struct {
@ -44,6 +50,7 @@ type E2eTestSuite struct {
} }
func (suite *E2eTestSuite) SetupSuite() { func (suite *E2eTestSuite) SetupSuite() {
fmt.Println("setting up test suite.")
app.SetSDKConfig() app.SetSDKConfig()
suite.encodingConfig = app.MakeEncodingConfig() suite.encodingConfig = app.MakeEncodingConfig()
@ -96,19 +103,35 @@ func (suite *E2eTestSuite) SetupSuite() {
// initialize accounts map // initialize accounts map
suite.accounts = make(map[string]*SigningAccount) suite.accounts = make(map[string]*SigningAccount)
// setup the signing account for the initially funded account (used to fund all other accounts) // setup the signing account for the initially funded account (used to fund all other accounts)
suite.AddNewSigningAccount( whale := suite.AddNewSigningAccount(
FundedAccountName, FundedAccountName,
hd.CreateHDPath(app.Bip44CoinType, 0, 0), hd.CreateHDPath(Bip44CoinType, 0, 0),
ChainId, ChainId,
fundedAccountMnemonic, fundedAccountMnemonic,
) )
// check that funded account is actually funded.
fmt.Printf("account used for funding (%s) address: %s\n", FundedAccountName, whale.SdkAddress)
whaleFunds := suite.QuerySdkForBalances(whale.SdkAddress)
if whaleFunds.IsZero() {
suite.FailNow("no available funds.", "funded account mnemonic is for account with no funds")
}
} }
func (suite *E2eTestSuite) TearDownSuite() { func (suite *E2eTestSuite) TearDownSuite() {
fmt.Println("tearing down test suite.")
// close all account request channels // close all account request channels
for _, a := range suite.accounts { for _, a := range suite.accounts {
close(a.requests) close(a.sdkReqChan)
} }
// gracefully shutdown docker container(s) // gracefully shutdown docker container(s)
suite.runner.Shutdown() suite.runner.Shutdown()
} }
func (suite *E2eTestSuite) QuerySdkForBalances(addr sdk.AccAddress) sdk.Coins {
res, err := suite.Bank.AllBalances(context.Background(), &banktypes.QueryAllBalancesRequest{
Address: addr.String(),
})
suite.NoError(err)
return res.Balances
}

14
tests/util/addresses.go Normal file
View File

@ -0,0 +1,14 @@
package util
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
)
func SdkToEvmAddress(addr sdk.AccAddress) common.Address {
return common.BytesToAddress(addr.Bytes())
}
func EvmToSdkAddress(addr common.Address) sdk.AccAddress {
return sdk.AccAddress(addr.Bytes())
}

View File

@ -0,0 +1,21 @@
package util_test
import (
"testing"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/tests/util"
)
func TestAddressConversion(t *testing.T) {
app.SetSDKConfig()
bech32Addr := sdk.MustAccAddressFromBech32("kava17d2wax0zhjrrecvaszuyxdf5wcu5a0p4qlx3t5")
hexAddr := common.HexToAddress("0xf354ee99e2bc863cE19d80b843353476394EbC35")
require.Equal(t, bech32Addr, util.EvmToSdkAddress(hexAddr))
require.Equal(t, hexAddr, util.SdkToEvmAddress(bech32Addr))
}

103
tests/util/evmsigner.go Normal file
View File

@ -0,0 +1,103 @@
package util
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
type EvmTxRequest struct {
Tx *ethtypes.Transaction
Data interface{}
}
type EvmTxResponse struct {
Request EvmTxRequest
TxHash common.Hash
Err error
}
type EvmFailedToSignError struct{ Err error }
func (e EvmFailedToSignError) Error() string {
return fmt.Sprintf("failed to sign tx: %s", e.Err)
}
type EvmFailedToBroadcastError struct{ Err error }
func (e EvmFailedToBroadcastError) Error() string {
return fmt.Sprintf("failed to broadcast tx: %s", e.Err)
}
// EvmSigner manages signing and broadcasting requests to transfer Erc20 tokens
// Will work for calling all contracts that have func signature `transfer(address,uint256)`
type EvmSigner struct {
auth *bind.TransactOpts
signerAddress common.Address
EvmClient *ethclient.Client
}
func NewEvmSigner(
evmClient *ethclient.Client,
privKey *ecdsa.PrivateKey,
chainId *big.Int,
) (*EvmSigner, error) {
auth, err := bind.NewKeyedTransactorWithChainID(privKey, chainId)
if err != nil {
return &EvmSigner{}, err
}
publicKey := privKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return &EvmSigner{}, fmt.Errorf("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
return &EvmSigner{
auth: auth,
signerAddress: crypto.PubkeyToAddress(*publicKeyECDSA),
EvmClient: evmClient,
}, nil
}
func (s *EvmSigner) Run(requests <-chan EvmTxRequest) <-chan EvmTxResponse {
responses := make(chan EvmTxResponse)
// receive tx requests, sign & broadcast them.
// Responses are sent once the tx is added to the pending tx pool.
// To see result, use TransactionReceipt after tx has been included in a block.
go func() {
for {
// wait for incoming request
req := <-requests
signedTx, err := s.auth.Signer(s.signerAddress, req.Tx)
if err != nil {
err = EvmFailedToSignError{Err: err}
} else {
err = s.EvmClient.SendTransaction(context.Background(), signedTx)
if err != nil {
err = EvmFailedToBroadcastError{Err: err}
}
}
responses <- EvmTxResponse{
Request: req,
TxHash: signedTx.Hash(),
Err: err,
}
}
}()
return responses
}
func (s *EvmSigner) Address() common.Address {
return s.signerAddress
}

View File

@ -18,18 +18,18 @@ import (
tmmempool "github.com/tendermint/tendermint/mempool" tmmempool "github.com/tendermint/tendermint/mempool"
) )
type MsgRequest struct { type KavaMsgRequest struct {
Msgs []sdk.Msg Msgs []sdk.Msg
GasLimit uint64 GasLimit uint64
FeeAmount sdk.Coins FeeAmount sdk.Coins
Memo string Memo string
// Arbitrary data to be referenced in the corresponding MsgResponse, unused // Arbitrary data to be referenced in the corresponding KavaMsgResponse, unused
// in signing. This is mostly useful to match MsgResponses with MsgRequests. // in signing. This is mostly useful to match KavaMsgResponses with KavaMsgRequests.
Data interface{} Data interface{}
} }
type MsgResponse struct { type KavaMsgResponse struct {
Request MsgRequest Request KavaMsgRequest
Tx authsigning.Tx Tx authsigning.Tx
TxBytes []byte TxBytes []byte
Result sdk.TxResponse Result sdk.TxResponse
@ -46,8 +46,8 @@ const (
txResetSequence txResetSequence
) )
// Signer broadcasts msgs to a single kava node // KavaSigner broadcasts msgs to a single kava node
type Signer struct { type KavaSigner struct {
chainID string chainID string
encodingConfig params.EncodingConfig encodingConfig params.EncodingConfig
authClient authtypes.QueryClient authClient authtypes.QueryClient
@ -56,15 +56,15 @@ type Signer struct {
inflightTxLimit uint64 inflightTxLimit uint64
} }
func NewSigner( func NewKavaSigner(
chainID string, chainID string,
encodingConfig params.EncodingConfig, encodingConfig params.EncodingConfig,
authClient authtypes.QueryClient, authClient authtypes.QueryClient,
txClient txtypes.ServiceClient, txClient txtypes.ServiceClient,
privKey cryptotypes.PrivKey, privKey cryptotypes.PrivKey,
inflightTxLimit uint64) *Signer { inflightTxLimit uint64) *KavaSigner {
return &Signer{ return &KavaSigner{
chainID: chainID, chainID: chainID,
encodingConfig: encodingConfig, encodingConfig: encodingConfig,
authClient: authClient, authClient: authClient,
@ -74,7 +74,7 @@ func NewSigner(
} }
} }
func (s *Signer) pollAccountState() <-chan authtypes.AccountI { func (s *KavaSigner) pollAccountState() <-chan authtypes.AccountI {
accountState := make(chan authtypes.AccountI) accountState := make(chan authtypes.AccountI)
go func() { go func() {
@ -100,7 +100,7 @@ func (s *Signer) pollAccountState() <-chan authtypes.AccountI {
return accountState return accountState
} }
func (s *Signer) Run(requests <-chan MsgRequest) (<-chan MsgResponse, error) { func (s *KavaSigner) Run(requests <-chan KavaMsgRequest) (<-chan KavaMsgResponse, error) {
// poll account state in it's own goroutine // poll account state in it's own goroutine
// and send status updates to the signing goroutine // and send status updates to the signing goroutine
// //
@ -108,15 +108,15 @@ func (s *Signer) Run(requests <-chan MsgRequest) (<-chan MsgResponse, error) {
// websocket events with a fallback to polling // websocket events with a fallback to polling
accountState := s.pollAccountState() accountState := s.pollAccountState()
responses := make(chan MsgResponse) responses := make(chan KavaMsgResponse)
go func() { go func() {
// wait until account is loaded to start signing // wait until account is loaded to start signing
account := <-accountState account := <-accountState
// store current request waiting to be broadcasted // store current request waiting to be broadcasted
var currentRequest *MsgRequest var currentRequest *KavaMsgRequest
// keep track of all successfully broadcasted txs // keep track of all successfully broadcasted txs
// index is sequence % inflightTxLimit // index is sequence % inflightTxLimit
inflight := make([]*MsgResponse, s.inflightTxLimit) inflight := make([]*KavaMsgResponse, s.inflightTxLimit)
// used for confirming sent txs only // used for confirming sent txs only
prevDeliverTxSeq := account.GetSequence() prevDeliverTxSeq := account.GetSequence()
// tx sequence of already signed messages // tx sequence of already signed messages
@ -243,7 +243,7 @@ func (s *Signer) Run(requests <-chan MsgRequest) (<-chan MsgResponse, error) {
tx, txBytes, err := Sign(s.encodingConfig.TxConfig, s.privKey, txBuilder, signerData) tx, txBytes, err := Sign(s.encodingConfig.TxConfig, s.privKey, txBuilder, signerData)
response = &MsgResponse{ response = &KavaMsgResponse{
Request: *currentRequest, Request: *currentRequest,
Tx: tx, Tx: tx,
TxBytes: txBytes, TxBytes: txBytes,
@ -367,7 +367,7 @@ func (s *Signer) Run(requests <-chan MsgRequest) (<-chan MsgResponse, error) {
} }
// Address returns the address of the Signer // Address returns the address of the Signer
func (s *Signer) Address() sdk.AccAddress { func (s *KavaSigner) Address() sdk.AccAddress {
return GetAccAddress(s.privKey) return GetAccAddress(s.privKey)
} }