@silentswap/react 0.0.52 → 0.0.55

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.
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useState } from 'react';
2
- import { isSolanaAsset, parseEvmCaip19, S_CAIP19_USDC_AVALANCHE, getAssetByCaip19, solveOptimalUsdcAmount, N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, isSolanaNativeToken, parseSolanaCaip19, EVM_PHONY_ADDRESS, isValidSolanaAddress, getAddressFromCaip10, S0X_ADDR_USDC_AVALANCHE, isEvmNativeToken, caip19FungibleEvmToken, FacilitatorKeyType, createHdFacilitatorGroupFromEntropy, PublicKeyArgGroups, SB58_CHAIN_ID_SOLANA_MAINNET, caip19SplToken, DeliveryMethod, X_MAX_IMPACT_PERCENT, } from '@silentswap/sdk';
2
+ import { isSolanaAsset, isBitcoinAsset, parseEvmCaip19, S_CAIP19_USDC_AVALANCHE, getAssetByCaip19, solveOptimalUsdcAmount, fetchRelayQuote, N_RELAY_CHAIN_ID_SOLANA, N_RELAY_CHAIN_ID_BITCOIN, SB58_ADDR_SOL_PROGRAM_SYSTEM, isSolanaNativeToken, parseSolanaCaip19, EVM_PHONY_ADDRESS, isValidSolanaAddress, isValidBitcoinAddress, isValidEvmAddress, getAddressFromCaip10, S0X_ADDR_USDC_AVALANCHE, isEvmNativeToken, caip19FungibleEvmToken, FacilitatorKeyType, createHdFacilitatorGroupFromEntropy, PublicKeyArgGroups, SB58_CHAIN_ID_SOLANA_MAINNET, caip19SplToken, DeliveryMethod, X_MAX_IMPACT_PERCENT, NI_CHAIN_ID_AVALANCHE, SBTC_ADDR_BITCOIN_NATIVE, } from '@silentswap/sdk';
3
3
  import { getAddress } from 'viem';
4
4
  import { BigNumber } from 'bignumber.js';
