mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
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:
parent
214393ccfd
commit
f051ea3a49
@ -2,3 +2,4 @@ out/
|
|||||||
**/node_modules/
|
**/node_modules/
|
||||||
.git/
|
.git/
|
||||||
docs/
|
docs/
|
||||||
|
tests/
|
||||||
|
2
Makefile
2
Makefile
@ -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:
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
@ -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": [
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var BroadcastTimeoutErr = errors.New("timed out waiting for tx to be committed to block")
|
||||||
|
|
||||||
type SigningAccount struct {
|
type SigningAccount struct {
|
||||||
name string
|
name string
|
||||||
mnemonic string
|
mnemonic string
|
||||||
signer *util.Signer
|
|
||||||
requests chan<- util.MsgRequest
|
|
||||||
responses <-chan util.MsgResponse
|
|
||||||
|
|
||||||
Address sdk.AccAddress
|
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 := ðsecp256k1.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,
|
|
||||||
requests: requests,
|
|
||||||
responses: responses,
|
|
||||||
l: logger,
|
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
|
||||||
}
|
}
|
||||||
|
@ -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
14
tests/util/addresses.go
Normal 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())
|
||||||
|
}
|
21
tests/util/addresses_test.go
Normal file
21
tests/util/addresses_test.go
Normal 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
103
tests/util/evmsigner.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user