hermez-node/eth/client.go
eth/client.goRaw
1package eth
2
3import (
4 "context"
5 "fmt"
6 "math/big"
7 "time"
8
9 "github.com/ethereum/go-ethereum/accounts"
10 "github.com/ethereum/go-ethereum/accounts/abi/bind"
11 ethKeystore "github.com/ethereum/go-ethereum/accounts/keystore"
12 ethCommon "github.com/ethereum/go-ethereum/common"
13 "github.com/ethereum/go-ethereum/core/types"
14 "github.com/ethereum/go-ethereum/ethclient"
15 "github.com/hermeznetwork/hermez-node/common"
16 "github.com/hermeznetwork/hermez-node/log"
17)
18
19var (
20 ErrAccountNil = fmt.Errorf("Authorized calls can't be made when the account is nil")
21 // ErrReceiptStatusFailed when receiving a failed transaction
22 ErrReceiptStatusFailed = fmt.Errorf("receipt status is failed")
23 // ErrReceiptNotRecieved when unable to retrieve a transaction
24 ErrReceiptNotReceived = fmt.Errorf("receipt not available")
25)
26
27const (
28 errStrDeploy = "deployment of %s failed: %w"
29 errStrWaitReceipt = "wait receipt of %s deploy failed: %w"
30
31 // default values
32 defaultCallGasLimit = 300000
33 defaultDeployGasLimit = 1000000
34 defaultGasPriceDiv = 100
35 defaultReceiptTimeout = 60
36 defaultIntervalReceiptLoop = 200
37)
38
39type Config struct {
40 CallGasLimit uint64
41 DeployGasLimit uint64
42 GasPriceDiv uint64
43 ReceiptTimeout time.Duration // in seconds
44 IntervalReceiptLoop time.Duration // in milliseconds
45}
46
47// Client is an ethereum client to call Smart Contract methods.
48type Client struct {
49 client *ethclient.Client
50 account *accounts.Account
51 ks *ethKeystore.KeyStore
52 ReceiptTimeout time.Duration
53 config *Config
54}
55
56// NewClient creates a Client instance. The account is not mandatory (it can
57// be nil). If the account is nil, CallAuth will fail with ErrAccountNil.
58func NewClient(client *ethclient.Client, account *accounts.Account, ks *ethKeystore.KeyStore, config *Config) *Client {
59 if config == nil {
60 config = &Config{
61 CallGasLimit: defaultCallGasLimit,
62 DeployGasLimit: defaultDeployGasLimit,
63 GasPriceDiv: defaultGasPriceDiv,
64 ReceiptTimeout: defaultReceiptTimeout,
65 IntervalReceiptLoop: defaultIntervalReceiptLoop,
66 }
67 }
68 return &Client{client: client, account: account, ks: ks, ReceiptTimeout: config.ReceiptTimeout * time.Second, config: config}
69}
70
71// BalanceAt retieves information about the default account
72func (c *Client) BalanceAt(addr ethCommon.Address) (*big.Int, error) {
73 return c.client.BalanceAt(context.TODO(), addr, nil)
74}
75
76// Account returns the underlying ethereum account
77func (c *Client) Account() *accounts.Account {
78 return c.account
79}
80
81// CallAuth performs a Smart Contract method call that requires authorization.
82// This call requires a valid account with Ether that can be spend during the
83// call.
84func (c *Client) CallAuth(gasLimit uint64,
85 fn func(*ethclient.Client, *bind.TransactOpts) (*types.Transaction, error)) (*types.Transaction, error) {
86 if c.account == nil {
87 return nil, ErrAccountNil
88 }
89
90 gasPrice, err := c.client.SuggestGasPrice(context.Background())
91 if err != nil {
92 return nil, err
93 }
94 inc := new(big.Int).Set(gasPrice)
95 inc.Div(inc, new(big.Int).SetUint64(c.config.GasPriceDiv))
96 gasPrice.Add(gasPrice, inc)
97 log.Debug("Transaction metadata", "gasPrice", gasPrice)
98
99 auth, err := bind.NewKeyStoreTransactor(c.ks, *c.account)
100 if err != nil {
101 return nil, err
102 }
103 auth.Value = big.NewInt(0) // in wei
104 if gasLimit == 0 {
105 auth.GasLimit = c.config.CallGasLimit // in units
106 } else {
107 auth.GasLimit = gasLimit // in units
108 }
109 auth.GasPrice = gasPrice
110
111 tx, err := fn(c.client, auth)
112 if tx != nil {
113 log.Debug("Transaction", "tx", tx.Hash().Hex(), "nonce", tx.Nonce())
114 }
115 return tx, err
116}
117
118type ContractData struct {
119 Address ethCommon.Address
120 Tx *types.Transaction
121 Receipt *types.Receipt
122}
123
124// Deploy a smart contract. `name` is used to log deployment information. fn
125// is a wrapper to the deploy function generated by abigen. In case of error,
126// the returned `ContractData` may have some parameters filled depending on the
127// kind of error that occurred.
128func (c *Client) Deploy(name string,
129 fn func(c *ethclient.Client, auth *bind.TransactOpts) (ethCommon.Address, *types.Transaction, interface{}, error)) (ContractData, error) {
130 var contractData ContractData
131 log.Info("Deploying", "contract", name)
132 tx, err := c.CallAuth(
133 c.config.DeployGasLimit,
134 func(client *ethclient.Client, auth *bind.TransactOpts) (*types.Transaction, error) {
135 addr, tx, _, err := fn(client, auth)
136 if err != nil {
137 return nil, err
138 }
139 contractData.Address = addr
140 return tx, nil
141 },
142 )
143 if err != nil {
144 return contractData, fmt.Errorf(errStrDeploy, name, err)
145 }
146 log.Info("Waiting receipt", "tx", tx.Hash().Hex(), "contract", name)
147 contractData.Tx = tx
148 receipt, err := c.WaitReceipt(tx)
149 if err != nil {
150 return contractData, fmt.Errorf(errStrWaitReceipt, name, err)
151 }
152 contractData.Receipt = receipt
153 return contractData, nil
154}
155
156// Call performs a read only Smart Contract method call.
157func (c *Client) Call(fn func(*ethclient.Client) error) error {
158 return fn(c.client)
159}
160
161// WaitReceipt will block until a transaction is confirmed. Internally it
162// polls the state every 200 milliseconds.
163func (c *Client) WaitReceipt(tx *types.Transaction) (*types.Receipt, error) {
164 return c.waitReceipt(tx, context.TODO(), c.ReceiptTimeout)
165}
166
167// GetReceipt will check if a transaction is confirmed and return
168// immediately, waiting at most 1 second and returning error if the transaction
169// is still pending.
170func (c *Client) GetReceipt(tx *types.Transaction) (*types.Receipt, error) {
171 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
172 defer cancel()
173 return c.waitReceipt(tx, ctx, 0)
174}
175
176func (c *Client) waitReceipt(tx *types.Transaction, ctx context.Context, timeout time.Duration) (*types.Receipt, error) {
177 var err error
178 var receipt *types.Receipt
179
180 txid := tx.Hash()
181 log.Debug("Waiting for receipt", "tx", txid.Hex())
182
183 start := time.Now()
184 for {
185 receipt, err = c.client.TransactionReceipt(ctx, txid)
186 if receipt != nil || time.Since(start) >= timeout {
187 break
188 }
189 time.Sleep(c.config.IntervalReceiptLoop * time.Millisecond)
190 }
191
192 if receipt != nil && receipt.Status == types.ReceiptStatusFailed {
193 log.Error("Failed transaction", "tx", txid.Hex())
194 return receipt, ErrReceiptStatusFailed
195 }
196
197 if receipt == nil {
198 log.Debug("Pendingtransaction / Wait receipt timeout", "tx", txid.Hex(), "lasterr", err)
199 return receipt, ErrReceiptNotReceived
200 }
201 log.Debug("Successful transaction", "tx", txid.Hex())
202
203 return receipt, err
204}
205
206// CurrentBlock returns the current block number in the blockchain
207func (c *Client) CurrentBlock() (*big.Int, error) {
208 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
209 defer cancel()
210 header, err := c.client.HeaderByNumber(ctx, nil)
211 if err != nil {
212 return nil, err
213 }
214 return header.Number, nil
215}
216
217// HeaderByNumber internally calls ethclient.Client HeaderByNumber
218func (c *Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
219 return c.client.HeaderByNumber(ctx, number)
220}
221
222// BlockByNumber internally calls ethclient.Client BlockByNumber and returns *common.Block
223func (c *Client) BlockByNumber(ctx context.Context, number *big.Int) (*common.Block, error) {
224 block, err := c.client.BlockByNumber(ctx, number)
225 if err != nil {
226 return nil, err
227 }
228 b := &common.Block{
229 EthBlockNum: block.Number().Uint64(),
230 Timestamp: time.Unix(int64(block.Time()), 0),
231 Hash: block.Hash(),
232 }
233 return b, nil
234}
235
236func (c *Client) ForgeCall(callData *common.CallDataForge) ([]byte, error) {
237 // TODO this depends on the smart contracts, once are ready this will be updated
238 return nil, nil
239}