Skip to content

Commit

Permalink
client/assets: Add ethereum.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Mar 9, 2021
1 parent e725834 commit 708ab8d
Show file tree
Hide file tree
Showing 8 changed files with 1,076 additions and 1 deletion.
162 changes: 162 additions & 0 deletions client/asset/eth/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package eth

import (
"bytes"
"context"
"errors"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)

// Check that client satisfies the ethFetcher interface.
var _ ethFetcher = (*client)(nil)

// client satisfies the ethFetcher interface. Do not use until Connect is
// called.
type client struct {
// c is a direct client for raw calls.
c *rpc.Client
// ec wraps the client with some useful calls.
ec *ethclient.Client
}

// Connect connects to an ipc socket. It then wraps ethclient's client and
// bundles commands in a form we can easily use.
func (c *client) Connect(ctx context.Context, IPC string) error {
client, err := rpc.DialIPC(ctx, IPC)
if err != nil {
return fmt.Errorf("unable to dial rpc: %v", err)
}
ec := ethclient.NewClient(client)
c.c = client
c.ec = ec
return nil
}

// Shutdown shuts down the client.
func (c *client) Shutdown() {
if c.ec != nil {
// this will also close c.c
c.ec.Close()
}
}

// BestBlockHash gets the best blocks hash at the time of calling. Due to the
// speed of Ethereum blocks, this changes often.
func (c *client) BestBlockHash(ctx context.Context) (common.Hash, error) {
header, err := c.bestHeader(ctx)
if err != nil {
return common.Hash{}, err
}
return header.Hash(), nil
}

func (c *client) bestHeader(ctx context.Context) (*types.Header, error) {
bn, err := c.ec.BlockNumber(ctx)
if err != nil {
return nil, err
}
header, err := c.ec.HeaderByNumber(ctx, big.NewInt(int64(bn)))
if err != nil {
return nil, err
}
return header, nil
}

// Block gets the block identified by hash.
func (c *client) Block(ctx context.Context, hash common.Hash) (*types.Block, error) {
block, err := c.ec.BlockByHash(ctx, hash)
if err != nil {
return nil, err
}
return block, nil
}

// Accounts uses a raw request to obtain all accounts from personal.listAccounts.
func (c *client) Accounts(ctx context.Context) ([]common.Address, error) {
var res []common.Address
if err := c.c.CallContext(ctx, &res, "personal_listAccounts"); err != nil {
return nil, err
}
return res, nil
}

// Balance gets the current balance of an account.
func (c *client) Balance(ctx context.Context, acct common.Address) (*big.Int, error) {
return c.ec.BalanceAt(ctx, acct, nil)
}

// Unlock uses a raw request to unlock an account indefinitely.
func (c *client) Unlock(ctx context.Context, pw string, acct common.Address) error {
// Passing 0 as the last argument unlocks with not lock time.
return c.c.CallContext(ctx, nil, "personal_unlockAccount", acct, pw, 0)
}

// Lock uses a raw request to unlock an account indefinitely.
func (c *client) Lock(ctx context.Context, acct common.Address) error {
return c.c.CallContext(ctx, nil, "personal_lockAccount", acct)
}

// Locked uses a raw request to unlock an account indefinitely.
func (c *client) Locked(ctx context.Context, acct common.Address) (bool, error) {
type rawWallet struct {
URL string `json:"url"`
Status string `json:"status"`
Failure string `json:"failure,omitempty"`
Accounts []accounts.Account `json:"accounts,omitempty"`
}
var res []rawWallet
if err := c.c.CallContext(ctx, &res, "personal_listWallets"); err != nil {
return false, err
}
var wallet rawWallet
findWallet := func() bool {
for _, w := range res {
for _, a := range w.Accounts {
if bytes.Equal(a.Address[:], acct[:]) {
wallet = w
return true
}
}
}
return false
}
if !findWallet() {
return false, errors.New("unable to find account")
}
return wallet.Status != "Unlocked", nil
}

