package krypa
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/google/uuid"
)
const DefaultBaseURL = "https://api.partners.krypa.io/v1"
type Client struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{APIKey: apiKey, BaseURL: DefaultBaseURL, HTTPClient: http.DefaultClient}
}
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("krypa: %s: %s", e.Code, e.Message)
}
type errorEnvelope struct {
Error APIError `json:"error"`
}
type Money = string
type Recipient struct {
Type string `json:"type"`
Phone string `json:"phone,omitempty"`
BankBIK string `json:"bank_bik,omitempty"`
AccountNumber string `json:"account_number,omitempty"`
BankCode string `json:"bank_code,omitempty"`
AccountHolderName string `json:"account_holder_name,omitempty"`
CardNumber string `json:"card_number,omitempty"`
}
type CreatePayoutRequest struct {
ExternalID string `json:"external_id"`
FromCurrency string `json:"from_currency"`
FromAmount Money `json:"from_amount"`
ToCurrency string `json:"to_currency"`
PaymentMethod string `json:"payment_method"`
Recipient Recipient `json:"recipient"`
WebhookURL string `json:"webhook_url,omitempty"`
}
type Payout struct {
ID string `json:"id"`
ExternalID string `json:"external_id"`
Status string `json:"status"`
FromCurrency string `json:"from_currency"`
FromAmount Money `json:"from_amount"`
ToCurrency string `json:"to_currency"`
ToAmount Money `json:"to_amount"`
PaymentMethod string `json:"payment_method"`
Recipient Recipient `json:"recipient"`
FailureReason *string `json:"failure_reason,omitempty"`
EstimatedCompletion *string `json:"estimated_completion,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type PayoutList struct {
Data []Payout `json:"data"`
HasMore bool `json:"has_more"`
NextCursor *string `json:"next_cursor,omitempty"`
}
type PayoutListFilters struct {
Cursor string
Limit int
Status string
FromCurrency string
ToCurrency string
CreatedAfter string
CreatedBefore string
}
type CreateOnRampOrderRequest struct {
ExternalID string `json:"external_id"`
FromCurrency string `json:"from_currency"`
FromAmount Money `json:"from_amount"`
ToCurrency string `json:"to_currency"`
ToNetwork string `json:"to_network"`
ToAddress string `json:"to_address"`
PaymentMethod string `json:"payment_method"`
}
type PaymentInstructions struct {
Type string `json:"type"`
Phone string `json:"phone,omitempty"`
BankName string `json:"bank_name,omitempty"`
AccountNumber string `json:"account_number,omitempty"`
Reference string `json:"reference,omitempty"`
}
type OnRampOrder struct {
ID string `json:"id"`
ExternalID string `json:"external_id"`
Status string `json:"status"`
FromCurrency string `json:"from_currency"`
FromAmount Money `json:"from_amount"`
ToCurrency string `json:"to_currency"`
ToAmount Money `json:"to_amount"`
ToNetwork string `json:"to_network"`
ToAddress string `json:"to_address"`
PaymentMethod string `json:"payment_method"`
PaymentInstructions PaymentInstructions `json:"payment_instructions"`
FailureReason *string `json:"failure_reason,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type OnRampOrderList struct {
Data []OnRampOrder `json:"data"`
HasMore bool `json:"has_more"`
NextCursor *string `json:"next_cursor,omitempty"`
}
type OnRampOrderListFilters struct {
Cursor string
Limit int
Status string
FromCurrency string
ToCurrency string
CreatedAfter string
CreatedBefore string
}
type Corridor struct {
ID string `json:"id"`
Direction string `json:"direction"`
FromCurrency string `json:"from_currency"`
ToCurrency string `json:"to_currency"`
Country string `json:"country"`
PaymentMethod string `json:"payment_method"`
Status string `json:"status"`
MinAmount Money `json:"min_amount"`
MaxAmount Money `json:"max_amount"`
}
type CorridorListFilters struct {
Direction string
Status string
Country string
ToCurrency string
}
type Currency struct {
Code string `json:"code"`
Type string `json:"type"`
Decimals int `json:"decimals"`
Networks []string `json:"networks,omitempty"`
}
type CurrencyListFilters struct {
Type string
}
type PaymentMethod struct {
Code string `json:"code"`
Label string `json:"label"`
Directions []string `json:"directions"`
}
type PaymentMethodListFilters struct {
Direction string
}
type Balance struct {
Available Money `json:"available"`
Currency string `json:"currency"`
CreditLimit Money `json:"credit_limit"`
}
func (c *Client) do(method, path string, query url.Values, body any, idempotencyKey string, out any) error {
fullURL := strings.TrimRight(c.BaseURL, "/") + path
if len(query) > 0 {
fullURL += "?" + query.Encode()
}
var reader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
reader = bytes.NewReader(b)
}
req, err := http.NewRequest(method, fullURL, reader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.APIKey)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var envelope errorEnvelope
if jsonErr := json.Unmarshal(respBody, &envelope); jsonErr == nil && envelope.Error.Code != "" {
return &envelope.Error
}
return &APIError{Code: "http_error", Message: fmt.Sprintf("unexpected status %d", resp.StatusCode)}
}
if out == nil || len(respBody) == 0 {
return nil
}
return json.Unmarshal(respBody, out)
}
func (c *Client) CreatePayout(req CreatePayoutRequest, idempotencyKey string) (*Payout, error) {
if idempotencyKey == "" {
idempotencyKey = uuid.NewString()
}
var out Payout
if err := c.do(http.MethodPost, "/payouts", nil, req, idempotencyKey, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) GetPayout(id string) (*Payout, error) {
var out Payout
if err := c.do(http.MethodGet, "/payouts/"+id, nil, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) ListPayouts(filters PayoutListFilters) (*PayoutList, error) {
q := url.Values{}
if filters.Cursor != "" {
q.Set("cursor", filters.Cursor)
}
if filters.Limit > 0 {
q.Set("limit", strconv.Itoa(filters.Limit))
}
if filters.Status != "" {
q.Set("status", filters.Status)
}
if filters.FromCurrency != "" {
q.Set("from_currency", filters.FromCurrency)
}
if filters.ToCurrency != "" {
q.Set("to_currency", filters.ToCurrency)
}
if filters.CreatedAfter != "" {
q.Set("created_after", filters.CreatedAfter)
}
if filters.CreatedBefore != "" {
q.Set("created_before", filters.CreatedBefore)
}
var out PayoutList
if err := c.do(http.MethodGet, "/payouts", q, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) CancelPayout(id string) (*Payout, error) {
var out Payout
if err := c.do(http.MethodPost, "/payouts/"+id+"/cancel", nil, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) CreateOnRampOrder(req CreateOnRampOrderRequest, idempotencyKey string) (*OnRampOrder, error) {
if idempotencyKey == "" {
idempotencyKey = uuid.NewString()
}
var out OnRampOrder
if err := c.do(http.MethodPost, "/onramp-orders", nil, req, idempotencyKey, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) GetOnRampOrder(id string) (*OnRampOrder, error) {
var out OnRampOrder
if err := c.do(http.MethodGet, "/onramp-orders/"+id, nil, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) ListOnRampOrders(filters OnRampOrderListFilters) (*OnRampOrderList, error) {
q := url.Values{}
if filters.Cursor != "" {
q.Set("cursor", filters.Cursor)
}
if filters.Limit > 0 {
q.Set("limit", strconv.Itoa(filters.Limit))
}
if filters.Status != "" {
q.Set("status", filters.Status)
}
if filters.FromCurrency != "" {
q.Set("from_currency", filters.FromCurrency)
}
if filters.ToCurrency != "" {
q.Set("to_currency", filters.ToCurrency)
}
if filters.CreatedAfter != "" {
q.Set("created_after", filters.CreatedAfter)
}
if filters.CreatedBefore != "" {
q.Set("created_before", filters.CreatedBefore)
}
var out OnRampOrderList
if err := c.do(http.MethodGet, "/onramp-orders", q, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) CancelOnRampOrder(id string) (*OnRampOrder, error) {
var out OnRampOrder
if err := c.do(http.MethodPost, "/onramp-orders/"+id+"/cancel", nil, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) ListCorridors(filters CorridorListFilters) ([]Corridor, error) {
q := url.Values{}
if filters.Direction != "" {
q.Set("direction", filters.Direction)
}
if filters.Status != "" {
q.Set("status", filters.Status)
}
if filters.Country != "" {
q.Set("country", filters.Country)
}
if filters.ToCurrency != "" {
q.Set("to_currency", filters.ToCurrency)
}
var out struct {
Data []Corridor `json:"data"`
}
if err := c.do(http.MethodGet, "/corridors", q, nil, "", &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) ListCurrencies(filters CurrencyListFilters) ([]Currency, error) {
q := url.Values{}
if filters.Type != "" {
q.Set("type", filters.Type)
}
var out struct {
Data []Currency `json:"data"`
}
if err := c.do(http.MethodGet, "/currencies", q, nil, "", &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) ListPaymentMethods(filters PaymentMethodListFilters) ([]PaymentMethod, error) {
q := url.Values{}
if filters.Direction != "" {
q.Set("direction", filters.Direction)
}
var out struct {
Data []PaymentMethod `json:"data"`
}
if err := c.do(http.MethodGet, "/payment-methods", q, nil, "", &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) GetBalance() (*Balance, error) {
var out Balance
if err := c.do(http.MethodGet, "/balance", nil, nil, "", &out); err != nil {
return nil, err
}
return &out, nil
}
func VerifyWebhookSignature(payload []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}