// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information. package stripecoinpayments import ( "context" "encoding/json" "strconv" "strings" "time" "github.com/stripe/stripe-go/v72" "go.uber.org/zap" "storj.io/common/uuid" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments/coinpayments" "storj.io/storj/satellite/payments/monetary" ) const ( // StripeDepositTransactionDescription is the description for Stripe // balance transactions representing STORJ deposits. StripeDepositTransactionDescription = "STORJ deposit" // StripeDepositBonusTransactionDescription is the description for Stripe // balance transactions representing bonuses received for STORJ deposits. StripeDepositBonusTransactionDescription = "STORJ deposit bonus" // StripeMigratedDepositBonusTransactionDescription is the description for // Stripe balance transactions representing bonuses migrated from the // 'credits' table of the satellite DB. StripeMigratedDepositBonusTransactionDescription = "Migrated STORJ deposit bonus" ) // ensure that storjTokens implements payments.StorjTokens. var _ payments.StorjTokens = (*storjTokens)(nil) // storjTokens implements payments.StorjTokens. // // architecture: Service type storjTokens struct { service *Service } // Deposit creates new deposit transaction with the given amount returning // ETH wallet address where funds should be sent. There is one // hour limit to complete the transaction. Transaction is saved to DB with // reference to the user who made the deposit. func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount int64) (_ *payments.Transaction, err error) { defer mon.Task()(&ctx, userID, amount)(&err) customerID, err := tokens.service.db.Customers().GetCustomerID(ctx, userID) if err != nil { return nil, Error.Wrap(err) } c, err := tokens.service.stripeClient.Customers().Get(customerID, nil) if err != nil { return nil, Error.Wrap(err) } rate, err := tokens.service.GetRate(ctx, monetary.StorjToken, monetary.USDollars) if err != nil { return nil, Error.Wrap(err) } tokenAmount := convertFromCents(rate, amount) tx, err := tokens.service.coinPayments.Transactions().Create(ctx, &coinpayments.CreateTX{ Amount: tokenAmount.AsDecimal(), CurrencyIn: monetary.StorjToken, CurrencyOut: monetary.StorjToken, BuyerEmail: c.Email, }, ) if err != nil { return nil, Error.Wrap(err) } key, err := coinpayments.GetTransactionKeyFromURL(tx.CheckoutURL) if err != nil { return nil, Error.Wrap(err) } if err = tokens.service.db.Transactions().LockRate(ctx, tx.ID, rate); err != nil { return nil, Error.Wrap(err) } createTime, err := tokens.service.db.Transactions().Insert(ctx, Transaction{ ID: tx.ID, AccountID: userID, Address: tx.Address, Amount: tx.Amount, Received: monetary.AmountFromBaseUnits(0, tx.Amount.Currency()), Status: coinpayments.StatusPending, Key: key, Timeout: tx.Timeout, }, ) if err != nil { return nil, Error.Wrap(err) } return &payments.Transaction{ ID: payments.TransactionID(tx.ID), Amount: tx.Amount, Rate: rate, Address: tx.Address, Status: payments.TransactionStatusPending, Timeout: tx.Timeout, Link: tx.CheckoutURL, CreatedAt: createTime, }, nil } // ListTransactionInfos fetches all transactions from the database for specified user, reconstructing checkout link. func (tokens *storjTokens) ListTransactionInfos(ctx context.Context, userID uuid.UUID) (_ []payments.TransactionInfo, err error) { defer mon.Task()(&ctx, userID)(&err) txs, err := tokens.service.db.Transactions().ListAccount(ctx, userID) if err != nil { return nil, Error.Wrap(err) } var infos []payments.TransactionInfo for _, tx := range txs { link := coinpayments.GetCheckoutURL(tx.Key, tx.ID) var status payments.TransactionStatus switch tx.Status { case coinpayments.StatusPending: status = payments.TransactionStatusPending case coinpayments.StatusReceived: status = payments.TransactionStatusPaid case coinpayments.StatusCancelled: status = payments.TransactionStatusCancelled default: // unknown status = payments.TransactionStatus(tx.Status.String()) } rate, err := tokens.service.db.Transactions().GetLockedRate(ctx, tx.ID) if err != nil { return nil, err } infos = append(infos, payments.TransactionInfo{ ID: []byte(tx.ID), Amount: tx.Amount, Received: tx.Received, AmountCents: convertToCents(rate, tx.Amount), ReceivedCents: convertToCents(rate, tx.Received), Address: tx.Address, Status: status, Link: link, ExpiresAt: tx.CreatedAt.Add(tx.Timeout), CreatedAt: tx.CreatedAt, }, ) } return infos, nil } // ListDepositBonuses returns all deposit bonuses from Stripe associated with user. func (tokens *storjTokens) ListDepositBonuses(ctx context.Context, userID uuid.UUID) (_ []payments.DepositBonus, err error) { defer mon.Task()(&ctx, userID)(&err) cusID, err := tokens.service.db.Customers().GetCustomerID(ctx, userID) if err != nil { return nil, err } var bonuses []payments.DepositBonus customer, err := tokens.service.stripeClient.Customers().Get(cusID, nil) if err != nil { return nil, err } for key, value := range customer.Metadata { if !strings.HasPrefix(key, "credit_") { continue } var credit payments.Credit err = json.Unmarshal([]byte(value), &credit) if err != nil { tokens.service.log.Error("Error unmarshaling credit history from Stripe metadata", zap.String("Customer ID", cusID), zap.String("Metadata Key", key), zap.String("Metadata Value", value), zap.Error(err), ) continue } bonuses = append(bonuses, payments.DepositBonus{ TransactionID: payments.TransactionID(credit.TransactionID), AmountCents: credit.Amount, Percentage: 10, CreatedAt: credit.Created, }, ) } it := tokens.service.stripeClient.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(cusID)}) for it.Next() { tx := it.CustomerBalanceTransaction() if tx.Type != stripe.CustomerBalanceTransactionTypeAdjustment { continue } if tx.Description != StripeDepositBonusTransactionDescription { continue } percentage := int64(10) percentageStr, ok := tx.Metadata["percentage"] if ok { percentage, err = strconv.ParseInt(percentageStr, 10, 64) if err != nil { return nil, err } } bonuses = append(bonuses, payments.DepositBonus{ TransactionID: []byte(tx.Metadata["txID"]), AmountCents: -tx.Amount, Percentage: percentage, CreatedAt: time.Unix(tx.Created, 0), }, ) } return bonuses, nil }