@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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WebSocket connection manager for order tracking
|
|
3
|
+
* Maintains one connection per client/baseUrl and handles multiple order subscriptions
|
|
4
|
+
*/
|
|
5
|
+
class OrderTrackingWebSocketManager {
|
|
6
|
+
connections = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Get connection key from client
|
|
9
|
+
*/
|
|
10
|
+
getConnectionKey(client) {
|
|
11
|
+
if (client) {
|
|
12
|
+
return client.baseUrl;
|
|
13
|
+
}
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
return window.location.origin;
|
|
16
|
+
}
|
|
17
|
+
return 'default';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get WebSocket URL from client
|
|
21
|
+
*/
|
|
22
|
+
getWebSocketUrl(client) {
|
|
23
|
+
let wsUrl;
|
|
24
|
+
if (typeof window !== 'undefined' && window.__WEBSOCKET_URL__) {
|
|
25
|
+
wsUrl = window.__WEBSOCKET_URL__;
|
|
26
|
+
}
|
|
27
|
+
else if (client) {
|
|
28
|
+
const baseUrl = client.baseUrl;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(baseUrl);
|
|
31
|
+
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
32
|
+
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
wsUrl = baseUrl.replace(/^https?:\/\//, 'wss://').replace(/^http:\/\//, 'ws://') + '/websocket';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (typeof window !== 'undefined') {
|
|
39
|
+
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/websocket`;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
wsUrl = 'ws://localhost/websocket';
|
|
43
|
+
}
|
|
44
|
+
// Ensure URL doesn't have any path or query params
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(wsUrl);
|
|
47
|
+
const protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
|
|
48
|
+
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
wsUrl = wsUrl.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/websocket';
|
|
52
|
+
}
|
|
53
|
+
return wsUrl;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get or create connection state
|
|
57
|
+
*/
|
|
58
|
+
getConnectionState(client) {
|
|
59
|
+
const key = this.getConnectionKey(client);
|
|
60
|
+
let state = this.connections.get(key);
|
|
61
|
+
if (!state) {
|
|
62
|
+
state = {
|
|
63
|
+
ws: null,
|
|
64
|
+
url: this.getWebSocketUrl(client),
|
|
65
|
+
isConnected: false,
|
|
66
|
+
isConnecting: false,
|
|
67
|
+
subscriptions: new Map(),
|
|
68
|
+
requestHandlers: new Map(),
|
|
69
|
+
requestCounter: 0,
|
|
70
|
+
reconnectTimeout: null,
|
|
71
|
+
reconnectAttempts: 0,
|
|
72
|
+
reconnectDelay: 1000,
|
|
73
|
+
lastReceived: 0,
|
|
74
|
+
isReconnecting: false,
|
|
75
|
+
maxReconnectAttempts: 5,
|
|
76
|
+
};
|
|
77
|
+
this.connections.set(key, state);
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Connect WebSocket if not already connected
|
|
83
|
+
*/
|
|
84
|
+
connectWebSocket(state) {
|
|
85
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
86
|
+
return; // Already connected
|
|
87
|
+
}
|
|
88
|
+
if (state.isConnecting || state.isReconnecting) {
|
|
89
|
+
return; // Already connecting
|
|
90
|
+
}
|
|
91
|
+
// Close existing connection if any
|
|
92
|
+
if (state.ws) {
|
|
93
|
+
state.ws.close();
|
|
94
|
+
state.ws = null;
|
|
95
|
+
}
|
|
96
|
+
state.isConnecting = true;
|
|
97
|
+
try {
|
|
98
|
+
const ws = new WebSocket(state.url);
|
|
99
|
+
state.ws = ws;
|
|
100
|
+
ws.onopen = () => {
|
|
101
|
+
state.lastReceived = Date.now();
|
|
102
|
+
state.isConnected = true;
|
|
103
|
+
state.isConnecting = false;
|
|
104
|
+
state.isReconnecting = false;
|
|
105
|
+
state.reconnectAttempts = 0;
|
|
106
|
+
state.reconnectDelay = 1000;
|
|
107
|
+
// Subscribe all existing subscriptions
|
|
108
|
+
state.subscriptions.forEach((handler) => {
|
|
109
|
+
this.subscribeOrder(state, handler.orderId, handler.auth, handler);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
ws.onmessage = (event) => {
|
|
113
|
+
state.lastReceived = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const response = JSON.parse(event.data);
|
|
116
|
+
if (response.jsonrpc !== '2.0' || typeof response.id !== 'number') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const handler = state.requestHandlers.get(response.id);
|
|
120
|
+
if (handler) {
|
|
121
|
+
handler(response.error, response.result);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (parseError) {
|
|
125
|
+
console.warn('Failed to parse WebSocket message:', parseError);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
ws.onerror = () => {
|
|
129
|
+
state.isConnecting = false;
|
|
130
|
+
// Error will be handled by onclose
|
|
131
|
+
};
|
|
132
|
+
ws.onclose = () => {
|
|
133
|
+
state.isConnected = false;
|
|
134
|
+
state.isConnecting = false;
|
|
135
|
+
state.requestHandlers.clear();
|
|
136
|
+
// Reconnect if we have active subscriptions
|
|
137
|
+
if (state.subscriptions.size > 0 && !state.isReconnecting) {
|
|
138
|
+
this.scheduleReconnect(state);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
state.isConnecting = false;
|
|
144
|
+
console.error('Failed to create WebSocket connection:', err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Schedule reconnection with exponential backoff
|
|
149
|
+
*/
|
|
150
|
+
scheduleReconnect(state) {
|
|
151
|
+
if (state.reconnectAttempts >= state.maxReconnectAttempts) {
|
|
152
|
+
// Notify all subscriptions of error
|
|
153
|
+
const error = new Error(`WebSocket connection failed after ${state.maxReconnectAttempts} attempts`);
|
|
154
|
+
state.subscriptions.forEach((handler) => {
|
|
155
|
+
handler.onError(error);
|
|
156
|
+
});
|
|
157
|
+
// Reset after delay to allow retry later
|
|
158
|
+
state.reconnectTimeout = setTimeout(() => {
|
|
159
|
+
state.reconnectAttempts = 0;
|
|
160
|
+
state.reconnectDelay = 1000;
|
|
161
|
+
}, 30000);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
state.isReconnecting = true;
|
|
165
|
+
state.reconnectAttempts += 1;
|
|
166
|
+
const currentDelay = state.reconnectDelay;
|
|
167
|
+
state.reconnectDelay = Math.min(currentDelay * 2, 30000);
|
|
168
|
+
const timeSinceLastReceived = Date.now() - state.lastReceived;
|
|
169
|
+
const extraDelay = timeSinceLastReceived > 90000 ? 5000 : 0;
|
|
170
|
+
const totalDelay = currentDelay + extraDelay;
|
|
171
|
+
state.reconnectTimeout = setTimeout(() => {
|
|
172
|
+
if (state.subscriptions.size > 0) {
|
|
173
|
+
state.isReconnecting = false;
|
|
174
|
+
this.connectWebSocket(state);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
state.isReconnecting = false;
|
|
178
|
+
state.reconnectAttempts = 0;
|
|
179
|
+
state.reconnectDelay = 1000;
|
|
180
|
+
}
|
|
181
|
+
}, totalDelay);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Subscribe to an order
|
|
185
|
+
*/
|
|
186
|
+
subscribeOrder(state, orderId, auth, handler) {
|
|
187
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const requestId = state.requestCounter++;
|
|
191
|
+
const request = {
|
|
192
|
+
jsonrpc: '2.0',
|
|
193
|
+
id: requestId,
|
|
194
|
+
method: 'connect',
|
|
195
|
+
params: {
|
|
196
|
+
auth: {
|
|
197
|
+
orderId,
|
|
198
|
+
viewingAuth: auth,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
// Set up handler for initial response and subsequent updates
|
|
203
|
+
// Note: The server sends multiple responses with the same request ID for status updates
|
|
204
|
+
state.requestHandlers.set(requestId, (error, result) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
const err = new Error(error.message || 'Connection error');
|
|
207
|
+
handler.onError(err);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Check if this is an initial OrderStatus or a StatusUpdate
|
|
211
|
+
// OrderStatus has a 'deposit' field, StatusUpdate has a 'type' field
|
|
212
|
+
const data = result;
|
|
213
|
+
if ('type' in data) {
|
|
214
|
+
// This is a StatusUpdate
|
|
215
|
+
handler.onStatusChange(data);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// This is an initial OrderStatus
|
|
219
|
+
handler.onStatusUpdate(data);
|
|
220
|
+
// Replace handler for subsequent status updates (reuse same request ID)
|
|
221
|
+
state.requestHandlers.set(requestId, (statusError, statusResult) => {
|
|
222
|
+
if (statusError) {
|
|
223
|
+
const err = new Error(statusError.message || 'Status error');
|
|
224
|
+
handler.onError(err);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Handle status update
|
|
228
|
+
const update = statusResult;
|
|
229
|
+
handler.onStatusChange(update);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
state.ws.send(JSON.stringify(request));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Subscribe to order tracking
|
|
237
|
+
*/
|
|
238
|
+
subscribe(client, orderId, auth, handlers) {
|
|
239
|
+
const state = this.getConnectionState(client);
|
|
240
|
+
const subscriptionKey = `${orderId}:${auth}`;
|
|
241
|
+
// Create subscription handler
|
|
242
|
+
const subscriptionHandler = {
|
|
243
|
+
orderId,
|
|
244
|
+
auth,
|
|
245
|
+
...handlers,
|
|
246
|
+
};
|
|
247
|
+
// Add subscription
|
|
248
|
+
state.subscriptions.set(subscriptionKey, subscriptionHandler);
|
|
249
|
+
// Connect if not connected
|
|
250
|
+
if (!state.isConnected && !state.isConnecting) {
|
|
251
|
+
this.connectWebSocket(state);
|
|
252
|
+
}
|
|
253
|
+
else if (state.isConnected) {
|
|
254
|
+
// Already connected, subscribe immediately
|
|
255
|
+
this.subscribeOrder(state, orderId, auth, subscriptionHandler);
|
|
256
|
+
}
|
|
257
|
+
// Return unsubscribe function
|
|
258
|
+
return () => {
|
|
259
|
+
state.subscriptions.delete(subscriptionKey);
|
|
260
|
+
// If no more subscriptions, close connection
|
|
261
|
+
if (state.subscriptions.size === 0) {
|
|
262
|
+
if (state.reconnectTimeout) {
|
|
263
|
+
clearTimeout(state.reconnectTimeout);
|
|
264
|
+
state.reconnectTimeout = null;
|
|
265
|
+
}
|
|
266
|
+
if (state.ws) {
|
|
267
|
+
state.ws.close();
|
|
268
|
+
state.ws = null;
|
|
269
|
+
}
|
|
270
|
+
state.isConnected = false;
|
|
271
|
+
state.isReconnecting = false;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get connection status
|
|
277
|
+
*/
|
|
278
|
+
isConnected(client) {
|
|
279
|
+
const state = this.getConnectionState(client);
|
|
280
|
+
return state.isConnected;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Singleton instance
|
|
284
|
+
export const orderTrackingWebSocketManager = new OrderTrackingWebSocketManager();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BridgeTransaction } from '@silentswap/sdk';
|
|
2
|
+
import type { SolanaTransactionExecutor } from '@silentswap/sdk';
|
|
3
|
+
import { type Signature, type Blockhash } from '@solana/kit';
|
|
4
|
+
import { createSolanaRpc } from '@solana/rpc';
|
|
5
|
+
/**
|
|
6
|
+
* Solana wallet connector interface
|
|
7
|
+
* Compatible with @solana/wallet-adapter-react and other Solana wallet adapters
|
|
8
|
+
*/
|
|
9
|
+
export interface SolanaWalletConnector {
|
|
10
|
+
signTransaction: (transaction: any) => Promise<any>;
|
|
11
|
+
sendTransaction: (transaction: any, connection: any) => Promise<string>;
|
|
12
|
+
publicKey: {
|
|
13
|
+
toString: () => string;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Solana connection interface
|
|
18
|
+
* Compatible with @solana/rpc RPC client
|
|
19
|
+
*/
|
|
20
|
+
export interface SolanaConnection {
|
|
21
|
+
getLatestBlockhash: (commitment?: string) => Promise<{
|
|
22
|
+
blockhash: Blockhash;
|
|
23
|
+
lastValidBlockHeight: bigint;
|
|
24
|
+
}>;
|
|
25
|
+
confirmTransaction: (config: {
|
|
26
|
+
signature: Signature;
|
|
27
|
+
blockhash: Blockhash;
|
|
28
|
+
lastValidBlockHeight: bigint;
|
|
29
|
+
}, commitment?: string) => Promise<{
|
|
30
|
+
value: {
|
|
31
|
+
err: any;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
rpc?: ReturnType<typeof createSolanaRpc>;
|
|
35
|
+
connection?: any;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a Solana transaction executor
|
|
39
|
+
* This function creates a transaction executor that can handle Solana transactions
|
|
40
|
+
* from relay.link bridge quotes
|
|
41
|
+
*
|
|
42
|
+
* @param connector - Solana wallet connector (from @solana/wallet-adapter-react or similar)
|
|
43
|
+
* @param connection - Solana RPC connection (from @solana/rpc)
|
|
44
|
+
* @returns Solana transaction executor function
|
|
45
|
+
*/
|
|
46
|
+
export declare function createSolanaTransactionExecutor(connector: SolanaWalletConnector, connection: SolanaConnection): SolanaTransactionExecutor;
|
|
47
|
+
/**
|
|
48
|
+
* Helper to convert relay.link Solana step data to BridgeTransaction
|
|
49
|
+
*/
|
|
50
|
+
export declare function convertRelaySolanaStepToTransaction(stepData: {
|
|
51
|
+
instructions: Array<{
|
|
52
|
+
programId: string;
|
|
53
|
+
keys: Array<{
|
|
54
|
+
pubkey: string;
|
|
55
|
+
isSigner: boolean;
|
|
56
|
+
isWritable: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
data: string;
|
|
59
|
+
}>;
|
|
60
|
+
}, feePayer: string, chainId: number): BridgeTransaction;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { address, appendTransactionMessageInstructions, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, compileTransaction, getBase64EncodedWireTransaction, getTransactionDecoder, getCompiledTransactionMessageDecoder, decompileTransactionMessage, pipe, AccountRole, } from '@solana/kit';
|
|
2
|
+
import { createSolanaRpc } from '@solana/rpc';
|
|
3
|
+
import { VersionedTransaction } from '@solana/web3.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a Solana transaction executor
|
|
6
|
+
* This function creates a transaction executor that can handle Solana transactions
|
|
7
|
+
* from relay.link bridge quotes
|
|
8
|
+
*
|
|
9
|
+
* @param connector - Solana wallet connector (from @solana/wallet-adapter-react or similar)
|
|
10
|
+
* @param connection - Solana RPC connection (from @solana/rpc)
|
|
11
|
+
* @returns Solana transaction executor function
|
|
12
|
+
*/
|
|
13
|
+
export function createSolanaTransactionExecutor(connector, connection) {
|
|
14
|
+
return async (tx) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!connector.publicKey) {
|
|
17
|
+
throw new Error('Solana wallet not connected');
|
|
18
|
+
}
|
|
19
|
+
console.log('Creating Solana transaction:', {
|
|
20
|
+
hasData: !!tx.data,
|
|
21
|
+
hasInstructions: !!tx.instructions,
|
|
22
|
+
instructionsCount: tx.instructions?.length,
|
|
23
|
+
feePayer: tx.feePayer,
|
|
24
|
+
publicKey: connector.publicKey.toString(),
|
|
25
|
+
});
|
|
26
|
+
// Get RPC client from connection or create a default one
|
|
27
|
+
const rpc = connection.rpc || createSolanaRpc('https://api.mainnet-beta.solana.com');
|
|
28
|
+
let transactionMessage;
|
|
29
|
+
let blockhash;
|
|
30
|
+
let lastValidBlockHeight;
|
|
31
|
+
// Check if transaction has serialized data (from deBridge)
|
|
32
|
+
// deBridge returns Solana transactions as serialized hex strings in tx.data
|
|
33
|
+
if (tx.data && !tx.instructions && !tx.to) {
|
|
34
|
+
// Deserialize transaction from hex data
|
|
35
|
+
// Remove 0x prefix if present
|
|
36
|
+
const hexData = tx.data.replace(/^0x/, '');
|
|
37
|
+
const buffer = Buffer.from(hexData, 'hex');
|
|
38
|
+
// Decode transaction using @solana/kit
|
|
39
|
+
const decoded = getTransactionDecoder().decode(new Uint8Array(buffer));
|
|
40
|
+
// Extract blockhash from decoded transaction
|
|
41
|
+
const compiledMessage = getCompiledTransactionMessageDecoder().decode(decoded.messageBytes);
|
|
42
|
+
transactionMessage = decompileTransactionMessage(compiledMessage);
|
|
43
|
+
// Get blockhash from the decoded transaction's lifetime token
|
|
44
|
+
// In @solana/kit v5, lifetime is stored as lifetimeToken (blockhash string)
|
|
45
|
+
if ('lifetimeToken' in compiledMessage) {
|
|
46
|
+
blockhash = compiledMessage.lifetimeToken;
|
|
47
|
+
// For deserialized transactions, we don't have lastValidBlockHeight
|
|
48
|
+
// We'll need to fetch it from RPC
|
|
49
|
+
const blockhashResponse = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
|
|
50
|
+
lastValidBlockHeight = blockhashResponse.value.lastValidBlockHeight;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (tx.instructions) {
|
|
54
|
+
// Transaction has instructions (from relay.link)
|
|
55
|
+
// Get fee payer from transaction or use connector's public key
|
|
56
|
+
const feePayer = tx.feePayer || connector.publicKey.toString();
|
|
57
|
+
const feePayerAddress = address(feePayer);
|
|
58
|
+
// Get latest blockhash using RPC
|
|
59
|
+
const blockhashResponse = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
|
|
60
|
+
const latestBlockhash = {
|
|
61
|
+
blockhash: blockhashResponse.value.blockhash,
|
|
62
|
+
lastValidBlockHeight: blockhashResponse.value.lastValidBlockHeight,
|
|
63
|
+
};
|
|
64
|
+
blockhash = latestBlockhash.blockhash;
|
|
65
|
+
lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
|
|
66
|
+
console.log('Got blockhash:', {
|
|
67
|
+
blockhash,
|
|
68
|
+
lastValidBlockHeight: lastValidBlockHeight.toString(),
|
|
69
|
+
});
|
|
70
|
+
// Convert relay.link instructions to @solana/kit Instruction format
|
|
71
|
+
// @solana/kit expects: { programAddress, accounts, data }
|
|
72
|
+
// where accounts is array of { address, role: AccountRole }
|
|
73
|
+
console.log('Converting instructions:', tx.instructions.length);
|
|
74
|
+
const instructions = tx.instructions.map((inst, instIndex) => {
|
|
75
|
+
try {
|
|
76
|
+
// Log original values for debugging
|
|
77
|
+
console.log(`Instruction ${instIndex}:`, {
|
|
78
|
+
programId: inst.programId,
|
|
79
|
+
programIdLength: inst.programId?.length,
|
|
80
|
+
keysCount: inst.keys?.length,
|
|
81
|
+
});
|
|
82
|
+
// Validate and convert programId to base58 address
|
|
83
|
+
if (!inst.programId || typeof inst.programId !== 'string') {
|
|
84
|
+
throw new Error(`Invalid programId in instruction ${instIndex}: ${inst.programId}`);
|
|
85
|
+
}
|
|
86
|
+
const programIdAddress = address(inst.programId);
|
|
87
|
+
// Validate and convert all pubkey addresses in keys
|
|
88
|
+
// @solana/kit expects AccountMeta with { address, role: AccountRole }
|
|
89
|
+
const accounts = inst.keys.map((key, keyIndex) => {
|
|
90
|
+
console.log('Key:', key);
|
|
91
|
+
if (!key.pubkey || typeof key.pubkey !== 'string') {
|
|
92
|
+
throw new Error(`Invalid pubkey in instruction ${instIndex}, key ${keyIndex}: ${key.pubkey}`);
|
|
93
|
+
}
|
|
94
|
+
// Log original pubkey for debugging
|
|
95
|
+
if (key.pubkey.length < 32 || key.pubkey.length > 44) {
|
|
96
|
+
console.warn(`Suspicious pubkey length in instruction ${instIndex}, key ${keyIndex}:`, {
|
|
97
|
+
pubkey: key.pubkey,
|
|
98
|
+
length: key.pubkey.length,
|
|
99
|
+
isSigner: key.isSigner,
|
|
100
|
+
isWritable: key.isWritable,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const validatedAddress = address(key.pubkey);
|
|
105
|
+
// @solana/kit AccountRole enum:
|
|
106
|
+
// WRITABLE_SIGNER = 3, READONLY_SIGNER = 2, WRITABLE = 1, READONLY = 0
|
|
107
|
+
let role;
|
|
108
|
+
if (key.isSigner && key.isWritable) {
|
|
109
|
+
role = AccountRole.WRITABLE_SIGNER;
|
|
110
|
+
}
|
|
111
|
+
else if (key.isSigner && !key.isWritable) {
|
|
112
|
+
role = AccountRole.READONLY_SIGNER;
|
|
113
|
+
}
|
|
114
|
+
else if (!key.isSigner && key.isWritable) {
|
|
115
|
+
role = AccountRole.WRITABLE;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
role = AccountRole.READONLY;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
address: validatedAddress,
|
|
122
|
+
role,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new Error(`Failed to validate pubkey in instruction ${instIndex}, key ${keyIndex}: ${key.pubkey}. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// Convert data to Uint8Array
|
|
130
|
+
const instructionData = inst.data.startsWith('0x')
|
|
131
|
+
? new Uint8Array(Buffer.from(inst.data.slice(2), 'hex'))
|
|
132
|
+
: new Uint8Array(Buffer.from(inst.data, 'hex'));
|
|
133
|
+
// @solana/kit Instruction format: { programAddress, accounts, data }
|
|
134
|
+
return {
|
|
135
|
+
programAddress: programIdAddress,
|
|
136
|
+
accounts,
|
|
137
|
+
data: instructionData,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error(`Error converting instruction ${instIndex}:`, {
|
|
142
|
+
instruction: inst,
|
|
143
|
+
error: error instanceof Error ? error.message : String(error),
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Build transaction message using pipe pattern (following @solana/kit best practices)
|
|
149
|
+
transactionMessage = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayer(feePayerAddress, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions(instructions, tx));
|
|
150
|
+
console.log('Transaction message built successfully');
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
throw new Error('Solana transaction must have either instructions or serialized data');
|
|
154
|
+
}
|
|
155
|
+
// Compile transaction message to get wire format
|
|
156
|
+
console.log('Compiling transaction...');
|
|
157
|
+
const compiledTransaction = compileTransaction(transactionMessage);
|
|
158
|
+
// Convert to VersionedTransaction for wallet adapter signing
|
|
159
|
+
const base64WireTransaction = getBase64EncodedWireTransaction(compiledTransaction);
|
|
160
|
+
const transactionBytes = Buffer.from(base64WireTransaction, 'base64');
|
|
161
|
+
console.log('Transaction bytes length:', transactionBytes.length);
|
|
162
|
+
// Create VersionedTransaction from wire bytes
|
|
163
|
+
const versionedTransaction = VersionedTransaction.deserialize(transactionBytes);
|
|
164
|
+
console.log('Requesting signature...');
|
|
165
|
+
// Sign transaction with wallet adapter
|
|
166
|
+
const signedTransaction = await connector.signTransaction(versionedTransaction);
|
|
167
|
+
console.log('Transaction signed');
|
|
168
|
+
// Send transaction using RPC directly (matching reference implementation pattern)
|
|
169
|
+
// Convert signed transaction to base64 for RPC
|
|
170
|
+
const signedTransactionBytes = signedTransaction.serialize();
|
|
171
|
+
const signedTransactionBase64 = Buffer.from(signedTransactionBytes).toString('base64'); // Type assertion for Base64EncodedWireTransaction
|
|
172
|
+
console.log('Sending transaction via RPC...');
|
|
173
|
+
// Use RPC sendTransaction with base64 encoding (matching reference implementation)
|
|
174
|
+
const signatureResponse = await rpc
|
|
175
|
+
.sendTransaction(signedTransactionBase64, {
|
|
176
|
+
encoding: 'base64',
|
|
177
|
+
skipPreflight: true,
|
|
178
|
+
preflightCommitment: 'confirmed',
|
|
179
|
+
})
|
|
180
|
+
.send();
|
|
181
|
+
const signatureString = signatureResponse;
|
|
182
|
+
console.log('Transaction sent:', signatureString);
|
|
183
|
+
// Wait for confirmation
|
|
184
|
+
// Use the adapter's confirmTransaction method
|
|
185
|
+
// Only confirm if we have blockhash and lastValidBlockHeight
|
|
186
|
+
let result;
|
|
187
|
+
if (blockhash && lastValidBlockHeight) {
|
|
188
|
+
console.log('Waiting for confirmation...');
|
|
189
|
+
result = await connection.confirmTransaction({
|
|
190
|
+
signature: signatureString,
|
|
191
|
+
blockhash,
|
|
192
|
+
lastValidBlockHeight,
|
|
193
|
+
}, 'confirmed');
|
|
194
|
+
console.log('Transaction confirmed');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// For transactions without blockhash info, return success
|
|
198
|
+
// The transaction will be confirmed by the network
|
|
199
|
+
console.log('No blockhash/lastValidBlockHeight, skipping confirmation');
|
|
200
|
+
result = {
|
|
201
|
+
value: {
|
|
202
|
+
err: null,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Check for errors
|
|
207
|
+
if (result.value.err) {
|
|
208
|
+
console.error('Transaction failed:', result.value.err);
|
|
209
|
+
throw new Error(`Solana transaction failed: ${result.value.err.toString()}`);
|
|
210
|
+
}
|
|
211
|
+
console.log('Transaction completed successfully:', signatureString);
|
|
212
|
+
return signatureString;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Solana transaction execution failed:', {
|
|
216
|
+
error: error instanceof Error ? error.message : String(error),
|
|
217
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
218
|
+
});
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Helper to convert relay.link Solana step data to BridgeTransaction
|
|
225
|
+
*/
|
|
226
|
+
export function convertRelaySolanaStepToTransaction(stepData, feePayer, chainId) {
|
|
227
|
+
return {
|
|
228
|
+
chainId,
|
|
229
|
+
feePayer,
|
|
230
|
+
instructions: stepData.instructions.map((inst) => ({
|
|
231
|
+
programId: inst.programId,
|
|
232
|
+
keys: inst.keys,
|
|
233
|
+
data: inst.data.startsWith('0x') ? inst.data : `0x${inst.data}`,
|
|
234
|
+
})),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createEip712DocForOrder, createEip712DocForWalletGeneration, type QuoteResponse, type SignInMessage, type AuthResponse } from '@silentswap/sdk';
|
|
2
|
+
import type { SilentSwapClient } from '@silentswap/sdk';
|
|
3
|
+
import type { WalletClient } from 'viem';
|
|
4
|
+
import type { Connector } from 'wagmi';
|
|
5
|
+
export interface useAuthOptions {
|
|
6
|
+
/** SilentSwap client instance */
|
|
7
|
+
client?: SilentSwapClient;
|
|
8
|
+
/** User's EVM address */
|
|
9
|
+
address?: `0x${string}`;
|
|
10
|
+
/** Wallet client for signing operations */
|
|
11
|
+
walletClient?: WalletClient;
|
|
12
|
+
/** Wagmi connector */
|
|
13
|
+
connector?: Connector;
|
|
14
|
+
/** Domain for SIWE message (defaults to window.location.host) */
|
|
15
|
+
domain?: string;
|
|
16
|
+
/** Whether to auto-authenticate when dependencies are available */
|
|
17
|
+
autoAuthenticate?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface useAuthReturn {
|
|
20
|
+
auth: AuthResponse | null;
|
|
21
|
+
nonce: string | null;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
createSignInMessage: (address: `0x${string}`, nonce: string, domain?: string) => SignInMessage;
|
|
25
|
+
createEip712DocForOrder: (quoteResponse: QuoteResponse) => ReturnType<typeof createEip712DocForOrder>;
|
|
26
|
+
createEip712DocForWalletGeneration: (scope: string, token: string) => ReturnType<typeof createEip712DocForWalletGeneration>;
|
|
27
|
+
getNonce: () => Promise<string>;
|
|
28
|
+
authenticate: () => Promise<AuthResponse | null>;
|
|
29
|
+
signIn: () => Promise<AuthResponse | null>;
|
|
30
|
+
signOut: () => void;
|
|
31
|
+
isAuthenticated: () => boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* React hook for SilentSwap authentication with full SIWE flow
|
|
35
|
+
*
|
|
36
|
+
* This hook provides:
|
|
37
|
+
* - SIWE authentication with nonce management
|
|
38
|
+
* - Automatic auth caching and restoration
|
|
39
|
+
* - Sign-in message creation and signing
|
|
40
|
+
* - Auth state management
|
|
41
|
+
* - Auto-authentication when dependencies are available
|
|
42
|
+
*
|
|
43
|
+
* @param options - Configuration options for authentication
|
|
44
|
+
* @returns Object with authentication state and methods
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import { useAuth, useSilentClient } from '@silentswap/react';
|
|
49
|
+
* import { useWalletClient, useAccount } from 'wagmi';
|
|
50
|
+
*
|
|
51
|
+
* function MyComponent() {
|
|
52
|
+
* const { address } = useAccount();
|
|
53
|
+
* const { data: walletClient } = useWalletClient();
|
|
54
|
+
* const client = useSilentClient({ config: { /* ... *\/ } }).client;
|
|
55
|
+
*
|
|
56
|
+
* const {
|
|
57
|
+
* auth,
|
|
58
|
+
* isAuthenticated,
|
|
59
|
+
* signIn,
|
|
60
|
+
* signOut,
|
|
61
|
+
* isLoading,
|
|
62
|
+
* error
|
|
63
|
+
* } = useAuth({
|
|
64
|
+
* client,
|
|
65
|
+
* address,
|
|
66
|
+
* walletClient: walletClient as any,
|
|
67
|
+
* autoAuthenticate: true,
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* return (
|
|
71
|
+
* <div>
|
|
72
|
+
* {isLoading && <div>Authenticating...</div>}
|
|
73
|
+
* {error && <div>Error: {error.message}</div>}
|
|
74
|
+
*
|
|
75
|
+
* {isAuthenticated() ? (
|
|
76
|
+
* <div>
|
|
77
|
+
* <p>Authenticated as {auth?.address}</p>
|
|
78
|
+
* <button onClick={signOut}>Sign Out</button>
|
|
79
|
+
* </div>
|
|
80
|
+
* ) : (
|
|
81
|
+
* <button onClick={signIn} disabled={isLoading}>
|
|
82
|
+
* Sign In with Ethereum
|
|
83
|
+
* </button>
|
|
84
|
+
* )}
|
|
85
|
+
* </div>
|
|
86
|
+
* );
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function useAuth({ client, address, walletClient, domain, autoAuthenticate, }?: useAuthOptions): useAuthReturn;
|