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

feat: system transfers #44

Merged
merged 7 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading