forked from stellar-deprecated/kelp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sdex.go
328 lines (287 loc) · 9.92 KB
/
sdex.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
package plugins
import (
"log"
"strconv"
"github.com/lightyeario/kelp/support/utils"
"github.com/pkg/errors"
"github.com/stellar/go/build"
"github.com/stellar/go/clients/horizon"
)
const baseReserve = 0.5
// SDEX helps with building and submitting transactions to the Stellar network
type SDEX struct {
API *horizon.Client
SourceAccount string
TradingAccount string
SourceSeed string
TradingSeed string
Network build.Network
FractionalReserveMultiplier int8
operationalBuffer float64
simMode bool
// uninitialized
seqNum uint64
reloadSeqNum bool
cachedXlmExposure *float64
}
// MakeSDEX is a factory method for SDEX
func MakeSDEX(
api *horizon.Client,
sourceSeed string,
tradingSeed string,
sourceAccount string,
tradingAccount string,
network build.Network,
fractionalReserveMultiplier int8,
operationalBuffer float64,
simMode bool,
) *SDEX {
sdex := &SDEX{
API: api,
SourceSeed: sourceSeed,
TradingSeed: tradingSeed,
SourceAccount: sourceAccount,
TradingAccount: tradingAccount,
Network: network,
FractionalReserveMultiplier: fractionalReserveMultiplier,
operationalBuffer: operationalBuffer,
simMode: simMode,
}
log.Printf("Using network passphrase: %s\n", sdex.Network.Passphrase)
if sdex.SourceAccount == "" {
sdex.SourceAccount = sdex.TradingAccount
sdex.SourceSeed = sdex.TradingSeed
log.Println("No Source Account Set")
}
sdex.reloadSeqNum = true
return sdex
}
func (sdex *SDEX) incrementSeqNum() {
if sdex.reloadSeqNum {
log.Println("reloading sequence number")
seqNum, err := sdex.API.SequenceForAccount(sdex.SourceAccount)
if err != nil {
log.Printf("error getting seq num: %s\n", err)
return
}
sdex.seqNum = uint64(seqNum)
sdex.reloadSeqNum = false
}
sdex.seqNum++
}
// DeleteAllOffers is a helper that accumulates delete operations for the passed in offers
func (sdex *SDEX) DeleteAllOffers(offers []horizon.Offer) []build.TransactionMutator {
ops := []build.TransactionMutator{}
for _, offer := range offers {
op := sdex.DeleteOffer(offer)
ops = append(ops, &op)
}
return ops
}
// DeleteOffer returns the op that needs to be submitted to the network in order to delete the passed in offer
func (sdex *SDEX) DeleteOffer(offer horizon.Offer) build.ManageOfferBuilder {
rate := build.Rate{
Selling: utils.Asset2Asset(offer.Selling),
Buying: utils.Asset2Asset(offer.Buying),
Price: build.Price(offer.Price),
}
if sdex.SourceAccount == sdex.TradingAccount {
return build.ManageOffer(false, build.Amount("0"), rate, build.OfferID(offer.ID))
}
return build.ManageOffer(false, build.Amount("0"), rate, build.OfferID(offer.ID), build.SourceAccount{AddressOrSeed: sdex.TradingAccount})
}
// ModifyBuyOffer modifies a buy offer
func (sdex *SDEX) ModifyBuyOffer(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder {
return sdex.ModifySellOffer(offer, 1/price, amount*price)
}
// ModifySellOffer modifies a sell offer
func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder {
return sdex.createModifySellOffer(&offer, offer.Selling, offer.Buying, price, amount)
}
// CreateSellOffer creates a sell offer
func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder {
if amount > 0 {
return sdex.createModifySellOffer(nil, base, counter, price, amount)
}
log.Println("error: cannot place sell order, zero amount")
return nil
}
// ParseOfferAmount is a convenience method to parse an offer amount
func (sdex *SDEX) ParseOfferAmount(amt string) (float64, error) {
offerAmt, err := strconv.ParseFloat(amt, 64)
if err != nil {
log.Printf("error parsing offer amount: %s\n", err)
return -1, err
}
return offerAmt, nil
}
func (sdex *SDEX) minReserve(subentries int32) float64 {
return float64(2+subentries) * baseReserve
}
func (sdex *SDEX) lumenBalance() (float64, float64, error) {
account, err := sdex.API.LoadAccount(sdex.TradingAccount)
if err != nil {
log.Printf("error loading account to fetch lumen balance: %s\n", err)
return -1, -1, nil
}
for _, balance := range account.Balances {
if balance.Asset.Type == utils.Native {
b, e := strconv.ParseFloat(balance.Balance, 64)
if e != nil {
log.Printf("error parsing native balance: %s\n", e)
}
return b, sdex.minReserve(account.SubentryCount), e
}
}
return -1, -1, errors.New("could not find a native lumen balance")
}
// createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar
func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder {
if selling.Type == utils.Native {
var incrementalXlmAmount float64
if offer != nil {
offerAmt, err := sdex.ParseOfferAmount(offer.Amount)
if err != nil {
log.Println(err)
return nil
}
// modifying an offer will not increase the min reserve but will affect the xlm exposure
incrementalXlmAmount = amount - offerAmt
} else {
// creating a new offer will incrase the min reserve on the account so add baseReserve
incrementalXlmAmount = amount + baseReserve
}
// check if incrementalXlmAmount is within budget
bal, minAccountBal, err := sdex.lumenBalance()
if err != nil {
log.Println(err)
return nil
}
xlmExposure, err := sdex.xlmExposure()
if err != nil {
log.Println(err)
return nil
}
additionalExposure := incrementalXlmAmount >= 0
possibleTerminalExposure := ((xlmExposure + incrementalXlmAmount) / float64(sdex.FractionalReserveMultiplier)) > (bal - minAccountBal - sdex.operationalBuffer)
if additionalExposure && possibleTerminalExposure {
log.Println("not placing offer because we run the risk of running out of lumens | xlmExposure:", xlmExposure,
"| incrementalXlmAmount:", incrementalXlmAmount, "| bal:", bal, "| minAccountBal:", minAccountBal,
"| operationalBuffer:", sdex.operationalBuffer, "| fractionalReserveMultiplier:", sdex.FractionalReserveMultiplier)
return nil
}
}
stringPrice := strconv.FormatFloat(price, 'f', int(utils.SdexPrecision), 64)
rate := build.Rate{
Selling: utils.Asset2Asset(selling),
Buying: utils.Asset2Asset(buying),
Price: build.Price(stringPrice),
}
mutators := []interface{}{
rate,
build.Amount(strconv.FormatFloat(amount, 'f', int(utils.SdexPrecision), 64)),
}
if offer != nil {
mutators = append(mutators, build.OfferID(offer.ID))
}
if sdex.SourceAccount != sdex.TradingAccount {
mutators = append(mutators, build.SourceAccount{AddressOrSeed: sdex.TradingAccount})
}
result := build.ManageOffer(false, mutators...)
return &result
}
// SubmitOps submits the passed in operations to the network asynchronously in a single transaction
func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator) error {
sdex.incrementSeqNum()
muts := []build.TransactionMutator{
build.Sequence{Sequence: sdex.seqNum},
sdex.Network,
build.SourceAccount{AddressOrSeed: sdex.SourceAccount},
}
muts = append(muts, ops...)
tx, e := build.Transaction(muts...)
if e != nil {
return errors.Wrap(e, "SubmitOps error: ")
}
// convert to xdr string
txeB64, e := sdex.sign(tx)
if e != nil {
return e
}
log.Printf("tx XDR: %s\n", txeB64)
// submit
if !sdex.simMode {
log.Println("submitting tx XDR to network (async)")
go sdex.submit(txeB64)
} else {
log.Println("not submitting tx XDR to network in simulation mode")
}
return nil
}
// CreateBuyOffer creates a buy offer
func (sdex *SDEX) CreateBuyOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder {
return sdex.CreateSellOffer(counter, base, 1/price, amount*price)
}
func (sdex *SDEX) sign(tx *build.TransactionBuilder) (string, error) {
var txe build.TransactionEnvelopeBuilder
var e error
if sdex.SourceSeed != sdex.TradingSeed {
txe, e = tx.Sign(sdex.SourceSeed, sdex.TradingSeed)
} else {
txe, e = tx.Sign(sdex.SourceSeed)
}
if e != nil {
return "", e
}
return txe.Base64()
}
func (sdex *SDEX) submit(txeB64 string) {
resp, err := sdex.API.SubmitTransaction(txeB64)
if err != nil {
if herr, ok := errors.Cause(err).(*horizon.Error); ok {
var rcs *horizon.TransactionResultCodes
rcs, err = herr.ResultCodes()
if err != nil {
log.Printf("(async) error: no result codes from horizon: %s\n", err)
return
}
if rcs.TransactionCode == "tx_bad_seq" {
log.Println("(async) error: tx_bad_seq, setting flag to reload seq number")
sdex.reloadSeqNum = true
}
log.Println("(async) error: result code details: tx code =", rcs.TransactionCode, ", opcodes =", rcs.OperationCodes)
} else {
log.Printf("(async) error: tx failed for unknown reason, error message: %s\n", err)
}
return
}
log.Printf("(async) tx confirmation hash: %s\n", resp.Hash)
}
// ResetCachedXlmExposure resets the cache
func (sdex *SDEX) ResetCachedXlmExposure() {
sdex.cachedXlmExposure = nil
}
func (sdex *SDEX) xlmExposure() (float64, error) {
if sdex.cachedXlmExposure != nil {
return *sdex.cachedXlmExposure, nil
}
// uses all offers for this trading account to accommodate sharing by other bots
offers, err := utils.LoadAllOffers(sdex.TradingAccount, sdex.API)
if err != nil {
log.Printf("error computing XLM exposure: %s\n", err)
return -1, err
}
var sum float64
for _, offer := range offers {
// only need to compute sum of selling because that's the max XLM we can give up if all our offers are taken
if offer.Selling.Type == utils.Native {
offerAmt, err := sdex.ParseOfferAmount(offer.Amount)
if err != nil {
return -1, err
}
sum += offerAmt
}
}
sdex.cachedXlmExposure = &sum
return sum, nil
}