forked from cosmos/cosmos-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
coin.go
554 lines (459 loc) · 13.3 KB
/
coin.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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
package types
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
)
//-----------------------------------------------------------------------------
// Coin
// Coin hold some amount of one currency.
//
// CONTRACT: A coin will never hold a negative amount of any denomination.
//
// TODO: Make field members private for further safety.
type Coin struct {
Denom string `json:"denom"`
// To allow the use of unsigned integers (see: #1273) a larger refactor will
// need to be made. So we use signed integers for now with safety measures in
// place preventing negative values being used.
Amount Int `json:"amount"`
}
// NewCoin returns a new coin with a denomination and amount. It will panic if
// the amount is negative.
func NewCoin(denom string, amount Int) Coin {
mustValidateDenom(denom)
if amount.LT(ZeroInt()) {
panic(fmt.Errorf("negative coin amount: %v", amount))
}
return Coin{
Denom: denom,
Amount: amount,
}
}
// NewInt64Coin returns a new coin with a denomination and amount. It will panic
// if the amount is negative.
func NewInt64Coin(denom string, amount int64) Coin {
return NewCoin(denom, NewInt(amount))
}
// String provides a human-readable representation of a coin
func (coin Coin) String() string {
return fmt.Sprintf("%v%v", coin.Amount, coin.Denom)
}
// IsZero returns if this represents no money
func (coin Coin) IsZero() bool {
return coin.Amount.IsZero()
}
// IsGTE returns true if they are the same type and the receiver is
// an equal or greater value
func (coin Coin) IsGTE(other Coin) bool {
if coin.Denom != other.Denom {
panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom))
}
return !coin.Amount.LT(other.Amount)
}
// IsLT returns true if they are the same type and the receiver is
// a smaller value
func (coin Coin) IsLT(other Coin) bool {
if coin.Denom != other.Denom {
panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom))
}
return coin.Amount.LT(other.Amount)
}
// IsEqual returns true if the two sets of Coins have the same value
func (coin Coin) IsEqual(other Coin) bool {
if coin.Denom != other.Denom {
panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom))
}
return coin.Amount.Equal(other.Amount)
}
// Adds amounts of two coins with same denom. If the coins differ in denom then
// it panics.
func (coin Coin) Add(coinB Coin) Coin {
if coin.Denom != coinB.Denom {
panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, coinB.Denom))
}
return Coin{coin.Denom, coin.Amount.Add(coinB.Amount)}
}
// Subtracts amounts of two coins with same denom. If the coins differ in denom
// then it panics.
func (coin Coin) Sub(coinB Coin) Coin {
if coin.Denom != coinB.Denom {
panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, coinB.Denom))
}
res := Coin{coin.Denom, coin.Amount.Sub(coinB.Amount)}
if res.IsNegative() {
panic("negative count amount")
}
return res
}
// IsPositive returns true if coin amount is positive.
//
// TODO: Remove once unsigned integers are used.
func (coin Coin) IsPositive() bool {
return coin.Amount.Sign() == 1
}
// IsNegative returns true if the coin amount is negative and false otherwise.
//
// TODO: Remove once unsigned integers are used.
func (coin Coin) IsNegative() bool {
return coin.Amount.Sign() == -1
}
//-----------------------------------------------------------------------------
// Coins
// Coins is a set of Coin, one per currency
type Coins []Coin
func (coins Coins) String() string {
if len(coins) == 0 {
return ""
}
out := ""
for _, coin := range coins {
out += fmt.Sprintf("%v,", coin.String())
}
return out[:len(out)-1]
}
// IsValid asserts the Coins are sorted, have positive amount,
// and Denom does not contain upper case characters.
func (coins Coins) IsValid() bool {
switch len(coins) {
case 0:
return true
case 1:
if err := validateDenom(coins[0].Denom); err != nil {
return false
}
return coins[0].IsPositive()
default:
// check single coin case
if !(Coins{coins[0]}).IsValid() {
return false
}
lowDenom := coins[0].Denom
for _, coin := range coins[1:] {
if strings.ToLower(coin.Denom) != coin.Denom {
return false
}
if coin.Denom <= lowDenom {
return false
}
if !coin.IsPositive() {
return false
}
// we compare each coin against the last denom
lowDenom = coin.Denom
}
return true
}
}
// Add adds two sets of coins.
//
// e.g.
// {2A} + {A, 2B} = {3A, 2B}
// {2A} + {0B} = {2A}
//
// NOTE: Add operates under the invariant that coins are sorted by
// denominations.
//
// CONTRACT: Add will never return Coins where one Coin has a non-positive
// amount. In otherwords, IsValid will always return true.
func (coins Coins) Add(coinsB Coins) Coins {
return coins.safeAdd(coinsB)
}
// safeAdd will perform addition of two coins sets. If both coin sets are
// empty, then an empty set is returned. If only a single set is empty, the
// other set is returned. Otherwise, the coins are compared in order of their
// denomination and addition only occurs when the denominations match, otherwise
// the coin is simply added to the sum assuming it's not zero.
func (coins Coins) safeAdd(coinsB Coins) Coins {
sum := ([]Coin)(nil)
indexA, indexB := 0, 0
lenA, lenB := len(coins), len(coinsB)
for {
if indexA == lenA {
if indexB == lenB {
// return nil coins if both sets are empty
return sum
}
// return set B (excluding zero coins) if set A is empty
return append(sum, removeZeroCoins(coinsB[indexB:])...)
} else if indexB == lenB {
// return set A (excluding zero coins) if set B is empty
return append(sum, removeZeroCoins(coins[indexA:])...)
}
coinA, coinB := coins[indexA], coinsB[indexB]
switch strings.Compare(coinA.Denom, coinB.Denom) {
case -1: // coin A denom < coin B denom
if !coinA.IsZero() {
sum = append(sum, coinA)
}
indexA++
case 0: // coin A denom == coin B denom
res := coinA.Add(coinB)
if !res.IsZero() {
sum = append(sum, res)
}
indexA++
indexB++
case 1: // coin A denom > coin B denom
if !coinB.IsZero() {
sum = append(sum, coinB)
}
indexB++
}
}
}
// Sub subtracts a set of coins from another.
//
// e.g.
// {2A, 3B} - {A} = {A, 3B}
// {2A} - {0B} = {2A}
// {A, B} - {A} = {B}
//
// CONTRACT: Sub will never return Coins where one Coin has a non-positive
// amount. In otherwords, IsValid will always return true.
func (coins Coins) Sub(coinsB Coins) Coins {
diff, hasNeg := coins.SafeSub(coinsB)
if hasNeg {
panic("negative coin amount")
}
return diff
}
// SafeSub performs the same arithmetic as Sub but returns a boolean if any
// negative coin amount was returned.
func (coins Coins) SafeSub(coinsB Coins) (Coins, bool) {
diff := coins.safeAdd(coinsB.negative())
return diff, diff.IsAnyNegative()
}
// IsAllGT returns true if for every denom in coins, the denom is present at a
// greater amount in coinsB.
func (coins Coins) IsAllGT(coinsB Coins) bool {
diff, _ := coins.SafeSub(coinsB)
if len(diff) == 0 {
return false
}
return diff.IsAllPositive()
}
// IsAllGTE returns true iff for every denom in coins, the denom is present at
// an equal or greater amount in coinsB.
func (coins Coins) IsAllGTE(coinsB Coins) bool {
diff, _ := coins.SafeSub(coinsB)
if len(diff) == 0 {
return true
}
return !diff.IsAnyNegative()
}
// IsAllLT returns True iff for every denom in coins, the denom is present at
// a smaller amount in coinsB.
func (coins Coins) IsAllLT(coinsB Coins) bool {
return coinsB.IsAllGT(coins)
}
// IsAllLTE returns true iff for every denom in coins, the denom is present at
// a smaller or equal amount in coinsB.
func (coins Coins) IsAllLTE(coinsB Coins) bool {
return coinsB.IsAllGTE(coins)
}
// IsAnyGTE returns true iff coins contains at least one denom that is present
// at a greater or equal amount in coinsB; it returns false otherwise.
//
// NOTE: IsAnyGTE operates under the invariant that both coin sets are sorted
// by denominations and there exists no zero coins.
func (coins Coins) IsAnyGTE(coinsB Coins) bool {
if len(coinsB) == 0 {
return false
}
for _, coin := range coins {
amt := coinsB.AmountOf(coin.Denom)
if coin.Amount.GTE(amt) && !amt.IsZero() {
return true
}
}
return false
}
// IsZero returns true if there are no coins or all coins are zero.
func (coins Coins) IsZero() bool {
for _, coin := range coins {
if !coin.IsZero() {
return false
}
}
return true
}
// IsEqual returns true if the two sets of Coins have the same value
func (coins Coins) IsEqual(coinsB Coins) bool {
if len(coins) != len(coinsB) {
return false
}
coins = coins.Sort()
coinsB = coinsB.Sort()
for i := 0; i < len(coins); i++ {
if !coins[i].IsEqual(coinsB[i]) {
return false
}
}
return true
}
// Empty returns true if there are no coins and false otherwise.
func (coins Coins) Empty() bool {
return len(coins) == 0
}
// Returns the amount of a denom from coins
func (coins Coins) AmountOf(denom string) Int {
mustValidateDenom(denom)
switch len(coins) {
case 0:
return ZeroInt()
case 1:
coin := coins[0]
if coin.Denom == denom {
return coin.Amount
}
return ZeroInt()
default:
midIdx := len(coins) / 2 // 2:1, 3:1, 4:2
coin := coins[midIdx]
if denom < coin.Denom {
return coins[:midIdx].AmountOf(denom)
} else if denom == coin.Denom {
return coin.Amount
} else {
return coins[midIdx+1:].AmountOf(denom)
}
}
}
// IsAllPositive returns true if there is at least one coin and all currencies
// have a positive value.
func (coins Coins) IsAllPositive() bool {
if len(coins) == 0 {
return false
}
for _, coin := range coins {
if !coin.IsPositive() {
return false
}
}
return true
}
// IsAnyNegative returns true if there is at least one coin whose amount
// is negative; returns false otherwise. It returns false if the coin set
// is empty too.
//
// TODO: Remove once unsigned integers are used.
func (coins Coins) IsAnyNegative() bool {
for _, coin := range coins {
if coin.IsNegative() {
return true
}
}
return false
}
// negative returns a set of coins with all amount negative.
//
// TODO: Remove once unsigned integers are used.
func (coins Coins) negative() Coins {
res := make([]Coin, 0, len(coins))
for _, coin := range coins {
res = append(res, Coin{
Denom: coin.Denom,
Amount: coin.Amount.Neg(),
})
}
return res
}
// removeZeroCoins removes all zero coins from the given coin set in-place.
func removeZeroCoins(coins Coins) Coins {
i, l := 0, len(coins)
for i < l {
if coins[i].IsZero() {
// remove coin
coins = append(coins[:i], coins[i+1:]...)
l--
} else {
i++
}
}
return coins[:i]
}
func copyCoins(coins Coins) Coins {
copyCoins := make(Coins, len(coins))
copy(copyCoins, coins)
return copyCoins
}
//-----------------------------------------------------------------------------
// Sort interface
//nolint
func (coins Coins) Len() int { return len(coins) }
func (coins Coins) Less(i, j int) bool { return coins[i].Denom < coins[j].Denom }
func (coins Coins) Swap(i, j int) { coins[i], coins[j] = coins[j], coins[i] }
var _ sort.Interface = Coins{}
// Sort is a helper function to sort the set of coins inplace
func (coins Coins) Sort() Coins {
sort.Sort(coins)
return coins
}
//-----------------------------------------------------------------------------
// Parsing
var (
// Denominations can be 3 ~ 16 characters long.
reDnmString = `[a-z][a-z0-9]{2,15}`
reAmt = `[[:digit:]]+`
reDecAmt = `[[:digit:]]*\.[[:digit:]]+`
reSpc = `[[:space:]]*`
reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, reDnmString))
reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnmString))
reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, reDnmString))
)
func validateDenom(denom string) error {
if !reDnm.MatchString(denom) {
return errors.New("illegal characters")
}
return nil
}
func mustValidateDenom(denom string) {
if err := validateDenom(denom); err != nil {
panic(err)
}
}
// ParseCoin parses a cli input for one coin type, returning errors if invalid.
// This returns an error on an empty string as well.
func ParseCoin(coinStr string) (coin Coin, err error) {
coinStr = strings.TrimSpace(coinStr)
matches := reCoin.FindStringSubmatch(coinStr)
if matches == nil {
return Coin{}, fmt.Errorf("invalid coin expression: %s", coinStr)
}
denomStr, amountStr := matches[2], matches[1]
amount, ok := NewIntFromString(amountStr)
if !ok {
return Coin{}, fmt.Errorf("failed to parse coin amount: %s", amountStr)
}
if err := validateDenom(denomStr); err != nil {
return Coin{}, fmt.Errorf("invalid denom cannot contain upper case characters or spaces: %s", err)
}
return NewCoin(denomStr, amount), nil
}
// ParseCoins will parse out a list of coins separated by commas.
// If nothing is provided, it returns nil Coins.
// Returned coins are sorted.
func ParseCoins(coinsStr string) (coins Coins, err error) {
coinsStr = strings.TrimSpace(coinsStr)
if len(coinsStr) == 0 {
return nil, nil
}
coinStrs := strings.Split(coinsStr, ",")
for _, coinStr := range coinStrs {
coin, err := ParseCoin(coinStr)
if err != nil {
return nil, err
}
coins = append(coins, coin)
}
// Sort coins for determinism.
coins.Sort()
// Validate coins before returning.
if !coins.IsValid() {
return nil, fmt.Errorf("parseCoins invalid: %#v", coins)
}
return coins, nil
}