-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
cosmos_transfer_controller.go
133 lines (115 loc) · 4.59 KB
/
cosmos_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
package web
import (
"net/http"
sdk "github.com/cosmos/cosmos-sdk/types"
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/client"
"github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/denom"
"github.com/smartcontractkit/chainlink/v2/core/chains/cosmos"
"github.com/smartcontractkit/chainlink/v2/core/logger/audit"
"github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
cosmosmodels "github.com/smartcontractkit/chainlink/v2/core/store/models/cosmos"
"github.com/smartcontractkit/chainlink/v2/core/web/presenters"
)
// maxGasUsedTransfer is an upper bound on how much gas we expect a MsgSend for a single coin to use.
const maxGasUsedTransfer = 100_000
// CosmosTransfersController can send LINK tokens to another address
type CosmosTransfersController struct {
App chainlink.Application
}
// Create sends native coins from the Chainlink's account to a specified address.
func (tc *CosmosTransfersController) Create(c *gin.Context) {
cosmosChains := tc.App.GetRelayers().LegacyCosmosChains()
if cosmosChains == nil {
jsonAPIError(c, http.StatusBadRequest, ErrCosmosNotEnabled)
return
}
var tr cosmosmodels.SendRequest
if err := c.ShouldBindJSON(&tr); err != nil {
jsonAPIError(c, http.StatusBadRequest, err)
return
}
if tr.CosmosChainID == "" {
jsonAPIError(c, http.StatusBadRequest, errors.New("missing cosmosChainID"))
return
}
// TODO what about ctx in Get? ctx was used here but not in ETH calls. maybe better to make the interface require ctx and
// put in TODOs in ETH...
chain, err := cosmosChains.Get(tr.CosmosChainID) //cosmosChains.Chain(c.Request.Context(), tr.CosmosChainID)
if errors.Is(err, cosmos.ErrChainIDInvalid) || errors.Is(err, cosmos.ErrChainIDEmpty) {
jsonAPIError(c, http.StatusBadRequest, err)
return
} else if err != nil {
jsonAPIError(c, http.StatusInternalServerError, err)
return
}
if tr.FromAddress.Empty() {
jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("withdrawal source address is missing: %v", tr.FromAddress))
return
}
coin, err := denom.ConvertDecCoinToDenom(sdk.NewDecCoinFromDec(tr.Token, tr.Amount), chain.Config().GasToken())
if err != nil {
jsonAPIError(c, http.StatusBadRequest, errors.Errorf("unable to convert %s to %s: %v", tr.Token, chain.Config().GasToken(), err))
return
} else if !coin.Amount.IsPositive() {
jsonAPIError(c, http.StatusBadRequest, errors.Errorf("amount must be greater than zero: %s", coin.Amount))
return
}
txm := chain.TxManager()
if !tr.AllowHigherAmounts {
var reader client.Reader
reader, err = chain.Reader("")
if err != nil {
jsonAPIError(c, http.StatusInternalServerError, errors.Errorf("chain unreachable: %v", err))
return
}
gasPrice, err2 := txm.GasPrice()
if err2 != nil {
jsonAPIError(c, http.StatusInternalServerError, errors.Errorf("gas price unavailable: %v", err2))
return
}
err = cosmosValidateBalance(reader, gasPrice, tr.FromAddress, coin)
if err != nil {
jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("failed to validate balance: %v", err))
return
}
}
sendMsg := bank.NewMsgSend(tr.FromAddress, tr.DestinationAddress, sdk.Coins{coin})
msgID, err := txm.Enqueue("", sendMsg)
if err != nil {
jsonAPIError(c, http.StatusInternalServerError, errors.Errorf("transaction failed: %v", err))
return
}
resource := presenters.NewCosmosMsgResource(msgID, tr.CosmosChainID, "")
msgs, err := txm.GetMsgs(msgID)
if err != nil {
jsonAPIError(c, http.StatusInternalServerError, errors.Errorf("failed to get message %d: %v", msgID, err))
return
}
if len(msgs) != 1 {
jsonAPIError(c, http.StatusInternalServerError, errors.Errorf("failed to get message %d: %v", msgID, err))
return
}
msg := msgs[0]
resource.TxHash = msg.TxHash
resource.State = string(msg.State)
tc.App.GetAuditLogger().Audit(audit.CosmosTransactionCreated, map[string]interface{}{
"cosmosTransactionResource": resource,
})
jsonAPIResponse(c, resource, "cosmos_msg")
}
// cosmosValidateBalance validates that fromAddr's balance can cover coin, including fees at gasPrice.
func cosmosValidateBalance(reader client.Reader, gasPrice sdk.DecCoin, fromAddr sdk.AccAddress, coin sdk.Coin) error {
balance, err := reader.Balance(fromAddr, coin.GetDenom())
if err != nil {
return err
}
fee := gasPrice.Amount.MulInt64(maxGasUsedTransfer).RoundInt()
need := coin.Amount.Add(fee)
if balance.Amount.LT(need) {
return errors.Errorf("balance %q is too low for this transaction to be executed: need %s total, including %s fee", balance, need, fee)
}
return nil
}