Skip to content

Commit

Permalink
satellite/{payments, console}: added functionality to get wallet's tr…
Browse files Browse the repository at this point in the history
…ansactions (including pending)

Added new functionality to query storjscan for all wallet transactions (including pending).
Added new endpoint to query all wallet transactions.

Issue:
#5978

Change-Id: Id15fddfc9c95efcaa32aa21403cb177f9297e1ab
  • Loading branch information
VitaliiShpital authored and Storj Robot committed Jul 18, 2023
1 parent 2ee0195 commit 583ad54
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 45 deletions.
5 changes: 4 additions & 1 deletion satellite/api.go
Expand Up @@ -547,7 +547,9 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.Payments.StorjscanService = storjscan.NewService(log.Named("storjscan-service"),
peer.DB.Wallets(),
peer.DB.StorjscanPayments(),
peer.Payments.StorjscanClient)
peer.Payments.StorjscanClient,
pc.Storjscan.Confirmations,
pc.BonusRate)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
Expand Down Expand Up @@ -610,6 +612,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
accountFreezeService,
peer.Console.Listener,
config.Payments.StripeCoinPayments.StripePublicKey,
config.Payments.Storjscan.Confirmations,
peer.URL(),
config.Payments.PackagePlans,
)
Expand Down
1 change: 1 addition & 0 deletions satellite/console/consoleweb/config.go
Expand Up @@ -48,6 +48,7 @@ type FrontendConfig struct {
PricingPackagesEnabled bool `json:"pricingPackagesEnabled"`
NewUploadModalEnabled bool `json:"newUploadModalEnabled"`
GalleryViewEnabled bool `json:"galleryViewEnabled"`
NeededTransactionConfirmations int `json:"neededTransactionConfirmations"`
}

// Satellites is a configuration value that contains a list of satellite names and addresses.
Expand Down
24 changes: 24 additions & 0 deletions satellite/console/consoleweb/consoleapi/payments.go
Expand Up @@ -466,6 +466,30 @@ func (p *Payments) WalletPayments(w http.ResponseWriter, r *http.Request) {
}
}

// WalletPaymentsWithConfirmations returns with the list of storjscan transactions (including confirmations count) for user`s wallet.
func (p *Payments) WalletPaymentsWithConfirmations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)

w.Header().Set("Content-Type", "application/json")

walletPayments, err := p.service.Payments().WalletPaymentsWithConfirmations(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}

p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}

if err = json.NewEncoder(w).Encode(walletPayments); err != nil {
p.log.Error("failed to encode wallet payments with confirmations", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}

// GetProjectUsagePriceModel returns the project usage price model for the user.
func (p *Payments) GetProjectUsagePriceModel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand Down
32 changes: 18 additions & 14 deletions satellite/console/consoleweb/server.go
Expand Up @@ -144,7 +144,8 @@ type Server struct {
userIDRateLimiter *web.RateLimiter
nodeURL storj.NodeURL

stripePublicKey string
stripePublicKey string
neededTokenPaymentConfirmations int

packagePlans paymentsconfig.PackagePlans

Expand Down Expand Up @@ -210,20 +211,21 @@ func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {
}

// NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL, packagePlans paymentsconfig.PackagePlans) *Server {
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, neededTokenPaymentConfirmations int, nodeURL storj.NodeURL, packagePlans paymentsconfig.PackagePlans) *Server {
server := Server{
log: logger,
config: config,
listener: listener,
service: service,
mailService: mailService,
analytics: analytics,
abTesting: abTesting,
stripePublicKey: stripePublicKey,
ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger),
userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger),
nodeURL: nodeURL,
packagePlans: packagePlans,
log: logger,
config: config,
listener: listener,
service: service,
mailService: mailService,
analytics: analytics,
abTesting: abTesting,
stripePublicKey: stripePublicKey,
neededTokenPaymentConfirmations: neededTokenPaymentConfirmations,
ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger),
userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger),
nodeURL: nodeURL,
packagePlans: packagePlans,
}

