Skip to content

Commit

Permalink
client/eth: refactor eth client
Browse files Browse the repository at this point in the history
The client/asset/eth package was using geth like an RPC client. That's
not crazy, considering we have still an *rpc.Client, but in reality,
there is no RPC server, and the client is just a shortcut to RPC handlers
that call functions that we already have access to through e.g.
*les.LightEthereum or *keystore.KeyStore. This removes most "rpc"
calls in favor of calling the underlying methods directly.

Moves some common types to dex/networks/eth. As a general rule, if
the client/asset/ imports from server/asset, we should just move it
to dex/networks.

Creates the Contractor interface, which serves as a clear boundary
between dex-space and abigen-space. This also enables clean upgrades,
implemented here.
  • Loading branch information
buck54321 committed Nov 16, 2021
1 parent 1890057 commit ff5f9ef
Show file tree
Hide file tree
Showing 14 changed files with 1,076 additions and 884 deletions.
177 changes: 95 additions & 82 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
)
Expand All @@ -46,9 +47,10 @@ func init() {

const (
// BipID is the BIP-0044 asset ID.
BipID = 60
defaultGasFee = 82 // gwei
defaultGasFeeLimit = 200 // gwei
BipID = 60
defaultGasFee = 82 // gwei
defaultGasFeeLimit = 200 // gwei
defaultSendGasLimit = 50_000

RedeemGas = 63000 // gas
)
Expand Down Expand Up @@ -91,7 +93,11 @@ var (
},
}

mainnetContractAddr = common.HexToAddress("")
chainIDs = map[dex.Network]int64{
dex.Mainnet: 1,
dex.Testnet: 5, // Görli
dex.Simnet: 42, // see dex/testing/eth/harness.sh
}
)

// Check that Driver implements asset.Driver.
Expand Down Expand Up @@ -133,15 +139,6 @@ func (d *Driver) Create(params *asset.CreateWalletParams) error {
return CreateWallet(params)
}

// rawWallet is an unexported return type from the eth client. Watch for changes at
// https://github.com/ethereum/go-ethereum/blob/c503f98f6d5e80e079c1d8a3601d188af2a899da/internal/ethapi/api.go#L227-L253
type rawWallet struct {
URL string `json:"url"`
Status string `json:"status"`
Failure string `json:"failure,omitempty"`
Accounts []accounts.Account `json:"accounts,omitempty"`
}

// ethFetcher represents a blockchain information fetcher. In practice, it is
// satisfied by rpcclient. For testing, it can be satisfied by a stub.
type ethFetcher interface {
Expand All @@ -152,23 +149,25 @@ type ethFetcher interface {
bestHeader(ctx context.Context) (*types.Header, error)
block(ctx context.Context, hash common.Hash) (*types.Block, error)
blockNumber(ctx context.Context) (uint64, error)
connect(ctx context.Context, node *node.Node, contractAddr *common.Address) error
listWallets(ctx context.Context) ([]rawWallet, error)
initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant *common.Address) (*types.Transaction, error)
estimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
connect(ctx context.Context) error
listWallets(ctx context.Context) []accounts.Wallet
// initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant *common.Address) (*types.Transaction, error)
initiateBatch(txOpts *bind.TransactOpts, contracts []*asset.Contract, serverVer uint32) (*types.Transaction, error)
estimateGas(ctx context.Context, data []byte, serverVer uint32) (uint64, error)
lock(ctx context.Context, acct *accounts.Account) error
nodeInfo(ctx context.Context) (*p2p.NodeInfo, error)
nodeInfo(ctx context.Context) *p2p.NodeInfo
pendingTransactions(ctx context.Context) ([]*types.Transaction, error)
peers(ctx context.Context) ([]*p2p.PeerInfo, error)
peers(ctx context.Context) []*p2p.Peer
transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
sendTransaction(ctx context.Context, tx map[string]string) (common.Hash, error)
sendTransaction(context.Context, *bind.TransactOpts, common.Address, []byte) (*types.Transaction, error)
shutdown()
syncProgress(ctx context.Context) (*ethereum.SyncProgress, error)
redeem(opts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error)
refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error)
swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*dexeth.ETHSwapSwap, error)
redeem(opts *bind.TransactOpts, secret, secretHash [32]byte, serverVer uint32) (*types.Transaction, error)
refund(opts *bind.TransactOpts, secretHash [32]byte, serverVer uint32) (*types.Transaction, error)
swap(ctx context.Context, secretHash [32]byte, serverVer uint32) (*dexeth.SwapState, error)
unlock(ctx context.Context, pw string, acct *accounts.Account) error
signData(addr common.Address, data []byte) ([]byte, error)
txOpts(ctx context.Context, val, maxFeeRate, maxGas uint64) (*bind.TransactOpts, error)
}

