swapped-commerce-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +948 -0
- package/package.json +68 -0
- package/src/client/createClient.ts +182 -0
- package/src/index.ts +29 -0
- package/src/resources/balances.ts +39 -0
- package/src/resources/kyc.ts +39 -0
- package/src/resources/orders.ts +54 -0
- package/src/resources/paymentLinks.ts +24 -0
- package/src/resources/paymentRoutes.ts +24 -0
- package/src/resources/payments.ts +20 -0
- package/src/resources/payouts.ts +58 -0
- package/src/resources/quotes.ts +24 -0
- package/src/types/balances.ts +27 -0
- package/src/types/common.ts +46 -0
- package/src/types/currencies.ts +26 -0
- package/src/types/index.ts +93 -0
- package/src/types/kyc.ts +65 -0
- package/src/types/orders.ts +122 -0
- package/src/types/payments.ts +115 -0
- package/src/types/payouts.ts +68 -0
- package/src/types/quotes.ts +49 -0
- package/src/types/webhooks.ts +31 -0
- package/src/utils/errors.ts +104 -0
- package/src/utils/http.ts +180 -0
- package/src/utils/retry.ts +57 -0
- package/src/utils/webhooks.ts +82 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RequiredSwappedConfig,
|
|
3
|
+
ApiResponse,
|
|
4
|
+
} from '../types/common'
|
|
5
|
+
import {
|
|
6
|
+
SwappedError,
|
|
7
|
+
SwappedAuthenticationError,
|
|
8
|
+
SwappedValidationError,
|
|
9
|
+
SwappedRateLimitError,
|
|
10
|
+
SwappedNotFoundError,
|
|
11
|
+
} from './errors'
|
|
12
|
+
import { withRetry } from './retry'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HTTP method types
|
|
16
|
+
*/
|
|
17
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base URL for Swapped Commerce API
|
|
21
|
+
*/
|
|
22
|
+
const BASE_URL = 'https://pay-api.swapped.com'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pure function for building URL with query parameters
|
|
26
|
+
*/
|
|
27
|
+
export function buildUrl(
|
|
28
|
+
baseUrl: string,
|
|
29
|
+
path: string,
|
|
30
|
+
params?: Readonly<Record<string, unknown>>
|
|
31
|
+
): string {
|
|
32
|
+
const url = new URL(path, baseUrl)
|
|
33
|
+
|
|
34
|
+
if (params) {
|
|
35
|
+
for (const [key, value] of Object.entries(params)) {
|
|
36
|
+
if (value !== undefined && value !== null) {
|
|
37
|
+
url.searchParams.append(key, String(value))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return url.toString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pure function for creating request configuration
|
|
47
|
+
*/
|
|
48
|
+
export function createRequestConfig(
|
|
49
|
+
config: RequiredSwappedConfig,
|
|
50
|
+
method: HttpMethod,
|
|
51
|
+
path: string,
|
|
52
|
+
body?: unknown
|
|
53
|
+
): RequestInit {
|
|
54
|
+
const headers: Record<string, string> = {
|
|
55
|
+
'X-API-Key': config.apiKey,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'Accept': 'application/json',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const requestInit: RequestInit = {
|
|
61
|
+
method,
|
|
62
|
+
headers,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (body !== undefined) {
|
|
66
|
+
requestInit.body = JSON.stringify(body)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return requestInit
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pure function for handling error responses
|
|
74
|
+
*/
|
|
75
|
+
async function handleErrorResponse(
|
|
76
|
+
response: Response
|
|
77
|
+
): Promise<SwappedError> {
|
|
78
|
+
let errorData: {
|
|
79
|
+
message?: string
|
|
80
|
+
code?: string
|
|
81
|
+
details?: Readonly<Record<string, unknown>>
|
|
82
|
+
} = {}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const text = await response.text()
|
|
86
|
+
if (text) {
|
|
87
|
+
errorData = JSON.parse(text) as typeof errorData
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// If parsing fails, use empty object
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const message = errorData.message ?? response.statusText
|
|
94
|
+
|
|
95
|
+
switch (response.status) {
|
|
96
|
+
case 401:
|
|
97
|
+
return new SwappedAuthenticationError(message)
|
|
98
|
+
case 400:
|
|
99
|
+
return new SwappedValidationError(message, errorData.details)
|
|
100
|
+
case 404:
|
|
101
|
+
return new SwappedNotFoundError(message)
|
|
102
|
+
case 429:
|
|
103
|
+
return new SwappedRateLimitError(message)
|
|
104
|
+
default:
|
|
105
|
+
return new SwappedError(
|
|
106
|
+
message,
|
|
107
|
+
response.status,
|
|
108
|
+
errorData.code,
|
|
109
|
+
errorData.details
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pure function for making HTTP requests with retry logic
|
|
116
|
+
*/
|
|
117
|
+
export async function request<T>(
|
|
118
|
+
config: RequiredSwappedConfig,
|
|
119
|
+
method: HttpMethod,
|
|
120
|
+
path: string,
|
|
121
|
+
options?: {
|
|
122
|
+
readonly body?: unknown
|
|
123
|
+
readonly params?: Readonly<Record<string, unknown>>
|
|
124
|
+
}
|
|
125
|
+
): Promise<ApiResponse<T>> {
|
|
126
|
+
const url = buildUrl(BASE_URL, path, options?.params)
|
|
127
|
+
const requestConfig = createRequestConfig(config, method, path, options?.body)
|
|
128
|
+
|
|
129
|
+
// Create abort controller for timeout
|
|
130
|
+
const controller = new AbortController()
|
|
131
|
+
const timeoutId = setTimeout(() => {
|
|
132
|
+
controller.abort()
|
|
133
|
+
}, config.timeout)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await withRetry(
|
|
137
|
+
async () => {
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
...requestConfig,
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
throw await handleErrorResponse(res)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return res
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
maxRetries: config.retries,
|
|
151
|
+
timeout: config.timeout,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const data = (await response.json()) as ApiResponse<T>
|
|
156
|
+
return data
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof SwappedError) {
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle abort/timeout
|
|
163
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
164
|
+
throw new SwappedError(
|
|
165
|
+
`Request timeout after ${config.timeout}ms`,
|
|
166
|
+
408,
|
|
167
|
+
'TIMEOUT_ERROR'
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle network errors
|
|
172
|
+
throw new SwappedError(
|
|
173
|
+
error instanceof Error ? error.message : 'Network error occurred',
|
|
174
|
+
0,
|
|
175
|
+
'NETWORK_ERROR'
|
|
176
|
+
)
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timeoutId)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SwappedError } from './errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryConfig {
|
|
7
|
+
readonly maxRetries: number
|
|
8
|
+
readonly timeout: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pure function for retrying with exponential backoff
|
|
13
|
+
*
|
|
14
|
+
* @param fn - Function to retry
|
|
15
|
+
* @param config - Retry configuration
|
|
16
|
+
* @returns Promise that resolves with the function result or rejects after all retries
|
|
17
|
+
*/
|
|
18
|
+
export async function withRetry<T>(
|
|
19
|
+
fn: () => Promise<T>,
|
|
20
|
+
config: RetryConfig
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
let lastError: Error | undefined
|
|
23
|
+
|
|
24
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
25
|
+
try {
|
|
26
|
+
return await fn()
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error as Error
|
|
29
|
+
|
|
30
|
+
// Don't retry on client errors (4xx) except 429 (rate limit)
|
|
31
|
+
if (
|
|
32
|
+
error instanceof SwappedError &&
|
|
33
|
+
error.statusCode !== undefined &&
|
|
34
|
+
error.statusCode >= 400 &&
|
|
35
|
+
error.statusCode < 500 &&
|
|
36
|
+
error.statusCode !== 429
|
|
37
|
+
) {
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wait before retry (exponential backoff)
|
|
42
|
+
if (attempt < config.maxRetries) {
|
|
43
|
+
const delayMs = Math.pow(2, attempt) * 1000
|
|
44
|
+
await delay(delayMs)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw lastError ?? new Error('Request failed after retries')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Delay helper function
|
|
54
|
+
*/
|
|
55
|
+
function delay(ms: number): Promise<void> {
|
|
56
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
57
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { WebhookEvent } from '../types/webhooks'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure function for verifying webhook signature using Web Crypto API
|
|
5
|
+
*
|
|
6
|
+
* @param payload - Raw webhook payload string
|
|
7
|
+
* @param signature - Signature from X-Signature header
|
|
8
|
+
* @param secret - Webhook secret key
|
|
9
|
+
* @returns Promise that resolves to true if signature is valid
|
|
10
|
+
*/
|
|
11
|
+
export async function verifyWebhookSignature(
|
|
12
|
+
payload: string,
|
|
13
|
+
signature: string,
|
|
14
|
+
secret: string
|
|
15
|
+
): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
// Import the secret key
|
|
18
|
+
const encoder = new TextEncoder()
|
|
19
|
+
const keyData = encoder.encode(secret)
|
|
20
|
+
const key = await crypto.subtle.importKey(
|
|
21
|
+
'raw',
|
|
22
|
+
keyData,
|
|
23
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
24
|
+
false,
|
|
25
|
+
['sign']
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Sign the payload
|
|
29
|
+
const payloadData = encoder.encode(payload)
|
|
30
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
31
|
+
'HMAC',
|
|
32
|
+
key,
|
|
33
|
+
payloadData
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Convert signature to hex string
|
|
37
|
+
const signatureArray = Array.from(new Uint8Array(signatureBuffer))
|
|
38
|
+
const expectedSignature = signatureArray
|
|
39
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
40
|
+
.join('')
|
|
41
|
+
|
|
42
|
+
// Compare signatures using constant-time comparison
|
|
43
|
+
return constantTimeEqual(expectedSignature, signature)
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Constant-time string comparison to prevent timing attacks
|
|
51
|
+
*/
|
|
52
|
+
function constantTimeEqual(a: string, b: string): boolean {
|
|
53
|
+
if (a.length !== b.length) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let result = 0
|
|
58
|
+
for (let i = 0; i < a.length; i++) {
|
|
59
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result === 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pure function for parsing webhook event payload
|
|
67
|
+
*
|
|
68
|
+
* @param payload - JSON string payload
|
|
69
|
+
* @returns Parsed webhook event
|
|
70
|
+
* @throws Error if payload is invalid JSON
|
|
71
|
+
*/
|
|
72
|
+
export function parseWebhookEvent(payload: string): WebhookEvent {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(payload) as WebhookEvent
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Failed to parse webhook payload: ${
|
|
78
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
79
|
+
}`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|