Skip to content

Commit

Permalink
feat(geth): add ethereum support for the geth binary (#1230)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Gouin <[email protected]>
  • Loading branch information
misko9 and agouin authored Aug 24, 2024
1 parent c3e1187 commit ef70987
Show file tree
Hide file tree
Showing 33 changed files with 2,014 additions and 434 deletions.
334 changes: 51 additions & 283 deletions chain/ethereum/ethererum_chain.go

Large diffs are not rendered by default.

228 changes: 228 additions & 0 deletions chain/ethereum/foundry/anvil_chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package foundry

import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"sync"

"github.com/docker/docker/api/types/mount"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/strangelove-ventures/interchaintest/v8/chain/ethereum"
"github.com/strangelove-ventures/interchaintest/v8/ibc"
"go.uber.org/zap"
)

var _ ibc.Chain = &AnvilChain{}

type AnvilChain struct {
*ethereum.EthereumChain

keystoreMap map[string]*NodeWallet
}

func NewAnvilChain(testName string, chainConfig ibc.ChainConfig, log *zap.Logger) *AnvilChain {
return &AnvilChain{
EthereumChain: ethereum.NewEthereumChain(testName, chainConfig, log),
keystoreMap: make(map[string]*NodeWallet),
}
}

func (c *AnvilChain) KeystoreDir() string {
return path.Join(c.HomeDir(), ".foundry", "keystores")
}

