satellitedb: add token balance to API endpoint

Add the users current wallet balance to the endpoints for claiming and listing storjscan wallets. Also prevent a user with a claimed wallet address from claiming a new wallet.

Change-Id: I0dbf1303699f924d05c8c52359038dc5ef6c42a1
This commit is contained in:
dlamarmorgan 2022-08-15 07:41:19 -07:00 committed by Antonio Franco (He/Him)
parent 119e61fcb0
commit 335e11dacd
12 changed files with 98 additions and 12 deletions

View File

@ -579,6 +579,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.Marketing.PartnersService,
peer.Payments.Accounts,
peer.Payments.DepositWallets,
peer.DB.Billing(),
peer.Analytics.Service,
peer.Console.AuthTokens,
peer.Mail.Service,

View File

@ -108,6 +108,7 @@ func TestGraphqlMutation(t *testing.T) {
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
db.Billing(),
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -92,6 +92,7 @@ func TestGraphqlQuery(t *testing.T) {
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
db.Billing(),
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -7,7 +7,6 @@ import (
"context"
"fmt"
"math"
"math/big"
"net/http"
"net/mail"
"sort"
@ -34,6 +33,7 @@ import (
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/monetary"
"storj.io/storj/satellite/rewards"
)
@ -126,6 +126,7 @@ type Service struct {
partners *rewards.PartnersService
accounts payments.Accounts
depositWallets payments.DepositWallets
billing billing.TransactionsDB
registrationCaptchaHandler CaptchaHandler
loginCaptchaHandler CaptchaHandler
analytics *analytics.Service
@ -195,7 +196,7 @@ type Payments struct {
}
// NewService returns new instance of Service.
func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, depositWallets payments.DepositWallets, analytics *analytics.Service, tokens *consoleauth.Service, mailService *mailservice.Service, satelliteAddress string, config Config) (*Service, error) {
func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, depositWallets payments.DepositWallets, billing billing.TransactionsDB, analytics *analytics.Service, tokens *consoleauth.Service, mailService *mailservice.Service, satelliteAddress string, config Config) (*Service, error) {
if store == nil {
return nil, errs.New("store can't be nil")
}
@ -234,6 +235,7 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting
partners: partners,
accounts: accounts,
depositWallets: depositWallets,
billing: billing,
registrationCaptchaHandler: registrationCaptchaHandler,
loginCaptchaHandler: loginCaptchaHandler,
analytics: analytics,
@ -2744,7 +2746,7 @@ func (s *Service) isProjectMember(ctx context.Context, userID uuid.UUID, project
// WalletInfo contains all the information about a destination wallet assigned to a user.
type WalletInfo struct {
Address blockchain.Address `json:"address"`
Balance *big.Int `json:"balance"`
Balance string `json:"balance"`
}
// PaymentInfo includes token payment information required by GUI.
@ -2785,9 +2787,13 @@ func (payment Payments) ClaimWallet(ctx context.Context) (_ WalletInfo, err erro
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
balance, err := payment.service.billing.GetBalance(ctx, user.ID)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
return WalletInfo{
Address: address,
Balance: nil, // TODO: populate with call to billing table
Balance: balance.AsDecimal().String(),
}, nil
}
@ -2803,9 +2809,13 @@ func (payment Payments) GetWallet(ctx context.Context) (_ WalletInfo, err error)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
balance, err := payment.service.billing.GetBalance(ctx, user.ID)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
return WalletInfo{
Address: address,
Balance: nil, // TODO: populate with call to billing table
Balance: balance.AsDecimal().String(),
}, nil
}

View File

@ -8,7 +8,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"math/big"
"sort"
"testing"
"time"
@ -911,13 +910,13 @@ func TestLockAccount(t *testing.T) {
func TestWalletJsonMarshall(t *testing.T) {
wi := console.WalletInfo{
Address: blockchain.Address{1, 2, 3},
Balance: big.NewInt(100),
Balance: monetary.AmountFromBaseUnits(10000, monetary.USDollars).AsDecimal().String(),
}
out, err := json.Marshal(wi)
require.NoError(t, err)
require.Contains(t, string(out), "\"address\":\"0102030000000000000000000000000000000000\"")
require.Contains(t, string(out), "\"balance\":100")
require.Contains(t, string(out), "\"balance\":\"100\"")
}

View File

@ -51,7 +51,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
for _, paymentType := range chore.paymentTypes {
lastTransactionTime, lastTransactionMetadata, err := chore.transactionsDB.LastTransaction(ctx, paymentType.Source(), paymentType.Type())
if err != nil {
if err != nil && !errs.Is(err, ErrNoTransactions) {
chore.log.Error("unable to determine timestamp of last transaction", zap.Error(ChoreErr.Wrap(err)))
continue
}

View File

@ -19,6 +19,12 @@ type TransactionStatus string
// ErrInsufficientFunds represents err when a user balance is too low for some transaction.
var ErrInsufficientFunds = errs.New("Insufficient funds for this transaction")
// ErrNoWallet represents err when there is no wallet in the DB.
var ErrNoWallet = errs.New("no wallet in the database")
// ErrNoTransactions represents err when there is no billing transactions in the DB.
var ErrNoTransactions = errs.New("no transactions in the database")
const (
// TransactionStatusPending indicates that status of this transaction is pending.
TransactionStatusPending = "pending"

View File

@ -55,6 +55,16 @@ func NewService(log *zap.Logger, walletsDB WalletsDB, paymentsDB PaymentsDB, cli
func (service *Service) Claim(ctx context.Context, userID uuid.UUID) (_ blockchain.Address, err error) {
defer mon.Task()(&ctx)(&err)
wallet, err := service.walletsDB.GetWallet(ctx, userID)
switch {
case err == nil:
return wallet, nil
case errs.Is(err, billing.ErrNoWallet):
// do nothing and continue
default:
return blockchain.Address{}, err
}
address, err := service.client.ClaimNewEthAddress(ctx)
if err != nil {
return blockchain.Address{}, ErrService.Wrap(err)
@ -113,11 +123,13 @@ func (service *Service) Type() billing.TransactionType {
return billing.TransactionTypeCredit
}
// GetNewTransactions returns the storjscan payments since the given timestamp as billing transactions type.
// GetNewTransactions returns the storjscan payments since the given block number and index as billing transactions type.
func (service *Service) GetNewTransactions(ctx context.Context, _ time.Time, lastPaymentMetadata []byte) ([]billing.Transaction, error) {
var latestMetadata storjscanMetadata
if err := json.Unmarshal(lastPaymentMetadata, &latestMetadata); err != nil {
if lastPaymentMetadata == nil {
latestMetadata = storjscanMetadata{}
} else if err := json.Unmarshal(lastPaymentMetadata, &latestMetadata); err != nil {
service.log.Error("error retrieving metadata from latest recorded billing payment")
return nil, err
}

View File

@ -12,6 +12,8 @@ import (
"go.uber.org/zap/zaptest"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/blockchain"
"storj.io/storj/satellite"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/monetary"
@ -130,6 +132,53 @@ func TestServicePayments(t *testing.T) {
})
}
func TestServiceWallets(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
userID1 := testrand.UUID()
userID2 := testrand.UUID()
userID3 := testrand.UUID()
walletAddress1, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
walletAddress2, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
walletAddress3, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
err = db.Wallets().Add(ctx, userID1, walletAddress1)
require.NoError(t, err)
err = db.Wallets().Add(ctx, userID2, walletAddress2)
require.NoError(t, err)
err = db.Wallets().Add(ctx, userID3, walletAddress3)
require.NoError(t, err)
service := storjscan.NewService(zaptest.NewLogger(t), db.Wallets(), db.StorjscanPayments(), nil)
t.Run("get Wallet", func(t *testing.T) {
actual, err := service.Get(ctx, userID1)
require.NoError(t, err)
require.Equal(t, walletAddress1, actual)
actual, err = service.Get(ctx, userID2)
require.NoError(t, err)
require.Equal(t, walletAddress2, actual)
actual, err = service.Get(ctx, userID3)
require.NoError(t, err)
require.Equal(t, walletAddress3, actual)
})
t.Run("claim Wallet already assigned", func(t *testing.T) {
actual, err := service.Get(ctx, userID1)
require.NoError(t, err)
require.Equal(t, walletAddress1, actual)
actual, err = service.Claim(ctx, userID1)
require.NoError(t, err)
require.Equal(t, walletAddress1, actual)
})
})
}
func TestListPayments(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
paymentsDB := db.StorjscanPayments()

View File

@ -86,6 +86,7 @@ func TestSignupCouponCodes(t *testing.T) {
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
db.Billing(),
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -135,7 +135,7 @@ func (db billingDB) LastTransaction(ctx context.Context, txSource string, txType
}
if lastTransaction == nil {
return time.Time{}, []byte{}, nil
return time.Time{}, nil, billing.ErrNoTransactions
}
return lastTransaction.Timestamp, lastTransaction.Metadata, nil

View File

@ -5,9 +5,12 @@ package satellitedb
import (
"context"
"database/sql"
"errors"
"storj.io/common/uuid"
"storj.io/storj/private/blockchain"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/satellitedb/dbx"
)
@ -35,6 +38,9 @@ func (walletsDB storjscanWalletsDB) GetWallet(ctx context.Context, userID uuid.U
defer mon.Task()(&ctx)(&err)
wallet, err := walletsDB.db.Get_StorjscanWallet_WalletAddress_By_UserId(ctx, dbx.StorjscanWallet_UserId(userID[:]))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return blockchain.Address{}, billing.ErrNoWallet
}
return blockchain.Address{}, Error.Wrap(err)
}
address, err := blockchain.BytesToAddress(wallet.WalletAddress)