logger.Debug("Starting Satellite Console server.", zap.Stringer("Address", server.listener.Addr()))
Expand Down Expand Up @@ -332,6 +334,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
paymentsRouter.HandleFunc("/wallet", paymentController.GetWallet).Methods(http.MethodGet, http.MethodOptions)
paymentsRouter.HandleFunc("/wallet", paymentController.ClaimWallet).Methods(http.MethodPost, http.MethodOptions)
paymentsRouter.HandleFunc("/wallet/payments", paymentController.WalletPayments).Methods(http.MethodGet, http.MethodOptions)
paymentsRouter.HandleFunc("/wallet/payments-with-confirmations", paymentController.WalletPaymentsWithConfirmations).Methods(http.MethodGet, http.MethodOptions)
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet, http.MethodOptions)
paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch, http.MethodOptions)
paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet, http.MethodOptions)
Expand Down Expand Up @@ -718,6 +721,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
PricingPackagesEnabled: server.config.PricingPackagesEnabled,
NewUploadModalEnabled: server.config.NewUploadModalEnabled,
GalleryViewEnabled: server.config.GalleryViewEnabled,
NeededTransactionConfirmations: server.neededTokenPaymentConfirmations,
}

err := json.NewEncoder(w).Encode(&cfg)
Expand Down
27 changes: 27 additions & 0 deletions satellite/console/service.go
Expand Up @@ -3091,6 +3091,12 @@ func EtherscanURL(tx string) string {
// ErrWalletNotClaimed shows that no address is claimed by the user.
var ErrWalletNotClaimed = errs.Class("wallet is not claimed")

// TestSwapDepositWallets replaces the existing handler for deposit wallets with
// the one specified for use in testing.
func (payment Payments) TestSwapDepositWallets(dw payments.DepositWallets) {
payment.service.depositWallets = dw
}

// ClaimWallet requests a new wallet for the users to be used for payments. If wallet is already claimed,
// it will return with the info without error.
func (payment Payments) ClaimWallet(ctx context.Context) (_ WalletInfo, err error) {
Expand Down Expand Up @@ -3211,6 +3217,27 @@ func (payment Payments) WalletPayments(ctx context.Context) (_ WalletPayments, e
}, nil
}

// WalletPaymentsWithConfirmations returns with all the native blockchain payments (including pending) for a user's wallet.
func (payment Payments) WalletPaymentsWithConfirmations(ctx context.Context) (paymentsWithConfirmations []payments.WalletPaymentWithConfirmations, err error) {
defer mon.Task()(&ctx)(&err)

user, err := GetUser(ctx)
if err != nil {
return nil, Error.Wrap(err)
}
address, err := payment.service.depositWallets.Get(ctx, user.ID)
if err != nil {
return nil, Error.Wrap(err)
}

paymentsWithConfirmations, err = payment.service.depositWallets.PaymentsWithConfirmations(ctx, address)
if err != nil {
return nil, Error.Wrap(err)
}

return
}

// Purchase makes a purchase of `price` amount with description of `desc` and payment method with id of `paymentMethodID`.
// If a paid invoice with the same description exists, then we assume this is a retried request and don't create and pay
// another invoice.
Expand Down
80 changes: 80 additions & 0 deletions satellite/console/service_test.go
Expand Up @@ -1702,6 +1702,86 @@ func TestPaymentsWalletPayments(t *testing.T) {
})
}

type mockDepositWallets struct {
address blockchain.Address
payments []payments.WalletPaymentWithConfirmations
}

func (dw mockDepositWallets) Claim(_ context.Context, _ uuid.UUID) (blockchain.Address, error) {
return dw.address, nil
}

func (dw mockDepositWallets) Get(_ context.Context, _ uuid.UUID) (blockchain.Address, error) {
return dw.address, nil
}

func (dw mockDepositWallets) Payments(_ context.Context, _ blockchain.Address, _ int, _ int64) (p []payments.WalletPayment, err error) {
return
}

func (dw mockDepositWallets) PaymentsWithConfirmations(_ context.Context, _ blockchain.Address) ([]payments.WalletPaymentWithConfirmations, error) {
return dw.payments, nil
}

func TestWalletPaymentsWithConfirmations(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
paymentsService := service.Payments()

user, err := sat.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "test@mail.test",
Password: "example",
}, 1)
require.NoError(t, err)