// SendToAddr uses a raw request to send funds to an addr from acct.
func (c *client) SendToAddr(ctx context.Context, acct, addr common.Address, amt, gasFee *big.Int) error {
tx := map[string]string{
"from": fmt.Sprintf("0x%x", acct),
"to": fmt.Sprintf("0x%x", addr),
"value": fmt.Sprintf("0x%x", amt),
"gasPrice": fmt.Sprintf("0x%x", gasFee),
}
return c.c.CallContext(ctx, nil, "eth_sendTransaction", tx)
}

// SyncStatus gets the current sync status of a node.
func (c *client) SyncStatus(ctx context.Context) (bool, float32, error) {
sync, err := c.ec.SyncProgress(ctx)
if err != nil {
return false, 0, err
}
// TODO: Ensure sync == nil means that the node is no longer syncing.
if sync == nil {
return true, 1, nil
}
ratio := float32(sync.CurrentBlock) / float32(sync.HighestBlock)
return false, ratio, nil
}
144 changes: 144 additions & 0 deletions client/asset/eth/client_harness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// +build harness
//
// This test requires that the testnet harness be running and the unix socket
// be located at $HOME/dextest/eth/gamma/node/geth.ipc

package eth

import (
"fmt"
"math/big"
"os"
"path/filepath"

"context"
"testing"
)

const pw = "abc"

var (
homeDir = os.Getenv("HOME")
ipc = filepath.Join(homeDir, "dextest/eth/gamma/node/geth.ipc")
ethClient = new(client)
ctx context.Context
)

func TestMain(m *testing.M) {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(context.Background())
defer func() {
cancel()
ethClient.Shutdown()
}()
if err := ethClient.Connect(ctx, ipc); err != nil {
fmt.Printf("Connect error: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}

func TestBestBlockHash(t *testing.T) {
_, err := ethClient.BestBlockHash(ctx)
if err != nil {
t.Fatal(err)
}
}

func TestBestHeader(t *testing.T) {
_, err := ethClient.bestHeader(ctx)
if err != nil {
t.Fatal(err)
}
}

func TestBlock(t *testing.T) {
h, err := ethClient.BestBlockHash(ctx)
if err != nil {
t.Fatal(err)
}
_, err = ethClient.Block(ctx, h)
if err != nil {
t.Fatal(err)
}
}

func TestAccounts(t *testing.T) {
_, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
}

func TestBalance(t *testing.T) {
accts, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
_, err = ethClient.Balance(ctx, accts[0])
if err != nil {
t.Fatal(err)
}
}

func TestUnlock(t *testing.T) {
accts, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
err = ethClient.Unlock(ctx, pw, accts[0])
if err != nil {
t.Fatal(err)
}
}

func TestLock(t *testing.T) {
accts, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
err = ethClient.Lock(ctx, accts[0])
if err != nil {
t.Fatal(err)
}
}

func TestLocked(t *testing.T) {
accts, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
err = ethClient.Unlock(ctx, pw, accts[0])
if err != nil {
t.Fatal(err)
}
locked, err := ethClient.Locked(ctx, accts[0])
if err != nil {
t.Fatal(err)
}
if locked {
t.Fatal("expected account to be unlocked")
}
}

func TestSendToAddr(t *testing.T) {
accts, err := ethClient.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
err = ethClient.Unlock(ctx, pw, accts[0])
if err != nil {
t.Fatal(err)
}
err = ethClient.SendToAddr(ctx, accts[0], accts[0], big.NewInt(1), big.NewInt(82000000000))
if err != nil {
t.Fatal(err)
}
}

func TestSyncStatus(t *testing.T) {
_, _, err := ethClient.SyncStatus(ctx)
if err != nil {
t.Fatal(err)
}
}
28 changes: 28 additions & 0 deletions client/asset/eth/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package eth

import (
"fmt"

"decred.org/dcrdex/dex"
)

var ()

// load checks the network.
//
// TODO: Test this with windows.
func load(network dex.Network) error {
switch network {
case dex.Simnet:
case dex.Testnet:
case dex.Mainnet:
// TODO: Allow.
return fmt.Errorf("eth cannot be used on mainnet")
default:
return fmt.Errorf("unknown network ID: %d", uint8(network))
}
return nil
}
Loading

0 comments on commit 708ab8d

Please sign in to comment.