forked from stellar-deprecated/kelp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
functions.go
432 lines (371 loc) · 12.2 KB
/
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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
package utils
import (
"encoding/json"
"fmt"
"hash/fnv"
"log"
"math/big"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
hProtocol "github.com/stellar/go/protocols/horizon"
"github.com/stellar/go/protocols/horizon/base"
"github.com/stellar/go/txnbuild"
)
// Common Utilities needed by various bots
// Native is the string representing the type for the native lumen asset
const Native = "native"
// NativeAsset represents the native asset
var NativeAsset = hProtocol.Asset{Type: Native}
// SdexPrecision defines the number of decimals used in SDEX
const SdexPrecision int8 = 7
// ByPrice implements sort.Interface for []horizon.Offer based on the price
type ByPrice []hProtocol.Offer
func (a ByPrice) Len() int { return len(a) }
func (a ByPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPrice) Less(i, j int) bool {
return PriceAsFloat(a[i].Price) < PriceAsFloat(a[j].Price)
}
// PriceAsFloat converts a string price to a float price
func PriceAsFloat(price string) float64 {
p, err := strconv.ParseFloat(price, 64)
if err != nil {
log.Printf("Error parsing price: %s | %s\n", price, err)
return 0
}
return p
}
// AmountStringAsFloat converts a string amount to a float amount
func AmountStringAsFloat(amount string) float64 {
if amount == "" {
return 0
}
p, err := strconv.ParseFloat(amount, 64)
if err != nil {
log.Printf("Error parsing amount: %s | %s\n", amount, err)
return 0
}
return p
}
// ParseOfferAmount is a convenience method to parse an offer amount
func ParseOfferAmount(amt string) (float64, error) {
offerAmt, e := strconv.ParseFloat(amt, 64)
if e != nil {
log.Printf("error parsing offer amount: %s\n", e)
return -1, e
}
return offerAmt, nil
}
// GetPrice gets the price from an offer
func GetPrice(offer hProtocol.Offer) float64 {
if int64(offer.PriceR.D) == 0 {
return 0.0
}
return PriceAsFloat(big.NewRat(int64(offer.PriceR.N), int64(offer.PriceR.D)).FloatString(10))
}
// GetInvertedPrice gets the inverted price from an offer
func GetInvertedPrice(offer hProtocol.Offer) float64 {
if int64(offer.PriceR.N) == 0 {
return 0.0
}
return PriceAsFloat(big.NewRat(int64(offer.PriceR.D), int64(offer.PriceR.N)).FloatString(10))
}
// Asset2Asset converts a horizon.Asset to a txnbuild.Asset.
func Asset2Asset(Asset hProtocol.Asset) txnbuild.Asset {
if Asset.Type == Native {
return txnbuild.NativeAsset{}
}
return txnbuild.CreditAsset{Code: Asset.Code, Issuer: Asset.Issuer}
}
// Asset2Asset2 converts a txnbuild.Asset to a horizon.Asset.
func Asset2Asset2(Asset txnbuild.Asset) hProtocol.Asset {
a := hProtocol.Asset{}
a.Code = Asset.GetCode()
a.Issuer = Asset.GetIssuer()
if Asset.IsNative() {
a.Type = Native
} else if len(a.Code) > 4 {
a.Type = "credit_alphanum12"
} else {
a.Type = "credit_alphanum4"
}
return a
}
// Asset2String converts a horizon.Asset to a string representation, using "native" for the native XLM
func Asset2String(asset hProtocol.Asset) string {
if asset.Type == Native {
return Native
}
return fmt.Sprintf("%s:%s", asset.Code, asset.Issuer)
}
// Asset2CodeString extracts the code out of a horizon.Asset
func Asset2CodeString(asset hProtocol.Asset) string {
if asset.Type == Native {
return "XLM"
}
return asset.Code
}
// String2Asset converts a code:issuer to a horizon.Asset
func String2Asset(code string, issuer string) hProtocol.Asset {
if code == "XLM" {
return Asset2Asset2(txnbuild.NativeAsset{})
}
return Asset2Asset2(txnbuild.CreditAsset{Code: code, Issuer: issuer})
}
// LoadAllOffers loads all the offers for a given account
func LoadAllOffers(account string, api *horizonclient.Client) ([]hProtocol.Offer, error) {
// get what orders are outstanding now
offerReq := horizonclient.OfferRequest{
ForAccount: account,
Limit: uint(200),
}
offersPage, e := api.Offers(offerReq)
if e != nil {
return []hProtocol.Offer{}, fmt.Errorf("Can't load offers: %s\n", e)
}
offersRet := offersPage.Embedded.Records
for len(offersPage.Embedded.Records) > 0 {
offersPage, e = api.NextOffersPage(offersPage)
if e != nil {
return []hProtocol.Offer{}, fmt.Errorf("Can't load offers: %s\n", e)
}
offersRet = append(offersRet, offersPage.Embedded.Records...)
}
return offersRet, nil
}
// FilterOffers filters out the offers into selling and buying, where sellOffers sells the sellAsset and buyOffers buys the sellAsset
func FilterOffers(offers []hProtocol.Offer, sellAsset hProtocol.Asset, buyAsset hProtocol.Asset) (sellOffers []hProtocol.Offer, buyOffers []hProtocol.Offer) {
for _, offer := range offers {
if offer.Selling == sellAsset {
if offer.Buying == buyAsset {
sellOffers = append(sellOffers, offer)
}
} else if offer.Selling == buyAsset {
if offer.Buying == sellAsset {
buyOffers = append(buyOffers, offer)
}
}
}
return
}
// ParseSecret returns the address from the secret
func ParseSecret(secret string) (*string, error) {
if secret == "" {
return nil, nil
}
sourceKP, err := keypair.Parse(secret)
if err != nil {
return nil, err
}
address := sourceKP.Address()
return &address, nil
}
// ParseNetwork checks the horizon url and returns the test network if it contains "test"
func ParseNetwork(horizonURL string) string {
if strings.Contains(horizonURL, "test") {
return network.TestNetworkPassphrase
}
return network.PublicNetworkPassphrase
}
// GetJSON is a helper method to get json from a URL
func GetJSON(client http.Client, url string, target interface{}) error {
r, err := client.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
}
// GetCreditBalance is a drop-in for the function in the GoSDK, we want it to return nil if there's no balance (as opposed to "0")
func GetCreditBalance(a hProtocol.Account, code string, issuer string) *string {
for _, balance := range a.Balances {
if balance.Asset.Code == code && balance.Asset.Issuer == issuer {
return &balance.Balance
}
}
return nil
}
// AssetsEqual is a convenience method to compare horizon.Asset and base.Asset because they are not type aliased
func AssetsEqual(baseAsset base.Asset, horizonAsset hProtocol.Asset) bool {
return horizonAsset.Type == baseAsset.Type &&
horizonAsset.Code == baseAsset.Code &&
horizonAsset.Issuer == baseAsset.Issuer
}
// CheckFetchFloat tries to fetch and then cast the value for the provided key
func CheckFetchFloat(m map[string]interface{}, key string) (float64, error) {
v, ok := m[key]
if !ok {
return 0.0, fmt.Errorf("'%s' field not in map: %v", key, m)
}
f, ok := v.(float64)
if !ok {
return 0.0, fmt.Errorf("unable to cast '%s' field to float64, value: %v", key, v)
}
return f, nil
}
// CheckedString returns "<nil>" if the object is nil, otherwise calls the String() function on the object
func CheckedString(v interface{}) string {
if v == nil {
return "<nil>"
}
return fmt.Sprintf("%v", v)
}
// CheckedFloatPtr returns "<nil>" if the object is nil, otherwise calls the String() function on the object
func CheckedFloatPtr(v *float64) string {
if v == nil {
return "<nil>"
}
return fmt.Sprintf("%.10f", *v)
}
// ParseAsset returns a horizon asset a string
func ParseAsset(code string, issuer string) (*hProtocol.Asset, error) {
if code != "XLM" && issuer == "" {
return nil, fmt.Errorf("error: issuer can only be empty if asset is XLM")
}
if code == "XLM" && issuer != "" {
return nil, fmt.Errorf("error: issuer needs to be empty if asset is XLM")
}
if code == "XLM" {
asset := Asset2Asset2(txnbuild.NativeAsset{})
return &asset, nil
}
asset := Asset2Asset2(txnbuild.CreditAsset{Code: code, Issuer: issuer})
return &asset, nil
}
// AssetOnlyCodeEquals only checks the type and code of these assets, i.e. insensitive to asset issuer
func AssetOnlyCodeEquals(hAsset hProtocol.Asset, txnAsset txnbuild.Asset) (bool, error) {
if txnAsset.IsNative() {
return hAsset.Type == Native, nil
} else if hAsset.Type == Native {
return false, nil
}
return txnAsset.GetCode() == hAsset.Code, nil
}
// assetEqualsExact does an exact comparison of two assets
func assetEqualsExact(hAsset hProtocol.Asset, xAsset txnbuild.Asset) (bool, error) {
if xAsset.IsNative() {
return hAsset.Type == Native, nil
} else if hAsset.Type == Native {
return false, nil
}
return xAsset.GetCode() == hAsset.Code && xAsset.GetIssuer() == hAsset.Issuer, nil
}
// IsSelling helper method
// TODO DS Add tests for the various possible errors.
func IsSelling(sdexBase hProtocol.Asset, sdexQuote hProtocol.Asset, selling txnbuild.Asset, buying txnbuild.Asset) (bool, error) {
sellingBase, e := assetEqualsExact(sdexBase, selling)
if e != nil {
return false, fmt.Errorf("error comparing sdexBase with selling asset")
}
buyingQuote, e := assetEqualsExact(sdexQuote, buying)
if e != nil {
return false, fmt.Errorf("error comparing sdexQuote with buying asset")
}
if sellingBase && buyingQuote {
return true, nil
}
sellingQuote, e := assetEqualsExact(sdexQuote, selling)
if e != nil {
return false, fmt.Errorf("error comparing sdexQuote with selling asset")
}
buyingBase, e := assetEqualsExact(sdexBase, buying)
if e != nil {
return false, fmt.Errorf("error comparing sdexBase with buying asset")
}
if sellingQuote && buyingBase {
return false, nil
}
return false, fmt.Errorf("invalid assets, there are more than 2 distinct assets: sdexBase=%s, sdexQuote=%s, selling=%s, buying=%s", sdexBase, sdexQuote, selling, buying)
}
// Shuffle any string slice
func Shuffle(slice []string) {
r := rand.New(rand.NewSource(time.Now().Unix()))
for n := len(slice); n > 0; n-- {
randIndex := r.Intn(n)
slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1]
}
}
// SignWithSeed returns a new tx with the signatures of the passed in seeds
func SignWithSeed(tx *txnbuild.Transaction, network string, seeds ...string) (*txnbuild.Transaction, error) {
// create a copy
signedTx := &txnbuild.Transaction{}
*signedTx = *tx
for i, s := range seeds {
kp, e := keypair.Parse(s)
if e != nil {
return nil, fmt.Errorf("cannot parse seed into keypair at index %d: %s", i, e)
}
// keep adding signatures
signedTx, e = signedTx.Sign(network, kp.(*keypair.Full))
if e != nil {
return nil, fmt.Errorf("cannot sign tx with keypair at index %d (pubKey: %s): %s", i, kp.Address(), e)
}
}
return signedTx, nil
}
// StringSet converts a string slice to a map of string to bool values to represent a Set
func StringSet(list []string) map[string]bool {
m := map[string]bool{}
for _, s := range list {
m[s] = true
}
return m
}
// Dedupe removes duplicates from the list
func Dedupe(list []string) []string {
seen := map[string]bool{}
out := []string{}
for _, elem := range list {
if _, ok := seen[elem]; !ok {
out = append(out, elem)
seen[elem] = true
}
}
return out
}
// PrintErrorHintf shows a helpful hint for the user when there is an error (likely recoverable)
func PrintErrorHintf(message string, args ...interface{}) {
log.Printf("\n")
log.Printf("\n")
log.Printf("**************************************** HINT ****************************************\n")
log.Printf("\n")
log.Printf(message, args...)
log.Printf("\n")
log.Printf("*************************************** /HINT ****************************************\n")
log.Printf("\n")
}
// ParseMaybeFloat parses an optional string value as a float pointer
func ParseMaybeFloat(valueString string) (*float64, error) {
if valueString == "" {
return nil, nil
}
valueFloat, e := strconv.ParseFloat(valueString, 64)
if e != nil {
return nil, fmt.Errorf("unable to parse value '%s' as float: %s", valueString, e)
}
return &valueFloat, nil
}
// Offer2TxnBuildSellOffer converts an hProtocol.Offer to a txnbuild.ManageSellOffer
func Offer2TxnBuildSellOffer(offer hProtocol.Offer) txnbuild.ManageSellOffer {
return txnbuild.ManageSellOffer{
Selling: Asset2Asset(offer.Selling),
Buying: Asset2Asset(offer.Buying),
Amount: offer.Amount,
Price: offer.Price,
OfferID: offer.ID,
}
}
// HashString hashes a string using the FNV-1 hash function.
func HashString(s string) (uint32, error) {
h := fnv.New32a()
_, e := h.Write([]byte(s))
if e != nil {
return 0, fmt.Errorf("error while hashing string: %s", e)
}
return h.Sum32(), nil
}