@flashnet/sdk 0.3.40 → 0.4.1

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 (43) 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 +9 -1
  9. package/dist/cjs/src/api/typed-endpoints.d.ts.map +1 -1
  10. package/dist/cjs/src/api/typed-endpoints.js +6 -2
  11. package/dist/cjs/src/api/typed-endpoints.js.map +1 -1
  12. package/dist/cjs/src/client/FlashnetClient.d.ts +232 -1
  13. package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
  14. package/dist/cjs/src/client/FlashnetClient.js +905 -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 +1 -1
  21. package/dist/cjs/src/types/index.d.ts.map +1 -1
  22. package/dist/esm/index.d.ts +3 -2
  23. package/dist/esm/index.d.ts.map +1 -1
  24. package/dist/esm/index.js +2 -1
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/src/api/client.d.ts.map +1 -1
  27. package/dist/esm/src/api/client.js +14 -15
  28. package/dist/esm/src/api/client.js.map +1 -1
  29. package/dist/esm/src/api/typed-endpoints.d.ts +9 -1
  30. package/dist/esm/src/api/typed-endpoints.d.ts.map +1 -1
  31. package/dist/esm/src/api/typed-endpoints.js +6 -2
  32. package/dist/esm/src/api/typed-endpoints.js.map +1 -1
  33. package/dist/esm/src/client/FlashnetClient.d.ts +232 -1
  34. package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
  35. package/dist/esm/src/client/FlashnetClient.js +905 -109
  36. package/dist/esm/src/client/FlashnetClient.js.map +1 -1
  37. package/dist/esm/src/types/errors.d.ts +264 -0
  38. package/dist/esm/src/types/errors.d.ts.map +1 -0
  39. package/dist/esm/src/types/errors.js +749 -0
  40. package/dist/esm/src/types/errors.js.map +1 -0
  41. package/dist/esm/src/types/index.d.ts +1 -1
  42. package/dist/esm/src/types/index.d.ts.map +1 -1
  43. 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
@@ -1278,6 +1343,254 @@ class FlashnetClient {
1278
1343
  await this.ensurePingOk();
1279
1344
  return this.typedApi.listClawbackableTransfers(query);
1280
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
+ }
1281
1594
  // ===== Token Address Operations =====
