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