func (c *AnvilChain) Start(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error {
cmd := []string{c.Config().Bin,
"--host", "0.0.0.0", // Anyone can call
"--no-cors",
"--gas-price", c.Config().GasPrices,
}

cmd = append(cmd, c.Config().AdditionalStartArgs...)

var mounts []mount.Mount
if loadState, ok := c.Config().ConfigFileOverrides["--load-state"].(string); ok {
pwd, err := os.Getwd()
if err != nil {
return err
}
localJsonFile := filepath.Join(pwd, loadState)
dockerJsonFile := path.Join(c.HomeDir(), path.Base(loadState))
mounts = []mount.Mount{
{
Type: mount.TypeBind,
Source: localJsonFile,
Target: dockerJsonFile,
},
}
cmd = append(cmd, "--load-state", dockerJsonFile)
}

return c.EthereumChain.Start(ctx, cmd, mounts)
}

type NewWalletOutput struct {
Address string `json:"address"`
Path string `json:"path"`
}

func (c *AnvilChain) MakeKeystoreDir(ctx context.Context) error {
cmd := []string{"mkdir", "-p", c.KeystoreDir()}
_, _, err := c.Exec(ctx, cmd, nil)
return err
}

func (c *AnvilChain) CreateKey(ctx context.Context, keyName string) error {
err := c.MakeKeystoreDir(ctx) // Ensure keystore directory is created
if err != nil {
return err
}

_, ok := c.keystoreMap[keyName]
if ok {
return fmt.Errorf("keyname (%s) already used", keyName)
}

cmd := []string{"cast", "wallet", "new", c.KeystoreDir(), "--unsafe-password", "", "--json"}
stdout, _, err := c.Exec(ctx, cmd, nil)
if err != nil {
return err
}

newWallet := []NewWalletOutput{}
err = json.Unmarshal(stdout, &newWallet)
if err != nil {
return err
}

c.keystoreMap[keyName] = &NodeWallet{
keystore: newWallet[0].Path,
}

return nil
}

func (c *AnvilChain) RecoverKey(ctx context.Context, keyName, mnemonic string) error {
err := c.MakeKeystoreDir(ctx) // Ensure keystore directory is created
if err != nil {
return err
}

cmd := []string{"cast", "wallet", "import", keyName, "--keystore-dir", c.KeystoreDir(), "--mnemonic", mnemonic, "--unsafe-password", ""}
_, _, err = c.Exec(ctx, cmd, nil)
if err != nil {
return err
}

// This is needed for CreateKey() since that keystore path does not use the keyname
c.keystoreMap[keyName] = &NodeWallet{
keystore: path.Join(c.KeystoreDir(), keyName),
}

return nil
}

// Get address of account, cast to a string to use
func (c *AnvilChain) GetAddress(ctx context.Context, keyName string) ([]byte, error) {
account, ok := c.keystoreMap[keyName]
if !ok {
return nil, fmt.Errorf("keyname (%s) not found", keyName)
}

if account.address != "" {
return hexutil.MustDecode(account.address), nil
}

cmd := []string{"cast", "wallet", "address", "--keystore", account.keystore, "--password", ""}
stdout, _, err := c.Exec(ctx, cmd, nil)
if err != nil {
return nil, err
}

addr := strings.TrimSpace(string(stdout))
account.address = addr
return hexutil.MustDecode(addr), nil
}

func (c *AnvilChain) SendFunds(ctx context.Context, keyName string, amount ibc.WalletAmount) error {
_, err := c.SendFundsWithNote(ctx, keyName, amount, "")
return err
}

type TransactionReceipt struct {
TxHash string `json:"transactionHash"`
}

func (c *AnvilChain) SendFundsWithNote(ctx context.Context, keyName string, amount ibc.WalletAmount, note string) (string, error) {
var cmd []string
if len(note) > 0 {
cmd = []string{"cast", "send", amount.Address, hexutil.Encode([]byte(note)), "--value", amount.Amount.String(), "--json"}
} else {
cmd = []string{"cast", "send", amount.Address, "--value", amount.Amount.String(), "--json"}
}

account, ok := c.keystoreMap[keyName]
if !ok {
return "", fmt.Errorf("keyname (%s) not found", keyName)
}
cmd = append(cmd,
"--keystore", account.keystore,
"--password", "",
"--rpc-url", c.GetRPCAddress(),
)

account.txLock.Lock()
defer account.txLock.Unlock()
stdout, _, err := c.Exec(ctx, cmd, nil)
if err != nil {
return "", fmt.Errorf("send funds, exec, %w", err)
}

var txReceipt TransactionReceipt
if err = json.Unmarshal([]byte(strings.TrimSpace(string(stdout))), &txReceipt); err != nil {
return "", fmt.Errorf("tx receipt unmarshal:\n %s\nerror: %w", string(stdout), err)
}

return txReceipt.TxHash, nil
}

func (c *AnvilChain) BuildWallet(ctx context.Context, keyName string, mnemonic string) (ibc.Wallet, error) {
if mnemonic != "" {
err := c.RecoverKey(ctx, keyName, mnemonic)
if err != nil {
return nil, err
}
} else {
// Use the genesis account
if keyName == "faucet" {
mnemonic = "test test test test test test test test test test test junk"
err := c.RecoverKey(ctx, keyName, mnemonic)
if err != nil {
return nil, err
}
} else {
// Create new account
err := c.CreateKey(ctx, keyName)
if err != nil {
return nil, err
}
}
}

address, err := c.GetAddress(ctx, keyName)
if err != nil {
return nil, err
}
return ethereum.NewWallet(keyName, address, mnemonic), nil
}

type NodeWallet struct {
address string
keystore string
txLock sync.Mutex
}
36 changes: 36 additions & 0 deletions chain/ethereum/foundry/default_configs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package foundry

import (
"github.com/strangelove-ventures/interchaintest/v8/ibc"
)

func DefaultEthereumAnvilChainConfig(
name string,
) ibc.ChainConfig {
return ibc.ChainConfig{
Type: "ethereum",
Name: name,
ChainID: "31337", // default anvil chain-id
Bech32Prefix: "n/a",
CoinType: "60",
Denom: "wei",
GasPrices: "20000000000", // 20 gwei
GasAdjustment: 0,
TrustingPeriod: "0",
NoHostMount: false,
Images: []ibc.DockerImage{
{
Repository: "ghcr.io/foundry-rs/foundry",
Version: "latest",
UidGid: "1000:1000",
},
},
Bin: "anvil",
AdditionalStartArgs: []string{
"--block-time", "2", // 2 second block times
"--accounts", "10", // We current only use the first account for the faucet, but tests may expect the default
"--balance", "10000000", // Genesis accounts loaded with 10mil ether, change as needed
"--block-base-fee-per-gas", "0",
},
}
}
33 changes: 18 additions & 15 deletions chain/ethereum/forge.go → chain/ethereum/foundry/forge.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ethereum
package foundry

import (
"context"
"fmt"
"os"
"path"
"path/filepath"
Expand All @@ -21,19 +22,15 @@ type ForgeScriptOpts struct {
}

// Add private-key or keystore to cmd
func (c *EthereumChain) AddKey(cmd []string, keyName string) []string {
// choose whether to use private-key or keystore
if keyName == "faucet" {
cmd = append(cmd,
"--private-key", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
)

} else {
cmd = append(cmd,
"--keystores", c.keystoreMap[keyName],
"--password", "",
)
func (c *AnvilChain) AddKey(cmd []string, keyName string) []string {
account, ok := c.keystoreMap[keyName]
if !ok {
panic(fmt.Sprintf("Keyname (%s) not found", keyName))
}
cmd = append(cmd,
"--keystores", account.keystore,
"--password", "",
)
return cmd
}

Expand Down Expand Up @@ -78,7 +75,13 @@ func WriteConfigFile(configFile string, localContractRootDir string, solidityCon

// Run "forge script"
// see: https://book.getfoundry.sh/reference/forge/forge-script
func (c *EthereumChain) ForgeScript(ctx context.Context, keyName string, opts ForgeScriptOpts) (stdout, stderr []byte, err error) {
func (c *AnvilChain) ForgeScript(ctx context.Context, keyName string, opts ForgeScriptOpts) (stdout, stderr []byte, err error) {
account, ok := c.keystoreMap[keyName]
if !ok {
return nil, nil, fmt.Errorf("keyname (%s) not found", keyName)
}
account.txLock.Lock()
defer account.txLock.Unlock()
pwd, err := os.Getwd()
if err != nil {
return nil, nil, err
Expand All @@ -96,7 +99,7 @@ func (c *EthereumChain) ForgeScript(ctx context.Context, keyName string, opts Fo
return nil, nil, err
}

job := dockerutil.NewImage(c.logger(), c.DockerClient, c.NetworkID, c.testName, c.cfg.Images[0].Repository, c.cfg.Images[0].Version)
job := c.NewJob()
containerOpts := dockerutil.ContainerOptions{
Binds: c.Bind(),
Mounts: []mount.Mount{
Expand Down
Loading

0 comments on commit ef70987

Please sign in to comment.