@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
@@ -8,6 +8,7 @@ import { getHexFromUint8Array } from '../utils/hex.js';
8
8
  import { generateConstantProductPoolInitializationIntentMessage, generatePoolInitializationIntentMessage, generatePoolConfirmInitialDepositIntentMessage, generatePoolSwapIntentMessage, generateRouteSwapIntentMessage, generateAddLiquidityIntentMessage, generateRemoveLiquidityIntentMessage, generateRegisterHostIntentMessage, generateWithdrawHostFeesIntentMessage, generateWithdrawIntegratorFeesIntentMessage, generateCreateEscrowIntentMessage, generateFundEscrowIntentMessage, generateClaimEscrowIntentMessage, generateClawbackIntentMessage } from '../utils/intents.js';
9
9
  import { getSparkNetworkFromAddress, encodeSparkAddressNew } from '../utils/spark-address.js';
10
10
  import { encodeSparkHumanReadableTokenIdentifier, decodeSparkHumanReadableTokenIdentifier } from '../utils/tokenAddress.js';
11
+ import { FlashnetError } from '../types/errors.js';
11
12
 
12
13
  /**
13
14
  * FlashnetClient - A comprehensive client for interacting with Flashnet AMM
@@ -525,10 +526,14 @@ class FlashnetClient {
525
526
  assetAddress: params.assetAAddress,
526
527
  amount: assetAInitialReserve,
527
528
  });
528
- const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
529
- if (!confirmResponse.confirmed) {
530
- throw new Error(`Failed to confirm initial deposit: ${confirmResponse.message}`);
531
- }
529
+ // Execute confirm with auto-clawback on failure
530
+ await this.executeWithAutoClawback(async () => {
531
+ const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
532
+ if (!confirmResponse.confirmed) {
533
+ throw new Error(`Failed to confirm initial deposit: ${confirmResponse.message}`);
534
+ }
535
+ return confirmResponse;
536
+ }, [assetATransferId], createResponse.poolId);
532
537
  return createResponse;
533
538
  }
534
539
  /**
@@ -575,6 +580,9 @@ class FlashnetClient {
575
580
  }
576
581
  /**
577
582
  * Execute a swap
583
+ *
584
+ * If the swap fails with a clawbackable error, the SDK will automatically
585
+ * attempt to recover the transferred funds via clawback.
578
586
  */
579
587
  async executeSwap(params) {
580
588
  await this.ensureInitialized();
@@ -596,11 +604,11 @@ class FlashnetClient {
596
604
  assetAddress: params.assetInAddress,
597
605
  amount: params.amountIn,
598
606
  }, "Insufficient balance for swap: ");
599
- const response = await this.executeSwapIntent({
607
+ // Execute with auto-clawback on failure
608
+ return this.executeWithAutoClawback(() => this.executeSwapIntent({
600
609
  ...params,
601
610
  transferId,
602
- });
603
- return response;
611
+ }), [transferId], params.poolId);
604
612
  }
