@silentswap/react 0.0.41

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 (86) hide show
  1. package/README.md +48 -0
  2. package/dist/contexts/AssetsContext.d.ts +24 -0
  3. package/dist/contexts/AssetsContext.js +83 -0
  4. package/dist/contexts/BalancesContext.d.ts +28 -0
  5. package/dist/contexts/BalancesContext.js +533 -0
  6. package/dist/contexts/OrdersContext.d.ts +53 -0
  7. package/dist/contexts/OrdersContext.js +240 -0
  8. package/dist/contexts/PricesContext.d.ts +12 -0
  9. package/dist/contexts/PricesContext.js +109 -0
  10. package/dist/contexts/SilentSwapContext.d.ts +58 -0
  11. package/dist/contexts/SilentSwapContext.js +205 -0
  12. package/dist/hooks/silent/orderTrackingWebSocketManager.d.ts +48 -0
  13. package/dist/hooks/silent/orderTrackingWebSocketManager.js +284 -0
  14. package/dist/hooks/silent/solana-transaction.d.ts +60 -0
  15. package/dist/hooks/silent/solana-transaction.js +236 -0
  16. package/dist/hooks/silent/useAuth.d.ts +90 -0
  17. package/dist/hooks/silent/useAuth.js +269 -0
  18. package/dist/hooks/silent/useBridgeExecution.d.ts +39 -0
  19. package/dist/hooks/silent/useBridgeExecution.js +877 -0
  20. package/dist/hooks/silent/useOrderSigning.d.ts +34 -0
  21. package/dist/hooks/silent/useOrderSigning.js +133 -0
  22. package/dist/hooks/silent/useOrderTracking.d.ts +174 -0
  23. package/dist/hooks/silent/useOrderTracking.js +524 -0
  24. package/dist/hooks/silent/useQuoteCalculation.d.ts +50 -0
  25. package/dist/hooks/silent/useQuoteCalculation.js +331 -0
  26. package/dist/hooks/silent/useQuoteFetching.d.ts +18 -0
  27. package/dist/hooks/silent/useQuoteFetching.js +54 -0
  28. package/dist/hooks/silent/useRefund.d.ts +26 -0
  29. package/dist/hooks/silent/useRefund.js +134 -0
  30. package/dist/hooks/silent/useSilentClient.d.ts +16 -0
  31. package/dist/hooks/silent/useSilentClient.js +32 -0
  32. package/dist/hooks/silent/useSilentOrders.d.ts +174 -0
  33. package/dist/hooks/silent/useSilentOrders.js +73 -0
  34. package/dist/hooks/silent/useSilentQuote.d.ts +88 -0
  35. package/dist/hooks/silent/useSilentQuote.js +381 -0
  36. package/dist/hooks/silent/useWallet.d.ts +76 -0
  37. package/dist/hooks/silent/useWallet.js +203 -0
  38. package/dist/hooks/useAssetPrice.d.ts +8 -0
  39. package/dist/hooks/useAssetPrice.js +47 -0
  40. package/dist/hooks/useContacts.d.ts +52 -0
  41. package/dist/hooks/useContacts.js +259 -0
  42. package/dist/hooks/useEgressEstimates.d.ts +32 -0
  43. package/dist/hooks/useEgressEstimates.js +230 -0
  44. package/dist/hooks/useHiddenSwapFees.d.ts +22 -0
  45. package/dist/hooks/useHiddenSwapFees.js +81 -0
  46. package/dist/hooks/useOrderEstimates.d.ts +37 -0
  47. package/dist/hooks/useOrderEstimates.js +393 -0
  48. package/dist/hooks/useOutputAssetInfo.d.ts +12 -0
  49. package/dist/hooks/useOutputAssetInfo.js +38 -0
  50. package/dist/hooks/usePrices.d.ts +60 -0
  51. package/dist/hooks/usePrices.js +188 -0
  52. package/dist/hooks/useQuote.d.ts +73 -0
  53. package/dist/hooks/useQuote.js +507 -0
  54. package/dist/hooks/useResetSwapForm.d.ts +16 -0
  55. package/dist/hooks/useResetSwapForm.js +68 -0
  56. package/dist/hooks/useSlippageUsd.d.ts +11 -0
  57. package/dist/hooks/useSlippageUsd.js +19 -0
  58. package/dist/hooks/useSolanaAdapter.d.ts +15 -0
  59. package/dist/hooks/useSolanaAdapter.js +55 -0
  60. package/dist/hooks/useStatus.d.ts +25 -0
  61. package/dist/hooks/useStatus.js +60 -0
  62. package/dist/hooks/useSwap.d.ts +67 -0
  63. package/dist/hooks/useSwap.js +285 -0
  64. package/dist/hooks/useTransaction.d.ts +119 -0
  65. package/dist/hooks/useTransaction.js +353 -0
  66. package/dist/hooks/useTransactionAddress.d.ts +11 -0
  67. package/dist/hooks/useTransactionAddress.js +26 -0
  68. package/dist/hooks/useUsdValue.d.ts +7 -0
  69. package/dist/hooks/useUsdValue.js +19 -0
  70. package/dist/index.d.ts +54 -0
  71. package/dist/index.js +41 -0
  72. package/dist/stories/SilentSwapOverview.stories.d.ts +10 -0
  73. package/dist/stories/SilentSwapOverview.stories.js +364 -0
  74. package/dist/stories/useAuth.stories.d.ts +6 -0
  75. package/dist/stories/useAuth.stories.js +55 -0
  76. package/dist/stories/useSilentClient.stories.d.ts +9 -0
  77. package/dist/stories/useSilentClient.stories.js +39 -0
  78. package/dist/stories/useSilentOrders.stories.d.ts +1 -0
  79. package/dist/stories/useSilentOrders.stories.js +1 -0
  80. package/dist/stories/useSilentQuote.stories.d.ts +6 -0
  81. package/dist/stories/useSilentQuote.stories.js +267 -0
  82. package/dist/stories/useTransaction.stories.d.ts +6 -0
  83. package/dist/stories/useTransaction.stories.js +121 -0
  84. package/dist/utils/formatters.d.ts +33 -0
  85. package/dist/utils/formatters.js +82 -0
  86. package/package.json +67 -0
