@smoothsend/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 +22 -0
- package/README.md +346 -0
- package/dist/adapters/aptos.d.ts +98 -0
- package/dist/adapters/aptos.d.ts.map +1 -0
- package/dist/core/SmoothSendSDK.d.ts +261 -0
- package/dist/core/SmoothSendSDK.d.ts.map +1 -0
- package/dist/index.d.ts +2072 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +2076 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2097 -0
- package/dist/index.js.map +1 -0
- package/dist/script-composer/ScriptComposerClient.d.ts +263 -0
- package/dist/script-composer/ScriptComposerClient.d.ts.map +1 -0
- package/dist/script-composer/index.d.ts +42 -0
- package/dist/script-composer/index.d.ts.map +1 -0
- package/dist/shared-constants.d.ts +103 -0
- package/dist/shared-constants.d.ts.map +1 -0
- package/dist/types/errors.d.ts +216 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +939 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/http.d.ts +77 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/wallet-adapter/SmoothSendTransactionSubmitter.d.ts +179 -0
- package/dist/wallet-adapter/SmoothSendTransactionSubmitter.d.ts.map +1 -0
- package/dist/wallet-adapter/index.d.ts +20 -0
- package/dist/wallet-adapter/index.d.ts.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var axios = require('axios');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error handling system for SmoothSend SDK v2
|
|
9
|
+
* Provides typed error classes for different failure scenarios
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* All SDK errors extend SmoothSendError for consistent error handling
|
|
13
|
+
* Use instanceof checks to handle specific error types
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* try {
|
|
18
|
+
* await sdk.transfer(request, wallet);
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* if (error instanceof AuthenticationError) {
|
|
21
|
+
* console.error('Invalid API key');
|
|
22
|
+
* } else if (error instanceof RateLimitError) {
|
|
23
|
+
* console.error('Rate limit exceeded');
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Base error class for all SmoothSend SDK errors
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* All SDK-specific errors extend this class
|
|
33
|
+
* Contains error code, HTTP status code, and additional details
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* throw new SmoothSendError(
|
|
38
|
+
* 'Something went wrong',
|
|
39
|
+
* 'CUSTOM_ERROR',
|
|
40
|
+
* 500,
|
|
41
|
+
* { additionalInfo: 'details' }
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
class SmoothSendError extends Error {
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new SmoothSendError
|
|
48
|
+
*
|
|
49
|
+
* @param message - Human-readable error message
|
|
50
|
+
* @param code - Error code for programmatic handling
|
|
51
|
+
* @param statusCode - HTTP status code (if applicable)
|
|
52
|
+
* @param details - Additional error details
|
|
53
|
+
*/
|
|
54
|
+
constructor(message, code, statusCode, details) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.statusCode = statusCode;
|
|
58
|
+
this.details = details;
|
|
59
|
+
this.name = 'SmoothSendError';
|
|
60
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
61
|
+
if (Error.captureStackTrace) {
|
|
62
|
+
Error.captureStackTrace(this, this.constructor);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Authentication error - thrown when API key is invalid, missing, or expired
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* HTTP Status Code: 401
|
|
71
|
+
* Indicates authentication failure with the proxy worker
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* try {
|
|
76
|
+
* await sdk.transfer(request, wallet);
|
|
77
|
+
* } catch (error) {
|
|
78
|
+
* if (error instanceof AuthenticationError) {
|
|
79
|
+
* console.error('Invalid API key:', error.message);
|
|
80
|
+
* console.log('Get a new key at:', error.details.suggestion);
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
class AuthenticationError extends SmoothSendError {
|
|
86
|
+
/**
|
|
87
|
+
* Creates a new AuthenticationError
|
|
88
|
+
*
|
|
89
|
+
* @param message - Human-readable error message
|
|
90
|
+
* @param details - Additional error details
|
|
91
|
+
*/
|
|
92
|
+
constructor(message, details) {
|
|
93
|
+
super(message, 'AUTHENTICATION_ERROR', 401, {
|
|
94
|
+
...details,
|
|
95
|
+
docs: 'https://docs.smoothsend.xyz/api-keys',
|
|
96
|
+
suggestion: 'Check your API key at dashboard.smoothsend.xyz'
|
|
97
|
+
});
|
|
98
|
+
this.name = 'AuthenticationError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Rate limit error - thrown when request rate limit is exceeded
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* HTTP Status Code: 429
|
|
106
|
+
* Contains rate limit details including reset time
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* try {
|
|
111
|
+
* await sdk.transfer(request, wallet);
|
|
112
|
+
* } catch (error) {
|
|
113
|
+
* if (error instanceof RateLimitError) {
|
|
114
|
+
* console.error('Rate limit exceeded');
|
|
115
|
+
* console.log(`Limit: ${error.limit}`);
|
|
116
|
+
* console.log(`Remaining: ${error.remaining}`);
|
|
117
|
+
* console.log(`Resets at: ${error.resetTime}`);
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
class RateLimitError extends SmoothSendError {
|
|
123
|
+
/**
|
|
124
|
+
* Creates a new RateLimitError
|
|
125
|
+
*
|
|
126
|
+
* @param message - Human-readable error message
|
|
127
|
+
* @param limit - Maximum requests allowed per period
|
|
128
|
+
* @param remaining - Remaining requests in current period
|
|
129
|
+
* @param resetTime - When the rate limit resets (ISO 8601 timestamp)
|
|
130
|
+
*/
|
|
131
|
+
constructor(message, limit, remaining, resetTime) {
|
|
132
|
+
super(message, 'RATE_LIMIT_EXCEEDED', 429, {
|
|
133
|
+
limit,
|
|
134
|
+
remaining,
|
|
135
|
+
resetTime,
|
|
136
|
+
docs: 'https://docs.smoothsend.xyz/rate-limits',
|
|
137
|
+
suggestion: 'Wait until rate limit resets or upgrade your tier'
|
|
138
|
+
});
|
|
139
|
+
this.limit = limit;
|
|
140
|
+
this.remaining = remaining;
|
|
141
|
+
this.resetTime = resetTime;
|
|
142
|
+
this.name = 'RateLimitError';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Validation error - thrown when request parameters are invalid
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* HTTP Status Code: 400
|
|
150
|
+
* Contains field name that failed validation
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* try {
|
|
155
|
+
* await sdk.transfer(request, wallet);
|
|
156
|
+
* } catch (error) {
|
|
157
|
+
* if (error instanceof ValidationError) {
|
|
158
|
+
* console.error(`Invalid ${error.field}:`, error.message);
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
class ValidationError extends SmoothSendError {
|
|
164
|
+
/**
|
|
165
|
+
* Creates a new ValidationError
|
|
166
|
+
*
|
|
167
|
+
* @param message - Human-readable error message
|
|
168
|
+
* @param field - Name of the field that failed validation
|
|
169
|
+
* @param details - Additional error details
|
|
170
|
+
*/
|
|
171
|
+
constructor(message, field, details) {
|
|
172
|
+
super(message, 'VALIDATION_ERROR', 400, {
|
|
173
|
+
field,
|
|
174
|
+
...details,
|
|
175
|
+
suggestion: `Check the '${field}' parameter and try again`
|
|
176
|
+
});
|
|
177
|
+
this.field = field;
|
|
178
|
+
this.name = 'ValidationError';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Network error - thrown when network connectivity issues occur
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* HTTP Status Code: 0 (no HTTP response)
|
|
186
|
+
* Indicates network connectivity problems
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* try {
|
|
191
|
+
* await sdk.transfer(request, wallet);
|
|
192
|
+
* } catch (error) {
|
|
193
|
+
* if (error instanceof NetworkError) {
|
|
194
|
+
* console.error('Network error:', error.message);
|
|
195
|
+
* console.log('Original error:', error.originalError);
|
|
196
|
+
* }
|
|
197
|
+
* }
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
class NetworkError extends SmoothSendError {
|
|
201
|
+
/**
|
|
202
|
+
* Creates a new NetworkError
|
|
203
|
+
*
|
|
204
|
+
* @param message - Human-readable error message
|
|
205
|
+
* @param originalError - Original error that caused the network failure
|
|
206
|
+
*/
|
|
207
|
+
constructor(message, originalError) {
|
|
208
|
+
super(message, 'NETWORK_ERROR', 0, {
|
|
209
|
+
originalError: originalError?.message,
|
|
210
|
+
suggestion: 'Check your internet connection and try again'
|
|
211
|
+
});
|
|
212
|
+
this.originalError = originalError;
|
|
213
|
+
this.name = 'NetworkError';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Helper function to create appropriate error from HTTP response
|
|
218
|
+
*
|
|
219
|
+
* @remarks
|
|
220
|
+
* Parses HTTP error response and creates typed error object
|
|
221
|
+
* Used internally by HTTP client
|
|
222
|
+
*
|
|
223
|
+
* @param statusCode - HTTP status code
|
|
224
|
+
* @param errorData - Error response data from API
|
|
225
|
+
* @param defaultMessage - Default message if none provided in response
|
|
226
|
+
* @returns Typed error object
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const error = createErrorFromResponse(401, {
|
|
231
|
+
* error: 'Invalid API key',
|
|
232
|
+
* details: { field: 'apiKey' }
|
|
233
|
+
* });
|
|
234
|
+
* throw error;
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
function createErrorFromResponse(statusCode, errorData, defaultMessage = 'An error occurred') {
|
|
238
|
+
const message = errorData?.error || errorData?.message || defaultMessage;
|
|
239
|
+
const details = errorData?.details || {};
|
|
240
|
+
switch (statusCode) {
|
|
241
|
+
case 401:
|
|
242
|
+
return new AuthenticationError(message, details);
|
|
243
|
+
case 429:
|
|
244
|
+
return new RateLimitError(message, parseInt(details.limit || '0'), parseInt(details.remaining || '0'), details.reset || details.resetTime || '');
|
|
245
|
+
case 400:
|
|
246
|
+
return new ValidationError(message, details.field || 'unknown', details);
|
|
247
|
+
default:
|
|
248
|
+
return new SmoothSendError(message, errorData?.errorCode || 'UNKNOWN_ERROR', statusCode, details);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Helper function to create network error from exception
|
|
253
|
+
*
|
|
254
|
+
* @remarks
|
|
255
|
+
* Wraps generic exceptions in NetworkError for consistent error handling
|
|
256
|
+
* Used internally by HTTP client
|
|
257
|
+
*
|
|
258
|
+
* @param error - Original error or exception
|
|
259
|
+
* @returns NetworkError instance
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* try {
|
|
264
|
+
* await fetch(url);
|
|
265
|
+
* } catch (error) {
|
|
266
|
+
* throw createNetworkError(error);
|
|
267
|
+
* }
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
function createNetworkError(error) {
|
|
271
|
+
const message = error?.message || 'Network request failed';
|
|
272
|
+
return new NetworkError(message, error instanceof Error ? error : undefined);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Export error classes
|
|
276
|
+
/**
|
|
277
|
+
* Mapping of supported chains to their respective ecosystems
|
|
278
|
+
* Used internally for adapter selection and routing
|
|
279
|
+
*/
|
|
280
|
+
const CHAIN_ECOSYSTEM_MAP = {
|
|
281
|
+
'avalanche': 'evm',
|
|
282
|
+
'aptos-testnet': 'aptos',
|
|
283
|
+
'aptos-mainnet': 'aptos'
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Aptos-specific error codes
|
|
287
|
+
* Used for detailed error handling in Aptos adapter
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* Error codes are prefixed with APTOS_ for easy identification
|
|
291
|
+
*/
|
|
292
|
+
const APTOS_ERROR_CODES = {
|
|
293
|
+
// Signature verification errors
|
|
294
|
+
/** Missing signature in request */
|
|
295
|
+
MISSING_SIGNATURE: 'APTOS_MISSING_SIGNATURE',
|
|
296
|
+
/** Missing public key for verification */
|
|
297
|
+
MISSING_PUBLIC_KEY: 'APTOS_MISSING_PUBLIC_KEY',
|
|
298
|
+
/** Invalid signature format */
|
|
299
|
+
INVALID_SIGNATURE_FORMAT: 'APTOS_INVALID_SIGNATURE_FORMAT',
|
|
300
|
+
/** Invalid public key format */
|
|
301
|
+
INVALID_PUBLIC_KEY_FORMAT: 'APTOS_INVALID_PUBLIC_KEY_FORMAT',
|
|
302
|
+
/** Address doesn't match public key */
|
|
303
|
+
ADDRESS_MISMATCH: 'APTOS_ADDRESS_MISMATCH',
|
|
304
|
+
/** Signature verification failed */
|
|
305
|
+
SIGNATURE_VERIFICATION_FAILED: 'APTOS_SIGNATURE_VERIFICATION_FAILED',
|
|
306
|
+
// Transaction errors
|
|
307
|
+
/** Missing transaction data */
|
|
308
|
+
MISSING_TRANSACTION_DATA: 'APTOS_MISSING_TRANSACTION_DATA',
|
|
309
|
+
/** Invalid transaction format */
|
|
310
|
+
INVALID_TRANSACTION_FORMAT: 'APTOS_INVALID_TRANSACTION_FORMAT',
|
|
311
|
+
// Address validation errors
|
|
312
|
+
/** Empty address provided */
|
|
313
|
+
EMPTY_ADDRESS: 'APTOS_EMPTY_ADDRESS',
|
|
314
|
+
/** Invalid address format */
|
|
315
|
+
INVALID_ADDRESS_FORMAT: 'APTOS_INVALID_ADDRESS_FORMAT',
|
|
316
|
+
// General errors
|
|
317
|
+
/** Error fetching quote */
|
|
318
|
+
QUOTE_ERROR: 'APTOS_QUOTE_ERROR',
|
|
319
|
+
/** Error executing transfer */
|
|
320
|
+
EXECUTE_ERROR: 'APTOS_EXECUTE_ERROR',
|
|
321
|
+
/** Error fetching balance */
|
|
322
|
+
BALANCE_ERROR: 'APTOS_BALANCE_ERROR',
|
|
323
|
+
/** Error fetching token info */
|
|
324
|
+
TOKEN_INFO_ERROR: 'APTOS_TOKEN_INFO_ERROR',
|
|
325
|
+
/** Error checking status */
|
|
326
|
+
STATUS_ERROR: 'APTOS_STATUS_ERROR',
|
|
327
|
+
/** Error calling Move function */
|
|
328
|
+
MOVE_CALL_ERROR: 'APTOS_MOVE_CALL_ERROR',
|
|
329
|
+
/** Unsupported token */
|
|
330
|
+
UNSUPPORTED_TOKEN: 'APTOS_UNSUPPORTED_TOKEN'
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Shared Constants for SmoothSend Platform
|
|
335
|
+
*
|
|
336
|
+
* IMPORTANT: This file must be kept in sync across all repositories:
|
|
337
|
+
* - smoothsend-worker-proxy/src/shared-constants.ts
|
|
338
|
+
* - smoothsend-dev-console/src/lib/shared-constants.ts
|
|
339
|
+
* - smoothsend-sdk/src/shared-constants.ts
|
|
340
|
+
*
|
|
341
|
+
* Any changes to these constants must be replicated to all three locations.
|
|
342
|
+
*/
|
|
343
|
+
/**
|
|
344
|
+
* API Key Prefixes
|
|
345
|
+
* Used to identify key types from their prefix
|
|
346
|
+
*/
|
|
347
|
+
/**
|
|
348
|
+
* Usage Response Headers
|
|
349
|
+
* Headers sent by worker and read by SDK/Console
|
|
350
|
+
*
|
|
351
|
+
* IMPORTANT: Header names must match exactly
|
|
352
|
+
*/
|
|
353
|
+
const USAGE_HEADERS = {
|
|
354
|
+
RATE_LIMIT: 'X-Rate-Limit-Limit',
|
|
355
|
+
RATE_REMAINING: 'X-Rate-Limit-Remaining',
|
|
356
|
+
RATE_RESET: 'X-Rate-Limit-Reset',
|
|
357
|
+
MONTHLY_LIMIT: 'X-Monthly-Limit',
|
|
358
|
+
MONTHLY_USAGE: 'X-Monthly-Usage',
|
|
359
|
+
MONTHLY_REMAINING: 'X-Monthly-Remaining',
|
|
360
|
+
REQUEST_ID: 'X-Request-ID'};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* HTTP Client for proxy worker integration
|
|
364
|
+
* Handles authentication, rate limiting, usage tracking, and retry logic
|
|
365
|
+
*/
|
|
366
|
+
class HttpClient {
|
|
367
|
+
/**
|
|
368
|
+
* Constructor supports both old (baseURL, timeout) and new (config object) patterns
|
|
369
|
+
* for backward compatibility during migration
|
|
370
|
+
*/
|
|
371
|
+
constructor(configOrBaseURL, timeout) {
|
|
372
|
+
// Determine if using new config object or old baseURL pattern
|
|
373
|
+
if (typeof configOrBaseURL === 'string') {
|
|
374
|
+
// Old pattern: constructor(baseURL, timeout)
|
|
375
|
+
this.baseURL = configOrBaseURL;
|
|
376
|
+
this.network = 'testnet';
|
|
377
|
+
this.maxRetries = 3;
|
|
378
|
+
this.isProxyMode = false;
|
|
379
|
+
this.includeOrigin = false;
|
|
380
|
+
this.client = axios.create({
|
|
381
|
+
baseURL: this.baseURL,
|
|
382
|
+
timeout: timeout || 30000,
|
|
383
|
+
headers: {
|
|
384
|
+
'Content-Type': 'application/json',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// New pattern: constructor(config)
|
|
390
|
+
const config = configOrBaseURL;
|
|
391
|
+
this.apiKey = config.apiKey;
|
|
392
|
+
this.network = config.network || 'testnet';
|
|
393
|
+
this.maxRetries = config.retries || 3;
|
|
394
|
+
this.baseURL = 'https://proxy.smoothsend.xyz';
|
|
395
|
+
this.isProxyMode = true;
|
|
396
|
+
this.includeOrigin = config.includeOrigin || false;
|
|
397
|
+
const headers = {
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
400
|
+
'X-Network': this.network,
|
|
401
|
+
...config.customHeaders,
|
|
402
|
+
};
|
|
403
|
+
// Add Origin header if in browser and includeOrigin is true
|
|
404
|
+
if (this.includeOrigin && typeof window !== 'undefined' && window.location) {
|
|
405
|
+
headers['Origin'] = window.location.origin;
|
|
406
|
+
}
|
|
407
|
+
this.client = axios.create({
|
|
408
|
+
baseURL: this.baseURL,
|
|
409
|
+
timeout: config.timeout || 30000,
|
|
410
|
+
headers,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// Request interceptor - add timestamp to prevent caching
|
|
414
|
+
this.client.interceptors.request.use((config) => {
|
|
415
|
+
config.params = { ...config.params, _t: Date.now() };
|
|
416
|
+
return config;
|
|
417
|
+
}, (error) => Promise.reject(error));
|
|
418
|
+
// Response interceptor - handle errors but don't transform responses
|
|
419
|
+
this.client.interceptors.response.use((response) => response, (error) => {
|
|
420
|
+
// Let the request methods handle errors for proper retry logic
|
|
421
|
+
return Promise.reject(error);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Extract usage metadata from response headers (proxy mode only)
|
|
426
|
+
* Uses USAGE_HEADERS constants for consistency across systems
|
|
427
|
+
*/
|
|
428
|
+
extractMetadata(response) {
|
|
429
|
+
if (!this.isProxyMode) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
// Convert header names to lowercase for case-insensitive lookup
|
|
433
|
+
const headers = response.headers;
|
|
434
|
+
return {
|
|
435
|
+
rateLimit: {
|
|
436
|
+
limit: headers[USAGE_HEADERS.RATE_LIMIT.toLowerCase()] || '0',
|
|
437
|
+
remaining: headers[USAGE_HEADERS.RATE_REMAINING.toLowerCase()] || '0',
|
|
438
|
+
reset: headers[USAGE_HEADERS.RATE_RESET.toLowerCase()] || '',
|
|
439
|
+
},
|
|
440
|
+
monthly: {
|
|
441
|
+
limit: headers[USAGE_HEADERS.MONTHLY_LIMIT.toLowerCase()] || '0',
|
|
442
|
+
usage: headers[USAGE_HEADERS.MONTHLY_USAGE.toLowerCase()] || '0',
|
|
443
|
+
remaining: headers[USAGE_HEADERS.MONTHLY_REMAINING.toLowerCase()] || '0',
|
|
444
|
+
},
|
|
445
|
+
requestId: headers[USAGE_HEADERS.REQUEST_ID.toLowerCase()] || '',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Determine if an error should be retried
|
|
450
|
+
*/
|
|
451
|
+
shouldRetry(error, attempt) {
|
|
452
|
+
// Don't retry if we've exceeded max retries
|
|
453
|
+
if (attempt >= this.maxRetries) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
// Only retry in proxy mode (legacy mode doesn't have retry logic)
|
|
457
|
+
if (!this.isProxyMode) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
// Network errors - retry
|
|
461
|
+
if (!error.response) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
const status = error.response.status;
|
|
465
|
+
// 5xx server errors - retry
|
|
466
|
+
if (status >= 500 && status < 600) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
// 4xx client errors - don't retry (including 429 rate limit)
|
|
470
|
+
if (status >= 400 && status < 500) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Calculate exponential backoff delay with jitter
|
|
477
|
+
*/
|
|
478
|
+
calculateBackoff(attempt) {
|
|
479
|
+
const baseDelay = 1000; // 1 second
|
|
480
|
+
const maxDelay = 10000; // 10 seconds
|
|
481
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
482
|
+
const delay = Math.min(exponentialDelay, maxDelay);
|
|
483
|
+
// Add jitter (±20%)
|
|
484
|
+
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
|
|
485
|
+
return Math.floor(delay + jitter);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Execute request with retry logic (proxy mode) or single attempt (legacy mode)
|
|
489
|
+
*/
|
|
490
|
+
async executeWithRetry(operation) {
|
|
491
|
+
let lastError;
|
|
492
|
+
const maxAttempts = this.isProxyMode ? this.maxRetries : 0;
|
|
493
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
494
|
+
try {
|
|
495
|
+
const response = await operation();
|
|
496
|
+
// Success - extract metadata (if proxy mode) and return
|
|
497
|
+
const result = {
|
|
498
|
+
success: true,
|
|
499
|
+
data: response.data,
|
|
500
|
+
};
|
|
501
|
+
const metadata = this.extractMetadata(response);
|
|
502
|
+
if (metadata) {
|
|
503
|
+
result.metadata = metadata;
|
|
504
|
+
}
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
lastError = error;
|
|
509
|
+
// Check if we should retry
|
|
510
|
+
if (!this.shouldRetry(error, attempt)) {
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
// Wait before retrying (except on last attempt)
|
|
514
|
+
if (attempt < maxAttempts) {
|
|
515
|
+
const delay = this.calculateBackoff(attempt);
|
|
516
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// All retries failed - handle error
|
|
521
|
+
return this.handleError(lastError);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Handle errors and create appropriate error responses
|
|
525
|
+
*/
|
|
526
|
+
handleError(error) {
|
|
527
|
+
if (this.isProxyMode) {
|
|
528
|
+
// Proxy mode: use typed errors
|
|
529
|
+
if (error.response) {
|
|
530
|
+
// Server responded with error status
|
|
531
|
+
const { status, data } = error.response;
|
|
532
|
+
const sdkError = createErrorFromResponse(status, data);
|
|
533
|
+
throw sdkError;
|
|
534
|
+
}
|
|
535
|
+
else if (error.request) {
|
|
536
|
+
// Network error - no response received
|
|
537
|
+
const networkError = createNetworkError(error);
|
|
538
|
+
throw networkError;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
// Other error (request setup, etc.)
|
|
542
|
+
const networkError = createNetworkError(error);
|
|
543
|
+
throw networkError;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// Legacy mode: return error response without throwing
|
|
548
|
+
if (error.response) {
|
|
549
|
+
const { status, data } = error.response;
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: data?.error || `HTTP Error ${status}`,
|
|
553
|
+
details: data?.details,
|
|
554
|
+
errorCode: data?.errorCode || `HTTP_${status}`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
else if (error.request) {
|
|
558
|
+
return {
|
|
559
|
+
success: false,
|
|
560
|
+
error: 'Network error - unable to connect to server',
|
|
561
|
+
errorCode: 'NETWORK_ERROR',
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
error: error.message || 'Unknown error occurred',
|
|
568
|
+
errorCode: 'UNKNOWN_ERROR',
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* GET request
|
|
575
|
+
*/
|
|
576
|
+
async get(url, config) {
|
|
577
|
+
return this.executeWithRetry(() => this.client.get(url, config));
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* POST request
|
|
581
|
+
*/
|
|
582
|
+
async post(url, data, config) {
|
|
583
|
+
return this.executeWithRetry(() => this.client.post(url, data, config));
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* PUT request
|
|
587
|
+
*/
|
|
588
|
+
async put(url, data, config) {
|
|
589
|
+
return this.executeWithRetry(() => this.client.put(url, data, config));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* DELETE request
|
|
593
|
+
*/
|
|
594
|
+
async delete(url, config) {
|
|
595
|
+
return this.executeWithRetry(() => this.client.delete(url, config));
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Update network parameter for subsequent requests
|
|
599
|
+
*/
|
|
600
|
+
setNetwork(network) {
|
|
601
|
+
this.network = network;
|
|
602
|
+
this.client.defaults.headers['X-Network'] = network;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get current network
|
|
606
|
+
*/
|
|
607
|
+
getNetwork() {
|
|
608
|
+
return this.network;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
var http = /*#__PURE__*/Object.freeze({
|
|
613
|
+
__proto__: null,
|
|
614
|
+
HttpClient: HttpClient
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Aptos Multi-Chain Adapter - v2 Proxy Architecture
|
|
619
|
+
* Handles all Aptos chains (aptos-testnet, aptos-mainnet)
|
|
620
|
+
* Routes all requests through proxy.smoothsend.xyz with API key authentication
|
|
621
|
+
* Supports Aptos-specific features like gasless transactions and Move-based contracts
|
|
622
|
+
*/
|
|
623
|
+
class AptosAdapter {
|
|
624
|
+
constructor(chain, config, apiKey, network = 'testnet', includeOrigin = false) {
|
|
625
|
+
// Validate this is an Aptos chain
|
|
626
|
+
if (CHAIN_ECOSYSTEM_MAP[chain] !== 'aptos') {
|
|
627
|
+
throw new SmoothSendError(`AptosAdapter can only handle Aptos chains, got: ${chain}`, 'INVALID_CHAIN_FOR_ADAPTER', 400, { chain });
|
|
628
|
+
}
|
|
629
|
+
this.chain = chain;
|
|
630
|
+
this.config = config;
|
|
631
|
+
this.apiKey = apiKey;
|
|
632
|
+
this.network = network;
|
|
633
|
+
// Initialize HTTP client with proxy configuration
|
|
634
|
+
this.httpClient = new HttpClient({
|
|
635
|
+
apiKey: this.apiKey,
|
|
636
|
+
network: this.network,
|
|
637
|
+
timeout: 30000,
|
|
638
|
+
retries: 3,
|
|
639
|
+
includeOrigin
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Build API path for proxy worker routing to Aptos relayer
|
|
644
|
+
* All requests route through /api/v1/relayer/aptos/* endpoints
|
|
645
|
+
*/
|
|
646
|
+
getApiPath(endpoint) {
|
|
647
|
+
return `/api/v1/relayer/aptos${endpoint}`;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Update network parameter for subsequent requests
|
|
651
|
+
* Network is passed via X-Network header to proxy worker
|
|
652
|
+
*/
|
|
653
|
+
setNetwork(network) {
|
|
654
|
+
this.network = network;
|
|
655
|
+
this.httpClient.setNetwork(network);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Get current network
|
|
659
|
+
*/
|
|
660
|
+
getNetwork() {
|
|
661
|
+
return this.network;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Estimate fee for a transfer (v2 interface method)
|
|
665
|
+
* Routes through proxy: POST /api/v1/relayer/aptos/quote
|
|
666
|
+
*/
|
|
667
|
+
async estimateFee(request) {
|
|
668
|
+
try {
|
|
669
|
+
const response = await this.httpClient.post(this.getApiPath('/quote'), {
|
|
670
|
+
fromAddress: request.from,
|
|
671
|
+
toAddress: request.to,
|
|
672
|
+
amount: request.amount,
|
|
673
|
+
coinType: this.getAptosTokenAddress(request.token)
|
|
674
|
+
});
|
|
675
|
+
const responseData = response.data;
|
|
676
|
+
const quote = responseData.quote;
|
|
677
|
+
const feeEstimate = {
|
|
678
|
+
relayerFee: quote.relayerFee,
|
|
679
|
+
feeInUSD: quote.feeInUSD || '0',
|
|
680
|
+
coinType: this.getAptosTokenAddress(request.token),
|
|
681
|
+
estimatedGas: quote.estimatedGas || '0',
|
|
682
|
+
network: this.network
|
|
683
|
+
};
|
|
684
|
+
// Attach usage metadata from proxy response headers
|
|
685
|
+
if (response.metadata) {
|
|
686
|
+
feeEstimate.metadata = response.metadata;
|
|
687
|
+
}
|
|
688
|
+
return feeEstimate;
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
if (error instanceof SmoothSendError) {
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
throw new SmoothSendError(`Failed to estimate Aptos fee: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.QUOTE_ERROR, 500, { chain: this.chain });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Execute gasless transfer (v2 interface method)
|
|
699
|
+
* Routes through proxy: POST /api/v1/relayer/aptos/execute
|
|
700
|
+
*/
|
|
701
|
+
async executeGaslessTransfer(signedData) {
|
|
702
|
+
return this.executeTransfer(signedData);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Get quote for a transfer (legacy method, kept for backward compatibility)
|
|
706
|
+
* Routes through proxy: POST /api/v1/relayer/aptos/quote
|
|
707
|
+
*/
|
|
708
|
+
async getQuote(request) {
|
|
709
|
+
try {
|
|
710
|
+
// Route through proxy: POST /api/v1/relayer/aptos/quote
|
|
711
|
+
const response = await this.httpClient.post(this.getApiPath('/quote'), {
|
|
712
|
+
fromAddress: request.from,
|
|
713
|
+
toAddress: request.to,
|
|
714
|
+
amount: request.amount,
|
|
715
|
+
coinType: this.getAptosTokenAddress(request.token)
|
|
716
|
+
});
|
|
717
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
718
|
+
const responseData = response.data;
|
|
719
|
+
const quote = responseData.quote;
|
|
720
|
+
return {
|
|
721
|
+
amount: request.amount,
|
|
722
|
+
relayerFee: quote.relayerFee,
|
|
723
|
+
total: (BigInt(request.amount) + BigInt(quote.relayerFee)).toString(),
|
|
724
|
+
feePercentage: 0, // Aptos uses different fee structure
|
|
725
|
+
contractAddress: responseData.transactionData.function.split('::')[0],
|
|
726
|
+
// Store Aptos-specific data for later use
|
|
727
|
+
aptosTransactionData: responseData.transactionData
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
732
|
+
if (error instanceof SmoothSendError) {
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
throw new SmoothSendError(`Failed to get Aptos quote: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.QUOTE_ERROR, 500, { chain: this.chain });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async prepareTransfer(request, quote) {
|
|
739
|
+
// For Aptos, the transaction data is provided in the quote response
|
|
740
|
+
// The user will sign this transaction directly in their wallet
|
|
741
|
+
const aptosQuote = quote;
|
|
742
|
+
if (!aptosQuote.aptosTransactionData) {
|
|
743
|
+
throw new SmoothSendError('Missing Aptos transaction data from quote', APTOS_ERROR_CODES.MISSING_TRANSACTION_DATA, 400, { chain: this.chain });
|
|
744
|
+
}
|
|
745
|
+
// Return the transaction data that needs to be signed
|
|
746
|
+
// NOTE: After signing, you must serialize the transaction and authenticator
|
|
747
|
+
// using the Aptos SDK and provide them as transactionBytes and authenticatorBytes
|
|
748
|
+
return {
|
|
749
|
+
domain: null, // Aptos doesn't use domain separation like EVM
|
|
750
|
+
types: null,
|
|
751
|
+
message: aptosQuote.aptosTransactionData,
|
|
752
|
+
primaryType: 'AptosTransaction',
|
|
753
|
+
// Add metadata to help with serialization - using any type for flexibility
|
|
754
|
+
metadata: {
|
|
755
|
+
requiresSerialization: true,
|
|
756
|
+
serializationInstructions: 'After signing, serialize the SimpleTransaction and AccountAuthenticator using Aptos SDK',
|
|
757
|
+
expectedFormat: 'transactionBytes and authenticatorBytes as number arrays'
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
async executeTransfer(signedData) {
|
|
762
|
+
try {
|
|
763
|
+
// Validate that we have the required serialized transaction data
|
|
764
|
+
this.validateSerializedTransactionData(signedData);
|
|
765
|
+
// Route through proxy: POST /api/v1/relayer/aptos/execute
|
|
766
|
+
const response = await this.httpClient.post(this.getApiPath('/execute'), {
|
|
767
|
+
transactionBytes: signedData.transactionBytes,
|
|
768
|
+
authenticatorBytes: signedData.authenticatorBytes
|
|
769
|
+
});
|
|
770
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
771
|
+
const transferData = response.data;
|
|
772
|
+
const result = {
|
|
773
|
+
success: transferData.success || true,
|
|
774
|
+
// Use standardized field names (txHash, transferId)
|
|
775
|
+
txHash: transferData.txHash || transferData.hash, // Support both formats
|
|
776
|
+
transferId: transferData.transferId || transferData.transactionId, // Support both formats
|
|
777
|
+
explorerUrl: this.buildAptosExplorerUrl(transferData.txHash || transferData.hash),
|
|
778
|
+
// Standard fields
|
|
779
|
+
gasUsed: transferData.gasUsed,
|
|
780
|
+
// Aptos-specific fields from enhanced response format
|
|
781
|
+
gasFeePaidBy: transferData.gasFeePaidBy || 'relayer',
|
|
782
|
+
userPaidAPT: transferData.userPaidAPT || false,
|
|
783
|
+
vmStatus: transferData.vmStatus,
|
|
784
|
+
sender: transferData.sender,
|
|
785
|
+
chain: transferData.chain,
|
|
786
|
+
relayerFee: transferData.relayerFee,
|
|
787
|
+
message: transferData.message
|
|
788
|
+
};
|
|
789
|
+
// Attach usage metadata from proxy response headers
|
|
790
|
+
if (response.metadata) {
|
|
791
|
+
result.metadata = response.metadata;
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
797
|
+
if (error instanceof SmoothSendError) {
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
throw new SmoothSendError(`Failed to execute Aptos transfer: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.EXECUTE_ERROR, 500, { chain: this.chain });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async getBalance(address, token) {
|
|
804
|
+
try {
|
|
805
|
+
// Route through proxy: GET /api/v1/relayer/aptos/balance/:address
|
|
806
|
+
const response = await this.httpClient.get(this.getApiPath(`/balance/${address}`));
|
|
807
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
808
|
+
const balanceData = response.data;
|
|
809
|
+
return [{
|
|
810
|
+
token: balanceData?.symbol || token || 'USDC',
|
|
811
|
+
balance: balanceData?.balance?.toString() || '0',
|
|
812
|
+
decimals: balanceData?.decimals || 6,
|
|
813
|
+
symbol: balanceData?.symbol || token || 'USDC',
|
|
814
|
+
name: balanceData?.name || 'USD Coin (Testnet)'
|
|
815
|
+
}];
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
819
|
+
if (error instanceof SmoothSendError) {
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
throw new SmoothSendError(`Failed to get Aptos balance: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.BALANCE_ERROR, 500, { chain: this.chain });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async getTokenInfo(token) {
|
|
826
|
+
try {
|
|
827
|
+
// Route through proxy: GET /api/v1/relayer/aptos/tokens
|
|
828
|
+
const response = await this.httpClient.get(this.getApiPath('/tokens'));
|
|
829
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
830
|
+
const tokens = response.data.tokens || {};
|
|
831
|
+
const tokenInfo = tokens[token.toUpperCase()];
|
|
832
|
+
if (!tokenInfo) {
|
|
833
|
+
throw new Error(`Token ${token} not supported on ${this.chain}`);
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
symbol: tokenInfo.symbol,
|
|
837
|
+
address: tokenInfo.address,
|
|
838
|
+
decimals: tokenInfo.decimals,
|
|
839
|
+
name: tokenInfo.name
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
844
|
+
if (error instanceof SmoothSendError) {
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
throw new SmoothSendError(`Failed to get Aptos token info: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.TOKEN_INFO_ERROR, 500, { chain: this.chain });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async getNonce(address) {
|
|
851
|
+
// Aptos uses sequence numbers instead of nonces
|
|
852
|
+
// For compatibility, we return a timestamp-based value
|
|
853
|
+
// The actual sequence number is managed by the Aptos blockchain
|
|
854
|
+
return Date.now().toString();
|
|
855
|
+
}
|
|
856
|
+
async getTransactionStatus(txHash) {
|
|
857
|
+
try {
|
|
858
|
+
// Route through proxy: GET /api/v1/relayer/aptos/status/:txHash
|
|
859
|
+
const response = await this.httpClient.get(this.getApiPath(`/status/${txHash}`));
|
|
860
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
861
|
+
return response.data;
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
865
|
+
if (error instanceof SmoothSendError) {
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
throw new SmoothSendError(`Failed to get Aptos transaction status: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.STATUS_ERROR, 500, { chain: this.chain });
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Get health status of Aptos relayer through proxy
|
|
873
|
+
* Routes through proxy: GET /api/v1/relayer/aptos/health
|
|
874
|
+
*/
|
|
875
|
+
async getHealth() {
|
|
876
|
+
try {
|
|
877
|
+
const response = await this.httpClient.get(this.getApiPath('/health'));
|
|
878
|
+
const healthResponse = {
|
|
879
|
+
success: true,
|
|
880
|
+
status: response.data.status || 'healthy',
|
|
881
|
+
timestamp: response.data.timestamp || new Date().toISOString(),
|
|
882
|
+
version: response.data.version || '2.0'
|
|
883
|
+
};
|
|
884
|
+
// Attach usage metadata from proxy response headers
|
|
885
|
+
if (response.metadata) {
|
|
886
|
+
healthResponse.metadata = response.metadata;
|
|
887
|
+
}
|
|
888
|
+
return healthResponse;
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
if (error instanceof SmoothSendError) {
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
throw new SmoothSendError(`Failed to get Aptos health status: ${error instanceof Error ? error.message : String(error)}`, 'HEALTH_CHECK_ERROR', 500, { chain: this.chain });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
validateAddress(address) {
|
|
898
|
+
// Aptos address validation (0x prefix, up to 64 hex characters)
|
|
899
|
+
// Aptos addresses can be shorter and are automatically padded
|
|
900
|
+
return /^0x[a-fA-F0-9]{1,64}$/.test(address);
|
|
901
|
+
}
|
|
902
|
+
validateAmount(amount) {
|
|
903
|
+
try {
|
|
904
|
+
const amountBN = BigInt(amount);
|
|
905
|
+
return amountBN > 0n;
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get Aptos token address from symbol
|
|
913
|
+
*/
|
|
914
|
+
getAptosTokenAddress(tokenSymbol) {
|
|
915
|
+
// This would typically come from the chain configuration
|
|
916
|
+
if (tokenSymbol.toUpperCase() === 'USDC') {
|
|
917
|
+
if (this.chain === 'aptos-testnet') {
|
|
918
|
+
return '0x3c27315fb69ba6e4b960f1507d1cefcc9a4247869f26a8d59d6b7869d23782c::test_coins::USDC';
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
throw new SmoothSendError(`Unsupported token: ${tokenSymbol} on ${this.chain}`, APTOS_ERROR_CODES.UNSUPPORTED_TOKEN, 400, { chain: this.chain, token: tokenSymbol });
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Build Aptos explorer URL for transaction
|
|
925
|
+
*/
|
|
926
|
+
buildAptosExplorerUrl(txHash) {
|
|
927
|
+
if (this.chain === 'aptos-testnet') {
|
|
928
|
+
return `https://explorer.aptoslabs.com/txn/${txHash}?network=testnet`;
|
|
929
|
+
}
|
|
930
|
+
return `https://explorer.aptoslabs.com/txn/${txHash}`;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Validate serialized transaction data for proxy worker
|
|
934
|
+
* @param signedData The signed transfer data to validate
|
|
935
|
+
*/
|
|
936
|
+
validateSerializedTransactionData(signedData) {
|
|
937
|
+
if (!signedData.transactionBytes) {
|
|
938
|
+
throw new SmoothSendError('Serialized transaction bytes are required for Aptos transactions', APTOS_ERROR_CODES.MISSING_TRANSACTION_DATA, 400, { chain: this.chain });
|
|
939
|
+
}
|
|
940
|
+
if (!signedData.authenticatorBytes) {
|
|
941
|
+
throw new SmoothSendError('Serialized authenticator bytes are required for Aptos transactions', APTOS_ERROR_CODES.MISSING_SIGNATURE, 400, { chain: this.chain });
|
|
942
|
+
}
|
|
943
|
+
// Validate that transaction bytes is an array of numbers (0-255)
|
|
944
|
+
if (!Array.isArray(signedData.transactionBytes) ||
|
|
945
|
+
!signedData.transactionBytes.every((b) => typeof b === 'number' && b >= 0 && b <= 255)) {
|
|
946
|
+
throw new SmoothSendError('Invalid transaction bytes format. Expected array of numbers 0-255.', APTOS_ERROR_CODES.INVALID_SIGNATURE_FORMAT, 400, { chain: this.chain });
|
|
947
|
+
}
|
|
948
|
+
// Validate that authenticator bytes is an array of numbers (0-255)
|
|
949
|
+
if (!Array.isArray(signedData.authenticatorBytes) ||
|
|
950
|
+
!signedData.authenticatorBytes.every((b) => typeof b === 'number' && b >= 0 && b <= 255)) {
|
|
951
|
+
throw new SmoothSendError('Invalid authenticator bytes format. Expected array of numbers 0-255.', APTOS_ERROR_CODES.INVALID_PUBLIC_KEY_FORMAT, 400, { chain: this.chain });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Enhanced address validation with detailed error messages
|
|
956
|
+
* @param address The address to validate
|
|
957
|
+
* @returns true if valid, throws error if invalid
|
|
958
|
+
*/
|
|
959
|
+
validateAddressStrict(address) {
|
|
960
|
+
if (!address) {
|
|
961
|
+
throw new SmoothSendError('Address cannot be empty', APTOS_ERROR_CODES.EMPTY_ADDRESS, 400, { chain: this.chain });
|
|
962
|
+
}
|
|
963
|
+
// Aptos address validation (0x prefix, up to 64 hex characters)
|
|
964
|
+
if (!/^0x[a-fA-F0-9]{1,64}$/.test(address)) {
|
|
965
|
+
throw new SmoothSendError('Invalid Aptos address format. Must start with 0x and contain 1-64 hex characters.', APTOS_ERROR_CODES.INVALID_ADDRESS_FORMAT, 400, { chain: this.chain });
|
|
966
|
+
}
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Verify that a public key corresponds to an expected address
|
|
971
|
+
* This mirrors the enhanced verification in the relayer
|
|
972
|
+
* @param publicKey The public key to verify
|
|
973
|
+
* @param expectedAddress The expected address
|
|
974
|
+
* @returns true if they match
|
|
975
|
+
*/
|
|
976
|
+
async verifyPublicKeyAddress(publicKey, expectedAddress) {
|
|
977
|
+
try {
|
|
978
|
+
// This would typically use the Aptos SDK to derive address from public key
|
|
979
|
+
// For now, we'll do basic validation and let the relayer handle the actual verification
|
|
980
|
+
this.validateAddressStrict(expectedAddress);
|
|
981
|
+
if (!publicKey || !publicKey.startsWith('0x')) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
// The actual verification is done by the relayer using the Aptos SDK
|
|
985
|
+
// This is just a preliminary check
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Enhanced transaction preparation with better signature data structure
|
|
994
|
+
* @param request Transfer request
|
|
995
|
+
* @param quote Transfer quote
|
|
996
|
+
* @returns Signature data with enhanced structure
|
|
997
|
+
*/
|
|
998
|
+
async prepareTransferEnhanced(request, quote) {
|
|
999
|
+
const baseSignatureData = await this.prepareTransfer(request, quote);
|
|
1000
|
+
return {
|
|
1001
|
+
...baseSignatureData,
|
|
1002
|
+
metadata: {
|
|
1003
|
+
chain: this.chain,
|
|
1004
|
+
fromAddress: request.from,
|
|
1005
|
+
toAddress: request.to,
|
|
1006
|
+
amount: request.amount,
|
|
1007
|
+
token: request.token,
|
|
1008
|
+
relayerFee: quote.relayerFee,
|
|
1009
|
+
signatureVersion: '2.0', // Version for tracking signature format changes
|
|
1010
|
+
requiresPublicKey: true, // Indicates this chain requires public key for verification
|
|
1011
|
+
verificationMethod: 'ed25519_with_address_derivation' // Indicates verification method used
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Aptos-specific Move contract interaction
|
|
1017
|
+
*/
|
|
1018
|
+
async callMoveFunction(functionName, args) {
|
|
1019
|
+
try {
|
|
1020
|
+
// Route through proxy: POST /api/v1/relayer/aptos/move/call
|
|
1021
|
+
const response = await this.httpClient.post(this.getApiPath('/move/call'), {
|
|
1022
|
+
function: functionName,
|
|
1023
|
+
arguments: args
|
|
1024
|
+
});
|
|
1025
|
+
// In proxy mode, errors are thrown by HttpClient, so we only handle success
|
|
1026
|
+
return response.data;
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
// Re-throw typed errors from HttpClient, wrap others
|
|
1030
|
+
if (error instanceof SmoothSendError) {
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
throw new SmoothSendError(`Failed to call Move function: ${error instanceof Error ? error.message : String(error)}`, APTOS_ERROR_CODES.MOVE_CALL_ERROR, 500, { chain: this.chain });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
class SmoothSendSDK {
|
|
1039
|
+
constructor(config) {
|
|
1040
|
+
this.adapters = new Map();
|
|
1041
|
+
this.eventListeners = [];
|
|
1042
|
+
this.hasWarnedAboutSecretKey = false;
|
|
1043
|
+
// Validate API key is provided
|
|
1044
|
+
if (!config.apiKey) {
|
|
1045
|
+
throw new SmoothSendError('API key is required. Get your API key from dashboard.smoothsend.xyz', 'MISSING_API_KEY');
|
|
1046
|
+
}
|
|
1047
|
+
// Detect and validate API key type
|
|
1048
|
+
this.keyType = this.detectKeyType(config.apiKey);
|
|
1049
|
+
// Warn if secret key is used in browser environment
|
|
1050
|
+
this.warnIfSecretKeyInBrowser();
|
|
1051
|
+
// Validate network parameter if provided
|
|
1052
|
+
if (config.network && config.network !== 'testnet' && config.network !== 'mainnet') {
|
|
1053
|
+
throw new SmoothSendError('Invalid network parameter. Must be "testnet" or "mainnet"', 'INVALID_NETWORK');
|
|
1054
|
+
}
|
|
1055
|
+
// Set configuration with defaults
|
|
1056
|
+
this.config = {
|
|
1057
|
+
apiKey: config.apiKey,
|
|
1058
|
+
network: config.network || 'testnet', // Default to testnet
|
|
1059
|
+
timeout: config.timeout || 30000,
|
|
1060
|
+
retries: config.retries || 3,
|
|
1061
|
+
customHeaders: config.customHeaders || {}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Detect key type from API key prefix
|
|
1066
|
+
* Supports pk_nogas_* (public), sk_nogas_* (secret), and no_gas_* (legacy)
|
|
1067
|
+
*/
|
|
1068
|
+
detectKeyType(apiKey) {
|
|
1069
|
+
if (apiKey.startsWith('pk_nogas_')) {
|
|
1070
|
+
return 'public';
|
|
1071
|
+
}
|
|
1072
|
+
if (apiKey.startsWith('sk_nogas_')) {
|
|
1073
|
+
return 'secret';
|
|
1074
|
+
}
|
|
1075
|
+
if (apiKey.startsWith('no_gas_')) {
|
|
1076
|
+
return 'legacy';
|
|
1077
|
+
}
|
|
1078
|
+
throw new SmoothSendError('Invalid API key format. API key must start with "pk_nogas_", "sk_nogas_", or "no_gas_"', 'INVALID_API_KEY_FORMAT');
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Check if running in browser environment
|
|
1082
|
+
* Used for conditional warnings and Origin header logic
|
|
1083
|
+
*/
|
|
1084
|
+
isBrowserEnvironment() {
|
|
1085
|
+
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Warn if secret key is used in browser environment
|
|
1089
|
+
* Only warns once per SDK instance
|
|
1090
|
+
*/
|
|
1091
|
+
warnIfSecretKeyInBrowser() {
|
|
1092
|
+
if (this.hasWarnedAboutSecretKey) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (this.keyType === 'secret' && this.isBrowserEnvironment()) {
|
|
1096
|
+
console.warn('⚠️ WARNING: Secret key detected in browser environment.\n' +
|
|
1097
|
+
'Secret keys (sk_nogas_*) should only be used in server-side code.\n' +
|
|
1098
|
+
'Use public keys (pk_nogas_*) for frontend applications.\n' +
|
|
1099
|
+
'Learn more: https://docs.smoothsend.xyz/security/api-keys');
|
|
1100
|
+
this.hasWarnedAboutSecretKey = true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Determine if Origin header should be included in requests
|
|
1105
|
+
* Include Origin header only for public keys in browser environment
|
|
1106
|
+
*/
|
|
1107
|
+
shouldIncludeOrigin() {
|
|
1108
|
+
return this.keyType === 'public' && this.isBrowserEnvironment();
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Get or create adapter for a specific chain on-demand
|
|
1112
|
+
*/
|
|
1113
|
+
getOrCreateAdapter(chain) {
|
|
1114
|
+
// Check if adapter already exists
|
|
1115
|
+
let adapter = this.adapters.get(chain);
|
|
1116
|
+
if (!adapter) {
|
|
1117
|
+
// Create adapter on-demand
|
|
1118
|
+
const ecosystem = CHAIN_ECOSYSTEM_MAP[chain];
|
|
1119
|
+
if (ecosystem === 'aptos') {
|
|
1120
|
+
// Create minimal config for adapter (proxy handles actual configuration)
|
|
1121
|
+
const minimalConfig = {
|
|
1122
|
+
name: chain,
|
|
1123
|
+
displayName: chain,
|
|
1124
|
+
chainId: 0,
|
|
1125
|
+
rpcUrl: '',
|
|
1126
|
+
relayerUrl: 'https://proxy.smoothsend.xyz',
|
|
1127
|
+
explorerUrl: '',
|
|
1128
|
+
tokens: [],
|
|
1129
|
+
nativeCurrency: {
|
|
1130
|
+
name: 'APT',
|
|
1131
|
+
symbol: 'APT',
|
|
1132
|
+
decimals: 8
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
adapter = new AptosAdapter(chain, minimalConfig, this.config.apiKey, this.config.network || 'testnet', this.shouldIncludeOrigin());
|
|
1136
|
+
}
|
|
1137
|
+
else if (ecosystem === 'evm') {
|
|
1138
|
+
// EVM adapter will be implemented in future phase
|
|
1139
|
+
throw new SmoothSendError(`EVM chains not yet supported in v2. Chain: ${chain}`, 'UNSUPPORTED_CHAIN');
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
throw new SmoothSendError(`Unsupported ecosystem: ${ecosystem}`, 'UNSUPPORTED_ECOSYSTEM');
|
|
1143
|
+
}
|
|
1144
|
+
// Cache the adapter for future use
|
|
1145
|
+
this.adapters.set(chain, adapter);
|
|
1146
|
+
}
|
|
1147
|
+
return adapter;
|
|
1148
|
+
}
|
|
1149
|
+
// Event handling
|
|
1150
|
+
addEventListener(listener) {
|
|
1151
|
+
this.eventListeners.push(listener);
|
|
1152
|
+
}
|
|
1153
|
+
removeEventListener(listener) {
|
|
1154
|
+
const index = this.eventListeners.indexOf(listener);
|
|
1155
|
+
if (index > -1) {
|
|
1156
|
+
this.eventListeners.splice(index, 1);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
emitEvent(event) {
|
|
1160
|
+
this.eventListeners.forEach(listener => {
|
|
1161
|
+
try {
|
|
1162
|
+
listener(event);
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
console.error('Error in event listener:', error);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
// Core transfer methods
|
|
1170
|
+
async estimateFee(request) {
|
|
1171
|
+
const adapter = this.getOrCreateAdapter(request.chain);
|
|
1172
|
+
this.emitEvent({
|
|
1173
|
+
type: 'transfer_initiated',
|
|
1174
|
+
data: { request },
|
|
1175
|
+
timestamp: Date.now(),
|
|
1176
|
+
chain: request.chain
|
|
1177
|
+
});
|
|
1178
|
+
try {
|
|
1179
|
+
const feeEstimate = await adapter.estimateFee(request);
|
|
1180
|
+
// Metadata is already attached by the adapter from HTTP response headers
|
|
1181
|
+
return feeEstimate;
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
this.emitEvent({
|
|
1185
|
+
type: 'transfer_failed',
|
|
1186
|
+
data: { error: error instanceof Error ? error.message : String(error), step: 'estimate_fee' },
|
|
1187
|
+
timestamp: Date.now(),
|
|
1188
|
+
chain: request.chain
|
|
1189
|
+
});
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async executeGaslessTransfer(signedData) {
|
|
1194
|
+
const adapter = this.getOrCreateAdapter(signedData.chain);
|
|
1195
|
+
this.emitEvent({
|
|
1196
|
+
type: 'transfer_submitted',
|
|
1197
|
+
data: { signedData },
|
|
1198
|
+
timestamp: Date.now(),
|
|
1199
|
+
chain: signedData.chain
|
|
1200
|
+
});
|
|
1201
|
+
try {
|
|
1202
|
+
const result = await adapter.executeGaslessTransfer(signedData);
|
|
1203
|
+
this.emitEvent({
|
|
1204
|
+
type: 'transfer_confirmed',
|
|
1205
|
+
data: { result },
|
|
1206
|
+
timestamp: Date.now(),
|
|
1207
|
+
chain: signedData.chain
|
|
1208
|
+
});
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
this.emitEvent({
|
|
1213
|
+
type: 'transfer_failed',
|
|
1214
|
+
data: { error: error instanceof Error ? error.message : String(error), step: 'execute' },
|
|
1215
|
+
timestamp: Date.now(),
|
|
1216
|
+
chain: signedData.chain
|
|
1217
|
+
});
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Convenience method for complete transfer flow
|
|
1223
|
+
* Combines estimateFee and executeGaslessTransfer into a single call
|
|
1224
|
+
*
|
|
1225
|
+
* @param request Transfer request with from, to, token, amount, chain
|
|
1226
|
+
* @param wallet Wallet instance that can build and sign transactions
|
|
1227
|
+
* @returns Transfer result with transaction hash and usage metadata
|
|
1228
|
+
*
|
|
1229
|
+
* Note: The wallet parameter should have methods:
|
|
1230
|
+
* - buildTransaction(params): Build transaction from parameters
|
|
1231
|
+
* - signTransaction(transaction): Sign and serialize transaction
|
|
1232
|
+
*
|
|
1233
|
+
* The wallet's signTransaction should return an object with:
|
|
1234
|
+
* - transactionBytes: number[] - Serialized transaction
|
|
1235
|
+
* - authenticatorBytes: number[] - Serialized authenticator
|
|
1236
|
+
*/
|
|
1237
|
+
async transfer(request, wallet) {
|
|
1238
|
+
this.emitEvent({
|
|
1239
|
+
type: 'transfer_initiated',
|
|
1240
|
+
data: { request },
|
|
1241
|
+
timestamp: Date.now(),
|
|
1242
|
+
chain: request.chain
|
|
1243
|
+
});
|
|
1244
|
+
try {
|
|
1245
|
+
// Step 1: Get fee estimate
|
|
1246
|
+
const feeEstimate = await this.estimateFee(request);
|
|
1247
|
+
// Step 2: Build transaction with wallet
|
|
1248
|
+
const transaction = await wallet.buildTransaction({
|
|
1249
|
+
sender: request.from,
|
|
1250
|
+
recipient: request.to,
|
|
1251
|
+
amount: request.amount,
|
|
1252
|
+
coinType: feeEstimate.coinType,
|
|
1253
|
+
relayerFee: feeEstimate.relayerFee
|
|
1254
|
+
});
|
|
1255
|
+
this.emitEvent({
|
|
1256
|
+
type: 'transfer_signed',
|
|
1257
|
+
data: { transaction },
|
|
1258
|
+
timestamp: Date.now(),
|
|
1259
|
+
chain: request.chain
|
|
1260
|
+
});
|
|
1261
|
+
// Step 3: Sign and serialize transaction with wallet
|
|
1262
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
1263
|
+
// Step 4: Execute gasless transfer
|
|
1264
|
+
const result = await this.executeGaslessTransfer({
|
|
1265
|
+
transactionBytes: signedTx.transactionBytes,
|
|
1266
|
+
authenticatorBytes: signedTx.authenticatorBytes,
|
|
1267
|
+
chain: request.chain,
|
|
1268
|
+
network: this.config.network
|
|
1269
|
+
});
|
|
1270
|
+
return result;
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
this.emitEvent({
|
|
1274
|
+
type: 'transfer_failed',
|
|
1275
|
+
data: {
|
|
1276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1277
|
+
step: 'transfer'
|
|
1278
|
+
},
|
|
1279
|
+
timestamp: Date.now(),
|
|
1280
|
+
chain: request.chain
|
|
1281
|
+
});
|
|
1282
|
+
throw error;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// Note: Batch transfer support will be implemented in a future phase
|
|
1286
|
+
// Utility methods
|
|
1287
|
+
/**
|
|
1288
|
+
* Get transaction status for a specific transaction
|
|
1289
|
+
* Routes through proxy to chain-specific status endpoint
|
|
1290
|
+
*
|
|
1291
|
+
* @param chain Chain where the transaction was executed
|
|
1292
|
+
* @param txHash Transaction hash to query
|
|
1293
|
+
* @returns Transaction status information
|
|
1294
|
+
* @throws SmoothSendError if chain is not supported or status check fails
|
|
1295
|
+
*
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```typescript
|
|
1298
|
+
* const status = await sdk.getTransactionStatus('aptos-testnet', '0x123...');
|
|
1299
|
+
* console.log('Transaction status:', status);
|
|
1300
|
+
* ```
|
|
1301
|
+
*/
|
|
1302
|
+
async getTransactionStatus(chain, txHash) {
|
|
1303
|
+
if (!this.isChainSupported(chain)) {
|
|
1304
|
+
throw new SmoothSendError(`Chain ${chain} is not supported`, 'UNSUPPORTED_CHAIN', 400, { chain, supportedChains: this.getSupportedChains() });
|
|
1305
|
+
}
|
|
1306
|
+
if (!txHash || txHash.trim() === '') {
|
|
1307
|
+
throw new SmoothSendError('Transaction hash is required', 'MISSING_TX_HASH', 400);
|
|
1308
|
+
}
|
|
1309
|
+
const adapter = this.getOrCreateAdapter(chain);
|
|
1310
|
+
return await adapter.getTransactionStatus(txHash);
|
|
1311
|
+
}
|
|
1312
|
+
validateAddress(chain, address) {
|
|
1313
|
+
const adapter = this.getOrCreateAdapter(chain);
|
|
1314
|
+
return adapter.validateAddress(address);
|
|
1315
|
+
}
|
|
1316
|
+
validateAmount(chain, amount) {
|
|
1317
|
+
const adapter = this.getOrCreateAdapter(chain);
|
|
1318
|
+
return adapter.validateAmount(amount);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Check proxy worker health status
|
|
1322
|
+
* Routes directly to proxy's /health endpoint (not chain-specific)
|
|
1323
|
+
*
|
|
1324
|
+
* @returns Health response with status, version, and timestamp
|
|
1325
|
+
* @throws NetworkError if proxy is unavailable
|
|
1326
|
+
* @throws SmoothSendError for other errors
|
|
1327
|
+
*
|
|
1328
|
+
* @example
|
|
1329
|
+
* ```typescript
|
|
1330
|
+
* try {
|
|
1331
|
+
* const health = await sdk.getHealth();
|
|
1332
|
+
* console.log('Proxy status:', health.status);
|
|
1333
|
+
* console.log('Version:', health.version);
|
|
1334
|
+
* } catch (error) {
|
|
1335
|
+
* if (error instanceof NetworkError) {
|
|
1336
|
+
* console.error('Proxy unavailable. Please retry later.');
|
|
1337
|
+
* }
|
|
1338
|
+
* }
|
|
1339
|
+
* ```
|
|
1340
|
+
*/
|
|
1341
|
+
async getHealth() {
|
|
1342
|
+
// Import HttpClient for direct proxy health check
|
|
1343
|
+
const { HttpClient } = await Promise.resolve().then(function () { return http; });
|
|
1344
|
+
// Create HTTP client for direct proxy communication
|
|
1345
|
+
const httpClient = new HttpClient({
|
|
1346
|
+
apiKey: this.config.apiKey,
|
|
1347
|
+
network: this.config.network || 'testnet',
|
|
1348
|
+
timeout: this.config.timeout,
|
|
1349
|
+
retries: this.config.retries,
|
|
1350
|
+
includeOrigin: this.shouldIncludeOrigin()
|
|
1351
|
+
});
|
|
1352
|
+
try {
|
|
1353
|
+
// Check proxy's general health endpoint (not chain-specific)
|
|
1354
|
+
const response = await httpClient.get('/health');
|
|
1355
|
+
const healthResponse = {
|
|
1356
|
+
success: true,
|
|
1357
|
+
status: response.data.status || 'healthy',
|
|
1358
|
+
timestamp: response.data.timestamp || new Date().toISOString(),
|
|
1359
|
+
version: response.data.version || '2.0'
|
|
1360
|
+
};
|
|
1361
|
+
// Attach usage metadata from proxy response headers
|
|
1362
|
+
if (response.metadata) {
|
|
1363
|
+
healthResponse.metadata = response.metadata;
|
|
1364
|
+
}
|
|
1365
|
+
return healthResponse;
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
if (error instanceof SmoothSendError) {
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
throw new SmoothSendError(`Failed to check proxy health: ${error instanceof Error ? error.message : String(error)}. Please check your connection and retry.`, 'HEALTH_CHECK_ERROR', 500);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Get list of supported chains (static list)
|
|
1376
|
+
* For dynamic list from proxy, use getSupportedChainsFromProxy()
|
|
1377
|
+
*
|
|
1378
|
+
* @returns Array of supported chain identifiers
|
|
1379
|
+
*
|
|
1380
|
+
* @example
|
|
1381
|
+
* ```typescript
|
|
1382
|
+
* const chains = sdk.getSupportedChains();
|
|
1383
|
+
* console.log('Supported chains:', chains);
|
|
1384
|
+
* // Output: ['aptos-testnet', 'aptos-mainnet']
|
|
1385
|
+
* ```
|
|
1386
|
+
*/
|
|
1387
|
+
getSupportedChains() {
|
|
1388
|
+
// Return statically supported chains for v2
|
|
1389
|
+
return ['aptos-testnet', 'aptos-mainnet'];
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Get list of supported chains from proxy worker (dynamic)
|
|
1393
|
+
* Queries the proxy for the current list of supported chains
|
|
1394
|
+
*
|
|
1395
|
+
* @returns Promise with array of chain information including status
|
|
1396
|
+
* @throws SmoothSendError if unable to fetch chains from proxy
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```typescript
|
|
1400
|
+
* const chains = await sdk.getSupportedChainsFromProxy();
|
|
1401
|
+
* chains.forEach(chain => {
|
|
1402
|
+
* console.log(`${chain.name} (${chain.id}): ${chain.status}`);
|
|
1403
|
+
* });
|
|
1404
|
+
* ```
|
|
1405
|
+
*/
|
|
1406
|
+
async getSupportedChainsFromProxy() {
|
|
1407
|
+
const { HttpClient } = await Promise.resolve().then(function () { return http; });
|
|
1408
|
+
const httpClient = new HttpClient({
|
|
1409
|
+
apiKey: this.config.apiKey,
|
|
1410
|
+
network: this.config.network || 'testnet',
|
|
1411
|
+
timeout: this.config.timeout,
|
|
1412
|
+
retries: this.config.retries,
|
|
1413
|
+
includeOrigin: this.shouldIncludeOrigin()
|
|
1414
|
+
});
|
|
1415
|
+
try {
|
|
1416
|
+
const response = await httpClient.get('/api/v1/chains');
|
|
1417
|
+
if (!response.data.chains) {
|
|
1418
|
+
throw new Error('Invalid response format from proxy');
|
|
1419
|
+
}
|
|
1420
|
+
return response.data.chains;
|
|
1421
|
+
}
|
|
1422
|
+
catch (error) {
|
|
1423
|
+
if (error instanceof SmoothSendError) {
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
throw new SmoothSendError(`Failed to fetch supported chains from proxy: ${error instanceof Error ? error.message : String(error)}`, 'CHAINS_FETCH_ERROR', 500);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Check if a specific chain is currently supported
|
|
1431
|
+
*
|
|
1432
|
+
* @param chain Chain identifier to check
|
|
1433
|
+
* @returns true if chain is supported, false otherwise
|
|
1434
|
+
*
|
|
1435
|
+
* @example
|
|
1436
|
+
* ```typescript
|
|
1437
|
+
* if (sdk.isChainSupported('aptos-testnet')) {
|
|
1438
|
+
* console.log('Aptos testnet is supported');
|
|
1439
|
+
* } else {
|
|
1440
|
+
* console.log('Chain not supported');
|
|
1441
|
+
* }
|
|
1442
|
+
* ```
|
|
1443
|
+
*/
|
|
1444
|
+
isChainSupported(chain) {
|
|
1445
|
+
const supportedChains = this.getSupportedChains();
|
|
1446
|
+
return supportedChains.includes(chain);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Check health status of a specific chain's relayer
|
|
1450
|
+
* Routes through proxy to chain-specific health endpoint
|
|
1451
|
+
*
|
|
1452
|
+
* @param chain Chain identifier to check
|
|
1453
|
+
* @returns Health response for the specific chain
|
|
1454
|
+
* @throws SmoothSendError if chain is not supported or health check fails
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* ```typescript
|
|
1458
|
+
* try {
|
|
1459
|
+
* const health = await sdk.getChainHealth('aptos-testnet');
|
|
1460
|
+
* console.log('Aptos testnet status:', health.status);
|
|
1461
|
+
* } catch (error) {
|
|
1462
|
+
* console.error('Chain health check failed:', error.message);
|
|
1463
|
+
* }
|
|
1464
|
+
* ```
|
|
1465
|
+
*/
|
|
1466
|
+
async getChainHealth(chain) {
|
|
1467
|
+
if (!this.isChainSupported(chain)) {
|
|
1468
|
+
throw new SmoothSendError(`Chain ${chain} is not supported`, 'UNSUPPORTED_CHAIN', 400, { chain, supportedChains: this.getSupportedChains() });
|
|
1469
|
+
}
|
|
1470
|
+
const adapter = this.getOrCreateAdapter(chain);
|
|
1471
|
+
return await adapter.getHealth();
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Get current usage statistics without making a transfer
|
|
1475
|
+
* Makes a lightweight health check request to retrieve usage metadata
|
|
1476
|
+
*
|
|
1477
|
+
* @returns Usage metadata with rate limit and monthly usage information
|
|
1478
|
+
* @throws Error if unable to retrieve usage stats
|
|
1479
|
+
*
|
|
1480
|
+
* @example
|
|
1481
|
+
* ```typescript
|
|
1482
|
+
* const usage = await sdk.getUsageStats();
|
|
1483
|
+
* console.log('Rate limit:', usage.rateLimit);
|
|
1484
|
+
* console.log('Monthly usage:', usage.monthly);
|
|
1485
|
+
* console.log('Request ID:', usage.requestId);
|
|
1486
|
+
*
|
|
1487
|
+
* // Check if approaching limits
|
|
1488
|
+
* if (parseInt(usage.rateLimit.remaining) < 2) {
|
|
1489
|
+
* console.warn('Approaching rate limit!');
|
|
1490
|
+
* }
|
|
1491
|
+
* ```
|
|
1492
|
+
*/
|
|
1493
|
+
async getUsageStats() {
|
|
1494
|
+
try {
|
|
1495
|
+
// Make a health check request to get usage metadata from headers
|
|
1496
|
+
const health = await this.getHealth();
|
|
1497
|
+
// The health response includes metadata from the HTTP client
|
|
1498
|
+
const healthWithMetadata = health;
|
|
1499
|
+
if (!healthWithMetadata.metadata) {
|
|
1500
|
+
throw new SmoothSendError('Usage metadata not available. This may occur if not using proxy mode.', 'METADATA_NOT_AVAILABLE');
|
|
1501
|
+
}
|
|
1502
|
+
return healthWithMetadata.metadata;
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
// If health check fails, we can't get usage stats
|
|
1506
|
+
if (error instanceof SmoothSendError) {
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
throw new SmoothSendError(`Failed to retrieve usage statistics: ${error instanceof Error ? error.message : String(error)}`, 'USAGE_STATS_ERROR');
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Extract request ID from a transfer result for debugging and support
|
|
1514
|
+
*
|
|
1515
|
+
* @param result Transfer result from executeGaslessTransfer or transfer
|
|
1516
|
+
* @returns Request ID if available, undefined otherwise
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* ```typescript
|
|
1520
|
+
* const result = await sdk.executeGaslessTransfer(signedData);
|
|
1521
|
+
* const requestId = sdk.getRequestId(result);
|
|
1522
|
+
* if (requestId) {
|
|
1523
|
+
* console.log('Request ID for support:', requestId);
|
|
1524
|
+
* }
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1527
|
+
getRequestId(result) {
|
|
1528
|
+
return result.metadata?.requestId;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Check if approaching rate limit based on transfer result metadata
|
|
1532
|
+
*
|
|
1533
|
+
* @param result Transfer result with metadata
|
|
1534
|
+
* @param threshold Percentage threshold (0-100) to consider as "approaching" (default: 20)
|
|
1535
|
+
* @returns true if remaining requests are below threshold percentage
|
|
1536
|
+
*
|
|
1537
|
+
* @example
|
|
1538
|
+
* ```typescript
|
|
1539
|
+
* const result = await sdk.transfer(request, wallet);
|
|
1540
|
+
* if (sdk.isApproachingRateLimit(result)) {
|
|
1541
|
+
* console.warn('Approaching rate limit, consider slowing down requests');
|
|
1542
|
+
* }
|
|
1543
|
+
* ```
|
|
1544
|
+
*/
|
|
1545
|
+
isApproachingRateLimit(result, threshold = 20) {
|
|
1546
|
+
if (!result.metadata?.rateLimit) {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
const limit = parseInt(result.metadata.rateLimit.limit);
|
|
1550
|
+
const remaining = parseInt(result.metadata.rateLimit.remaining);
|
|
1551
|
+
if (isNaN(limit) || isNaN(remaining) || limit === 0) {
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
const percentageRemaining = (remaining / limit) * 100;
|
|
1555
|
+
return percentageRemaining <= threshold;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Check if approaching monthly usage limit based on transfer result metadata
|
|
1559
|
+
*
|
|
1560
|
+
* @param result Transfer result with metadata
|
|
1561
|
+
* @param threshold Percentage threshold (0-100) to consider as "approaching" (default: 90)
|
|
1562
|
+
* @returns true if monthly usage is above threshold percentage
|
|
1563
|
+
*
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```typescript
|
|
1566
|
+
* const result = await sdk.transfer(request, wallet);
|
|
1567
|
+
* if (sdk.isApproachingMonthlyLimit(result)) {
|
|
1568
|
+
* console.warn('Approaching monthly limit, consider upgrading plan');
|
|
1569
|
+
* }
|
|
1570
|
+
* ```
|
|
1571
|
+
*/
|
|
1572
|
+
isApproachingMonthlyLimit(result, threshold = 90) {
|
|
1573
|
+
if (!result.metadata?.monthly) {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
const limit = parseInt(result.metadata.monthly.limit);
|
|
1577
|
+
const usage = parseInt(result.metadata.monthly.usage);
|
|
1578
|
+
if (isNaN(limit) || isNaN(usage) || limit === 0) {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
const percentageUsed = (usage / limit) * 100;
|
|
1582
|
+
return percentageUsed >= threshold;
|
|
1583
|
+
}
|
|
1584
|
+
// Ecosystem-specific methods for advanced usage
|
|
1585
|
+
/**
|
|
1586
|
+
* Check if a chain belongs to a specific ecosystem
|
|
1587
|
+
*/
|
|
1588
|
+
getChainEcosystem(chain) {
|
|
1589
|
+
return CHAIN_ECOSYSTEM_MAP[chain];
|
|
1590
|
+
}
|
|
1591
|
+
// Static utility methods
|
|
1592
|
+
/**
|
|
1593
|
+
* Get list of supported chains (static method)
|
|
1594
|
+
* Can be called without instantiating the SDK
|
|
1595
|
+
*
|
|
1596
|
+
* @returns Array of supported chain identifiers
|
|
1597
|
+
*
|
|
1598
|
+
* @example
|
|
1599
|
+
* ```typescript
|
|
1600
|
+
* const chains = SmoothSendSDK.getSupportedChains();
|
|
1601
|
+
* console.log('Supported chains:', chains);
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1604
|
+
static getSupportedChains() {
|
|
1605
|
+
return ['aptos-testnet', 'aptos-mainnet'];
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* SmoothSend Transaction Submitter
|
|
1611
|
+
*
|
|
1612
|
+
* A TransactionSubmitter implementation that integrates with the Aptos Wallet Adapter
|
|
1613
|
+
* to enable gasless transactions via SmoothSend's relayer network.
|
|
1614
|
+
*
|
|
1615
|
+
* @example
|
|
1616
|
+
* ```typescript
|
|
1617
|
+
* import { SmoothSendTransactionSubmitter } from '@smoothsend/sdk';
|
|
1618
|
+
* import { AptosWalletAdapterProvider } from '@aptos-labs/wallet-adapter-react';
|
|
1619
|
+
*
|
|
1620
|
+
* // Create the transaction submitter
|
|
1621
|
+
* const transactionSubmitter = new SmoothSendTransactionSubmitter({
|
|
1622
|
+
* apiKey: 'pk_nogas_your_api_key_here',
|
|
1623
|
+
* network: 'testnet'
|
|
1624
|
+
* });
|
|
1625
|
+
*
|
|
1626
|
+
* // Use in your wallet provider - that's it!
|
|
1627
|
+
* <AptosWalletAdapterProvider
|
|
1628
|
+
* dappConfig={{
|
|
1629
|
+
* network: Network.TESTNET,
|
|
1630
|
+
* transactionSubmitter: transactionSubmitter
|
|
1631
|
+
* }}
|
|
1632
|
+
* >
|
|
1633
|
+
* <App />
|
|
1634
|
+
* </AptosWalletAdapterProvider>
|
|
1635
|
+
* ```
|
|
1636
|
+
*/
|
|
1637
|
+
/**
|
|
1638
|
+
* SmoothSend Transaction Submitter
|
|
1639
|
+
*
|
|
1640
|
+
* Implements the TransactionSubmitter interface to enable gasless transactions
|
|
1641
|
+
* through the Aptos Wallet Adapter. Simply pass this to your AptosWalletAdapterProvider
|
|
1642
|
+
* and all transactions will automatically be submitted as gasless through SmoothSend.
|
|
1643
|
+
*/
|
|
1644
|
+
class SmoothSendTransactionSubmitter {
|
|
1645
|
+
constructor(config) {
|
|
1646
|
+
if (!config.apiKey) {
|
|
1647
|
+
throw new Error('SmoothSend API key is required. Get your key at dashboard.smoothsend.xyz');
|
|
1648
|
+
}
|
|
1649
|
+
// Validate API key format
|
|
1650
|
+
if (!config.apiKey.startsWith('pk_nogas_') &&
|
|
1651
|
+
!config.apiKey.startsWith('sk_nogas_') &&
|
|
1652
|
+
!config.apiKey.startsWith('no_gas_')) {
|
|
1653
|
+
throw new Error('Invalid API key format. Key must start with pk_nogas_, sk_nogas_, or no_gas_');
|
|
1654
|
+
}
|
|
1655
|
+
// Warn if using secret key in browser
|
|
1656
|
+
if (config.apiKey.startsWith('sk_nogas_') && typeof window !== 'undefined') {
|
|
1657
|
+
console.warn('⚠️ WARNING: Secret key detected in browser environment.\n' +
|
|
1658
|
+
'Secret keys (sk_nogas_*) should only be used in server-side code.\n' +
|
|
1659
|
+
'Use public keys (pk_nogas_*) for frontend applications.');
|
|
1660
|
+
}
|
|
1661
|
+
this.apiKey = config.apiKey;
|
|
1662
|
+
this.network = config.network || 'testnet';
|
|
1663
|
+
this.gatewayUrl = config.gatewayUrl || 'https://proxy.smoothsend.xyz';
|
|
1664
|
+
this.timeout = config.timeout || 30000;
|
|
1665
|
+
this.debug = config.debug || false;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Submit a transaction through SmoothSend's gasless relayer
|
|
1669
|
+
*
|
|
1670
|
+
* This method is called automatically by the Aptos Wallet Adapter when
|
|
1671
|
+
* you use signAndSubmitTransaction. The transaction is submitted to
|
|
1672
|
+
* SmoothSend's relayer which sponsors the gas fees.
|
|
1673
|
+
*/
|
|
1674
|
+
async submitTransaction(args) {
|
|
1675
|
+
const { transaction, senderAuthenticator, pluginParams } = args;
|
|
1676
|
+
if (this.debug) {
|
|
1677
|
+
console.log('[SmoothSend] Submitting gasless transaction:', {
|
|
1678
|
+
sender: transaction.rawTransaction?.sender?.toString(),
|
|
1679
|
+
network: this.network,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
// Serialize transaction and authenticator to bytes
|
|
1684
|
+
const transactionBytes = Array.from(transaction.bcsToBytes());
|
|
1685
|
+
const authenticatorBytes = Array.from(senderAuthenticator.bcsToBytes());
|
|
1686
|
+
// Prepare request payload
|
|
1687
|
+
const payload = {
|
|
1688
|
+
transactionBytes,
|
|
1689
|
+
authenticatorBytes,
|
|
1690
|
+
network: this.network,
|
|
1691
|
+
functionName: pluginParams?.functionName || 'unknown',
|
|
1692
|
+
};
|
|
1693
|
+
// Make request to SmoothSend gateway
|
|
1694
|
+
const response = await this.makeRequest('/api/v1/relayer/gasless-transaction', payload);
|
|
1695
|
+
if (!response.success || !response.txnHash) {
|
|
1696
|
+
throw new Error(response.error || response.details || 'Transaction submission failed');
|
|
1697
|
+
}
|
|
1698
|
+
if (this.debug) {
|
|
1699
|
+
console.log('[SmoothSend] Transaction successful:', {
|
|
1700
|
+
hash: response.txnHash,
|
|
1701
|
+
gasUsed: response.gasUsed,
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
// Return in the format expected by Aptos SDK
|
|
1705
|
+
return {
|
|
1706
|
+
hash: response.txnHash,
|
|
1707
|
+
sender: response.sender || transaction.rawTransaction?.sender?.toString() || '',
|
|
1708
|
+
sequence_number: transaction.rawTransaction?.sequence_number?.toString() || '0',
|
|
1709
|
+
max_gas_amount: transaction.rawTransaction?.max_gas_amount?.toString() || '0',
|
|
1710
|
+
gas_unit_price: transaction.rawTransaction?.gas_unit_price?.toString() || '0',
|
|
1711
|
+
expiration_timestamp_secs: transaction.rawTransaction?.expiration_timestamp_secs?.toString() || '0',
|
|
1712
|
+
payload: {},
|
|
1713
|
+
signature: undefined,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
catch (error) {
|
|
1717
|
+
if (this.debug) {
|
|
1718
|
+
console.error('[SmoothSend] Transaction failed:', error);
|
|
1719
|
+
}
|
|
1720
|
+
throw new Error(`SmoothSend gasless transaction failed: ${error.message}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Make an HTTP request to the SmoothSend gateway
|
|
1725
|
+
*/
|
|
1726
|
+
async makeRequest(endpoint, payload) {
|
|
1727
|
+
const url = `${this.gatewayUrl}${endpoint}`;
|
|
1728
|
+
const headers = {
|
|
1729
|
+
'Content-Type': 'application/json',
|
|
1730
|
+
'X-API-Key': this.apiKey,
|
|
1731
|
+
'X-Chain': `aptos-${this.network}`,
|
|
1732
|
+
};
|
|
1733
|
+
// Add Origin header for public keys in browser
|
|
1734
|
+
if (this.apiKey.startsWith('pk_nogas_') && typeof window !== 'undefined') {
|
|
1735
|
+
headers['Origin'] = window.location.origin;
|
|
1736
|
+
}
|
|
1737
|
+
const controller = new AbortController();
|
|
1738
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1739
|
+
try {
|
|
1740
|
+
const response = await fetch(url, {
|
|
1741
|
+
method: 'POST',
|
|
1742
|
+
headers,
|
|
1743
|
+
body: JSON.stringify(payload),
|
|
1744
|
+
signal: controller.signal,
|
|
1745
|
+
});
|
|
1746
|
+
clearTimeout(timeoutId);
|
|
1747
|
+
if (!response.ok) {
|
|
1748
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1749
|
+
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`);
|
|
1750
|
+
}
|
|
1751
|
+
return await response.json();
|
|
1752
|
+
}
|
|
1753
|
+
catch (error) {
|
|
1754
|
+
clearTimeout(timeoutId);
|
|
1755
|
+
if (error.name === 'AbortError') {
|
|
1756
|
+
throw new Error('Request timed out');
|
|
1757
|
+
}
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Get the current configuration
|
|
1763
|
+
*/
|
|
1764
|
+
getConfig() {
|
|
1765
|
+
return {
|
|
1766
|
+
apiKey: this.apiKey.substring(0, 15) + '...', // Don't expose full key
|
|
1767
|
+
network: this.network,
|
|
1768
|
+
gatewayUrl: this.gatewayUrl,
|
|
1769
|
+
timeout: this.timeout,
|
|
1770
|
+
debug: this.debug,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Create a SmoothSend transaction submitter with minimal configuration
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```typescript
|
|
1779
|
+
* const submitter = createSmoothSendSubmitter('pk_nogas_your_key');
|
|
1780
|
+
* ```
|
|
1781
|
+
*/
|
|
1782
|
+
function createSmoothSendSubmitter(apiKey, options) {
|
|
1783
|
+
return new SmoothSendTransactionSubmitter({
|
|
1784
|
+
apiKey,
|
|
1785
|
+
...options,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* Script Composer Client
|
|
1791
|
+
*
|
|
1792
|
+
* Wrapper for Script Composer gasless transactions.
|
|
1793
|
+
* Use this for token transfers on mainnet with free tier (fee deducted from token).
|
|
1794
|
+
*
|
|
1795
|
+
* @remarks
|
|
1796
|
+
* Script Composer builds a batched transaction that:
|
|
1797
|
+
* 1. Withdraws (amount + fee) from sender
|
|
1798
|
+
* 2. Deposits amount to recipient
|
|
1799
|
+
* 3. Deposits fee to treasury
|
|
1800
|
+
*
|
|
1801
|
+
* This allows gasless transactions where the fee is paid in the token being transferred.
|
|
1802
|
+
*
|
|
1803
|
+
* @example
|
|
1804
|
+
* ```typescript
|
|
1805
|
+
* import { ScriptComposerClient } from '@smoothsend/aptos-sdk';
|
|
1806
|
+
*
|
|
1807
|
+
* const client = new ScriptComposerClient({
|
|
1808
|
+
* apiKey: 'pk_nogas_xxx',
|
|
1809
|
+
* network: 'mainnet'
|
|
1810
|
+
* });
|
|
1811
|
+
*
|
|
1812
|
+
* // Step 1: Build transaction (fee calculated automatically)
|
|
1813
|
+
* const { transactionBytes, fee, totalAmount } = await client.buildTransfer({
|
|
1814
|
+
* sender: wallet.address,
|
|
1815
|
+
* recipient: '0x123...',
|
|
1816
|
+
* amount: '1000000', // 1 USDC (6 decimals)
|
|
1817
|
+
* assetType: '0x...::usdc::USDC',
|
|
1818
|
+
* decimals: 6,
|
|
1819
|
+
* symbol: 'USDC'
|
|
1820
|
+
* });
|
|
1821
|
+
*
|
|
1822
|
+
* // Step 2: Sign with wallet
|
|
1823
|
+
* const signedTx = await wallet.signTransaction(transactionBytes);
|
|
1824
|
+
*
|
|
1825
|
+
* // Step 3: Submit
|
|
1826
|
+
* const result = await client.submitSignedTransaction({
|
|
1827
|
+
* transactionBytes: signedTx.transactionBytes,
|
|
1828
|
+
* authenticatorBytes: signedTx.authenticatorBytes
|
|
1829
|
+
* });
|
|
1830
|
+
*
|
|
1831
|
+
* console.log('Tx:', result.txHash);
|
|
1832
|
+
* ```
|
|
1833
|
+
*/
|
|
1834
|
+
/**
|
|
1835
|
+
* Script Composer Client
|
|
1836
|
+
*
|
|
1837
|
+
* For gasless token transfers with fee deducted from the token.
|
|
1838
|
+
* Use this on mainnet with free tier, or anytime you want fee-in-token.
|
|
1839
|
+
*/
|
|
1840
|
+
class ScriptComposerClient {
|
|
1841
|
+
constructor(config) {
|
|
1842
|
+
if (!config.apiKey) {
|
|
1843
|
+
throw new SmoothSendError('API key is required', 'MISSING_API_KEY', 400);
|
|
1844
|
+
}
|
|
1845
|
+
if (!config.network) {
|
|
1846
|
+
throw new SmoothSendError('Network is required (testnet or mainnet)', 'MISSING_NETWORK', 400);
|
|
1847
|
+
}
|
|
1848
|
+
this.config = {
|
|
1849
|
+
apiKey: config.apiKey,
|
|
1850
|
+
network: config.network,
|
|
1851
|
+
proxyUrl: config.proxyUrl,
|
|
1852
|
+
timeout: config.timeout || 30000,
|
|
1853
|
+
debug: config.debug || false,
|
|
1854
|
+
};
|
|
1855
|
+
this.httpClient = new HttpClient({
|
|
1856
|
+
apiKey: this.config.apiKey,
|
|
1857
|
+
network: this.config.network,
|
|
1858
|
+
timeout: this.config.timeout,
|
|
1859
|
+
retries: 3,
|
|
1860
|
+
includeOrigin: this.isBrowser(),
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
isBrowser() {
|
|
1864
|
+
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
1865
|
+
}
|
|
1866
|
+
log(message, data) {
|
|
1867
|
+
if (this.config.debug) {
|
|
1868
|
+
console.log(`[ScriptComposer] ${message}`, data || '');
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Estimate fee for a transfer without building the transaction
|
|
1873
|
+
*
|
|
1874
|
+
* @param params Transfer parameters
|
|
1875
|
+
* @returns Fee estimation with pricing details
|
|
1876
|
+
*/
|
|
1877
|
+
async estimateFee(params) {
|
|
1878
|
+
this.log('Estimating fee', params);
|
|
1879
|
+
try {
|
|
1880
|
+
const response = await this.httpClient.post('/api/v1/relayer/estimate-fee', {
|
|
1881
|
+
sender: params.sender,
|
|
1882
|
+
recipient: params.recipient,
|
|
1883
|
+
amount: params.amount,
|
|
1884
|
+
assetType: params.assetType,
|
|
1885
|
+
decimals: params.decimals,
|
|
1886
|
+
symbol: params.symbol,
|
|
1887
|
+
network: this.config.network,
|
|
1888
|
+
apiKey: this.config.apiKey,
|
|
1889
|
+
});
|
|
1890
|
+
this.log('Fee estimate received', response.data);
|
|
1891
|
+
return response.data;
|
|
1892
|
+
}
|
|
1893
|
+
catch (error) {
|
|
1894
|
+
this.log('Fee estimation failed', error);
|
|
1895
|
+
if (error instanceof SmoothSendError) {
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1898
|
+
throw new SmoothSendError(`Failed to estimate fee: ${error.message}`, 'FEE_ESTIMATION_FAILED', 500, { originalError: error.message });
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Build a gasless transfer transaction
|
|
1903
|
+
*
|
|
1904
|
+
* Returns unsigned transaction bytes that must be signed by the user's wallet.
|
|
1905
|
+
* The fee will be deducted from the token being transferred.
|
|
1906
|
+
*
|
|
1907
|
+
* @param params Transfer parameters
|
|
1908
|
+
* @returns Transaction bytes for signing and fee details
|
|
1909
|
+
*/
|
|
1910
|
+
async buildTransfer(params) {
|
|
1911
|
+
this.log('Building transfer', params);
|
|
1912
|
+
// Validate parameters
|
|
1913
|
+
if (!params.sender || !params.recipient || !params.amount) {
|
|
1914
|
+
throw new SmoothSendError('Missing required parameters: sender, recipient, amount', 'INVALID_PARAMETERS', 400);
|
|
1915
|
+
}
|
|
1916
|
+
if (!params.assetType || params.decimals === undefined || !params.symbol) {
|
|
1917
|
+
throw new SmoothSendError('Missing token parameters: assetType, decimals, symbol', 'INVALID_PARAMETERS', 400);
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
// Call the gasless-transaction endpoint in Script Composer mode
|
|
1921
|
+
const response = await this.httpClient.post('/api/v1/relayer/gasless-transaction', {
|
|
1922
|
+
sender: params.sender,
|
|
1923
|
+
recipient: params.recipient,
|
|
1924
|
+
amount: params.amount,
|
|
1925
|
+
assetType: params.assetType,
|
|
1926
|
+
decimals: params.decimals,
|
|
1927
|
+
symbol: params.symbol,
|
|
1928
|
+
network: this.config.network,
|
|
1929
|
+
});
|
|
1930
|
+
this.log('Transaction built', response.data);
|
|
1931
|
+
return response.data;
|
|
1932
|
+
}
|
|
1933
|
+
catch (error) {
|
|
1934
|
+
this.log('Build transfer failed', error);
|
|
1935
|
+
if (error instanceof SmoothSendError) {
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1938
|
+
throw new SmoothSendError(`Failed to build transfer: ${error.message}`, 'BUILD_TRANSFER_FAILED', 500, { originalError: error.message });
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Submit a signed transaction
|
|
1943
|
+
*
|
|
1944
|
+
* After the user signs the transaction bytes returned by buildTransfer(),
|
|
1945
|
+
* use this method to submit the signed transaction.
|
|
1946
|
+
*
|
|
1947
|
+
* @param params Signed transaction bytes
|
|
1948
|
+
* @returns Transaction result with hash
|
|
1949
|
+
*/
|
|
1950
|
+
async submitSignedTransaction(params) {
|
|
1951
|
+
this.log('Submitting signed transaction');
|
|
1952
|
+
if (!params.transactionBytes || !params.authenticatorBytes) {
|
|
1953
|
+
throw new SmoothSendError('Missing required parameters: transactionBytes, authenticatorBytes', 'INVALID_PARAMETERS', 400);
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
// Call the gasless-transaction endpoint in Legacy mode (with signed tx)
|
|
1957
|
+
const response = await this.httpClient.post('/api/v1/relayer/gasless-transaction', {
|
|
1958
|
+
transactionBytes: params.transactionBytes,
|
|
1959
|
+
authenticatorBytes: params.authenticatorBytes,
|
|
1960
|
+
network: this.config.network,
|
|
1961
|
+
});
|
|
1962
|
+
this.log('Transaction submitted', response.data);
|
|
1963
|
+
return {
|
|
1964
|
+
success: response.data.success,
|
|
1965
|
+
requestId: response.data.requestId,
|
|
1966
|
+
txHash: response.data.txnHash || response.data.txHash,
|
|
1967
|
+
gasUsed: response.data.gasUsed,
|
|
1968
|
+
vmStatus: response.data.vmStatus,
|
|
1969
|
+
sender: response.data.sender,
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
catch (error) {
|
|
1973
|
+
this.log('Submit transaction failed', error);
|
|
1974
|
+
if (error instanceof SmoothSendError) {
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
throw new SmoothSendError(`Failed to submit transaction: ${error.message}`, 'SUBMIT_FAILED', 500, { originalError: error.message });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Complete transfer flow: build, sign, and submit
|
|
1982
|
+
*
|
|
1983
|
+
* Convenience method that handles the entire flow.
|
|
1984
|
+
* Requires a wallet that can sign transactions.
|
|
1985
|
+
*
|
|
1986
|
+
* @param params Transfer parameters
|
|
1987
|
+
* @param wallet Wallet with signTransaction method
|
|
1988
|
+
* @returns Transaction result
|
|
1989
|
+
*
|
|
1990
|
+
* @example
|
|
1991
|
+
* ```typescript
|
|
1992
|
+
* const result = await client.transfer({
|
|
1993
|
+
* sender: wallet.address,
|
|
1994
|
+
* recipient: '0x123...',
|
|
1995
|
+
* amount: '1000000',
|
|
1996
|
+
* assetType: USDC_ADDRESS,
|
|
1997
|
+
* decimals: 6,
|
|
1998
|
+
* symbol: 'USDC'
|
|
1999
|
+
* }, wallet);
|
|
2000
|
+
* ```
|
|
2001
|
+
*/
|
|
2002
|
+
async transfer(params, wallet) {
|
|
2003
|
+
this.log('Starting complete transfer flow');
|
|
2004
|
+
// Step 1: Build transaction
|
|
2005
|
+
const buildResult = await this.buildTransfer(params);
|
|
2006
|
+
this.log('Transaction built, fee:', buildResult.fee);
|
|
2007
|
+
// Step 2: Sign with wallet
|
|
2008
|
+
const signedTx = await wallet.signTransaction(buildResult.transactionBytes);
|
|
2009
|
+
this.log('Transaction signed');
|
|
2010
|
+
// Step 3: Submit
|
|
2011
|
+
const result = await this.submitSignedTransaction({
|
|
2012
|
+
transactionBytes: signedTx.transactionBytes,
|
|
2013
|
+
authenticatorBytes: signedTx.authenticatorBytes,
|
|
2014
|
+
});
|
|
2015
|
+
this.log('Transfer complete', result);
|
|
2016
|
+
return result;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Get current network
|
|
2020
|
+
*/
|
|
2021
|
+
getNetwork() {
|
|
2022
|
+
return this.config.network;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Set network
|
|
2026
|
+
*/
|
|
2027
|
+
setNetwork(network) {
|
|
2028
|
+
this.config.network = network;
|
|
2029
|
+
this.httpClient.setNetwork(network);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Create a Script Composer client (convenience function)
|
|
2034
|
+
*/
|
|
2035
|
+
function createScriptComposerClient(config) {
|
|
2036
|
+
return new ScriptComposerClient(config);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* SmoothSend SDK v2.0
|
|
2041
|
+
*
|
|
2042
|
+
* Multi-chain gasless transaction SDK for seamless dApp integration
|
|
2043
|
+
*
|
|
2044
|
+
* @remarks
|
|
2045
|
+
* The SDK provides a simple interface for executing gasless token transfers
|
|
2046
|
+
* across multiple blockchain networks. All requests route through the proxy
|
|
2047
|
+
* worker at proxy.smoothsend.xyz with API key authentication.
|
|
2048
|
+
*
|
|
2049
|
+
* @packageDocumentation
|
|
2050
|
+
*
|
|
2051
|
+
* @example
|
|
2052
|
+
* Basic usage:
|
|
2053
|
+
* ```typescript
|
|
2054
|
+
* import { SmoothSendSDK } from '@smoothsend/sdk';
|
|
2055
|
+
*
|
|
2056
|
+
* const sdk = new SmoothSendSDK({
|
|
2057
|
+
* apiKey: 'no_gas_abc123...',
|
|
2058
|
+
* network: 'testnet'
|
|
2059
|
+
* });
|
|
2060
|
+
*
|
|
2061
|
+
* const result = await sdk.transfer({
|
|
2062
|
+
* from: '0x123...',
|
|
2063
|
+
* to: '0x456...',
|
|
2064
|
+
* token: 'USDC',
|
|
2065
|
+
* amount: '1000000',
|
|
2066
|
+
* chain: 'aptos-testnet'
|
|
2067
|
+
* }, wallet);
|
|
2068
|
+
*
|
|
2069
|
+
* console.log('Transaction:', result.txHash);
|
|
2070
|
+
* ```
|
|
2071
|
+
*/
|
|
2072
|
+
// Main SDK export
|
|
2073
|
+
/**
|
|
2074
|
+
* SDK version
|
|
2075
|
+
* @public
|
|
2076
|
+
*/
|
|
2077
|
+
const VERSION = '2.1.0'; // Updated for wallet adapter support
|
|
2078
|
+
|
|
2079
|
+
exports.APTOS_ERROR_CODES = APTOS_ERROR_CODES;
|
|
2080
|
+
exports.AptosAdapter = AptosAdapter;
|
|
2081
|
+
exports.AuthenticationError = AuthenticationError;
|
|
2082
|
+
exports.CHAIN_ECOSYSTEM_MAP = CHAIN_ECOSYSTEM_MAP;
|
|
2083
|
+
exports.HttpClient = HttpClient;
|
|
2084
|
+
exports.NetworkError = NetworkError;
|
|
2085
|
+
exports.RateLimitError = RateLimitError;
|
|
2086
|
+
exports.ScriptComposerClient = ScriptComposerClient;
|
|
2087
|
+
exports.SmoothSendError = SmoothSendError;
|
|
2088
|
+
exports.SmoothSendSDK = SmoothSendSDK;
|
|
2089
|
+
exports.SmoothSendTransactionSubmitter = SmoothSendTransactionSubmitter;
|
|
2090
|
+
exports.VERSION = VERSION;
|
|
2091
|
+
exports.ValidationError = ValidationError;
|
|
2092
|
+
exports.createErrorFromResponse = createErrorFromResponse;
|
|
2093
|
+
exports.createNetworkError = createNetworkError;
|
|
2094
|
+
exports.createScriptComposerClient = createScriptComposerClient;
|
|
2095
|
+
exports.createSmoothSendSubmitter = createSmoothSendSubmitter;
|
|
2096
|
+
exports.default = SmoothSendSDK;
|
|
2097
|
+
//# sourceMappingURL=index.js.map
|