-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
evm_transfer_controller.go
159 lines (134 loc) · 4.94 KB
/
evm_transfer_controller.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
package web
import (
"context"
"fmt"
"math/big"
"net/http"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"
commontxmgr "github.com/smartcontractkit/chainlink/v2/common/txmgr"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils"
"github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm"
"github.com/smartcontractkit/chainlink/v2/core/logger/audit"
"github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
"github.com/smartcontractkit/chainlink/v2/core/store/models"
"github.com/smartcontractkit/chainlink/v2/core/web/presenters"
"github.com/gin-gonic/gin"
)
// EVMTransfersController can send LINK tokens to another address
type EVMTransfersController struct {
App chainlink.Application
}
// Create sends ETH from the Chainlink's account to a specified address.
//
// Example: "<application>/withdrawals"
func (tc *EVMTransfersController) Create(c *gin.Context) {
var tr models.SendEtherRequest
if err := c.ShouldBindJSON(&tr); err != nil {
jsonAPIError(c, http.StatusBadRequest, err)
return
}
chain, err := getChain(tc.App.GetRelayers().LegacyEVMChains(), tr.EVMChainID.String())
if err != nil {
if errors.Is(err, ErrInvalidChainID) || errors.Is(err, ErrMultipleChains) || errors.Is(err, ErrMissingChainID) {
jsonAPIError(c, http.StatusUnprocessableEntity, err)
return
}
jsonAPIError(c, http.StatusInternalServerError, err)
return
}
if tr.FromAddress == utils.ZeroAddress {
jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("withdrawal source address is missing: %v", tr.FromAddress))
return
}
if !tr.AllowHigherAmounts {
err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount)
if err != nil {
jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("transaction failed: %v", err))
return
}
}
etx, err := chain.TxManager().SendNativeToken(c, chain.ID(), tr.FromAddress, tr.DestinationAddress, *tr.Amount.ToInt(), chain.Config().EVM().GasEstimator().LimitTransfer())
if err != nil {
jsonAPIError(c, http.StatusBadRequest, errors.Errorf("transaction failed: %v", err))
return
}
tc.App.GetAuditLogger().Audit(audit.EthTransactionCreated, map[string]interface{}{
"ethTX": etx,
})
// skip waiting for txmgr to create TxAttempt
if tr.SkipWaitTxAttempt {
jsonAPIResponse(c, presenters.NewEthTxResource(etx), "eth_tx")
return
}
timeout := 10 * time.Second // default
if tr.WaitAttemptTimeout != nil {
timeout = *tr.WaitAttemptTimeout
}
// wait and retrieve tx attempt matching tx ID
attempt, err := FindTxAttempt(c, timeout, etx, tc.App.TxmStorageService().FindTxWithAttempts)
if err != nil {
jsonAPIError(c, http.StatusGatewayTimeout, fmt.Errorf("failed to find transaction within timeout: %w", err))
return
}
jsonAPIResponse(c, presenters.NewEthTxResourceFromAttempt(attempt), "eth_tx")
}
// ValidateEthBalanceForTransfer validates that the current balance can cover the transaction amount
func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth) error {
var err error
var balance *big.Int
balanceMonitor := chain.BalanceMonitor()
if balanceMonitor != nil {
balance = balanceMonitor.GetEthBalance(fromAddr).ToInt()
} else {
balance, err = chain.Client().BalanceAt(c, fromAddr, nil)
if err != nil {
return err
}
}
zero := big.NewInt(0)
if balance == nil || balance.Cmp(zero) == 0 {
return errors.Errorf("balance is too low for this transaction to be executed: %v", balance)
}
gasLimit := chain.Config().EVM().GasEstimator().LimitTransfer()
estimator := chain.GasEstimator()
amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr))
if err != nil {
return err
}
if balance.Cmp(amountWithFees) < 0 {
// ETH balance is less than the sent amount + fees
return errors.Errorf("balance is too low for this transaction to be executed: %v", balance)
}
return nil
}
func FindTxAttempt(ctx context.Context, timeout time.Duration, etx txmgr.Tx, FindTxWithAttempts func(int64) (txmgr.Tx, error)) (attempt txmgr.TxAttempt, err error) {
recheckTime := time.Second
tick := time.After(0)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return attempt, fmt.Errorf("%w - tx may still have been broadcast", ctx.Err())
case <-tick:
etx, err = FindTxWithAttempts(etx.ID)
if err != nil {
return attempt, fmt.Errorf("failed to find transaction: %w", err)
}
}
// exit if tx attempts are found
// also validate etx.State != unstarted (ensure proper tx state for tx with attempts)
if len(etx.TxAttempts) > 0 && etx.State != commontxmgr.TxUnstarted {
break
}
tick = time.After(recheckTime)
}
// attach original tx to attempt
attempt = etx.TxAttempts[0]
attempt.Tx = etx
return attempt, nil
}