@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,47 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { usePricesContext } from '../contexts/PricesContext.js';
3
+ /**
4
+ * Hook to fetch and manage asset price from the global context.
5
+ * Handles price fetching with optional override.
6
+ * This hook now uses the PricesContext to prevent duplicate API calls.
7
+ * Automatically updates when prices are refreshed (after 10 seconds).
8
+ */
9
+ export function useAssetPrice(asset, priceOverride) {
10
+ const { getPrice, prices } = usePricesContext();
11
+ const [price, setPrice] = useState(priceOverride);
12
+ useEffect(() => {
13
+ if (priceOverride !== undefined) {
14
+ setPrice(priceOverride);
15
+ return;
16
+ }
17
+ if (!asset) {
18
+ setPrice(undefined);
19
+ return;
20
+ }
21
+ // Get price from context (will trigger fetch if not cached or stale)
22
+ const contextPrice = getPrice(asset);
23
+ setPrice(contextPrice);
24
+ // Set up a small interval to check for updated price if it was undefined
25
+ // This handles the case where the price is being fetched asynchronously
26
+ if (contextPrice === undefined) {
27
+ const interval = setInterval(() => {
28
+ const updatedPrice = getPrice(asset);
29
+ if (updatedPrice !== undefined) {
30
+ setPrice(updatedPrice);
31
+ clearInterval(interval);
32
+ }
33
+ }, 100);
34
+ return () => clearInterval(interval);
35
+ }
36
+ }, [asset, priceOverride, getPrice]);
37
+ // Update price when context price changes (handles stale price refreshes)
38
+ useEffect(() => {
39
+ if (asset && priceOverride === undefined) {
40
+ const contextPrice = prices[asset.caip19];
41
+ if (contextPrice !== undefined && contextPrice !== price) {
42
+ setPrice(contextPrice);
43
+ }
44
+ }
45
+ }, [asset, priceOverride, prices, price]);
46
+ return price;
47
+ }
@@ -0,0 +1,52 @@
1
+ export declare enum AddressBookContactSource {
2
+ UNKNOWN = "unknown",
3
+ WALLET = "wallet",
4
+ SAVED = "saved"
5
+ }
6
+ export type AddressBookContactId = `caip10:${string}` | `ens:${string}` | `sns:${string}`;
7
+ export type AddressBookContactInfo = {
8
+ id: AddressBookContactId;
9
+ label?: string;
10
+ source?: AddressBookContactSource;
11
+ resolved?: string;
12
+ updated?: number;
13
+ };
14
+ export declare class AddressBookContact {
15
+ protected _g_info: AddressBookContactInfo;
16
+ protected _s_caip10: string;
17
+ protected _s_ens: string;
18
+ protected _s_sns: string;
19
+ protected _s_caip2_ns: 'eip155' | 'solana' | 'bip122' | 'cosmos';
20
+ private _saveCallback?;
21
+ constructor(_g_info: AddressBookContactInfo, b_from_id?: boolean, saveCallback?: (contact: AddressBookContact) => void);
22
+ private updateENSRecord;
23
+ get id(): AddressBookContactId;
24
+ get info(): AddressBookContactInfo;
25
+ get caip10(): string;
26
+ get ens(): string;
27
+ get sns(): string;
28
+ get label(): string;
29
+ set label(s_label: string);
30
+ get source(): AddressBookContactSource;
31
+ get caip2Ns(): 'eip155' | 'solana' | 'bip122' | 'cosmos';
32
+ isEvm(): boolean;
33
+ isSolana(): boolean;
34
+ isBip122(): boolean;
35
+ isCosmos(): boolean;
36
+ rawAddress(): string | undefined;
37
+ address(): Promise<string>;
38
+ save(): this;
39
+ }
40
+ export type AddressBookContactsQuery = {
41
+ exclude?: ('' | undefined | AddressBookContactId)[];
42
+ include?: ('' | undefined | AddressBookContactId)[];
43
+ };
44
+ export declare const useContacts: () => {
45
+ contacts: AddressBookContact[];
46
+ loading: boolean;
47
+ addContact: (contact: AddressBookContact) => void;
48
+ findContact: (id: AddressBookContactId) => AddressBookContact | undefined;
49
+ getContacts: (query?: AddressBookContactsQuery) => AddressBookContact[];
50
+ createContactFromId: (id: AddressBookContactId) => AddressBookContact;
51
+ deleteContact: (id: AddressBookContactId) => void;
52
+ };
@@ -0,0 +1,259 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { getAddress, http } from 'viem';
3
+ import { mainnet } from 'viem/chains';
4
+ import { createEnsPublicClient } from '@ensdomains/ensjs';
5
+ export var AddressBookContactSource;
6
+ (function (AddressBookContactSource) {
7
+ AddressBookContactSource["UNKNOWN"] = "unknown";
8
+ AddressBookContactSource["WALLET"] = "wallet";
9
+ AddressBookContactSource["SAVED"] = "saved";
10
+ })(AddressBookContactSource || (AddressBookContactSource = {}));
11
+ export class AddressBookContact {
12
+ _g_info;
13
+ _s_caip10 = '';
14
+ _s_ens = '';
15
+ _s_sns = '';
16
+ _s_caip2_ns;
17
+ _saveCallback;
18
+ constructor(_g_info, b_from_id, saveCallback) {
19
+ this._g_info = _g_info;
20
+ this._saveCallback = saveCallback;
21
+ if (!this._g_info.id)
22
+ throw new Error('Improper contact info');
23
+ const namespaceMatch = /^([^:]+):(.+)$/.exec(_g_info.id);
24
+ if (!namespaceMatch)
25
+ throw new Error(`Invalid contact ID "${_g_info.id}"`);
26
+ const [, s_type, s_qualifier] = namespaceMatch;
27
+ if (s_type === 'caip10') {
28
+ this._s_caip10 = s_qualifier;
29
+ this._s_caip2_ns = s_qualifier.split(':')[0];
30
+ }
31
+ else if (s_type === 'ens') {
32
+ this._s_ens = s_qualifier;
33
+ this._s_caip2_ns = 'eip155';
34
+ }
35
+ else if (s_type === 'sns') {
36
+ this._s_sns = s_qualifier;
37
+ this._s_caip2_ns = 'solana';
38
+ }
39
+ else {
40
+ throw new Error(`Invalid contact ID "${_g_info.id}"`);
41
+ }
42
+ // Update ENS if needed
43
+ if (s_type === 'ens' && !b_from_id && this._saveCallback) {
44
+ this.updateENSRecord();
45
+ }
46
+ }
47
+ async updateENSRecord() {
48
+ const XT_MAX_ENS_CACHE_DURATION = 3600 * 1000 * 24; // 24 hours
49
+ if (Date.now() - (this._g_info.updated ?? 0) > XT_MAX_ENS_CACHE_DURATION) {
50
+ try {
51
+ const ensClient = createEnsPublicClient({
52
+ chain: mainnet,
53
+ transport: http(),
54
+ });
55
+ const record = await ensClient.getAddressRecord({
56
+ name: this._s_ens,
57
+ });
58
+ if (record?.value) {
59
+ this._g_info.resolved = getAddress(record.value);
60
+ this._g_info.updated = Date.now();
61
+ // Use callback to save if available
62
+ if (this._saveCallback) {
63
+ this._saveCallback(this);
64
+ }
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.warn(`Failed to resolve ENS ${this._s_ens}:`, error);
69
+ }
70
+ }
71
+ }
72
+ get id() {
73
+ return this._g_info.id;
74
+ }
75
+ get info() {
76
+ return this._g_info;
77
+ }
78
+ get caip10() {
79
+ return this._s_caip10;
80
+ }
81
+ get ens() {
82
+ return this._s_ens;
83
+ }
84
+ get sns() {
85
+ return this._s_sns;
86
+ }
87
+ get label() {
88
+ return this._g_info.label || this._s_ens || this._s_sns || this._s_caip10.replace(/^([^:]*:)*/, '');
89
+ }
90
+ set label(s_label) {
91
+ this._g_info.label = s_label;
92
+ if (this._saveCallback) {
93
+ this._saveCallback(this);
94
+ }
95
+ }
96
+ get source() {
97
+ return this._g_info.source ?? AddressBookContactSource.UNKNOWN;
98
+ }
99
+ get caip2Ns() {
100
+ return this._s_caip2_ns;
101
+ }
102
+ isEvm() {
103
+ return this._s_caip2_ns === 'eip155';
104
+ }
105
+ isSolana() {
106
+ return this._s_caip2_ns === 'solana';
107
+ }
108
+ isBip122() {
109
+ return this._s_caip2_ns === 'bip122';
110
+ }
111
+ isCosmos() {
112
+ return this._s_caip2_ns === 'cosmos';
113
+ }
114
+ rawAddress() {
115
+ if (this._s_caip10) {
116
+ const caip10Match = /^([^:]*):(.+)$/.exec(this._s_caip10);
117
+ if (caip10Match) {
118
+ const addressMatch = /^([^:]*):(.+)$/.exec(caip10Match[2]);
119
+ return addressMatch?.[2];
120
+ }
121
+ }
122
+ return this._g_info.resolved;
123
+ }
124
+ async address() {
125
+ const rawAddr = this.rawAddress();
126
+ if (rawAddr)
127
+ return rawAddr;
128
+ if (this._s_ens) {
129
+ try {
130
+ const ensClient = createEnsPublicClient({
131
+ chain: mainnet,
132
+ transport: http(),
133
+ });
134
+ const record = await ensClient.getAddressRecord({
135
+ name: this._s_ens,
136
+ });
137
+ if (record?.value) {
138
+ return getAddress(record.value);
139
+ }
140
+ throw new Error(`Could not resolve ENS name "${this._s_ens}"`);
141
+ }
142
+ catch (error) {
143
+ throw new Error(`Could not resolve ENS name "${this._s_ens}": ${error}`);
144
+ }
145
+ }
146
+ if (this._s_sns) {
147
+ throw new Error(`SNS resolution not implemented for "${this._s_sns}"`);
148
+ }
149
+ throw new Error(`Could not resolve address for contact "${this._g_info.id}"`);
150
+ }
151
+ save() {
152
+ if (this._saveCallback) {
153
+ this._saveCallback(this);
154
+ }
155
+ return this;
156
+ }
157
+ }
158
+ const STORAGE_KEY_CONTACTS = 'silentswap:contacts';
159
+ export const useContacts = () => {
160
+ const [contacts, setContacts] = useState([]);
161
+ const [loading, setLoading] = useState(true);
162
+ // Create save callback that directly updates state
163
+ const createSaveCallback = useCallback((contact) => {
164
+ setContacts((prev) => {
165
+ // Remove existing contact if it exists
166
+ const filtered = prev.filter((c) => c.id !== contact.id);
167
+ // Add updated contact and sort by label
168
+ const updated = [...filtered, contact].sort((a, b) => a.label.localeCompare(b.label));
169
+ return updated;
170
+ });
171
+ }, []);
172
+ const addContact = useCallback((contact) => {
173
+ setContacts((prev) => {
174
+ // Remove existing contact if it exists
175
+ const filtered = prev.filter((c) => c.id !== contact.id);
176
+ // Ensure contact has save callback
177
+ if (!contact._saveCallback) {
178
+ // Create new contact instance with callback
179
+ const contactWithCallback = new AddressBookContact(contact.info, false, createSaveCallback);
180
+ const updated = [...filtered, contactWithCallback].sort((a, b) => a.label.localeCompare(b.label));
181
+ return updated;
182
+ }
183
+ // Add new contact and sort by label
184
+ const updated = [...filtered, contact].sort((a, b) => a.label.localeCompare(b.label));
185
+ return updated;
186
+ });
187
+ }, [createSaveCallback]);
188
+ // Load contacts from localStorage on mount
189
+ useEffect(() => {
190
+ const loadContacts = () => {
191
+ try {
192
+ if (typeof localStorage === 'undefined')
193
+ return;
194
+ const stored = localStorage.getItem(STORAGE_KEY_CONTACTS);
195
+ if (stored) {
196
+ try {
197
+ const parsed = JSON.parse(stored);
198
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'object' && item !== null)) {
199
+ const contactInfos = parsed;
200
+ // Create contacts with save callback from the start
201
+ const contactInstances = contactInfos.map((info) => new AddressBookContact(info, true, createSaveCallback));
202
+ setContacts(contactInstances);
203
+ }
204
+ }
205
+ catch (error) {
206
+ console.warn('Failed to parse contacts from localStorage:', error);
207
+ }
208
+ }
209
+ }
210
+ catch (error) {
211
+ console.warn('Failed to load contacts from localStorage:', error);
212
+ }
213
+ finally {
214
+ setLoading(false);
215
+ }
216
+ };
217
+ loadContacts();
218
+ }, [createSaveCallback]);
219
+ // Save contacts to localStorage whenever contacts change
220
+ useEffect(() => {
221
+ if (!loading) {
222
+ try {
223
+ const serialized = contacts.map((contact) => contact.info);
224
+ localStorage.setItem(STORAGE_KEY_CONTACTS, JSON.stringify(serialized));
225
+ }
226
+ catch (error) {
227
+ console.warn('Failed to save contacts to localStorage:', error);
228
+ }
229
+ }
230
+ }, [contacts, loading]);
231
+ const findContact = useCallback((id) => {
232
+ return contacts.find((contact) => contact.id === id);
233
+ }, [contacts]);
234
+ const getContacts = useCallback((query) => {
235
+ let result = [...contacts];
236
+ if (query?.exclude) {
237
+ result = result.filter((contact) => !query.exclude?.includes(contact.id));
238
+ }
239
+ if (query?.include) {
240
+ result = result.filter((contact) => query.include?.includes(contact.id));
241
+ }
242
+ return result;
243
+ }, [contacts]);
244
+ const createContactFromId = useCallback((id) => {
245
+ return new AddressBookContact({ id }, true, createSaveCallback);
246
+ }, [createSaveCallback]);
247
+ const deleteContact = useCallback((id) => {
248
+ setContacts((prev) => prev.filter((contact) => contact.id !== id));
249
+ }, []);
250
+ return {
251
+ contacts,
252
+ loading,
253
+ addContact,
254
+ findContact,
255
+ getContacts,
256
+ createContactFromId,
257
+ deleteContact,
258
+ };
259
+ };
@@ -0,0 +1,32 @@
1
+ import { BridgeProvider, type AssetInfo } from '@silentswap/sdk';
2
+ import type { Destination } from './useSwap.js';
3
+ export interface UseEgressEstimatesOptions {
4
+ evmAddress: `0x${string}` | string | undefined;
5
+ solAddress: string | undefined;
6
+ quoteAddress: string | undefined;
7
+ tokenIn: AssetInfo | null;
8
+ inputAmount: string;
9
+ destinations: Destination[];
10
+ splits: number[];
11
+ updateDestinationAmount: (index: number, amount: string) => void;
12
+ serviceFeeRate?: number;
13
+ overheadUsd?: number;
14
+ calculationDirection?: 'input-to-output' | 'output-to-input';
15
+ setInputAmount?: (amount: string) => void;
16
+ }
17
+ /**
18
+ * Hook to fetch egress estimates and calculate accurate output amounts
19
+ * Matches the behavior of Svelte Form.svelte's fetch_live_estimate()
20
+ */
21
+ export declare function useEgressEstimates({ evmAddress, solAddress, quoteAddress, tokenIn, inputAmount, destinations, splits, updateDestinationAmount, serviceFeeRate, overheadUsd, calculationDirection, setInputAmount, }: UseEgressEstimatesOptions): {
22
+ fetchEstimates: (direction?: "input-to-output" | "output-to-input") => Promise<any>;
23
+ isLoading: boolean;
24
+ error: Error | null;
25
+ egressRates: number[];
26
+ egressQuotes: any[];
27
+ ingressRate: number;
28
+ ingressQuote: any;
29
+ depositAmountUsd: number;
30
+ bridgeProviderFromQuote: BridgeProvider | null;
31
+ usdcPrice: number;
32
+ };
@@ -0,0 +1,230 @@
1
+ import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import { BigNumber } from 'bignumber.js';
3
+ import { useOrderEstimates } from './useOrderEstimates.js';
4
+ import { getAssetByCaip19, S_CAIP19_USDC_AVALANCHE } from '@silentswap/sdk';
5
+ import { usePrices } from './usePrices.js';
6
+ /**
7
+ * Hook to fetch egress estimates and calculate accurate output amounts
8
+ * Matches the behavior of Svelte Form.svelte's fetch_live_estimate()
9
+ */
10
+ export function useEgressEstimates({ evmAddress, solAddress, quoteAddress, tokenIn, inputAmount, destinations, splits, updateDestinationAmount, serviceFeeRate = 0.01, overheadUsd = 0, calculationDirection = 'input-to-output', setInputAmount, }) {
11
+ const { estimateOrder, isLoading, error } = useOrderEstimates({
12
+ evmAddress: evmAddress,
13
+ solAddress: solAddress,
14
+ sourceAsset: tokenIn,
15
+ });
16
+ const { getPrice } = usePrices();
17
+ // Store egress rates and quotes
18
+ const [egressRates, setEgressRates] = useState([]);
19
+ const [egressQuotes, setEgressQuotes] = useState([]);
20
+ const [ingressRate, setIngressRate] = useState(1);
21
+ const [ingressQuote, setIngressQuote] = useState(null);
22
+ const [depositAmountUsd, setDepositAmountUsd] = useState(0); // USD value after ingress retention
23
+ const [usdcPrice, setUsdcPrice] = useState(1); // USDC price (usually ~1)
24
+ const lastEstimateParams = useRef('');
25
+ const inFlightRequestRef = useRef(null);
26
+ /**
27
+ * Fetch egress estimates and calculate output amounts
28
+ * This matches Svelte's fetch_live_estimate() function
29
+ * @param direction - Calculation direction: 'input-to-output' (normal) or 'output-to-input' (reverse)
30
+ */
31
+ const fetchEstimates = useCallback(async (direction = 'input-to-output') => {
32
+ console.log('[EgressEstimates] Step 1: Starting fetchEstimates', { direction, tokenIn: tokenIn?.caip19, inputAmount, destinationsCount: destinations.length });
33
+ // Validate all required inputs before proceeding
34
+ if (!tokenIn ||
35
+ !tokenIn.caip19 ||
36
+ !inputAmount ||
37
+ parseFloat(inputAmount) <= 0 ||
38
+ destinations.length === 0 ||
39
+ !quoteAddress) {
40
+ console.log('[EgressEstimates] Step 1: Validation failed - missing required inputs');
41
+ return;
42
+ }
43
+ const estimateKey = `${tokenIn.caip19}-${inputAmount}-${destinations.map((d) => d.asset).join(',')}-${splits.join(',')}`;
44
+ // If same parameters and request is in flight, return the existing promise (prevents duplicate requests)
45
+ if (lastEstimateParams.current === estimateKey && inFlightRequestRef.current) {
46
+ console.log('[EgressEstimates] Step 1: Request already in flight with same parameters, returning existing promise');
47
+ return inFlightRequestRef.current;
48
+ }
49
+ // Update params before starting request
50
+ lastEstimateParams.current = estimateKey;
51
+ console.log('[EgressEstimates] Step 2: Fetching prices for USDC, source token, and destinations');
52
+ try {
53
+ // Get prices
54
+ const usdcAsset = getAssetByCaip19(S_CAIP19_USDC_AVALANCHE);
55
+ if (!usdcAsset)
56
+ throw new Error('USDC asset not found');
57
+ const [fetchedUsdcPrice, sourcePrice, ...destinationPrices] = await Promise.all([
58
+ getPrice(usdcAsset),
59
+ getPrice(tokenIn),
60
+ ...destinations.map((dest) => {
61
+ const destAsset = getAssetByCaip19(dest.asset);
62
+ return destAsset ? getPrice(destAsset) : Promise.resolve(0);
63
+ }),
64
+ ]);
65
+ console.log('[EgressEstimates] Step 2: Prices fetched', { usdcPrice: fetchedUsdcPrice, sourcePrice, destinationPrices });
66
+ // Store USDC price for use in fee calculations
67
+ setUsdcPrice(fetchedUsdcPrice || 1);
68
+ // Get destination assets
69
+ console.log('[EgressEstimates] Step 3: Resolving destination assets');
70
+ const destinationAssets = destinations
71
+ .map((dest) => getAssetByCaip19(dest.asset))
72
+ .filter((asset) => asset !== null);
73
+ if (destinationAssets.length !== destinations.length) {
74
+ throw new Error('Some destination assets not found');
75
+ }
76
+ // Fetch estimates (both ingress and egress in parallel)
77
+ // For reverse calculation (output-to-input), pass output amounts and use EXACT_OUTPUT
78
+ const effectiveDirection = direction || calculationDirection || 'input-to-output';
79
+ console.log('[EgressEstimates] Step 4: Calling estimateOrder', {
80
+ effectiveDirection,
81
+ sourceAsset: tokenIn.caip19,
82
+ inputAmount,
83
+ destinationsCount: destinationAssets.length,
84
+ splits,
85
+ });
86
+ const requestPromise = estimateOrder(fetchedUsdcPrice || 1, tokenIn, sourcePrice, inputAmount, destinationAssets, destinationPrices, splits, effectiveDirection, effectiveDirection === 'output-to-input'
87
+ ? destinations.map((dest) => dest.amount) // Pass output amounts for reverse calculation
88
+ : undefined);
89
+ // Store the in-flight request
90
+ inFlightRequestRef.current = requestPromise;
91
+ console.log('[EgressEstimates] Step 5: Waiting for estimateOrder result');
92
+ const result = await requestPromise;
93
+ console.log('[EgressEstimates] Step 5: estimateOrder completed', {
94
+ ingressRate: result.ingressRate,
95
+ egressRatesCount: result.egressRates.length,
96
+ calculatedInputAmount: result.calculatedInputAmount,
97
+ });
98
+ // Clear in-flight request when done
99
+ if (inFlightRequestRef.current === requestPromise) {
100
+ inFlightRequestRef.current = null;
101
+ }
102
+ // Store rates and quotes
103
+ console.log('[EgressEstimates] Step 6: Storing rates and quotes');
104
+ setIngressRate(result.ingressRate);
105
+ setIngressQuote(result.ingressQuote);
106
+ setEgressRates(result.egressRates);
107
+ setEgressQuotes(result.egressQuotes || []);
108
+ // Calculate deposit amount in USD (matches Svelte calculation - lines 173-177)
109
+ // Note: This is the USD value after ingress retention, not the USDC amount
110
+ let depositUsd;
111
+ // Handle reverse calculation: if direction is 'output-to-input',
112
+ // the estimate system calculated the required input amount
113
+ if (effectiveDirection === 'output-to-input' && result.calculatedInputAmount && setInputAmount) {
114
+ console.log('[EgressEstimates] Step 7: Handling reverse calculation - updating input amount', {
115
+ calculatedInputAmount: result.calculatedInputAmount,
116
+ });
117
+ // Update input amount with the value calculated by the estimate system
118
+ const precision = tokenIn.precision ?? 6;
119
+ const decimals = tokenIn.decimals ?? 6;
120
+ const formattedInputAmount = BigNumber(result.calculatedInputAmount).shiftedBy(-decimals)
121
+ .toFixed(Math.min(precision, 8));
122
+ setInputAmount(formattedInputAmount);
123
+ console.log('[EgressEstimates] Step 7: Input amount updated', { formattedInputAmount });
124
+ // Recalculate depositUsd with the new input amount
125
+ const newInputAmountNum = parseFloat(formattedInputAmount) || 0;
126
+ depositUsd = BigNumber.max(0, BigNumber(newInputAmountNum).times(sourcePrice).times(result.ingressRate)).toNumber();
127
+ }
128
+ else {
129
+ console.log('[EgressEstimates] Step 7: Normal calculation - calculating deposit from input');
130
+ // Normal calculation: calculate deposit amount from input
131
+ depositUsd = BigNumber.max(0, BigNumber(inputAmount).times(sourcePrice).times(result.ingressRate)).toNumber();
132
+ }
133
+ console.log('[EgressEstimates] Step 8: Calculating fees and actionable amount', { depositUsd });
134
+ // Store USD deposit value (for fee calculations)
135
+ // This is the USD value after ingress retention (matches Svelte _yx_usd_deposit)
136
+ setDepositAmountUsd(depositUsd);
137
+ // USD value of actionable (after service fees) - matches Svelte line 215
138
+ const serviceFeeUsd = depositUsd * serviceFeeRate + overheadUsd;
139
+ const actionableUsd = depositUsd - serviceFeeUsd;
140
+ // Update destination amounts (matches Svelte Form.svelte lines 217-229)
141
+ // For reverse calculation, preserve user's manual changes
142
+ if (effectiveDirection === 'output-to-input') {
143
+ console.log('[EgressEstimates] Step 9: Reverse calculation - preserving user output amounts');
144
+ // Don't overwrite user's manual output changes
145
+ // The outputs are already set correctly
146
+ }
147
+ else {
148
+ console.log('[EgressEstimates] Step 9: Normal calculation - updating destination amounts', {
149
+ actionableUsd,
150
+ destinationsCount: destinations.length,
151
+ });
152
+ // Normal calculation: calculate outputs from input
153
+ destinations.forEach((dest, idx) => {
154
+ const destAsset = destinationAssets[idx];
155
+ if (!destAsset)
156
+ return;
157
+ // Calculate split difference: for first destination, use splits[0] - 0
158
+ // For subsequent destinations, use splits[idx] - splits[idx - 1]
159
+ // Special case: if only one destination, ensure it gets 100% (splitDiff = 1.0)
160
+ const prevSplit = idx > 0 ? splits[idx - 1] : 0;
161
+ let splitDiff = splits[idx] - prevSplit;
162
+ // Ensure single destination always gets 100%
163
+ if (destinations.length === 1) {
164
+ splitDiff = 1.0;
165
+ }
166
+ const egressRate = result.egressRates[idx] || 1;
167
+ const destPrice = destinationPrices[idx] || 1;
168
+ // Calculate output amount (matches Svelte Form.svelte line 225-228)
169
+ // Formula: actionableUsd / destPrice * splitDiff * egressRate
170
+ const outputAmount = BigNumber(actionableUsd)
171
+ .div(destPrice)
172
+ .times(splitDiff)
173
+ .times(egressRate)
174
+ .toFixed(destAsset.precision ?? 6);
175
+ updateDestinationAmount(idx, outputAmount);
176
+ });
177
+ }
178
+ console.log('[EgressEstimates] Step 10: fetchEstimates completed successfully');
179
+ return result;
180
+ }
181
+ catch (err) {
182
+ // Clear in-flight request on error
183
+ inFlightRequestRef.current = null;
184
+ if (err instanceof Error && err.name === 'AbortError') {
185
+ console.log('[EgressEstimates] Request aborted');
186
+ return;
187
+ }
188
+ console.error('[EgressEstimates] Error: Failed to fetch egress estimates', err);
189
+ throw err;
190
+ }
191
+ }, [
192
+ tokenIn,
193
+ inputAmount,
194
+ destinations,
195
+ splits,
196
+ estimateOrder,
197
+ getPrice,
198
+ updateDestinationAmount,
199
+ quoteAddress,
200
+ serviceFeeRate,
201
+ overheadUsd,
202
+ calculationDirection,
203
+ setInputAmount,
204
+ ]);
205
+ // Determine provider from ingress quote (matches Svelte behavior where provider is determined from solve_uusdc_amount)
206
+ // Relay quotes have 'fees' property, DeBridge quotes have 'estimation' property
207
+ const bridgeProviderFromQuote = useMemo(() => {
208
+ if (!ingressQuote)
209
+ return null;
210
+ if ('fees' in ingressQuote) {
211
+ return 'relay';
212
+ }
213
+ else if ('estimation' in ingressQuote) {
214
+ return 'debridge';
215
+ }
216
+ return null;
217
+ }, [ingressQuote]);
218
+ return {
219
+ fetchEstimates,
220
+ isLoading,
221
+ error,
222
+ egressRates,
223
+ egressQuotes,
224
+ ingressRate,
225
+ ingressQuote,
226
+ depositAmountUsd, // USD value after ingress retention (for fee calculations)
227
+ bridgeProviderFromQuote,
228
+ usdcPrice, // USDC price (usually ~1, matches x_usd_price_usdc in Svelte)
229
+ };
230
+ }
@@ -0,0 +1,22 @@
1
+ import type { RelayQuoteResponse, DeBridgeOrderResponse } from '@silentswap/sdk';
2
+ export interface HiddenSwapFees {
3
+ serviceFeeUsd: number;
4
+ bridgeFeeIngressUsd: number;
5
+ bridgeFeeEgressUsd: number;
6
+ totalFeesUsd: number;
7
+ }
8
+ export interface UseHiddenSwapFeesOptions {
9
+ depositAmountUsdc: number;
10
+ bridgeProvider: string | null;
11
+ serviceFeeRate?: number;
12
+ overheadUsd?: number;
13
+ usdcPrice?: number;
14
+ bridgeFeeRate?: number;
15
+ ingressQuote?: RelayQuoteResponse | DeBridgeOrderResponse | null;
16
+ egressQuotes?: (RelayQuoteResponse | DeBridgeOrderResponse)[] | null;
17
+ }
18
+ /**
19
+ * Hook to calculate fees for hidden swap
20
+ * Extracts actual fees from bridge quotes when available, otherwise uses estimated rates
21
+ */
22
+ export declare function useHiddenSwapFees({ depositAmountUsdc, bridgeProvider, serviceFeeRate, overheadUsd, usdcPrice, bridgeFeeRate, ingressQuote, egressQuotes, }: UseHiddenSwapFeesOptions): HiddenSwapFees;