Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/asset/eth: fix client balance calcs #1321

Merged
merged 4 commits into from Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions client/asset/eth/contractor.go
Expand Up @@ -39,6 +39,11 @@ type contractor interface {
estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error)
estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error)
isRedeemable(secretHash, secret [32]byte) (bool, error)
// incomingValue checks if the transaction redeems or refunds to the
// contract and sums the incoming value. It is not an error if the
// transaction does not pay to the contract, and the value returned in that
// case will always be zero.
incomingValue(context.Context, *types.Transaction) (uint64, error)
}

type contractorConstructor func(net dex.Network, addr common.Address, ec *ethclient.Client) (contractor, error)
Expand Down Expand Up @@ -223,6 +228,32 @@ func (c *contractorV0) estimateInitGas(ctx context.Context, n int) (uint64, erro
})
}

func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) {
if *tx.To() != c.contractAddr {
return 0, nil
}
if redeems, err := dexeth.ParseRedeemData(tx.Data()); err == nil {
var redeemed uint64
for _, redeem := range redeems {
swap, err := c.swap(ctx, redeem.SecretHash)
if err != nil {
return 0, fmt.Errorf("redeem swap error: %w", err)
}
redeemed += swap.Value
}
return redeemed, nil
}
secretHash, err := dexeth.ParseRefundData(tx.Data())
if err != nil {
return 0, nil
}
swap, err := c.swap(ctx, secretHash)
if err != nil {
return 0, fmt.Errorf("refund swap error: %w", err)
}
return swap.Value, nil
}