605
613
  async executeSwapIntent(params) {
606
614
  await this.ensureInitialized();
@@ -647,10 +655,27 @@ class FlashnetClient {
647
655
  // Check if the swap was accepted
648
656
  if (!response.accepted) {
649
657
  const errorMessage = response.error || "Swap rejected by the AMM";
650
- const refundInfo = response.refundedAmount
658
+ const hasRefund = !!response.refundedAmount;
659
+ const refundInfo = hasRefund
651
660
  ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetAddress} via transfer ${response.refundTransferId}`
652
661
  : "";
653
- throw new Error(`${errorMessage}.${refundInfo}`);
662
+ // If refund was provided, funds are safe - use auto_refund recovery
663
+ // If no refund, funds may need clawback
664
+ throw new FlashnetError(`${errorMessage}.${refundInfo}`, {
665
+ response: {
666
+ errorCode: hasRefund ? "FSAG-4202" : "UNKNOWN", // Slippage if refunded
667
+ errorCategory: hasRefund ? "Business" : "System",
668
+ message: `${errorMessage}.${refundInfo}`,
669
+ requestId: "",
670
+ timestamp: new Date().toISOString(),
671
+ service: "amm-gateway",
672
+ severity: "Error",
673
+ },
674
+ httpStatus: 400,
675
+ // Don't include transferIds if refunded - no clawback needed
676
+ transferIds: hasRefund ? [] : [params.transferId],
677
+ lpIdentityPublicKey: params.poolId,
678
+ });
654
679
  }
655
680
  return response;
656
681
  }
@@ -667,6 +692,9 @@ class FlashnetClient {
667
692
  }
668
693
  /**
669
694
  * Execute a route swap (multi-hop swap)
695
+ *
696
+ * If the route swap fails with a clawbackable error, the SDK will automatically
697
+ * attempt to recover the transferred funds via clawback.
670
698
  */
671
699
  async executeRouteSwap(params) {
672
700
  await this.ensureInitialized();
@@ -703,68 +731,85 @@ class FlashnetClient {
703
731
  assetAddress: params.initialAssetAddress,
704
732
  amount: params.inputAmount,
705
733
  }, "Insufficient balance for route swap: ");
706
- // Prepare hops for validation
707
- const hops = params.hops.map((hop) => ({
708
- lpIdentityPublicKey: hop.poolId,
709
- inputAssetAddress: this.toHexTokenIdentifier(hop.assetInAddress),
710
- outputAssetAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
711
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
712
- hop.hopIntegratorFeeRateBps !== null
713
- ? hop.hopIntegratorFeeRateBps.toString()
714
- : "0",
715
- }));
716
- // Convert hops and ensure integrator fee is always present
717
- const requestHops = params.hops.map((hop) => ({
718
- poolId: hop.poolId,
719
- assetInAddress: this.toHexTokenIdentifier(hop.assetInAddress),
720
- assetOutAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
721
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
722
- hop.hopIntegratorFeeRateBps !== null
723
- ? hop.hopIntegratorFeeRateBps.toString()
724
- : "0",
725
- }));
726
- // Generate route swap intent
727
- const nonce = generateNonce();
728
- const intentMessage = generateRouteSwapIntentMessage({
729
- userPublicKey: this.publicKey,
730
- hops: hops.map((hop) => ({
731
- lpIdentityPublicKey: hop.lpIdentityPublicKey,
732
- inputAssetAddress: hop.inputAssetAddress,
733
- outputAssetAddress: hop.outputAssetAddress,
734
- hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps,
735
- })),
736
- initialSparkTransferId: initialTransferId,
737
- inputAmount: params.inputAmount.toString(),
738
- maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
739
- minAmountOut: params.minAmountOut,
740
- nonce,
741
- defaultIntegratorFeeRateBps: params.integratorFeeRateBps?.toString(),
742
- });
743
- // Sign intent
744
- const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
745
- const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
746
- const request = {
747
- userPublicKey: this.publicKey,
748
- hops: requestHops,
749
- initialSparkTransferId: initialTransferId,
750
- inputAmount: params.inputAmount.toString(),
751
- maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
752
- minAmountOut: params.minAmountOut,
753
- nonce,
754
- signature: getHexFromUint8Array(signature),
755
- integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
756
- integratorPublicKey: params.integratorPublicKey || "",
757
- };
758
- const response = await this.typedApi.executeRouteSwap(request);
759
- // Check if the route swap was accepted
760
- if (!response.accepted) {
761
- const errorMessage = response.error || "Route swap rejected by the AMM";
762
- const refundInfo = response.refundedAmount
763
- ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetPublicKey} via transfer ${response.refundTransferId}`
764
- : "";
765
- throw new Error(`${errorMessage}.${refundInfo}`);
766
- }
767
- return response;
734
+ // Execute with auto-clawback on failure
735
+ return this.executeWithAutoClawback(async () => {
736
+ // Prepare hops for validation
737
+ const hops = params.hops.map((hop) => ({
738
+ lpIdentityPublicKey: hop.poolId,
739
+ inputAssetAddress: this.toHexTokenIdentifier(hop.assetInAddress),
740
+ outputAssetAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
741
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
742
+ hop.hopIntegratorFeeRateBps !== null
743
+ ? hop.hopIntegratorFeeRateBps.toString()
744
+ : "0",
745
+ }));
746
+ // Convert hops and ensure integrator fee is always present
747
+ const requestHops = params.hops.map((hop) => ({
748
+ poolId: hop.poolId,
749
+ assetInAddress: this.toHexTokenIdentifier(hop.assetInAddress),
750
+ assetOutAddress: this.toHexTokenIdentifier(hop.assetOutAddress),
751
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps !== undefined &&
752
+ hop.hopIntegratorFeeRateBps !== null
753
+ ? hop.hopIntegratorFeeRateBps.toString()
754
+ : "0",
755
+ }));
756
+ // Generate route swap intent
757
+ const nonce = generateNonce();
758
+ const intentMessage = generateRouteSwapIntentMessage({
759
+ userPublicKey: this.publicKey,
760
+ hops: hops.map((hop) => ({
761
+ lpIdentityPublicKey: hop.lpIdentityPublicKey,
762
+ inputAssetAddress: hop.inputAssetAddress,
763
+ outputAssetAddress: hop.outputAssetAddress,
764
+ hopIntegratorFeeRateBps: hop.hopIntegratorFeeRateBps,
765
+ })),
766
+ initialSparkTransferId: initialTransferId,
767
+ inputAmount: params.inputAmount.toString(),
768
+ maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
769
+ minAmountOut: params.minAmountOut,
770
+ nonce,
771
+ defaultIntegratorFeeRateBps: params.integratorFeeRateBps?.toString(),
772
+ });
773
+ // Sign intent
774
+ const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
775
+ const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
776
+ const request = {
777
+ userPublicKey: this.publicKey,
778
+ hops: requestHops,
779
+ initialSparkTransferId: initialTransferId,
780
+ inputAmount: params.inputAmount.toString(),
781
+ maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
782
+ minAmountOut: params.minAmountOut,
783
+ nonce,
784
+ signature: getHexFromUint8Array(signature),
785
+ integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
786
+ integratorPublicKey: params.integratorPublicKey || "",
787
+ };
788
+ const response = await this.typedApi.executeRouteSwap(request);
789
+ // Check if the route swap was accepted
790
+ if (!response.accepted) {
791
+ const errorMessage = response.error || "Route swap rejected by the AMM";
792
+ const hasRefund = !!response.refundedAmount;
793
+ const refundInfo = hasRefund
794
+ ? ` Refunded ${response.refundedAmount} of ${response.refundedAssetPublicKey} via transfer ${response.refundTransferId}`
795
+ : "";
796
+ throw new FlashnetError(`${errorMessage}.${refundInfo}`, {
797
+ response: {
798
+ errorCode: hasRefund ? "FSAG-4202" : "UNKNOWN",
799
+ errorCategory: hasRefund ? "Business" : "System",
800
+ message: `${errorMessage}.${refundInfo}`,
801
+ requestId: "",
802
+ timestamp: new Date().toISOString(),
803
+ service: "amm-gateway",
804
+ severity: "Error",
805
+ },
806
+ httpStatus: 400,
807
+ transferIds: hasRefund ? [] : [initialTransferId],
808
+ lpIdentityPublicKey: firstPoolId,
809
+ });
810
+ }
811
+ return response;
812
+ }, [initialTransferId], firstPoolId);
768
813
  }
769
814
  // ===== Liquidity Operations =====
770
815
  /**
@@ -777,6 +822,9 @@ class FlashnetClient {
777
822
  }
778
823
  /**
779
824
  * Add liquidity to a pool
825
+ *
826
+ * If adding liquidity fails with a clawbackable error, the SDK will automatically
827
+ * attempt to recover the transferred funds via clawback.
780
828
  */
781
829
  async addLiquidity(params) {
782
830
  await this.ensureInitialized();
@@ -806,44 +854,61 @@ class FlashnetClient {
806
854
  amount: params.assetBAmount,
807
855
  },
808
856
  ], "Insufficient balance for adding liquidity: ");
