mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
170 lines
5.2 KiB
Go
170 lines
5.2 KiB
Go
package coinbaseprime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
func (p *Plugin) fetchNextConversions(ctx context.Context, req models.FetchNextConversionsRequest) (models.FetchNextConversionsResponse, error) {
|
|
var oldState incrementalState
|
|
if req.State != nil {
|
|
if err := json.Unmarshal(req.State, &oldState); err != nil {
|
|
return models.FetchNextConversionsResponse{}, fmt.Errorf("failed to unmarshal state: %w", err)
|
|
}
|
|
}
|
|
|
|
resp, err := p.client.GetTransactions(ctx, oldState.Cursor, req.PageSize, TransactionTypeConversion)
|
|
if err != nil {
|
|
return models.FetchNextConversionsResponse{}, fmt.Errorf("failed to list transactions: %w", err)
|
|
}
|
|
|
|
conversions := make([]models.PSPConversion, 0, len(resp.Transactions))
|
|
for _, tx := range resp.Transactions {
|
|
conv, err := p.transactionToConversion(ctx, tx)
|
|
if err != nil {
|
|
return models.FetchNextConversionsResponse{}, fmt.Errorf("failed to convert transaction %s: %w", tx.ID, err)
|
|
}
|
|
if conv != nil {
|
|
conversions = append(conversions, *conv)
|
|
}
|
|
}
|
|
|
|
newState := incrementalState{Cursor: advanceCursor(oldState.Cursor, resp.Pagination.NextCursor)}
|
|
stateBytes, err := json.Marshal(newState)
|
|
if err != nil {
|
|
return models.FetchNextConversionsResponse{}, fmt.Errorf("failed to marshal state: %w", err)
|
|
}
|
|
|
|
return models.FetchNextConversionsResponse{
|
|
Conversions: conversions,
|
|
NewState: stateBytes,
|
|
HasMore: resp.Pagination.HasNext,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Plugin) transactionToConversion(ctx context.Context, tx client.Transaction) (*models.PSPConversion, error) {
|
|
sourceAsset, sourcePrecision, sourceOk, err := p.resolveAssetAndPrecision(ctx, tx.Symbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !sourceOk {
|
|
p.logger.Infof("skipping conversion %s: unsupported source currency %q", tx.ID, tx.Symbol)
|
|
return nil, nil
|
|
}
|
|
|
|
targetSymbol := tx.DestinationSymbol
|
|
if targetSymbol == "" {
|
|
p.logger.Infof("skipping conversion %s: missing destination_symbol", tx.ID)
|
|
return nil, nil
|
|
}
|
|
|
|
targetAsset, targetPrecision, targetOk, err := p.resolveAssetAndPrecision(ctx, targetSymbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !targetOk {
|
|
p.logger.Infof("skipping conversion %s: unsupported target currency %q", tx.ID, targetSymbol)
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(tx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal raw: %w", err)
|
|
}
|
|
|
|
sourceAmount, err := currency.GetAmountWithPrecisionFromString(tx.Amount, sourcePrecision)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse source amount: %w", err)
|
|
}
|
|
|
|
// For 1:1 conversions (e.g. stablecoin USDC->USD), the Coinbase API provides
|
|
// a single amount field. Source and destination represent the same nominal value
|
|
// parsed at each asset's precision. For FX conversions where amounts truly differ,
|
|
// the PSP plugin must supply distinct values from the API response.
|
|
targetAmount, err := currency.GetAmountWithPrecisionFromString(tx.Amount, targetPrecision)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse target amount: %w", err)
|
|
}
|
|
|
|
var fee *big.Int
|
|
var feeAsset *string
|
|
if tx.Fees != "" && tx.Fees != "0" {
|
|
feeSymbol := tx.FeeSymbol
|
|
if feeSymbol == "" {
|
|
feeSymbol = tx.Symbol
|
|
}
|
|
fAsset, fPrecision, fOk, err := p.resolveAssetAndPrecision(ctx, feeSymbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !fOk {
|
|
p.logger.Infof("skipping fee for conversion %s: unsupported fee currency %q", tx.ID, feeSymbol)
|
|
} else {
|
|
fee, err = currency.GetAmountWithPrecisionFromString(tx.Fees, fPrecision)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse fee: %w", err)
|
|
}
|
|
feeAsset = &fAsset
|
|
}
|
|
}
|
|
|
|
status := mapTransactionToConversionStatus(tx.Status)
|
|
|
|
var sourceWalletID, targetWalletID string
|
|
if tx.TransferFrom != nil && tx.TransferFrom.Value != "" {
|
|
sourceWalletID = tx.TransferFrom.Value
|
|
}
|
|
if tx.TransferTo != nil && tx.TransferTo.Value != "" {
|
|
targetWalletID = tx.TransferTo.Value
|
|
}
|
|
|
|
metadata := map[string]string{
|
|
MetadataPrefix + "transaction_id": tx.TransactionID,
|
|
MetadataPrefix + "type": tx.Type,
|
|
}
|
|
if tx.PortfolioID != "" {
|
|
metadata[MetadataPrefix+"portfolio_id"] = tx.PortfolioID
|
|
}
|
|
|
|
return &models.PSPConversion{
|
|
Reference: tx.ID,
|
|
CreatedAt: tx.CreatedAt,
|
|
SourceAsset: sourceAsset,
|
|
DestinationAsset: targetAsset,
|
|
SourceAmount: sourceAmount,
|
|
DestinationAmount: targetAmount,
|
|
Fee: fee,
|
|
FeeAsset: feeAsset,
|
|
Status: status,
|
|
SourceAccountReference: ptrIfNotEmpty(sourceWalletID),
|
|
DestinationAccountReference: ptrIfNotEmpty(targetWalletID),
|
|
Metadata: metadata,
|
|
Raw: raw,
|
|
}, nil
|
|
}
|
|
|
|
func ptrIfNotEmpty(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
func mapTransactionToConversionStatus(status string) models.ConversionStatus {
|
|
switch strings.ToUpper(status) {
|
|
case "TRANSACTION_DONE", "TRANSACTION_IMPORTED":
|
|
return models.CONVERSION_STATUS_COMPLETED
|
|
case "TRANSACTION_FAILED", "TRANSACTION_REJECTED", "TRANSACTION_CANCELLED":
|
|
return models.CONVERSION_STATUS_FAILED
|
|
default:
|
|
return models.CONVERSION_STATUS_PENDING
|
|
}
|
|
}
|