5
5
  /**
@@ -28,6 +28,7 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
28
28
  }
29
29
  const isSourceUsdcAvalanche = debouncedSourceAsset === S_CAIP19_USDC_AVALANCHE;
30
30
  const isSourceSolana = isSolanaAsset(debouncedSourceAsset);
31
+ const isSourceBitcoin = isBitcoinAsset(debouncedSourceAsset);
31
32
  let usdcAmountOut;
32
33
  let bridgeProviderResult = 'none';
33
34
  let allowanceTargetResult = undefined;
@@ -39,8 +40,7 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
39
40
  usdcAmountOut = sourceAmountBN.shiftedBy(assetInfo.decimals).toFixed(0);
40
41
  }
41
42
  else if (isSourceSolana) {
42
- // Solana assets - both relay.link and deBridge support Solana
43
- // solveOptimalUsdcAmount will compare both providers and choose the best rate
43
+ // Solana assets - use relay.link only (not deBridge)
44
44
  const assetInfo = getAssetByCaip19(debouncedSourceAsset);
45
45
  if (!assetInfo)
46
46
  throw new Error(`Solana asset not found`);
@@ -69,21 +69,68 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
69
69
  if (!solanaAddress) {
70
70
  throw new Error('Solana address required for Solana swaps');
71
71
  }
72
- // Pass Solana address for user parameter, and EVM address for recipient and deposit calldata
73
- // This matches Svelte behavior where s_addr_sender (Solana) is used for user, and s0x_signer (EVM) for recipient
74
- const solveResult = await solveOptimalUsdcAmount(N_RELAY_CHAIN_ID_SOLANA, originCurrency, // Base58 Solana address (system program or SPL token)
75
- sourceAmountInUnits, solanaAddress, // Pass Solana address for user parameter in relay quote
76
- undefined, // depositCalldata will be created with EVM address
77
- X_MAX_IMPACT_PERCENT, depositorAddress, // Required depositor address
78
- getAddress(evmAddress));
72
+ // Use relay.link only for Solana (not deBridge)
73
+ const relayQuote = await fetchRelayQuote({
74
+ user: solanaAddress, // Solana address for user parameter
75
+ originChainId: N_RELAY_CHAIN_ID_SOLANA,
76
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
77
+ originCurrency: originCurrency, // Base58 Solana address (system program or SPL token)
78
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
79
+ amount: sourceAmountInUnits,
80
+ tradeType: 'EXACT_INPUT',
81
+ referrer: 'silentswap',
82
+ recipient: getAddress(evmAddress), // EVM address for recipient
83
+ }, abortController.signal);
79
84
  if (abortController.signal.aborted) {
80
85
  setLoadingAmounts(false);
81
86
  return null;
82
87
  }
83
- // solveOptimalUsdcAmount compares both relay and deBridge and returns the best provider
84
- usdcAmountOut = solveResult.usdcAmountOut.toString();
85
- bridgeProviderResult = solveResult.provider; // Use the provider chosen by solveOptimalUsdcAmount
86
- allowanceTargetResult = solveResult.allowanceTarget; // deBridge may return allowance target
88
+ // Extract USDC amount from relay quote
89
+ usdcAmountOut = relayQuote.details.currencyOut.amount;
90
+ bridgeProviderResult = 'relay'; // Solana uses relay only
91
+ allowanceTargetResult = undefined; // Relay doesn't need allowance target
92
+ }
93
+ else if (isSourceBitcoin) {
94
+ // Bitcoin assets - only relay.link supports Bitcoin
95
+ const assetInfo = getAssetByCaip19(debouncedSourceAsset);
96
+ if (!assetInfo)
97
+ throw new Error(`Bitcoin asset not found`);
98
+ const sourceAmountBN = BigNumber(debouncedSourceAmount);
99
+ const sourceAmountInUnits = sourceAmountBN.shiftedBy(assetInfo.decimals).toFixed(0);
100
+ // For Bitcoin swaps, we need:
101
+ // - Bitcoin address for the 'user' parameter in relay quote
102
+ // - EVM address for the 'recipient' parameter
103
+ // - EVM address for deposit calldata
104
+ if (!evmAddress) {
105
+ throw new Error('EVM address required for Bitcoin swaps (needed for deposit calldata and recipient)');
106
+ }
107
+ // Bitcoin address should be a string that doesn't start with 0x
108
+ // For now, we'll use the address parameter if it's not an EVM address
109
+ const bitcoinAddress = typeof address === 'string' && !address.startsWith('0x') ? address : null;
110
+ if (!bitcoinAddress) {
111
+ throw new Error('Bitcoin address required for Bitcoin swaps');
112
+ }
113
+ // Bitcoin only supports relay, not deBridge
114
+ // Use relay.link directly (not solveOptimalUsdcAmount which compares both providers)
115
+ const relayQuote = await fetchRelayQuote({
116
+ user: bitcoinAddress, // Bitcoin address for user parameter
117
+ originChainId: N_RELAY_CHAIN_ID_BITCOIN,
118
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
119
+ originCurrency: SBTC_ADDR_BITCOIN_NATIVE, // Bitcoin native token (BTC) - relay.link requires this specific address
120
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
121
+ amount: sourceAmountInUnits,
122
+ tradeType: 'EXACT_INPUT',
123
+ referrer: 'silentswap',
124
+ recipient: getAddress(evmAddress), // EVM address for recipient
125
+ }, abortController.signal);
126
+ if (abortController.signal.aborted) {
127
+ setLoadingAmounts(false);
128
+ return null;
129
+ }
130
+ // Extract USDC amount from relay quote
131
+ usdcAmountOut = relayQuote.details.currencyOut.amount;
132
+ bridgeProviderResult = 'relay'; // Bitcoin only supports relay
133
+ allowanceTargetResult = undefined; // Bitcoin doesn't need allowance
87
134
  }
88
135
  else {
89
136
  // EVM assets
@@ -136,8 +183,9 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
136
183
  });
137
184
  const outputs = destinations.map((dest, idx) => {
138
185
  const isDestSolana = isSolanaAsset(dest.asset);
186
+ const isDestBitcoin = isBitcoinAsset(dest.asset);
139
187
  let asset;
140
- let recipientAddress; // Changed from `0x${string}` to string to support Solana addresses
188
+ let recipientAddress; // Changed from `0x${string}` to string to support Solana and Bitcoin addresses
141
189
  if (isDestSolana) {
142
190
  // Solana destination
143
191
  const destParsed = parseSolanaCaip19(dest.asset);
@@ -172,33 +220,78 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
172
220
  throw new Error(`Solana destination ${idx} requires a recipient address in the contact field`);
173
221
  }
174
222
  }
223
+ else if (isDestBitcoin) {
224
+ // Bitcoin destination
225
+ // Bitcoin assets use the asset CAIP-19 as-is (native Bitcoin only supported currently)
226
+ asset = dest.asset;
227
+ // Extract Bitcoin address from contact field
228
+ // CRITICAL: Must be a valid Bitcoin address (bc1, 1, or 3 prefix)
229
+ if (dest.contact) {
230
+ // Check if contact is in CAIP10 format
231
+ if (dest.contact.startsWith('caip10:bip122:')) {
232
+ recipientAddress = getAddressFromCaip10(dest.contact);
233
+ // Validate it's actually a Bitcoin address
234
+ if (!isValidBitcoinAddress(recipientAddress)) {
235
+ throw new Error(`Invalid Bitcoin address extracted from CAIP10: ${dest.contact} -> ${recipientAddress}`);
236
+ }
237
+ }
238
+ else {
239
+ // Check if contact is a valid Bitcoin address format
240
+ if (isValidBitcoinAddress(dest.contact)) {
241
+ recipientAddress = dest.contact;
242
+ }
243
+ else {
244
+ throw new Error(`Invalid Bitcoin recipient address for destination ${idx}: ${dest.contact}. Expected Bitcoin address (bc1, 1, or 3 prefix) or caip10:bip122:*: format.`);
245
+ }
246
+ }
247
+ }
248
+ else {
249
+ throw new Error(`Bitcoin destination ${idx} requires a recipient address in the contact field`);
250
+ }
251
+ }
175
252
  else {
176
253
  // EVM destination
177
254
  const destParsed = parseEvmCaip19(dest.asset);
178
- if (!destParsed)
179
- throw new Error(`Invalid destination asset format`);
255
+ if (!destParsed) {
256
+ throw new Error(`Failed to parse EVM destination CAIP-19: ${dest.asset}. Expected EVM asset format (eip155:chainId/erc20:address or eip155:chainId/slip44:cointype).`);
257
+ }
180
258
  const isNative = isEvmNativeToken(dest.asset);
181
259
  const destChainId = destParsed.chainId;
182
260
  asset = isNative
183
261
  ? dest.asset
184
262
  : caip19FungibleEvmToken(destChainId, destParsed.tokenAddress);
263
+ // Extract EVM address from contact field
264
+ // CRITICAL: Must be a valid EVM address (0x prefix)
185
265
  if (dest.contact) {
186
- try {
187
- // Extract EVM address from contact (handles both CAIP10 and plain addresses)
188
- if (dest.contact.startsWith('caip10:eip155:')) {
189
- recipientAddress = getAddressFromCaip10(dest.contact);
266
+ // Check if contact is in CAIP10 format
267
+ if (dest.contact.startsWith('caip10:eip155:')) {
268
+ recipientAddress = getAddressFromCaip10(dest.contact);
269
+ // Validate it's actually an EVM address
270
+ if (!isValidEvmAddress(recipientAddress)) {
271
+ throw new Error(`Invalid EVM address extracted from CAIP10: ${dest.contact} -> ${recipientAddress}`);
190
272
  }
191
- else {
192
- recipientAddress = getAddress(dest.contact);
273
+ // Normalize to checksummed format
274
+ try {
275
+ recipientAddress = getAddress(recipientAddress);
276
+ }
277
+ catch (error) {
278
+ throw new Error(`Invalid EVM address checksum for destination ${idx}: ${recipientAddress}. ${error instanceof Error ? error.message : String(error)}`);
193
279
  }
194
280
  }
195
- catch (error) {
196
- console.error('Failed to parse EVM recipient address:', {
197
- destination: idx,
198
- contact: dest.contact,
199
- error: error instanceof Error ? error.message : String(error),
200
- });
201
- throw new Error(`Invalid EVM recipient address for destination ${idx}: ${dest.contact}. ${error instanceof Error ? error.message : String(error)}`);
281
+ else {
282
+ // Check if contact is a valid EVM address format
283
+ if (isValidEvmAddress(dest.contact)) {
284
+ // Normalize to checksummed format
285
+ try {
286
+ recipientAddress = getAddress(dest.contact);
287
+ }
288
+ catch (error) {
289
+ throw new Error(`Invalid EVM address checksum for destination ${idx}: ${dest.contact}. ${error instanceof Error ? error.message : String(error)}`);
290
+ }
291
+ }
292
+ else {
293
+ throw new Error(`Invalid EVM recipient address for destination ${idx}: ${dest.contact}. Expected 0x-prefixed EVM address or caip10:eip155:*: format.`);
294
+ }
202
295
  }
203
296
  }
204
297
  else {
@@ -209,12 +302,21 @@ export function useQuoteCalculation({ address, evmAddress, wallet, depositorAddr
209
302
  // The value field in the quote request should be USDC micro units, not the destination asset amount
210
303
  // This matches how Svelte calculates a_amounts_out and uses it as value in the quote request
211
304
  const outputValue = splitUsdcAmounts[idx];
212
- // Final validation: Ensure Solana destinations have Solana addresses, EVM destinations have EVM addresses
305
+ // Final validation: Ensure addresses match their chain type
213
306
  if (isDestSolana && !isValidSolanaAddress(recipientAddress)) {
214
307
  throw new Error(`Invalid recipient address type for Solana destination ${idx}: ${recipientAddress}. Expected base58 Solana address.`);
215
308
  }
216
- else if (!isDestSolana && !recipientAddress.startsWith('0x')) {
217
- throw new Error(`Invalid recipient address type for EVM destination ${idx}: ${recipientAddress}. Expected 0x-prefixed EVM address.`);
309
+ else if (isDestBitcoin) {
310
+ // Validate Bitcoin address format
311
+ if (!isValidBitcoinAddress(recipientAddress)) {
312
+ throw new Error(`Invalid recipient address type for Bitcoin destination ${idx}: ${recipientAddress}. Expected Bitcoin address (bc1, 1, or 3 prefix).`);
313
+ }
314
+ }
315
+ else if (!isDestSolana && !isDestBitcoin) {
316
+ // Validate EVM address format
317
+ if (!isValidEvmAddress(recipientAddress)) {
318
+ throw new Error(`Invalid recipient address type for EVM destination ${idx}: ${recipientAddress}. Expected 0x-prefixed EVM address.`);
319
+ }
218
320
  }
219
321
  return {
220
322
  method: DeliveryMethod.SNIP,
@@ -2,6 +2,7 @@ import type { SilentSwapClient, QuoteResponse, AssetInfo } from '@silentswap/sdk
2
2
  import type { WalletClient } from 'viem';
3
3
  import type { Connector } from 'wagmi';
4
4
  import type { SolanaWalletConnector, SolanaConnection } from './solana-transaction.js';
5
+ import type { BitcoinWalletConnector, BitcoinConnection } from './bitcoin-transaction.js';
5
6
  import { type SwapTransaction } from '../useTransaction.js';
6
7
  import type { SilentSwapWallet } from './useWallet.js';
7
8
  export type { SwapTransaction };
@@ -40,6 +41,10 @@ export interface useSilentQuoteOptions {
40
41
  wallet: SilentSwapWallet | null;
41
42
  /** Whether wallet is currently being generated (to prevent swap execution before wallet is ready) */