809
- // Generate add liquidity intent
810
- const nonce = generateNonce();
811
- const intentMessage = generateAddLiquidityIntentMessage({
812
- userPublicKey: this.publicKey,
813
- lpIdentityPublicKey: params.poolId,
814
- assetASparkTransferId: assetATransferId,
815
- assetBSparkTransferId: assetBTransferId,
816
- assetAAmount: params.assetAAmount.toString(),
817
- assetBAmount: params.assetBAmount.toString(),
818
- assetAMinAmountIn: params.assetAMinAmountIn.toString(),
819
- assetBMinAmountIn: params.assetBMinAmountIn.toString(),
820
- nonce,
821
- });
822
- // Sign intent
823
- const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
824
- const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
825
- const request = {
826
- userPublicKey: this.publicKey,
827
- poolId: params.poolId,
828
- assetASparkTransferId: assetATransferId,
829
- assetBSparkTransferId: assetBTransferId,
830
- assetAAmountToAdd: params.assetAAmount.toString(),
831
- assetBAmountToAdd: params.assetBAmount.toString(),
832
- assetAMinAmountIn: params.assetAMinAmountIn.toString(),
833
- assetBMinAmountIn: params.assetBMinAmountIn.toString(),
834
- nonce,
835
- signature: getHexFromUint8Array(signature),
836
- };
837
- const response = await this.typedApi.addLiquidity(request);
838
- // Check if the liquidity addition was accepted
839
- if (!response.accepted) {
840
- const errorMessage = response.error || "Add liquidity rejected by the AMM";
841
- const refundInfo = response.refund
842
- ? ` Refunds: Asset A: ${response.refund.assetAAmount || 0}, Asset B: ${response.refund.assetBAmount || 0}`
843
- : "";
844
- throw new Error(`${errorMessage}.${refundInfo}`);
845
- }
846
- return response;
857
+ // Execute with auto-clawback on failure
858
+ return this.executeWithAutoClawback(async () => {
859
+ // Generate add liquidity intent
860
+ const nonce = generateNonce();
861
+ const intentMessage = generateAddLiquidityIntentMessage({
862
+ userPublicKey: this.publicKey,
863
+ lpIdentityPublicKey: params.poolId,
864
+ assetASparkTransferId: assetATransferId,
865
+ assetBSparkTransferId: assetBTransferId,
866
+ assetAAmount: params.assetAAmount.toString(),
867
+ assetBAmount: params.assetBAmount.toString(),
868
+ assetAMinAmountIn: params.assetAMinAmountIn.toString(),
869
+ assetBMinAmountIn: params.assetBMinAmountIn.toString(),
870
+ nonce,
871
+ });
872
+ // Sign intent
873
+ const messageHash = new Uint8Array(await crypto.subtle.digest("SHA-256", intentMessage));
874
+ const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
875
+ const request = {
876
+ userPublicKey: this.publicKey,
877
+ poolId: params.poolId,
878
+ assetASparkTransferId: assetATransferId,
879
+ assetBSparkTransferId: assetBTransferId,
880
+ assetAAmountToAdd: params.assetAAmount.toString(),
881
+ assetBAmountToAdd: params.assetBAmount.toString(),
882
+ assetAMinAmountIn: params.assetAMinAmountIn.toString(),
883
+ assetBMinAmountIn: params.assetBMinAmountIn.toString(),
884
+ nonce,
885
+ signature: getHexFromUint8Array(signature),
886
+ };
887
+ const response = await this.typedApi.addLiquidity(request);
888
+ // Check if the liquidity addition was accepted
889
+ if (!response.accepted) {
890
+ const errorMessage = response.error || "Add liquidity rejected by the AMM";
891
+ const hasRefund = !!(response.refund?.assetAAmount || response.refund?.assetBAmount);
892
+ const refundInfo = response.refund
893
+ ? ` Refunds: Asset A: ${response.refund.assetAAmount || 0}, Asset B: ${response.refund.assetBAmount || 0}`
894
+ : "";
895
+ throw new FlashnetError(`${errorMessage}.${refundInfo}`, {
896
+ response: {
897
+ errorCode: hasRefund ? "FSAG-4203" : "UNKNOWN", // Phase error if refunded
898
+ errorCategory: hasRefund ? "Business" : "System",
899
+ message: `${errorMessage}.${refundInfo}`,
900
+ requestId: "",
901
+ timestamp: new Date().toISOString(),
902
+ service: "amm-gateway",
903
+ severity: "Error",
904
+ },
905
+ httpStatus: 400,
906
+ transferIds: hasRefund ? [] : [assetATransferId, assetBTransferId],
907
+ lpIdentityPublicKey: params.poolId,
908
+ });
909
+ }
910
+ return response;
911
+ }, [assetATransferId, assetBTransferId], params.poolId);
847
912
  }
