2019-10-10 18:12:23 +01:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package stripecoinpayments
import (
2019-10-23 13:04:54 +01:00
"context"
"time"
2019-10-29 16:04:34 +00:00
"github.com/stripe/stripe-go"
2019-10-15 12:23:54 +01:00
"github.com/stripe/stripe-go/client"
2019-10-10 18:12:23 +01:00
"github.com/zeebo/errs"
2019-10-23 13:04:54 +01:00
"go.uber.org/zap"
2019-10-17 15:42:18 +01:00
monkit "gopkg.in/spacemonkeygo/monkit.v2"
2019-10-15 12:23:54 +01:00
"storj.io/storj/satellite/payments"
2019-10-17 15:04:50 +01:00
"storj.io/storj/satellite/payments/coinpayments"
2019-10-10 18:12:23 +01:00
)
var mon = monkit . Package ( )
2019-10-17 15:04:50 +01:00
// Error defines stripecoinpayments service error.
var Error = errs . Class ( "stripecoinpayments service error" )
2019-10-10 18:12:23 +01:00
2019-10-17 15:04:50 +01:00
// Config stores needed information for payment service initialization.
2019-10-15 12:23:54 +01:00
type Config struct {
2019-10-29 16:04:34 +00:00
StripeSecretKey string ` help:"stripe API secret key" default:"" `
CoinpaymentsPublicKey string ` help:"coinpayments API public key" default:"" `
CoinpaymentsPrivateKey string ` help:"coinpayments API preivate key key" default:"" `
TransactionUpdateInterval time . Duration ` help:"amount of time we wait before running next transaction update loop" devDefault:"1m" releaseDefault:"30m" `
AccountBalanceUpdateInterval time . Duration ` help:"amount of time we wait before running next account balance update loop" devDefault:"3m" releaseDefault:"1h30m" `
2019-10-10 18:12:23 +01:00
}
2019-10-15 12:23:54 +01:00
// Service is an implementation for payment service via Stripe and Coinpayments.
type Service struct {
2019-10-23 13:04:54 +01:00
log * zap . Logger
2019-10-17 15:04:50 +01:00
customers CustomersDB
transactionsDB TransactionsDB
stripeClient * client . API
2019-10-23 13:04:54 +01:00
coinPayments * coinpayments . Client
2019-10-10 18:12:23 +01:00
}
2019-10-15 12:23:54 +01:00
// NewService creates a Service instance.
2019-10-23 13:04:54 +01:00
func NewService ( log * zap . Logger , config Config , customers CustomersDB , transactionsDB TransactionsDB ) * Service {
2019-10-17 15:04:50 +01:00
stripeClient := client . New ( config . StripeSecretKey , nil )
2019-10-23 13:04:54 +01:00
coinPaymentsClient := coinpayments . NewClient (
2019-10-17 15:04:50 +01:00
coinpayments . Credentials {
PublicKey : config . CoinpaymentsPublicKey ,
PrivateKey : config . CoinpaymentsPrivateKey ,
} ,
)
2019-10-10 18:12:23 +01:00
2019-10-15 12:23:54 +01:00
return & Service {
2019-10-23 13:04:54 +01:00
log : log ,
2019-10-17 15:04:50 +01:00
customers : customers ,
transactionsDB : transactionsDB ,
stripeClient : stripeClient ,
2019-10-23 13:04:54 +01:00
coinPayments : coinPaymentsClient ,
2019-10-10 18:12:23 +01:00
}
}
2019-10-11 16:00:35 +01:00
2019-10-15 12:23:54 +01:00
// Accounts exposes all needed functionality to manage payment accounts.
2019-10-17 15:42:18 +01:00
func ( service * Service ) Accounts ( ) payments . Accounts {
2019-10-15 12:23:54 +01:00
return & accounts { service : service }
2019-10-11 16:00:35 +01:00
}
2019-10-23 13:04:54 +01:00
// updateTransactionsLoop updates all pending transactions in a loop.
func ( service * Service ) updateTransactionsLoop ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-10-29 16:04:34 +00:00
const limit = 25
2019-10-23 13:04:54 +01:00
before := time . Now ( )
txsPage , err := service . transactionsDB . ListPending ( ctx , 0 , limit , before )
if err != nil {
return err
}
if err := service . updateTransactions ( ctx , txsPage . IDList ( ) ) ; err != nil {
return err
}
for txsPage . Next {
2019-10-29 16:04:34 +00:00
if err = ctx . Err ( ) ; err != nil {
return err
2019-10-23 13:04:54 +01:00
}
txsPage , err = service . transactionsDB . ListPending ( ctx , txsPage . NextOffset , limit , before )
if err != nil {
return err
}
if err := service . updateTransactions ( ctx , txsPage . IDList ( ) ) ; err != nil {
return err
}
}
return nil
}
// updateTransactions updates statuses and received amount for given transactions.
func ( service * Service ) updateTransactions ( ctx context . Context , ids coinpayments . TransactionIDList ) ( err error ) {
defer mon . Task ( ) ( & ctx , ids ) ( & err )
if len ( ids ) == 0 {
service . log . Debug ( "no transactions found, skipping update" )
return nil
}
infos , err := service . coinPayments . Transactions ( ) . ListInfos ( ctx , ids )
if err != nil {
return err
}
var updates [ ] TransactionUpdate
2019-10-29 16:04:34 +00:00
var applies coinpayments . TransactionIDList
2019-10-23 13:04:54 +01:00
for id , info := range infos {
updates = append ( updates ,
TransactionUpdate {
TransactionID : id ,
Status : info . Status ,
Received : info . Received ,
} ,
)
2019-10-29 16:04:34 +00:00
// moment of transition to received state, which indicates
// that customer funds were accepted, so we can apply this
// amount to customer balance. So we create intent to update
// customer balance in the future.
if info . Status == coinpayments . StatusReceived {
applies = append ( applies , id )
}
}
return service . transactionsDB . Update ( ctx , updates , applies )
}
// applyAccountBalanceLoop fetches all unapplied transaction in a loop, applying transaction
// received amount to stripe customer balance.
func ( service * Service ) updateAccountBalanceLoop ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
const limit = 25
before := time . Now ( )
txsPage , err := service . transactionsDB . ListUnapplied ( ctx , 0 , limit , before )
if err != nil {
return err
}
for _ , tx := range txsPage . Transactions {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . applyTransactionBalance ( ctx , tx ) ; err != nil {
return err
}
}
for txsPage . Next {
if err = ctx . Err ( ) ; err != nil {
return err
}
txsPage , err := service . transactionsDB . ListUnapplied ( ctx , txsPage . NextOffset , limit , before )
if err != nil {
return err
}
for _ , tx := range txsPage . Transactions {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . applyTransactionBalance ( ctx , tx ) ; err != nil {
return err
}
}
2019-10-23 13:04:54 +01:00
}
2019-10-29 16:04:34 +00:00
return nil
}
// applyTransactionBalance applies transaction received amount to stripe customer balance.
func ( service * Service ) applyTransactionBalance ( ctx context . Context , tx Transaction ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
cusID , err := service . customers . GetCustomerID ( ctx , tx . AccountID )
if err != nil {
return err
}
if err = service . transactionsDB . Consume ( ctx , tx . ID ) ; err != nil {
return err
}
// TODO: add conversion logic
amount , _ := tx . Received . Int64 ( )
params := & stripe . CustomerBalanceTransactionParams {
Amount : stripe . Int64 ( amount ) ,
Customer : stripe . String ( cusID ) ,
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Description : stripe . String ( "storj token deposit" ) ,
}
params . AddMetadata ( "txID" , tx . ID . String ( ) )
_ , err = service . stripeClient . CustomerBalanceTransactions . New ( params )
return err
2019-10-23 13:04:54 +01:00
}