@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
|
@@ -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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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
|
|
660
|
+
const hasRefund = !!response.refundedAmount;
|
|
661
|
+
const refundInfo = hasRefund
|
|
653
662
|
? ` Refunded ${response.refundedAmount} of ${response.refundedAssetAddress} via transfer ${response.refundTransferId}`
|
|
654
663
|
: "";
|
|
655
|
-
|
|
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
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
hop.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
hop.
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|