// Check that ExchangeWallet satisfies the asset.Wallet interface.
Expand All @@ -180,6 +179,7 @@ var _ asset.Wallet = (*ExchangeWallet)(nil)
type ExchangeWallet struct {
// 64-bit atomic variables first. See
// https://golang.org/pkg/sync/atomic/#pkg-note-BUG
net dex.Network
tipAtConnect int64

ctx context.Context // the asset subsystem starts with Connect(ctx)
Expand All @@ -188,11 +188,12 @@ type ExchangeWallet struct {
tipChange func(error)

internalNode *node.Node
leth *les.LightEthereum

tipMtx sync.RWMutex
currentTip *types.Block

acct *accounts.Account
creds *accountCredentials

cachedInitGas uint64
cachedInitGasMtx sync.Mutex
Expand Down Expand Up @@ -247,10 +248,10 @@ func CreateWallet(createWalletParams *asset.CreateWalletParams) error {

// NewWallet is the exported constructor by which the DEX will import the
// exchange wallet. It starts an internal light node.
func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, network dex.Network) (*ExchangeWallet, error) {
walletDir := getWalletDir(assetCFG.DataDir, network)
func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (*ExchangeWallet, error) {
walletDir := getWalletDir(assetCFG.DataDir, net)
nodeCFG := &nodeConfig{
net: network,
net: net,
appDir: walletDir,
logger: logger.SubLogger("NODE"),
}
Expand All @@ -260,27 +261,30 @@ func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, network dex.Netw
return nil, err
}

err = startNode(node, network)
leth, err := startNode(node, net)
if err != nil {
return nil, err
}

accounts, err := exportAccountsFromNode(node)
creds, err := nodeCredentials(node)
if err != nil {
return nil, err
}
if len(accounts) != 1 {
return nil,
fmt.Errorf("NewWallet: eth node keystore should only contain 1 account, but contains %d",
len(accounts))

cl, err := newNodeClient(node, leth, creds, net, logger.SubLogger("NODE"))
if err != nil {
return nil, err
}

return &ExchangeWallet{
log: logger,
leth: leth,
net: net,
node: cl,
tipChange: assetCFG.TipChange,
internalNode: node,
lockedFunds: make(map[string]uint64),
acct: &accounts[0],
creds: creds,
}, nil
}

Expand All @@ -296,13 +300,12 @@ func (eth *ExchangeWallet) shutdown() {

// Connect connects to the node RPC server. A dex.Connector.
func (eth *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) {
c := rpcclient{}
err := c.connect(ctx, eth.internalNode, &mainnetContractAddr)
eth.ctx = ctx

err := eth.node.connect(ctx)
if err != nil {
return nil, err
}
eth.node = &c
eth.ctx = ctx

// Initialize the best block.
bestHash, err := eth.node.bestBlockHash(ctx)
Expand Down Expand Up @@ -334,7 +337,7 @@ func (eth *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error)
//
// In Ethereum, an address is an account.
func (eth *ExchangeWallet) OwnsAddress(address string) (bool, error) {
return strings.ToLower(eth.acct.Address.String()) == strings.ToLower(address), nil
return strings.ToLower(eth.creds.addr.String()) == strings.ToLower(address), nil
}

// Balance returns the total available funds in the account. The eth node
Expand All @@ -351,10 +354,10 @@ func (eth *ExchangeWallet) Balance() (*asset.Balance, error) {
// balanceImpl returns the total available funds in the account.
// This function expects eth.lockedFundsMtx to be held.
func (eth *ExchangeWallet) balanceImpl() (*asset.Balance, error) {
if eth.acct == nil {
if eth.creds.acct == nil {
return nil, errors.New("account not set")
}
bigBal, err := eth.node.balance(eth.ctx, &eth.acct.Address)
bigBal, err := eth.node.balance(eth.ctx, &eth.creds.addr)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -398,18 +401,11 @@ func (eth *ExchangeWallet) getInitGas() (uint64, error) {
if err != nil {
return 0, err
}
data, err := parsedAbi.Pack("initiate", big.NewInt(1), secretHash, &eth.acct.Address)
data, err := parsedAbi.Pack("initiate", big.NewInt(1), secretHash, &eth.creds.addr)
if err != nil {
return 0, err
}
msg := ethereum.CallMsg{
From: eth.acct.Address,
To: &mainnetContractAddr,
Value: big.NewInt(1),
Gas: 0,
Data: data,
}
gas, err := eth.node.estimateGas(eth.ctx, msg)
gas, err := eth.node.estimateGas(eth.ctx, data, 0)
if err != nil {
return 0, err
}
Expand Down Expand Up @@ -557,7 +553,7 @@ func (eth *ExchangeWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes
var nonce [8]byte
copy(nonce[:], encode.RandomBytes(8))
var address [20]byte
copy(address[:], eth.acct.Address.Bytes())
copy(address[:], eth.creds.addr.Bytes())
coin := coin{
id: srveth.AmountCoinID{
Address: address,
Expand Down Expand Up @@ -590,9 +586,9 @@ func (eth *ExchangeWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
if err != nil {
return nil, err
}
if !bytes.Equal(coin.id.Address.Bytes(), eth.acct.Address.Bytes()) {
if !bytes.Equal(coin.id.Address.Bytes(), eth.creds.addr.Bytes()) {
return nil, fmt.Errorf("FundingCoins: coin address %v != wallet address %v",
coin.id.Address, eth.acct.Address)
coin.id.Address, eth.creds.addr)
}

coins = append(coins, coin)
Expand Down Expand Up @@ -682,18 +678,18 @@ func (*ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
// SignMessage signs the message with the private key associated with the
// specified funding Coin. Only a coin that came from the address this wallet
// is initialized with can be used to sign.
func (e *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
func (eth *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
ethCoin, err := decodeCoinID(coin.ID())
if err != nil {
return nil, nil, err
}

if !bytes.Equal(ethCoin.id.Address.Bytes(), e.acct.Address.Bytes()) {
if !bytes.Equal(ethCoin.id.Address.Bytes(), eth.creds.addr.Bytes()) {
return nil, nil, fmt.Errorf("SignMessage: coin address: %v != wallet address: %v",
ethCoin.id.Address, e.acct.Address)
ethCoin.id.Address, eth.creds.addr)
}

sig, err := e.node.signData(e.acct.Address, msg)
sig, err := eth.node.signData(eth.creds.addr, msg)
if err != nil {
return nil, nil, fmt.Errorf("SignMessage: error signing data: %v", err)
}
Expand Down Expand Up @@ -737,31 +733,27 @@ func (*ExchangeWallet) Refund(coinID, contract dex.Bytes, feeSuggestion uint64)

// Address returns an address for the exchange wallet.
func (eth *ExchangeWallet) Address() (string, error) {
return eth.acct.Address.String(), nil
return eth.creds.addr.String(), nil
}

// Unlock unlocks the exchange wallet.
func (eth *ExchangeWallet) Unlock(pw []byte) error {
return eth.node.unlock(eth.ctx, string(pw), eth.acct)
return eth.node.unlock(eth.ctx, string(pw), eth.creds.acct)
}

// Lock locks the exchange wallet.
func (eth *ExchangeWallet) Lock() error {
return eth.node.lock(eth.ctx, eth.acct)
return eth.node.lock(eth.ctx, eth.creds.acct)
}

// Locked will be true if the wallet is currently locked.
func (eth *ExchangeWallet) Locked() bool {
wallets, err := eth.node.listWallets(eth.ctx)
if err != nil {
eth.log.Errorf("list wallets error: %v", err)
return false
}
var wallet rawWallet
wallets := eth.node.listWallets(eth.ctx)
var wallet accounts.Wallet
findWallet := func() bool {
for _, w := range wallets {
for _, a := range w.Accounts {
if bytes.Equal(a.Address[:], eth.acct.Address[:]) {
for _, a := range w.Accounts() {
if bytes.Equal(a.Address[:], eth.creds.addr[:]) {
wallet = w
return true
}
Expand All @@ -770,10 +762,11 @@ func (eth *ExchangeWallet) Locked() bool {
return false
}
if !findWallet() {
eth.log.Error("unable to find wallet for account: %v", eth.acct)
eth.log.Error("unable to find wallet for account: %v", eth.creds.acct)
return false
}
return wallet.Status != "Unlocked"
status, _ := wallet.Status()
return status != "Unlocked"
}

// PayFee sends the dex registration fee. Transaction fees are in addition to
Expand All @@ -789,25 +782,20 @@ func (*ExchangeWallet) SwapConfirmations(ctx context.Context, id dex.Bytes, cont
}

// sendToAddr sends funds from acct to addr.
func (eth *ExchangeWallet) sendToAddr(addr common.Address, amt, gasPrice *big.Int) (common.Hash, error) {
tx := map[string]string{
"from": fmt.Sprintf("0x%x", eth.acct.Address),
"to": fmt.Sprintf("0x%x", addr),
"value": fmt.Sprintf("0x%x", amt),
"gasPrice": fmt.Sprintf("0x%x", gasPrice),
func (eth *ExchangeWallet) sendToAddr(addr common.Address, amt uint64) (*types.Transaction, error) {
txOpts, err := eth.node.txOpts(eth.ctx, amt, 0, defaultSendGasLimit)
if err != nil {
return nil, err
}
return eth.node.sendTransaction(eth.ctx, tx)
return eth.node.sendTransaction(eth.ctx, txOpts, addr, nil)
}

// Withdraw withdraws funds to the specified address. Value is gwei.
//
// TODO: Return the asset.Coin.
// TODO: Subtract fees from the value.
func (eth *ExchangeWallet) Withdraw(addr string, value, feeSuggestion uint64) (asset.Coin, error) {
bigVal := big.NewInt(0).SetUint64(value)
gweiFactorBig := big.NewInt(srveth.GweiFactor)
_, err := eth.sendToAddr(common.HexToAddress(addr),
bigVal.Mul(bigVal, gweiFactorBig), big.NewInt(0).SetUint64(defaultGasFee))
func (eth *ExchangeWallet) Withdraw(addr string, value, _ uint64) (asset.Coin, error) {
_, err := eth.sendToAddr(common.HexToAddress(addr), value)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -916,3 +904,28 @@ func (eth *ExchangeWallet) checkForNewBlocks() {
prevTip.Hash(), newTip.NumberU64(), newTip.Hash())
go eth.tipChange(nil)
}

func ethToGwei(v uint64) uint64 {
return v * dexeth.GweiFactor
}

func ethToWei(v uint64) *big.Int {
return big.NewInt(0).Mul(big.NewInt(int64(v)*dexeth.GweiFactor), big.NewInt(dexeth.GweiFactor))
}

func gweiToWei(v uint64) *big.Int {
return big.NewInt(0).Mul(big.NewInt(int64(v)), big.NewInt(dexeth.GweiFactor))
}

func weiToGwei(v *big.Int) uint64 {
return v.Div(v, big.NewInt(dexeth.GweiFactor)).Uint64()
}

func bytesToArray(b []byte) (a [32]byte) {
copy(a[:], b)
return
}

func bigUint(v uint64) *big.Int {
return big.NewInt(int64(v))
}
Loading

0 comments on commit ff5f9ef

Please sign in to comment.