1282
1595
  /**
1283
1596
  * Encode a token identifier into a human-readable token address using the client's Spark network
@@ -1475,6 +1788,489 @@ class FlashnetClient {
1475
1788
  throw new Error(errorMessage);
1476
1789
  }
1477
1790
  }
1791
+ // ===== Lightning Payment with Token =====
1792
+ /**
1793
+ * Get a quote for paying a Lightning invoice with a token.
1794
+ * This calculates the optimal pool and token amount needed.
1795
+ *
1796
+ * @param invoice - BOLT11-encoded Lightning invoice
1797
+ * @param tokenAddress - Token identifier to use for payment
1798
+ * @param options - Optional configuration (slippage, integrator fees, etc.)
1799
+ * @returns Quote with pricing details
1800
+ * @throws Error if invoice amount or token amount is below Flashnet minimums
1801
+ */
1802
+ async getPayLightningWithTokenQuote(invoice, tokenAddress, options) {
1803
+ await this.ensureInitialized();
1804
+ // Decode the invoice to get the amount
1805
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1806
+ if (!invoiceAmountSats || invoiceAmountSats <= 0) {
1807
+ throw new Error("Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.");
1808
+ }
1809
+ // Get Lightning fee estimate
1810
+ const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1811
+ // Total BTC needed = invoice amount + lightning fee
1812
+ const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1813
+ // Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
1814
+ // The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
1815
+ // that after masking we still have the required amount.
1816
+ // Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
1817
+ const BTC_VARIABLE_FEE_BITS = 6n;
1818
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
1819
+ const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
1820
+ BTC_VARIABLE_FEE_MASK;
1821
+ // Check Flashnet minimum amounts early to provide clear error messages
1822
+ const minAmounts = await this.getEnabledMinAmountsMap();
1823
+ // Check BTC minimum (output from swap)
1824
+ const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
1825
+ if (btcMinAmount && totalBtcNeeded < btcMinAmount) {
1826
+ throw new Error(`Invoice amount too small. Flashnet minimum BTC output is ${btcMinAmount} sats, ` +
1827
+ `but invoice + lightning fee totals only ${totalBtcNeeded} sats. ` +
1828
+ `Please use an invoice of at least ${btcMinAmount} sats.`);
1829
+ }
1830
+ // Find the best pool to swap token -> BTC
1831
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1832
+ // Check token minimum (input to swap)
1833
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1834
+ const tokenMinAmount = minAmounts.get(tokenHex);
1835
+ if (tokenMinAmount &&
1836
+ BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1837
+ throw new Error(`Token amount too small. Flashnet minimum input is ${tokenMinAmount} units, ` +
1838
+ `but calculated amount is only ${poolQuote.tokenAmountRequired} units. ` +
1839
+ `Please use a larger invoice amount.`);
1840
+ }
1841
+ // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1842
+ const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1843
+ return {
1844
+ poolId: poolQuote.poolId,
1845
+ tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1846
+ tokenAmountRequired: poolQuote.tokenAmountRequired,
1847
+ btcAmountRequired: totalBtcNeeded.toString(),
1848
+ invoiceAmountSats: invoiceAmountSats,
1849
+ estimatedAmmFee: poolQuote.estimatedAmmFee,
1850
+ estimatedLightningFee: lightningFeeEstimate,
1851
+ btcVariableFeeAdjustment,
1852
+ executionPrice: poolQuote.executionPrice,
1853
+ priceImpactPct: poolQuote.priceImpactPct,
1854
+ tokenIsAssetA: poolQuote.tokenIsAssetA,
1855
+ poolReserves: poolQuote.poolReserves,
1856
+ warningMessage: poolQuote.warningMessage,
1857
+ };
1858
+ }
1859
+ /**
1860
+ * Pay a Lightning invoice using a token.
1861
+ * This swaps the token to BTC on Flashnet and uses the BTC to pay the invoice.
1862
+ *
1863
+ * @param options - Payment options including invoice and token address
1864
+ * @returns Payment result with transaction details
1865
+ */
1866
+ async payLightningWithToken(options) {
1867
+ await this.ensureInitialized();
1868
+ const { invoice, tokenAddress, maxSlippageBps = 100, // 1% default
1869
+ maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 10000, } = options;
1870
+ try {
1871
+ // Step 1: Get a quote for the payment
1872
+ const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1873
+ maxSlippageBps,
1874
+ integratorFeeRateBps,
1875
+ });
1876
+ // Step 2: Check balance
1877
+ await this.checkBalance({
1878
+ balancesToCheck: [
1879
+ {
1880
+ assetAddress: tokenAddress,
1881
+ amount: quote.tokenAmountRequired,
1882
+ },
1883
+ ],
1884
+ errorPrefix: "Insufficient token balance for Lightning payment: ",
1885
+ });
1886
+ // Step 3: Get pool details
1887
+ const pool = await this.getPool(quote.poolId);
1888
+ // Step 4: Determine swap direction and execute
1889
+ const assetInAddress = quote.tokenIsAssetA
1890
+ ? pool.assetAAddress
1891
+ : pool.assetBAddress;
1892
+ const assetOutAddress = quote.tokenIsAssetA
1893
+ ? pool.assetBAddress
1894
+ : pool.assetAAddress;
1895
+ // Calculate min amount out with slippage protection
1896
+ const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
1897
+ // Execute the swap
1898
+ const swapResponse = await this.executeSwap({
1899
+ poolId: quote.poolId,
1900
+ assetInAddress,
1901
+ assetOutAddress,
1902
+ amountIn: quote.tokenAmountRequired,
1903
+ maxSlippageBps,
1904
+ minAmountOut: minBtcOut,
1905
+ integratorFeeRateBps,
1906
+ integratorPublicKey,
1907
+ });
1908
+ if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
1909
+ return {
1910
+ success: false,
1911
+ poolId: quote.poolId,
1912
+ tokenAmountSpent: quote.tokenAmountRequired,
1913
+ btcAmountReceived: "0",
1914
+ swapTransferId: swapResponse.outboundTransferId || "",
1915
+ ammFeePaid: quote.estimatedAmmFee,
1916
+ error: swapResponse.error || "Swap was not accepted",
1917
+ };
1918
+ }
1919
+ // Step 5: Wait for the transfer to complete
1920
+ const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1921
+ if (!transferComplete) {
1922
+ return {
1923
+ success: false,
1924
+ poolId: quote.poolId,
1925
+ tokenAmountSpent: quote.tokenAmountRequired,
1926
+ btcAmountReceived: swapResponse.amountOut || "0",
1927
+ swapTransferId: swapResponse.outboundTransferId,
1928
+ ammFeePaid: quote.estimatedAmmFee,
1929
+ error: "Transfer did not complete within timeout",
1930
+ };
1931
+ }
1932
+ // Step 6: Calculate Lightning fee limit
1933
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1934
+ const effectiveMaxLightningFee = maxLightningFeeSats ??
1935
+ Math.max(5, Math.ceil(invoiceAmountSats * 0.0017)); // 17 bps or 5 sats minimum
1936
+ // Step 7: Pay the Lightning invoice
1937
+ const lightningPayment = await this._wallet.payLightningInvoice({
1938
+ invoice,
1939
+ maxFeeSats: effectiveMaxLightningFee,
1940
+ preferSpark,
1941
+ });
1942
+ return {
1943
+ success: true,
1944
+ poolId: quote.poolId,
1945
+ tokenAmountSpent: quote.tokenAmountRequired,
1946
+ btcAmountReceived: swapResponse.amountOut || quote.btcAmountRequired,
1947
+ swapTransferId: swapResponse.outboundTransferId,
1948
+ lightningPaymentId: lightningPayment.id,
1949
+ ammFeePaid: quote.estimatedAmmFee,
1950
+ lightningFeePaid: effectiveMaxLightningFee,
1951
+ };
1952
+ }
1953
+ catch (error) {
1954
+ const errorMessage = error instanceof Error ? error.message : String(error);
1955
+ return {
1956
+ success: false,
1957
+ poolId: "",
1958
+ tokenAmountSpent: "0",
1959
+ btcAmountReceived: "0",
1960
+ swapTransferId: "",
1961
+ ammFeePaid: "0",
1962
+ error: errorMessage,
1963
+ };
1964
+ }
1965
+ }
1966
+ /**
1967
+ * Find the best pool for swapping a token to BTC
1968
+ * @private
1969
+ */
1970
+ async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
1971
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
1972
+ const btcHex = index$1.BTC_ASSET_PUBKEY;
1973
+ // Find all pools that have this token paired with BTC
1974
+ const poolsWithTokenAsA = await this.listPools({
1975
+ assetAAddress: tokenHex,
1976
+ assetBAddress: btcHex,
1977
+ });
1978
+ const poolsWithTokenAsB = await this.listPools({
1979
+ assetAAddress: btcHex,
1980
+ assetBAddress: tokenHex,
1981
+ });
1982
+ const allPools = [
1983
+ ...poolsWithTokenAsA.pools.map((p) => ({ ...p, tokenIsAssetA: true })),
1984
+ ...poolsWithTokenAsB.pools.map((p) => ({ ...p, tokenIsAssetA: false })),
1985
+ ];
1986
+ if (allPools.length === 0) {
1987
+ throw new Error(`No liquidity pool found for token ${tokenAddress} paired with BTC`);
1988
+ }
1989
+ // Find the best pool (lowest token cost for the required BTC)
1990
+ let bestPool = null;
1991
+ let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
1992
+ let bestSimulation = null;
1993
+ for (const pool of allPools) {
1994
+ try {
1995
+ // Get pool details for reserves
1996
+ const poolDetails = await this.getPool(pool.lpPublicKey);
1997
+ // Calculate the token amount needed using AMM math
1998
+ const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
1999
+ const tokenAmount = BigInt(calculation.amountIn);
2000
+ // Check if this is better than our current best
2001
+ if (tokenAmount < bestTokenAmount) {
2002
+ // Verify with simulation
2003
+ const simulation = await this.simulateSwap({
2004
+ poolId: pool.lpPublicKey,
2005
+ assetInAddress: pool.tokenIsAssetA
2006
+ ? poolDetails.assetAAddress
2007
+ : poolDetails.assetBAddress,
2008
+ assetOutAddress: pool.tokenIsAssetA
2009
+ ? poolDetails.assetBAddress
2010
+ : poolDetails.assetAAddress,
2011
+ amountIn: calculation.amountIn,
2012
+ integratorBps: integratorFeeRateBps,
2013
+ });
2014
+ // Verify the output is sufficient
2015
+ if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
2016
+ bestPool = pool;
2017
+ bestTokenAmount = tokenAmount;
2018
+ bestSimulation = {
2019
+ amountIn: calculation.amountIn,
2020
+ fee: calculation.totalFee,
2021
+ executionPrice: simulation.executionPrice || "0",
2022
+ priceImpactPct: simulation.priceImpactPct || "0",
2023
+ warningMessage: simulation.warningMessage,
2024
+ };
2025
+ }
2026
+ }
2027
+ }
2028
+ catch { }
2029
+ }
2030
+ if (!bestPool || !bestSimulation) {
2031
+ throw new Error(`No pool has sufficient liquidity for ${btcAmountNeeded} sats`);
2032
+ }
2033
+ const poolDetails = await this.getPool(bestPool.lpPublicKey);
2034
+ return {
2035
+ poolId: bestPool.lpPublicKey,
2036
+ tokenAmountRequired: bestSimulation.amountIn,
2037
+ estimatedAmmFee: bestSimulation.fee,
2038
+ executionPrice: bestSimulation.executionPrice,
2039
+ priceImpactPct: bestSimulation.priceImpactPct,
2040
+ tokenIsAssetA: bestPool.tokenIsAssetA,
2041
+ poolReserves: {
2042
+ assetAReserve: poolDetails.assetAReserve,
2043
+ assetBReserve: poolDetails.assetBReserve,
2044
+ },
2045
+ warningMessage: bestSimulation.warningMessage,
2046
+ };
2047
+ }
2048
+ /**
2049
+ * Calculate the token amount needed to get a specific BTC output.
2050
+ * Implements the AMM fee-inclusive model.
2051
+ * @private
2052
+ */
2053
+ calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2054
+ const amountOut = BigInt(btcAmountOut);
2055
+ const resA = BigInt(reserveA);
2056
+ const resB = BigInt(reserveB);
2057
+ const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2058
+ const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2059
+ // Token is the input asset
2060
+ // BTC is the output asset
2061
+ if (tokenIsAssetA) {
2062
+ // Token is asset A, BTC is asset B
2063
+ // A → B swap: we want BTC out (asset B)
2064
+ // reserve_in = reserveA (token), reserve_out = reserveB (BTC)
2065
+ // Constant product formula for amount_in given amount_out:
2066
+ // amount_in_effective = (reserve_in * amount_out) / (reserve_out - amount_out)
2067
+ const reserveIn = resA;
2068
+ const reserveOut = resB;
2069
+ if (amountOut >= reserveOut) {
2070
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2071
+ }
2072
+ // Calculate effective amount in (before fees)
2073
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2074
+ // A→B swap: LP fee deducted from input A, integrator fee from output B
2075
+ // amount_in = amount_in_effective * (1 + lp_fee_rate)
2076
+ // Then integrator fee is deducted from output, so we need slightly more input
2077
+ const lpFeeRate = Number(lpFeeBps) / 10000;
2078
+ const integratorFeeRate = Number(integratorFeeBps || 0) / 10000;
2079
+ // Account for LP fee on input
2080
+ const amountInWithLpFee = BigInt(Math.ceil(Number(amountInEffective) * (1 + lpFeeRate)));
2081
+ // Account for integrator fee on output (need more input to get same output after fee)
2082
+ const amountIn = integratorFeeRate > 0
2083
+ ? BigInt(Math.ceil(Number(amountInWithLpFee) * (1 + integratorFeeRate)))
2084
+ : amountInWithLpFee;
2085
+ const totalFee = amountIn - amountInEffective;
2086
+ return {
2087
+ amountIn: amountIn.toString(),
2088
+ totalFee: totalFee.toString(),
2089
+ };
2090
+ }
2091
+ else {
2092
+ // Token is asset B, BTC is asset A
2093
+ // B → A swap: we want BTC out (asset A)
2094
+ // reserve_in = reserveB (token), reserve_out = reserveA (BTC)
2095
+ const reserveIn = resB;
2096
+ const reserveOut = resA;
2097
+ if (amountOut >= reserveOut) {
2098
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2099
+ }
2100
+ // Calculate effective amount in (before fees)
2101
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2102
+ // B→A swap: ALL fees (LP + integrator) deducted from input B
2103
+ // amount_in = amount_in_effective * (1 + total_fee_rate)
2104
+ const amountIn = BigInt(Math.ceil(Number(amountInEffective) * (1 + feeRate)));
2105
+ // Fee calculation: fee = amount_in * fee_rate / (1 + fee_rate)
2106
+ const totalFee = BigInt(Math.ceil((Number(amountIn) * feeRate) / (1 + feeRate)));
2107
+ return {
2108
+ amountIn: amountIn.toString(),
2109
+ totalFee: totalFee.toString(),
2110
+ };
2111
+ }
2112
+ }
2113
+ /**
2114
+ * Calculate minimum amount out with slippage protection
2115
+ * @private
2116
+ */
2117
+ calculateMinAmountOut(expectedAmount, slippageBps) {
2118
+ const amount = BigInt(expectedAmount);
2119
+ const slippageFactor = BigInt(10000 - slippageBps);
2120
+ const minAmount = (amount * slippageFactor) / 10000n;
2121
+ return minAmount.toString();
2122
+ }
2123
+ /**
2124
+ * Wait for a transfer to be claimed using wallet events.
2125
+ * This is more efficient than polling as it uses the wallet's event stream.
2126
+ * @private
2127
+ */
2128
+ async waitForTransferCompletion(transferId, timeoutMs) {
2129
+ return new Promise((resolve) => {
2130
+ const timeout = setTimeout(() => {
2131
+ // Remove listener on timeout
2132
+ try {
2133
+ this._wallet.removeListener?.("transfer:claimed", handler);
2134
+ }
2135
+ catch {
2136
+ // Ignore if removeListener doesn't exist
2137
+ }
2138
+ resolve(false);
2139
+ }, timeoutMs);
2140
+ const handler = (claimedTransferId, _balance) => {
2141
+ if (claimedTransferId === transferId) {
2142
+ clearTimeout(timeout);
2143
+ try {
2144
+ this._wallet.removeListener?.("transfer:claimed", handler);
2145
+ }
2146
+ catch {
2147
+ // Ignore if removeListener doesn't exist
2148
+ }
2149
+ resolve(true);
2150
+ }
2151
+ };
2152
+ // Subscribe to transfer claimed events
2153
+ // The wallet's RPC stream will automatically claim incoming transfers
2154
+ try {
2155
+ this._wallet.on?.("transfer:claimed", handler);
2156
+ }
2157
+ catch {
2158
+ // If event subscription fails, fall back to polling
2159
+ clearTimeout(timeout);
2160
+ this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
2161
+ }
2162
+ });
2163
+ }
2164
+ /**
2165
+ * Fallback polling method for transfer completion
2166
+ * @private
2167
+ */
2168
+ async pollForTransferCompletion(transferId, timeoutMs) {
2169
+ const startTime = Date.now();
2170
+ const pollIntervalMs = 500;
2171
+ while (Date.now() - startTime < timeoutMs) {
2172
+ try {
2173
+ const transfer = await this._wallet.getTransfer(transferId);
2174
+ if (transfer) {
2175
+ if (transfer.status === "TRANSFER_STATUS_COMPLETED") {
2176
+ return true;
2177
+ }
2178
+ }
2179
+ }
2180
+ catch {
2181
+ // Ignore errors and continue polling
2182
+ }
2183
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2184
+ }
2185
+ return false;
2186
+ }
2187
+ /**
2188
+ * Get Lightning fee estimate for an invoice
2189
+ * @private
2190
+ */
2191
+ async getLightningFeeEstimate(invoice) {
2192
+ try {
2193
+ const feeEstimate = await this._wallet.getLightningSendFeeEstimate({
2194
+ encodedInvoice: invoice,
2195
+ });
2196
+ // The fee estimate might be returned as a number or an object
2197
+ if (typeof feeEstimate === "number") {
2198
+ return feeEstimate;
2199
+ }
2200
+ if (feeEstimate?.fee || feeEstimate?.feeEstimate) {
2201
+ return Number(feeEstimate.fee || feeEstimate.feeEstimate);
2202
+ }
2203
+ // Fallback to invoice amount-based estimate
2204
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2205
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017)); // 17 bps or 5 sats minimum
2206
+ }
2207
+ catch {
2208
+ // Fallback to invoice amount-based estimate
2209
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2210
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017));
2211
+ }
2212
+ }
2213
+ /**
2214
+ * Decode the amount from a Lightning invoice (in sats)
2215
+ * @private
2216
+ */
2217
+ async decodeInvoiceAmount(invoice) {
2218
+ // Extract amount from BOLT11 invoice
2219
+ // Format: ln[network][amount][multiplier]...
2220
+ // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2221
+ const lowerInvoice = invoice.toLowerCase();
2222
+ // Find where the amount starts (after network prefix)
2223
+ let amountStart = 0;
2224
+ if (lowerInvoice.startsWith("lnbc")) {
2225
+ amountStart = 4;
2226
+ }
2227
+ else if (lowerInvoice.startsWith("lntb")) {
2228
+ amountStart = 4;
2229
+ }
2230
+ else if (lowerInvoice.startsWith("lnbcrt")) {
2231
+ amountStart = 6;
2232
+ }
2233
+ else if (lowerInvoice.startsWith("lntbs")) {
2234
+ amountStart = 5;
2235
+ }
2236
+ else {
2237
+ // Unknown format, try to find amount
2238
+ const match = lowerInvoice.match(/^ln[a-z]+/);
2239
+ if (match) {
2240
+ amountStart = match[0].length;
2241
+ }
2242
+ }
2243
+ // Extract amount and multiplier
2244
+ const afterPrefix = lowerInvoice.substring(amountStart);
2245
+ const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2246
+ if (!amountMatch || !amountMatch[1]) {
2247
+ return 0; // Zero-amount invoice
2248
+ }
2249
+ const amount = parseInt(amountMatch[1], 10);
2250
+ const multiplier = amountMatch[2] ?? "";
2251
+ // Convert to satoshis (1 BTC = 100,000,000 sats)
2252
+ // Invoice amounts are in BTC by default
2253
+ let btcAmount;
2254
+ switch (multiplier) {
2255
+ case "m": // milli-BTC (0.001 BTC)
2256
+ btcAmount = amount * 0.001;
2257
+ break;
2258
+ case "u": // micro-BTC (0.000001 BTC)
2259
+ btcAmount = amount * 0.000001;
2260
+ break;
2261
+ case "n": // nano-BTC (0.000000001 BTC)
2262
+ btcAmount = amount * 0.000000001;
2263
+ break;
2264
+ case "p": // pico-BTC (0.000000000001 BTC)
2265
+ btcAmount = amount * 0.000000000001;
2266
+ break;
2267
+ default: // BTC
2268
+ btcAmount = amount;
2269
+ break;
2270
+ }
2271
+ // Convert BTC to sats
2272
+ return Math.round(btcAmount * 100000000);
2273
+ }
1478
2274
  /**
1479
2275
  * Clean up wallet connections
1480
2276
  */