@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.
- package/README.md +48 -0
- package/dist/contexts/AssetsContext.d.ts +24 -0
- package/dist/contexts/AssetsContext.js +83 -0
- package/dist/contexts/BalancesContext.d.ts +28 -0
- package/dist/contexts/BalancesContext.js +533 -0
- package/dist/contexts/OrdersContext.d.ts +53 -0
- package/dist/contexts/OrdersContext.js +240 -0
- package/dist/contexts/PricesContext.d.ts +12 -0
- package/dist/contexts/PricesContext.js +109 -0
- package/dist/contexts/SilentSwapContext.d.ts +58 -0
- package/dist/contexts/SilentSwapContext.js +205 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.d.ts +48 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.js +284 -0
- package/dist/hooks/silent/solana-transaction.d.ts +60 -0
- package/dist/hooks/silent/solana-transaction.js +236 -0
- package/dist/hooks/silent/useAuth.d.ts +90 -0
- package/dist/hooks/silent/useAuth.js +269 -0
- package/dist/hooks/silent/useBridgeExecution.d.ts +39 -0
- package/dist/hooks/silent/useBridgeExecution.js +877 -0
- package/dist/hooks/silent/useOrderSigning.d.ts +34 -0
- package/dist/hooks/silent/useOrderSigning.js +133 -0
- package/dist/hooks/silent/useOrderTracking.d.ts +174 -0
- package/dist/hooks/silent/useOrderTracking.js +524 -0
- package/dist/hooks/silent/useQuoteCalculation.d.ts +50 -0
- package/dist/hooks/silent/useQuoteCalculation.js +331 -0
- package/dist/hooks/silent/useQuoteFetching.d.ts +18 -0
- package/dist/hooks/silent/useQuoteFetching.js +54 -0
- package/dist/hooks/silent/useRefund.d.ts +26 -0
- package/dist/hooks/silent/useRefund.js +134 -0
- package/dist/hooks/silent/useSilentClient.d.ts +16 -0
- package/dist/hooks/silent/useSilentClient.js +32 -0
- package/dist/hooks/silent/useSilentOrders.d.ts +174 -0
- package/dist/hooks/silent/useSilentOrders.js +73 -0
- package/dist/hooks/silent/useSilentQuote.d.ts +88 -0
- package/dist/hooks/silent/useSilentQuote.js +381 -0
- package/dist/hooks/silent/useWallet.d.ts +76 -0
- package/dist/hooks/silent/useWallet.js +203 -0
- package/dist/hooks/useAssetPrice.d.ts +8 -0
- package/dist/hooks/useAssetPrice.js +47 -0
- package/dist/hooks/useContacts.d.ts +52 -0
- package/dist/hooks/useContacts.js +259 -0
- package/dist/hooks/useEgressEstimates.d.ts +32 -0
- package/dist/hooks/useEgressEstimates.js +230 -0
- package/dist/hooks/useHiddenSwapFees.d.ts +22 -0
- package/dist/hooks/useHiddenSwapFees.js +81 -0
- package/dist/hooks/useOrderEstimates.d.ts +37 -0
- package/dist/hooks/useOrderEstimates.js +393 -0
- package/dist/hooks/useOutputAssetInfo.d.ts +12 -0
- package/dist/hooks/useOutputAssetInfo.js +38 -0
- package/dist/hooks/usePrices.d.ts +60 -0
- package/dist/hooks/usePrices.js +188 -0
- package/dist/hooks/useQuote.d.ts +73 -0
- package/dist/hooks/useQuote.js +507 -0
- package/dist/hooks/useResetSwapForm.d.ts +16 -0
- package/dist/hooks/useResetSwapForm.js +68 -0
- package/dist/hooks/useSlippageUsd.d.ts +11 -0
- package/dist/hooks/useSlippageUsd.js +19 -0
- package/dist/hooks/useSolanaAdapter.d.ts +15 -0
- package/dist/hooks/useSolanaAdapter.js +55 -0
- package/dist/hooks/useStatus.d.ts +25 -0
- package/dist/hooks/useStatus.js +60 -0
- package/dist/hooks/useSwap.d.ts +67 -0
- package/dist/hooks/useSwap.js +285 -0
- package/dist/hooks/useTransaction.d.ts +119 -0
- package/dist/hooks/useTransaction.js +353 -0
- package/dist/hooks/useTransactionAddress.d.ts +11 -0
- package/dist/hooks/useTransactionAddress.js +26 -0
- package/dist/hooks/useUsdValue.d.ts +7 -0
- package/dist/hooks/useUsdValue.js +19 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +41 -0
- package/dist/stories/SilentSwapOverview.stories.d.ts +10 -0
- package/dist/stories/SilentSwapOverview.stories.js +364 -0
- package/dist/stories/useAuth.stories.d.ts +6 -0
- package/dist/stories/useAuth.stories.js +55 -0
- package/dist/stories/useSilentClient.stories.d.ts +9 -0
- package/dist/stories/useSilentClient.stories.js +39 -0
- package/dist/stories/useSilentOrders.stories.d.ts +1 -0
- package/dist/stories/useSilentOrders.stories.js +1 -0
- package/dist/stories/useSilentQuote.stories.d.ts +6 -0
- package/dist/stories/useSilentQuote.stories.js +267 -0
- package/dist/stories/useTransaction.stories.d.ts +6 -0
- package/dist/stories/useTransaction.stories.js +121 -0
- package/dist/utils/formatters.d.ts +33 -0
- package/dist/utils/formatters.js +82 -0
- 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;
|