@flashnet/sdk 0.3.39 → 0.4.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.
Files changed (45) hide show
  1. package/dist/cjs/index.d.ts +3 -2
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/index.js +10 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/src/api/client.d.ts.map +1 -1
  6. package/dist/cjs/src/api/client.js +14 -15
  7. package/dist/cjs/src/api/client.js.map +1 -1
  8. package/dist/cjs/src/api/typed-endpoints.d.ts +15 -1
  9. package/dist/cjs/src/api/typed-endpoints.d.ts.map +1 -1
  10. package/dist/cjs/src/api/typed-endpoints.js +14 -2
  11. package/dist/cjs/src/api/typed-endpoints.js.map +1 -1
  12. package/dist/cjs/src/client/FlashnetClient.d.ts +109 -1
  13. package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
  14. package/dist/cjs/src/client/FlashnetClient.js +440 -109
  15. package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
  16. package/dist/cjs/src/types/errors.d.ts +264 -0
  17. package/dist/cjs/src/types/errors.d.ts.map +1 -0
  18. package/dist/cjs/src/types/errors.js +758 -0
  19. package/dist/cjs/src/types/errors.js.map +1 -0
  20. package/dist/cjs/src/types/index.d.ts +13 -1
  21. package/dist/cjs/src/types/index.d.ts.map +1 -1
  22. package/dist/cjs/src/types/index.js.map +1 -1
  23. package/dist/esm/index.d.ts +3 -2
  24. package/dist/esm/index.d.ts.map +1 -1
  25. package/dist/esm/index.js +2 -1
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/src/api/client.d.ts.map +1 -1
  28. package/dist/esm/src/api/client.js +14 -15
  29. package/dist/esm/src/api/client.js.map +1 -1
  30. package/dist/esm/src/api/typed-endpoints.d.ts +15 -1
  31. package/dist/esm/src/api/typed-endpoints.d.ts.map +1 -1
  32. package/dist/esm/src/api/typed-endpoints.js +14 -2
  33. package/dist/esm/src/api/typed-endpoints.js.map +1 -1
  34. package/dist/esm/src/client/FlashnetClient.d.ts +109 -1
  35. package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
  36. package/dist/esm/src/client/FlashnetClient.js +440 -109
  37. package/dist/esm/src/client/FlashnetClient.js.map +1 -1
  38. package/dist/esm/src/types/errors.d.ts +264 -0
  39. package/dist/esm/src/types/errors.d.ts.map +1 -0
  40. package/dist/esm/src/types/errors.js +749 -0
  41. package/dist/esm/src/types/errors.js.map +1 -0
  42. package/dist/esm/src/types/index.d.ts +13 -1
  43. package/dist/esm/src/types/index.d.ts.map +1 -1
  44. package/dist/esm/src/types/index.js.map +1 -1
  45. package/package.json +5 -5
@@ -10,6 +10,7 @@ var hex = require('../utils/hex.js');
10
10
  var intents = require('../utils/intents.js');
11
11
  var sparkAddress = require('../utils/spark-address.js');
12
12
  var tokenAddress = require('../utils/tokenAddress.js');
13
+ var errors = require('../types/errors.js');
13
14
 
14
15
  /**
15
16
  * FlashnetClient - A comprehensive client for interacting with Flashnet AMM
@@ -527,10 +528,14 @@ class FlashnetClient {
527
528
  assetAddress: params.assetAAddress,
528
529
  amount: assetAInitialReserve,
529
530
  });
530
- const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
531
- if (!confirmResponse.confirmed) {
532
- throw new Error(`Failed to confirm initial deposit: ${confirmResponse.message}`);
533
- }
531
+ // Execute confirm with auto-clawback on failure
532
+ await this.executeWithAutoClawback(async () => {
533
+ const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
534
+ if (!confirmResponse.confirmed) {
535
+ throw new Error(`Failed to confirm initial deposit: ${confirmResponse.message}`);
536
+ }
537
+ return confirmResponse;
538
+ }, [assetATransferId], createResponse.poolId);
534
539
  return createResponse;
535
540
  }
536
541
  /**
@@ -577,6 +582,9 @@ class FlashnetClient {
577
582
  }
578
583
  /**
579
584
  * Execute a swap
585
+ *
586
+ * If the swap fails with a clawbackable error, the SDK will automatically
587
+ * attempt to recover the transferred funds via clawback.
580
588
  */
