+151
@@ -0,0 +1,151 @@
|
||||
package currency
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
iso4217 "github.com/ladydascalie/currency"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/market"
|
||||
"github.com/status-im/status-go/services/wallet/token"
|
||||
)
|
||||
|
||||
const decimalsCalculationCurrency = "USD"
|
||||
|
||||
const lowerTokenResolutionInUsd = 0.1
|
||||
const higherTokenResolutionInUsd = 0.01
|
||||
|
||||
type Format struct {
|
||||
Symbol string `json:"symbol"`
|
||||
DisplayDecimals uint `json:"displayDecimals"`
|
||||
StripTrailingZeroes bool `json:"stripTrailingZeroes"`
|
||||
}
|
||||
|
||||
type FormatPerSymbol = map[string]Format
|
||||
|
||||
type Currency struct {
|
||||
marketManager *market.Manager
|
||||
}
|
||||
|
||||
func NewCurrency(marketManager *market.Manager) *Currency {
|
||||
return &Currency{
|
||||
marketManager: marketManager,
|
||||
}
|
||||
}
|
||||
|
||||
func IsCurrencyFiat(symbol string) bool {
|
||||
return iso4217.Valid(strings.ToUpper(symbol))
|
||||
}
|
||||
|
||||
func GetAllFiatCurrencySymbols() []string {
|
||||
return iso4217.ValidCodes
|
||||
}
|
||||
|
||||
func calculateFiatDisplayDecimals(symbol string) (uint, error) {
|
||||
currency, err := iso4217.Get(strings.ToUpper(symbol))
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uint(currency.MinorUnits()), nil
|
||||
}
|
||||
|
||||
func calculateFiatCurrencyFormat(symbol string) (*Format, error) {
|
||||
displayDecimals, err := calculateFiatDisplayDecimals(symbol)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
format := &Format{
|
||||
Symbol: symbol,
|
||||
DisplayDecimals: displayDecimals,
|
||||
StripTrailingZeroes: false,
|
||||
}
|
||||
|
||||
return format, nil
|
||||
}
|
||||
|
||||
func calculateTokenDisplayDecimals(price float64) uint {
|
||||
var displayDecimals float64 = 0.0
|
||||
|
||||
if price > 0 {
|
||||
lowerDecimalsBound := math.Max(0.0, math.Log10(price)-math.Log10(lowerTokenResolutionInUsd))
|
||||
upperDecimalsBound := math.Max(0.0, math.Log10(price)-math.Log10(higherTokenResolutionInUsd))
|
||||
|
||||
// Use as few decimals as needed to ensure lower precision
|
||||
displayDecimals = math.Ceil(lowerDecimalsBound)
|
||||
if displayDecimals+1.0 <= upperDecimalsBound {
|
||||
// If allowed by upper bound, ensure resolution changes as soon as currency hits multiple of 10
|
||||
displayDecimals += 1.0
|
||||
}
|
||||
}
|
||||
|
||||
return uint(displayDecimals)
|
||||
}
|
||||
|
||||
func (cm *Currency) calculateTokenCurrencyFormat(symbol string, price float64) (*Format, error) {
|
||||
pegSymbol := token.GetTokenPegSymbol(symbol)
|
||||
|
||||
if pegSymbol != "" {
|
||||
var currencyFormat, err = calculateFiatCurrencyFormat(pegSymbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currencyFormat.Symbol = symbol
|
||||
return currencyFormat, nil
|
||||
}
|
||||
|
||||
currencyFormat := &Format{
|
||||
Symbol: symbol,
|
||||
DisplayDecimals: calculateTokenDisplayDecimals(price),
|
||||
StripTrailingZeroes: true,
|
||||
}
|
||||
return currencyFormat, nil
|
||||
}
|
||||
|
||||
func GetFiatCurrencyFormats(symbols []string) (FormatPerSymbol, error) {
|
||||
formats := make(FormatPerSymbol)
|
||||
|
||||
for _, symbol := range symbols {
|
||||
format, err := calculateFiatCurrencyFormat(symbol)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formats[symbol] = *format
|
||||
}
|
||||
|
||||
return formats, nil
|
||||
}
|
||||
|
||||
func (cm *Currency) FetchTokenCurrencyFormats(symbols []string) (FormatPerSymbol, error) {
|
||||
formats := make(FormatPerSymbol)
|
||||
|
||||
// Get latest cached price, fetch only if not available
|
||||
prices, err := cm.marketManager.GetOrFetchPrices(symbols, []string{decimalsCalculationCurrency}, math.MaxInt64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, symbol := range symbols {
|
||||
priceData, ok := prices[symbol][decimalsCalculationCurrency]
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("Could not get price for: " + symbol)
|
||||
}
|
||||
|
||||
format, err := cm.calculateTokenCurrencyFormat(symbol, priceData.Price)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formats[symbol] = *format
|
||||
}
|
||||
|
||||
return formats, nil
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package currency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCurrencyDB(sqlDb *sql.DB) *DB {
|
||||
return &DB{
|
||||
db: sqlDb,
|
||||
}
|
||||
}
|
||||
|
||||
func getCachedFormatsFromDBRows(rows *sql.Rows) (FormatPerSymbol, error) {
|
||||
formats := make(FormatPerSymbol)
|
||||
|
||||
for rows.Next() {
|
||||
var format Format
|
||||
if err := rows.Scan(&format.Symbol, &format.DisplayDecimals, &format.StripTrailingZeroes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formats[format.Symbol] = format
|
||||
}
|
||||
|
||||
return formats, nil
|
||||
}
|
||||
|
||||
func (cdb *DB) GetCachedFormats() (FormatPerSymbol, error) {
|
||||
rows, err := cdb.db.Query("SELECT symbol, display_decimals, strip_trailing_zeroes FROM currency_format_cache")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return getCachedFormatsFromDBRows(rows)
|
||||
}
|
||||
|
||||
func (cdb *DB) UpdateCachedFormats(formats FormatPerSymbol) error {
|
||||
tx, err := cdb.db.BeginTx(context.Background(), &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
return
|
||||
}
|
||||
// don't shadow original error
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
insert, err := tx.Prepare(`INSERT OR REPLACE INTO currency_format_cache
|
||||
(symbol, display_decimals, strip_trailing_zeroes)
|
||||
VALUES
|
||||
(?, ?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
_, err = insert.Exec(format.Symbol, format.DisplayDecimals, format.StripTrailingZeroes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package currency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/status-im/status-go/services/wallet/market"
|
||||
"github.com/status-im/status-go/services/wallet/token"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
const (
|
||||
EventCurrencyTickUpdateFormat walletevent.EventType = "wallet-currency-tick-update-format"
|
||||
|
||||
currencyFormatUpdateInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
currency *Currency
|
||||
db *DB
|
||||
|
||||
tokenManager *token.Manager
|
||||
walletFeed *event.Feed
|
||||
cancelFn context.CancelFunc
|
||||
}
|
||||
|
||||
func NewService(db *sql.DB, walletFeed *event.Feed, tokenManager *token.Manager, marketManager *market.Manager) *Service {
|
||||
return &Service{
|
||||
currency: NewCurrency(marketManager),
|
||||
db: NewCurrencyDB(db),
|
||||
tokenManager: tokenManager,
|
||||
walletFeed: walletFeed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Start() {
|
||||
// Update all fiat currency formats in cache
|
||||
fiatFormats, err := s.getAllFiatCurrencyFormats()
|
||||
|
||||
if err == nil {
|
||||
_ = s.db.UpdateCachedFormats(fiatFormats)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.cancelFn = cancel
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(currencyFormatUpdateInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.walletFeed.Send(walletevent.Event{
|
||||
Type: EventCurrencyTickUpdateFormat,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
if s.cancelFn != nil {
|
||||
s.cancelFn()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetCachedCurrencyFormats() (FormatPerSymbol, error) {
|
||||
return s.db.GetCachedFormats()
|
||||
}
|
||||
|
||||
func (s *Service) FetchAllCurrencyFormats() (FormatPerSymbol, error) {
|
||||
// Only token prices can change, so we fetch those
|
||||
tokenFormats, err := s.fetchAllTokenCurrencyFormats()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.db.UpdateCachedFormats(tokenFormats)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetCachedCurrencyFormats()
|
||||
}
|
||||
|
||||
func (s *Service) getAllFiatCurrencyFormats() (FormatPerSymbol, error) {
|
||||
return GetFiatCurrencyFormats(GetAllFiatCurrencySymbols())
|
||||
}
|
||||
|
||||
func (s *Service) fetchAllTokenCurrencyFormats() (FormatPerSymbol, error) {
|
||||
tokens, err := s.tokenManager.GetAllTokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenPerSymbolMap := make(map[string]bool)
|
||||
tokenSymbols := make([]string, 0)
|
||||
for _, t := range tokens {
|
||||
symbol := t.Symbol
|
||||
if !tokenPerSymbolMap[symbol] {
|
||||
tokenPerSymbolMap[symbol] = true
|
||||
tokenSymbols = append(tokenSymbols, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
tokenFormats, err := s.currency.FetchTokenCurrencyFormats(tokenSymbols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gweiSymbol := "Gwei"
|
||||
tokenFormats[gweiSymbol] = Format{
|
||||
Symbol: gweiSymbol,
|
||||
DisplayDecimals: 9,
|
||||
StripTrailingZeroes: true,
|
||||
}
|
||||
return tokenFormats, err
|
||||
}
|
||||
Reference in New Issue
Block a user