satellite/payments/monetary: add USDMicro and json marshaling

Adds USDMicro currency which support fraction of a cent with decimal places
for better billing amounts accuracy.
Adds JSON marshaling and unmarshaling for monetary.Amount, so that it
can be converted to/from JSON.

Change-Id: I034eba120ed23b6ba00b2d81a4f1b9db5f9a203f
This commit is contained in:
Yaroslav Vorobiov 2022-08-18 15:21:03 +02:00 committed by Storj Robot
parent e0d3e48b66
commit 7a9b2a707b
3 changed files with 133 additions and 0 deletions

View File

@ -4,6 +4,7 @@
package monetary
import (
"encoding/json"
"fmt"
"math"
"math/big"
@ -43,6 +44,9 @@ var (
// USDollars is the currency of United States dollars, where fractional
// cents are not supported.
USDollars = NewCurrency("US dollars", "USD", 2)
// USDollarsMicro is the currency of United States dollars, where fractional
// cents are supported with 2 decimal places.
USDollarsMicro = NewCurrency("US dollars", "USDMicro", 6)
// Bitcoin is the currency for the well-known cryptocurrency Bitcoin
// (a.k.a. BTC).
Bitcoin = NewCurrency("Bitcoin (BTC)", "BTC", 8)
@ -54,6 +58,24 @@ var (
Error = errs.Class("monetary error")
)
// CurrencyFromSymbol returns currency based on symbol.
func CurrencyFromSymbol(symbol string) (*Currency, error) {
switch symbol {
case "STORJ":
return StorjToken, nil
case "BTC":
return Bitcoin, nil
case "USD":
return USDollars, nil
case "USDMicro":
return USDollarsMicro, nil
case "goats":
return LiveGoats, nil
default:
return nil, errs.New("invalid currency symbol")
}
}
// 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
@ -115,6 +137,38 @@ func (a Amount) Equal(other Amount) bool {
return a.currency == other.currency && a.baseUnits == other.baseUnits
}
// amountJSON is amount json data structure.
type amountJSON struct {
Value decimal.Decimal `json:"value"`
Currency string `json:"currency"`
}
// UnmarshalJSON unmarshals json bytes into amount.
func (a *Amount) UnmarshalJSON(data []byte) error {
var amountJSON amountJSON
if err := json.Unmarshal(data, &amountJSON); err != nil {
return err
}
curr, err := CurrencyFromSymbol(amountJSON.Currency)
if err != nil {
return err
}
*a = AmountFromDecimal(amountJSON.Value, curr)
return nil
}
// MarshalJSON marshals amount into json.
func (a Amount) MarshalJSON() ([]byte, error) {
amountJSON := amountJSON{
Value: a.AsDecimal(),
Currency: a.currency.symbol,
}
return json.Marshal(amountJSON)
}
// 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 {

View File

@ -4,6 +4,8 @@
package monetary
import (
"encoding/json"
"fmt"
"math/big"
"testing"
@ -97,3 +99,79 @@ func TestAmountFromDecimalAndAmountAsDecimal(t *testing.T) {
})
}
}
func TestAmountJSONMarshal(t *testing.T) {
tests := []struct {
Amount Amount
JSON string
}{
{
Amount: AmountFromBaseUnits(100000000000, StorjToken),
JSON: fmt.Sprintf(`{"value":"1000","currency":"%s"}`, StorjToken.Symbol()),
},
{
Amount: AmountFromBaseUnits(10055, USDollars),
JSON: fmt.Sprintf(`{"value":"100.55","currency":"%s"}`, USDollars.Symbol()),
},
{
Amount: AmountFromBaseUnits(100555500, USDollarsMicro),
JSON: fmt.Sprintf(`{"value":"100.5555","currency":"%s"}`, USDollarsMicro.Symbol()),
},
{
Amount: AmountFromBaseUnits(100555555, USDollarsMicro),
JSON: fmt.Sprintf(`{"value":"100.555555","currency":"%s"}`, USDollarsMicro.Symbol()),
},
}
for _, test := range tests {
b, err := json.Marshal(test.Amount)
require.NoError(t, err)
require.Equal(t, test.JSON, string(b))
}
}
func TestAmountJSONUnmarshal(t *testing.T) {
tests := []struct {
JSON string
BaseUnits int64
Currency *Currency
}{
{
JSON: fmt.Sprintf(`{"value":"100","currency":"%s"}`, StorjToken.Symbol()),
BaseUnits: 10000000000,
Currency: StorjToken,
},
{
JSON: fmt.Sprintf(`{"value":"50","currency":"%s"}`, Bitcoin.Symbol()),
BaseUnits: 5000000000,
Currency: Bitcoin,
},
{
JSON: fmt.Sprintf(`{"value":"100.55","currency":"%s"}`, USDollars.Symbol()),
BaseUnits: 10055,
Currency: USDollars,
},
{
JSON: fmt.Sprintf(`{"value":"100.5555","currency":"%s"}`, USDollarsMicro.Symbol()),
BaseUnits: 100555500,
Currency: USDollarsMicro,
},
{
JSON: fmt.Sprintf(`{"value":"100.555555","currency":"%s"}`, USDollarsMicro.Symbol()),
BaseUnits: 100555555,
Currency: USDollarsMicro,
},
{
JSON: fmt.Sprintf(`{"value":"10","currency":"%s"}`, LiveGoats.Symbol()),
BaseUnits: 10,
Currency: LiveGoats,
},
}
for _, test := range tests {
var amount Amount
err := json.Unmarshal([]byte(test.JSON), &amount)
require.NoError(t, err)
require.Equal(t, test.BaseUnits, amount.BaseUnits())
require.Equal(t, test.Currency, amount.Currency())
}
}

View File

@ -62,6 +62,7 @@ func TestChore(t *testing.T) {
From: blockchain2.Address(accs[0].Address),
To: blockchain2.Address(receiver),
TokenValue: monetary.AmountFromBaseUnits(10000, monetary.StorjToken),
USDValue: monetary.AmountFromBaseUnits(1000000, monetary.USDollars),
Status: payments.PaymentStatusPending,
BlockHash: blockchain2.Hash(block.Hash()),
BlockNumber: block.Number().Int64(),