var contractorConstructors = map[uint32]contractorConstructor{
0: newV0contractor,
}
10 changes: 5 additions & 5 deletions client/asset/eth/eth.go
Expand Up @@ -148,9 +148,9 @@ type ethFetcher interface {
redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption, contractVer uint32) (*types.Transaction, error)
refund(txOpts *bind.TransactOpts, secretHash [32]byte, contractVer uint32) (*types.Transaction, error)
swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (*dexeth.SwapState, error)
lock(ctx context.Context) error
lock() error
locked() bool
unlock(ctx context.Context, pw string) error
unlock(pw string) error
signData(addr common.Address, data []byte) ([]byte, error)
sendToAddr(ctx context.Context, addr common.Address, val uint64) (*types.Transaction, error)
transactionConfirmations(context.Context, common.Hash) (uint32, error)
Expand Down Expand Up @@ -734,12 +734,12 @@ func (eth *ExchangeWallet) Address() (string, error) {

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

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

// Locked will be true if the wallet is currently locked.
Expand Down Expand Up @@ -902,7 +902,7 @@ func (eth *ExchangeWallet) checkForNewBlocks() {
// Balance is the current balance, including information about the pending
// balance.
type Balance struct {
Current, Pending, PendingIn, PendingOut *big.Int
Current, PendingIn, PendingOut *big.Int
}

func versionedBytes(ver uint32, h []byte) []byte {
Expand Down
29 changes: 14 additions & 15 deletions client/asset/eth/eth_test.go
Expand Up @@ -64,10 +64,9 @@ type testNode struct {
nonce uint64
}

func newBalance(current, pending, in, out uint64) *Balance {
func newBalance(current, in, out uint64) *Balance {
return &Balance{
Current: dexeth.GweiToWei(current),
Pending: dexeth.GweiToWei(pending),
PendingIn: dexeth.GweiToWei(in),
PendingOut: dexeth.GweiToWei(out),
}
Expand Down Expand Up @@ -95,10 +94,10 @@ func (n *testNode) balance(ctx context.Context) (*Balance, error) {
func (n *testNode) syncStatus(ctx context.Context) (bool, float32, error) {
return false, 0, nil
}
func (n *testNode) unlock(ctx context.Context, pw string) error {
func (n *testNode) unlock(pw string) error {
return nil
}
func (n *testNode) lock(ctx context.Context) error {
func (n *testNode) lock() error {
return nil
}
func (n *testNode) locked() bool {
Expand Down Expand Up @@ -354,7 +353,7 @@ func TestBalance(t *testing.T) {
// overMaxWei := new(big.Int).Set(maxWei)
// overMaxWei.Add(overMaxWei, gweiFactorBig)

tinyBal := newBalance(0, 0, 0, 0)
tinyBal := newBalance(0, 0, 0)
tinyBal.Current = big.NewInt(dexeth.GweiFactor - 1)

ctx, cancel := context.WithCancel(context.Background())
Expand All @@ -377,7 +376,7 @@ func TestBalance(t *testing.T) {
wantLocked uint64
}{{
name: "ok zero",
bal: newBalance(0, 0, 0, 0),
bal: newBalance(0, 0, 0),
wantBal: 0,
wantImmature: 0,
}, {
Expand All @@ -386,21 +385,21 @@ func TestBalance(t *testing.T) {
wantBal: 0,
}, {
name: "ok one",
bal: newBalance(1, 0, 0, 0),
bal: newBalance(1, 0, 0),
wantBal: 1,
}, {
name: "ok pending out",
bal: newBalance(4e8, (4-1.4)*1e8, 0, 1.4e8),
bal: newBalance(4e8, 0, 1.4e8),
wantBal: 2.6e8,
wantLocked: 1.4e8,
}, {
name: "ok pending in",
bal: newBalance(1e8, 4e8, 3e8, 0),
bal: newBalance(1e8, 3e8, 0),
wantBal: 1e8,
wantImmature: 3e8,
}, {
name: "ok pending out and in",
bal: newBalance(4e8, 5e8, 2e8, 1e8),
bal: newBalance(4e8, 2e8, 1e8),
wantBal: 3e8,
wantLocked: 1e8,
wantImmature: 2e8,
Expand Down Expand Up @@ -475,7 +474,7 @@ func TestFundOrderReturnCoinsFundingCoins(t *testing.T) {
Address: common.HexToAddress(address),
}
node := newTestNode(&account)
node.bal = newBalance(walletBalanceGwei, 0, 0, 0)
node.bal = newBalance(walletBalanceGwei, 0, 0)
eth := &ExchangeWallet{
node: node,
addr: node.address(),
Expand Down Expand Up @@ -822,7 +821,7 @@ func TestPreSwap(t *testing.T) {
FeeSuggestion: test.feeSuggestion,
}
node := newTestNode(nil)
node.bal = newBalance(test.bal*1e9, 0, 0, 0)
node.bal = newBalance(test.bal*1e9, 0, 0)
node.balErr = test.balErr
eth := &ExchangeWallet{
node: node,
Expand Down Expand Up @@ -867,7 +866,7 @@ func TestPreSwap(t *testing.T) {

func TestSwap(t *testing.T) {
node := &testNode{
bal: newBalance(0, 0, 0, 0),
bal: newBalance(0, 0, 0),
}
address := "0xB6De8BB5ed28E6bE6d671975cad20C03931bE981"
receivingAddress := "0x2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27"
Expand Down Expand Up @@ -1219,7 +1218,7 @@ func TestMaxOrder(t *testing.T) {
for _, test := range tests {
ctx, cancel := context.WithCancel(context.Background())
node := newTestNode(nil)
node.bal = newBalance(test.bal*1e9, 0, 0, 0)
node.bal = newBalance(test.bal*1e9, 0, 0)
node.balErr = test.balErr
eth := &ExchangeWallet{
node: node,
Expand Down Expand Up @@ -1428,7 +1427,7 @@ func TestSwapConfirmation(t *testing.T) {
hdr := &types.Header{}

node := &testNode{
bal: newBalance(0, 0, 0, 0),
bal: newBalance(0, 0, 0),
bestHdr: hdr,
swapMap: map[[32]byte]*dexeth.SwapState{
secretHash: state,
Expand Down
120 changes: 52 additions & 68 deletions client/asset/eth/nodeclient.go
Expand Up @@ -155,17 +155,6 @@ func (n *nodeClient) block(ctx context.Context, hash common.Hash) (*types.Block,
return n.leth.ApiBackend.BlockByHash(ctx, hash)
}

// accounts returns all accounts from the internal node.
func (n *nodeClient) accounts() []*accounts.Account {
var accts []*accounts.Account
for _, wallet := range n.node.AccountManager().Wallets() {
for _, acct := range wallet.Accounts() {
accts = append(accts, &acct)
}
}
return accts
}

func (n *nodeClient) stateAt(ctx context.Context, bn rpc.BlockNumber) (*state.StateDB, error) {
state, _, err := n.leth.ApiBackend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(bn))
if err != nil {
Expand All @@ -191,48 +180,74 @@ func (n *nodeClient) addressBalance(ctx context.Context, addr common.Address) (*

// balance gets the current and pending balances.
func (n *nodeClient) balance(ctx context.Context) (*Balance, error) {
pendingBal, err := n.balanceAt(ctx, n.creds.addr, rpc.PendingBlockNumber)
if err != nil {
return nil, fmt.Errorf("pending balance error: %w", err)
}

bal, err := n.balanceAt(ctx, n.creds.addr, rpc.LatestBlockNumber)
if err != nil {
return nil, fmt.Errorf("pending balance error: %w", err)
}

pendingTxs, err := n.pendingTransactions(ctx)
pendingTxs, err := n.pendingTransactions()
if err != nil {
return nil, fmt.Errorf("error getting pending txs: %w", err)
}

outgoing := new(big.Int)
for _, tx := range pendingTxs {
// TODO: Check that the from address matches addr. types.Transaction
// does not expose the from address directly.
outgoing.Add(outgoing, tx.Value())
incoming := new(big.Int)
zero := new(big.Int)

addFees := func(tx *types.Transaction) {
gas := new(big.Int).SetUint64(tx.Gas())
if gasPrice := tx.GasPrice(); gasPrice != nil && gasPrice.Cmp(zero) > 0 {
outgoing.Add(outgoing, new(big.Int).Mul(gas, gasPrice))
} else if gasFeeCap := tx.GasFeeCap(); gasFeeCap != nil {
outgoing.Add(outgoing, new(big.Int).Mul(gas, gasFeeCap))
} else {
n.log.Errorf("unable to calculate fees for tx %s", tx.Hash())
}
}

// pending = current + incoming - outgoing
// => incoming = pending + outgoing - current
incoming := new(big.Int).Add(pendingBal, outgoing)
incoming.Sub(incoming, bal)
ethSigner := types.LatestSigner(n.leth.ApiBackend.ChainConfig()) // "latest" good for pending

for _, tx := range pendingTxs {
from, _ := ethSigner.Sender(tx) // zero Address on error
if from != n.creds.addr {
continue
}
addFees(tx)
v := tx.Value()
if v.Cmp(zero) == 0 {
// If zero value, attempt to find redemptions or refunds that pay
// to us.
for ver, c := range n.contractors {
in, err := c.incomingValue(ctx, tx)
if err != nil {
n.log.Errorf("version %d contractor incomingValue error: %v", ver, err)
chappjc marked this conversation as resolved.
Show resolved Hide resolved
continue
}
if in > 0 {
incoming.Add(incoming, dexeth.GweiToWei(in))
}
}
} else {
// If non-zero outgoing value, this is a swap or a send of some
// type.
outgoing.Add(outgoing, v)
}
}

return &Balance{
Current: bal,
Pending: pendingBal,
PendingOut: outgoing,
PendingIn: incoming,
}, nil
}

// unlock the account indefinitely.
func (n *nodeClient) unlock(ctx context.Context, pw string) error {
func (n *nodeClient) unlock(pw string) error {
return n.creds.ks.TimedUnlock(*n.creds.acct, pw, 0)
}

// lock the account indefinitely.
func (n *nodeClient) lock(ctx context.Context) error {
func (n *nodeClient) lock() error {
return n.creds.ks.Lock(n.creds.addr)
}

Expand Down Expand Up @@ -264,16 +279,20 @@ func (n *nodeClient) transactionReceipt(ctx context.Context, txHash common.Hash)
if len(receipts) <= int(index) {
return nil, fmt.Errorf("no receipt at index %d in block %s for tx %s", index, blockHash, txHash)
}
return receipts[index], nil
receipt := receipts[index]
if receipt == nil {
return nil, fmt.Errorf("nil receipt at index %d in block %s for tx %s", index, blockHash, txHash)
}
return receipt, nil
buck54321 marked this conversation as resolved.
Show resolved Hide resolved
}

// pendingTransactions returns pending transactions.
func (n *nodeClient) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) {
func (n *nodeClient) pendingTransactions() ([]*types.Transaction, error) {
return n.leth.ApiBackend.GetPoolTransactions()
}

// addPeer adds a peer.
func (n *nodeClient) addPeer(ctx context.Context, peerURL string) error {
func (n *nodeClient) addPeer(peerURL string) error {
peer, err := enode.Parse(enode.ValidSchemes, peerURL)
if err != nil {
return err
Expand All @@ -282,17 +301,6 @@ func (n *nodeClient) addPeer(ctx context.Context, peerURL string) error {
return nil
}

// nodeInfo retrieves useful information about a node.
func (n *nodeClient) nodeInfo(ctx context.Context) *p2p.NodeInfo {
return n.p2pSrv.NodeInfo()
}

// listWallets list all of the wallet's wallets? and accounts along with details
// such as locked status.
func (n *nodeClient) listWallets(ctx context.Context) []accounts.Wallet {
return n.creds.ks.Wallets()
}

// sendTransaction sends a tx.
func (n *nodeClient) sendTransaction(ctx context.Context, txOpts *bind.TransactOpts,
to common.Address, data []byte) (*types.Transaction, error) {
Expand Down Expand Up @@ -328,11 +336,6 @@ func (n *nodeClient) syncProgress() ethereum.SyncProgress {
return n.leth.ApiBackend.SyncProgress()
}

// peers returns connected peers.
func (n *nodeClient) peers(ctx context.Context) []*p2p.Peer {
return n.p2pSrv.Peers()
}

// swap gets a swap keyed by secretHash in the contract.
func (n *nodeClient) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (swap *dexeth.SwapState, err error) {
return swap, n.withcontractor(contractVer, func(c contractor) error {
Expand All @@ -341,36 +344,17 @@ func (n *nodeClient) swap(ctx context.Context, secretHash [32]byte, contractVer
})
}

// wallet returns a wallet that owns acct from an ethereum wallet.
func (n *nodeClient) wallet(acct accounts.Account) (accounts.Wallet, error) {
wallet, err := n.node.AccountManager().Find(acct)
if err != nil {
return nil, fmt.Errorf("error finding wallet for account %s: %w", acct.Address, err)
}
return wallet, nil
}

// signData uses the private key of the address to sign a piece of data.
// The address must have been imported and unlocked to use this function.
func (n *nodeClient) signData(addr common.Address, data []byte) ([]byte, error) {
account := accounts.Account{Address: addr}
wallet, err := n.wallet(account)
if err != nil {
return nil, err
}

// The mime type argument to SignData is not used in the keystore wallet in geth.
// It treats any data like plain text.
return wallet.SignData(account, accounts.MimetypeTextPlain, data)
return n.creds.wallet.SignData(*n.creds.acct, accounts.MimetypeTextPlain, data)
}

func (n *nodeClient) addSignerToOpts(txOpts *bind.TransactOpts) error {
wallet, err := n.wallet(accounts.Account{Address: txOpts.From})
if err != nil {
return err
}
txOpts.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return wallet.SignTx(accounts.Account{Address: addr}, tx, n.chainID)
return n.creds.wallet.SignTx(accounts.Account{Address: addr}, tx, n.chainID)
}
return nil
}
Expand Down