mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
244 lines
7.3 KiB
Go
244 lines
7.3 KiB
Go
package coinbaseprime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/currency"
|
|
"github.com/hanzo-fi/payments/ee/plugins/coinbaseprime/client"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
)
|
|
|
|
type TransferEndpointType string
|
|
|
|
const (
|
|
// CoinbasePrime transfer types
|
|
transferTypeWallet TransferEndpointType = "WALLET"
|
|
transferTypePaymentMethod TransferEndpointType = "PAYMENT_METHOD"
|
|
transferTypeAddress TransferEndpointType = "ADDRESS"
|
|
transferTypeOther TransferEndpointType = "OTHER"
|
|
transferTypeMultipleAddress TransferEndpointType = "MULTIPLE_ADDRESSES"
|
|
transferTypeCounterpartyID TransferEndpointType = "COUNTERPARTY_ID"
|
|
)
|
|
|
|
func (p *Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) {
|
|
var oldState incrementalState
|
|
if req.State != nil {
|
|
if err := json.Unmarshal(req.State, &oldState); err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
}
|
|
|
|
response, err := p.client.GetTransactions(ctx, oldState.Cursor, req.PageSize)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
payments := make([]models.PSPPayment, 0, len(response.Transactions))
|
|
for _, tx := range response.Transactions {
|
|
payment, err := p.transactionToPayment(ctx, tx)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, fmt.Errorf("failed to convert transaction %s: %w", tx.ID, err)
|
|
}
|
|
if payment == nil {
|
|
continue
|
|
}
|
|
payments = append(payments, *payment)
|
|
}
|
|
|
|
newState := incrementalState{Cursor: advanceCursor(oldState.Cursor, response.Pagination.NextCursor)}
|
|
payload, err := json.Marshal(newState)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
return models.FetchNextPaymentsResponse{
|
|
Payments: payments,
|
|
NewState: payload,
|
|
HasMore: response.Pagination.HasNext,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Plugin) transactionToPayment(ctx context.Context, tx client.Transaction) (*models.PSPPayment, error) {
|
|
if strings.EqualFold(tx.Type, TransactionTypeConversion) {
|
|
return nil, nil
|
|
}
|
|
|
|
asset, precision, ok, err := p.resolveAssetAndPrecision(ctx, tx.Symbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
p.logger.Infof("skipping transaction %s: unsupported currency %q", tx.ID, tx.Symbol)
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
paymentType := transactionTypeToPaymentType(tx.Type)
|
|
status := transactionStatusToPaymentStatus(tx.Status)
|
|
|
|
amount, err := currency.GetAmountWithPrecisionFromString(tx.Amount, precision)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse amount: %w", err)
|
|
}
|
|
|
|
metadata := buildTransactionMetadata(tx)
|
|
|
|
payment := models.PSPPayment{
|
|
Reference: tx.ID,
|
|
CreatedAt: tx.CreatedAt,
|
|
Type: paymentType,
|
|
Amount: amount,
|
|
Asset: asset,
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: status,
|
|
Metadata: metadata,
|
|
Raw: raw,
|
|
}
|
|
|
|
sourceAccountReference, destinationAccountReference := resolveAccountReferences(tx)
|
|
payment.SourceAccountReference = sourceAccountReference
|
|
payment.DestinationAccountReference = destinationAccountReference
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func buildTransactionMetadata(tx client.Transaction) map[string]string {
|
|
metadata := make(map[string]string)
|
|
set := func(k, v string) {
|
|
if v != "" {
|
|
metadata[MetadataPrefix+k] = v
|
|
}
|
|
}
|
|
|
|
set("wallet_id", tx.WalletID)
|
|
set("portfolio_id", tx.PortfolioID)
|
|
metadata[MetadataPrefix+"type"] = tx.Type
|
|
metadata[MetadataPrefix+"status"] = tx.Status
|
|
|
|
hasFees := (tx.Fees != "" && tx.Fees != "0") || (tx.NetworkFees != "" && tx.NetworkFees != "0")
|
|
if tx.Fees != "" && tx.Fees != "0" {
|
|
set("fees", tx.Fees)
|
|
}
|
|
if hasFees {
|
|
set("fee_symbol", tx.FeeSymbol)
|
|
}
|
|
if tx.NetworkFees != "" && tx.NetworkFees != "0" {
|
|
set("network_fees", tx.NetworkFees)
|
|
}
|
|
|
|
set("network", tx.Network)
|
|
if len(tx.BlockchainIDs) > 0 {
|
|
metadata[MetadataPrefix+"blockchain_ids"] = strings.Join(tx.BlockchainIDs, ",")
|
|
}
|
|
|
|
if tx.CompletedAt != nil {
|
|
metadata[MetadataPrefix+"completed_at"] = tx.CompletedAt.Format(time.RFC3339)
|
|
}
|
|
|
|
sourceAddress := ""
|
|
if tx.TransferFrom != nil && tx.TransferFrom.Address != "" {
|
|
sourceAddress = tx.TransferFrom.Address
|
|
} else if tx.SourceAddress != "" {
|
|
sourceAddress = tx.SourceAddress
|
|
}
|
|
set("source_address", sourceAddress)
|
|
|
|
depositAddress := ""
|
|
if tx.TransferTo != nil && tx.TransferTo.Address != "" {
|
|
depositAddress = tx.TransferTo.Address
|
|
} else if tx.DepositAddress != "" {
|
|
depositAddress = tx.DepositAddress
|
|
}
|
|
set("deposit_address", depositAddress)
|
|
set("external_tx_id", tx.ExternalTxID)
|
|
|
|
return metadata
|
|
}
|
|
|
|
func isTransferType(t string, expected TransferEndpointType) bool {
|
|
return strings.ToUpper(t) == string(expected)
|
|
}
|
|
|
|
func resolveAccountReferences(tx client.Transaction) (*string, *string) {
|
|
var source, dest *string
|
|
|
|
// Use transfer_from/transfer_to only when type is WALLET
|
|
if tx.TransferFrom != nil && tx.TransferFrom.Value != "" && isTransferType(tx.TransferFrom.Type, transferTypeWallet) {
|
|
v := tx.TransferFrom.Value
|
|
source = &v
|
|
}
|
|
if tx.TransferTo != nil && tx.TransferTo.Value != "" && isTransferType(tx.TransferTo.Type, transferTypeWallet) {
|
|
v := tx.TransferTo.Value
|
|
dest = &v
|
|
}
|
|
|
|
// Fallback to walletID based on payment direction
|
|
if tx.WalletID != "" {
|
|
switch transactionTypeToPaymentType(tx.Type) {
|
|
case models.PAYMENT_TYPE_PAYIN:
|
|
if dest == nil {
|
|
v := tx.WalletID
|
|
dest = &v
|
|
}
|
|
case models.PAYMENT_TYPE_PAYOUT:
|
|
if source == nil {
|
|
v := tx.WalletID
|
|
source = &v
|
|
}
|
|
}
|
|
}
|
|
|
|
return source, dest
|
|
}
|
|
|
|
func transactionTypeToPaymentType(txType string) models.PaymentType {
|
|
switch strings.ToUpper(txType) {
|
|
case "DEPOSIT",
|
|
"COINBASE_DEPOSIT", "COINBASE_REFUND", "REWARD",
|
|
"DEPOSIT_ADJUSTMENT", "CLAIM_REWARDS":
|
|
return models.PAYMENT_TYPE_PAYIN
|
|
case "WITHDRAWAL", "SWEEP_WITHDRAWAL",
|
|
"PROXY_WITHDRAWAL", "BILLING_WITHDRAWAL", "WITHDRAWAL_ADJUSTMENT", "SLASH":
|
|
return models.PAYMENT_TYPE_PAYOUT
|
|
case "INTERNAL_DEPOSIT", "INTERNAL_WITHDRAWAL", "SWEEP_DEPOSIT", "PROXY_DEPOSIT",
|
|
"STAKE", "RESTAKE", "PORTFOLIO_STAKE", "UNSTAKE", "PORTFOLIO_UNSTAKE":
|
|
return models.PAYMENT_TYPE_TRANSFER
|
|
case "KEY_REGISTRATION", "COMPLETE_UNBONDING", "WITHDRAW_UNBONDED", "STAKE_ACCOUNT_CREATE", "CHANGE_VALIDATOR",
|
|
"DELEGATION", "UNDELEGATION", "REMOVE_AUTHORIZED_PARTY", "STAKE_AUTHORIZE_WITH_SEED", "VOTE_AUTHORIZE", "ONCHAIN_TRANSACTION":
|
|
return models.PAYMENT_TYPE_OTHER
|
|
default:
|
|
return models.PAYMENT_TYPE_OTHER
|
|
}
|
|
}
|
|
|
|
func transactionStatusToPaymentStatus(status string) models.PaymentStatus {
|
|
switch strings.ToUpper(status) {
|
|
case "TRANSACTION_PENDING", "TRANSACTION_CREATED", "TRANSACTION_REQUESTED",
|
|
"TRANSACTION_APPROVED", "TRANSACTION_GASSING", "TRANSACTION_GASSED",
|
|
"TRANSACTION_PROVISIONED", "TRANSACTION_PLANNED", "TRANSACTION_PROCESSING",
|
|
"TRANSACTION_RESTORED", "TRANSACTION_IMPORT_PENDING", "TRANSACTION_DELAYED",
|
|
"TRANSACTION_BROADCASTING", "TRANSACTION_CONSTRUCTED":
|
|
return models.PAYMENT_STATUS_PENDING
|
|
case "TRANSACTION_DONE", "TRANSACTION_IMPORTED":
|
|
return models.PAYMENT_STATUS_SUCCEEDED
|
|
case "TRANSACTION_CANCELLED":
|
|
return models.PAYMENT_STATUS_CANCELLED
|
|
case "TRANSACTION_EXPIRED":
|
|
return models.PAYMENT_STATUS_EXPIRED
|
|
case "TRANSACTION_FAILED", "TRANSACTION_REJECTED":
|
|
return models.PAYMENT_STATUS_FAILED
|
|
case "OTHER_TRANSACTION_STATUS":
|
|
return models.PAYMENT_STATUS_OTHER
|
|
default:
|
|
return models.PAYMENT_STATUS_UNKNOWN
|
|
}
|
|
}
|