-
Notifications
You must be signed in to change notification settings - Fork 6
/
call_functions.go
329 lines (283 loc) · 9.27 KB
/
call_functions.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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/fiatjaf/etleneum/data"
"github.com/fiatjaf/etleneum/runlua"
)
var balanceNotifyClient = http.Client{
Timeout: time.Second * 2,
CheckRedirect: func(r *http.Request, via []*http.Request) error {
return fmt.Errorf("target '%s' has returned a redirect", r.URL)
},
}
type CallContext struct {
VisitedContracts map[string]bool
Transfers []data.Transfer
Funds map[string]int64
AccountBalances map[string]int64
}
func getCallCosts(c data.Call, isLnurl bool) int64 {
cost := s.FixedCallCostSatoshis * 1000 // a fixed cost of 1 satoshi by default
if !isLnurl {
chars := int64(len(string(c.Payload)))
cost += 10 * chars // 50 msatoshi for each character in the payload
}
return cost
}
func callFromRedis(callid string) (call *data.Call, err error) {
var jcall []byte
call = &data.Call{}
jcall, err = rds.Get("call:" + callid).Bytes()
if err != nil {
return
}
err = json.Unmarshal(jcall, call)
if err != nil {
return
}
return
}
func saveCallOnRedis(call data.Call) (jcall []byte, err error) {
jcall, err = json.Marshal(call)
if err != nil {
return
}
err = rds.Set("call:"+call.Id, jcall, time.Hour*20).Err()
if err != nil {
return
}
return
}
func runCallGlobal(call *data.Call, useBalance bool) (err error) {
// initialize context
callContext := &CallContext{
VisitedContracts: make(map[string]bool),
Funds: make(map[string]int64),
AccountBalances: make(map[string]int64),
}
// actually run the call
err = runCall(call, callContext, useBalance)
if err != nil {
return err
}
// check balances of contracts and accounts involved
for key, balance := range callContext.AccountBalances {
if balance < 0 {
log.Warn().Err(err).Str("callid", call.Id).Str("account", key).
Msg("account balance out of funds")
return errors.New("account balance out of funds")
}
// also write this
if err := data.SaveAccountBalance(key, balance); err != nil {
return fmt.Errorf("error saving account balance: %w", err)
}
}
for id, funds := range callContext.Funds {
if funds < 0 {
log.Warn().Err(err).Str("callid", call.Id).Str("contract", id).
Msg("contract out of funds")
return errors.New("contract out of funds")
}
// also write this
if err := data.SaveContractFunds(id, funds); err != nil {
return fmt.Errorf("error saving contract funds: %w", err)
}
}
if err := data.SaveTransfers(call, callContext.Transfers); err != nil {
return err
}
// at this point the call has succeeded, we can then notify all accounts
// that have a balanceNotify URL set on their metadata
go func() {
time.Sleep(2 * time.Second) // give some time for the call to be finished
for key, balance := range callContext.AccountBalances {
if balanceWithReserve(balance) >= MIN_WITHDRAWABLE {
metadata := data.GetAccountMetadata(key)
if metadata.BalanceNotify != "" {
log := log.With().Str("account", key).Str("url", metadata.BalanceNotify).Int64("balance", balance).Logger()
resp, err := balanceNotifyClient.Post(metadata.BalanceNotify, "", nil)
if err != nil {
log.Warn().Err(err).Msg("balanceNotify call failed")
} else if resp.StatusCode >= 300 {
log.Warn().Int("status", resp.StatusCode).Msg("balanceNotify call returned bad status code")
} else {
log.Info().Msg("balanceNotify call succeeded")
}
}
}
}
}()
return nil
}
func runCall(call *data.Call, callContext *CallContext, useBalance bool) (err error) {
if _, visited := callContext.VisitedContracts[call.ContractId]; visited {
// can't call a method on the same contract (for now?)
// (if this ever change see ##callswithsameid)
return errors.New("can't call a method on the same contract")
}
// get contract data
ct, err := data.GetContract(call.ContractId)
if err != nil {
return fmt.Errorf("failed to load contract %s: %w", call.ContractId, err)
}
callContext.VisitedContracts[call.ContractId] = true
// pay for this with the caller's balance?
if call.Caller != "" && useBalance {
// burn amount corresponding to the call msatoshi + call cost.
// we don't transfer to the contract directly
// because the call already has the msatoshi amount assigned to it and that
// is already automatically added to the contract balance,
// so the contract would receive the money twice if we also did a transfer here.
balance := data.GetAccountBalance(call.Caller)
if balance < call.Msatoshi+call.Cost {
log.Warn().Err(err).Msg("account has insufficient funds to execute call")
dispatchContractEvent(call.ContractId,
ctevent{
call.Id, call.ContractId, call.Method, call.Msatoshi,
"", "balance",
},
"call-error")
return errors.New("insufficient account balance")
}
callContext.AccountBalances[call.Caller] = balance - (call.Msatoshi + call.Cost)
callContext.Transfers = append(callContext.Transfers, data.Transfer{
From: call.Caller,
To: "",
Msatoshi: call.Cost,
})
callContext.Transfers = append(callContext.Transfers, data.Transfer{
From: call.Caller,
To: call.ContractId,
Msatoshi: call.Msatoshi,
})
} else {
// take note of the amount sent in this call as a transfer
callContext.Transfers = append(callContext.Transfers, data.Transfer{
From: "",
To: call.ContractId,
Msatoshi: call.Msatoshi,
})
}
callContext.Funds[call.ContractId] = ct.Funds + call.Msatoshi
// actually run the call
dispatchContractEvent(call.ContractId, ctevent{call.Id, call.ContractId, call.Method, call.Msatoshi, "", "start"}, "call-run-event")
newStateO, err := runlua.RunCall(
log,
&callPrinter{call.ContractId, call.Id, call.Method},
func(r *http.Request) (*http.Response, error) { return http.DefaultClient.Do(r) },
// get external contract
func(contractId string) (state interface{}, funds int64, err error) {
ct, err := data.GetContract(contractId)
if err != nil {
return
}
err = json.Unmarshal(ct.State, &state)
if err != nil {
return
}
return state, ct.Funds, nil
},
// call external method
func(externalContractId string, method string, payload interface{}, msatoshi int64) (err error) {
jpayload, _ := json.Marshal(payload)
// build the call
externalCall := &data.Call{
ContractId: externalContractId,
Id: call.Id, // repeat the call id ##callswithsameid
// (since we won't do two calls with the same id on the same contract)
Method: method,
Payload: jpayload,
Msatoshi: msatoshi,
Cost: 1000, // only the fixed cost, the other costs are included
Caller: call.ContractId,
}
// pay for the call (by burning msatoshis from the caller contract)
callContext.Funds[call.ContractId] -= (externalCall.Cost + externalCall.Msatoshi)
callContext.Transfers = append(callContext.Transfers, data.Transfer{
From: call.ContractId,
To: externalCall.ContractId,
Msatoshi: externalCall.Cost + externalCall.Msatoshi,
})
// then run
err = runCall(externalCall, callContext, false)
if err != nil {
return err
}
return nil
},
// get contract balance
func() (contractFunds int64, err error) {
ct, err := data.GetContract(ct.Id)
if err != nil {
return
}
return ct.Funds, nil
},
// send from contract
func(target string, msat int64) (msatoshiSent int64, err error) {
if len(target) == 0 {
return 0, errors.New("can't send to blank recipient")
}
callContext.Transfers = append(callContext.Transfers, data.Transfer{
From: call.ContractId,
To: target,
Msatoshi: msat,
})
callContext.Funds[call.ContractId] -= msat
if target[0] == 'c' {
// it's a contract
if current, ok := callContext.Funds[target]; ok {
callContext.Funds[target] = current + msat
} else {
targetContract, err := data.GetContract(target)
if err != nil {
return 0, errors.New("contract " + target + " not found")
}
callContext.Funds[target] = targetContract.Funds + msat
}
} else if target[0] == '0' {
// it's an account
if current, ok := callContext.AccountBalances[target]; ok {
callContext.AccountBalances[target] = current + msat
} else {
current := data.GetAccountBalance(target)
callContext.AccountBalances[target] = current + msat
}
} else {
return 0, errors.New("invalid recipient " + target)
}
dispatchContractEvent(call.ContractId, ctevent{call.Id, call.ContractId, call.Method, call.Msatoshi, fmt.Sprintf("contract.send(%s, %d)", target, msat), "function"}, "call-run-event")
return msat, nil
},
// get account balance
func() (userBalance int64, err error) {
if call.Caller == "" {
return 0, errors.New("no account")
}
return data.GetAccountBalance(call.Caller), nil
},
*ct,
*call,
)
if err != nil {
return fmt.Errorf("error executing method: %w", err)
}
newState, err := json.Marshal(newStateO)
if err != nil {
return fmt.Errorf("error marshaling new state: %w", err)
}
// write call files
if err = data.SaveCall(call); err != nil {
return fmt.Errorf("error saving call data: %w", err)
}
if err := data.SaveContractState(call.ContractId, newState); err != nil {
return fmt.Errorf("error saving contract state: %w", err)
}
// ok, all is good
log.Info().Str("callid", call.Id).Msg("call done")
return
}