-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e725834
commit 708ab8d
Showing
8 changed files
with
1,076 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.