@flashnet/sdk 0.3.40 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/src/api/client.d.ts.map +1 -1
- package/dist/cjs/src/api/client.js +14 -15
- package/dist/cjs/src/api/client.js.map +1 -1
- package/dist/cjs/src/api/typed-endpoints.d.ts +9 -1
- package/dist/cjs/src/api/typed-endpoints.d.ts.map +1 -1
- package/dist/cjs/src/api/typed-endpoints.js +6 -2
- package/dist/cjs/src/api/typed-endpoints.js.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.d.ts +95 -1
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +422 -109
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/cjs/src/types/errors.d.ts +264 -0
- package/dist/cjs/src/types/errors.d.ts.map +1 -0
- package/dist/cjs/src/types/errors.js +758 -0
- package/dist/cjs/src/types/errors.js.map +1 -0
- package/dist/cjs/src/types/index.d.ts +1 -1
- package/dist/cjs/src/types/index.d.ts.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/src/api/client.d.ts.map +1 -1
- package/dist/esm/src/api/client.js +14 -15
- package/dist/esm/src/api/client.js.map +1 -1
- package/dist/esm/src/api/typed-endpoints.d.ts +9 -1
- package/dist/esm/src/api/typed-endpoints.d.ts.map +1 -1
- package/dist/esm/src/api/typed-endpoints.js +6 -2
- package/dist/esm/src/api/typed-endpoints.js.map +1 -1
- package/dist/esm/src/client/FlashnetClient.d.ts +95 -1
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +422 -109
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/src/types/errors.d.ts +264 -0
- package/dist/esm/src/types/errors.d.ts.map +1 -0
- package/dist/esm/src/types/errors.js +749 -0
- package/dist/esm/src/types/errors.js.map +1 -0
- package/dist/esm/src/types/index.d.ts +1 -1
- package/dist/esm/src/types/index.d.ts.map +1 -1
- 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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
|
658
|
+
const hasRefund = !!response.refundedAmount;
|
|
659
|
+
const refundInfo = hasRefund
|
|
651
660
|
? ` Refunded ${response.refundedAmount} of ${response.refundedAssetAddress} via transfer ${response.refundTransferId}`
|
|
652
661
|
: "";
|
|
653
|
-
|
|
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
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
hop.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
hop.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|