42
43
  walletLoading?: boolean;
44
+ /** Wallet generation error (if previous generation failed) */
45
+ walletError?: Error | null;
46
+ /** Function to generate wallet (for retry on swap execution) */
47
+ generateWallet?: () => Promise<void>;
43
48
  /** Facilitator group for the swap, or a function that resolves it */
44
49
  /** Status update callback */
45
50
  onStatus?: (status: string) => void;
@@ -49,6 +54,10 @@ export interface useSilentQuoteOptions {
49
54
  solanaConnection?: SolanaConnection;
50
55
  /** Solana RPC URL (optional, will create connection if not provided) */
51
56
  solanaRpcUrl?: string;
57
+ /** Bitcoin wallet connector (required for Bitcoin swaps) */
58
+ bitcoinConnector?: BitcoinWalletConnector;
59
+ /** Bitcoin connection (optional, for consistency) */
60
+ bitcoinConnection?: BitcoinConnection;
52
61
  /** Get price function */
53
62
  getPrice: (asset: AssetInfo) => Promise<number>;
54
63
  setDestinations: (updater: Destination[] | ((prev: Destination[]) => Destination[])) => void;
@@ -87,4 +96,4 @@ export interface ExecuteSwapParams {
87
96
  /** Optional integrator ID for tracking */
88
97
  integratorId?: string;
89
98
  }
90
- export declare function useSilentQuote({ client, address, evmAddress, solAddress, walletClient, connector, wallet, walletLoading, onStatus, solanaConnector, solanaConnection, solanaRpcUrl, getPrice, setDestinations, }: useSilentQuoteOptions): useSilentQuoteReturn;
99
+ export declare function useSilentQuote({ client, address, evmAddress, solAddress, walletClient, connector, wallet, walletLoading, walletError, generateWallet, onStatus, solanaConnector, solanaConnection, solanaRpcUrl, bitcoinConnector, bitcoinConnection, getPrice, setDestinations, }: useSilentQuoteOptions): useSilentQuoteReturn;
@@ -1,5 +1,5 @@
1
- import { useCallback, useMemo, useState } from 'react';
2
- import { hexToBase58, isSolanaAsset, parseEvmCaip19, S_CAIP19_USDC_AVALANCHE, NI_CHAIN_ID_AVALANCHE, getAssetByCaip19, } from '@silentswap/sdk';
1
+ import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2
+ import { hexToBase58, isSolanaAsset, isBitcoinAsset, parseEvmCaip19, S_CAIP19_USDC_AVALANCHE, NI_CHAIN_ID_AVALANCHE, getAssetByCaip19, } from '@silentswap/sdk';
3
3
  import { getAddress } from 'viem';
4
4
  import { BigNumber } from 'bignumber.js';
5
5
  import { useQuoteFetching } from './useQuoteFetching.js';
@@ -7,7 +7,7 @@ import { useOrderSigning } from './useOrderSigning.js';
7
7
  import { useBridgeExecution } from './useBridgeExecution.js';
8
8
  import { useTransaction } from '../useTransaction.js';
9
9
  import { useQuoteCalculation } from './useQuoteCalculation.js';
10
- export function useSilentQuote({ client, address, evmAddress, solAddress, walletClient, connector, wallet, walletLoading = false, onStatus, solanaConnector, solanaConnection, solanaRpcUrl, getPrice, setDestinations, }) {
10
+ export function useSilentQuote({ client, address, evmAddress, solAddress, walletClient, connector, wallet, walletLoading = false, walletError = null, generateWallet, onStatus, solanaConnector, solanaConnection, solanaRpcUrl, bitcoinConnector, bitcoinConnection, getPrice, setDestinations, }) {
11
11
  const [isLoading, setIsLoading] = useState(false);
12
12
  const [currentStep, setCurrentStep] = useState('');
13
13
  const [quote, setQuote] = useState(null);
@@ -15,6 +15,18 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
15
15
  const [orderId, setOrderId] = useState(null);
16
16
  const [viewingAuth, setViewingAuth] = useState(null);
17
17
  const [isSwapping, setIsSwapping] = useState(false);
18
+ // Use refs to track current wallet state (to avoid stale closure values)
19
+ const walletRef = useRef(wallet);
20
+ const walletLoadingRef = useRef(walletLoading);
21
+ const walletErrorRef = useRef(walletError);
22
+ const generateWalletRef = useRef(generateWallet);
23
+ // Update refs when wallet state changes
24
+ useEffect(() => {
25
+ walletRef.current = wallet;
26
+ walletLoadingRef.current = walletLoading;
27
+ walletErrorRef.current = walletError;
28
+ generateWalletRef.current = generateWallet;
29
+ }, [wallet, walletLoading, walletError, generateWallet]);
18
30
  // Normalize address - only normalize EVM addresses
19
31
  // Note: For Solana swaps, we still need an EVM address for facilitator operations
20
32
  // The address parameter should be EVM for facilitator operations, but can be Solana for quote requests
@@ -42,7 +54,7 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
42
54
  // Initialize specialized hooks
43
55
  const { getQuote: getQuoteInternal } = useQuoteFetching(client, rawAddress, setIsLoading, setCurrentStep, setError, onStatus);
44
56
  const { signAuthorizations, createOrder } = useOrderSigning(walletClient, connector, client, setCurrentStep, onStatus);
45
- const { executeSolanaBridge, executeEvmBridge } = useBridgeExecution(walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, depositorAddress, onStatus);
57
+ const { executeSolanaBridge, executeBitcoinBridge, executeEvmBridge } = useBridgeExecution(walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, depositorAddress, onStatus, bitcoinConnector, bitcoinConnection);
46
58
  const { executeSwapTransaction, approveTokenSpending } = useTransaction({
47
59
  address: normalizedAddress || '0x0000000000000000000000000000000000000000',
48
60
  walletClient,
@@ -93,9 +105,49 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
93
105
  if (!walletClient) {
94
106
  throw new Error('Wallet client required for swap execution');
95
107
  }
96
- // Check if wallet is being generated - wait a bit and check again
97
- if (walletLoading) {
98
- throw new Error('Wallet is being generated. Please wait for wallet generation to complete before executing swap.');
108
+ // Check if wallet generation previously failed - retry if so
109
+ if (!walletRef.current && walletErrorRef.current && generateWalletRef.current && !walletLoadingRef.current) {
110
+ setCurrentStep('Retrying wallet generation...');
111
+ onStatus?.('Retrying wallet generation...');
112
+ try {
113
+ await generateWalletRef.current();
114
+ // Wait for wallet to be ready after retry (with timeout)
115
+ const maxWaitTime = 30000; // 30 seconds
116
+ const checkInterval = 100; // Check every 100ms
117
+ const startTime = Date.now();
118
+ while (!walletRef.current && Date.now() - startTime < maxWaitTime) {
119
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
120
+ }
121
+ // If wallet is still not ready after retry, throw error
122
+ if (!walletRef.current) {
123
+ throw new Error('Wallet generation retry completed but wallet is still not available. Please try again.');
124
+ }
125
+ }
126
+ catch (err) {
127
+ const error = err instanceof Error ? err : new Error('Failed to generate wallet');
128
+ throw new Error(`Wallet generation failed: ${error.message}. Please try again or ensure wallet is connected and authenticated.`);
129
+ }
130
+ }
131
+ // Wait for wallet to be ready if it's being generated
132
+ // Use refs to check current state (avoid stale closure values)
133
+ if (!walletRef.current && walletLoadingRef.current) {
134
+ setCurrentStep('Waiting for wallet generation...');
135
+ onStatus?.('Waiting for wallet generation...');
136
+ // Wait for wallet generation to complete (with timeout)
137
+ const maxWaitTime = 30000; // 30 seconds
138
+ const checkInterval = 100; // Check every 100ms
139
+ const startTime = Date.now();
140
+ while (!walletRef.current && walletLoadingRef.current && Date.now() - startTime < maxWaitTime) {
141
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
142
+ }
143
+ // If wallet is still not ready after waiting, throw error
144
+ if (!walletRef.current) {
145
+ throw new Error('Wallet is being generated. Please wait for wallet generation to complete before executing swap.');
146
+ }
147
+ }
148
+ // Final check: wallet must exist for swap execution
149
+ if (!walletRef.current) {
150
+ throw new Error('Wallet is required for swap execution. Please ensure wallet is connected and authenticated.');
99
151
  }
100
152
  setIsLoading(true);
101
153
  setError(null);
@@ -140,8 +192,9 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
140
192
  }
141
193
  // Check swap type
142
194
  const isSourceSolana = isSolanaAsset(sourceAsset);
195
+ const isSourceBitcoin = isBitcoinAsset(sourceAsset);
143
196
  const isDepositingDirectly = sourceAsset === S_CAIP19_USDC_AVALANCHE;
144
- const isSourceEvm = !isSourceSolana && !isDepositingDirectly && sourceAsset.startsWith('eip155:');
197
+ const isSourceEvm = !isSourceSolana && !isSourceBitcoin && !isDepositingDirectly && sourceAsset.startsWith('eip155:');
145
198
  // Generate viewing authorization
146
199
  const viewer = await resolvedGroup.viewer();
147
200
  const evmSigner = await viewer.evmSigner();
@@ -163,6 +216,15 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
163
216
  setViewingAuth(result.viewingAuth);
164
217
  return result;
165
218
  }
219
+ // Handle Bitcoin swaps
220
+ if (isSourceBitcoin) {
221
+ const result = await executeBitcoinSwap(quoteResponse, sourceAsset, sourceAmountInUnits, effectiveUsdcAmount, senderContactId, bitcoinConnector, evmSignerAddress, // Use EVM signer address, not normalizedAddress
222
+ viewingAuth, createOrder, executeBitcoinBridge, resolvedGroup, // Pass resolved group for order creation
223
+ integratorId);
224
+ setOrderId(result.orderId);
225
+ setViewingAuth(result.viewingAuth);
226
+ return result;
227
+ }
166
228
  // Sign authorizations (for EVM swaps)
167
229
  const signedAuths = await signAuthorizations(quoteResponse.authorizations, isDepositingDirectly);
168
230
  // Create order with resolved facilitator group
@@ -233,7 +295,10 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
233
295
  evmAddress,
234
296
  walletClient,
235
297
  walletLoading,
298
+ walletError,
299
+ generateWallet,
236
300
  solanaConnector,
301
+ bitcoinConnector,
237
302
  signAuthorizations,
238
303
  createOrder,
239
304
  approveTokenSpending,
@@ -331,6 +396,67 @@ viewingAuth, createOrder, executeSolanaBridge, facilitatorGroup, integratorId) {
331
396
  },
332
397
  };
333
398
  }
399
+ /**
400
+ * Execute Bitcoin swap flow
401
+ *
402
+ * Handles the complete flow for Bitcoin-based swaps:
403
+ * 1. Validates Bitcoin connection
404
+ * 2. Creates order (with empty authorizations for Bitcoin)
405
+ * 3. Executes bridge transaction
406
+ * 4. Returns swap result
407
+ */
408
+ async function executeBitcoinSwap(quoteResponse, sourceAsset, sourceAmount, usdcAmount, senderContactId, bitcoinConnector, evmSignerAddress, // EVM signer address (matches Svelte's s0x_signer)
409
+ viewingAuth, createOrder, executeBitcoinBridge, facilitatorGroup, integratorId) {
410
+ if (!bitcoinConnector) {
411
+ throw new Error('Bitcoin connector required for Bitcoin swaps');
412
+ }
413
+ // Get Bitcoin sender address
414
+ const accounts = await bitcoinConnector.getAccounts();
415
+ const bitcoinSenderAddress = bitcoinConnector.currentAccount || accounts[0] || '';
416
+ if (!bitcoinSenderAddress) {
417
+ throw new Error('Failed to get Bitcoin sender address');
418
+ }
419
+ // Create order with empty authorizations (Bitcoin doesn't use EIP-3009)
420
+ // Pass resolved facilitator group (matches Svelte behavior)
421
+ const orderResponse = await createOrder(quoteResponse, quoteResponse.authorizations.map((auth) => ({
422
+ ...auth,
423
+ signature: '0x', // No EIP-3009 deposit for Bitcoin
424
+ })), {
425
+ sourceAsset: {
426
+ caip19: sourceAsset,
427
+ amount: `${BigInt(sourceAmount)}`,
428
+ },
429
+ sourceSender: {
430
+ contactId: senderContactId,
431
+ },
432
+ ...(integratorId && { integratorId }),
433
+ }, facilitatorGroup);
434
+ // Get deposit parameters from order
435
+ const depositParams = orderResponse.transaction.metadata?.params;
436
+ if (!depositParams) {
437
+ throw new Error('Missing deposit parameters in order response');
438
+ }
439
+ // Execute bridge transaction
440
+ // Use EVM signer address for deposit calldata (matches Svelte's s0x_signer)
441
+ // Pass depositParams so executeBitcoinBridge can encode the actual deposit calldata
442
+ // Use usdcAmount from solveOptimalUsdcAmount (matches Svelte: zg_amount_src_usdc is passed directly)
443
+ const bridgeResult = await executeBitcoinBridge(sourceAsset, sourceAmount, usdcAmount, // Use amount from solveOptimalUsdcAmount (matches Svelte line 1028)
444
+ bitcoinSenderAddress, evmSignerAddress, // Use EVM signer address, not normalizedAddress
445
+ depositParams);
446
+ const resultOrderId = orderResponse.response.orderId;
447
+ // Note: setOrderId is not available in this function scope,
448
+ // but the orderId will be set by the calling executeSwap function
449
+ return {
450
+ orderId: resultOrderId,
451
+ viewingAuth,
452
+ depositTransaction: {
453
+ hash: bridgeResult.depositTxHash,
454
+ chainId: NI_CHAIN_ID_AVALANCHE,
455
+ status: 'confirmed',
456
+ confirmations: 1,
457
+ },
458
+ };
459
+ }
334
460
  /**
335
461
  * Execute EVM swap flow
336
462
  *
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef, useState, useMemo } from 'react';
2
2
  import BigNumber from 'bignumber.js';
3
- import { parseEvmCaip19, parseSolanaCaip19, isEvmNativeToken, isSolanaNativeToken, isSolanaAsset, EVM_PHONY_ADDRESS, SB58_ADDR_SOL_PROGRAM_SYSTEM, CALCULATION_DIRECTION_INPUT_TO_OUTPUT, CALCULATION_DIRECTION_OUTPUT_TO_INPUT, } from '@silentswap/sdk';
3
+ import { parseEvmCaip19, parseSolanaCaip19, isEvmNativeToken, isSolanaNativeToken, isSolanaAsset, isBitcoinAsset, EVM_PHONY_ADDRESS, SB58_ADDR_SOL_PROGRAM_SYSTEM, CALCULATION_DIRECTION_INPUT_TO_OUTPUT, CALCULATION_DIRECTION_OUTPUT_TO_INPUT, } from '@silentswap/sdk';
4
4
  import { useQuote } from './useQuote.js';
5
5
  /**
6
6
  * Hook for fetching both ingress and egress estimates in parallel
@@ -27,6 +27,11 @@ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpa
27
27
  if (sourceAsset.caip19.includes('solana')) {
28
28
  return solAddress || SB58_ADDR_SOL_PROGRAM_SYSTEM;
29
29
  }
30
+ else if (sourceAsset.caip19.includes('bip122')) {
31
+ // Bitcoin source - Bitcoin address not available in this hook, return undefined
32
+ // Note: Bitcoin addresses would need to be passed as a parameter to this hook
33
+ return undefined;
34
+ }
30
35
  else {
31
36
  return evmAddress || EVM_PHONY_ADDRESS;
32
37
  }
@@ -114,7 +119,8 @@ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpa
114
119
  // Parse source asset to get chain ID and token address from CAIP-19
115
120
  console.log('[OrderEstimates] Step 3: Parsing source asset CAIP-19');
116
121
  const isSourceSolana = isSolanaAsset(sourceAsset.caip19);
117
- const sourceEvmParsed = !isSourceSolana ? parseEvmCaip19(sourceAsset.caip19) : null;
122
+ const isSourceBitcoin = isBitcoinAsset(sourceAsset.caip19);
123
+ const sourceEvmParsed = !isSourceSolana && !isSourceBitcoin ? parseEvmCaip19(sourceAsset.caip19) : null;
118
124
  const sourceSolanaParsed = isSourceSolana ? parseSolanaCaip19(sourceAsset.caip19) : null;
119
125
  const sourceChainId = sourceEvmParsed?.chainId ?? sourceSolanaParsed?.chainId ?? 0;
120
126
  const sourceTokenAddress = isEvmNativeToken(sourceAsset.caip19)
@@ -122,7 +128,7 @@ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpa
122
128
  : isSolanaNativeToken(sourceAsset.caip19)
123
129
  ? '11111111111111111111111111111111'
124
130
  : (sourceEvmParsed?.tokenAddress ?? sourceSolanaParsed?.tokenAddress ?? '');
125
- console.log('[OrderEstimates] Step 3: Source asset parsed', { sourceChainId, sourceTokenAddress, isSourceSolana });
131
+ console.log('[OrderEstimates] Step 3: Source asset parsed', { sourceChainId, sourceTokenAddress, isSourceSolana, isSourceBitcoin });
126
132
  // For reverse calculation, calculate total output USD value first
127
133
  let totalOutputUsd = 0;
128
134
  let calculatedInputAmount = undefined;
@@ -173,7 +179,8 @@ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpa
173
179
  Promise.all(destinationAssets.map((destAsset, idx) => {
174
180
  // Parse destination asset to get chain ID and token address from CAIP-19
175
181
  const isDestSolana = isSolanaAsset(destAsset.caip19);
176
- const destEvmParsed = !isDestSolana ? parseEvmCaip19(destAsset.caip19) : null;
182
+ const isDestBitcoin = isBitcoinAsset(destAsset.caip19);
183
+ const destEvmParsed = !isDestSolana && !isDestBitcoin ? parseEvmCaip19(destAsset.caip19) : null;
177
184
  const destSolanaParsed = isDestSolana ? parseSolanaCaip19(destAsset.caip19) : null;
178
185
  const destChainId = destEvmParsed?.chainId ?? destSolanaParsed?.chainId ?? 0;
179
186
  const destTokenAddress = isEvmNativeToken(destAsset.caip19)
@@ -182,8 +189,9 @@ export function useOrderEstimates({ evmAddress, solAddress, sourceAsset, maxImpa
182
189
  ? '11111111111111111111111111111111'
183
190
  : (destEvmParsed?.tokenAddress ?? destSolanaParsed?.tokenAddress ?? '');
184
191
  // Determine recipient address based on destination chain
185
- // For Solana destinations, use Solana address; for EVM, use EVM address
186
- const recipientAddress = isDestSolana ? solAddress : evmAddress;
192
+ // For Solana destinations, use Solana address; for Bitcoin, use empty string (Bitcoin address not available in this hook); for EVM, use EVM address
193
+ // Note: Bitcoin addresses would need to be passed as a parameter to this hook if Bitcoin destinations are used
194
+ const recipientAddress = isDestSolana ? solAddress : isDestBitcoin ? '' : evmAddress;
187
195
  // For reverse calculation, use output amount directly with EXACT_OUTPUT
188
196
  if (calculationDirection === CALCULATION_DIRECTION_OUTPUT_TO_INPUT && outputAmounts && outputAmounts[idx]) {
189
197
  const outputAmount = parseFloat(outputAmounts[idx]);
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import BigNumber from 'bignumber.js';
3
- import { NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, S_CAIP19_USDC_AVALANCHE, X_MAX_IMPACT_PERCENT, getBridgeQuote as sdkGetBridgeQuote, interpolateSamples as sdkInterpolateSamples, fetchRelayQuote, fetchDebridgeOrder, normalizeAddress, getAssetByCaip19, N_DEBRIDGE_CHAIN_ID_SOLANA, N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, S0X_ADDR_EVM_RELAY_LINK_DEAD, SB58_ADDR_SOL_RELAY_LINK_RECIPIENT, isSolanaAsset, EVM_PHONY_ADDRESS, } from '@silentswap/sdk';
3
+ import { NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, S_CAIP19_USDC_AVALANCHE, X_MAX_IMPACT_PERCENT, getBridgeQuote as sdkGetBridgeQuote, interpolateSamples as sdkInterpolateSamples, fetchRelayQuote, fetchDebridgeOrder, normalizeAddress, getAssetByCaip19, N_DEBRIDGE_CHAIN_ID_SOLANA, N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, S0X_ADDR_EVM_RELAY_LINK_DEAD, SB58_ADDR_SOL_RELAY_LINK_RECIPIENT, isSolanaAsset, isBitcoinAsset, EVM_PHONY_ADDRESS, } from '@silentswap/sdk';
4
4
  // Constants for estimateLive
5
5
  const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
6
6
  /**
@@ -316,6 +316,10 @@ export function useQuote({ address, maxImpactPercent = X_MAX_IMPACT_PERCENT } =
316
316
  if (direction === 'egress') {
317
317
  return null; // Skip DeBridge for egress, will use Relay.link only
318
318
  }
319
+ // Skip DeBridge for Solana and Bitcoin - use relay only
320
+ if (isSolanaChain || isBitcoinAsset(assetCaip19)) {
321
+ return null; // Skip DeBridge for Solana and Bitcoin, will use Relay.link only
322
+ }
319
323
  // For reverse calculation ingress: use srcChainTokenInAmount: 'auto' and specify dstChainTokenOutAmount
320
324
  const debridgeParams = {
321
325
  srcChainId: debridgeChainId,
@@ -13,7 +13,7 @@ export const DEFAULT_OUTPUT_ASSET = 'eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C3
13
13
  export const getSourceAssetCaip19 = (tokenIn) => {
14
14
  return tokenIn?.caip19 || DEFAULT_SOURCE_ASSET;
15
15
  };
16
- export const useSwap = create()(persist((set, get) => ({
16
+ const useSwapStore = create()(persist((set, get) => ({
17
17
  // Regular swap state
18
18
  isAutoSlippage: true,
19
19
  tokenIn: getAssetByCaip19(DEFAULT_SOURCE_ASSET) ?? null,
@@ -283,3 +283,9 @@ export const useSwap = create()(persist((set, get) => ({
283
283
  };
284
284
  },
285
285
  }));
286
+ // Cache server snapshot for SSR compatibility
287
+ // This prevents the "getServerSnapshot should be cached" error in Next.js
288
+ if (typeof window === 'undefined') {
289
+ useSwapStore.getState(); // Cache the snapshot on server
290
+ }
291
+ export const useSwap = useSwapStore;
@@ -2,6 +2,7 @@ import type { Hex, WalletClient } from 'viem';
2
2
  import type { Connector } from 'wagmi';
3
3
  import type { BridgeProvider, BridgeQuote, BridgeStatus, OrderResponse } from '@silentswap/sdk';
4
4
  import type { SolanaWalletConnector, SolanaConnection } from './silent/solana-transaction.js';
5
+ import type { BitcoinWalletConnector, BitcoinConnection } from './silent/bitcoin-transaction.js';
5
6
  export interface useTransactionOptions {
6
7
  /** User's EVM address */
7
8
  address: `0x${string}`;
@@ -15,6 +16,10 @@ export interface useTransactionOptions {
15
16
  solanaConnection?: SolanaConnection;
16
17
  /** Solana RPC URL (optional, will create connection if not provided) */
17
18
  solanaRpcUrl?: string;
19
+ /** Bitcoin wallet connector (required for Bitcoin transactions) */
20
+ bitcoinConnector?: BitcoinWalletConnector;
21
+ /** Bitcoin connection (optional, for consistency with Solana pattern) */
22
+ bitcoinConnection?: BitcoinConnection;
18
23
  /** Optional callback to set current step (for external state management) */
19
24
  setCurrentStep?: (step: string) => void;
20
25
  /** Optional status update callback */
@@ -116,4 +121,4 @@ export interface useTransactionReturn {
116
121
  * }
117
122
  * ```
118
123
  */
119
- export declare function useTransaction({ walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep: externalSetCurrentStep, onStatus: externalOnStatus, }: useTransactionOptions): useTransactionReturn;
124
+ export declare function useTransaction({ walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, bitcoinConnector, bitcoinConnection, setCurrentStep: externalSetCurrentStep, onStatus: externalOnStatus, }: useTransactionOptions): useTransactionReturn;