mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55: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/
|
||||
.git/
|
||||
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)
|
||||
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/...
|
||||
|
||||
test:
|
||||
|
@ -8,10 +8,18 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/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/util"
|
||||
)
|
||||
|
||||
var (
|
||||
minEvmGasPrice = big.NewInt(1e10) // akava
|
||||
)
|
||||
|
||||
func ukava(amt int64) sdk.Coin {
|
||||
@ -26,14 +34,18 @@ func TestIntegrationTestSuite(t *testing.T) {
|
||||
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() {
|
||||
// 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())
|
||||
suite.NoError(err)
|
||||
suite.Equal(big.NewInt(8888), evmNetworkId)
|
||||
suite.Equal(expectedEvmNetworkId, evmNetworkId)
|
||||
|
||||
// SDK query
|
||||
nodeInfo, err := suite.Tm.GetNodeInfo(context.Background(), &tmservice.GetNodeInfoRequest{})
|
||||
suite.NoError(err)
|
||||
suite.Equal(testutil.ChainId, nodeInfo.DefaultNodeInfo.Network)
|
||||
@ -43,9 +55,55 @@ func (suite *IntegrationTestSuite) TestChainID() {
|
||||
func (suite *IntegrationTestSuite) TestFundedAccount() {
|
||||
funds := ukava(1e7)
|
||||
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(
|
||||
acc.Address, "ukava",
|
||||
acc.SdkAddress, "ukava",
|
||||
))
|
||||
suite.NoError(err)
|
||||
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",
|
||||
"initial_height": "1",
|
||||
"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",
|
||||
"base_account": {
|
||||
@ -268,6 +278,47 @@
|
||||
"default_send_enabled": true
|
||||
},
|
||||
"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",
|
||||
"coins": [
|
||||
|
@ -1,28 +1,45 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/hd"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"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/tests/util"
|
||||
)
|
||||
|
||||
type SigningAccount struct {
|
||||
name string
|
||||
mnemonic string
|
||||
signer *util.Signer
|
||||
requests chan<- util.MsgRequest
|
||||
responses <-chan util.MsgResponse
|
||||
var BroadcastTimeoutErr = errors.New("timed out waiting for tx to be committed to block")
|
||||
|
||||
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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
// Kava signing account for SDK side
|
||||
privKeyBytes, err := hd.Secp256k1.Derive()(mnemonic, "", hdPath.String())
|
||||
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,
|
||||
suite.encodingConfig,
|
||||
suite.Auth,
|
||||
@ -55,36 +73,58 @@ func (suite *E2eTestSuite) AddNewSigningAccount(name string, hdPath *hd.BIP44Par
|
||||
100,
|
||||
)
|
||||
|
||||
requests := make(chan util.MsgRequest)
|
||||
responses, err := signer.Run(requests)
|
||||
sdkReqChan := make(chan util.KavaMsgRequest)
|
||||
sdkResChan, err := kavaSigner.Run(sdkReqChan)
|
||||
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)
|
||||
|
||||
// TODO: authenticated eth client.
|
||||
suite.accounts[name] = &SigningAccount{
|
||||
name: name,
|
||||
mnemonic: mnemonic,
|
||||
signer: signer,
|
||||
requests: requests,
|
||||
responses: responses,
|
||||
l: logger,
|
||||
name: name,
|
||||
mnemonic: mnemonic,
|
||||
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]
|
||||
}
|
||||
|
||||
// SignAndBroadcastKavaTx sends a request to the signer and awaits its response.
|
||||
func (a *SigningAccount) SignAndBroadcastKavaTx(req util.MsgRequest) util.MsgResponse {
|
||||
a.l.Printf("broadcasting tx %+v\n", req.Data)
|
||||
func (a *SigningAccount) SignAndBroadcastKavaTx(req util.KavaMsgRequest) util.KavaMsgResponse {
|
||||
a.l.Printf("broadcasting sdk tx %+v\n", req.Data)
|
||||
// send the request to signer
|
||||
a.requests <- req
|
||||
a.sdkReqChan <- req
|
||||
|
||||
// TODO: timeout awaiting the response.
|
||||
// block and await response
|
||||
// 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)
|
||||
if res.Err != nil {
|
||||
@ -96,6 +136,50 @@ func (a *SigningAccount) SignAndBroadcastKavaTx(req util.MsgRequest) util.MsgRes
|
||||
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 {
|
||||
entropy, err := bip39.NewEntropy(128)
|
||||
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.l.Printf("attempting to fund created account (%s=%s)\n", name, acc.SdkAddress.String())
|
||||
res := whale.SignAndBroadcastKavaTx(
|
||||
util.MsgRequest{
|
||||
util.KavaMsgRequest{
|
||||
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))),
|
||||
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)
|
||||
|
||||
whale.l.Printf("successfully funded [%s]\n", name)
|
||||
|
||||
return acc
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/hd"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
@ -25,6 +27,10 @@ const (
|
||||
ChainId = "kavalocalnet_8888-1"
|
||||
FundedAccountName = "whale"
|
||||
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 {
|
||||
@ -44,6 +50,7 @@ type E2eTestSuite struct {
|
||||
}
|
||||
|
||||
func (suite *E2eTestSuite) SetupSuite() {
|
||||
fmt.Println("setting up test suite.")
|
||||
app.SetSDKConfig()
|
||||
suite.encodingConfig = app.MakeEncodingConfig()
|
||||
|
||||
@ -96,19 +103,35 @@ func (suite *E2eTestSuite) SetupSuite() {
|
||||
// initialize accounts map
|
||||
suite.accounts = make(map[string]*SigningAccount)
|
||||
// setup the signing account for the initially funded account (used to fund all other accounts)
|
||||
suite.AddNewSigningAccount(
|
||||
whale := suite.AddNewSigningAccount(
|
||||
FundedAccountName,
|
||||
hd.CreateHDPath(app.Bip44CoinType, 0, 0),
|
||||
hd.CreateHDPath(Bip44CoinType, 0, 0),
|
||||
ChainId,
|
||||
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() {
|
||||
fmt.Println("tearing down test suite.")
|
||||
// close all account request channels
|
||||
for _, a := range suite.accounts {
|
||||
close(a.requests)
|
||||
close(a.sdkReqChan)
|
||||
}
|
||||
// gracefully shutdown docker container(s)
|
||||
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"
|
||||
)
|
||||
|
||||
type MsgRequest struct {
|
||||
type KavaMsgRequest struct {
|
||||
Msgs []sdk.Msg
|
||||
GasLimit uint64
|
||||
FeeAmount sdk.Coins
|
||||
Memo string
|
||||
// Arbitrary data to be referenced in the corresponding MsgResponse, unused
|
||||
// in signing. This is mostly useful to match MsgResponses with MsgRequests.
|
||||
// Arbitrary data to be referenced in the corresponding KavaMsgResponse, unused
|
||||
// in signing. This is mostly useful to match KavaMsgResponses with KavaMsgRequests.
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type MsgResponse struct {
|
||||
Request MsgRequest
|
||||
type KavaMsgResponse struct {
|
||||
Request KavaMsgRequest
|
||||
Tx authsigning.Tx
|
||||
TxBytes []byte
|
||||
Result sdk.TxResponse
|
||||
@ -46,8 +46,8 @@ const (
|
||||
txResetSequence
|
||||
)
|
||||
|
||||
// Signer broadcasts msgs to a single kava node
|
||||
type Signer struct {
|
||||
// KavaSigner broadcasts msgs to a single kava node
|
||||
type KavaSigner struct {
|
||||
chainID string
|
||||
encodingConfig params.EncodingConfig
|
||||
authClient authtypes.QueryClient
|
||||
@ -56,15 +56,15 @@ type Signer struct {
|
||||
inflightTxLimit uint64
|
||||
}
|
||||
|
||||
func NewSigner(
|
||||
func NewKavaSigner(
|
||||
chainID string,
|
||||
encodingConfig params.EncodingConfig,
|
||||
authClient authtypes.QueryClient,
|
||||
txClient txtypes.ServiceClient,
|
||||
privKey cryptotypes.PrivKey,
|
||||
inflightTxLimit uint64) *Signer {
|
||||
inflightTxLimit uint64) *KavaSigner {
|
||||
|
||||
return &Signer{
|
||||
return &KavaSigner{
|
||||
chainID: chainID,
|
||||
encodingConfig: encodingConfig,
|
||||
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)
|
||||
|
||||
go func() {
|
||||
@ -100,7 +100,7 @@ func (s *Signer) pollAccountState() <-chan authtypes.AccountI {
|
||||
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
|
||||
// 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
|
||||
accountState := s.pollAccountState()
|
||||
|
||||
responses := make(chan MsgResponse)
|
||||
responses := make(chan KavaMsgResponse)
|
||||
go func() {
|
||||
// wait until account is loaded to start signing
|
||||
account := <-accountState
|
||||
// store current request waiting to be broadcasted
|
||||
var currentRequest *MsgRequest
|
||||
var currentRequest *KavaMsgRequest
|
||||
// keep track of all successfully broadcasted txs
|
||||
// index is sequence % inflightTxLimit
|
||||
inflight := make([]*MsgResponse, s.inflightTxLimit)
|
||||
inflight := make([]*KavaMsgResponse, s.inflightTxLimit)
|
||||
// used for confirming sent txs only
|
||||
prevDeliverTxSeq := account.GetSequence()
|
||||
// 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)
|
||||
|
||||
response = &MsgResponse{
|
||||
response = &KavaMsgResponse{
|
||||
Request: *currentRequest,
|
||||
Tx: tx,
|
||||
TxBytes: txBytes,
|
||||
@ -367,7 +367,7 @@ func (s *Signer) Run(requests <-chan MsgRequest) (<-chan MsgResponse, error) {
|
||||
}
|
||||
|
||||
// Address returns the address of the Signer
|
||||
func (s *Signer) Address() sdk.AccAddress {
|
||||
func (s *KavaSigner) Address() sdk.AccAddress {
|
||||
return GetAccAddress(s.privKey)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user