now := time.Now()
wallet := blockchaintest.NewAddress()

var expected []payments.WalletPaymentWithConfirmations
for i := 0; i < 3; i++ {
expected = append(expected, payments.WalletPaymentWithConfirmations{
From: blockchaintest.NewAddress().Hex(),
To: wallet.Hex(),
TokenValue: currency.AmountFromBaseUnits(int64(i), currency.StorjToken).AsDecimal(),
USDValue: currency.AmountFromBaseUnits(int64(i), currency.USDollarsMicro).AsDecimal(),
Status: payments.PaymentStatusConfirmed,
BlockHash: blockchaintest.NewHash().Hex(),
BlockNumber: int64(i),
Transaction: blockchaintest.NewHash().Hex(),
LogIndex: i,
Timestamp: now,
Confirmations: int64(i),
BonusTokens: decimal.NewFromInt(int64(i)),
})
}

paymentsService.TestSwapDepositWallets(mockDepositWallets{address: wallet, payments: expected})

reqCtx := console.WithUser(ctx, user)

walletPayments, err := paymentsService.WalletPaymentsWithConfirmations(reqCtx)
require.NoError(t, err)
require.NotZero(t, len(walletPayments))

for i, wp := range walletPayments {
require.Equal(t, expected[i].From, wp.From)
require.Equal(t, expected[i].To, wp.To)
require.Equal(t, expected[i].TokenValue, wp.TokenValue)
require.Equal(t, expected[i].USDValue, wp.USDValue)
require.Equal(t, expected[i].Status, wp.Status)
require.Equal(t, expected[i].BlockHash, wp.BlockHash)
require.Equal(t, expected[i].BlockNumber, wp.BlockNumber)
require.Equal(t, expected[i].Transaction, wp.Transaction)
require.Equal(t, expected[i].LogIndex, wp.LogIndex)
require.Equal(t, expected[i].Timestamp, wp.Timestamp)
}
})
}

func TestPaymentsPurchase(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
Expand Down
4 changes: 3 additions & 1 deletion satellite/core.go
Expand Up @@ -496,7 +496,9 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.Payments.StorjscanService = storjscan.NewService(log.Named("storjscan-service"),
peer.DB.Wallets(),
peer.DB.StorjscanPayments(),
peer.Payments.StorjscanClient)
peer.Payments.StorjscanClient,
pc.Storjscan.Confirmations,
pc.BonusRate)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
Expand Down
13 changes: 7 additions & 6 deletions satellite/payments/billing/transactions.go
Expand Up @@ -105,6 +105,12 @@ type Transaction struct {
CreatedAt time.Time
}

// CalculateBonusAmount calculates bonus for given currency amount and bonus rate.
func CalculateBonusAmount(amount currency.Amount, bonusRate int64) currency.Amount {
bonusUnits := amount.BaseUnits() * bonusRate / 100
return currency.AmountFromBaseUnits(bonusUnits, amount.Currency())
}