581
589
  async executeSwap(params) {
582
590
  await this.ensureInitialized();
@@ -598,11 +606,11 @@ class FlashnetClient {
598
606
  assetAddress: params.assetInAddress,
599
607
  amount: params.amountIn,
600
608
  }, "Insufficient balance for swap: ");
601
- const response = await this.executeSwapIntent({
609
+ // Execute with auto-clawback on failure
610
+ return this.executeWithAutoClawback(() => this.executeSwapIntent({
602
611
  ...params,
603
612
  transferId,
604
- });
605
- return response;
613
+ }), [transferId], params.poolId);
606
614
  }
607
615
  async executeSwapIntent(params) {
608
616
  await this.ensureInitialized();
@@ -649,10 +657,27 @@ class FlashnetClient {
649
657
  // Check if the swap was accepted
650
658
  if (!response.accepted) {
651
659
  const errorMessage = response.error || "Swap rejected by the AMM";
652
- const refundInfo = response.refundedAmount
660
+ const hasRefund = !!response.refundedAmount;
661
+ const refundInfo = hasRefund
653
662
  ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetAddress} via transfer ${response.refundTransferId}`
654
663
  : "";
655
- throw new Error(`${errorMessage}.${refundInfo}`);
664
+ // If refund was provided, funds are safe - use auto_refund recovery
665
+ // If no refund, funds may need clawback
666
+ throw new errors.FlashnetError(`${errorMessage}.${refundInfo}`, {
667
+ response: {
668
+ errorCode: hasRefund ? "FSAG-4202" : "UNKNOWN", // Slippage if refunded
669
+ errorCategory: hasRefund ? "Business" : "System",
670
+ message: `${errorMessage}.${refundInfo}`,
671
+ requestId: "",
672
+ timestamp: new Date().toISOString(),
673
+ service: "amm-gateway",
674
+ severity: "Error",
675
+ },
676
+ httpStatus: 400,
677
+ // Don't include transferIds if refunded - no clawback needed
678
+ transferIds: hasRefund ? [] : [params.transferId],
679
+ lpIdentityPublicKey: params.poolId,
680
+ });
656
681
  }
657
682
  return response;
658
683
  }
@@ -669,6 +694,9 @@ class FlashnetClient {
669
694
  }
670
695
  /**
671
696
  * Execute a route swap (multi-hop swap)
697
+ *
698
+ * If the route swap fails with a clawbackable error, the SDK will automatically
699
+ * attempt to recover the transferred funds via clawback.
672
700
  */
673
701
  async executeRouteSwap(params) {
674
702
  await this.ensureInitialized();
@@ -705,68 +733,85 @@ class FlashnetClient {
705
733
  assetAddress: params.initialAssetAddress,
706
734
  amount: params.inputAmount,
707
735
  }, "Insufficient balance for route swap: ");
708
- // Prepare hops for validation
709
- const hops = params.hops.map((hop) => ({
710
- lpIdentityPublicKey: hop.poolId,
711
- inputAssetAddress: this.toHexTokenIdentifier(hop.assetInAddress),
712
- outputAssetAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
713
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
714
- hop.hopIntegratorFeeRateBps !== null
715
- ? hop.hopIntegratorFeeRateBps.toString()
716
- : "0",
717
- }));
718
- // Convert hops and ensure integrator fee is always present
719
- const requestHops = params.hops.map((hop) => ({
720
- poolId: hop.poolId,
721
- assetInAddress: this.toHexTokenIdentifier(hop.assetInAddress),
722
- assetOutAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
723
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
724
- hop.hopIntegratorFeeRateBps !== null
725
- ? hop.hopIntegratorFeeRateBps.toString()
726
- : "0",
727
- }));
728
- // Generate route swap intent
729
- const nonce = index$2.generateNonce();
730
- const intentMessage = intents.generateRouteSwapIntentMessage({
731
- userPublicKey: this.publicKey,
732
- hops: hops.map((hop) => ({
733
- lpIdentityPublicKey: hop.lpIdentityPublicKey,
734
- inputAssetAddress: hop.inputAssetAddress,
735
- outputAssetAddress: hop.outputAssetAddress,
736
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps,
737
- })),
738
- initialSparkTransferId: initialTransferId,
739
- inputAmount: params.inputAmount.toString(),
740
- maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
741
- minAmountOut: params.minAmountOut,
742
- nonce,
743
- defaultIntegratorFeeRateBps: params.integratorFeeRateBps?.toString(),
744
- });
745
- // Sign intent
746
- const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
747
- const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
748
- const request = {
749
- userPublicKey: this.publicKey,
750
- hops: requestHops,
751
- initialSparkTransferId: initialTransferId,
752
- inputAmount: params.inputAmount.toString(),
753
- maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
754
- minAmountOut: params.minAmountOut,
755
- nonce,
756
- signature: hex.getHexFromUint8Array(signature),
757
- integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
758
- integratorPublicKey: params.integratorPublicKey || "",
759
- };
760
- const response = await this.typedApi.executeRouteSwap(request);
761
- // Check if the route swap was accepted
762
- if (!response.accepted) {
763
- const errorMessage = response.error || "Route swap rejected by the AMM";
764
- const refundInfo = response.refundedAmount
765
- ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetPublicKey} via transfer ${response.refundTransferId}`
766
- : "";
767
- throw new Error(`${errorMessage}.${refundInfo}`);
768
- }
769
- return response;
736
+ // Execute with auto-clawback on failure
737
+ return this.executeWithAutoClawback(async () => {
738
+ // Prepare hops for validation
739
+ const hops = params.hops.map((hop) => ({
740
+ lpIdentityPublicKey: hop.poolId,
741
+ inputAssetAddress: this.toHexTokenIdentifier(hop.assetInAddress),
742
+ outputAssetAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
743
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
744
+ hop.hopIntegratorFeeRateBps !== null
745
+ ? hop.hopIntegratorFeeRateBps.toString()
746
+ : "0",
747
+ }));
748
+ // Convert hops and ensure integrator fee is always present
749
+ const requestHops = params.hops.map((hop) => ({
750
+ poolId: hop.poolId,
751
+ assetInAddress: this.toHexTokenIdentifier(hop.assetInAddress),
752
+ assetOutAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
753
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
754
+ hop.hopIntegratorFeeRateBps !== null
755
+ ? hop.hopIntegratorFeeRateBps.toString()
756
+ : "0",
757
+ }));
758
+ // Generate route swap intent
759
+ const nonce = index$2.generateNonce();
760
+ const intentMessage = intents.generateRouteSwapIntentMessage({
761
+ userPublicKey: this.publicKey,
762
+ hops: hops.map((hop) => ({
763
+ lpIdentityPublicKey: hop.lpIdentityPublicKey,
764
+ inputAssetAddress: hop.inputAssetAddress,
765
+ outputAssetAddress: hop.outputAssetAddress,
766
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps,
767
+ })),
768
+ initialSparkTransferId: initialTransferId,
769
+ inputAmount: params.inputAmount.toString(),
770
+ maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
771
+ minAmountOut: params.minAmountOut,
772
+ nonce,
773
+ defaultIntegratorFeeRateBps: params.integratorFeeRateBps?.toString(),
774
+ });
775
+ // Sign intent
776
+ const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
777
+ const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
778
+ const request = {
779
+ userPublicKey: this.publicKey,
780
+ hops: requestHops,
781
+ initialSparkTransferId: initialTransferId,
782
+ inputAmount: params.inputAmount.toString(),
783
+ maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
784
+ minAmountOut: params.minAmountOut,
785
+ nonce,
786
+ signature: hex.getHexFromUint8Array(signature),
787
+ integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
788
+ integratorPublicKey: params.integratorPublicKey || "",
789
+ };
790
+ const response = await this.typedApi.executeRouteSwap(request);
791
+ // Check if the route swap was accepted
792
+ if (!response.accepted) {
793
+ const errorMessage = response.error || "Route swap rejected by the AMM";
794
+ const hasRefund = !!response.refundedAmount;
795
+ const refundInfo = hasRefund
796
+ ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetPublicKey} via transfer ${response.refundTransferId}`
797
+ : "";
798
+ throw new errors.FlashnetError(`${errorMessage}.${refundInfo}`, {
799
+ response: {
800
+ errorCode: hasRefund ? "FSAG-4202" : "UNKNOWN",
801
+ errorCategory: hasRefund ? "Business" : "System",
802
+ message: `${errorMessage}.${refundInfo}`,
803
+ requestId: "",
804
+ timestamp: new Date().toISOString(),
805
+ service: "amm-gateway",
806
+ severity: "Error",
807
+ },
808
+ httpStatus: 400,
809
+ transferIds: hasRefund ? [] : [initialTransferId],
810
+ lpIdentityPublicKey: firstPoolId,
811
+ });
812
+ }
813
+ return response;
814
+ }, [initialTransferId], firstPoolId);
770
815
  }
771
816
  // ===== Liquidity Operations =====
772
817
  /**
@@ -779,6 +824,9 @@ class FlashnetClient {
779
824
  }
780
825
  /**
781
826
  * Add liquidity to a pool
827
+ *
828
+ * If adding liquidity fails with a clawbackable error, the SDK will automatically
829
+ * attempt to recover the transferred funds via clawback.
782
830
  */
783
831
  async addLiquidity(params) {
784
832
  await this.ensureInitialized();
@@ -808,44 +856,61 @@ class FlashnetClient {
808
856
  amount: params.assetBAmount,
809
857
  },
810
858
  ], "Insufficient balance for adding liquidity: ");
811
- // Generate add liquidity intent
812
- const nonce = index$2.generateNonce();
813
- const intentMessage = intents.generateAddLiquidityIntentMessage({
814
- userPublicKey: this.publicKey,
815
- lpIdentityPublicKey: params.poolId,
816
- assetASparkTransferId: assetATransferId,
817
- assetBSparkTransferId: assetBTransferId,
818
- assetAAmount: params.assetAAmount.toString(),
819
- assetBAmount: params.assetBAmount.toString(),
820
- assetAMinAmountIn: params.assetAMinAmountIn.toString(),
821
- assetBMinAmountIn: params.assetBMinAmountIn.toString(),
822
- nonce,
823
- });
824
- // Sign intent
825
- const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
826
- const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
827
- const request = {
828
- userPublicKey: this.publicKey,
829
- poolId: params.poolId,
830
- assetASparkTransferId: assetATransferId,
831
- assetBSparkTransferId: assetBTransferId,
832
- assetAAmountToAdd: params.assetAAmount.toString(),
833
- assetBAmountToAdd: params.assetBAmount.toString(),
834
- assetAMinAmountIn: params.assetAMinAmountIn.toString(),
835
- assetBMinAmountIn: params.assetBMinAmountIn.toString(),
836
- nonce,
837
- signature: hex.getHexFromUint8Array(signature),
838
- };
839
- const response = await this.typedApi.addLiquidity(request);
840
- // Check if the liquidity addition was accepted
841
- if (!response.accepted) {
842
- const errorMessage = response.error || "Add liquidity rejected by the AMM";
843
- const refundInfo = response.refund
844
- ? ` Refunds: Asset A: ${response.refund.assetAAmount || 0}, Asset B: ${response.refund.assetBAmount || 0}`
845
- : "";
846
- throw new Error(`${errorMessage}.${refundInfo}`);
847
- }
848
- return response;
859
+ // Execute with auto-clawback on failure
860
+ return this.executeWithAutoClawback(async () => {
861
+ // Generate add liquidity intent
862
+ const nonce = index$2.generateNonce();
863
+ const intentMessage = intents.generateAddLiquidityIntentMessage({
864
+ userPublicKey: this.publicKey,
865
+ lpIdentityPublicKey: params.poolId,
866
+ assetASparkTransferId: assetATransferId,
867
+ assetBSparkTransferId: assetBTransferId,
868
+ assetAAmount: params.assetAAmount.toString(),
869
+ assetBAmount: params.assetBAmount.toString(),
870
+ assetAMinAmountIn: params.assetAMinAmountIn.toString(),
871
+ assetBMinAmountIn: params.assetBMinAmountIn.toString(),
872
+ nonce,
873
+ });
874
+ // Sign intent
875
+ const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
876
+ const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
877
+ const request = {
878
+ userPublicKey: this.publicKey,
879
+ poolId: params.poolId,
880
+ assetASparkTransferId: assetATransferId,
881
+ assetBSparkTransferId: assetBTransferId,
882
+ assetAAmountToAdd: params.assetAAmount.toString(),
883
+ assetBAmountToAdd: params.assetBAmount.toString(),
884
+ assetAMinAmountIn: params.assetAMinAmountIn.toString(),
885
+ assetBMinAmountIn: params.assetBMinAmountIn.toString(),
886
+ nonce,
887
+ signature: hex.getHexFromUint8Array(signature),
888
+ };
889
+ const response = await this.typedApi.addLiquidity(request);
890
+ // Check if the liquidity addition was accepted
891
+ if (!response.accepted) {
892
+ const errorMessage = response.error || "Add liquidity rejected by the AMM";
893
+ const hasRefund = !!(response.refund?.assetAAmount || response.refund?.assetBAmount);
894
+ const refundInfo = response.refund
895
+ ? ` Refunds: Asset A: ${response.refund.assetAAmount || 0}, Asset B: ${response.refund.assetBAmount || 0}`
896
+ : "";
897
+ throw new errors.FlashnetError(`${errorMessage}.${refundInfo}`, {
898
+ response: {
899
+ errorCode: hasRefund ? "FSAG-4203" : "UNKNOWN", // Phase error if refunded
900
+ errorCategory: hasRefund ? "Business" : "System",
901
+ message: `${errorMessage}.${refundInfo}`,
902
+ requestId: "",
903
+ timestamp: new Date().toISOString(),
904
+ service: "amm-gateway",
905
+ severity: "Error",
906
+ },
907
+ httpStatus: 400,
908
+ transferIds: hasRefund ? [] : [assetATransferId, assetBTransferId],
909
+ lpIdentityPublicKey: params.poolId,
910
+ });
911
+ }
912
+ return response;
913
+ }, [assetATransferId, assetBTransferId], params.poolId);
849
914
  }
850
915
  /**
851
916
  * Simulate removing liquidity
@@ -1260,6 +1325,272 @@ class FlashnetClient {
1260
1325
  };
1261
1326
  return this.typedApi.checkClawbackEligibility(request);
1262
1327
  }
1328
+ /**
1329
+ * List transfers eligible for clawback
1330
+ *
1331
+ * Returns a paginated list of transfers that the authenticated user
1332
+ * can potentially clawback. Filters based on:
1333
+ * - Transfers sent by the authenticated user
1334
+ * - Transfers to pools the user has interacted with
1335
+ * - Not already spent or reserved
1336
+ * - Less than 10 days old
1337
+ *
1338
+ * @param query - Optional pagination parameters (limit, offset)
1339
+ * @returns List of eligible transfers with IDs and timestamps
1340
+ */
1341
+ async listClawbackableTransfers(query) {
1342
+ await this.ensureInitialized();
1343
+ await this.ensurePingOk();
1344
+ return this.typedApi.listClawbackableTransfers(query);
1345
+ }
1346
+ /**
1347
+ * Attempt to clawback multiple transfers
1348
+ *
1349
+ * @param transferIds - Array of transfer IDs to clawback
1350
+ * @param lpIdentityPublicKey - The LP wallet public key
1351
+ * @returns Array of results for each clawback attempt
1352
+ */
1353
+ async clawbackMultiple(transferIds, lpIdentityPublicKey) {
1354
+ const results = [];
1355
+ for (const transferId of transferIds) {
1356
+ try {
1357
+ const response = await this.clawback({
1358
+ sparkTransferId: transferId,
1359
+ lpIdentityPublicKey,
1360
+ });
1361
+ results.push({
1362
+ transferId,
1363
+ success: true,
1364
+ response,
1365
+ });
1366
+ }
1367
+ catch (err) {
1368
+ results.push({
1369
+ transferId,
1370
+ success: false,
1371
+ error: err instanceof Error ? err.message : String(err),
1372
+ });
1373
+ }
1374
+ }
1375
+ return results;
1376
+ }
1377
+ /**
1378
+ * Internal helper to execute an operation with automatic clawback on failure
1379
+ *
1380
+ * @param operation - The async operation to execute
1381
+ * @param transferIds - Transfer IDs that were sent and may need clawback
1382
+ * @param lpIdentityPublicKey - The LP wallet public key for clawback
1383
+ * @returns The result of the operation
1384
+ * @throws FlashnetError with typed clawbackSummary attached
1385
+ */
1386
+ async executeWithAutoClawback(operation, transferIds, lpIdentityPublicKey) {
1387
+ try {
1388
+ return await operation();
1389
+ }
1390
+ catch (error) {
1391
+ // Convert to FlashnetError if not already
1392
+ const flashnetError = errors.FlashnetError.fromUnknown(error, {
1393
+ transferIds,
1394
+ lpIdentityPublicKey,
1395
+ });
1396
+ // Check if we should attempt clawback
1397
+ if (flashnetError.shouldClawback() && transferIds.length > 0) {
1398
+ // Attempt to clawback all transfers
1399
+ const clawbackResults = await this.clawbackMultiple(transferIds, lpIdentityPublicKey);
1400
+ // Separate successful and failed clawbacks
1401
+ const successfulClawbacks = clawbackResults.filter((r) => r.success);
1402
+ const failedClawbacks = clawbackResults.filter((r) => !r.success);
1403
+ // Build typed clawback summary
1404
+ const clawbackSummary = {
1405
+ attempted: true,
1406
+ totalTransfers: transferIds.length,
1407
+ successCount: successfulClawbacks.length,
1408
+ failureCount: failedClawbacks.length,
1409
+ results: clawbackResults,
1410
+ recoveredTransferIds: successfulClawbacks.map((r) => r.transferId),
1411
+ unrecoveredTransferIds: failedClawbacks.map((r) => r.transferId),
1412
+ };
1413
+ // Create enhanced error message
1414
+ let enhancedMessage = flashnetError.message;
1415
+ if (successfulClawbacks.length > 0) {
1416
+ enhancedMessage += ` [Auto-clawback: ${successfulClawbacks.length}/${transferIds.length} transfers recovered]`;
1417
+ }
1418
+ if (failedClawbacks.length > 0) {
1419
+ const failedIds = failedClawbacks.map((r) => r.transferId).join(", ");
1420
+ enhancedMessage += ` [Clawback failed for: ${failedIds}]`;
1421
+ }
1422
+ // Determine remediation based on clawback results
1423
+ let remediation;
1424
+ if (clawbackSummary.failureCount === 0) {
1425
+ remediation =
1426
+ "Your funds have been automatically recovered. No action needed.";
1427
+ }
1428
+ else if (clawbackSummary.successCount > 0) {
1429
+ remediation = `${clawbackSummary.successCount} transfer(s) recovered. Manual clawback needed for remaining transfers.`;
1430
+ }
1431
+ else {
1432
+ remediation =
1433
+ flashnetError.remediation ??
1434
+ "Automatic recovery failed. Please initiate a manual clawback.";
1435
+ }
1436
+ // Throw new error with typed clawback summary
1437
+ const errorWithClawback = new errors.FlashnetError(enhancedMessage, {
1438
+ response: {
1439
+ errorCode: flashnetError.errorCode,
1440
+ errorCategory: flashnetError.category,
1441
+ message: enhancedMessage,
1442
+ details: flashnetError.details,
1443
+ requestId: flashnetError.requestId,
1444
+ timestamp: flashnetError.timestamp,
1445
+ service: flashnetError.service,
1446
+ severity: flashnetError.severity,
1447
+ remediation,
1448
+ },
1449
+ httpStatus: flashnetError.httpStatus,
1450
+ transferIds: clawbackSummary.unrecoveredTransferIds,
1451
+ lpIdentityPublicKey,
1452
+ clawbackSummary,
1453
+ });
1454
+ throw errorWithClawback;
1455
+ }
1456
+ // Not a clawbackable error, just re-throw
1457
+ throw flashnetError;
1458
+ }
1459
+ }
1460
+ // ===== Clawback Monitor =====
1461
+ /**
1462
+ * Start a background job that periodically polls for clawbackable transfers
1463
+ * and automatically claws them back.
1464
+ *
1465
+ * @param options - Monitor configuration options
1466
+ * @returns ClawbackMonitorHandle to control the monitor
1467
+ *
1468
+ * @example
1469
+ * ```typescript
1470
+ * const monitor = client.startClawbackMonitor({
1471
+ * intervalMs: 60000, // Poll every 60 seconds
1472
+ * onClawbackSuccess: (result) => console.log('Recovered:', result.transferId),
1473
+ * onClawbackError: (transferId, error) => console.error('Failed:', transferId, error),
1474
+ * });
1475
+ *
1476
+ * // Later, to stop:
1477
+ * monitor.stop();
1478
+ * ```
1479
+ */
1480
+ startClawbackMonitor(options = {}) {
1481
+ const { intervalMs = 60000, // Default: 1 minute
1482
+ batchSize = 2, // Default: 2 clawbacks per batch (rate limit safe)
1483
+ batchDelayMs = 500, // Default: 500ms between batches
1484
+ maxTransfersPerPoll = 100, // Default: max 100 transfers per poll
1485
+ onClawbackSuccess, onClawbackError, onPollComplete, onPollError, } = options;
1486
+ let isRunning = true;
1487
+ let timeoutId = null;
1488
+ let currentPollPromise = null;
1489
+ const poll = async () => {
1490
+ const result = {
1491
+ transfersFound: 0,
1492
+ clawbacksAttempted: 0,
1493
+ clawbacksSucceeded: 0,
1494
+ clawbacksFailed: 0,
1495
+ results: [],
1496
+ };
1497
+ try {
1498
+ // Fetch clawbackable transfers
1499
+ const response = await this.listClawbackableTransfers({
1500
+ limit: maxTransfersPerPoll,
1501
+ });
1502
+ result.transfersFound = response.transfers.length;
1503
+ if (response.transfers.length === 0) {
1504
+ return result;
1505
+ }
1506
+ // Process in batches to respect rate limits
1507
+ for (let i = 0; i < response.transfers.length; i += batchSize) {
1508
+ if (!isRunning) {
1509
+ break;
1510
+ }
1511
+ const batch = response.transfers.slice(i, i + batchSize);
1512
+ // Process batch concurrently
1513
+ const batchResults = await Promise.all(batch.map(async (transfer) => {
1514
+ result.clawbacksAttempted++;
1515
+ try {
1516
+ const clawbackResponse = await this.clawback({
1517
+ sparkTransferId: transfer.id,
1518
+ lpIdentityPublicKey: transfer.lpIdentityPublicKey,
1519
+ });
1520
+ const attemptResult = {
1521
+ transferId: transfer.id,
1522
+ success: true,
1523
+ response: clawbackResponse,
1524
+ };
1525
+ result.clawbacksSucceeded++;
1526
+ onClawbackSuccess?.(attemptResult);
1527
+ return attemptResult;
1528
+ }
1529
+ catch (err) {
1530
+ const attemptResult = {
1531
+ transferId: transfer.id,
1532
+ success: false,
1533
+ error: err instanceof Error ? err.message : String(err),
1534
+ };
1535
+ result.clawbacksFailed++;
1536
+ onClawbackError?.(transfer.id, err);
1537
+ return attemptResult;
1538
+ }
1539
+ }));
1540
+ result.results.push(...batchResults);
1541
+ // Wait between batches if there are more to process
1542
+ if (i + batchSize < response.transfers.length && isRunning) {
1543
+ await new Promise((resolve) => setTimeout(resolve, batchDelayMs));
1544
+ }
1545
+ }
1546
+ }
1547
+ catch (err) {
1548
+ onPollError?.(err);
1549
+ }
1550
+ return result;
1551
+ };
1552
+ const scheduleNextPoll = () => {
1553
+ if (!isRunning) {
1554
+ return;
1555
+ }
1556
+ timeoutId = setTimeout(async () => {
1557
+ if (!isRunning) {
1558
+ return;
1559
+ }
1560
+ currentPollPromise = (async () => {
1561
+ const result = await poll();
1562
+ onPollComplete?.(result);
1563
+ scheduleNextPoll();
1564
+ })();
1565
+ }, intervalMs);
1566
+ };
1567
+ // Start first poll immediately
1568
+ currentPollPromise = (async () => {
1569
+ const result = await poll();
1570
+ onPollComplete?.(result);
1571
+ scheduleNextPoll();
1572
+ })();
1573
+ return {
1574
+ isRunning: () => isRunning,
1575
+ stop: async () => {
1576
+ isRunning = false;
1577
+ if (timeoutId) {
1578
+ clearTimeout(timeoutId);
1579
+ timeoutId = null;
1580
+ }
1581
+ // Wait for current poll to complete
1582
+ if (currentPollPromise) {
1583
+ await currentPollPromise.catch(() => { });
1584
+ }
1585
+ },
1586
+ pollNow: async () => {
1587
+ if (!isRunning) {
1588
+ throw new Error("Monitor is stopped");
1589
+ }
1590
+ return poll();
1591
+ },
1592
+ };
1593
+ }
1263
1594
  // ===== Token Address Operations =====
1264
1595
  /**
1265
1596
  * Encode a token identifier into a human-readable token address using the client's Spark network