2019-10-17 15:04:50 +01:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package satellitedb
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"math/big"
|
2019-10-23 13:04:54 +01:00
|
|
|
"time"
|
2019-10-17 15:04:50 +01:00
|
|
|
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
|
2020-03-30 10:08:50 +01:00
|
|
|
"storj.io/common/uuid"
|
2019-10-17 15:04:50 +01:00
|
|
|
"storj.io/storj/satellite/payments/coinpayments"
|
|
|
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
2020-01-15 02:29:51 +00:00
|
|
|
"storj.io/storj/satellite/satellitedb/dbx"
|
2019-10-17 15:04:50 +01:00
|
|
|
)
|
|
|
|
|
2019-11-05 13:16:02 +00:00
|
|
|
// ensure that coinpaymentsTransactions implements stripecoinpayments.TransactionsDB.
|
|
|
|
var _ stripecoinpayments.TransactionsDB = (*coinPaymentsTransactions)(nil)
|
2019-10-17 15:04:50 +01:00
|
|
|
|
2019-10-29 16:04:34 +00:00
|
|
|
// applyBalanceIntentState defines states of the apply balance intents.
|
|
|
|
type applyBalanceIntentState int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// apply balance intent waits to be applied.
|
|
|
|
applyBalanceIntentStateUnapplied applyBalanceIntentState = 0
|
|
|
|
// transaction which balance intent points to has been consumed.
|
|
|
|
applyBalanceIntentStateConsumed applyBalanceIntentState = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
// Int returns intent state as int.
|
|
|
|
func (intent applyBalanceIntentState) Int() int {
|
|
|
|
return int(intent)
|
|
|
|
}
|
|
|
|
|
2019-11-05 13:16:02 +00:00
|
|
|
// coinPaymentsTransactions is CoinPayments transactions DB.
|
2019-10-17 15:04:50 +01:00
|
|
|
//
|
|
|
|
// architecture: Database
|
2019-11-05 13:16:02 +00:00
|
|
|
type coinPaymentsTransactions struct {
|
2019-12-14 02:29:54 +00:00
|
|
|
db *satelliteDB
|
2019-10-17 15:04:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Insert inserts new coinpayments transaction into DB.
|
2019-11-15 14:59:39 +00:00
|
|
|
func (db *coinPaymentsTransactions) Insert(ctx context.Context, tx stripecoinpayments.Transaction) (_ *stripecoinpayments.Transaction, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-10-17 15:04:50 +01:00
|
|
|
amount, err := tx.Amount.GobEncode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
received, err := tx.Received.GobEncode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
dbxCPTX, err := db.db.Create_CoinpaymentsTransaction(ctx,
|
|
|
|
dbx.CoinpaymentsTransaction_Id(tx.ID.String()),
|
|
|
|
dbx.CoinpaymentsTransaction_UserId(tx.AccountID[:]),
|
|
|
|
dbx.CoinpaymentsTransaction_Address(tx.Address),
|
|
|
|
dbx.CoinpaymentsTransaction_Amount(amount),
|
|
|
|
dbx.CoinpaymentsTransaction_Received(received),
|
|
|
|
dbx.CoinpaymentsTransaction_Status(tx.Status.Int()),
|
|
|
|
dbx.CoinpaymentsTransaction_Key(tx.Key),
|
2019-11-15 14:59:39 +00:00
|
|
|
dbx.CoinpaymentsTransaction_Timeout(int(tx.Timeout.Seconds())),
|
2019-10-17 15:04:50 +01:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return fromDBXCoinpaymentsTransaction(dbxCPTX)
|
|
|
|
}
|
|
|
|
|
2019-10-23 13:04:54 +01:00
|
|
|
// Update updates status and received for set of transactions.
|
2019-11-15 14:59:39 +00:00
|
|
|
func (db *coinPaymentsTransactions) Update(ctx context.Context, updates []stripecoinpayments.TransactionUpdate, applies coinpayments.TransactionIDList) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-10-23 13:04:54 +01:00
|
|
|
if len(updates) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return db.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
|
|
|
|
for _, update := range updates {
|
|
|
|
received, err := update.Received.GobEncode()
|
|
|
|
if err != nil {
|
|
|
|
return errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = tx.Update_CoinpaymentsTransaction_By_Id(ctx,
|
|
|
|
dbx.CoinpaymentsTransaction_Id(update.TransactionID.String()),
|
|
|
|
dbx.CoinpaymentsTransaction_Update_Fields{
|
|
|
|
Received: dbx.CoinpaymentsTransaction_Received(received),
|
|
|
|
Status: dbx.CoinpaymentsTransaction_Status(update.Status.Int()),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-29 16:04:34 +00:00
|
|
|
for _, txID := range applies {
|
|
|
|
_, err := tx.Create_StripecoinpaymentsApplyBalanceIntent(ctx,
|
|
|
|
dbx.StripecoinpaymentsApplyBalanceIntent_TxId(txID.String()),
|
|
|
|
dbx.StripecoinpaymentsApplyBalanceIntent_State(applyBalanceIntentStateUnapplied.Int()))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-23 13:04:54 +01:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-10-29 16:04:34 +00:00
|
|
|
// Consume marks transaction as consumed, so it won't participate in apply account balance loop.
|
2019-11-15 14:59:39 +00:00
|
|
|
func (db *coinPaymentsTransactions) Consume(ctx context.Context, id coinpayments.TransactionID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-02-13 18:08:45 +00:00
|
|
|
query := db.db.Rebind(`
|
|
|
|
WITH intent AS (
|
|
|
|
SELECT tx_id, state FROM stripecoinpayments_apply_balance_intents WHERE tx_id = ?
|
|
|
|
), updated AS (
|
|
|
|
UPDATE stripecoinpayments_apply_balance_intents AS ints
|
|
|
|
SET
|
|
|
|
state = ?
|
|
|
|
FROM intent
|
|
|
|
WHERE intent.tx_id = ints.tx_id AND ints.state = ?
|
|
|
|
RETURNING 1
|
|
|
|
)
|
|
|
|
SELECT EXISTS(SELECT 1 FROM intent) AS intent_exists, EXISTS(SELECT 1 FROM updated) AS intent_consumed;
|
|
|
|
`)
|
|
|
|
|
|
|
|
row := db.db.QueryRowContext(ctx, query, id, applyBalanceIntentStateConsumed, applyBalanceIntentStateUnapplied)
|
|
|
|
|
|
|
|
var exists, consumed bool
|
|
|
|
if err = row.Scan(&exists, &consumed); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
return errs.New("can not consume transaction without apply balance intent")
|
|
|
|
}
|
|
|
|
if !consumed {
|
|
|
|
return stripecoinpayments.ErrTransactionConsumed
|
|
|
|
}
|
|
|
|
|
2019-10-29 16:04:34 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-11-15 14:59:39 +00:00
|
|
|
// LockRate locks conversion rate for transaction.
|
|
|
|
func (db *coinPaymentsTransactions) LockRate(ctx context.Context, id coinpayments.TransactionID, rate *big.Float) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
buff, err := rate.GobEncode()
|
|
|
|
if err != nil {
|
|
|
|
return errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = db.db.Create_StripecoinpaymentsTxConversionRate(ctx,
|
|
|
|
dbx.StripecoinpaymentsTxConversionRate_TxId(id.String()),
|
|
|
|
dbx.StripecoinpaymentsTxConversionRate_Rate(buff))
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetLockedRate returns locked conversion rate for transaction or error if non exists.
|
|
|
|
func (db *coinPaymentsTransactions) GetLockedRate(ctx context.Context, id coinpayments.TransactionID) (_ *big.Float, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
dbxRate, err := db.db.Get_StripecoinpaymentsTxConversionRate_By_TxId(ctx,
|
|
|
|
dbx.StripecoinpaymentsTxConversionRate_TxId(id.String()),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rate := new(big.Float)
|
|
|
|
if err = rate.GobDecode(dbxRate.Rate); err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rate, nil
|
|
|
|
}
|
|
|
|
|
2019-11-12 11:14:34 +00:00
|
|
|
// ListAccount returns all transaction for specific user.
|
2019-11-15 14:59:39 +00:00
|
|
|
func (db *coinPaymentsTransactions) ListAccount(ctx context.Context, userID uuid.UUID) (_ []stripecoinpayments.Transaction, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-11-12 11:14:34 +00:00
|
|
|
dbxTXs, err := db.db.All_CoinpaymentsTransaction_By_UserId_OrderBy_Desc_CreatedAt(ctx,
|
|
|
|
dbx.CoinpaymentsTransaction_UserId(userID[:]),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var txs []stripecoinpayments.Transaction
|
|
|
|
for _, dbxTX := range dbxTXs {
|
|
|
|
tx, err := fromDBXCoinpaymentsTransaction(dbxTX)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
txs = append(txs, *tx)
|
|
|
|
}
|
|
|
|
|
|
|
|
return txs, nil
|
|
|
|
}
|
|
|
|
|
2019-10-23 13:04:54 +01:00
|
|
|
// ListPending returns paginated list of pending transactions.
|
2019-11-15 14:59:39 +00:00
|
|
|
func (db *coinPaymentsTransactions) ListPending(ctx context.Context, offset int64, limit int, before time.Time) (_ stripecoinpayments.TransactionsPage, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
query := db.db.Rebind(`SELECT
|
|
|
|
id,
|
|
|
|
user_id,
|
|
|
|
address,
|
|
|
|
amount,
|
|
|
|
received,
|
|
|
|
status,
|
|
|
|
key,
|
|
|
|
created_at
|
|
|
|
FROM coinpayments_transactions
|
|
|
|
WHERE status IN (?,?)
|
|
|
|
AND created_at <= ?
|
|
|
|
ORDER by created_at DESC
|
|
|
|
LIMIT ? OFFSET ?`)
|
2019-10-23 13:04:54 +01:00
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
rows, err := db.db.QueryContext(ctx, query, coinpayments.StatusPending, coinpayments.StatusReceived, before, limit+1, offset)
|
2019-10-23 13:04:54 +01:00
|
|
|
if err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
defer func() {
|
|
|
|
err = errs.Combine(err, rows.Close())
|
|
|
|
}()
|
2019-10-23 13:04:54 +01:00
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
var page stripecoinpayments.TransactionsPage
|
2019-10-23 13:04:54 +01:00
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
for rows.Next() {
|
|
|
|
var id, address string
|
2020-03-31 17:49:16 +01:00
|
|
|
var userID uuid.UUID
|
2020-01-14 13:38:32 +00:00
|
|
|
var amountB, receivedB []byte
|
|
|
|
var status int
|
|
|
|
var key string
|
|
|
|
var createdAt time.Time
|
|
|
|
|
2020-03-31 17:49:16 +01:00
|
|
|
err := rows.Scan(&id, &userID, &address, &amountB, &receivedB, &status, &key, &createdAt)
|
2019-10-23 13:04:54 +01:00
|
|
|
if err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
|
|
|
|
2020-01-14 13:38:32 +00:00
|
|
|
var amount, received big.Float
|
|
|
|
if err := amount.GobDecode(amountB); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
if err := received.GobDecode(receivedB); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
page.Transactions = append(page.Transactions,
|
|
|
|
stripecoinpayments.Transaction{
|
|
|
|
ID: coinpayments.TransactionID(id),
|
|
|
|
AccountID: userID,
|
|
|
|
Address: address,
|
|
|
|
Amount: amount,
|
|
|
|
Received: received,
|
|
|
|
Status: coinpayments.Status(status),
|
|
|
|
Key: key,
|
|
|
|
CreatedAt: createdAt,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(page.Transactions) == limit+1 {
|
|
|
|
page.Next = true
|
2020-02-11 13:11:14 +00:00
|
|
|
page.NextOffset = offset + int64(limit)
|
2020-01-14 13:38:32 +00:00
|
|
|
page.Transactions = page.Transactions[:len(page.Transactions)-1]
|
2019-10-23 13:04:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return page, nil
|
|
|
|
}
|
|
|
|
|
2020-07-01 16:26:23 +01:00
|
|
|
// List Unapplied returns TransactionsPage with a pending or completed status, that should be applied to account balance.
|
2019-11-05 13:16:02 +00:00
|
|
|
func (db *coinPaymentsTransactions) ListUnapplied(ctx context.Context, offset int64, limit int, before time.Time) (_ stripecoinpayments.TransactionsPage, err error) {
|
2019-11-15 14:59:39 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-10-29 16:04:34 +00:00
|
|
|
query := db.db.Rebind(`SELECT
|
|
|
|
txs.id,
|
|
|
|
txs.user_id,
|
|
|
|
txs.address,
|
|
|
|
txs.amount,
|
|
|
|
txs.received,
|
2020-01-14 13:38:32 +00:00
|
|
|
txs.status,
|
2019-10-29 16:04:34 +00:00
|
|
|
txs.key,
|
|
|
|
txs.created_at
|
|
|
|
FROM coinpayments_transactions as txs
|
|
|
|
INNER JOIN stripecoinpayments_apply_balance_intents as ints
|
|
|
|
ON txs.id = ints.tx_id
|
2020-07-01 16:26:23 +01:00
|
|
|
WHERE txs.status >= ?
|
2019-10-29 16:04:34 +00:00
|
|
|
AND txs.created_at <= ?
|
|
|
|
AND ints.state = ?
|
|
|
|
ORDER by txs.created_at DESC
|
|
|
|
LIMIT ? OFFSET ?`)
|
|
|
|
|
2020-07-01 16:26:23 +01:00
|
|
|
rows, err := db.db.QueryContext(ctx, query, coinpayments.StatusReceived, before, applyBalanceIntentStateUnapplied, limit+1, offset)
|
2019-10-29 16:04:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
2020-01-16 14:27:24 +00:00
|
|
|
defer func() { err = errs.Combine(err, rows.Close()) }()
|
2019-10-29 16:04:34 +00:00
|
|
|
|
|
|
|
var page stripecoinpayments.TransactionsPage
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
var id, address string
|
2020-03-31 17:49:16 +01:00
|
|
|
var userID uuid.UUID
|
2019-10-29 16:04:34 +00:00
|
|
|
var amountB, receivedB []byte
|
2020-01-14 13:38:32 +00:00
|
|
|
var status int
|
2019-10-29 16:04:34 +00:00
|
|
|
var key string
|
|
|
|
var createdAt time.Time
|
|
|
|
|
2020-03-31 17:49:16 +01:00
|
|
|
err := rows.Scan(&id, &userID, &address, &amountB, &receivedB, &status, &key, &createdAt)
|
2019-10-29 16:04:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var amount, received big.Float
|
|
|
|
if err := amount.GobDecode(amountB); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
if err := received.GobDecode(receivedB); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
page.Transactions = append(page.Transactions,
|
|
|
|
stripecoinpayments.Transaction{
|
|
|
|
ID: coinpayments.TransactionID(id),
|
|
|
|
AccountID: userID,
|
|
|
|
Address: address,
|
|
|
|
Amount: amount,
|
|
|
|
Received: received,
|
2020-01-14 13:38:32 +00:00
|
|
|
Status: coinpayments.Status(status),
|
2019-10-29 16:04:34 +00:00
|
|
|
Key: key,
|
|
|
|
CreatedAt: createdAt,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
return stripecoinpayments.TransactionsPage{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(page.Transactions) == limit+1 {
|
|
|
|
page.Next = true
|
2020-02-11 13:11:14 +00:00
|
|
|
page.NextOffset = offset + int64(limit)
|
2019-10-29 16:04:34 +00:00
|
|
|
page.Transactions = page.Transactions[:len(page.Transactions)-1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return page, nil
|
|
|
|
}
|
|
|
|
|
2019-10-17 15:04:50 +01:00
|
|
|
// fromDBXCoinpaymentsTransaction converts *dbx.CoinpaymentsTransaction to *stripecoinpayments.Transaction.
|
|
|
|
func fromDBXCoinpaymentsTransaction(dbxCPTX *dbx.CoinpaymentsTransaction) (*stripecoinpayments.Transaction, error) {
|
2020-03-31 17:49:16 +01:00
|
|
|
userID, err := uuid.FromBytes(dbxCPTX.UserId)
|
2019-10-17 15:04:50 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var amount, received big.Float
|
|
|
|
if err := amount.GobDecode(dbxCPTX.Amount); err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
if err := received.GobDecode(dbxCPTX.Received); err != nil {
|
|
|
|
return nil, errs.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &stripecoinpayments.Transaction{
|
|
|
|
ID: coinpayments.TransactionID(dbxCPTX.Id),
|
|
|
|
AccountID: userID,
|
|
|
|
Address: dbxCPTX.Address,
|
|
|
|
Amount: amount,
|
|
|
|
Received: received,
|
|
|
|
Status: coinpayments.Status(dbxCPTX.Status),
|
|
|
|
Key: dbxCPTX.Key,
|
2019-11-15 14:59:39 +00:00
|
|
|
Timeout: time.Second * time.Duration(dbxCPTX.Timeout),
|
2019-10-17 15:04:50 +01:00
|
|
|
CreatedAt: dbxCPTX.CreatedAt,
|
|
|
|
}, nil
|
|
|
|
}
|