848
913
  /**
849
914
  * Simulate removing liquidity
@@ -1276,6 +1341,254 @@ class FlashnetClient {
1276
1341
  await this.ensurePingOk();
1277
1342
  return this.typedApi.listClawbackableTransfers(query);
1278
1343
  }
1344
+ /**
1345
+ * Attempt to clawback multiple transfers
1346
+ *
1347
+ * @param transferIds - Array of transfer IDs to clawback
1348
+ * @param lpIdentityPublicKey - The LP wallet public key
1349
+ * @returns Array of results for each clawback attempt
1350
+ */
1351
+ async clawbackMultiple(transferIds, lpIdentityPublicKey) {
1352
+ const results = [];
1353
+ for (const transferId of transferIds) {
1354
+ try {
1355
+ const response = await this.clawback({
1356
+ sparkTransferId: transferId,
1357
+ lpIdentityPublicKey,
1358
+ });
1359
+ results.push({
1360
+ transferId,
1361
+ success: true,
1362
+ response,
1363
+ });
1364
+ }
1365
+ catch (err) {
1366
+ results.push({
1367
+ transferId,
1368
+ success: false,
1369
+ error: err instanceof Error ? err.message : String(err),
1370
+ });
1371
+ }
1372
+ }
1373
+ return results;
1374
+ }
1375
+ /**
1376
+ * Internal helper to execute an operation with automatic clawback on failure
1377
+ *
1378
+ * @param operation - The async operation to execute
1379
+ * @param transferIds - Transfer IDs that were sent and may need clawback
1380
+ * @param lpIdentityPublicKey - The LP wallet public key for clawback
1381
+ * @returns The result of the operation
1382
+ * @throws FlashnetError with typed clawbackSummary attached
1383
+ */
1384
+ async executeWithAutoClawback(operation, transferIds, lpIdentityPublicKey) {
1385
+ try {
1386
+ return await operation();
1387
+ }
1388
+ catch (error) {
1389
+ // Convert to FlashnetError if not already
1390
+ const flashnetError = FlashnetError.fromUnknown(error, {
1391
+ transferIds,
1392
+ lpIdentityPublicKey,
1393
+ });
1394
+ // Check if we should attempt clawback
1395
+ if (flashnetError.shouldClawback() && transferIds.length > 0) {
1396
+ // Attempt to clawback all transfers
1397
+ const clawbackResults = await this.clawbackMultiple(transferIds, lpIdentityPublicKey);
1398
+ // Separate successful and failed clawbacks
1399
+ const successfulClawbacks = clawbackResults.filter((r) => r.success);
1400
+ const failedClawbacks = clawbackResults.filter((r) => !r.success);
1401
+ // Build typed clawback summary
1402
+ const clawbackSummary = {
1403
+ attempted: true,
1404
+ totalTransfers: transferIds.length,
1405
+ successCount: successfulClawbacks.length,
1406
+ failureCount: failedClawbacks.length,
1407
+ results: clawbackResults,
1408
+ recoveredTransferIds: successfulClawbacks.map((r) => r.transferId),
1409
+ unrecoveredTransferIds: failedClawbacks.map((r) => r.transferId),
1410
+ };
1411
+ // Create enhanced error message
1412
+ let enhancedMessage = flashnetError.message;
1413
+ if (successfulClawbacks.length > 0) {
1414
+ enhancedMessage += ` [Auto-clawback: ${successfulClawbacks.length}/${transferIds.length} transfers recovered]`;
1415
+ }
1416
+ if (failedClawbacks.length > 0) {
1417
+ const failedIds = failedClawbacks.map((r) => r.transferId).join(", ");
1418
+ enhancedMessage += ` [Clawback failed for: ${failedIds}]`;
1419
+ }
1420
+ // Determine remediation based on clawback results
1421
+ let remediation;
1422
+ if (clawbackSummary.failureCount === 0) {
1423
+ remediation =
1424
+ "Your funds have been automatically recovered. No action needed.";
1425
+ }
1426
+ else if (clawbackSummary.successCount > 0) {
1427
+ remediation = `${clawbackSummary.successCount} transfer(s) recovered. Manual clawback needed for remaining transfers.`;
1428
+ }
1429
+ else {
1430
+ remediation =
1431
+ flashnetError.remediation ??
1432
+ "Automatic recovery failed. Please initiate a manual clawback.";
1433
+ }
1434
+ // Throw new error with typed clawback summary
1435
+ const errorWithClawback = new FlashnetError(enhancedMessage, {
1436
+ response: {
1437
+ errorCode: flashnetError.errorCode,
1438
+ errorCategory: flashnetError.category,
1439
+ message: enhancedMessage,
1440
+ details: flashnetError.details,
1441
+ requestId: flashnetError.requestId,
1442
+ timestamp: flashnetError.timestamp,
1443
+ service: flashnetError.service,
1444
+ severity: flashnetError.severity,
1445
+ remediation,
1446
+ },
1447
+ httpStatus: flashnetError.httpStatus,
1448
+ transferIds: clawbackSummary.unrecoveredTransferIds,
1449
+ lpIdentityPublicKey,
1450
+ clawbackSummary,
1451
+ });
1452
+ throw errorWithClawback;
1453
+ }
1454
+ // Not a clawbackable error, just re-throw
1455
+ throw flashnetError;
1456
+ }
1457
+ }
1458
+ // ===== Clawback Monitor =====
1459
+ /**
1460
+ * Start a background job that periodically polls for clawbackable transfers
1461
+ * and automatically claws them back.
1462
+ *
1463
+ * @param options - Monitor configuration options
1464
+ * @returns ClawbackMonitorHandle to control the monitor
1465
+ *
1466
+ * @example
1467
+ * ```typescript
1468
+ * const monitor = client.startClawbackMonitor({
1469
+ * intervalMs: 60000, // Poll every 60 seconds
1470
+ * onClawbackSuccess: (result) => console.log('Recovered:', result.transferId),
1471
+ * onClawbackError: (transferId, error) => console.error('Failed:', transferId, error),
1472
+ * });
1473
+ *
1474
+ * // Later, to stop:
1475
+ * monitor.stop();
1476
+ * ```
1477
+ */
1478
+ startClawbackMonitor(options = {}) {
1479
+ const { intervalMs = 60000, // Default: 1 minute
1480
+ batchSize = 2, // Default: 2 clawbacks per batch (rate limit safe)
1481
+ batchDelayMs = 500, // Default: 500ms between batches
1482
+ maxTransfersPerPoll = 100, // Default: max 100 transfers per poll
1483
+ onClawbackSuccess, onClawbackError, onPollComplete, onPollError, } = options;
1484
+ let isRunning = true;
1485
+ let timeoutId = null;
1486
+ let currentPollPromise = null;
1487
+ const poll = async () => {
1488
+ const result = {
1489
+ transfersFound: 0,
1490
+ clawbacksAttempted: 0,
1491
+ clawbacksSucceeded: 0,
1492
+ clawbacksFailed: 0,
1493
+ results: [],
1494
+ };
1495
+ try {
1496
+ // Fetch clawbackable transfers
1497
+ const response = await this.listClawbackableTransfers({
1498
+ limit: maxTransfersPerPoll,
1499
+ });
1500
+ result.transfersFound = response.transfers.length;
1501
+ if (response.transfers.length === 0) {
1502
+ return result;
1503
+ }
1504
+ // Process in batches to respect rate limits
1505
+ for (let i = 0; i < response.transfers.length; i += batchSize) {
1506
+ if (!isRunning) {
1507
+ break;
1508
+ }
1509
+ const batch = response.transfers.slice(i, i + batchSize);
1510
+ // Process batch concurrently
1511
+ const batchResults = await Promise.all(batch.map(async (transfer) => {
1512
+ result.clawbacksAttempted++;
1513
+ try {
1514
+ const clawbackResponse = await this.clawback({
1515
+ sparkTransferId: transfer.id,
1516
+ lpIdentityPublicKey: transfer.lpIdentityPublicKey,
1517
+ });
1518
+ const attemptResult = {
1519
+ transferId: transfer.id,
1520
+ success: true,
1521
+ response: clawbackResponse,
1522
+ };
1523
+ result.clawbacksSucceeded++;
1524
+ onClawbackSuccess?.(attemptResult);
1525
+ return attemptResult;
1526
+ }
1527
+ catch (err) {
1528
+ const attemptResult = {
1529
+ transferId: transfer.id,
1530
+ success: false,
1531
+ error: err instanceof Error ? err.message : String(err),
1532
+ };
1533
+ result.clawbacksFailed++;
1534
+ onClawbackError?.(transfer.id, err);
1535
+ return attemptResult;
1536
+ }
1537
+ }));
1538
+ result.results.push(...batchResults);
1539
+ // Wait between batches if there are more to process
1540
+ if (i + batchSize < response.transfers.length && isRunning) {
1541
+ await new Promise((resolve) => setTimeout(resolve, batchDelayMs));
1542
+ }
1543
+ }
1544
+ }
1545
+ catch (err) {
1546
+ onPollError?.(err);
1547
+ }
1548
+ return result;
1549
+ };
1550
+ const scheduleNextPoll = () => {
1551
+ if (!isRunning) {
1552
+ return;
1553
+ }
1554
+ timeoutId = setTimeout(async () => {
1555
+ if (!isRunning) {
1556
+ return;
1557
+ }
1558
+ currentPollPromise = (async () => {
1559
+ const result = await poll();
1560
+ onPollComplete?.(result);
1561
+ scheduleNextPoll();
1562
+ })();
1563
+ }, intervalMs);
1564
+ };
1565
+ // Start first poll immediately
1566
+ currentPollPromise = (async () => {
1567
+ const result = await poll();
1568
+ onPollComplete?.(result);
1569
+ scheduleNextPoll();
1570
+ })();
1571
+ return {
1572
+ isRunning: () => isRunning,
1573
+ stop: async () => {
1574
+ isRunning = false;
1575
+ if (timeoutId) {
1576
+ clearTimeout(timeoutId);
1577
+ timeoutId = null;
1578
+ }
1579
+ // Wait for current poll to complete
1580
+ if (currentPollPromise) {
1581
+ await currentPollPromise.catch(() => { });
1582
+ }
1583
+ },
1584
+ pollNow: async () => {
1585
+ if (!isRunning) {
1586
+ throw new Error("Monitor is stopped");
1587
+ }
1588
+ return poll();
1589
+ },
1590
+ };
1591
+ }
1279
1592
  // ===== Token Address Operations =====
