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
32// Client is an ethereum client to call Smart Contract methods.
33type Client struct {
34 client *ethclient.Client
35 account *accounts.Account
36 ks *ethKeystore.KeyStore
37 ReceiptTimeout time.Duration
38}
39
40// NewClient creates a Client instance. The account is not mandatory (it can
41// be nil). If the account is nil, CallAuth will fail with ErrAccountNil.
42func NewClient(client *ethclient.Client, account *accounts.Account, ks *ethKeystore.KeyStore) *Client {
43 return &Client{client: client, account: account, ks: ks, ReceiptTimeout: 60 * time.Second}
44}
45
46// BalanceAt retieves information about the default account
47func (c *Client) BalanceAt(addr ethCommon.Address) (*big.Int, error) {
48 return c.client.BalanceAt(context.TODO(), addr, nil)
49}
50
51// Account returns the underlying ethereum account
52func (c *Client) Account() *accounts.Account {
53 return c.account
54}
55
56// CallAuth performs a Smart Contract method call that requires authorization.
57// This call requires a valid account with Ether that can be spend during the
58// call.
59func (c *Client) CallAuth(gasLimit uint64,
60 fn func(*ethclient.Client, *bind.TransactOpts) (*types.Transaction, error)) (*types.Transaction, error) {
61 if c.account == nil {
62 return nil, ErrAccountNil
63 }
64
65 gasPrice, err := c.client.SuggestGasPrice(context.Background())
66 if err != nil {
67 return nil, err
68 }
69 inc := new(big.Int).Set(gasPrice)
70 inc.Div(inc, new(big.Int).SetUint64(100))
71 gasPrice.Add(gasPrice, inc)
72 log.Debug("Transaction metadata", "gasPrice", gasPrice)
73
74 auth, err := bind.NewKeyStoreTransactor(c.ks, *c.account)
75 if err != nil {
76 return nil, err
77 }
78 auth.Value = big.NewInt(0) // in wei
79 if gasLimit == 0 {
80 auth.GasLimit = uint64(300000) // in units
81 } else {
82 auth.GasLimit = gasLimit // in units
83 }
84 auth.GasPrice = gasPrice
85
86 tx, err := fn(c.client, auth)
87 if tx != nil {
88 log.Debug("Transaction", "tx", tx.Hash().Hex(), "nonce", tx.Nonce())
89 }
90 return tx, err
91}
92
93type ContractData struct {
94 Address ethCommon.Address
95 Tx *types.Transaction
96 Receipt *types.Receipt
97}
98
99// Deploy a smart contract. `name` is used to log deployment information. fn
100// is a wrapper to the deploy function generated by abigen. In case of error,
101// the returned `ContractData` may have some parameters filled depending on the
102// kind of error that ocurred.
103// successfull.
104func (c *Client) Deploy(name string,
105 fn func(c *ethclient.Client, auth *bind.TransactOpts) (ethCommon.Address, *types.Transaction, interface{}, error)) (ContractData, error) {
106 var contractData ContractData
107 log.Info("Deploying", "contract", name)
108 tx, err := c.CallAuth(
109 1000000,
110 func(client *ethclient.Client, auth *bind.TransactOpts) (*types.Transaction, error) {
111 addr, tx, _, err := fn(client, auth)
112 if err != nil {
113 return nil, err
114 }
115 contractData.Address = addr
116 return tx, nil
117 },
118 )
119 if err != nil {
120 return contractData, fmt.Errorf(errStrDeploy, name, err)
121 }
122 log.Info("Waiting receipt", "tx", tx.Hash().Hex(), "contract", name)
123 contractData.Tx = tx
124 receipt, err := c.WaitReceipt(tx)
125 if err != nil {
126 return contractData, fmt.Errorf(errStrWaitReceipt, name, err)
127 }
128 contractData.Receipt = receipt
129 return contractData, nil
130}
131
132// Call performs a read only Smart Contract method call.
133func (c *Client) Call(fn func(*ethclient.Client) error) error {
134 return fn(c.client)
135}
136
137// WaitReceipt will block until a transaction is confirmed. Internally it
138// polls the state every 200 milliseconds.
139func (c *Client) WaitReceipt(tx *types.Transaction) (*types.Receipt, error) {
140 return c.waitReceipt(tx, context.TODO(), c.ReceiptTimeout)
141}
142
143// GetReceipt will check if a transaction is confirmed and return
144// immediately, waiting at most 1 second and returning error if the transaction
145// is still pending.
146func (c *Client) GetReceipt(tx *types.Transaction) (*types.Receipt, error) {
147 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
148 defer cancel()
149 return c.waitReceipt(tx, ctx, 0)
150}
151
152func (c *Client) waitReceipt(tx *types.Transaction, ctx context.Context, timeout time.Duration) (*types.Receipt, error) {
153 var err error
154 var receipt *types.Receipt
155
156 txid := tx.Hash()
157 log.Debug("Waiting for receipt", "tx", txid.Hex())
158
159 start := time.Now()
160 for {
161 receipt, err = c.client.TransactionReceipt(ctx, txid)
162 if receipt != nil || time.Since(start) >= timeout {
163 break
164 }
165 time.Sleep(200 * time.Millisecond)
166 }
167
168 if receipt != nil && receipt.Status == types.ReceiptStatusFailed {
169 log.Error("Failed transaction", "tx", txid.Hex())
170 return receipt, ErrReceiptStatusFailed
171 }
172
173 if receipt == nil {
174 log.Debug("Pendingtransaction / Wait receipt timeout", "tx", txid.Hex(), "lasterr", err)
175 return receipt, ErrReceiptNotReceived
176 }
177 log.Debug("Successfull transaction", "tx", txid.Hex())
178
179 return receipt, err
180}
181
182// CurrentBlock returns the current block number in the blockchain
183func (c *Client) CurrentBlock() (*big.Int, error) {
184 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
185 defer cancel()
186 header, err := c.client.HeaderByNumber(ctx, nil)
187 if err != nil {
188 return nil, err
189 }
190 return header.Number, nil
191}
192
193// HeaderByNumber internally calls ethclient.Client HeaderByNumber
194func (c *Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
195 return c.client.HeaderByNumber(ctx, number)
196}
197
198// BlockByNumber internally calls ethclient.Client BlockByNumber
199func (c *Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
200 return c.client.BlockByNumber(ctx, number)
201}
202
203func (c *Client) ForgeCall(callData *common.CallDataForge) ([]byte, error) {
204 // TODO this depends on the smart contracts, once are ready this will be updated
205 return nil, nil
206}