@@ -0,0 +1,81 @@
1
+ import { useMemo } from 'react';
2
+ import { BigNumber } from 'bignumber.js';
3
+ /**
4
+ * Hook to calculate fees for hidden swap
5
+ * Extracts actual fees from bridge quotes when available, otherwise uses estimated rates
6
+ */
7
+ export function useHiddenSwapFees({ depositAmountUsdc, bridgeProvider, serviceFeeRate = 0.01, overheadUsd = 0, usdcPrice = 1, bridgeFeeRate = 0.001, ingressQuote, egressQuotes, }) {
8
+ const fees = useMemo(() => {
9
+ if (depositAmountUsdc <= 0) {
10
+ return {
11
+ serviceFeeUsd: 0,
12
+ bridgeFeeIngressUsd: 0,
13
+ bridgeFeeEgressUsd: 0,
14
+ totalFeesUsd: 0,
15
+ };
16
+ }
17
+ // Extract ingress bridge fee from quote
18
+ let bridgeFeeIngress = 0;
19
+ if (ingressQuote) {
20
+ if ('fees' in ingressQuote) {
21
+ // Relay.link quote
22
+ bridgeFeeIngress = BigNumber(ingressQuote.fees?.relayer?.amountUsd || 0).toNumber();
23
+ }
24
+ else if ('estimation' in ingressQuote) {
25
+ // DeBridge quote
26
+ const debridgeQuote = ingressQuote;
27
+ bridgeFeeIngress = BigNumber(debridgeQuote.estimation.costDetails
28
+ ?.map((detail) => Number(detail.payload?.feeApproximateUsdValue || 0))
29
+ ?.reduce((sum, fee) => sum + fee, 0) || 0).toNumber();
30
+ }
31
+ }
32
+ else if (bridgeProvider && bridgeProvider !== 'none') {
33
+ bridgeFeeIngress = depositAmountUsdc * bridgeFeeRate;
34
+ }
35
+ // Extract egress bridge fees from quotes
36
+ let bridgeFeeEgress = 0;
37
+ if (egressQuotes && egressQuotes.length > 0) {
38
+ const egressFees = egressQuotes.map((quote) => {
39
+ if (!quote)
40
+ return BigNumber(0);
41
+ if ('fees' in quote) {
42
+ const relayerFee = BigNumber(quote.fees?.relayer?.amountUsd || 0);
43
+ const gasFee = BigNumber(quote.fees?.gas?.amountUsd || 0);
44
+ return relayerFee.plus(gasFee);
45
+ }
46
+ else if ('estimation' in quote) {
47
+ const debridgeQuote = quote;
48
+ const feeFromCostDetails = BigNumber(debridgeQuote.estimation.costDetails
49
+ ?.map((detail) => Number(detail.payload?.feeApproximateUsdValue || 0))
50
+ ?.reduce((sum, fee) => sum + fee, 0) || 0);
51
+ // Match Svelte behavior: add the same value twice
52
+ return feeFromCostDetails.plus(feeFromCostDetails);
53
+ }
54
+ return BigNumber(0);
55
+ });
56
+ bridgeFeeEgress = egressFees.reduce((sum, fee) => sum.plus(fee), BigNumber(0)).toNumber();
57
+ }
58
+ else if (bridgeProvider && bridgeProvider !== 'none') {
59
+ bridgeFeeEgress = depositAmountUsdc * bridgeFeeRate;
60
+ }
61
+ // Calculate service fees
62
+ const serviceFee = BigNumber(depositAmountUsdc).times(serviceFeeRate).plus(overheadUsd).times(usdcPrice).toNumber();
63
+ const totalFees = serviceFee + bridgeFeeIngress + bridgeFeeEgress;
64
+ return {
65
+ serviceFeeUsd: serviceFee,
66
+ bridgeFeeIngressUsd: bridgeFeeIngress,
67
+ bridgeFeeEgressUsd: bridgeFeeEgress,
68
+ totalFeesUsd: totalFees,
69
+ };
70
+ }, [
71
+ depositAmountUsdc,
72
+ bridgeProvider,
73
+ serviceFeeRate,
74
+ overheadUsd,
75
+ usdcPrice,
76
+ bridgeFeeRate,
77
+ ingressQuote,
78
+ egressQuotes,
79
+ ]);
80
+ return fees;
81
+ }
@@ -0,0 +1,37 @@
1
+ import type { Estimate, RelayQuoteResponse, DeBridgeOrderResponse } from '@silentswap/sdk';
2
+ import { type AssetInfo } from '@silentswap/sdk';
3
+ export interface OrderEstimateResult {
4
+ ingressGasAmount: bigint;
5
+ ingressRate: number;
6
+ egressRates: number[];
7
+ ingressEstimate: Estimate;
8
+ egressEstimates: Estimate[];
9
+ ingressQuote?: RelayQuoteResponse | DeBridgeOrderResponse;
10
+ egressQuotes?: (RelayQuoteResponse | DeBridgeOrderResponse)[];
11
+ calculatedInputAmount?: number;
12
+ }
13
+ export interface UseOrderEstimatesOptions {
14
+ /** User's EVM address (hex address starting with 0x) */
15
+ evmAddress?: `0x${string}`;
16
+ /** User's Solana address (base58 address) */
17
+ solAddress?: string;
18
+ /** Source asset for determining which address to use (if Solana, use solAddress; if EVM, use evmAddress) */
19
+ sourceAsset?: AssetInfo | null;
20
+ /** Maximum price impact percentage allowed */
21
+ maxImpactPercent?: number;
22
+ }
23
+ /**
24
+ * Hook for fetching both ingress and egress estimates in parallel
25
+ * Matches the behavior of estimate_order() from silentswap-v2-ui/src/web3/estimate.ts
26
+ *
27
+ * This hook fetches:
28
+ * - Ingress quote: from source chain to Avalanche (as USDC)
29
+ * - Egress quotes: from Avalanche (USDC) to each destination chain
30
+ *
31
+ * Returns retention rates that account for bridge fees and slippage
32
+ */
33
+ export declare function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpactPercent, }?: UseOrderEstimatesOptions): {
34
+ estimateOrder: (usdcPrice: number, sourceAsset: AssetInfo, sourcePrice: number, sourceAmount: number | string, destinationAssets: AssetInfo[], destinationPrices: number[], splits: number[], calculationDirection?: "input-to-output" | "output-to-input", outputAmounts?: string[]) => Promise<OrderEstimateResult>;
35
+ isLoading: boolean;
36
+ error: Error | null;
37
+ };
@@ -0,0 +1,393 @@
1
+ import { useCallback, useRef, useState, useMemo } from 'react';
2
+ import BigNumber from 'bignumber.js';
3
+ import { parseEvmCaip19, parseSolanaCaip19, isEvmNativeToken, isSolanaNativeToken, isSolanaAsset, } from '@silentswap/sdk';
4
+ import { useQuote } from './useQuote.js';
5
+ // Fallback EVM address for egress quotes when evmAddress is not available
6
+ // Egress quotes always start from Avalanche (EVM), so they require an EVM address
7
+ const EVM_PHONY_ADDRESS_FALLBACK = '0x1111111111111111111111111111111111111111';
8
+ /**
9
+ * Hook for fetching both ingress and egress estimates in parallel
10
+ * Matches the behavior of estimate_order() from silentswap-v2-ui/src/web3/estimate.ts
11
+ *
12
+ * This hook fetches:
13
+ * - Ingress quote: from source chain to Avalanche (as USDC)
14
+ * - Egress quotes: from Avalanche (USDC) to each destination chain
15
+ *
16
+ * Returns retention rates that account for bridge fees and slippage
17
+ */
18
+ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpactPercent, } = {}) {
19
+ const [error, setError] = useState(null);
20
+ // Determine address for ingress quotes based on source chain
21
+ // For ingress, we need the address that matches the source chain (not sourceAsset)
22
+ // For egress, we always start from Avalanche (EVM), so we can use evmAddress as default
23
+ // But we'll create separate useQuote instances to handle this correctly
24
+ const ingressAddress = useMemo(() => {
25
+ if (sourceAsset) {
26
+ // For ingress, address should match the source chain
27
+ return sourceAsset.caip19.includes('solana') ? solAddress : evmAddress;
28
+ }
29
+ return undefined;
30
+ }, [sourceAsset, solAddress, evmAddress]);
31
+ // For egress, we always start from Avalanche (EVM), so use EVM address
32
+ // If evmAddress is undefined, use the phony EVM address as fallback
33
+ // This ensures egress quotes always use an EVM address format (required for Avalanche origin)
34
+ const egressAddress = evmAddress || EVM_PHONY_ADDRESS_FALLBACK;
35
+ // Initialize useQuote hooks - one for ingress (uses address based on source chain)
36
+ // and one for egress (uses EVM address since egress always starts from Avalanche)
37
+ const ingressQuote = useQuote({
38
+ address: ingressAddress,
39
+ maxImpactPercent,
40
+ });
41
+ const egressQuote = useQuote({
42
+ address: egressAddress, // Always an EVM address (either evmAddress or EVM_PHONY_ADDRESS_FALLBACK)
43
+ maxImpactPercent,
44
+ });
45
+ const { estimateLive: estimateLiveIngress, interpolateSamples: interpolate } = ingressQuote;
46
+ const { estimateLive: estimateLiveEgress } = egressQuote;
47
+ const abortControllerRef = useRef(null);
48
+ const lastEstimateParamsRef = useRef('');
49
+ const inFlightRequestRef = useRef(null);
50
+ /**
51
+ * Fetch both ingress and egress estimates in parallel
52
+ * Matches estimate_order() from Svelte app
53
+ */
54
+ const estimateOrder = useCallback(async (usdcPrice, sourceAsset, sourcePrice, sourceAmount, destinationAssets, destinationPrices, splits, calculationDirection = 'input-to-output', outputAmounts) => {
55
+ // Validate inputs
56
+ if (!sourceAsset) {
57
+ throw new Error('sourceAsset is required');
58
+ }
59
+ if (!sourceAsset.caip19) {
60
+ throw new Error('sourceAsset.caip19 is required');
61
+ }
62
+ if (!destinationAssets || destinationAssets.length === 0) {
63
+ throw new Error('destinationAssets is required and must not be empty');
64
+ }
65
+ if (destinationAssets.some((asset) => !asset || !asset.caip19)) {
66
+ throw new Error('All destinationAssets must have a valid caip19 property');
67
+ }
68
+ // For reverse calculation, validate output amounts
69
+ if (calculationDirection === 'output-to-input') {
70
+ if (!outputAmounts || outputAmounts.length !== destinationAssets.length) {
71
+ throw new Error('outputAmounts are required for reverse calculation');
72
+ }
73
+ if (outputAmounts.some((amt) => !amt || parseFloat(amt) <= 0)) {
74
+ throw new Error('All outputAmounts must be valid positive numbers for reverse calculation');
75
+ }
76
+ }
77
+ // Create a key for these parameters to detect duplicate calls
78
+ const sourceAmountStr = typeof sourceAmount === 'string' ? sourceAmount : sourceAmount.toString();
79
+ const estimateKey = `${sourceAsset.caip19}-${sourceAmountStr}-${destinationAssets.map((a) => a.caip19).join(',')}-${splits.join(',')}`;
80
+ // If same parameters and request is in flight, return the existing promise (prevents duplicate requests)
81
+ if (lastEstimateParamsRef.current === estimateKey && inFlightRequestRef.current) {
82
+ return inFlightRequestRef.current;
83
+ }
84
+ // Cancel any previous request (parameters changed)
85
+ if (abortControllerRef.current) {
86
+ abortControllerRef.current.abort();
87
+ }
88
+ lastEstimateParamsRef.current = estimateKey;
89
+ const abortController = new AbortController();
90
+ abortControllerRef.current = abortController;
91
+ setError(null);
92
+ // Create the request promise (declare first to avoid closure issues)
93
+ let requestPromise;
94
+ requestPromise = (async () => {
95
+ try {
96
+ console.log('[OrderEstimates] Step 1: Starting estimateOrder', {
97
+ calculationDirection,
98
+ sourceAsset: sourceAsset.caip19,
99
+ sourceAmount,
100
+ destinationsCount: destinationAssets.length,
101
+ splits,
102
+ hasOutputAmounts: !!outputAmounts,
103
+ });
104
+ // Calculate USD value of source amount
105
+ const sourceAmountNum = typeof sourceAmount === 'string' ? parseFloat(sourceAmount) : sourceAmount;
106
+ const usdValueSrc = BigNumber(sourcePrice).times(sourceAmountNum).toNumber();
107
+ console.log('[OrderEstimates] Step 2: Calculated source USD value', { sourceAmountNum, sourcePrice, usdValueSrc });
108
+ // Parse source asset to get chain ID and token address from CAIP-19
109
+ console.log('[OrderEstimates] Step 3: Parsing source asset CAIP-19');
110
+ const isSourceSolana = isSolanaAsset(sourceAsset.caip19);
111
+ const sourceEvmParsed = !isSourceSolana ? parseEvmCaip19(sourceAsset.caip19) : null;
112
+ const sourceSolanaParsed = isSourceSolana ? parseSolanaCaip19(sourceAsset.caip19) : null;
113
+ const sourceChainId = sourceEvmParsed?.chainId ?? sourceSolanaParsed?.chainId ?? 0;
114
+ const sourceTokenAddress = isEvmNativeToken(sourceAsset.caip19)
115
+ ? '0x0000000000000000000000000000000000000000'
116
+ : isSolanaNativeToken(sourceAsset.caip19)
117
+ ? '11111111111111111111111111111111'
118
+ : (sourceEvmParsed?.tokenAddress ?? sourceSolanaParsed?.tokenAddress ?? '');
119
+ console.log('[OrderEstimates] Step 3: Source asset parsed', { sourceChainId, sourceTokenAddress, isSourceSolana });
120
+ // For reverse calculation, calculate total output USD value first
121
+ let totalOutputUsd = 0;
122
+ let calculatedInputAmount = undefined;
123
+ if (calculationDirection === 'output-to-input' && outputAmounts) {
124
+ console.log('[OrderEstimates] Step 4: Reverse calculation - calculating total output USD');
125
+ // Calculate total USD value of outputs
126
+ totalOutputUsd = destinationAssets.reduce((sum, destAsset, idx) => {
127
+ const outputAmount = parseFloat(outputAmounts[idx] || '0');
128
+ const destPrice = destinationPrices[idx] || 0;
129
+ return sum + (outputAmount * destPrice);
130
+ }, 0);
131
+ console.log('[OrderEstimates] Step 4: Total output USD calculated', { totalOutputUsd, outputAmounts });
132
+ // For reverse calculation, we'll use EXACT_OUTPUT for egress quotes
133
+ // and let ingress calculate the required input with srcChainTokenInAmount: 'auto'
134
+ }
135
+ else {
136
+ console.log('[OrderEstimates] Step 4: Normal calculation (input-to-output)');
137
+ }
138
+ // Fetch ingress and egress estimates in parallel (matching Svelte behavior)
139
+ // Pass abort signal to estimateLive so both can be cancelled together
140
+ console.log('[OrderEstimates] Step 5: Fetching ingress and egress estimates in parallel', {
141
+ calculationDirection,
142
+ sourceChainId,
143
+ sourceTokenAddress,
144
+ destinationsCount: destinationAssets.length,
145
+ });
146
+ const [ingressResult, egressResults] = await Promise.all([
147
+ // Ingress: source chain -> Avalanche (USDC)
148
+ // For reverse calculation, use EXACT_OUTPUT with calculated deposit USD
149
+ // For normal calculation, use EXACT_INPUT with source amount
150
+ calculationDirection === 'output-to-input' && totalOutputUsd > 0
151
+ ? estimateLiveIngress('ingress', sourceAsset.caip19, sourceChainId, sourceTokenAddress, totalOutputUsd / sourcePrice, // Approximate input for quote (will be refined)
152
+ sourcePrice, abortController.signal, undefined, // recipientAddress (not needed for ingress)
153
+ true, // isReverseCalculation
154
+ totalOutputUsd).catch((err) => {
155
+ if (err instanceof Error && err.name === 'AbortError')
156
+ throw err;
157
+ console.warn('Ingress estimate failed (reverse):', err);
158
+ return null;
159
+ })
160
+ : estimateLiveIngress('ingress', sourceAsset.caip19, sourceChainId, sourceTokenAddress, sourceAmountNum, sourcePrice, abortController.signal).catch((err) => {
161
+ if (err instanceof Error && err.name === 'AbortError')
162
+ throw err;
163
+ console.warn('Ingress estimate failed:', err);
164
+ return null;
165
+ }),
166
+ // Egress: Avalanche (USDC) -> each destination chain
167
+ Promise.all(destinationAssets.map((destAsset, idx) => {
168
+ // Parse destination asset to get chain ID and token address from CAIP-19
169
+ const isDestSolana = isSolanaAsset(destAsset.caip19);
170
+ const destEvmParsed = !isDestSolana ? parseEvmCaip19(destAsset.caip19) : null;
171
+ const destSolanaParsed = isDestSolana ? parseSolanaCaip19(destAsset.caip19) : null;
172
+ const destChainId = destEvmParsed?.chainId ?? destSolanaParsed?.chainId ?? 0;
173
+ const destTokenAddress = isEvmNativeToken(destAsset.caip19)
174
+ ? '0x0000000000000000000000000000000000000000'
175
+ : isSolanaNativeToken(destAsset.caip19)
176
+ ? '11111111111111111111111111111111'
177
+ : (destEvmParsed?.tokenAddress ?? destSolanaParsed?.tokenAddress ?? '');
178
+ // Determine recipient address based on destination chain
179
+ // For Solana destinations, use Solana address; for EVM, use EVM address
180
+ const recipientAddress = isDestSolana ? solAddress : evmAddress;
181
+ // For reverse calculation, use output amount directly with EXACT_OUTPUT
182
+ if (calculationDirection === 'output-to-input' && outputAmounts && outputAmounts[idx]) {
183
+ const outputAmount = parseFloat(outputAmounts[idx]);
184
+ // Use EXACT_OUTPUT for egress: we know the output amount, calculate required USDC
185
+ // Pass the output amount directly (in human-readable format)
186
+ // estimateLive will convert it to micro units and use EXACT_OUTPUT tradeType
187
+ return estimateLiveEgress('egress', destAsset.caip19, destChainId, destTokenAddress, outputAmount, // Output amount in human-readable format (destination token)
188
+ destinationPrices[idx] || 1, // Destination token price for USD conversion
189
+ abortController.signal, recipientAddress, // recipientAddress
190
+ true, // isReverseCalculation
191
+ outputAmount).catch((err) => {
192
+ if (err instanceof Error && err.name === 'AbortError')
193
+ throw err;
194
+ console.warn(`Egress estimate failed (reverse) for ${destAsset.caip19}:`, err);
195
+ return null;
196
+ });
197
+ }
198
+ else {
199
+ // Normal calculation: calculate from input
200
+ const splitDiff = idx === 0 ? splits[0] : splits[idx] - splits[idx - 1];
201
+ const splitUsdValue = usdValueSrc * splitDiff;
202
+ // For egress quotes, pass USD value directly (matches Svelte behavior)
203
+ // Svelte passes USD value to estimate_live, which then converts to USDC micro units
204
+ // In Svelte: amount = splitDiff * x_usd_value_src (USD value)
205
+ // Then estimate_live calls: asset_get(S_CAIP19_USDC_AVALANCHE)?.humanToUnit(zx_amount)
206
+ // Since USDC price ≈ 1, USD value ≈ USDC amount in human-readable format
207
+ // estimateLive will convert this to micro units using shiftedBy(6) for USDC
208
+ // So we pass the USD value directly, and estimateLive treats it as human-readable USDC
209
+ const usdcAmountHumanReadable = splitUsdValue; // USD value ≈ USDC amount when price = 1
210
+ // Use egress-specific estimateLive which uses EVM address (since egress always starts from Avalanche)
211
+ return estimateLiveEgress('egress', destAsset.caip19, destChainId, // Already a number from parseSolanaCaip19 (7565164) or parseEvmCaip19
212
+ destTokenAddress, usdcAmountHumanReadable, usdcPrice, abortController.signal, recipientAddress).catch((err) => {
213
+ if (err instanceof Error && err.name === 'AbortError')
214
+ throw err;
215
+ console.warn(`Egress estimate failed for ${destAsset.caip19}:`, err);
216
+ return null;
217
+ });
218
+ }
219
+ })),
220
+ ]);
221
+ if (abortController.signal.aborted) {
222
+ console.log('[OrderEstimates] Request aborted');
223
+ throw new Error('Request aborted');
224
+ }
225
+ console.log('[OrderEstimates] Step 6: Estimates received', {
226
+ hasIngressResult: !!ingressResult,
227
+ egressResultsCount: egressResults.length,
228
+ });
229
+ // Handle ingress result
230
+ if (!ingressResult) {
231
+ throw new Error('Ingress estimate failed');
232
+ }
233
+ const ingressEstimate = ingressResult.estimate;
234
+ const ingressQuote = ingressResult.response;
235
+ const ingressGasAmount = ingressResult.gasAmount;
236
+ console.log('[OrderEstimates] Step 7: Processing ingress result', {
237
+ ingressRate: ingressResult.estimate?.samples?.[0]?.retention,
238
+ ingressGasAmount: ingressGasAmount.toString(),
239
+ calculatedInputAmount: ingressResult.calculatedInputAmount,
240
+ });
241
+ // Handle egress results first (needed for both normal and reverse calculation)
242
+ console.log('[OrderEstimates] Step 8: Processing egress results', {
243
+ destinationsCount: destinationAssets.length,
244
+ egressResultsCount: egressResults.length,
245
+ });
246
+ const egressEstimates = [];
247
+ const egressQuotes = [];
248
+ const egressRetentions = [];
249
+ for (let idx = 0; idx < destinationAssets.length; idx++) {
250
+ const egressResult = egressResults[idx];
251
+ if (calculationDirection === 'output-to-input' && outputAmounts && outputAmounts[idx]) {
252
+ // For reverse calculation, use output USD value for interpolation
253
+ const outputAmount = parseFloat(outputAmounts[idx]);
254
+ const destPrice = destinationPrices[idx] || 1;
255
+ const outputUsdValue = outputAmount * destPrice;
256
+ console.log('[OrderEstimates] Step 8: Processing egress result (reverse)', {
257
+ index: idx,
258
+ outputAmount,
259
+ outputUsdValue,
260
+ hasResult: !!egressResult,
261
+ });
262
+ if (egressResult) {
263
+ egressEstimates.push(egressResult.estimate);
264
+ egressQuotes.push(egressResult.response);
265
+ // Interpolate egress retention rate using output USD value
266
+ const egressRetention = interpolate(egressResult.estimate.samples, outputUsdValue);
267
+ egressRetentions.push(egressRetention);
268
+ }
269
+ else {
270
+ // Fallback: use 1.0 retention if egress estimate failed
271
+ egressEstimates.push({
272
+ gasFee: 0,
273
+ samples: [
274
+ {
275
+ baseline: Infinity,
276
+ retention: 1,
277
+ source: 'none',
278
+ },
279
+ ],
280
+ });
281
+ egressQuotes.push({});
282
+ egressRetentions.push(1.0);
283
+ }
284
+ }
285
+ else {
286
+ // Normal calculation: use split USD value
287
+ const splitDiff = idx === 0 ? splits[0] : splits[idx] - splits[idx - 1];
288
+ const splitUsdValue = usdValueSrc * splitDiff;
289
+ console.log('[OrderEstimates] Step 8: Processing egress result (normal)', {
290
+ index: idx,
291
+ splitDiff,
292
+ splitUsdValue,
293
+ hasResult: !!egressResult,
294
+ });
295
+ if (egressResult) {
296
+ egressEstimates.push(egressResult.estimate);
297
+ egressQuotes.push(egressResult.response);
298
+ // Interpolate egress retention rate for this split
299
+ const egressRetention = interpolate(egressResult.estimate.samples, splitUsdValue);
300
+ egressRetentions.push(egressRetention);
301
+ }
302
+ else {
303
+ // Fallback: use 1.0 retention if egress estimate failed
304
+ // This matches Svelte behavior where missing estimates use cached values
305
+ egressEstimates.push({
306
+ gasFee: 0,
307
+ samples: [
308
+ {
309
+ baseline: Infinity,
310
+ retention: 1,
311
+ source: 'none',
312
+ },
313
+ ],
314
+ });
315
+ egressQuotes.push({});
316
+ egressRetentions.push(1.0);
317
+ }
318
+ }
319
+ }
320
+ console.log('[OrderEstimates] Step 8: Egress results processed', {
321
+ egressRetentions,
322
+ });
323
+ // For reverse calculation, extract calculated input amount from ingress result
324
+ if (calculationDirection === 'output-to-input' && ingressResult.calculatedInputAmount) {
325
+ console.log('[OrderEstimates] Step 9: Reverse calculation - interpolating retention with calculated input', {
326
+ calculatedInputAmount: ingressResult.calculatedInputAmount,
327
+ });
328
+ calculatedInputAmount = ingressResult.calculatedInputAmount;
329
+ // Use the calculated input amount for retention rate interpolation
330
+ const calculatedInputUsd = calculatedInputAmount * sourcePrice;
331
+ const ingressRetention = interpolate(ingressEstimate.samples, calculatedInputUsd);
332
+ const result = {
333
+ ingressGasAmount,
334
+ ingressRate: ingressRetention,
335
+ egressRates: egressRetentions,
336
+ ingressEstimate,
337
+ egressEstimates,
338
+ ingressQuote,
339
+ egressQuotes,
340
+ calculatedInputAmount,
341
+ };
342
+ console.log('[OrderEstimates] Step 10: estimateOrder completed (reverse)', {
343
+ ingressRate: ingressRetention,
344
+ egressRates: egressRetentions,
345
+ calculatedInputAmount,
346
+ });
347
+ return result;
348
+ }
349
+ // Normal calculation: interpolate ingress retention rate
350
+ console.log('[OrderEstimates] Step 9: Normal calculation - interpolating ingress retention', { usdValueSrc });
351
+ const ingressRetention = interpolate(ingressEstimate.samples, usdValueSrc);
352
+ const result = {
353
+ ingressGasAmount,
354
+ ingressRate: ingressRetention,
355
+ egressRates: egressRetentions,
356
+ ingressEstimate,
357
+ egressEstimates,
358
+ ingressQuote,
359
+ egressQuotes,
360
+ };
361
+ console.log('[OrderEstimates] Step 10: estimateOrder completed (normal)', {
362
+ ingressRate: ingressRetention,
363
+ egressRates: egressRetentions,
364
+ });
365
+ return result;
366
+ }
367
+ catch (err) {
368
+ if (err instanceof Error && err.name === 'AbortError') {
369
+ console.log('[OrderEstimates] Request aborted');
370
+ throw err;
371
+ }
372
+ const error = err instanceof Error ? err : new Error('Failed to estimate order');
373
+ console.error('[OrderEstimates] Error: Failed to estimate order', error);
374
+ setError(error);
375
+ throw error;
376
+ }
377
+ })();
378
+ // Store the in-flight request
379
+ inFlightRequestRef.current = requestPromise;
380
+ // Clear in-flight request when promise completes (success or error)
381
+ requestPromise.finally(() => {
382
+ if (inFlightRequestRef.current === requestPromise) {
383
+ inFlightRequestRef.current = null;
384
+ }
385
+ });
386
+ return requestPromise;
387
+ }, [estimateLiveIngress, estimateLiveEgress, interpolate, ingressAddress, egressAddress, sourceAsset, solAddress, evmAddress, maxImpactPercent]);
388
+ return {
389
+ estimateOrder,
390
+ isLoading: !!inFlightRequestRef.current,
391
+ error,
392
+ };
393
+ }
@@ -0,0 +1,12 @@
1
+ import type { AssetInfo } from '@silentswap/sdk';
2
+ import type { Destination } from './useSwap.js';
3
+ /**
4
+ * Hook to calculate output asset info and secondary value for swap transaction card
5
+ * Used in InputPlate and OutputSwapTransactionCard
6
+ */
7
+ export declare function useOutputAssetInfo(destinations: Destination[], tokenOut: AssetInfo | null): {
8
+ outputAssetInfo: AssetInfo | null | undefined;
9
+ outputAmount: string;
10
+ outputAssetPrice: number | undefined;
11
+ outputSecondaryValue: string | null;
12
+ };
@@ -0,0 +1,38 @@
1
+ import { useMemo } from 'react';
2
+ import { getAssetByCaip19 } from '@silentswap/sdk';
3
+ import { useAssetPrice } from './useAssetPrice.js';
4
+ import { useUsdValue } from './useUsdValue.js';
5
+ import { formatSecondaryValue } from '../utils/formatters.js';
6
+ /**
7
+ * Hook to calculate output asset info and secondary value for swap transaction card
8
+ * Used in InputPlate and OutputSwapTransactionCard
9
+ */
10
+ export function useOutputAssetInfo(destinations, tokenOut) {
11
+ // Get output asset info for swap transaction card
12
+ const outputAssetInfo = useMemo(() => {
13
+ if (destinations.length > 0 && destinations[0].asset) {
14
+ return getAssetByCaip19(destinations[0].asset);
15
+ }
16
+ return tokenOut;
17
+ }, [destinations, tokenOut]);
18
+ const outputAmount = useMemo(() => {
19
+ if (destinations.length > 0 && destinations[0].amount) {
20
+ return destinations[0].amount;
21
+ }
22
+ return '';
23
+ }, [destinations]);
24
+ // Calculate secondary value for output asset (USD equivalent)
25
+ const outputAssetPrice = useAssetPrice(outputAssetInfo || null, destinations[0]?.priceUsd);
26
+ const { usdValue: outputUsdValue } = useUsdValue(outputAmount, outputAssetPrice);
27
+ const outputSecondaryValue = useMemo(() => {
28
+ if (!outputAssetPrice || !outputAmount)
29
+ return null;
30
+ return formatSecondaryValue(outputAssetInfo || null, outputUsdValue);
31
+ }, [outputAssetPrice, outputAmount, outputUsdValue, outputAssetInfo]);
32
+ return {
33
+ outputAssetInfo,
34
+ outputAmount,
35
+ outputAssetPrice,
36
+ outputSecondaryValue,
37
+ };
38
+ }
@@ -0,0 +1,60 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import type { AssetInfo } from '@silentswap/sdk';
3
+ export type CachedPrice = {
4
+ priceUsd: number;
5
+ published: number;
6
+ expiration: number;
7
+ source: string;
8
+ };
9
+ export interface usePricesOptions {
10
+ /** Maximum age for cached prices in milliseconds (default: 15000) */
11
+ maxPriceAge?: number;
12
+ /** CoinGecko Pro API key (optional, uses default if not provided) */
13
+ coingeckoApiKey?: string;
14
+ }
15
+ export interface usePricesReturn {
16
+ /** Get USD price for a single asset */
17
+ getPrice: (asset: AssetInfo, maxAge?: number) => Promise<number>;
18
+ /** Get prices for multiple assets (batch fetch) */
19
+ getPrices: (assets: AssetInfo[], maxAge?: number) => Promise<number[]>;
20
+ /** Get USD value for atomic units */
21
+ getUnitValueUsd: (asset: AssetInfo, amount: bigint) => Promise<number>;
22
+ /** Get USD value for human-readable amounts */
23
+ getWholeValueUsd: (asset: AssetInfo, amount: number | string | BigNumber) => Promise<number>;
24
+ /** Check if price is currently loading */
25
+ isPriceLoading: (caip19: string) => boolean;
26
+ /** Get cached price without fetching */
27
+ getCachedPrice: (caip19: string, maxAge?: number) => number | null;
28
+ /** Whether any prices are currently loading */
29
+ loadingPrices: boolean;
30
+ }
31
+ /**
32
+ * React hook for fetching and caching asset prices from CoinGecko
33
+ *
34
+ * Provides price fetching functionality similar to the Svelte Asset.priceUsd() method
35
+ * with caching and batch fetching support.
36
+ *
37
+ * @param options - Configuration options
38
+ * @returns Object with price fetching methods and state
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { usePrices } from '@silentswap/react';
43
+ * import { getAssetByCaip19 } from '@silentswap/sdk';
44
+ *
45
+ * function MyComponent() {
46
+ * const { getPrice, getPrices } = usePrices();
47
+ *
48
+ * const handleGetPrice = async () => {
49
+ * const asset = getAssetByCaip19('eip155:1/erc20:0xA0b8...');
50
+ * if (asset) {
51
+ * const price = await getPrice(asset);
52
+ * console.log('Price:', price);
53
+ * }
54
+ * };
55
+ *
56
+ * return <button onClick={handleGetPrice}>Get Price</button>;
57
+ * }
58
+ * ```
59
+ */
60
+ export declare function usePrices(options?: usePricesOptions): usePricesReturn;