1280
1593
  /**
1281
1594
  * Encode a token identifier into a human-readable token address using the client's Spark network
@@ -1473,6 +1786,489 @@ class FlashnetClient {
1473
1786
  throw new Error(errorMessage);
1474
1787
  }
1475
1788
  }
1789
+ // ===== Lightning Payment with Token =====
1790
+ /**
1791
+ * Get a quote for paying a Lightning invoice with a token.
1792
+ * This calculates the optimal pool and token amount needed.
1793
+ *
1794
+ * @param invoice - BOLT11-encoded Lightning invoice
1795
+ * @param tokenAddress - Token identifier to use for payment
1796
+ * @param options - Optional configuration (slippage, integrator fees, etc.)
1797
+ * @returns Quote with pricing details
1798
+ * @throws Error if invoice amount or token amount is below Flashnet minimums
1799
+ */
1800
+ async getPayLightningWithTokenQuote(invoice, tokenAddress, options) {
1801
+ await this.ensureInitialized();
1802
+ // Decode the invoice to get the amount
1803
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1804
+ if (!invoiceAmountSats || invoiceAmountSats <= 0) {
1805
+ throw new Error("Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.");
1806
+ }
1807
+ // Get Lightning fee estimate
1808
+ const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1809
+ // Total BTC needed = invoice amount + lightning fee
1810
+ const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1811
+ // Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
1812
+ // The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
1813
+ // that after masking we still have the required amount.
1814
+ // Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
1815
+ const BTC_VARIABLE_FEE_BITS = 6n;
1816
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
1817
+ const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
1818
+ BTC_VARIABLE_FEE_MASK;
1819
+ // Check Flashnet minimum amounts early to provide clear error messages
1820
+ const minAmounts = await this.getEnabledMinAmountsMap();
1821
+ // Check BTC minimum (output from swap)
1822
+ const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
1823
+ if (btcMinAmount && totalBtcNeeded < btcMinAmount) {
1824
+ throw new Error(`Invoice amount too small. Flashnet minimum BTC output is ${btcMinAmount} sats, ` +
1825
+ `but invoice + lightning fee totals only ${totalBtcNeeded} sats. ` +
1826
+ `Please use an invoice of at least ${btcMinAmount} sats.`);
1827
+ }
1828
+ // Find the best pool to swap token -> BTC
1829
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1830
+ // Check token minimum (input to swap)
1831
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1832
+ const tokenMinAmount = minAmounts.get(tokenHex);
1833
+ if (tokenMinAmount &&
1834
+ BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1835
+ throw new Error(`Token amount too small. Flashnet minimum input is ${tokenMinAmount} units, ` +
1836
+ `but calculated amount is only ${poolQuote.tokenAmountRequired} units. ` +
1837
+ `Please use a larger invoice amount.`);
1838
+ }
1839
+ // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1840
+ const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1841
+ return {
1842
+ poolId: poolQuote.poolId,
1843
+ tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1844
+ tokenAmountRequired: poolQuote.tokenAmountRequired,
1845
+ btcAmountRequired: totalBtcNeeded.toString(),
1846
+ invoiceAmountSats: invoiceAmountSats,
1847
+ estimatedAmmFee: poolQuote.estimatedAmmFee,
1848
+ estimatedLightningFee: lightningFeeEstimate,
1849
+ btcVariableFeeAdjustment,
1850
+ executionPrice: poolQuote.executionPrice,
1851
+ priceImpactPct: poolQuote.priceImpactPct,
1852
+ tokenIsAssetA: poolQuote.tokenIsAssetA,
1853
+ poolReserves: poolQuote.poolReserves,
1854
+ warningMessage: poolQuote.warningMessage,
1855
+ };
1856
+ }
1857
+ /**
1858
+ * Pay a Lightning invoice using a token.
1859
+ * This swaps the token to BTC on Flashnet and uses the BTC to pay the invoice.
1860
+ *
1861
+ * @param options - Payment options including invoice and token address
1862
+ * @returns Payment result with transaction details
1863
+ */
1864
+ async payLightningWithToken(options) {
1865
+ await this.ensureInitialized();
1866
+ const { invoice, tokenAddress, maxSlippageBps = 100, // 1% default
1867
+ maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 10000, } = options;
1868
+ try {
1869
+ // Step 1: Get a quote for the payment
1870
+ const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1871
+ maxSlippageBps,
1872
+ integratorFeeRateBps,
1873
+ });
1874
+ // Step 2: Check balance
1875
+ await this.checkBalance({
1876
+ balancesToCheck: [
1877
+ {
1878
+ assetAddress: tokenAddress,
1879
+ amount: quote.tokenAmountRequired,
1880
+ },
1881
+ ],
1882
+ errorPrefix: "Insufficient token balance for Lightning payment: ",
1883
+ });
1884
+ // Step 3: Get pool details
1885
+ const pool = await this.getPool(quote.poolId);
1886
+ // Step 4: Determine swap direction and execute
1887
+ const assetInAddress = quote.tokenIsAssetA
1888
+ ? pool.assetAAddress
1889
+ : pool.assetBAddress;
1890
+ const assetOutAddress = quote.tokenIsAssetA
1891
+ ? pool.assetBAddress
1892
+ : pool.assetAAddress;
1893
+ // Calculate min amount out with slippage protection
1894
+ const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
1895
+ // Execute the swap
1896
+ const swapResponse = await this.executeSwap({
1897
+ poolId: quote.poolId,
1898
+ assetInAddress,
1899
+ assetOutAddress,
1900
+ amountIn: quote.tokenAmountRequired,
1901
+ maxSlippageBps,
1902
+ minAmountOut: minBtcOut,
1903
+ integratorFeeRateBps,
1904
+ integratorPublicKey,
1905
+ });
1906
+ if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
1907
+ return {
1908
+ success: false,
1909
+ poolId: quote.poolId,
1910
+ tokenAmountSpent: quote.tokenAmountRequired,
1911
+ btcAmountReceived: "0",
1912
+ swapTransferId: swapResponse.outboundTransferId || "",
1913
+ ammFeePaid: quote.estimatedAmmFee,
1914
+ error: swapResponse.error || "Swap was not accepted",
1915
+ };
1916
+ }
1917
+ // Step 5: Wait for the transfer to complete
1918
+ const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1919
+ if (!transferComplete) {
1920
+ return {
1921
+ success: false,
1922
+ poolId: quote.poolId,
1923
+ tokenAmountSpent: quote.tokenAmountRequired,
1924
+ btcAmountReceived: swapResponse.amountOut || "0",
1925
+ swapTransferId: swapResponse.outboundTransferId,
1926
+ ammFeePaid: quote.estimatedAmmFee,
1927
+ error: "Transfer did not complete within timeout",
1928
+ };
1929
+ }
1930
+ // Step 6: Calculate Lightning fee limit
1931
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1932
+ const effectiveMaxLightningFee = maxLightningFeeSats ??
1933
+ Math.max(5, Math.ceil(invoiceAmountSats * 0.0017)); // 17 bps or 5 sats minimum
1934
+ // Step 7: Pay the Lightning invoice
1935
+ const lightningPayment = await this._wallet.payLightningInvoice({
1936
+ invoice,
1937
+ maxFeeSats: effectiveMaxLightningFee,
1938
+ preferSpark,
1939
+ });
1940
+ return {
1941
+ success: true,
1942
+ poolId: quote.poolId,
1943
+ tokenAmountSpent: quote.tokenAmountRequired,
1944
+ btcAmountReceived: swapResponse.amountOut || quote.btcAmountRequired,
1945
+ swapTransferId: swapResponse.outboundTransferId,
1946
+ lightningPaymentId: lightningPayment.id,
1947
+ ammFeePaid: quote.estimatedAmmFee,
1948
+ lightningFeePaid: effectiveMaxLightningFee,
1949
+ };
1950
+ }
1951
+ catch (error) {
1952
+ const errorMessage = error instanceof Error ? error.message : String(error);
1953
+ return {
1954
+ success: false,
1955
+ poolId: "",
1956
+ tokenAmountSpent: "0",
1957
+ btcAmountReceived: "0",
1958
+ swapTransferId: "",
1959
+ ammFeePaid: "0",
1960
+ error: errorMessage,
1961
+ };
1962
+ }
1963
+ }
1964
+ /**
1965
+ * Find the best pool for swapping a token to BTC
1966
+ * @private
1967
+ */
1968
+ async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
1969
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
1970
+ const btcHex = BTC_ASSET_PUBKEY;
1971
+ // Find all pools that have this token paired with BTC
1972
+ const poolsWithTokenAsA = await this.listPools({
1973
+ assetAAddress: tokenHex,
1974
+ assetBAddress: btcHex,
1975
+ });
1976
+ const poolsWithTokenAsB = await this.listPools({
1977
+ assetAAddress: btcHex,
1978
+ assetBAddress: tokenHex,
1979
+ });
1980
+ const allPools = [
1981
+ ...poolsWithTokenAsA.pools.map((p) => ({ ...p, tokenIsAssetA: true })),
1982
+ ...poolsWithTokenAsB.pools.map((p) => ({ ...p, tokenIsAssetA: false })),
1983
+ ];
1984
+ if (allPools.length === 0) {
1985
+ throw new Error(`No liquidity pool found for token ${tokenAddress} paired with BTC`);
1986
+ }
1987
+ // Find the best pool (lowest token cost for the required BTC)
1988
+ let bestPool = null;
1989
+ let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
1990
+ let bestSimulation = null;
1991
+ for (const pool of allPools) {
1992
+ try {
1993
+ // Get pool details for reserves
1994
+ const poolDetails = await this.getPool(pool.lpPublicKey);
1995
+ // Calculate the token amount needed using AMM math
1996
+ const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
1997
+ const tokenAmount = BigInt(calculation.amountIn);
1998
+ // Check if this is better than our current best
1999
+ if (tokenAmount < bestTokenAmount) {
2000
+ // Verify with simulation
2001
+ const simulation = await this.simulateSwap({
2002
+ poolId: pool.lpPublicKey,
2003
+ assetInAddress: pool.tokenIsAssetA
2004
+ ? poolDetails.assetAAddress
2005
+ : poolDetails.assetBAddress,
2006
+ assetOutAddress: pool.tokenIsAssetA
2007
+ ? poolDetails.assetBAddress
2008
+ : poolDetails.assetAAddress,
2009
+ amountIn: calculation.amountIn,
2010
+ integratorBps: integratorFeeRateBps,
2011
+ });
2012
+ // Verify the output is sufficient
2013
+ if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
2014
+ bestPool = pool;
2015
+ bestTokenAmount = tokenAmount;
2016
+ bestSimulation = {
2017
+ amountIn: calculation.amountIn,
2018
+ fee: calculation.totalFee,
2019
+ executionPrice: simulation.executionPrice || "0",
2020
+ priceImpactPct: simulation.priceImpactPct || "0",
2021
+ warningMessage: simulation.warningMessage,
2022
+ };
2023
+ }
2024
+ }
2025
+ }
2026
+ catch { }
2027
+ }
2028
+ if (!bestPool || !bestSimulation) {
2029
+ throw new Error(`No pool has sufficient liquidity for ${btcAmountNeeded} sats`);
2030
+ }
2031
+ const poolDetails = await this.getPool(bestPool.lpPublicKey);
2032
+ return {
2033
+ poolId: bestPool.lpPublicKey,
2034
+ tokenAmountRequired: bestSimulation.amountIn,
2035
+ estimatedAmmFee: bestSimulation.fee,
2036
+ executionPrice: bestSimulation.executionPrice,
2037
+ priceImpactPct: bestSimulation.priceImpactPct,
2038
+ tokenIsAssetA: bestPool.tokenIsAssetA,
2039
+ poolReserves: {
2040
+ assetAReserve: poolDetails.assetAReserve,
2041
+ assetBReserve: poolDetails.assetBReserve,
2042
+ },
2043
+ warningMessage: bestSimulation.warningMessage,
2044
+ };
2045
+ }
2046
+ /**
2047
+ * Calculate the token amount needed to get a specific BTC output.
2048
+ * Implements the AMM fee-inclusive model.
2049
+ * @private
2050
+ */
2051
+ calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2052
+ const amountOut = BigInt(btcAmountOut);
2053
+ const resA = BigInt(reserveA);
2054
+ const resB = BigInt(reserveB);
2055
+ const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2056
+ const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2057
+ // Token is the input asset
2058
+ // BTC is the output asset
2059
+ if (tokenIsAssetA) {
2060
+ // Token is asset A, BTC is asset B
2061
+ // A → B swap: we want BTC out (asset B)
2062
+ // reserve_in = reserveA (token), reserve_out = reserveB (BTC)
2063
+ // Constant product formula for amount_in given amount_out:
2064
+ // amount_in_effective = (reserve_in * amount_out) / (reserve_out - amount_out)
2065
+ const reserveIn = resA;
2066
+ const reserveOut = resB;
2067
+ if (amountOut >= reserveOut) {
2068
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2069
+ }
2070
+ // Calculate effective amount in (before fees)
2071
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2072
+ // A→B swap: LP fee deducted from input A, integrator fee from output B
2073
+ // amount_in = amount_in_effective * (1 + lp_fee_rate)
2074
+ // Then integrator fee is deducted from output, so we need slightly more input
2075
+ const lpFeeRate = Number(lpFeeBps) / 10000;
2076
+ const integratorFeeRate = Number(integratorFeeBps || 0) / 10000;
2077
+ // Account for LP fee on input
2078
+ const amountInWithLpFee = BigInt(Math.ceil(Number(amountInEffective) * (1 + lpFeeRate)));
2079
+ // Account for integrator fee on output (need more input to get same output after fee)
2080
+ const amountIn = integratorFeeRate > 0
2081
+ ? BigInt(Math.ceil(Number(amountInWithLpFee) * (1 + integratorFeeRate)))
2082
+ : amountInWithLpFee;
2083
+ const totalFee = amountIn - amountInEffective;
2084
+ return {
2085
+ amountIn: amountIn.toString(),
2086
+ totalFee: totalFee.toString(),
2087
+ };
2088
+ }
2089
+ else {
2090
+ // Token is asset B, BTC is asset A
2091
+ // B → A swap: we want BTC out (asset A)
2092
+ // reserve_in = reserveB (token), reserve_out = reserveA (BTC)
2093
+ const reserveIn = resB;
2094
+ const reserveOut = resA;
2095
+ if (amountOut >= reserveOut) {
2096
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2097
+ }
2098
+ // Calculate effective amount in (before fees)
2099
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2100
+ // B→A swap: ALL fees (LP + integrator) deducted from input B
2101
+ // amount_in = amount_in_effective * (1 + total_fee_rate)
2102
+ const amountIn = BigInt(Math.ceil(Number(amountInEffective) * (1 + feeRate)));
2103
+ // Fee calculation: fee = amount_in * fee_rate / (1 + fee_rate)
2104
+ const totalFee = BigInt(Math.ceil((Number(amountIn) * feeRate) / (1 + feeRate)));
2105
+ return {
2106
+ amountIn: amountIn.toString(),
2107
+ totalFee: totalFee.toString(),
2108
+ };
2109
+ }
2110
+ }
2111
+ /**
2112
+ * Calculate minimum amount out with slippage protection
2113
+ * @private
2114
+ */
2115
+ calculateMinAmountOut(expectedAmount, slippageBps) {
2116
+ const amount = BigInt(expectedAmount);
2117
+ const slippageFactor = BigInt(10000 - slippageBps);
2118
+ const minAmount = (amount * slippageFactor) / 10000n;
2119
+ return minAmount.toString();
2120
+ }
2121
+ /**
2122
+ * Wait for a transfer to be claimed using wallet events.
2123
+ * This is more efficient than polling as it uses the wallet's event stream.
2124
+ * @private
2125
+ */
2126
+ async waitForTransferCompletion(transferId, timeoutMs) {
2127
+ return new Promise((resolve) => {
2128
+ const timeout = setTimeout(() => {
2129
+ // Remove listener on timeout
2130
+ try {
2131
+ this._wallet.removeListener?.("transfer:claimed", handler);
2132
+ }
2133
+ catch {
2134
+ // Ignore if removeListener doesn't exist
2135
+ }
2136
+ resolve(false);
2137
+ }, timeoutMs);
2138
+ const handler = (claimedTransferId, _balance) => {
2139
+ if (claimedTransferId === transferId) {
2140
+ clearTimeout(timeout);
2141
+ try {
2142
+ this._wallet.removeListener?.("transfer:claimed", handler);
2143
+ }
2144
+ catch {
2145
+ // Ignore if removeListener doesn't exist
2146
+ }
2147
+ resolve(true);
2148
+ }
2149
+ };
2150
+ // Subscribe to transfer claimed events
2151
+ // The wallet's RPC stream will automatically claim incoming transfers
2152
+ try {
2153
+ this._wallet.on?.("transfer:claimed", handler);
2154
+ }
2155
+ catch {
2156
+ // If event subscription fails, fall back to polling
2157
+ clearTimeout(timeout);
2158
+ this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
2159
+ }
2160
+ });
2161
+ }
2162
+ /**
2163
+ * Fallback polling method for transfer completion
2164
+ * @private
2165
+ */
2166
+ async pollForTransferCompletion(transferId, timeoutMs) {
2167
+ const startTime = Date.now();
2168
+ const pollIntervalMs = 500;
2169
+ while (Date.now() - startTime < timeoutMs) {
2170
+ try {
2171
+ const transfer = await this._wallet.getTransfer(transferId);
2172
+ if (transfer) {
2173
+ if (transfer.status === "TRANSFER_STATUS_COMPLETED") {
2174
+ return true;
2175
+ }
2176
+ }
2177
+ }
2178
+ catch {
2179
+ // Ignore errors and continue polling
2180
+ }
2181
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2182
+ }
2183
+ return false;
2184
+ }
2185
+ /**
2186
+ * Get Lightning fee estimate for an invoice
2187
+ * @private
2188
+ */
2189
+ async getLightningFeeEstimate(invoice) {
2190
+ try {
2191
+ const feeEstimate = await this._wallet.getLightningSendFeeEstimate({
2192
+ encodedInvoice: invoice,
2193
+ });
2194
+ // The fee estimate might be returned as a number or an object
2195
+ if (typeof feeEstimate === "number") {
2196
+ return feeEstimate;
2197
+ }
2198
+ if (feeEstimate?.fee || feeEstimate?.feeEstimate) {
2199
+ return Number(feeEstimate.fee || feeEstimate.feeEstimate);
2200
+ }
2201
+ // Fallback to invoice amount-based estimate
2202
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2203
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017)); // 17 bps or 5 sats minimum
2204
+ }
2205
+ catch {
2206
+ // Fallback to invoice amount-based estimate
2207
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2208
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017));
2209
+ }
2210
+ }
2211
+ /**
2212
+ * Decode the amount from a Lightning invoice (in sats)
2213
+ * @private
2214
+ */
2215
+ async decodeInvoiceAmount(invoice) {
2216
+ // Extract amount from BOLT11 invoice
2217
+ // Format: ln[network][amount][multiplier]...
2218
+ // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2219
+ const lowerInvoice = invoice.toLowerCase();
2220
+ // Find where the amount starts (after network prefix)
2221
+ let amountStart = 0;
2222
+ if (lowerInvoice.startsWith("lnbc")) {
2223
+ amountStart = 4;
2224
+ }
2225
+ else if (lowerInvoice.startsWith("lntb")) {
2226
+ amountStart = 4;
2227
+ }
2228
+ else if (lowerInvoice.startsWith("lnbcrt")) {
2229
+ amountStart = 6;
2230
+ }
2231
+ else if (lowerInvoice.startsWith("lntbs")) {
2232
+ amountStart = 5;
2233
+ }
2234
+ else {
2235
+ // Unknown format, try to find amount
2236
+ const match = lowerInvoice.match(/^ln[a-z]+/);
2237
+ if (match) {
2238
+ amountStart = match[0].length;
2239
+ }
2240
+ }
2241
+ // Extract amount and multiplier
2242
+ const afterPrefix = lowerInvoice.substring(amountStart);
2243
+ const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2244
+ if (!amountMatch || !amountMatch[1]) {
2245
+ return 0; // Zero-amount invoice
2246
+ }
2247
+ const amount = parseInt(amountMatch[1], 10);
2248
+ const multiplier = amountMatch[2] ?? "";
2249
+ // Convert to satoshis (1 BTC = 100,000,000 sats)
2250
+ // Invoice amounts are in BTC by default
2251
+ let btcAmount;
2252
+ switch (multiplier) {
2253
+ case "m": // milli-BTC (0.001 BTC)
2254
+ btcAmount = amount * 0.001;
2255
+ break;
2256
+ case "u": // micro-BTC (0.000001 BTC)
2257
+ btcAmount = amount * 0.000001;
2258
+ break;
2259
+ case "n": // nano-BTC (0.000000001 BTC)
2260
+ btcAmount = amount * 0.000000001;
2261
+ break;
2262
+ case "p": // pico-BTC (0.000000000001 BTC)
2263
+ btcAmount = amount * 0.000000000001;
2264
+ break;
2265
+ default: // BTC
2266
+ btcAmount = amount;
2267
+ break;
2268
+ }
2269
+ // Convert BTC to sats
2270
+ return Math.round(btcAmount * 100000000);
2271
+ }
1476
2272
  /**
1477
2273
  * Clean up wallet connections
1478
2274
  */