@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/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