func prepareBonusTransaction(bonusRate int64, source string, transaction Transaction) (Transaction, bool) {
// Bonus transactions only apply when enabled (i.e. positive rate) and
// for StorjScan transactions.
Expand All @@ -120,7 +126,7 @@ func prepareBonusTransaction(bonusRate int64, source string, transaction Transac

return Transaction{
UserID: transaction.UserID,
Amount: calculateBonusAmount(transaction.Amount, bonusRate),
Amount: CalculateBonusAmount(transaction.Amount, bonusRate),
Description: fmt.Sprintf("STORJ Token Bonus (%d%%)", bonusRate),
Source: StorjScanBonusSource,
Status: TransactionStatusCompleted,
Expand All @@ -129,8 +135,3 @@ func prepareBonusTransaction(bonusRate int64, source string, transaction Transac
Metadata: append([]byte(nil), transaction.Metadata...),
}, true
}

func calculateBonusAmount(amount currency.Amount, bonusRate int64) currency.Amount {
bonusUnits := amount.BaseUnits() * bonusRate / 100
return currency.AmountFromBaseUnits(bonusUnits, amount.Currency())
}
2 changes: 1 addition & 1 deletion satellite/payments/storjscan/chore.go
Expand Up @@ -65,7 +65,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
return nil
}

latestPayments, err := chore.client.Payments(ctx, from)
latestPayments, err := chore.client.AllPayments(ctx, from)
if err != nil {
chore.log.Error("error retrieving payments", zap.Error(ChoreErr.Wrap(err)))
return nil
Expand Down
23 changes: 20 additions & 3 deletions satellite/payments/storjscan/client.go
Expand Up @@ -67,13 +67,30 @@ func NewClient(endpoint, identifier, secret string) *Client {
}
}

// Payments retrieves all payments after specified block for wallets associated with particular API key.
func (client *Client) Payments(ctx context.Context, from int64) (_ LatestPayments, err error) {
// AllPayments retrieves all payments after specified block for wallets associated with particular API key.
func (client *Client) AllPayments(ctx context.Context, from int64) (payments LatestPayments, err error) {
defer mon.Task()(&ctx)(&err)

p := client.endpoint + "/api/v0/tokens/payments"

req, err := http.NewRequestWithContext(ctx, http.MethodGet, p, nil)
payments, err = client.getPayments(ctx, p, from)

return
}

// Payments retrieves payments after specified block for given address associated with particular API key.
func (client *Client) Payments(ctx context.Context, from int64, address string) (payments LatestPayments, err error) {
defer mon.Task()(&ctx)(&err)

p := client.endpoint + "/api/v0/tokens/payments/" + address

payments, err = client.getPayments(ctx, p, from)

return
}

func (client *Client) getPayments(ctx context.Context, path string, from int64) (_ LatestPayments, err error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil)
if err != nil {
return LatestPayments{}, ClientErr.Wrap(err)
}
Expand Down
14 changes: 7 additions & 7 deletions satellite/payments/storjscan/client_test.go
Expand Up @@ -69,17 +69,17 @@ func TestClientMocked(t *testing.T) {
}))
defer server.Close()

client := storjscan.NewClient(server.URL, "eu", "secret")
client := storjscan.NewClient(server.URL, identifier, secret)

t.Run("all payments from 0", func(t *testing.T) {
actual, err := client.Payments(ctx, 0)
actual, err := client.AllPayments(ctx, 0)
require.NoError(t, err)
require.Equal(t, latestBlock, actual.LatestBlock)
require.Equal(t, len(payments), len(actual.Payments))
require.Equal(t, payments, actual.Payments)
})
t.Run("payments from 50", func(t *testing.T) {
actual, err := client.Payments(ctx, 50)
t.Run("all payments from 50", func(t *testing.T) {
actual, err := client.AllPayments(ctx, 50)
require.NoError(t, err)
require.Equal(t, latestBlock, actual.LatestBlock)
require.Equal(t, 50, len(actual.Payments))
Expand All @@ -104,23 +104,23 @@ func TestClientMockedUnauthorized(t *testing.T) {

t.Run("empty credentials", func(t *testing.T) {
client := storjscan.NewClient(server.URL, "", "")
_, err := client.Payments(ctx, 0)
_, err := client.AllPayments(ctx, 0)
require.Error(t, err)
require.True(t, storjscan.ClientErrUnauthorized.Has(err))
require.Equal(t, "identifier is invalid", errs.Unwrap(err).Error())
})

t.Run("invalid identifier", func(t *testing.T) {
client := storjscan.NewClient(server.URL, "invalid", "secret")
_, err := client.Payments(ctx, 0)
_, err := client.AllPayments(ctx, 0)
require.Error(t, err)
require.True(t, storjscan.ClientErrUnauthorized.Has(err))
require.Equal(t, "identifier is invalid", errs.Unwrap(err).Error())
})

t.Run("invalid secret", func(t *testing.T) {
client := storjscan.NewClient(server.URL, "eu", "invalid")
_, err := client.Payments(ctx, 0)
_, err := client.AllPayments(ctx, 0)
require.Error(t, err)
require.True(t, storjscan.ClientErrUnauthorized.Has(err))
require.Equal(t, "secret is invalid", errs.Unwrap(err).Error())
Expand Down

0 comments on commit 583ad54

Please sign in to comment.