/
amounts.go
234 lines (213 loc) · 6.72 KB
/
amounts.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
package stellarnet
import (
"fmt"
"math/big"
"regexp"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
stellaramount "github.com/stellar/go/amount"
"github.com/stellar/go/xdr"
)
const (
// StroopsPerLumen is the number of stroops in a lumen.
StroopsPerLumen = 10000000
)
var (
// Alow "-1", "1.", ".1", "1.1".
// But not "." or "" or fractions or exponents
decimalStrictRE = regexp.MustCompile(`^-?((\d+\.?\d*)|(\d*\.?\d+))$`)
)
func validateNumericalString(s string) (ok bool, err error) {
if s == "" {
return false, fmt.Errorf("expected decimal number but found empty string")
}
if !decimalStrictRE.MatchString(s) {
return false, fmt.Errorf("expected decimal number: %s", s)
}
return true, nil
}
// ParseStellarAmount parses a decimal number into an int64 suitable
// for the stellar protocol (7 significant digits).
// See also stellar/go/amount#ParseInt64
func ParseStellarAmount(s string) (int64, error) {
if _, err := validateNumericalString(s); err != nil {
return 0, err
}
return stellaramount.ParseInt64(s)
}
// StringFromStellarAmount returns an "amount string" from the provided raw int64 value `v`.
func StringFromStellarAmount(v int64) string {
return stellaramount.StringFromInt64(v)
}
// StringFromStellarXdrAmount returns StringFromStellarAmount with casting to int64.
func StringFromStellarXdrAmount(v xdr.Int64) string {
return stellaramount.String(v)
}
// ParseAmount parses a decimal number into a big rational.
// Used instead of big.Rat.SetString because the latter accepts
// additional formats like "1/2" and "1e10".
func ParseAmount(s string) (*big.Rat, error) {
if _, err := validateNumericalString(s); err != nil {
return nil, err
}
v, ok := new(big.Rat).SetString(s)
if !ok {
return nil, fmt.Errorf("expected decimal number: %s", s)
}
return v, nil
}
// ParseAmount parses a decimal number into a big decimal.
func parseAmountIntoDecimal(s string) (ret decimal.Decimal, err error) {
if _, err = validateNumericalString(s); err != nil {
return ret, err
}
ret, err = decimal.NewFromString(s)
if err != nil {
return ret, fmt.Errorf("expected decimal number: %s %v", s, err)
}
return ret, nil
}
// ConvertXLMToOutside converts an amount of lumens into an amount of outside currency.
// `rate` is the amount of outside currency that 1 XLM is worth. Example: "0.9389014463" = PLN / XLM
// The result is rounded to 7 digits past the decimal.
// The rounding is arbitrary but expected to be sufficient precision.
func ConvertXLMToOutside(xlmAmount, rate string) (outsideAmount string, err error) {
rateRat, err := parseExchangeRate(rate)
if err != nil {
return "", err
}
amountInt64, err := ParseStellarAmount(xlmAmount)
if err != nil {
return "", fmt.Errorf("parsing amount to convert: %q", err)
}
acc := big.NewRat(amountInt64, StroopsPerLumen)
acc.Mul(acc, rateRat)
return acc.FloatString(7), nil
}
// ConvertOutsideToXLM converts an amount of outside currency into an amount of lumens.
// `rate` is the amount of outside currency that 1 XLM is worth. Example: "0.9389014463" = PLN / XLM
// The result is rounded to 7 digits past the decimal (which is what XLM supports).
// The result returned can of a greater magnitude than XLM supports.
func ConvertOutsideToXLM(outsideAmount, rate string) (xlmAmount string, err error) {
rateRat, err := parseExchangeRate(rate)
if err != nil {
return "", err
}
acc, err := ParseAmount(outsideAmount)
if err != nil {
return "", fmt.Errorf("parsing amount to convert: %q", outsideAmount)
}
acc.Quo(acc, rateRat)
return acc.FloatString(7), nil
}
// CompareStellarAmounts compares amounts of stellar assets.
// Returns:
//
// -1 if x < y
// 0 if x == y
// +1 if x > y
//
func CompareStellarAmounts(amount1, amount2 string) (int, error) {
amountx, err := ParseStellarAmount(amount1)
if err != nil {
return 0, err
}
amounty, err := ParseStellarAmount(amount2)
if err != nil {
return 0, err
}
switch {
case amountx < amounty:
return -1, nil
case amountx > amounty:
return 1, nil
default:
return 0, nil
}
}
// WithinFactorStellarAmounts returns whether two amounts are within a factor of
// `maxFactor` of each other.
// For example maxFactor="0.01" returns whether they are within 1% of each other.
// <- (abs((a - b) / a) < fac) || (abs((a - b) / b < fac)
func WithinFactorStellarAmounts(amount1, amount2, maxFactor string) (bool, error) {
a, err := ParseStellarAmount(amount1)
if err != nil {
return false, err
}
b, err := ParseStellarAmount(amount2)
if err != nil {
return false, err
}
fac, err := ParseAmount(maxFactor)
if err != nil {
return false, fmt.Errorf("error parsing factor: %q %v", maxFactor, err)
}
if fac.Sign() < 0 {
return false, fmt.Errorf("negative factor: %q", maxFactor)
}
if a == 0 && b == 0 {
return true, nil
}
if a == 0 || b == 0 {
return false, nil
}
// BigRat method signatures are bizarre. This does not do what it looks like.
left := big.NewRat(a, StroopsPerLumen)
left.Sub(left, big.NewRat(b, StroopsPerLumen))
right := big.NewRat(1, 1)
right.Set(left)
left.Quo(left, big.NewRat(a, StroopsPerLumen))
right.Quo(right, big.NewRat(b, StroopsPerLumen))
left.Abs(left)
right.Abs(right)
return (left.Cmp(fac) < 1) || (right.Cmp(fac) < 1), nil
}
func parseExchangeRate(rate string) (*big.Rat, error) {
rateRat, err := ParseAmount(rate)
if err != nil {
return nil, fmt.Errorf("error parsing exchange rate: %q", rate)
}
sign := rateRat.Sign()
switch sign {
case 1:
return rateRat, nil
case 0:
return nil, errors.New("zero-value exchange rate")
case -1:
return nil, errors.New("negative exchange rate")
default:
return nil, fmt.Errorf("exchange rate of unknown sign (%v)", sign)
}
}
// PathPaymentMaxValue returns 105% * amount.
func PathPaymentMaxValue(amount string) (string, error) {
amtInt, err := stellaramount.ParseInt64(amount)
if err != nil {
return "", err
}
amtMax := (105 * amtInt) / 100
return StringFromStellarAmount(amtMax), nil
}
// FeeString converts a horizon.Transaction.FeePaid int32 from
// stroops to a lumens string.
func FeeString(fee int32) string {
n := big.NewRat(int64(fee), StroopsPerLumen)
return n.FloatString(7)
}
// GetStellarExchangeRate takes two amounts, and returns the exchange rate of 1 source unit to destination units.
// This is useful for comparing two different assets on the Stellar network, say XLM and AnchorUSD.
func GetStellarExchangeRate(source, destination string) (string, error) {
s, err := ParseStellarAmount(source)
if err != nil {
return "", fmt.Errorf("parsing source amount: %q", err)
}
if s == 0 {
return "", fmt.Errorf("cannot have a source amount of 0")
}
d, err := ParseStellarAmount(destination)
if err != nil {
return "", fmt.Errorf("parsing destination amount: %q", err)
}
rate := big.NewRat(d, s)
return rate.FloatString(7), nil
}