Skip to content
This repository has been archived by the owner on Jul 1, 2024. It is now read-only.

Commit

Permalink
feat: system transfers (#44)
Browse files Browse the repository at this point in the history
* Support for subtracting negative numbers

* Add command for listing balances. Calculate system balances

* Scaffold topup-balance command

* Implement system topup feature

* Wire cmd command; fix currency ticker for internal payments

* Fix test cases

* Fix money.String() for negative numbers
  • Loading branch information
swift1337 authored Dec 17, 2023
1 parent ce41a2f commit 7dda6a1
Show file tree
Hide file tree
Showing 20 changed files with 1,052 additions and 33 deletions.
89 changes: 89 additions & 0 deletions cmd/list_balances.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package cmd

import (
"context"
"fmt"
"os"
"strings"

"github.com/olekukonko/tablewriter"
"github.com/oxygenpay/oxygen/internal/app"
"github.com/oxygenpay/oxygen/internal/money"
"github.com/oxygenpay/oxygen/internal/service/wallet"
"github.com/spf13/cobra"
)

var listBalancesCommand = &cobra.Command{
Use: "list-balances",
Short: "List all balances including system balances",
Run: listBalances,
}

func listBalances(_ *cobra.Command, _ []string) {
var (
ctx = context.Background()
cfg = resolveConfig()
service = app.New(ctx, cfg)
walletsService = service.Locator().WalletService()
blockchainService = service.Locator().BlockchainService()
logger = service.Logger()
)

opts := wallet.ListAllBalancesOpts{
WithUSD: true,
WithSystemBalances: true,
HideEmpty: true,
}

balances, err := walletsService.ListAllBalances(ctx, opts)
if err != nil {
logger.Error().Err(err).Msg("Unable to list wallets")
}

t := tablewriter.NewWriter(os.Stdout)
defer t.Render()

t.SetBorder(false)
t.SetAutoWrapText(false)
t.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
t.SetAlignment(tablewriter.ALIGN_LEFT)

t.SetHeader([]string{"id", "type", "entity Id", "currency", "test", "amount", "usd"})

add := func(b *wallet.Balance) {
currency, err := blockchainService.GetCurrencyByTicker(b.Currency)
if err != nil {
logger.Error().Err(err)
return
}

t.Append(balanceAsRow(currency, b))
}

for _, b := range balances[wallet.EntityTypeMerchant] {
add(b)
}
for _, b := range balances[wallet.EntityTypeWallet] {
add(b)
}
for _, b := range balances[wallet.EntityTypeSystem] {
add(b)
}
}

func balanceAsRow(currency money.CryptoCurrency, b *wallet.Balance) []string {
isTest := b.NetworkID != currency.NetworkID

line := fmt.Sprintf(
"%d,%s,%d,%s,%t,%s,%s",
b.ID,
b.EntityType,
b.EntityID,
b.Currency,
isTest,
b.Amount.String(),
b.UsdAmount.String(),
)

return strings.Split(line, ",")
}
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func init() {
createUserCommand.PersistentFlags().BoolVar(&overridePassword, "override-password", false, "overrides password if user already exists")

rootCmd.AddCommand(listWalletsCommand)
rootCmd.AddCommand(listBalancesCommand)

topupBalanceSetup(topupBalanceCommand)
rootCmd.AddCommand(topupBalanceCommand)

rand.Seed(time.Now().Unix())
}
159 changes: 159 additions & 0 deletions cmd/topup_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package cmd

import (
"bufio"
"context"
"fmt"
"os"
"strings"

"github.com/oxygenpay/oxygen/internal/app"
"github.com/oxygenpay/oxygen/internal/money"
"github.com/oxygenpay/oxygen/internal/service/processing"
"github.com/oxygenpay/oxygen/internal/service/wallet"
"github.com/oxygenpay/oxygen/internal/util"
"github.com/samber/lo"
"github.com/spf13/cobra"
)

var topupBalanceCommand = &cobra.Command{
Use: "topup-balance",
Short: "Topup Merchant's balance using 'system' funds",
Run: topupBalance,
}

var topupBalanceArgs = struct {
MerchantID *int64
Ticker *string
Amount *string
Comment *string
IsTest *bool
}{
MerchantID: util.Ptr(int64(0)),
Ticker: util.Ptr(""),
Amount: util.Ptr(""),
Comment: util.Ptr(""),
IsTest: util.Ptr(false),
}

func topupBalance(_ *cobra.Command, _ []string) {
var (
ctx = context.Background()
cfg = resolveConfig()
service = app.New(ctx, cfg)
blockchainService = service.Locator().BlockchainService()
merchantService = service.Locator().MerchantService()
walletService = service.Locator().WalletService()
processingService = service.Locator().ProcessingService()
logger = service.Logger()
exit = func(err error, message string) { logger.Fatal().Err(err).Msg(message) }
)

// 1. Get input
currency, err := blockchainService.GetCurrencyByTicker(*topupBalanceArgs.Ticker)
if err != nil {
exit(err, "invalid ticker")
}

amount, err := money.CryptoFromStringFloat(currency.Ticker, *topupBalanceArgs.Amount, currency.Decimals)
if err != nil {
exit(err, "invalid amount")
}

merchant, err := merchantService.GetByID(ctx, *topupBalanceArgs.MerchantID, false)
if err != nil {
exit(err, "invalid merchant id")
}

if *topupBalanceArgs.Comment == "" {
exit(nil, "comment should not be empty")
}

isTest := *topupBalanceArgs.IsTest
comment := *topupBalanceArgs.Comment

// 2. Locate system balance
balances, err := walletService.ListAllBalances(ctx, wallet.ListAllBalancesOpts{WithSystemBalances: true})
if err != nil {
exit(err, "unable to list balances")
}

systemBalance, found := lo.Find(balances[wallet.EntityTypeSystem], func(b *wallet.Balance) bool {
tickerMatches := b.Currency == currency.Ticker
networkMatches := b.NetworkID == currency.ChooseNetwork(isTest)

return tickerMatches && networkMatches
})

if !found {
exit(err, "unable to locate system balance")
}

logger.Info().
Str("amount", amount.String()).
Str("currency", currency.Ticker).
Str("merchant.name", merchant.Name).
Int64("merchant.id", merchant.ID).
Str("merchant.uuid", merchant.UUID.String()).
Str("system_balance", systemBalance.Amount.String()).
Msg("Performing internal topup from the system balance")

// 3. Confirm
if !confirm("Are you sure you want to continue?") {
logger.Info().Msg("Aborting.")
return
}

// 4. Perform topup
logger.Info().Msg("Sending...")

input := processing.TopupInput{
Currency: currency,
Amount: amount,
Comment: comment,
IsTest: isTest,
}

out, err := processingService.TopupMerchantFromSystem(ctx, merchant.ID, input)
if err != nil {
exit(err, "unable to topup the balance")
}

logger.
Info().
Int64("payment.id", out.Payment.ID).
Int64("tx.id", out.Transaction.ID).
Str("tx.usd_amount", out.Transaction.USDAmount.String()).
Str("merchant.balance", out.MerchantBalance.Amount.String()).
Msg("Done")
}

func topupBalanceSetup(cmd *cobra.Command) {
f := cmd.Flags()

f.Int64Var(topupBalanceArgs.MerchantID, "merchant-id", 0, "Merchant ID")
f.StringVar(topupBalanceArgs.Ticker, "ticker", "", "Ticker")
f.StringVar(topupBalanceArgs.Amount, "amount", "0", "Amount")
f.StringVar(topupBalanceArgs.Comment, "comment", "", "Comment")
f.BoolVar(topupBalanceArgs.IsTest, "is-test", false, "Test balance")

for _, name := range []string{"merchant-id", "ticker", "amount", "comment"} {
if err := cmd.MarkFlagRequired(name); err != nil {
panic(name + ": " + err.Error())
}
}
}

func confirm(message string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s (y/n): ", message)

response, err := reader.ReadString('\n')
if err != nil {
return false
}

response = strings.ToLower(strings.TrimSpace(response))

return response == "y" || response == "yes"
}
45 changes: 45 additions & 0 deletions internal/db/repository/balances.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion internal/db/repository/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,5 @@ func MoneyToNumeric(m money.Money) pgtype.Numeric {

func MoneyToNegNumeric(m money.Money) pgtype.Numeric {
bigInt, _ := m.BigInt()

return BigIntToNumeric(big.NewInt(0).Neg(bigInt))
}
1 change: 1 addition & 0 deletions internal/db/repository/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 29 additions & 7 deletions internal/money/money.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ func (m Money) Decimals() int64 {

func (m Money) String() string {
stringRaw := m.StringRaw()

isNegative := m.IsNegative()
if isNegative {
stringRaw = stringRaw[1:]
}

l, d := len(stringRaw), int(m.decimals)

var result string
Expand All @@ -159,10 +165,16 @@ func (m Money) String() string {
}

if m.moneyType == Fiat {
return strings.TrimSuffix(result, ".00")
result = strings.TrimSuffix(result, ".00")
} else {
result = strings.TrimRight(strings.TrimRight(result, "0"), ".")
}

return strings.TrimRight(strings.TrimRight(result, "0"), ".")
if isNegative {
result = "-" + result
}

return result
}

func (m Money) StringRaw() string {
Expand Down Expand Up @@ -202,8 +214,22 @@ func (m Money) Add(amount Money) (Money, error) {
return NewFromBigInt(m.moneyType, m.ticker, a.Add(a, b), m.decimals)
}

// Sub subtracts money of the same type.
// Sub subtracts money of the same type. Restricts having negative values.
func (m Money) Sub(amount Money) (Money, error) {
out, err := m.SubNegative(amount)
if err != nil {
return Money{}, err
}

if out.IsNegative() {
return Money{}, ErrNegative
}

return out, nil
}

// SubNegative subtracts money allowing negative outcome
func (m Money) SubNegative(amount Money) (Money, error) {
if !m.CompatibleTo(amount) {
return Money{}, errors.Wrapf(
ErrIncompatibleMoney,
Expand All @@ -220,10 +246,6 @@ func (m Money) Sub(amount Money) (Money, error) {
return Money{}, nil
}

if m.IsNegative() {
return Money{}, ErrNegative
}

return m, nil
}

Expand Down
Loading

0 comments on commit 7dda6a1

Please sign in to comment.