satellite/payments: specialized type for monetary amounts
Why: big.Float is not an ideal type for dealing with monetary amounts, because no matter how high the precision, some non-integer decimal values can not be represented exactly in base-2 floating point. Also, storing gob-encoded big.Float values in the database makes it very hard to use those values in meaningful queries, making it difficult to do any sort of analysis on billing. For better accuracy, then, we can just represent monetary values as integers (in whatever base units are appropriate for the currency). For example, STORJ tokens or Bitcoins can not be split into pieces smaller than 10^-8, so we can store amounts of STORJ or BTC with precision simply by moving the decimal point 8 digits to the right. For USD values (assuming we don't want to deal with fractional cents), we can move the decimal point 2 digits to the right. To make it easier and less error-prone to deal with the math involved, I introduce here a new type, monetary.Amount, instances of which have an associated value _and_ a currency. Change-Id: I03395d52f0e2473cf301361f6033722b54640265
This commit is contained in:
parent
c911360eb5
commit
a16aecfa96
@ -297,8 +297,8 @@ func (p *Payments) TokenDeposit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
responseData.Address = tx.Address
|
||||
responseData.Amount = float64(requestData.Amount) / 100
|
||||
responseData.TokenAmount = tx.Amount.String()
|
||||
responseData.Rate = tx.Rate.Text('f', 8)
|
||||
responseData.TokenAmount = tx.Amount.AsDecimal().String()
|
||||
responseData.Rate = tx.Rate.StringFixed(8)
|
||||
responseData.Status = tx.Status.String()
|
||||
responseData.Link = tx.Link
|
||||
responseData.ExpiresAt = tx.CreatedAt.Add(tx.Timeout)
|
||||
|
@ -1,18 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package coinpayments
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Precision is precision amount used to parse currency amount.
|
||||
// Set enough precision to to able to handle up to 8 digits after point.
|
||||
const Precision = 32
|
||||
|
||||
// parseAmount parses amount string into big.Float with package wide defined precision.
|
||||
func parseAmount(s string) (*big.Float, error) {
|
||||
amount, _, err := big.ParseFloat(s, 10, Precision, big.ToNearestEven)
|
||||
return amount, err
|
||||
}
|
@ -6,10 +6,13 @@ package coinpayments
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
// cmdRates is API command for retrieving currency rate infos.
|
||||
@ -28,8 +31,8 @@ const (
|
||||
// CurrencyRateInfo holds currency conversion info.
|
||||
type CurrencyRateInfo struct {
|
||||
IsFiat bool
|
||||
RateBTC big.Float
|
||||
TXFee big.Float
|
||||
RateBTC decimal.Decimal
|
||||
TXFee decimal.Decimal
|
||||
Status ExchangeStatus
|
||||
LastUpdate time.Time
|
||||
}
|
||||
@ -48,16 +51,11 @@ func (rateInfo *CurrencyRateInfo) UnmarshalJSON(b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
parseBigFloat := func(s string) (*big.Float, error) {
|
||||
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
|
||||
return f, err
|
||||
}
|
||||
|
||||
rateBTC, err := parseBigFloat(rateRaw.RateBTC)
|
||||
rateBTC, err := decimal.NewFromString(rateRaw.RateBTC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txFee, err := parseBigFloat(rateRaw.TXFee)
|
||||
txFee, err := decimal.NewFromString(rateRaw.TXFee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -69,8 +67,8 @@ func (rateInfo *CurrencyRateInfo) UnmarshalJSON(b []byte) error {
|
||||
|
||||
*rateInfo = CurrencyRateInfo{
|
||||
IsFiat: rateRaw.IsFiat > 0,
|
||||
RateBTC: *rateBTC,
|
||||
TXFee: *txFee,
|
||||
RateBTC: rateBTC,
|
||||
TXFee: txFee,
|
||||
Status: ExchangeStatus(rateRaw.Status),
|
||||
LastUpdate: time.Unix(lastUpdate, 0),
|
||||
}
|
||||
@ -79,7 +77,7 @@ func (rateInfo *CurrencyRateInfo) UnmarshalJSON(b []byte) error {
|
||||
}
|
||||
|
||||
// CurrencyRateInfos maps currency to currency rate info.
|
||||
type CurrencyRateInfos map[Currency]CurrencyRateInfo
|
||||
type CurrencyRateInfos map[*monetary.Currency]CurrencyRateInfo
|
||||
|
||||
// ConversionRates collection of API methods for retrieving currency
|
||||
// conversion rates.
|
||||
|
@ -3,19 +3,26 @@
|
||||
|
||||
package coinpayments
|
||||
|
||||
// Currency is a type wrapper for defined currencies.
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
// CurrencyUSD defines USD.
|
||||
CurrencyUSD Currency = "USD"
|
||||
// CurrencyLTCT defines LTCT, coins used for testing purpose.
|
||||
CurrencyLTCT Currency = "LTCT"
|
||||
// CurrencySTORJ defines STORJ tokens.
|
||||
CurrencySTORJ Currency = "STORJ"
|
||||
import (
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
// String returns Currency string representation.
|
||||
func (c Currency) String() string {
|
||||
return string(c)
|
||||
}
|
||||
// CurrencySymbol is a symbol for a currency as recognized by coinpayments.net.
|
||||
type CurrencySymbol string
|
||||
|
||||
var (
|
||||
// CurrencyLTCT defines LTCT, coins used for testing purpose.
|
||||
CurrencyLTCT = monetary.NewCurrency("LTCT test coins", "LTCT", 8)
|
||||
|
||||
// currencySymbols maps known currency objects to the currency symbols
|
||||
// as recognized on coinpayments.net. In many cases, the currency's own
|
||||
// idea of its symbol (currency.Symbol()) will be the same as this
|
||||
// CurrencySymbol, but we should probably not count on that always being
|
||||
// the case.
|
||||
currencySymbols = map[*monetary.Currency]CurrencySymbol{
|
||||
monetary.USDollars: "USD",
|
||||
monetary.StorjToken: "STORJ",
|
||||
monetary.Bitcoin: "BTC",
|
||||
CurrencyLTCT: "LTCT",
|
||||
}
|
||||
)
|
||||
|
@ -6,13 +6,15 @@ package coinpayments
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -90,7 +92,7 @@ func (list TransactionIDList) Encode() string {
|
||||
type Transaction struct {
|
||||
ID TransactionID
|
||||
Address string
|
||||
Amount big.Float
|
||||
Amount monetary.Amount
|
||||
DestTag string
|
||||
ConfirmsNeeded int
|
||||
Timeout time.Duration
|
||||
@ -117,7 +119,7 @@ func (tx *Transaction) UnmarshalJSON(b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := parseAmount(txRaw.Amount)
|
||||
amount, err := monetary.AmountFromString(txRaw.Amount, monetary.StorjToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -130,7 +132,7 @@ func (tx *Transaction) UnmarshalJSON(b []byte) error {
|
||||
*tx = Transaction{
|
||||
ID: TransactionID(txRaw.TxID),
|
||||
Address: txRaw.Address,
|
||||
Amount: *amount,
|
||||
Amount: amount,
|
||||
DestTag: txRaw.DestTag,
|
||||
ConfirmsNeeded: int(confirms),
|
||||
Timeout: time.Second * time.Duration(txRaw.Timeout),
|
||||
@ -145,9 +147,9 @@ func (tx *Transaction) UnmarshalJSON(b []byte) error {
|
||||
// TransactionInfo holds transaction information.
|
||||
type TransactionInfo struct {
|
||||
Address string
|
||||
Coin Currency
|
||||
Amount big.Float
|
||||
Received big.Float
|
||||
Coin CurrencySymbol
|
||||
Amount decimal.Decimal
|
||||
Received decimal.Decimal
|
||||
ConfirmsReceived int
|
||||
Status Status
|
||||
ExpiresAt time.Time
|
||||
@ -157,34 +159,25 @@ type TransactionInfo struct {
|
||||
// UnmarshalJSON handles json unmarshaling for transaction info.
|
||||
func (info *TransactionInfo) UnmarshalJSON(b []byte) error {
|
||||
var txInfoRaw struct {
|
||||
Address string `json:"payment_address"`
|
||||
Coin string `json:"coin"`
|
||||
Status int `json:"status"`
|
||||
AmountF string `json:"amountf"`
|
||||
ReceivedF string `json:"receivedf"`
|
||||
ConfirmsRecv int `json:"recv_confirms"`
|
||||
ExpiresAt int64 `json:"time_expires"`
|
||||
CreatedAt int64 `json:"time_created"`
|
||||
Address string `json:"payment_address"`
|
||||
Coin string `json:"coin"`
|
||||
Status int `json:"status"`
|
||||
Amount decimal.Decimal `json:"amountf"`
|
||||
Received decimal.Decimal `json:"receivedf"`
|
||||
ConfirmsRecv int `json:"recv_confirms"`
|
||||
ExpiresAt int64 `json:"time_expires"`
|
||||
CreatedAt int64 `json:"time_created"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &txInfoRaw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := parseAmount(txInfoRaw.AmountF)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
received, err := parseAmount(txInfoRaw.ReceivedF)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*info = TransactionInfo{
|
||||
Address: txInfoRaw.Address,
|
||||
Coin: Currency(txInfoRaw.Coin),
|
||||
Amount: *amount,
|
||||
Received: *received,
|
||||
Coin: CurrencySymbol(txInfoRaw.Coin),
|
||||
Amount: txInfoRaw.Amount,
|
||||
Received: txInfoRaw.Received,
|
||||
ConfirmsReceived: txInfoRaw.ConfirmsRecv,
|
||||
Status: Status(txInfoRaw.Status),
|
||||
ExpiresAt: time.Unix(txInfoRaw.ExpiresAt, 0),
|
||||
@ -233,9 +226,9 @@ func (infos *TransactionInfos) UnmarshalJSON(b []byte) error {
|
||||
|
||||
// CreateTX defines parameters for transaction creating.
|
||||
type CreateTX struct {
|
||||
Amount big.Float
|
||||
CurrencyIn Currency
|
||||
CurrencyOut Currency
|
||||
Amount decimal.Decimal
|
||||
CurrencyIn *monetary.Currency
|
||||
CurrencyOut *monetary.Currency
|
||||
BuyerEmail string
|
||||
}
|
||||
|
||||
@ -246,10 +239,18 @@ type Transactions struct {
|
||||
|
||||
// Create creates new transaction.
|
||||
func (t Transactions) Create(ctx context.Context, params *CreateTX) (*Transaction, error) {
|
||||
cpSymbolIn, ok := currencySymbols[params.CurrencyIn]
|
||||
if !ok {
|
||||
return nil, Error.New("can't identify coinpayments currency symbol for %q", params.CurrencyIn.Name())
|
||||
}
|
||||
cpSymbolOut, ok := currencySymbols[params.CurrencyOut]
|
||||
if !ok {
|
||||
return nil, Error.New("can't identify coinpayments currency symbol for %q", params.CurrencyOut.Name())
|
||||
}
|
||||
values := make(url.Values)
|
||||
values.Set("amount", params.Amount.Text('f', -1))
|
||||
values.Set("currency1", params.CurrencyIn.String())
|
||||
values.Set("currency2", params.CurrencyOut.String())
|
||||
values.Set("amount", params.Amount.String())
|
||||
values.Set("currency1", string(cpSymbolIn))
|
||||
values.Set("currency2", string(cpSymbolOut))
|
||||
values.Set("buyer_email", params.BuyerEmail)
|
||||
|
||||
tx := new(Transaction)
|
||||
|
@ -4,14 +4,15 @@
|
||||
package coinpayments_test
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
)
|
||||
|
||||
@ -36,9 +37,9 @@ func TestListInfos(t *testing.T) {
|
||||
for x := 0; x < 27; x++ {
|
||||
tx, err := payments.Create(ctx,
|
||||
&coinpayments.CreateTX{
|
||||
Amount: *big.NewFloat(100),
|
||||
CurrencyIn: coinpayments.CurrencySTORJ,
|
||||
CurrencyOut: coinpayments.CurrencySTORJ,
|
||||
Amount: decimal.NewFromInt(100),
|
||||
CurrencyIn: monetary.StorjToken,
|
||||
CurrencyOut: monetary.StorjToken,
|
||||
BuyerEmail: "test@test.com",
|
||||
},
|
||||
)
|
||||
|
@ -12,10 +12,10 @@ import (
|
||||
// ErrNoAuthorizationKey is error that indicates that there is no authorization key.
|
||||
var ErrNoAuthorizationKey = Error.New("no authorization key")
|
||||
|
||||
// GetTransacationKeyFromURL parses provided raw url string
|
||||
// GetTransactionKeyFromURL parses provided raw url string
|
||||
// and extracts authorization key from it. Returns ErrNoAuthorizationKey if
|
||||
// there is no authorization key and error if rawurl cannot be parsed.
|
||||
func GetTransacationKeyFromURL(rawurl string) (string, error) {
|
||||
func GetTransactionKeyFromURL(rawurl string) (string, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return "", errs.Wrap(err)
|
||||
|
@ -17,7 +17,7 @@ func TestGetCheckoutURL(t *testing.T) {
|
||||
|
||||
url := coinpayments.GetCheckoutURL(expected, "id")
|
||||
|
||||
key, err := coinpayments.GetTransacationKeyFromURL(url)
|
||||
key, err := coinpayments.GetTransactionKeyFromURL(url)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, key)
|
||||
|
172
satellite/payments/monetary/amount.go
Normal file
172
satellite/payments/monetary/amount.go
Normal file
@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package monetary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeebo/errs"
|
||||
)
|
||||
|
||||
// Currency represents a currency for the purpose of representing amounts in
|
||||
// that currency. Currency instances have a name, a symbol, and a number of
|
||||
// supported decimal places of supported precision.
|
||||
type Currency struct {
|
||||
name string
|
||||
symbol string
|
||||
decimalPlaces int32
|
||||
}
|
||||
|
||||
// NewCurrency creates a new Currency instance.
|
||||
func NewCurrency(name, symbol string, decimalPlaces int32) *Currency {
|
||||
return &Currency{name: name, symbol: symbol, decimalPlaces: decimalPlaces}
|
||||
}
|
||||
|
||||
// Name returns the name of the currency.
|
||||
func (c *Currency) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Symbol returns the symbol of the currency.
|
||||
func (c *Currency) Symbol() string {
|
||||
return c.symbol
|
||||
}
|
||||
|
||||
var (
|
||||
// StorjToken is the currency for the STORJ ERC20 token, which powers
|
||||
// most payments on the current Storj network.
|
||||
StorjToken = NewCurrency("STORJ Token", "STORJ", 8)
|
||||
// USDollars is the currency of United States dollars, where fractional
|
||||
// cents are not supported.
|
||||
USDollars = NewCurrency("US dollars", "USD", 2)
|
||||
// Bitcoin is the currency for the well-known cryptocurrency Bitcoin
|
||||
// (a.k.a. BTC).
|
||||
Bitcoin = NewCurrency("Bitcoin (BTC)", "BTC", 8)
|
||||
// LiveGoats is the currency of live goats, which some Storj network
|
||||
// satellites may elect to support for payments.
|
||||
LiveGoats = NewCurrency("Live goats", "goats", 0)
|
||||
|
||||
// Error is a class of errors encountered in the monetary package.
|
||||
Error = errs.Class("monetary error")
|
||||
)
|
||||
|
||||
// Amount represents a monetary amount, encapsulating a value and a currency.
|
||||
//
|
||||
// The value of the Amount is represented in "base units", meaning units of the
|
||||
// smallest indivisible portion of the given currency. For example, when
|
||||
// the currency is USDollars, the base unit would be cents.
|
||||
type Amount struct {
|
||||
baseUnits int64
|
||||
currency *Currency
|
||||
}
|
||||
|
||||
// AsFloat returns the monetary value in currency units expressed as a
|
||||
// floating point number. _Warning_ may lose precision! (float64 has the
|
||||
// equivalent of 53 bits of precision, as defined by big.Float.)
|
||||
func (a Amount) AsFloat() float64 {
|
||||
return float64(a.baseUnits) * math.Pow10(int(-a.currency.decimalPlaces))
|
||||
}
|
||||
|
||||
// AsBigFloat returns the monetary value in currency units expressed as an
|
||||
// instance of *big.Float with precision=64. _Warning_ may lose precision!
|
||||
func (a Amount) AsBigFloat() *big.Float {
|
||||
return a.AsBigFloatWithPrecision(64)
|
||||
}
|
||||
|
||||
// AsBigFloatWithPrecision returns the monetary value in currency units
|
||||
// expressed as an instance of *big.Float with the given precision.
|
||||
// _Warning_ this may lose precision if the specified precision is not
|
||||
// large enough!
|
||||
func (a Amount) AsBigFloatWithPrecision(p uint) *big.Float {
|
||||
stringVal := a.AsDecimal().String()
|
||||
bf, _, err := big.ParseFloat(stringVal, 10, p, big.ToNearestEven)
|
||||
if err != nil {
|
||||
// it does not appear that this is possible, after a review of
|
||||
// decimal.Decimal{}.String() and big.ParseFloat().
|
||||
panic(fmt.Sprintf("could not parse output of Decimal.String() (%q) as big.Float: %v", stringVal, err))
|
||||
}
|
||||
return bf
|
||||
}
|
||||
|
||||
// AsDecimal returns the monetary value in currency units expressed as an
|
||||
// arbitrary precision decimal number.
|
||||
func (a Amount) AsDecimal() decimal.Decimal {
|
||||
d := decimal.NewFromInt(a.baseUnits)
|
||||
return d.Shift(-a.currency.decimalPlaces)
|
||||
}
|
||||
|
||||
// BaseUnits returns the monetary value expressed in its base units.
|
||||
func (a Amount) BaseUnits() int64 {
|
||||
return a.baseUnits
|
||||
}
|
||||
|
||||
// Currency returns the currency of the amount.
|
||||
func (a Amount) Currency() *Currency {
|
||||
return a.currency
|
||||
}
|
||||
|
||||
// Equal returns true if a and other are in the same currency and have the
|
||||
// same value.
|
||||
func (a Amount) Equal(other Amount) bool {
|
||||
return a.currency == other.currency && a.baseUnits == other.baseUnits
|
||||
}
|
||||
|
||||
// AmountFromBaseUnits creates a new Amount instance from the given count of
|
||||
// base units and in the given currency.
|
||||
func AmountFromBaseUnits(units int64, currency *Currency) Amount {
|
||||
return Amount{
|
||||
baseUnits: units,
|
||||
currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
// AmountFromDecimal creates a new Amount instance from the given decimal
|
||||
// value and in the given currency. The decimal value is expected to be in
|
||||
// currency units.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// AmountFromDecimal(decimal.NewFromFloat(3.50), USDollars) == Amount{baseUnits: 350, currency: USDollars}
|
||||
func AmountFromDecimal(d decimal.Decimal, currency *Currency) Amount {
|
||||
return AmountFromBaseUnits(d.Shift(currency.decimalPlaces).Round(0).IntPart(), currency)
|
||||
}
|
||||
|
||||
// AmountFromString creates a new Amount instance from the given base 10
|
||||
// value and in the given currency. The string is expected to give the
|
||||
// value of the amount in currency units.
|
||||
func AmountFromString(s string, currency *Currency) (Amount, error) {
|
||||
d, err := decimal.NewFromString(s)
|
||||
if err != nil {
|
||||
return Amount{}, Error.Wrap(err)
|
||||
}
|
||||
return AmountFromDecimal(d, currency), nil
|
||||
}
|
||||
|
||||
// AmountFromBigFloat creates a new Amount instance from the given floating
|
||||
// point value and in the given currency. The big.Float is expected to give
|
||||
// the value of the amount in currency units.
|
||||
func AmountFromBigFloat(f *big.Float, currency *Currency) (Amount, error) {
|
||||
dec, err := DecimalFromBigFloat(f)
|
||||
if err != nil {
|
||||
return Amount{}, err
|
||||
}
|
||||
return AmountFromDecimal(dec, currency), nil
|
||||
}
|
||||
|
||||
// DecimalFromBigFloat creates a new decimal.Decimal instance from the given
|
||||
// floating point value.
|
||||
func DecimalFromBigFloat(f *big.Float) (decimal.Decimal, error) {
|
||||
if f.IsInf() {
|
||||
return decimal.Decimal{}, Error.New("Cannot represent infinite amount")
|
||||
}
|
||||
// This is probably not computationally ideal, but it should be the most
|
||||
// straightforward way to convert (unless/until the decimal package adds
|
||||
// a NewFromBigFloat method).
|
||||
stringVal := f.Text('e', -1)
|
||||
dec, err := decimal.NewFromString(stringVal)
|
||||
return dec, Error.Wrap(err)
|
||||
}
|
99
satellite/payments/monetary/amount_test.go
Normal file
99
satellite/payments/monetary/amount_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package monetary
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
// If we use more than 18 decimal places, baseUnits values will overflow int64
|
||||
// even if there is only one digit to the left of the decimal point.
|
||||
manyDigitsCurrency = &Currency{name: "manyDigitsCurrency", symbol: "mdc", decimalPlaces: 18}
|
||||
noDigitsCurrency = &Currency{name: "noDigitsCurrency", symbol: "ndc", decimalPlaces: 0}
|
||||
)
|
||||
|
||||
func TestAmountFromBigFloatAndAmountAsBigFloat(t *testing.T) {
|
||||
parseFloat := func(s string) *big.Float {
|
||||
bf, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse %q as float: %v", s, err)
|
||||
}
|
||||
return bf
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
floatValue *big.Float
|
||||
currency *Currency
|
||||
baseUnits int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero", big.NewFloat(0), StorjToken, 0, false},
|
||||
{"one", big.NewFloat(1), USDollars, 100, false},
|
||||
{"negative", big.NewFloat(-1), Bitcoin, -100000000, false},
|
||||
{"smallest", big.NewFloat(1e-8), StorjToken, 1, false},
|
||||
{"minus smallest", big.NewFloat(-1e-8), StorjToken, -1, false},
|
||||
{"one+delta", parseFloat("1.000000000000000001"), manyDigitsCurrency, 1000000000000000001, false},
|
||||
{"minus one+delta", parseFloat("-1.000000000000000001"), manyDigitsCurrency, -1000000000000000001, false},
|
||||
{"large number", parseFloat("4611686018427387904.0"), noDigitsCurrency, 4611686018427387904, false},
|
||||
{"infinity", parseFloat("Inf"), StorjToken, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := AmountFromBigFloat(tt.floatValue, tt.currency)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AmountFromBigFloat() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
want := Amount{baseUnits: tt.baseUnits, currency: tt.currency}
|
||||
assert.Equal(t, want, got)
|
||||
assert.Equal(t, tt.baseUnits, got.BaseUnits())
|
||||
|
||||
fullPrecValue, err := tt.floatValue.MarshalText()
|
||||
require.NoError(t, err)
|
||||
gotAsFloat := got.AsBigFloatWithPrecision(tt.floatValue.Prec())
|
||||
fullPrecGot, err := gotAsFloat.MarshalText()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Truef(t, tt.floatValue.Cmp(gotAsFloat) == 0,
|
||||
"(expected) %v != (got) %v", string(fullPrecValue), string(fullPrecGot))
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestAmountFromDecimalAndAmountAsDecimal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
decimalValue decimal.Decimal
|
||||
currency *Currency
|
||||
baseUnits int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero", decimal.Decimal{}, StorjToken, 0, false},
|
||||
{"one", decimal.NewFromInt(1), USDollars, 100, false},
|
||||
{"negative", decimal.NewFromInt(-1), Bitcoin, -100000000, false},
|
||||
{"smallest", decimal.NewFromFloat(1e-8), StorjToken, 1, false},
|
||||
{"one+delta", decimal.RequireFromString("1.000000000000000001"), manyDigitsCurrency, 1000000000000000001, false},
|
||||
{"minus one+delta", decimal.RequireFromString("-1.000000000000000001"), manyDigitsCurrency, -1000000000000000001, false},
|
||||
{"large number", decimal.RequireFromString("4611686018427387904.0"), noDigitsCurrency, 4611686018427387904, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := AmountFromDecimal(tt.decimalValue, tt.currency)
|
||||
want := Amount{baseUnits: tt.baseUnits, currency: tt.currency}
|
||||
assert.Equal(t, want, got)
|
||||
assert.Equal(t, tt.baseUnits, got.BaseUnits())
|
||||
assert.Truef(t, tt.decimalValue.Equal(got.AsDecimal()),
|
||||
"%v != %v", tt.decimalValue, got.AsDecimal())
|
||||
})
|
||||
}
|
||||
}
|
@ -5,27 +5,29 @@ package stripecoinpayments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/sync2"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
// convertToCents convert amount to cents with given rate.
|
||||
func convertToCents(rate, amount *big.Float) int64 {
|
||||
f, _ := new(big.Float).Mul(amount, rate).Float64()
|
||||
return int64(math.Round(f * 100))
|
||||
// convertToCents convert amount to USD cents with given rate.
|
||||
func convertToCents(rate decimal.Decimal, amount monetary.Amount) int64 {
|
||||
amountDecimal := amount.AsDecimal()
|
||||
usd := amountDecimal.Mul(rate)
|
||||
usdCents := usd.Shift(2)
|
||||
return usdCents.Round(0).IntPart()
|
||||
}
|
||||
|
||||
// convertFromCents convert amount in cents to big.Float with given rate.
|
||||
func convertFromCents(rate *big.Float, amount int64) *big.Float {
|
||||
a := new(big.Float).SetInt64(amount)
|
||||
a = a.Quo(a, new(big.Float).SetInt64(100))
|
||||
return new(big.Float).Quo(a, rate)
|
||||
// convertFromCents convert amount in cents to a StorjTokenAmount with given rate.
|
||||
func convertFromCents(rate decimal.Decimal, usdCents int64) monetary.Amount {
|
||||
usd := decimal.NewFromInt(usdCents).Shift(-2)
|
||||
numStorj := usd.Div(rate)
|
||||
return monetary.AmountFromDecimal(numStorj, monetary.USDollars)
|
||||
}
|
||||
|
||||
// ErrConversion defines version service error.
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -25,6 +24,7 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -199,7 +199,7 @@ func (service *Service) updateTransactions(ctx context.Context, ids TransactionA
|
||||
TransactionUpdate{
|
||||
TransactionID: id,
|
||||
Status: info.Status,
|
||||
Received: info.Received,
|
||||
Received: monetary.AmountFromDecimal(info.Received, monetary.StorjToken),
|
||||
},
|
||||
)
|
||||
|
||||
@ -275,7 +275,7 @@ func (service *Service) applyTransactionBalance(ctx context.Context, tx Transact
|
||||
return err
|
||||
}
|
||||
|
||||
cents := convertToCents(rate, &tx.Received)
|
||||
cents := convertToCents(rate, tx.Received)
|
||||
|
||||
if cents <= 0 {
|
||||
service.log.Warn("Trying to deposit non-positive amount.",
|
||||
@ -321,7 +321,7 @@ func (service *Service) applyTransactionBalance(ctx context.Context, tx Transact
|
||||
Description: stripe.String(StripeDepositTransactionDescription),
|
||||
}
|
||||
params.AddMetadata("txID", tx.ID.String())
|
||||
params.AddMetadata("storj_amount", tx.Amount.String())
|
||||
params.AddMetadata("storj_amount", tx.Amount.AsDecimal().String())
|
||||
params.AddMetadata("storj_usd_rate", rate.String())
|
||||
_, err = service.stripeClient.CustomerBalanceTransactions().New(params)
|
||||
if err != nil {
|
||||
@ -370,26 +370,26 @@ func (service *Service) UpdateRates(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
// GetRate returns conversion rate for specified currencies.
|
||||
func (service *Service) GetRate(ctx context.Context, curr1, curr2 coinpayments.Currency) (_ *big.Float, err error) {
|
||||
func (service *Service) GetRate(ctx context.Context, curr1, curr2 *monetary.Currency) (_ decimal.Decimal, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
if service.ratesErr != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
return decimal.Decimal{}, Error.Wrap(err)
|
||||
}
|
||||
|
||||
info1, ok := service.rates[curr1]
|
||||
if !ok {
|
||||
return nil, Error.New("no rate for currency %s", curr1)
|
||||
return decimal.Decimal{}, Error.New("no rate for currency %s", curr1.Name())
|
||||
}
|
||||
info2, ok := service.rates[curr2]
|
||||
if !ok {
|
||||
return nil, Error.New("no rate for currency %s", curr2)
|
||||
return decimal.Decimal{}, Error.New("no rate for currency %s", curr2.Name())
|
||||
}
|
||||
|
||||
return new(big.Float).Quo(&info1.RateBTC, &info2.RateBTC), nil
|
||||
return info1.RateBTC.Div(info2.RateBTC), nil
|
||||
}
|
||||
|
||||
// PrepareInvoiceProjectRecords iterates through all projects and creates invoice records if
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -60,18 +61,18 @@ func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
rate, err := tokens.service.GetRate(ctx, coinpayments.CurrencySTORJ, coinpayments.CurrencyUSD)
|
||||
rate, err := tokens.service.GetRate(ctx, monetary.StorjToken, monetary.USDollars)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
tokenAmount := convertFromCents(rate, amount).SetPrec(payments.STORJTokenPrecision)
|
||||
tokenAmount := convertFromCents(rate, amount)
|
||||
|
||||
tx, err := tokens.service.coinPayments.Transactions().Create(ctx,
|
||||
&coinpayments.CreateTX{
|
||||
Amount: *tokenAmount,
|
||||
CurrencyIn: coinpayments.CurrencySTORJ,
|
||||
CurrencyOut: coinpayments.CurrencySTORJ,
|
||||
Amount: tokenAmount.AsDecimal(),
|
||||
CurrencyIn: monetary.StorjToken,
|
||||
CurrencyOut: monetary.StorjToken,
|
||||
BuyerEmail: c.Email,
|
||||
},
|
||||
)
|
||||
@ -79,7 +80,7 @@ func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
key, err := coinpayments.GetTransacationKeyFromURL(tx.CheckoutURL)
|
||||
key, err := coinpayments.GetTransactionKeyFromURL(tx.CheckoutURL)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
@ -105,8 +106,8 @@ func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount
|
||||
|
||||
return &payments.Transaction{
|
||||
ID: payments.TransactionID(tx.ID),
|
||||
Amount: *payments.TokenAmountFromBigFloat(&tx.Amount),
|
||||
Rate: *rate,
|
||||
Amount: tx.Amount,
|
||||
Rate: rate,
|
||||
Address: tx.Address,
|
||||
Status: payments.TransactionStatusPending,
|
||||
Timeout: tx.Timeout,
|
||||
@ -149,10 +150,10 @@ func (tokens *storjTokens) ListTransactionInfos(ctx context.Context, userID uuid
|
||||
infos = append(infos,
|
||||
payments.TransactionInfo{
|
||||
ID: []byte(tx.ID),
|
||||
Amount: *payments.TokenAmountFromBigFloat(&tx.Amount),
|
||||
Received: *payments.TokenAmountFromBigFloat(&tx.Received),
|
||||
AmountCents: convertToCents(rate, &tx.Amount),
|
||||
ReceivedCents: convertToCents(rate, &tx.Received),
|
||||
Amount: tx.Amount,
|
||||
Received: tx.Received,
|
||||
AmountCents: convertToCents(rate, tx.Amount),
|
||||
ReceivedCents: convertToCents(rate, tx.Received),
|
||||
Address: tx.Address,
|
||||
Status: status,
|
||||
Link: link,
|
||||
|
@ -5,13 +5,14 @@ package stripecoinpayments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
// ErrTransactionConsumed is thrown when trying to consume already consumed transaction.
|
||||
@ -29,9 +30,9 @@ type TransactionsDB interface {
|
||||
// Consume marks transaction as consumed, so it won't participate in apply account balance loop.
|
||||
Consume(ctx context.Context, id coinpayments.TransactionID) error
|
||||
// LockRate locks conversion rate for transaction.
|
||||
LockRate(ctx context.Context, id coinpayments.TransactionID, rate *big.Float) error
|
||||
LockRate(ctx context.Context, id coinpayments.TransactionID, rate decimal.Decimal) error
|
||||
// GetLockedRate returns locked conversion rate for transaction or error if non exists.
|
||||
GetLockedRate(ctx context.Context, id coinpayments.TransactionID) (*big.Float, error)
|
||||
GetLockedRate(ctx context.Context, id coinpayments.TransactionID) (decimal.Decimal, error)
|
||||
// ListAccount returns all transaction for specific user.
|
||||
ListAccount(ctx context.Context, userID uuid.UUID) ([]Transaction, error)
|
||||
// ListPending returns TransactionsPage with pending transactions.
|
||||
@ -45,8 +46,8 @@ type Transaction struct {
|
||||
ID coinpayments.TransactionID
|
||||
AccountID uuid.UUID
|
||||
Address string
|
||||
Amount big.Float
|
||||
Received big.Float
|
||||
Amount monetary.Amount
|
||||
Received monetary.Amount
|
||||
Status coinpayments.Status
|
||||
Key string
|
||||
Timeout time.Duration
|
||||
@ -57,7 +58,7 @@ type Transaction struct {
|
||||
type TransactionUpdate struct {
|
||||
TransactionID coinpayments.TransactionID
|
||||
Status coinpayments.Status
|
||||
Received big.Float
|
||||
Received monetary.Amount
|
||||
}
|
||||
|
||||
// TransactionsPage holds set of transaction and indicates if
|
||||
|
@ -6,11 +6,11 @@ package stripecoinpayments_test
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
)
|
||||
@ -32,17 +33,18 @@ func TestTransactionsDB(t *testing.T) {
|
||||
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
||||
transactions := db.StripeCoinPayments().Transactions()
|
||||
|
||||
amount, ok := new(big.Float).SetPrec(1000).SetString("2.0000000000000000005")
|
||||
require.True(t, ok)
|
||||
received, ok := new(big.Float).SetPrec(1000).SetString("1.0000000000000000003")
|
||||
require.True(t, ok)
|
||||
amount, err := monetary.AmountFromString("2.0000000000000000005", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
received, err := monetary.AmountFromString("1.0000000000000000003", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
userID := testrand.UUID()
|
||||
|
||||
createTx := stripecoinpayments.Transaction{
|
||||
ID: "testID",
|
||||
AccountID: uuid.UUID{1, 2, 3},
|
||||
AccountID: userID,
|
||||
Address: "testAddress",
|
||||
Amount: *amount,
|
||||
Received: *received,
|
||||
Amount: amount,
|
||||
Received: received,
|
||||
Status: coinpayments.StatusPending,
|
||||
Key: "testKey",
|
||||
Timeout: time.Second * 60,
|
||||
@ -51,29 +53,30 @@ func TestTransactionsDB(t *testing.T) {
|
||||
t.Run("insert", func(t *testing.T) {
|
||||
tx, err := transactions.Insert(ctx, createTx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tx)
|
||||
|
||||
compareTransactions(t, createTx, *tx)
|
||||
requireSaneTimestamp(t, tx.CreatedAt)
|
||||
txs, err := transactions.ListAccount(ctx, userID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, txs, 1)
|
||||
compareTransactions(t, createTx, txs[0])
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
received, ok := new(big.Float).SetPrec(1000).SetString("6.0000000000000000001")
|
||||
require.True(t, ok)
|
||||
received, err := monetary.AmountFromString("6.0000000000000000001", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
update := stripecoinpayments.TransactionUpdate{
|
||||
TransactionID: createTx.ID,
|
||||
Status: coinpayments.StatusReceived,
|
||||
Received: *received,
|
||||
Received: received,
|
||||
}
|
||||
|
||||
err := transactions.Update(ctx, []stripecoinpayments.TransactionUpdate{update}, nil)
|
||||
err = transactions.Update(ctx, []stripecoinpayments.TransactionUpdate{update}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
page, err := transactions.ListPending(ctx, 0, 1, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, page.Transactions)
|
||||
require.Equal(t, 1, len(page.Transactions))
|
||||
require.Len(t, page.Transactions, 1)
|
||||
assert.Equal(t, createTx.ID, page.Transactions[0].ID)
|
||||
assert.Equal(t, update.Received, page.Transactions[0].Received)
|
||||
assert.Equal(t, update.Status, page.Transactions[0].Status)
|
||||
@ -83,7 +86,7 @@ func TestTransactionsDB(t *testing.T) {
|
||||
{
|
||||
TransactionID: createTx.ID,
|
||||
Status: coinpayments.StatusCompleted,
|
||||
Received: *received,
|
||||
Received: received,
|
||||
},
|
||||
},
|
||||
coinpayments.TransactionIDList{
|
||||
@ -115,24 +118,34 @@ func TestTransactionsDB(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func requireSaneTimestamp(t *testing.T, when time.Time) {
|
||||
// ensure time value is sane. I apologize to you people of the future when this starts breaking
|
||||
require.Truef(t, when.After(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
"%s seems too small to be a valid creation timestamp", when)
|
||||
require.Truef(t, when.Before(time.Date(2500, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
"%s seems too large to be a valid creation timestamp", when)
|
||||
}
|
||||
|
||||
func TestConcurrentConsume(t *testing.T) {
|
||||
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
||||
transactions := db.StripeCoinPayments().Transactions()
|
||||
|
||||
const concurrentTries = 30
|
||||
|
||||
amount, ok := new(big.Float).SetPrec(1000).SetString("2.0000000000000000005")
|
||||
require.True(t, ok)
|
||||
received, ok := new(big.Float).SetPrec(1000).SetString("1.0000000000000000003")
|
||||
require.True(t, ok)
|
||||
amount, err := monetary.AmountFromString("2.0000000000000000005", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
received, err := monetary.AmountFromString("1.0000000000000000003", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
userID := testrand.UUID()
|
||||
txID := coinpayments.TransactionID("testID")
|
||||
|
||||
tx, err := transactions.Insert(ctx,
|
||||
stripecoinpayments.Transaction{
|
||||
ID: "testID",
|
||||
AccountID: uuid.UUID{1, 2, 3},
|
||||
ID: txID,
|
||||
AccountID: userID,
|
||||
Address: "testAddress",
|
||||
Amount: *amount,
|
||||
Received: *received,
|
||||
Amount: amount,
|
||||
Received: received,
|
||||
Status: coinpayments.StatusPending,
|
||||
Key: "testKey",
|
||||
Timeout: time.Second * 60,
|
||||
@ -144,7 +157,7 @@ func TestConcurrentConsume(t *testing.T) {
|
||||
[]stripecoinpayments.TransactionUpdate{{
|
||||
TransactionID: tx.ID,
|
||||
Status: coinpayments.StatusCompleted,
|
||||
Received: *received,
|
||||
Received: received,
|
||||
}},
|
||||
coinpayments.TransactionIDList{
|
||||
tx.ID,
|
||||
@ -194,10 +207,10 @@ func TestTransactionsDBList(t *testing.T) {
|
||||
)
|
||||
|
||||
// create transactions
|
||||
amount, ok := new(big.Float).SetPrec(1000).SetString("4.0000000000000000005")
|
||||
require.True(t, ok)
|
||||
received, ok := new(big.Float).SetPrec(1000).SetString("5.0000000000000000003")
|
||||
require.True(t, ok)
|
||||
amount, err := monetary.AmountFromString("4.0000000000000000005", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
received, err := monetary.AmountFromString("5.0000000000000000003", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
var txs []stripecoinpayments.Transaction
|
||||
for i := 0; i < transactionCount; i++ {
|
||||
@ -214,8 +227,8 @@ func TestTransactionsDBList(t *testing.T) {
|
||||
ID: coinpayments.TransactionID(id),
|
||||
AccountID: uuid.UUID{},
|
||||
Address: addr,
|
||||
Amount: *amount,
|
||||
Received: *received,
|
||||
Amount: amount,
|
||||
Received: received,
|
||||
Status: status,
|
||||
Key: key,
|
||||
}
|
||||
@ -312,12 +325,12 @@ func TestTransactionsDBRates(t *testing.T) {
|
||||
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
||||
transactions := db.StripeCoinPayments().Transactions()
|
||||
|
||||
val, ok := new(big.Float).SetPrec(1000).SetString("4.0000000000000000005")
|
||||
require.True(t, ok)
|
||||
val, err := decimal.NewFromString("4.000000000000000005")
|
||||
require.NoError(t, err)
|
||||
|
||||
const txID = "tx_id"
|
||||
|
||||
err := transactions.LockRate(ctx, txID, val)
|
||||
err = transactions.LockRate(ctx, txID, val)
|
||||
require.NoError(t, err)
|
||||
|
||||
rate, err := transactions.GetLockedRate(ctx, txID)
|
||||
@ -355,17 +368,17 @@ func TestTransactions_ApplyTransactionBalance(t *testing.T) {
|
||||
|
||||
// Emulate a deposit through CoinPayments.
|
||||
txID := coinpayments.TransactionID("testID")
|
||||
storjAmount, ok := new(big.Float).SetString("100")
|
||||
require.True(t, ok)
|
||||
storjUSDRate, ok := new(big.Float).SetString("0.2")
|
||||
require.True(t, ok)
|
||||
storjAmount, err := monetary.AmountFromString("100", monetary.StorjToken)
|
||||
require.NoError(t, err)
|
||||
storjUSDRate, err := decimal.NewFromString("0.2")
|
||||
require.NoError(t, err)
|
||||
|
||||
createTx := stripecoinpayments.Transaction{
|
||||
ID: txID,
|
||||
AccountID: userID,
|
||||
Address: "testAddress",
|
||||
Amount: *storjAmount,
|
||||
Received: *storjAmount,
|
||||
Amount: storjAmount,
|
||||
Received: storjAmount,
|
||||
Status: coinpayments.StatusPending,
|
||||
Key: "testKey",
|
||||
Timeout: time.Second * 60,
|
||||
@ -378,7 +391,7 @@ func TestTransactions_ApplyTransactionBalance(t *testing.T) {
|
||||
update := stripecoinpayments.TransactionUpdate{
|
||||
TransactionID: createTx.ID,
|
||||
Status: coinpayments.StatusReceived,
|
||||
Received: *storjAmount,
|
||||
Received: storjAmount,
|
||||
}
|
||||
|
||||
err = transactions.Update(ctx, []stripecoinpayments.TransactionUpdate{update}, coinpayments.TransactionIDList{createTx.ID})
|
||||
|
@ -5,10 +5,12 @@ package payments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
)
|
||||
|
||||
// StorjTokens defines all payments STORJ token related functionality.
|
||||
@ -53,8 +55,8 @@ func (id TransactionID) String() string {
|
||||
// accepts user funds on a specific wallet address.
|
||||
type Transaction struct {
|
||||
ID TransactionID
|
||||
Amount TokenAmount
|
||||
Rate big.Float
|
||||
Amount monetary.Amount
|
||||
Rate decimal.Decimal
|
||||
Address string
|
||||
Status TransactionStatus
|
||||
Timeout time.Duration
|
||||
@ -66,8 +68,8 @@ type Transaction struct {
|
||||
// such as links and expiration time.
|
||||
type TransactionInfo struct {
|
||||
ID TransactionID
|
||||
Amount TokenAmount
|
||||
Received TokenAmount
|
||||
Amount monetary.Amount
|
||||
Received monetary.Amount
|
||||
AmountCents int64
|
||||
ReceivedCents int64
|
||||
Address string
|
||||
@ -77,49 +79,6 @@ type TransactionInfo struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// TokenAmount is a wrapper type for STORJ token amount.
|
||||
// Uses big.Float as inner representation. Precision is set to 32
|
||||
// so it can properly handle 8 digits after point which is STORJ token
|
||||
// decimal set.
|
||||
type TokenAmount struct {
|
||||
inner big.Float
|
||||
}
|
||||
|
||||
// STORJTokenPrecision defines STORJ token precision.
|
||||
const STORJTokenPrecision = 32
|
||||
|
||||
// NewTokenAmount creates new zeroed TokenAmount with fixed precision.
|
||||
func NewTokenAmount() *TokenAmount {
|
||||
return &TokenAmount{inner: *new(big.Float).SetPrec(STORJTokenPrecision)}
|
||||
}
|
||||
|
||||
// BigFloat returns inner representation of TokenAmount.
|
||||
func (amount *TokenAmount) BigFloat() *big.Float {
|
||||
f := new(big.Float).Set(&amount.inner)
|
||||
return f
|
||||
}
|
||||
|
||||
// String representation of TokenValue.
|
||||
func (amount *TokenAmount) String() string {
|
||||
return amount.inner.Text('f', -1)
|
||||
}
|
||||
|
||||
// ParseTokenAmount parses string representing floating point and returns
|
||||
// TokenAmount.
|
||||
func ParseTokenAmount(s string) (*TokenAmount, error) {
|
||||
inner, _, err := big.ParseFloat(s, 10, STORJTokenPrecision, big.ToNearestEven)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TokenAmount{inner: *inner}, nil
|
||||
}
|
||||
|
||||
// TokenAmountFromBigFloat converts big.Float to TokenAmount.
|
||||
func TokenAmountFromBigFloat(f *big.Float) *TokenAmount {
|
||||
inner := (*f).SetMode(big.ToNearestEven).SetPrec(STORJTokenPrecision)
|
||||
return &TokenAmount{inner: *inner}
|
||||
}
|
||||
|
||||
// DepositBonus defines a bonus received for depositing tokens.
|
||||
type DepositBonus struct {
|
||||
TransactionID TransactionID
|
||||
|
@ -8,10 +8,12 @@ import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/monetary"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
"storj.io/storj/satellite/satellitedb/dbx"
|
||||
)
|
||||
@ -45,11 +47,11 @@ type coinPaymentsTransactions struct {
|
||||
func (db *coinPaymentsTransactions) Insert(ctx context.Context, tx stripecoinpayments.Transaction) (_ *stripecoinpayments.Transaction, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
amount, err := tx.Amount.GobEncode()
|
||||
amount, err := tx.Amount.AsBigFloat().GobEncode()
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
received, err := tx.Received.GobEncode()
|
||||
received, err := tx.Received.AsBigFloat().GobEncode()
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
@ -81,7 +83,7 @@ func (db *coinPaymentsTransactions) Update(ctx context.Context, updates []stripe
|
||||
|
||||
return db.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
|
||||
for _, update := range updates {
|
||||
received, err := update.Received.GobEncode()
|
||||
received, err := update.Received.AsBigFloat().GobEncode()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
@ -147,10 +149,10 @@ func (db *coinPaymentsTransactions) Consume(ctx context.Context, id coinpayments
|
||||
}
|
||||
|
||||
// LockRate locks conversion rate for transaction.
|
||||
func (db *coinPaymentsTransactions) LockRate(ctx context.Context, id coinpayments.TransactionID, rate *big.Float) (err error) {
|
||||
func (db *coinPaymentsTransactions) LockRate(ctx context.Context, id coinpayments.TransactionID, rate decimal.Decimal) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
buff, err := rate.GobEncode()
|
||||
buff, err := rate.BigFloat().GobEncode()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
@ -163,19 +165,23 @@ func (db *coinPaymentsTransactions) LockRate(ctx context.Context, id coinpayment
|
||||
}
|
||||
|
||||
// GetLockedRate returns locked conversion rate for transaction or error if non exists.
|
||||
func (db *coinPaymentsTransactions) GetLockedRate(ctx context.Context, id coinpayments.TransactionID) (_ *big.Float, err error) {
|
||||
func (db *coinPaymentsTransactions) GetLockedRate(ctx context.Context, id coinpayments.TransactionID) (_ decimal.Decimal, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
dbxRate, err := db.db.Get_StripecoinpaymentsTxConversionRate_By_TxId(ctx,
|
||||
dbx.StripecoinpaymentsTxConversionRate_TxId(id.String()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return decimal.Decimal{}, err
|
||||
}
|
||||
|
||||
rate := new(big.Float)
|
||||
if err = rate.GobDecode(dbxRate.Rate); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
var rateF big.Float
|
||||
if err = rateF.GobDecode(dbxRate.Rate); err != nil {
|
||||
return decimal.Decimal{}, errs.Wrap(err)
|
||||
}
|
||||
rate, err := monetary.DecimalFromBigFloat(&rateF)
|
||||
if err != nil {
|
||||
return decimal.Decimal{}, errs.Wrap(err)
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
@ -248,12 +254,17 @@ func (db *coinPaymentsTransactions) ListPending(ctx context.Context, offset int6
|
||||
return stripecoinpayments.TransactionsPage{}, err
|
||||
}
|
||||
|
||||
var amount, received big.Float
|
||||
if err := amount.GobDecode(amountB); err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
||||
// TODO: the currency here should be passed in to this function or stored
|
||||
// in the database.
|
||||
currency := monetary.StorjToken
|
||||
|
||||
amount, err := monetaryAmountFromGobEncodedBigFloat(amountB, currency)
|
||||
if err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, err
|
||||
}
|
||||
if err := received.GobDecode(receivedB); err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
||||
received, err := monetaryAmountFromGobEncodedBigFloat(receivedB, currency)
|
||||
if err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, err
|
||||
}
|
||||
|
||||
page.Transactions = append(page.Transactions,
|
||||
@ -283,7 +294,7 @@ func (db *coinPaymentsTransactions) ListPending(ctx context.Context, offset int6
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// List Unapplied returns TransactionsPage with a pending or completed status, that should be applied to account balance.
|
||||
// ListUnapplied returns TransactionsPage with a pending or completed status, that should be applied to account balance.
|
||||
func (db *coinPaymentsTransactions) ListUnapplied(ctx context.Context, offset int64, limit int, before time.Time) (_ stripecoinpayments.TransactionsPage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
@ -326,11 +337,16 @@ func (db *coinPaymentsTransactions) ListUnapplied(ctx context.Context, offset in
|
||||
return stripecoinpayments.TransactionsPage{}, err
|
||||
}
|
||||
|
||||
var amount, received big.Float
|
||||
if err := amount.GobDecode(amountB); err != nil {
|
||||
// TODO: the currency here should be passed in to this function or stored
|
||||
// in the database.
|
||||
currency := monetary.StorjToken
|
||||
|
||||
amount, err := monetaryAmountFromGobEncodedBigFloat(amountB, currency)
|
||||
if err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
||||
}
|
||||
if err := received.GobDecode(receivedB); err != nil {
|
||||
received, err := monetaryAmountFromGobEncodedBigFloat(receivedB, currency)
|
||||
if err != nil {
|
||||
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
||||
}
|
||||
|
||||
@ -368,11 +384,16 @@ func fromDBXCoinpaymentsTransaction(dbxCPTX *dbx.CoinpaymentsTransaction) (*stri
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
var amount, received big.Float
|
||||
if err := amount.GobDecode(dbxCPTX.Amount); err != nil {
|
||||
// TODO: the currency here should be passed in to this function or stored
|
||||
// in the database.
|
||||
currency := monetary.StorjToken
|
||||
|
||||
amount, err := monetaryAmountFromGobEncodedBigFloat(dbxCPTX.Amount, currency)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
if err := received.GobDecode(dbxCPTX.Received); err != nil {
|
||||
received, err := monetaryAmountFromGobEncodedBigFloat(dbxCPTX.Received, currency)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
@ -388,3 +409,11 @@ func fromDBXCoinpaymentsTransaction(dbxCPTX *dbx.CoinpaymentsTransaction) (*stri
|
||||
CreatedAt: dbxCPTX.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func monetaryAmountFromGobEncodedBigFloat(encoded []byte, currency *monetary.Currency) (_ monetary.Amount, err error) {
|
||||
var bf big.Float
|
||||
if err := bf.GobDecode(encoded); err != nil {
|
||||
return monetary.Amount{}, Error.Wrap(err)
|
||||
}
|
||||
return monetary.AmountFromBigFloat(&bf, currency)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user