-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathvalidation.go
110 lines (95 loc) · 3.95 KB
/
validation.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
package exchangerate
import (
"context"
"fmt"
"math"
"strings"
"time"
"github.com/pkg/errors"
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
currency_lib "github.com/code-payments/code-server/pkg/currency"
"github.com/code-payments/code-server/pkg/database/query"
"github.com/code-payments/code-server/pkg/kin"
code_data "github.com/code-payments/code-server/pkg/code/data"
"github.com/code-payments/code-server/pkg/code/data/currency"
)
// todo: add tests, but generally well tested in server tests since that's where most of this originated
// GetPotentialClientExchangeRates gets a set of exchange rates that a client
// attempting to maintain a latest state could have fetched from the currency
// server.
func GetPotentialClientExchangeRates(ctx context.Context, data code_data.Provider, code currency_lib.Code) ([]*currency.ExchangeRateRecord, error) {
exchangeRecords, err := data.GetExchangeRateHistory(
ctx,
code,
query.WithStartTime(time.Now().Add(-30*time.Minute)), // Give enough leeway to allow for 15 minute old rates
query.WithEndTime(time.Now().Add(time.Minute)), // Ensure we pick up recent exchange rates
query.WithLimit(32), // Try to pick up all records
query.WithDirection(query.Descending), // Optimize for most recent records first
)
if err != nil && err != currency.ErrNotFound {
return nil, err
}
// To handle cases where the exchange rate worker might be down, try
// loading the latest rate as clients would have and add it to valid
// set of comparable records.
latestExchangeRecord, err := data.GetExchangeRate(
ctx,
code,
GetLatestExchangeRateTime(),
)
if err != nil && err != currency.ErrNotFound {
return nil, err
}
if latestExchangeRecord != nil {
exchangeRecords = append(exchangeRecords, latestExchangeRecord)
}
// This is bad, and means we can't query for any records
if len(exchangeRecords) == 0 {
return nil, errors.Errorf("found no exchange records for %s currency", code)
}
return exchangeRecords, nil
}
// ValidateClientExchangeData validates proto exchange data provided by a client
func ValidateClientExchangeData(ctx context.Context, data code_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) {
currencyCode := strings.ToLower(proto.Currency)
switch currencyCode {
case string(currency_lib.KIN):
if proto.ExchangeRate != 1.0 {
return false, "kin exchange rate must be 1", nil
}
default:
// Validate the exchange rate with what Code would have returned
exchangeRecords, err := GetPotentialClientExchangeRates(ctx, data, currency_lib.Code(currencyCode))
if err != nil {
return false, "", err
}
// Alternatively, we could find the highest and lowest value and ensure
// the requested rate falls in that range. However, this method allows
// us to ensure clients are getting their data from code-server.
var foundExchangeRate bool
for _, exchangeRecord := range exchangeRecords {
// Avoid issues with floating points by examining the percentage
// difference
percentDiff := math.Abs(exchangeRecord.Rate-proto.ExchangeRate) / exchangeRecord.Rate
if percentDiff < 0.001 {
foundExchangeRate = true
break
}
}
if !foundExchangeRate {
return false, "fiat exchange rate is stale", nil
}
}
// Validate that the native amount and exchange rate fall reasonably within
// the amount of quarks to send in the transaction. This must consider any
// truncation at the client due to minimum bucket sizes.
//
// todo: This uses string conversions, which is less than ideal, but the only
// thing available at the time of writing this for conversion.
smallestSendAmount := float64(kin.ToQuarks(1))
quarksFromCurrency, _ := kin.StrToQuarks(fmt.Sprintf("%.5f", proto.NativeAmount/proto.ExchangeRate))
if math.Abs(float64(quarksFromCurrency-int64(proto.Quarks))) > smallestSendAmount+100 {
return false, "payment native amount and quark value mismatch", nil
}
return true, "", nil
}