satellite/{payments/storjscan,satellitedb}: Add wallet implementation

Add storjscan wallets implementation to the satellite. The wallets interface allows you to add and claim new wallets as called by the API. The storjscan specific implementation of this interface uses a wallets DB to associate the user to a wallet address, as well as a storjscan client to request and associate new wallets to the satellite.

Change-Id: I54081edb5545d4e3ee07cf1cce3d3e87cc00c4a1
This commit is contained in:
dlamarmorgan 2022-04-27 19:54:56 -07:00 committed by Storj Robot
parent cbaca8b17e
commit 270204f352
14 changed files with 276 additions and 91 deletions

View File

@ -539,10 +539,10 @@ func TestProjectCheckUsage_lastMonthUnappliedInvoice(t *testing.T) {
err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally)
require.NoError(t, err)
planet.Satellites[0].API.Payments.Service.SetNow(func() time.Time {
planet.Satellites[0].API.Payments.StripeService.SetNow(func() time.Time {
return oneMonthAhead
})
err = planet.Satellites[0].API.Payments.Service.PrepareInvoiceProjectRecords(ctx, now)
err = planet.Satellites[0].API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, now)
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://"+address.String()+"/api/projects/%s/usage", projectID), nil)

View File

@ -48,6 +48,7 @@ import (
"storj.io/storj/satellite/overlay"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/paymentsconfig"
"storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/reputation"
"storj.io/storj/satellite/rewards"
@ -128,9 +129,14 @@ type API struct {
Payments struct {
Accounts payments.Accounts
DepositWallets payments.DepositWallets
StorjscanService *storjscan.Service
StorjscanClient *storjscan.Client
Conversion *stripecoinpayments.ConversionService
Service *stripecoinpayments.Service
Stripe stripecoinpayments.StripeClient
StripeService *stripecoinpayments.Service
StripeClient stripecoinpayments.StripeClient
}
REST struct {
@ -485,7 +491,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
}
peer.Payments.Service, err = stripecoinpayments.NewService(
peer.Payments.StripeService, err = stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"),
stripeClient,
pc.StripeCoinPayments,
@ -501,11 +507,11 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.Stripe = stripeClient
peer.Payments.Accounts = peer.Payments.Service.Accounts()
peer.Payments.StripeClient = stripeClient
peer.Payments.Accounts = peer.Payments.StripeService.Accounts()
peer.Payments.Conversion = stripecoinpayments.NewConversionService(
peer.Log.Named("payments.stripe:version"),
peer.Payments.Service,
peer.Payments.StripeService,
pc.StripeCoinPayments.ConversionRatesCycleInterval)
peer.Services.Add(lifecycle.Item{
@ -513,6 +519,16 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
Run: peer.Payments.Conversion.Run,
Close: peer.Payments.Conversion.Close,
})
peer.Payments.StorjscanClient = storjscan.NewClient(
pc.Storjscan.Endpoint,
pc.Storjscan.Credentials.PublicKey,
pc.Storjscan.Credentials.PrivateKey)
peer.Payments.StorjscanService, err = storjscan.NewService(peer.DB, peer.Payments.StorjscanClient)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.DepositWallets = peer.Payments.StorjscanService
}
{ // setup account management api keys
@ -540,6 +556,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.Buckets.Service,
peer.Marketing.PartnersService,
peer.Payments.Accounts,
peer.Payments.DepositWallets,
peer.Analytics.Service,
peer.Console.AuthTokens,
consoleConfig.Config,

View File

@ -104,6 +104,8 @@ func TestGraphqlMutation(t *testing.T) {
sat.API.Buckets.Service,
partnersService,
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -88,6 +88,8 @@ func TestGraphqlQuery(t *testing.T) {
sat.API.Buckets.Service,
partnersService,
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -123,6 +123,7 @@ type Service struct {
buckets Buckets
partners *rewards.PartnersService
accounts payments.Accounts
depositWallets payments.DepositWallets
captchaHandler CaptchaHandler
analytics *analytics.Service
tokens *consoleauth.Service
@ -169,13 +170,13 @@ type HcaptchaConfig struct {
SecretKey string `help:"hCaptcha secret key"`
}
// PaymentsService separates all payment related functionality.
type PaymentsService struct {
// Payments separates all payment related functionality.
type Payments struct {
service *Service
}
// 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, analytics *analytics.Service, tokens *consoleauth.Service, 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, analytics *analytics.Service, tokens *consoleauth.Service, config Config) (*Service, error) {
if store == nil {
return nil, errs.New("store can't be nil")
}
@ -203,6 +204,7 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting
buckets: buckets,
partners: partners,
accounts: accounts,
depositWallets: depositWallets,
captchaHandler: captchaHandler,
analytics: analytics,
tokens: tokens,
@ -254,79 +256,79 @@ func (s *Service) getAuthAndAuditLog(ctx context.Context, operation string, extr
}
// Payments separates all payment related functionality.
func (s *Service) Payments() PaymentsService {
return PaymentsService{service: s}
func (s *Service) Payments() Payments {
return Payments{service: s}
}
// SetupAccount creates payment account for authorized user.
func (paymentService PaymentsService) SetupAccount(ctx context.Context) (_ payments.CouponType, err error) {
func (payment Payments) SetupAccount(ctx context.Context) (_ payments.CouponType, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "setup payment account")
auth, err := payment.service.getAuthAndAuditLog(ctx, "setup payment account")
if err != nil {
return payments.NoCoupon, Error.Wrap(err)
}
return paymentService.service.accounts.Setup(ctx, auth.User.ID, auth.User.Email, auth.User.SignupPromoCode)
return payment.service.accounts.Setup(ctx, auth.User.ID, auth.User.Email, auth.User.SignupPromoCode)
}
// AccountBalance return account balance.
func (paymentService PaymentsService) AccountBalance(ctx context.Context) (balance payments.Balance, err error) {
func (payment Payments) AccountBalance(ctx context.Context) (balance payments.Balance, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get account balance")
auth, err := payment.service.getAuthAndAuditLog(ctx, "get account balance")
if err != nil {
return payments.Balance{}, Error.Wrap(err)
}
return paymentService.service.accounts.Balance(ctx, auth.User.ID)
return payment.service.accounts.Balance(ctx, auth.User.ID)
}
// AddCreditCard is used to save new credit card and attach it to payment account.
func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditCardToken string) (err error) {
func (payment Payments) AddCreditCard(ctx context.Context, creditCardToken string) (err error) {
defer mon.Task()(&ctx, creditCardToken)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "add credit card")
auth, err := payment.service.getAuthAndAuditLog(ctx, "add credit card")
if err != nil {
return Error.Wrap(err)
}
err = paymentService.service.accounts.CreditCards().Add(ctx, auth.User.ID, creditCardToken)
err = payment.service.accounts.CreditCards().Add(ctx, auth.User.ID, creditCardToken)
if err != nil {
return Error.Wrap(err)
}
paymentService.service.analytics.TrackCreditCardAdded(auth.User.ID, auth.User.Email)
payment.service.analytics.TrackCreditCardAdded(auth.User.ID, auth.User.Email)
if !auth.User.PaidTier {
// put this user into the paid tier and convert projects to upgraded limits.
err = paymentService.service.store.Users().UpdatePaidTier(ctx, auth.User.ID, true,
paymentService.service.config.UsageLimits.Bandwidth.Paid,
paymentService.service.config.UsageLimits.Storage.Paid,
paymentService.service.config.UsageLimits.Segment.Paid,
paymentService.service.config.UsageLimits.Project.Paid,
err = payment.service.store.Users().UpdatePaidTier(ctx, auth.User.ID, true,
payment.service.config.UsageLimits.Bandwidth.Paid,
payment.service.config.UsageLimits.Storage.Paid,
payment.service.config.UsageLimits.Segment.Paid,
payment.service.config.UsageLimits.Project.Paid,
)
if err != nil {
return Error.Wrap(err)
}
projects, err := paymentService.service.store.Projects().GetOwn(ctx, auth.User.ID)
projects, err := payment.service.store.Projects().GetOwn(ctx, auth.User.ID)
if err != nil {
return Error.Wrap(err)
}
for _, project := range projects {
if project.StorageLimit == nil || *project.StorageLimit < paymentService.service.config.UsageLimits.Storage.Paid {
if project.StorageLimit == nil || *project.StorageLimit < payment.service.config.UsageLimits.Storage.Paid {
project.StorageLimit = new(memory.Size)
*project.StorageLimit = paymentService.service.config.UsageLimits.Storage.Paid
*project.StorageLimit = payment.service.config.UsageLimits.Storage.Paid
}
if project.BandwidthLimit == nil || *project.BandwidthLimit < paymentService.service.config.UsageLimits.Bandwidth.Paid {
if project.BandwidthLimit == nil || *project.BandwidthLimit < payment.service.config.UsageLimits.Bandwidth.Paid {
project.BandwidthLimit = new(memory.Size)
*project.BandwidthLimit = paymentService.service.config.UsageLimits.Bandwidth.Paid
*project.BandwidthLimit = payment.service.config.UsageLimits.Bandwidth.Paid
}
if project.SegmentLimit == nil || *project.SegmentLimit < paymentService.service.config.UsageLimits.Segment.Paid {
*project.SegmentLimit = paymentService.service.config.UsageLimits.Segment.Paid
if project.SegmentLimit == nil || *project.SegmentLimit < payment.service.config.UsageLimits.Segment.Paid {
*project.SegmentLimit = payment.service.config.UsageLimits.Segment.Paid
}
err = paymentService.service.store.Projects().Update(ctx, &project)
err = payment.service.store.Projects().Update(ctx, &project)
if err != nil {
return Error.Wrap(err)
}
@ -337,63 +339,63 @@ func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditC
}
// MakeCreditCardDefault makes a credit card default payment method.
func (paymentService PaymentsService) MakeCreditCardDefault(ctx context.Context, cardID string) (err error) {
func (payment Payments) MakeCreditCardDefault(ctx context.Context, cardID string) (err error) {
defer mon.Task()(&ctx, cardID)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "make credit card default")
auth, err := payment.service.getAuthAndAuditLog(ctx, "make credit card default")
if err != nil {
return Error.Wrap(err)
}
return paymentService.service.accounts.CreditCards().MakeDefault(ctx, auth.User.ID, cardID)
return payment.service.accounts.CreditCards().MakeDefault(ctx, auth.User.ID, cardID)
}
// ProjectsCharges returns how much money current user will be charged for each project which he owns.
func (paymentService PaymentsService) ProjectsCharges(ctx context.Context, since, before time.Time) (_ []payments.ProjectCharge, err error) {
func (payment Payments) ProjectsCharges(ctx context.Context, since, before time.Time) (_ []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "project charges")
auth, err := payment.service.getAuthAndAuditLog(ctx, "project charges")
if err != nil {
return nil, Error.Wrap(err)
}
return paymentService.service.accounts.ProjectCharges(ctx, auth.User.ID, since, before)
return payment.service.accounts.ProjectCharges(ctx, auth.User.ID, since, before)
}
// ListCreditCards returns a list of credit cards for a given payment account.
func (paymentService PaymentsService) ListCreditCards(ctx context.Context) (_ []payments.CreditCard, err error) {
func (payment Payments) ListCreditCards(ctx context.Context) (_ []payments.CreditCard, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "list credit cards")
auth, err := payment.service.getAuthAndAuditLog(ctx, "list credit cards")
if err != nil {
return nil, Error.Wrap(err)
}
return paymentService.service.accounts.CreditCards().List(ctx, auth.User.ID)
return payment.service.accounts.CreditCards().List(ctx, auth.User.ID)
}
// RemoveCreditCard is used to detach a credit card from payment account.
func (paymentService PaymentsService) RemoveCreditCard(ctx context.Context, cardID string) (err error) {
func (payment Payments) RemoveCreditCard(ctx context.Context, cardID string) (err error) {
defer mon.Task()(&ctx, cardID)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "remove credit card")
auth, err := payment.service.getAuthAndAuditLog(ctx, "remove credit card")
if err != nil {
return Error.Wrap(err)
}
return paymentService.service.accounts.CreditCards().Remove(ctx, auth.User.ID, cardID)
return payment.service.accounts.CreditCards().Remove(ctx, auth.User.ID, cardID)
}
// BillingHistory returns a list of billing history items for payment account.
func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
func (payment Payments) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get billing history")
auth, err := payment.service.getAuthAndAuditLog(ctx, "get billing history")
if err != nil {
return nil, Error.Wrap(err)
}
invoices, couponUsages, err := paymentService.service.accounts.Invoices().ListWithDiscounts(ctx, auth.User.ID)
invoices, couponUsages, err := payment.service.accounts.Invoices().ListWithDiscounts(ctx, auth.User.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -411,7 +413,7 @@ func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billi
})
}
txsInfos, err := paymentService.service.accounts.StorjTokens().ListTransactionInfos(ctx, auth.User.ID)
txsInfos, err := payment.service.accounts.StorjTokens().ListTransactionInfos(ctx, auth.User.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -430,7 +432,7 @@ func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billi
})
}
charges, err := paymentService.service.accounts.Charges(ctx, auth.User.ID)
charges, err := payment.service.accounts.Charges(ctx, auth.User.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -465,7 +467,7 @@ func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billi
})
}
bonuses, err := paymentService.service.accounts.StorjTokens().ListDepositBonuses(ctx, auth.User.ID)
bonuses, err := payment.service.accounts.StorjTokens().ListDepositBonuses(ctx, auth.User.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -492,29 +494,29 @@ func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billi
}
// TokenDeposit creates new deposit transaction for adding STORJ tokens to account balance.
func (paymentService PaymentsService) TokenDeposit(ctx context.Context, amount int64) (_ *payments.Transaction, err error) {
func (payment Payments) TokenDeposit(ctx context.Context, amount int64) (_ *payments.Transaction, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "token deposit")
auth, err := payment.service.getAuthAndAuditLog(ctx, "token deposit")
if err != nil {
return nil, Error.Wrap(err)
}
tx, err := paymentService.service.accounts.StorjTokens().Deposit(ctx, auth.User.ID, amount)
tx, err := payment.service.accounts.StorjTokens().Deposit(ctx, auth.User.ID, amount)
return tx, Error.Wrap(err)
}
// checkOutstandingInvoice returns if the payment account has any unpaid/outstanding invoices or/and invoice items.
func (paymentService PaymentsService) checkOutstandingInvoice(ctx context.Context) (err error) {
func (payment Payments) checkOutstandingInvoice(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get outstanding invoices")
auth, err := payment.service.getAuthAndAuditLog(ctx, "get outstanding invoices")
if err != nil {
return err
}
invoices, err := paymentService.service.accounts.Invoices().List(ctx, auth.User.ID)
invoices, err := payment.service.accounts.Invoices().List(ctx, auth.User.ID)
if err != nil {
return err
}
@ -526,7 +528,7 @@ func (paymentService PaymentsService) checkOutstandingInvoice(ctx context.Contex
}
}
hasItems, err := paymentService.service.accounts.Invoices().CheckPendingItems(ctx, auth.User.ID)
hasItems, err := payment.service.accounts.Invoices().CheckPendingItems(ctx, auth.User.ID)
if err != nil {
return err
}
@ -538,28 +540,28 @@ func (paymentService PaymentsService) checkOutstandingInvoice(ctx context.Contex
// checkProjectInvoicingStatus returns if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe).
func (paymentService PaymentsService) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) {
func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) {
defer mon.Task()(&ctx)(&err)
_, err = paymentService.service.getAuthAndAuditLog(ctx, "project charges")
_, err = payment.service.getAuthAndAuditLog(ctx, "project charges")
if err != nil {
return false, Error.Wrap(err)
}
return paymentService.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
return payment.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
}
// ApplyCouponCode applies a coupon code to a Stripe customer
// and returns the coupon corresponding to the code.
func (paymentService PaymentsService) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) {
func (payment Payments) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "apply coupon code")
auth, err := payment.service.getAuthAndAuditLog(ctx, "apply coupon code")
if err != nil {
return nil, Error.Wrap(err)
}
coupon, err = paymentService.service.accounts.Coupons().ApplyCouponCode(ctx, auth.User.ID, couponCode)
coupon, err = payment.service.accounts.Coupons().ApplyCouponCode(ctx, auth.User.ID, couponCode)
if err != nil {
return nil, Error.Wrap(err)
}
@ -568,15 +570,15 @@ func (paymentService PaymentsService) ApplyCouponCode(ctx context.Context, coupo
}
// GetCoupon returns the coupon applied to the user's account.
func (paymentService PaymentsService) GetCoupon(ctx context.Context) (coupon *payments.Coupon, err error) {
func (payment Payments) GetCoupon(ctx context.Context) (coupon *payments.Coupon, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get coupon")
auth, err := payment.service.getAuthAndAuditLog(ctx, "get coupon")
if err != nil {
return nil, Error.Wrap(err)
}
coupon, err = paymentService.service.accounts.Coupons().GetByUserID(ctx, auth.User.ID)
coupon, err = payment.service.accounts.Coupons().GetByUserID(ctx, auth.User.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -2601,17 +2603,43 @@ var ErrWalletNotClaimed = errs.Class("wallet is not claimed")
// 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 (paymentService PaymentsService) ClaimWallet(ctx context.Context) (WalletInfo, error) {
panic("Not yet implemented")
func (payment Payments) ClaimWallet(ctx context.Context) (_ WalletInfo, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := payment.service.getAuthAndAuditLog(ctx, "claim wallet")
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
address, err := payment.service.depositWallets.Claim(ctx, auth.User.ID)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
return WalletInfo{
Address: address,
Balance: nil, //TODO: populate with call to billing table
}, nil
}
// GetWallet returns with the assigned wallet, or with ErrWalletNotClaimed if not yet claimed.
func (paymentService PaymentsService) GetWallet(ctx context.Context) (WalletInfo, error) {
panic("Not yet implemented")
func (payment Payments) GetWallet(ctx context.Context) (_ WalletInfo, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := GetAuth(ctx)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
address, err := payment.service.depositWallets.Get(ctx, auth.User.ID)
if err != nil {
return WalletInfo{}, Error.Wrap(err)
}
return WalletInfo{
Address: address,
Balance: nil, //TODO: populate with call to billing table
}, nil
}
// Transactions returns with all the native blockchain transactions.
func (paymentService PaymentsService) Transactions(ctx context.Context) (TokenTransactions, error) {
func (payment Payments) Transactions(ctx context.Context) (TokenTransactions, error) {
panic("Not yet implemented")
}

View File

@ -4,6 +4,7 @@
package paymentsconfig
import (
"storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
@ -11,6 +12,7 @@ import (
type Config struct {
Provider string `help:"payments provider to use" default:""`
StripeCoinPayments stripecoinpayments.Config
Storjscan storjscan.Config
StorageTBPrice string `help:"price user should pay for storing TB per month" default:"4" testDefault:"10"`
EgressTBPrice string `help:"price user should pay for each TB of egress" default:"7" testDefault:"45"`
SegmentPrice string `help:"price user should pay for each segment stored in network per month" default:"0.0000088" testDefault:"0.0000022"`

View File

@ -114,3 +114,54 @@ func (client *Client) Payments(ctx context.Context, from int64) (_ LatestPayment
return payments, nil
}
// ClaimNewEthAddress claims a new ethereum wallet address for the given user.
func (client *Client) ClaimNewEthAddress(ctx context.Context) (_ blockchain.Address, err error) {
defer mon.Task()(&ctx)(&err)
p := client.endpoint + "/api/v0/wallets/claim"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p, nil)
if err != nil {
return blockchain.Address{}, ClientErr.Wrap(err)
}
req.SetBasicAuth(client.identifier, client.secret)
resp, err := client.http.Do(req)
if err != nil {
return blockchain.Address{}, ClientErr.Wrap(err)
}
defer func() {
err = errs.Combine(err, ClientErr.Wrap(resp.Body.Close()))
}()
if resp.StatusCode != http.StatusOK {
var data struct {
Error string `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return blockchain.Address{}, ClientErr.Wrap(err)
}
switch resp.StatusCode {
case http.StatusUnauthorized:
return blockchain.Address{}, ClientErrUnauthorized.New("%s", data.Error)
default:
return blockchain.Address{}, ClientErr.New("%s", data.Error)
}
}
var addressHex string
if err = json.NewDecoder(resp.Body).Decode(&addressHex); err != nil {
return blockchain.Address{}, ClientErr.Wrap(err)
}
var address blockchain.Address
if err = address.UnmarshalJSON([]byte(addressHex)); err != nil {
return blockchain.Address{}, ClientErr.Wrap(err)
}
return address, nil
}

View File

@ -0,0 +1,64 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package storjscan
import (
"context"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/private/blockchain"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/coinpayments"
)
var (
// Error defines storjscan service error.
Error = errs.Class("storjscan service")
)
// ensures that Wallets implements payments.Wallets.
var _ payments.DepositWallets = (*Service)(nil)
// Config stores needed information for storjscan service initialization.
type Config struct {
Credentials coinpayments.Credentials
Endpoint string
}
// Service is an implementation for payment service via Stripe and Coinpayments.
//
// architecture: Service
type Service struct {
walletsDB WalletsDB
storjscanClient *Client
}
// NewService creates a Service instance.
func NewService(db DB, storjscanClient *Client) (*Service, error) {
return &Service{
walletsDB: db.Wallets(),
storjscanClient: storjscanClient,
}, nil
}
// Claim gets a new crypto wallet and associates it with a user.
func (service *Service) Claim(ctx context.Context, userID uuid.UUID) (_ blockchain.Address, err error) {
defer mon.Task()(&ctx)(&err)
address, err := service.storjscanClient.ClaimNewEthAddress(ctx)
if err != nil {
return blockchain.Address{}, err
}
err = service.walletsDB.Add(ctx, userID, address)
return address, err
}
// Get returns the crypto wallet address associated with the given user.
func (service *Service) Get(ctx context.Context, userID uuid.UUID) (_ blockchain.Address, err error) {
defer mon.Task()(&ctx)(&err)
return service.walletsDB.Get(ctx, userID)
}

View File

@ -83,6 +83,8 @@ func TestSignupCouponCodes(t *testing.T) {
sat.API.Buckets.Service,
partnersService,
paymentsService.Accounts(),
// TODO: do we need a payment deposit wallet here?
nil,
analyticsService,
consoleauth.NewService(consoleauth.Config{
TokenExpirationTime: 24 * time.Hour,

View File

@ -54,10 +54,10 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
require.NoError(t, err)
}
satellite.API.Payments.Service.SetNow(func() time.Time {
satellite.API.Payments.StripeService.SetNow(func() time.Time {
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
})
err := satellite.API.Payments.Service.PrepareInvoiceProjectRecords(ctx, period)
err := satellite.API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
require.NoError(t, err)
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
@ -68,7 +68,7 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
require.NoError(t, err)
require.Equal(t, numberOfProjects, len(recordsPage.Records))
err = satellite.API.Payments.Service.InvoiceApplyProjectRecords(ctx, period)
err = satellite.API.Payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
require.NoError(t, err)
// verify that we applied all unapplied project records
@ -94,7 +94,7 @@ func TestService_InvoiceUserWithManyProjects(t *testing.T) {
// keep month + 1 because user needs to be created before calculation
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
payments.Service.SetNow(func() time.Time {
payments.StripeService.SetNow(func() time.Time {
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
})
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
@ -148,7 +148,7 @@ func TestService_InvoiceUserWithManyProjects(t *testing.T) {
require.Nil(t, projectRecord)
}
err = payments.Service.PrepareInvoiceProjectRecords(ctx, period)
err = payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
require.NoError(t, err)
for i := 0; i < len(projects); i++ {
@ -166,10 +166,10 @@ func TestService_InvoiceUserWithManyProjects(t *testing.T) {
}
// run all parts of invoice generation to see if there are no unexpected errors
err = payments.Service.InvoiceApplyProjectRecords(ctx, period)
err = payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
require.NoError(t, err)
err = payments.Service.CreateInvoices(ctx, period)
err = payments.StripeService.CreateInvoices(ctx, period)
require.NoError(t, err)
})
}
@ -215,10 +215,10 @@ func TestService_ProjectsWithMembers(t *testing.T) {
}
}
satellite.API.Payments.Service.SetNow(func() time.Time {
satellite.API.Payments.StripeService.SetNow(func() time.Time {
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
})
err := satellite.API.Payments.Service.PrepareInvoiceProjectRecords(ctx, period)
err := satellite.API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
require.NoError(t, err)
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
@ -281,7 +281,7 @@ func TestService_InvoiceItemsFromProjectRecord(t *testing.T) {
Segments: tc.Segments,
}
items := satellite.API.Payments.Service.InvoiceItemsFromProjectRecord("project name", record)
items := satellite.API.Payments.StripeService.InvoiceItemsFromProjectRecord("project name", record)
require.Equal(t, tc.StorageQuantity, *items[0].Quantity)
require.Equal(t, expectedStoragePrice, *items[0].UnitAmountDecimal)

View File

@ -45,7 +45,7 @@ func TestTokens_ListDepositBonuses(t *testing.T) {
Description: stripe.String(stripecoinpayments.StripeDepositTransactionDescription),
}
txParams.AddMetadata("txID", txID)
_, err = satellite.API.Payments.Stripe.CustomerBalanceTransactions().New(&txParams)
_, err = satellite.API.Payments.StripeClient.CustomerBalanceTransactions().New(&txParams)
require.NoError(t, err)
// Credit the Stripe balance with a 13% bonus - $1.30
@ -56,7 +56,7 @@ func TestTokens_ListDepositBonuses(t *testing.T) {
}
txParams.AddMetadata("txID", txID)
txParams.AddMetadata("percentage", "13")
bonusTx, err := satellite.API.Payments.Stripe.CustomerBalanceTransactions().New(&txParams)
bonusTx, err := satellite.API.Payments.StripeClient.CustomerBalanceTransactions().New(&txParams)
require.NoError(t, err)
// Add migrated deposit bonus to Stripe metadata
@ -72,7 +72,7 @@ func TestTokens_ListDepositBonuses(t *testing.T) {
customerParams.Metadata = map[string]string{
"credit_" + credit.TransactionID.String(): string(b),
}
_, err = satellite.API.Payments.Stripe.Customers().Update(customerID, &customerParams)
_, err = satellite.API.Payments.StripeClient.Customers().Update(customerID, &customerParams)
require.NoError(t, err)
// Expect the list to contain the 10% bonus from Stripe metadata and 13% bonus from Stripe balance

View File

@ -412,7 +412,7 @@ func TestTransactions_ApplyTransactionBalance(t *testing.T) {
require.NoError(t, err)
// Check that the CoinPayments deposit is reflected in the Stripe customer balance.
it := satellite.API.Payments.Stripe.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(cusID)})
it := satellite.API.Payments.StripeClient.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(cusID)})
require.NoError(t, it.Err())
require.True(t, it.Next())
cbt := it.CustomerBalanceTransaction()

View File

@ -10,6 +10,7 @@ import (
"github.com/shopspring/decimal"
"storj.io/common/uuid"
"storj.io/storj/private/blockchain"
"storj.io/storj/satellite/payments/monetary"
)
@ -25,6 +26,16 @@ type StorjTokens interface {
ListDepositBonuses(ctx context.Context, userID uuid.UUID) ([]DepositBonus, error)
}
// DepositWallets exposes all needed functionality to manage token deposit wallets.
//
// architecture: Service
type DepositWallets interface {
// Claim gets a new crypto wallet and associates it with a user.
Claim(ctx context.Context, userID uuid.UUID) (blockchain.Address, error)
// Get returns the crypto wallet address associated with the given user.
Get(ctx context.Context, userID uuid.UUID) (blockchain.Address, error)
}
// TransactionStatus defines allowed statuses
// for deposit transactions.
type TransactionStatus string

View File

@ -664,6 +664,12 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# price user should pay for storing TB per month
# payments.storage-tb-price: "4"
# payments.storjscan.credentials.private-key: ""
# payments.storjscan.credentials.public-key: ""
# payments.storjscan.endpoint: ""
# amount of time we wait before running next account balance update loop
# payments.stripe-coin-payments.account-balance-update-interval: 2m0s