-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathquotes.go
241 lines (192 loc) · 7.85 KB
/
quotes.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
package quotes
import (
"errors"
"fmt"
"net/http"
"github.com/imroc/req/v3"
"github.com/shopspring/decimal"
"github.com/spf13/afero"
"github.com/surahman/FTeX/pkg/constants"
"github.com/surahman/FTeX/pkg/logger"
"github.com/surahman/FTeX/pkg/models"
"go.uber.org/zap"
)
// Mock Quotes interface stub generation. This is local to the Quotes package.
//go:generate mockgen -destination=quotes_mocks.go -package=quotes github.com/surahman/FTeX/pkg/quotes Quotes
// Quotes is the interface through which the currency quote services can be accessed. Created to support mock testing.
type Quotes interface {
// fiatQuote will retrieve a quote for a Fiat currency price.
fiatQuote(source, destination string, sourceAmount decimal.Decimal) (models.FiatQuote, error)
// cryptoQuote will retrieve a quote for a Cryptocurrency price.
cryptoQuote(source, destination string) (models.CryptoQuote, error)
// FiatConversion will convert a source currency, in a given amount, to the destination currency.
FiatConversion(source, destination string, amount decimal.Decimal,
fiatQuote func(source, destination string, amount decimal.Decimal) (models.FiatQuote, error)) (
decimal.Decimal, decimal.Decimal, error)
// CryptoConversion will convert Fiat to Crypto and Crypto to Fiat currencies, for a given amount.
CryptoConversion(fiatSymbol, cryptoSymbol string, amount decimal.Decimal, isPurchasingCrypto bool,
cryptoQuote func(source, destination string) (models.CryptoQuote, error)) (
decimal.Decimal, decimal.Decimal, error)
}
// Check to ensure the Redis interface has been implemented.
var _ Quotes = "esImpl{}
// quoteImpl implements the Quote interface and contains the logic to interface with currency price services.
type quotesImpl struct {
clientCrypto *req.Client
clientFiat *req.Client
conf *config
logger *logger.Logger
}
// NewQuote will create a new Quote configuration by loading it.
func NewQuote(fs *afero.Fs, logger *logger.Logger) (Quotes, error) {
if fs == nil || logger == nil {
return nil, errors.New("nil file system or logger supplied")
}
return newQuotesImpl(fs, logger)
}
// newQuoteImpl will create a new quoteImpl configuration and load it from disk.
func newQuotesImpl(fs *afero.Fs, logger *logger.Logger) (q *quotesImpl, err error) {
q = "esImpl{conf: newConfig(), logger: logger}
if err = q.conf.Load(*fs); err != nil {
q.logger.Error("failed to load Quote configurations from disk", zap.Error(err))
return nil, err
}
// Fiat Client configuration.
q.clientFiat, err = configFiatClient(q.conf)
if err != nil {
q.logger.Error("failed to configure Fiat client", zap.Error(err))
return nil, err
}
// Crypto Client configuration.
q.clientCrypto, err = configCryptoClient(q.conf)
if err != nil {
q.logger.Error("failed to configure Crypto client", zap.Error(err))
return nil, err
}
return
}
// configFiatClient will setup the global configurations for the Fiat client.
func configFiatClient(conf *config) (*req.Client, error) {
if conf == nil {
return nil, errors.New("configurations not loaded")
}
return req.C().
SetUserAgent(conf.Connection.UserAgent).
SetTimeout(conf.Connection.Timeout).
SetCommonHeader(conf.FiatCurrency.HeaderKey, conf.FiatCurrency.APIKey),
nil
}
// configCryptoClient will setup the global configurations for the Crypto client.
func configCryptoClient(conf *config) (*req.Client, error) {
if conf == nil {
return nil, errors.New("configurations not loaded")
}
return req.C().
SetUserAgent(conf.Connection.UserAgent).
SetTimeout(conf.Connection.Timeout).
SetCommonHeader(conf.CryptoCurrency.HeaderKey, conf.CryptoCurrency.APIKey),
nil
}
// FiatQuote will access the Fiat currency price quote service and get the latest exchange rate.
func (q *quotesImpl) fiatQuote(source, destination string, sourceAmount decimal.Decimal) (models.FiatQuote, error) {
result := models.FiatQuote{}
_, err := q.clientFiat.R().
SetQueryParam("from", source).
SetQueryParam("to", destination).
SetQueryParam("amount", sourceAmount.String()).
SetSuccessResult(&result).
Get(q.conf.FiatCurrency.Endpoint)
// Failed to query endpoint for price.
if err != nil {
q.logger.Warn("failed to get Fiat currency price quote", zap.Error(err))
return result, NewError(constants.RetryMessageString()).SetStatus(http.StatusServiceUnavailable)
}
// Check for a successful rate retrieval.
if !result.Success {
return result, NewError("invalid Fiat currency code").SetStatus(http.StatusBadRequest)
}
return result, nil
}
// FiatConversion will convert a source currency, of a given amount, to the destination currency.
func (q *quotesImpl) FiatConversion(
source,
destination string,
amount decimal.Decimal,
fiatQuote func(source, destination string, amount decimal.Decimal) (models.FiatQuote, error)) (
decimal.Decimal, decimal.Decimal, error) {
var (
err error
rawQuote models.FiatQuote
)
// The fiatQuote parameter is exposed for stub injection used for testing.
if fiatQuote == nil {
fiatQuote = q.fiatQuote
}
rawQuote, err = fiatQuote(source, destination, amount)
if err != nil {
q.logger.Warn("failed to convert Fiat currency", zap.Error(err))
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w", err)
}
// For precision-related concerns, the amount to be posted will be recalculated here.
// We only rely on the quote provider for rate quote's precision and not the amount converted precision.
convertedAmount := rawQuote.Info.Rate.
Mul(amount).
RoundBank(constants.DecimalPlacesFiat())
return rawQuote.Info.Rate, convertedAmount, nil
}
// CryptoQuote will access the Fiat currency price quote service and get the latest exchange rate.
func (q *quotesImpl) cryptoQuote(source, destination string) (models.CryptoQuote, error) {
result := models.CryptoQuote{}
resp, err := q.clientCrypto.R().
SetPathParam("base_symbol", source).
SetPathParam("quote_symbol", destination).
SetSuccessResult(&result).
Get(q.conf.CryptoCurrency.Endpoint)
// Failed to query endpoint for price.
if err != nil {
q.logger.Warn("failed to get Fiat currency price quote", zap.Error(err))
return result, NewError("crypto price service unreachable").SetStatus(http.StatusInternalServerError)
}
if !resp.IsSuccessState() {
// Invalid cryptocurrency codes.
if resp.StatusCode == 550 { //nolint:mnd,gomnd
return result, NewError("invalid Crypto currency code").SetStatus(http.StatusBadRequest)
}
// Log and other API related errors and return an internal server error to user.
q.logger.Error("API error", zap.String("Response", resp.String()))
return result, NewError(constants.RetryMessageString()).SetStatus(http.StatusInternalServerError)
}
return result, nil
}
// CryptoConversion will convert Fiat to Crypto and Crypto to Fiat currencies, for a given amount.
func (q *quotesImpl) CryptoConversion(
sourceCurrency,
destinationCurrency string,
sourceAmount decimal.Decimal,
isPurchasingCrypto bool,
cryptoQuote func(source, destination string) (models.CryptoQuote, error)) (
decimal.Decimal, decimal.Decimal, error) {
var (
precision = constants.DecimalPlacesCrypto()
err error
rawQuote models.CryptoQuote
)
// The fiatQuote parameter is exposed for stub injection used for testing.
if cryptoQuote == nil {
cryptoQuote = q.cryptoQuote
}
if !isPurchasingCrypto {
precision = constants.DecimalPlacesFiat()
}
rawQuote, err = cryptoQuote(sourceCurrency, destinationCurrency)
if err != nil {
q.logger.Warn("failed to retrieve Fiat to Cryptocurrency exchange quote", zap.Error(err))
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w", err)
}
// For precision-related concerns, the amount to be posted will be recalculated here.
// We only rely on the quote provider for rate quote's precision and not the amount converted precision.
convertedAmount := rawQuote.Rate.
Mul(sourceAmount).
RoundBank(precision)
return rawQuote.Rate, convertedAmount, nil
}