diff --git a/coinbase/client.go b/coinbase/client.go index 36eb659..bbbfdab 100644 --- a/coinbase/client.go +++ b/coinbase/client.go @@ -3,7 +3,6 @@ package coinbase import ( "context" "encoding/json" - "fmt" "net/http" "net/url" "time" @@ -18,12 +17,9 @@ import ( const ( metricsStructName = "coinbase.client" - // DefaultHost is the host serving the Onramp v1 transaction-status API. - // Override via Client.Host for staging or to pin to a different region. - DefaultHost = "api.developer.coinbase.com" - - defaultPageSize = 100 - maxPages = 50 // hard cap to avoid runaway pagination + // DefaultHost is the host serving the Onramp API. Override via + // Client.Host for staging or to pin to a different region. + DefaultHost = "api.cdp.coinbase.com" ) // Client talks to the Coinbase Developer Platform Onramp API. Construct via @@ -55,77 +51,29 @@ func NewClient(auth *Authenticator) *Client { } } -// GetUserTransactions returns every Onramp transaction Coinbase has recorded -// for the given partnerUserRef under the authenticated CDP project. -// Pagination is handled internally. -func (c *Client) GetUserTransactions(ctx context.Context, partnerUserRef string) ([]*Transaction, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserTransactions") - defer tracer.End() - - if partnerUserRef == "" { - return nil, errors.New("partner user ref is required") - } - - var all []*Transaction - var pageKey string - for range maxPages { - path := fmt.Sprintf("/onramp/v1/buy/user/%s/transactions", url.PathEscape(partnerUserRef)) - query := url.Values{} - query.Set("page_size", fmt.Sprintf("%d", defaultPageSize)) - if pageKey != "" { - query.Set("page_key", pageKey) - } - - var resp transactionsResponse - if err := c.get(ctx, path, query, &resp); err != nil { - tracer.OnError(err) - return nil, err - } - - for i := range resp.Transactions { - all = append(all, resp.Transactions[i].toTransaction()) - } - - if resp.NextPageKey == "" { - return all, nil - } - pageKey = resp.NextPageKey - } - err := errors.Errorf("page cap (%d) reached for partner user ref %q", maxPages, partnerUserRef) - tracer.OnError(err) - return nil, err -} - -// GetUserTransactionByOrderID looks up a single transaction by order ID -// within the partnerUserRef's namespace. Returns ErrTransactionNotFound if -// the order isn't found. The orderID alone is not queryable — callers must -// pair it with the partnerUserRef the order was created under. -func (c *Client) GetUserTransactionByOrderID(ctx context.Context, partnerUserRef, orderID string) (*Transaction, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserTransactionByOrderID") +// GetOrder returns the Onramp order with the given order ID. Returns +// ErrOrderNotFound if the order isn't present in the authenticated CDP +// project. +func (c *Client) GetOrder(ctx context.Context, orderID string) (*Order, error) { + tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetOrder") defer tracer.End() if orderID == "" { return nil, errors.New("order id is required") } - txns, err := c.GetUserTransactions(ctx, partnerUserRef) - if err != nil { + path := "/platform/v2/onramp/orders/" + url.PathEscape(orderID) + + var resp orderResponse + if err := c.get(ctx, path, &resp); err != nil { tracer.OnError(err) return nil, err } - for _, t := range txns { - if t.OrderID == orderID { - return t, nil - } - } - return nil, ErrTransactionNotFound + return resp.Order.toOrder(), nil } -func (c *Client) get(ctx context.Context, path string, query url.Values, out any) error { +func (c *Client) get(ctx context.Context, path string, out any) error { fullURL := "https://" + c.Host + path - if len(query) > 0 { - fullURL += "?" + query.Encode() - } var lastErr error _, err := c.retrier.Retry(func() error { @@ -153,7 +101,7 @@ func (c *Client) get(ctx context.Context, path string, query url.Values, out any defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - lastErr = ErrTransactionNotFound + lastErr = ErrOrderNotFound return nil // do not retry } if resp.StatusCode < 200 || resp.StatusCode >= 300 { @@ -178,46 +126,58 @@ func (c *Client) get(ctx context.Context, path string, query url.Values, out any return lastErr } -// transactionsResponse mirrors the JSON shape of the v1 transactions -// endpoint. Field names match Coinbase's snake_case payload. -type transactionsResponse struct { - Transactions []transactionPayload `json:"transactions"` - NextPageKey string `json:"next_page_key"` - TotalCount string `json:"total_count"` +// orderResponse mirrors the JSON envelope of the v2 order endpoint. +type orderResponse struct { + Order orderPayload `json:"order"` } -type transactionPayload struct { - Status string `json:"status"` - TxHash string `json:"tx_hash"` - WalletAddress string `json:"wallet_address"` - PurchaseAmount amountPayload `json:"purchase_amount"` - PurchaseCurrency string `json:"purchase_currency"` - PaymentTotal *amountPayload `json:"payment_total,omitempty"` - PartnerUserRef string `json:"partner_user_ref"` - PartnerUserID string `json:"partner_user_id"` // legacy field name - UserID string `json:"user_id"` - TransactionID string `json:"transaction_id"` // == orderId UUID - CreatedAt time.Time `json:"created_at"` +type orderPayload struct { + OrderID string `json:"orderId"` + Status string `json:"status"` + PaymentTotal string `json:"paymentTotal"` + PaymentSubtotal string `json:"paymentSubtotal"` + PaymentCurrency string `json:"paymentCurrency"` + PaymentMethod string `json:"paymentMethod"` + PurchaseAmount string `json:"purchaseAmount"` + PurchaseCurrency string `json:"purchaseCurrency"` + Fees []feePayload `json:"fees"` + ExchangeRate string `json:"exchangeRate"` + DestinationAddress string `json:"destinationAddress"` + DestinationNetwork string `json:"destinationNetwork"` + TxHash string `json:"txHash"` + PartnerUserRef string `json:"partnerUserRef"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } -type amountPayload struct { - Value string `json:"value"` +type feePayload struct { + Type string `json:"type"` + Amount string `json:"amount"` Currency string `json:"currency"` } -func (p transactionPayload) toTransaction() *Transaction { - ref := p.PartnerUserRef - if ref == "" { - ref = p.PartnerUserID +func (p orderPayload) toOrder() *Order { + fees := make([]Fee, 0, len(p.Fees)) + for _, f := range p.Fees { + fees = append(fees, Fee{ + Type: FeeType(f.Type), + Amount: Amount{Value: f.Amount, Currency: f.Currency}, + }) } - return &Transaction{ - OrderID: p.TransactionID, - PartnerUserRef: ref, - Status: TransactionStatus(p.Status), - TxHash: p.TxHash, - WalletAddress: p.WalletAddress, - PurchaseAmount: Amount{Value: p.PurchaseAmount.Value, Currency: p.PurchaseAmount.Currency}, - PurchaseAssetID: p.PurchaseCurrency, - CreatedAt: p.CreatedAt, + return &Order{ + OrderID: p.OrderID, + Status: OrderStatus(p.Status), + PaymentTotal: Amount{Value: p.PaymentTotal, Currency: p.PaymentCurrency}, + PaymentSubtotal: Amount{Value: p.PaymentSubtotal, Currency: p.PaymentCurrency}, + PaymentMethod: PaymentMethod(p.PaymentMethod), + PurchaseAmount: Amount{Value: p.PurchaseAmount, Currency: p.PurchaseCurrency}, + Fees: fees, + ExchangeRate: p.ExchangeRate, + DestinationAddress: p.DestinationAddress, + DestinationNetwork: p.DestinationNetwork, + TxHash: p.TxHash, + PartnerUserRef: p.PartnerUserRef, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, } } diff --git a/coinbase/order.go b/coinbase/order.go new file mode 100644 index 0000000..f2e5a03 --- /dev/null +++ b/coinbase/order.go @@ -0,0 +1,68 @@ +// Package coinbase provides a client for Coinbase Developer Platform APIs, +// scoped to the surfaces this repo currently consumes (Onramp order +// status). Authentication uses CDP API keys signed with Ed25519 (EdDSA), +// regenerated per request — see auth.go. +package coinbase + +import ( + "errors" + "time" +) + +type OrderStatus string + +const ( + OrderStatusUnknown OrderStatus = "" + OrderStatusPendingAuth OrderStatus = "ONRAMP_ORDER_STATUS_PENDING_AUTH" + OrderStatusPendingPayment OrderStatus = "ONRAMP_ORDER_STATUS_PENDING_PAYMENT" + OrderStatusProcessing OrderStatus = "ONRAMP_ORDER_STATUS_PROCESSING" + OrderStatusCompleted OrderStatus = "ONRAMP_ORDER_STATUS_COMPLETED" + OrderStatusFailed OrderStatus = "ONRAMP_ORDER_STATUS_FAILED" +) + +type PaymentMethod string + +const ( + PaymentMethodApplePay PaymentMethod = "GUEST_CHECKOUT_APPLE_PAY" + PaymentMethodGooglePay PaymentMethod = "GUEST_CHECKOUT_GOOGLE_PAY" +) + +type FeeType string + +const ( + FeeTypeNetwork FeeType = "FEE_TYPE_NETWORK" + FeeTypeExchange FeeType = "FEE_TYPE_EXCHANGE" +) + +// Order is a Coinbase Onramp order as returned by the v2 API. +type Order struct { + OrderID string // UUID assigned by Coinbase + Status OrderStatus + PaymentTotal Amount // Total charged to the buyer, including fees + PaymentSubtotal Amount // Charge before fees + PaymentMethod PaymentMethod + PurchaseAmount Amount // Amount of the purchased asset delivered + Fees []Fee + ExchangeRate string + DestinationAddress string + DestinationNetwork string + TxHash string // Empty until on-chain settlement + PartnerUserRef string // Stable per-user reference supplied by us at widget init + CreatedAt time.Time + UpdatedAt time.Time +} + +// Amount mirrors Coinbase's value/currency object. +type Amount struct { + Value string // Decimal string — preserve precision + Currency string +} + +type Fee struct { + Type FeeType + Amount Amount +} + +// ErrOrderNotFound is returned when the requested order ID isn't present in +// the authenticated CDP project. +var ErrOrderNotFound = errors.New("coinbase order not found") diff --git a/coinbase/transaction.go b/coinbase/transaction.go deleted file mode 100644 index 4ffe63d..0000000 --- a/coinbase/transaction.go +++ /dev/null @@ -1,45 +0,0 @@ -// Package coinbase provides a client for Coinbase Developer Platform APIs, -// scoped to the surfaces this repo currently consumes (Onramp transaction -// status). Authentication uses CDP API keys signed with Ed25519 (EdDSA), -// regenerated per request — see auth.go. -package coinbase - -import ( - "errors" - "time" -) - -type TransactionStatus string - -const ( - TransactionStatusUnknown TransactionStatus = "" - TransactionStatusCreated TransactionStatus = "ONRAMP_TRANSACTION_STATUS_CREATED" - TransactionStatusInProgress TransactionStatus = "ONRAMP_TRANSACTION_STATUS_IN_PROGRESS" - TransactionStatusSuccess TransactionStatus = "ONRAMP_TRANSACTION_STATUS_SUCCESS" - TransactionStatusFailed TransactionStatus = "ONRAMP_TRANSACTION_STATUS_FAILED" -) - -// Transaction is the subset of the Coinbase Onramp transaction record this -// repo cares about. The full payload is larger; fields not used by callers -// are intentionally omitted. -type Transaction struct { - OrderID string // UUID assigned by Coinbase - PartnerUserRef string // Stable per-user reference supplied by us at widget init - Status TransactionStatus - TxHash string // Empty until on-chain settlement - WalletAddress string // Destination wallet on the target chain - PurchaseAmount Amount // Amount in the purchased asset - PurchaseAssetID string // Coinbase asset identifier (e.g. "USDC") - CreatedAt time.Time -} - -// Amount mirrors Coinbase's value/currency object. -type Amount struct { - Value string // Decimal string — preserve precision - Currency string -} - -// ErrTransactionNotFound is returned when an order ID isn't present under the -// given partnerUserRef (either it doesn't exist in our project, or the -// partnerUserRef is wrong). -var ErrTransactionNotFound = errors.New("coinbase transaction not found")