-
Notifications
You must be signed in to change notification settings - Fork 34
/
client.go
193 lines (167 loc) · 5.99 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package eth
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethkeystore "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
// "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
log "github.com/sirupsen/logrus"
)
var (
ErrAccountNil = fmt.Errorf("Authorized calls can't be made when the account is nil")
// ErrReceiptStatusFailed when receiving a failed transaction
ErrReceiptStatusFailed = fmt.Errorf("receipt status is failed")
// ErrReceiptNotRecieved when unable to retrieve a transaction
ErrReceiptNotReceived = fmt.Errorf("receipt not available")
)
const (
errStrDeploy = "deployment of %s failed: %w"
errStrWaitReceipt = "wait receipt of %s deploy failed: %w"
)
// Client is an ethereum client to call Smart Contract methods.
type Client struct {
client *ethclient.Client
account *accounts.Account
ks *ethkeystore.KeyStore
ReceiptTimeout time.Duration
}
// NewClient creates a Client instance. The account is not mandatory (it can
// be nil). If the account is nil, CallAuth will fail with ErrAccountNil.
func NewClient(client *ethclient.Client, account *accounts.Account, ks *ethkeystore.KeyStore) *Client {
return &Client{client: client, account: account, ks: ks, ReceiptTimeout: 60 * time.Second}
}
// BalanceAt retieves information about the default account
func (c *Client) BalanceAt(addr common.Address) (*big.Int, error) {
return c.client.BalanceAt(context.TODO(), addr, nil)
}
// Account returns the underlying ethereum account
func (c *Client) Account() *accounts.Account {
return c.account
}
// CallAuth performs a Smart Contract method call that requires authorization.
// This call requires a valid account with Ether that can be spend during the
// call.
func (c *Client) CallAuth(gasLimit uint64,
fn func(*ethclient.Client, *bind.TransactOpts) (*types.Transaction, error)) (*types.Transaction, error) {
if c.account == nil {
return nil, ErrAccountNil
}
gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
return nil, err
}
inc := new(big.Int).Set(gasPrice)
inc.Div(inc, new(big.Int).SetUint64(100))
gasPrice.Add(gasPrice, inc)
log.WithField("gasPrice", gasPrice).Debug("Transaction metadata")
auth, err := bind.NewKeyStoreTransactor(c.ks, *c.account)
if err != nil {
return nil, err
}
auth.Value = big.NewInt(0) // in wei
if gasLimit == 0 {
auth.GasLimit = uint64(300000) // in units
} else {
auth.GasLimit = gasLimit // in units
}
auth.GasPrice = gasPrice
tx, err := fn(c.client, auth)
if tx != nil {
log.WithField("tx", tx.Hash().Hex()).WithField("nonce", tx.Nonce()).Debug("Transaction")
}
return tx, err
}
type ContractData struct {
Address common.Address
Tx *types.Transaction
Receipt *types.Receipt
}
// Deploy a smart contract. `name` is used to log deployment information. fn
// is a wrapper to the deploy function generated by abigen. In case of error,
// the returned `ContractData` may have some parameters filled depending on the
// kind of error that ocurred.
// successfull.
func (c *Client) Deploy(name string,
fn func(c *ethclient.Client, auth *bind.TransactOpts) (common.Address, *types.Transaction, interface{}, error)) (ContractData, error) {
var contractData ContractData
log.WithField("contract", name).Infof("Deploying")
tx, err := c.CallAuth(
1000000,
func(client *ethclient.Client, auth *bind.TransactOpts) (*types.Transaction, error) {
addr, tx, _, err := fn(client, auth)
if err != nil {
return nil, err
}
contractData.Address = addr
return tx, nil
},
)
if err != nil {
return contractData, fmt.Errorf(errStrDeploy, name, err)
}
log.WithField("tx", tx.Hash().Hex()).WithField("contract", name).Infof("Waiting receipt")
contractData.Tx = tx
receipt, err := c.WaitReceipt(tx)
if err != nil {
return contractData, fmt.Errorf(errStrWaitReceipt, name, err)
}
contractData.Receipt = receipt
return contractData, nil
}
// Call performs a read only Smart Contract method call.
func (c *Client) Call(fn func(*ethclient.Client) error) error {
return fn(c.client)
}
// WaitReceipt will block until a transaction is confirmed. Internally it
// polls the state every 200 milliseconds.
func (c *Client) WaitReceipt(tx *types.Transaction) (*types.Receipt, error) {
return c.waitReceipt(tx, context.TODO(), c.ReceiptTimeout)
}
// GetReceipt will check if a transaction is confirmed and return
// immediately, waiting at most 1 second and returning error if the transaction
// is still pending.
func (c *Client) GetReceipt(tx *types.Transaction) (*types.Receipt, error) {
ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
defer cancel()
return c.waitReceipt(tx, ctx, 0)
}
func (c *Client) waitReceipt(tx *types.Transaction, ctx context.Context, timeout time.Duration) (*types.Receipt, error) {
var err error
var receipt *types.Receipt
txid := tx.Hash()
log.WithField("tx", txid.Hex()).Debug("Waiting for receipt")
start := time.Now()
for {
receipt, err = c.client.TransactionReceipt(ctx, txid)
if receipt != nil || time.Since(start) >= timeout {
break
}
time.Sleep(200 * time.Millisecond)
}
if receipt != nil && receipt.Status == types.ReceiptStatusFailed {
log.WithField("tx", txid.Hex()).Error("Failed transaction")
return receipt, ErrReceiptStatusFailed
}
if receipt == nil {
log.WithField("tx", txid.Hex()).WithField("lasterr", err).Debug("Pending transaction / Wait receipt timeout")
return receipt, ErrReceiptNotReceived
}
log.WithField("tx", txid.Hex()).Debug("Successfull transaction")
return receipt, err
}
// CurrentBlock returns the current block number in the blockchain
func (c *Client) CurrentBlock() (*big.Int, error) {
ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
defer cancel()
header, err := c.client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
return header.Number, nil
}