@silentswap/react 0.0.53 → 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.
@@ -21,6 +21,8 @@ export declare const BalancesProvider: React.FC<{
21
21
  evmAddress?: string;
22
22
  solAddress?: string;
23
23
  solanaRpcUrl?: string;
24
+ bitcoinAddress?: string;
25
+ bitcoinRpcUrl?: string;
24
26
  }>;
25
27
  /**
26
28
  * Hook to access user balances
@@ -8,7 +8,7 @@ import { getAssociatedTokenAddress } from '@solana/spl-token';
8
8
  import { PublicKey } from '@solana/web3.js';
9
9
  import { useAssetsContext } from './AssetsContext.js';
10
10
  import { usePrices } from '../hooks/usePrices.js';
11
- import { isSolanaAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, isEvmNativeToken, SB58_CHAIN_ID_SOLANA_MAINNET, A_VIEM_CHAINS, } from '@silentswap/sdk';
11
+ import { isSolanaAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, isEvmNativeToken, isBitcoinAsset, SB58_CHAIN_ID_SOLANA_MAINNET, A_VIEM_CHAINS, BITCOIN_CHAIN_ID, } from '@silentswap/sdk';
12
12
  const BalancesContext = createContext(undefined);
13
13
  // Custom RPC endpoints from 0xrpc.io (fast, free, private public RPC)
14
14
  // Reference: https://0xrpc.io
@@ -68,6 +68,18 @@ const separateSolanaAssets = (solanaAssets) => {
68
68
  }
69
69
  return { a_assets_native, a_assets_tokens };
70
70
  };
71
+ // Utility: Separate Bitcoin assets (currently only native BTC is supported)
72
+ const separateBitcoinAssets = (bitcoinAssets) => {
73
+ const a_assets_native = [];
74
+ for (const asset of bitcoinAssets) {
75
+ // Bitcoin native token uses slip44:0 or similar
76
+ // For now, we'll treat all Bitcoin assets as native (BTC)
77
+ if (isBitcoinAsset(asset.caip19)) {
78
+ a_assets_native.push(asset);
79
+ }
80
+ }
81
+ return { a_assets_native };
82
+ };
71
83
  // Utility: Fetch ERC-20 balance for a single token (fallback when multicall not available)
72
84
  const fetchSingleErc20Balance = async (y_client, s0x_account, k_asset, getUnitValueUsd) => {
73
85
  try {
@@ -354,10 +366,80 @@ const fetchSolanaBalances = async (solAddress, solanaRpcUrl, assets, getUnitValu
354
366
  return { balances: {}, error: errorMsg };
355
367
  }
356
368
  };
369
+ // Utility: Fetch Bitcoin native balances using Mempool.space API
370
+ const fetchBitcoinNativeBalances = async (bitcoinAddress, a_assets_native, getUnitValueUsd) => {
371
+ const updatedBalances = {};
372
+ if (a_assets_native.length === 0)
373
+ return updatedBalances;
374
+ try {
375
+ // Fetch balance from Mempool.space API (free, no auth required)
376
+ // Returns balance in satoshis (smallest unit, 1 BTC = 100,000,000 satoshis)
377
+ const response = await fetch(`https://mempool.space/api/address/${bitcoinAddress}`);
378
+ if (!response.ok) {
379
+ throw new Error(`Failed to fetch Bitcoin balance: ${response.status} ${response.statusText}`);
380
+ }
381
+ const data = await response.json();
382
+ // Mempool.space returns chain_stats with funded_txo_sum and spent_txo_sum
383
+ // Balance = funded - spent
384
+ const funded = BigInt(data.chain_stats?.funded_txo_sum || 0);
385
+ const spent = BigInt(data.chain_stats?.spent_txo_sum || 0);
386
+ const xg_balance = funded - spent;
387
+ await Promise.all(a_assets_native.map(async (k_asset) => {
388
+ let x_value_usd = 0;
389
+ try {
390
+ x_value_usd = await getUnitValueUsd(k_asset, xg_balance);
391
+ }
392
+ catch (error) {
393
+ // console.warn(`Failed to calculate USD value for native ${k_asset.caip19}:`, error);
394
+ }
395
+ updatedBalances[k_asset.caip19] = {
396
+ asset: k_asset,
397
+ balance: xg_balance,
398
+ usdValue: x_value_usd,
399
+ };
400
+ }));
401
+ }
402
+ catch (error) {
403
+ // Log error for debugging but don't throw - allow other operations to continue
404
+ if (process.env.NODE_ENV === 'development') {
405
+ console.warn(`Failed to fetch native BTC balance:`, error);
406
+ }
407
+ }
408
+ return updatedBalances;
409
+ };
410
+ // Utility: Fetch all Bitcoin balances using direct API calls
411
+ const fetchBitcoinBalances = async (bitcoinAddress, bitcoinRpcUrl, // Not used currently, but kept for potential future RPC usage
412
+ assets, getUnitValueUsd) => {
413
+ try {
414
+ const bitcoinAssets = Object.values(assets).filter((asset) => isBitcoinAsset(asset.caip19));
415
+ const { a_assets_native } = separateBitcoinAssets(bitcoinAssets);
416
+ // Fetch native Bitcoin balances using Mempool.space API
417
+ const nativeResult = await Promise.allSettled([
418
+ fetchBitcoinNativeBalances(bitcoinAddress, a_assets_native, getUnitValueUsd),
419
+ ]);
420
+ const nativeBalances = nativeResult[0]?.status === 'fulfilled' ? nativeResult[0].value : {};
421
+ // Collect errors if any
422
+ const errors = [];
423
+ if (nativeResult[0]?.status === 'rejected') {
424
+ errors.push(`Native: ${nativeResult[0].reason?.message || 'Unknown error'}`);
425
+ }
426
+ return {
427
+ balances: nativeBalances,
428
+ error: errors.length > 0 ? errors.join('; ') : undefined,
429
+ };
430
+ }
431
+ catch (error) {
432
+ const errorMsg = `Failed to fetch Bitcoin balances: ${error instanceof Error ? error.message : String(error)}`;
433
+ if (process.env.NODE_ENV === 'development') {
434
+ console.error(errorMsg, error);
435
+ }
436
+ return { balances: {}, error: errorMsg };
437
+ }
438
+ };
357
439
  /**
358
440
  * Provider for user balances across all supported chains
359
441
  */
360
- export const BalancesProvider = ({ children, evmAddress, solAddress, solanaRpcUrl }) => {
442
+ export const BalancesProvider = ({ children, evmAddress, solAddress, solanaRpcUrl, bitcoinAddress, bitcoinRpcUrl }) => {
361
443
  const { assets, loading: assetsLoading } = useAssetsContext();
362
444
  const { getUnitValueUsd } = usePrices();
363
445
  const [balances, setBalances] = useState({});
@@ -388,6 +470,15 @@ export const BalancesProvider = ({ children, evmAddress, solAddress, solanaRpcUr
388
470
  promise: fetchSolanaBalances(solAddress, solanaRpcUrl, assets, getUnitValueUsd),
389
471
  });
390
472
  }
473
+ console.log('[BalancesContext] bitcoinAddress', bitcoinAddress);
474
+ console.log('[BalancesContext] bitcoinRpcUrl', bitcoinRpcUrl);
475
+ // Process Bitcoin balances if Bitcoin address is available
476
+ if (bitcoinAddress && bitcoinRpcUrl) {
477
+ balanceTasks.push({
478
+ chainId: BITCOIN_CHAIN_ID,
479
+ promise: fetchBitcoinBalances(bitcoinAddress, bitcoinRpcUrl, assets, getUnitValueUsd),
480
+ });
481
+ }
391
482
  // Use Promise.allSettled to handle partial failures gracefully
392
483
  // This ensures one chain failure doesn't prevent others from updating
393
484
  const results = await Promise.allSettled(balanceTasks.map((task) => task.promise));
@@ -454,6 +545,15 @@ export const BalancesProvider = ({ children, evmAddress, solAddress, solanaRpcUr
454
545
  promise: fetchSolanaBalances(solAddress, solanaRpcUrl, assets, getUnitValueUsd),
455
546
  });
456
547
  }
548
+ // Process Bitcoin balances if requested
549
+ const shouldRefetchBitcoin = chainIds.includes(BITCOIN_CHAIN_ID);
550
+ console.log('[BalancesContext] shouldRefetchBitcoin', shouldRefetchBitcoin);
551
+ if (bitcoinAddress && bitcoinRpcUrl && shouldRefetchBitcoin) {
552
+ balanceTasks.push({
553
+ chainId: BITCOIN_CHAIN_ID,
554
+ promise: fetchBitcoinBalances(bitcoinAddress, bitcoinRpcUrl, assets, getUnitValueUsd),
555
+ });
556
+ }
457
557
  // Use Promise.allSettled to handle partial failures gracefully
458
558
  const results = await Promise.allSettled(balanceTasks.map((task) => task.promise));
459
559
  const newBalances = {};
@@ -500,17 +600,17 @@ export const BalancesProvider = ({ children, evmAddress, solAddress, solanaRpcUr
500
600
  finally {
501
601
  setLoading(false);
502
602
  }
503
- }, [evmAddress, solAddress, solanaRpcUrl, assets, assetsLoading, getUnitValueUsd]);
603
+ }, [evmAddress, solAddress, solanaRpcUrl, bitcoinAddress, bitcoinRpcUrl, assets, assetsLoading, getUnitValueUsd]);
504
604
  // Calculate total USD value
505
605
  const totalUsdValue = useMemo(() => {
506
606
  return Object.values(balances).reduce((total, balance) => total + balance.usdValue, 0);
507
607
  }, [balances]);
508
- // Auto-update balances when address, solAddress, or assets change
608
+ // Auto-update balances when address, solAddress, bitcoinAddress, or assets change
509
609
  useEffect(() => {
510
- if ((evmAddress || solAddress) && !assetsLoading) {
610
+ if ((evmAddress || solAddress || bitcoinAddress) && !assetsLoading) {
511
611
  updateBalances();
512
612
  }
513
- }, [evmAddress, solAddress, solanaRpcUrl, assetsLoading, updateBalances]);
613
+ }, [evmAddress, solAddress, solanaRpcUrl, bitcoinAddress, bitcoinRpcUrl, assetsLoading, updateBalances]);
514
614
  const value = useMemo(() => ({
515
615
  balances,
516
616
  loading: loading || assetsLoading,
@@ -5,6 +5,7 @@ import { ENVIRONMENT, type SilentSwapClient, type SilentSwapClientConfig, type A
5
5
  import { type ExecuteSwapParams, type SwapResult } from '../hooks/silent/useSilentQuote.js';
6
6
  import { type OutputStatus } from '../hooks/silent/useOrderTracking.js';
7
7
  import { type SolanaWalletConnector, type SolanaConnection } from '../hooks/silent/solana-transaction.js';
8
+ import { type BitcoinWalletConnector, type BitcoinConnection } from '../hooks/silent/bitcoin-transaction.js';
8
9
  import { type SilentSwapWallet } from '../hooks/silent/useWallet.js';
9
10
  export interface SilentSwapContextType {
10
11
  client: SilentSwapClient;
@@ -42,7 +43,7 @@ export interface SilentSwapContextType {
42
43
  solanaRpcUrl?: string;
43
44
  }
44
45
  export declare function useSilentSwap(): SilentSwapContextType;
45
- export declare function SilentSwapProvider({ children, client, evmAddress, solAddress, connector, isConnected, solanaConnector, solanaConnection, environment, baseUrl, solanaRpcUrl, walletClient, }: {
46
+ export declare function SilentSwapProvider({ children, client, evmAddress, solAddress, connector, isConnected, solanaConnector, solanaConnection, bitcoinConnector, bitcoinConnection, environment, baseUrl, solanaRpcUrl, walletClient, bitcoinAddress, bitcoinRpcUrl, }: {
46
47
  children: React.ReactNode;
47
48
  client: SilentSwapClient;
48
49
  evmAddress?: string;
@@ -52,7 +53,11 @@ export declare function SilentSwapProvider({ children, client, evmAddress, solAd
52
53
  walletClient?: WalletClient;
53
54
  solanaConnector?: SolanaWalletConnector;
54
55
  solanaConnection?: SolanaConnection;
56
+ bitcoinConnector?: BitcoinWalletConnector;
57
+ bitcoinConnection?: BitcoinConnection;
55
58
  environment?: ENVIRONMENT;
56
59
  baseUrl?: string;
57
60
  solanaRpcUrl?: string;
61
+ bitcoinAddress?: string;
62
+ bitcoinRpcUrl?: string;
58
63
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext, useEffect, useMemo } from 'react';
3
+ import { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
4
4
  import { useAuth } from '../hooks/silent/useAuth.js';
5
5
  import { useWallet } from '../hooks/silent/useWallet.js';
6
6
  import { useSilentQuote } from '../hooks/silent/useSilentQuote.js';
@@ -63,7 +63,7 @@ export function useSilentSwap() {
63
63
  }
64
64
  return context;
65
65
  }
66
- function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, solanaConnector, solanaConnection, environment, config, solanaRpcUrl, connector, isConnected = false, walletClient, }) {
66
+ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, bitcoinAddress, solanaConnector, solanaConnection, bitcoinConnector, bitcoinConnection, environment, config, solanaRpcUrl, connector, isConnected = false, walletClient, }) {
67
67
  // Authentication hook - only for EVM
68
68
  const { auth, isLoading: authLoading } = useAuth({
69
69
  client,
@@ -90,7 +90,7 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, sol
90
90
  const setDestinations = useSwap((state) => state.setDestinations);
91
91
  const updateDestinationAmount = useSwap((state) => state.updateDestinationAmount);
92
92
  const splits = useSwap((state) => state.splits);
93
- const transactionAddress = useTransactionAddress(tokenIn, evmAddress, solAddress);
93
+ const transactionAddress = useTransactionAddress(tokenIn, evmAddress, solAddress, bitcoinAddress);
94
94
  const effectiveQuoteAddress = transactionAddress;
95
95
  const { getPrice } = usePrices();
96
96
  const { serviceFeeRate, overheadUsd } = useStatus();
@@ -127,19 +127,28 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, sol
127
127
  connector,
128
128
  solanaConnector,
129
129
  solanaConnection,
130
+ bitcoinConnector,
131
+ bitcoinConnection,
130
132
  getPrice,
131
133
  });
132
- const { handleNewSwap } = useResetSwapForm({
134
+ const effectiveDepositAmountUsd = useMemo(() => {
135
+ return depositAmountUsdFromEstimates > 0 ? depositAmountUsdFromEstimates : depositAmountUsdc;
136
+ }, [depositAmountUsdFromEstimates, depositAmountUsdc]);
137
+ const usdcPrice = usdcPriceFromEstimates || 1;
138
+ // Preserve last calculated fees to persist across component unmounts (e.g., when navigating to order tracking)
139
+ const [preservedFees, setPreservedFees] = useState(null);
140
+ const { handleNewSwap: handleNewSwapBase } = useResetSwapForm({
133
141
  clearQuote,
134
142
  isConnected,
135
143
  wallet,
136
144
  setFacilitatorGroups,
137
145
  refreshWallet,
138
146
  });
139
- const effectiveDepositAmountUsd = useMemo(() => {
140
- return depositAmountUsdFromEstimates > 0 ? depositAmountUsdFromEstimates : depositAmountUsdc;
141
- }, [depositAmountUsdFromEstimates, depositAmountUsdc]);
142
- const usdcPrice = usdcPriceFromEstimates || 1;
147
+ // Wrap handleNewSwap to also clear preserved fees
148
+ const handleNewSwap = useCallback(() => {
149
+ setPreservedFees(null);
150
+ handleNewSwapBase();
151
+ }, [handleNewSwapBase]);
143
152
  const { serviceFeeUsd, bridgeFeeIngressUsd, bridgeFeeEgressUsd } = useHiddenSwapFees({
144
153
  depositAmountUsdc: effectiveDepositAmountUsd,
145
154
  bridgeProvider: bridgeProviderFromQuote,
@@ -150,6 +159,21 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, sol
150
159
  ingressQuote: ingressQuoteFromEstimates,
151
160
  egressQuotes: egressQuotesFromEstimates,
152
161
  });
162
+ // Preserve fees when they're calculated (non-zero values indicate valid fees)
163
+ useEffect(() => {
164
+ if (serviceFeeUsd > 0 || bridgeFeeIngressUsd > 0 || bridgeFeeEgressUsd > 0) {
165
+ setPreservedFees({
166
+ serviceFeeUsd,
167
+ bridgeFeeIngressUsd,
168
+ bridgeFeeEgressUsd,
169
+ depositAmountUsdc: effectiveDepositAmountUsd,
170
+ });
171
+ }
172
+ }, [serviceFeeUsd, bridgeFeeIngressUsd, bridgeFeeEgressUsd, effectiveDepositAmountUsd]);
173
+ // Use preserved fees if current fees are zero (component was unmounted/remounted)
174
+ const effectiveServiceFeeUsd = serviceFeeUsd > 0 ? serviceFeeUsd : (preservedFees?.serviceFeeUsd ?? 0);
175
+ const effectiveBridgeFeeIngressUsd = bridgeFeeIngressUsd > 0 ? bridgeFeeIngressUsd : (preservedFees?.bridgeFeeIngressUsd ?? 0);
176
+ const effectiveBridgeFeeEgressUsd = bridgeFeeEgressUsd > 0 ? bridgeFeeEgressUsd : (preservedFees?.bridgeFeeEgressUsd ?? 0);
153
177
  const slippageUsd = useSlippageUsd({
154
178
  inputUsdValue: effectiveDepositAmountUsd,
155
179
  slippage,
@@ -185,9 +209,9 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, sol
185
209
  orderComplete,
186
210
  orderTrackingError,
187
211
  orderOutputs,
188
- serviceFeeUsd,
189
- bridgeFeeIngressUsd,
190
- bridgeFeeEgressUsd,
212
+ serviceFeeUsd: effectiveServiceFeeUsd,
213
+ bridgeFeeIngressUsd: effectiveBridgeFeeIngressUsd,
214
+ bridgeFeeEgressUsd: effectiveBridgeFeeEgressUsd,
191
215
  slippageUsd,
192
216
  serviceFeeRate,
193
217
  overheadUsd,
@@ -197,7 +221,7 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, sol
197
221
  solanaRpcUrl,
198
222
  }, children: children });
199
223
  }
200
- export function SilentSwapProvider({ children, client, evmAddress, solAddress, connector, isConnected, solanaConnector, solanaConnection, environment = ENVIRONMENT.STAGING, baseUrl, solanaRpcUrl, walletClient, }) {
224
+ export function SilentSwapProvider({ children, client, evmAddress, solAddress, connector, isConnected, solanaConnector, solanaConnection, bitcoinConnector, bitcoinConnection, environment = ENVIRONMENT.STAGING, baseUrl, solanaRpcUrl, walletClient, bitcoinAddress, bitcoinRpcUrl, }) {
201
225
  const config = useMemo(() => {
202
226
  const computedBaseUrl = baseUrl ?? ENVIRONMENT_CONFIGS[environment].baseUrl;
203
227
  console.log('[SilentSwapProvider] Creating config:', { environment, baseUrl, computedBaseUrl });
@@ -206,5 +230,5 @@ export function SilentSwapProvider({ children, client, evmAddress, solAddress, c
206
230
  baseUrl: computedBaseUrl,
207
231
  };
208
232
  }, [environment, baseUrl]);
209
- return (_jsx(AssetsProvider, { children: _jsx(PricesProvider, { children: _jsx(BalancesProvider, { evmAddress: evmAddress, solAddress: solAddress, solanaRpcUrl: solanaRpcUrl, children: _jsx(OrdersProvider, { baseUrl: config.baseUrl, children: _jsx(SilentSwapInnerProvider, { client: client, connector: connector, isConnected: isConnected, evmAddress: evmAddress, solAddress: solAddress, solanaConnector: solanaConnector, solanaConnection: solanaConnection, environment: environment, config: config, solanaRpcUrl: solanaRpcUrl, walletClient: walletClient, children: children }) }) }) }) }));
233
+ return (_jsx(AssetsProvider, { children: _jsx(PricesProvider, { children: _jsx(BalancesProvider, { evmAddress: evmAddress, solAddress: solAddress, solanaRpcUrl: solanaRpcUrl, bitcoinAddress: bitcoinAddress, bitcoinRpcUrl: bitcoinRpcUrl, children: _jsx(OrdersProvider, { baseUrl: config.baseUrl, children: _jsx(SilentSwapInnerProvider, { client: client, connector: connector, isConnected: isConnected, evmAddress: evmAddress, solAddress: solAddress, bitcoinAddress: bitcoinAddress, solanaConnector: solanaConnector, solanaConnection: solanaConnection, bitcoinConnector: bitcoinConnector, bitcoinConnection: bitcoinConnection, environment: environment, config: config, solanaRpcUrl: solanaRpcUrl, walletClient: walletClient, children: children }) }) }) }) }));
210
234
  }
@@ -2,6 +2,7 @@ import type { Hex, WalletClient } from 'viem';
2
2
  import type { Connector } from 'wagmi';
3
3
  import type { DepositParams } from '@silentswap/sdk';
4
4
  import type { SolanaWalletConnector, SolanaConnection } from './solana-transaction.js';
5
+ import type { BitcoinWalletConnector, BitcoinConnection } from './bitcoin-transaction.js';
5
6
  /**
6
7
  * Result from executing a bridge transaction
7
8
  */
@@ -29,11 +30,14 @@ export interface BridgeExecutionResult {
29
30
  * @param solanaConnector - Solana wallet connector (required for Solana swaps)
30
31
  * @param solanaConnection - Solana RPC connection (required for Solana swaps)
31
32
  * @param solanaRpcUrl - Optional Solana RPC URL
33
+ * @param bitcoinConnector - Bitcoin wallet connector (required for Bitcoin swaps)
34
+ * @param bitcoinConnection - Bitcoin connection (optional, for consistency)
32
35
  * @param setCurrentStep - Callback to set current step message
33
36
  * @param onStatus - Optional status update callback
34
37
  * @returns Functions for executing bridge transactions
35
38
  */
36
- export declare function useBridgeExecution(walletClient: WalletClient | undefined, connector: Connector | undefined, solanaConnector: SolanaWalletConnector | undefined, solanaConnection: SolanaConnection | undefined, solanaRpcUrl: string | undefined, setCurrentStep: (step: string) => void, depositorAddress: Hex, onStatus?: (status: string) => void): {
39
+ export declare function useBridgeExecution(walletClient: WalletClient | undefined, connector: Connector | undefined, solanaConnector: SolanaWalletConnector | undefined, solanaConnection: SolanaConnection | undefined, solanaRpcUrl: string | undefined, setCurrentStep: (step: string) => void, depositorAddress: Hex, onStatus?: (status: string) => void, bitcoinConnector?: BitcoinWalletConnector, bitcoinConnection?: BitcoinConnection): {
37
40
  executeSolanaBridge: (sourceAsset: string, sourceAmount: string, usdcAmount: string | undefined, solanaSenderAddress: string, evmSignerAddress: `0x${string}`, depositParams?: DepositParams<`${bigint}`>) => Promise<BridgeExecutionResult>;
41
+ executeBitcoinBridge: (sourceAsset: string, sourceAmount: string, usdcAmount: string | undefined, bitcoinSenderAddress: string, evmSignerAddress: `0x${string}`, depositParams?: DepositParams<`${bigint}`>) => Promise<BridgeExecutionResult>;
38
42
  executeEvmBridge: (sourceChainId: number, sourceTokenAddress: string, sourceAmount: string, usdcAmount: string | undefined, depositParams: DepositParams<`${bigint}`>, evmSignerAddress: `0x${string}`, evmSenderAddress?: `0x${string}`, provider?: "relay" | "debridge") => Promise<BridgeExecutionResult>;
39
43
  };
@@ -1,7 +1,8 @@
1
1
  import { useCallback } from 'react';
2
2
  import { encodeFunctionData, erc20Abi, getAddress } from 'viem';
3
- import { ensureChain, waitForTransactionConfirmation, createPublicClientWithRpc, NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, X_MAX_IMPACT_PERCENT, getRelayStatus, getDebridgeStatus, fetchDebridgeOrder, fetchRelayQuote, createPhonyDepositCalldata, isSolanaAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, getChainById, } from '@silentswap/sdk';
3
+ import { ensureChain, waitForTransactionConfirmation, createPublicClientWithRpc, NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, X_MAX_IMPACT_PERCENT, getRelayStatus, getDebridgeStatus, fetchDebridgeOrder, fetchRelayQuote, createPhonyDepositCalldata, isSolanaAsset, isBitcoinAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, N_RELAY_CHAIN_ID_SOLANA, N_RELAY_CHAIN_ID_BITCOIN, SB58_ADDR_SOL_PROGRAM_SYSTEM, getChainById, SBTC_ADDR_BITCOIN_NATIVE, } from '@silentswap/sdk';
4
4
  import { createSolanaTransactionExecutor, convertRelaySolanaStepToTransaction } from './solana-transaction.js';
5
+ import { createBitcoinTransactionExecutor, convertRelayBitcoinStepToTransaction } from './bitcoin-transaction.js';
5
6
  const S0X_ADDR_EVM_ZERO = '0x0000000000000000000000000000000000000000';
6
7
  const XG_UINT256_MAX = (1n << 256n) - 1n;
7
8
  /**
@@ -124,29 +125,40 @@ async function sendTransactionAndWait(walletClient, connector, chainId, transact
124
125
  }
125
126
  /**
126
127
  * Get relay.link origin asset parameters from CAIP-19
128
+ * Supports both Solana and Bitcoin assets
127
129
  */
128
- function getRelayOriginAsset(caip19) {
129
- if (!isSolanaAsset(caip19)) {
130
- throw new Error('Only Solana assets are supported by this helper');
130
+ function getRelayOriginAssetFromCaip19(caip19) {
131
+ if (isSolanaAsset(caip19)) {
132
+ const parsed = parseSolanaCaip19(caip19);
133
+ if (!parsed) {
134
+ throw new Error(`Invalid Solana CAIP-19: ${caip19}`);
135
+ }
136
+ const originCurrency = isSolanaNativeToken(caip19)
137
+ ? SB58_ADDR_SOL_PROGRAM_SYSTEM
138
+ : isSplToken(caip19)
139
+ ? parsed.tokenAddress ||
140
+ (() => {
141
+ throw new Error(`Missing token address in Solana CAIP-19: ${caip19}`);
142
+ })()
143
+ : (() => {
144
+ throw new Error(`Unsupported Solana asset type: ${caip19}`);
145
+ })();
146
+ return {
147
+ originChainId: N_RELAY_CHAIN_ID_SOLANA,
148
+ originCurrency,
149
+ };
131
150
  }
132
- const parsed = parseSolanaCaip19(caip19);
133
- if (!parsed) {
134
- throw new Error(`Invalid Solana CAIP-19: ${caip19}`);
151
+ if (isBitcoinAsset(caip19)) {
152
+ // For Bitcoin, use the relay chain ID and pass the asset reference as currency
153
+ // Bitcoin CAIP-19 format: bip122:chainId/slip44:0 (for native BTC)
154
+ // For Bitcoin, we typically only support native BTC
155
+ // relay.link requires SBTC_ADDR_BITCOIN_NATIVE for native Bitcoin token
156
+ return {
157
+ originChainId: N_RELAY_CHAIN_ID_BITCOIN,
158
+ originCurrency: SBTC_ADDR_BITCOIN_NATIVE, // Bitcoin native token (BTC) - relay.link requires this specific address
159
+ };
135
160
  }
136
- const originCurrency = isSolanaNativeToken(caip19)
137
- ? SB58_ADDR_SOL_PROGRAM_SYSTEM
138
- : isSplToken(caip19)
139
- ? parsed.tokenAddress ||
140
- (() => {
141
- throw new Error(`Missing token address in Solana CAIP-19: ${caip19}`);
142
- })()
143
- : (() => {
144
- throw new Error(`Unsupported Solana asset type: ${caip19}`);
145
- })();
146
- return {
147
- originChainId: N_RELAY_CHAIN_ID_SOLANA,
148
- originCurrency,
149
- };
161
+ throw new Error(`Unsupported asset type: ${caip19}`);
150
162
  }
151
163
  // Depositor ABI for encoding deposit calldata
152
164
  const DEPOSITOR_ABI = [
@@ -195,11 +207,13 @@ const DEPOSITOR_ABI = [
195
207
  * @param solanaConnector - Solana wallet connector (required for Solana swaps)
196
208
  * @param solanaConnection - Solana RPC connection (required for Solana swaps)
197
209
  * @param solanaRpcUrl - Optional Solana RPC URL
210
+ * @param bitcoinConnector - Bitcoin wallet connector (required for Bitcoin swaps)
211
+ * @param bitcoinConnection - Bitcoin connection (optional, for consistency)
198
212
  * @param setCurrentStep - Callback to set current step message
199
213
  * @param onStatus - Optional status update callback
200
214
  * @returns Functions for executing bridge transactions
201
215
  */
202
- export function useBridgeExecution(walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, depositorAddress, onStatus) {
216
+ export function useBridgeExecution(walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, depositorAddress, onStatus, bitcoinConnector, bitcoinConnection) {
203
217
  /**
204
218
  * Execute Solana bridge transaction
205
219
  *
@@ -224,7 +238,7 @@ export function useBridgeExecution(walletClient, connector, solanaConnector, sol
224
238
  throw new Error('Solana connector and connection are required for Solana swaps');
225
239
  }
226
240
  // Get relay origin asset parameters
227
- const { originChainId, originCurrency } = getRelayOriginAsset(sourceAsset);
241
+ const { originChainId, originCurrency } = getRelayOriginAssetFromCaip19(sourceAsset);
228
242
  // CRITICAL: In Svelte, solve_uusdc_amount is called ONCE before order creation,
229
243
  // and the result (usdcAmount) is used directly in bridge execution.
230
244
  // We should NEVER solve again here - the usdcAmount must be provided
@@ -381,6 +395,171 @@ export function useBridgeExecution(walletClient, connector, solanaConnector, sol
381
395
  throw error;
382
396
  }
383
397
  }, [solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, onStatus, depositorAddress]);
398
+ /**
399
+ * Execute Bitcoin bridge transaction
400
+ *
401
+ * Handles bridge execution for Bitcoin source assets:
402
+ * 1. Solves for optimal USDC amount (if not provided)
403
+ * 2. Gets quotes from relay.link (Bitcoin only supports relay)
404
+ * 3. Executes Bitcoin transactions
405
+ * 4. Monitors bridge status
406
+ *
407
+ * @param sourceAsset - Source asset CAIP-19 (Bitcoin)
408
+ * @param sourceAmount - Source amount in token units
409
+ * @param usdcAmount - Optional USDC amount (will be solved if not provided)
410
+ * @param depositParams - Deposit parameters from order response
411
+ * @param bitcoinSenderAddress - Bitcoin sender address
412
+ * @param evmSignerAddress - EVM signer address for deposit operations
413
+ * @returns Promise resolving to bridge execution result
414
+ */
415
+ const executeBitcoinBridge = useCallback(async (sourceAsset, sourceAmount, usdcAmount, bitcoinSenderAddress, evmSignerAddress, depositParams) => {
416
+ try {
417
+ if (!bitcoinConnector) {
418
+ throw new Error('Bitcoin connector required for Bitcoin swaps');
419
+ }
420
+ // Get relay origin asset parameters
421
+ const { originChainId, originCurrency } = getRelayOriginAssetFromCaip19(sourceAsset);
422
+ // CRITICAL: In Svelte, solve_uusdc_amount is called ONCE before order creation,
423
+ // and the result (usdcAmount) is used directly in bridge execution.
424
+ // We should NEVER solve again here - the usdcAmount must be provided
425
+ // from the initial solve in handleGetQuote (matching Svelte Form.svelte line 2535 and 2576-2584).
426
+ if (!usdcAmount) {
427
+ throw new Error('USDC amount is required for Bitcoin bridge execution. ' +
428
+ 'It should be provided from the initial solveOptimalUsdcAmount call in handleGetQuote. ' +
429
+ 'This matches Svelte behavior where solve_uusdc_amount is called once before order creation.');
430
+ }
431
+ // Use the provided usdcAmount directly (matches Svelte behavior)
432
+ const bridgeUsdcAmount = usdcAmount;
433
+ setCurrentStep('Fetching bridge quote');
434
+ onStatus?.('Fetching bridge quote');
435
+ // Encode USDC on Avalanche approval calldata
436
+ const approveUsdcCalldata = encodeFunctionData({
437
+ abi: erc20Abi,
438
+ functionName: 'approve',
439
+ args: [depositorAddress, XG_UINT256_MAX],
440
+ });
441
+ // Encode depositProxy2 calldata from depositParams
442
+ let depositCalldataForExecution;
443
+ if (depositParams) {
444
+ const checksummedSigner = getAddress(evmSignerAddress);
445
+ const finalDepositParams = {
446
+ ...depositParams,
447
+ signer: checksummedSigner,
448
+ approvalExpiration: typeof depositParams.approvalExpiration === 'bigint'
449
+ ? depositParams.approvalExpiration
450
+ : BigInt(String(depositParams.approvalExpiration)),
451
+ duration: typeof depositParams.duration === 'bigint'
452
+ ? depositParams.duration
453
+ : BigInt(String(depositParams.duration)),
454
+ };
455
+ depositCalldataForExecution = encodeFunctionData({
456
+ abi: DEPOSITOR_ABI,
457
+ functionName: 'depositProxy2',
458
+ args: [finalDepositParams],
459
+ });
460
+ }
461
+ else {
462
+ console.warn('DepositParams not provided, using phony calldata for bridge quote');
463
+ depositCalldataForExecution = createPhonyDepositCalldata(evmSignerAddress);
464
+ }
465
+ // Request bridge quote for Bitcoin → USDC Avalanche using EXACT_OUTPUT
466
+ const relayQuote = await fetchRelayQuote({
467
+ user: bitcoinSenderAddress, // Bitcoin address
468
+ originChainId, // Bitcoin relay chain ID
469
+ originCurrency, // Bitcoin currency (SBTC_ADDR_BITCOIN_NATIVE for native BTC)
470
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
471
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
472
+ amount: bridgeUsdcAmount, // USDC amount in micro units (EXACT_OUTPUT target)
473
+ recipient: depositorAddress, // EVM depositor address
474
+ tradeType: 'EXACT_OUTPUT', // CRITICAL: Must use EXACT_OUTPUT for execution
475
+ txsGasLimit: 600_000,
476
+ txs: [
477
+ {
478
+ to: S0X_ADDR_USDC_AVALANCHE,
479
+ value: '0',
480
+ data: approveUsdcCalldata, // USDC approval calldata
481
+ },
482
+ {
483
+ to: depositorAddress,
484
+ value: '0',
485
+ data: depositCalldataForExecution, // Deposit calldata
486
+ },
487
+ ],
488
+ });
489
+ // Check price impact
490
+ const impactPercent = Number(relayQuote.details.totalImpact.percent);
491
+ if (impactPercent > X_MAX_IMPACT_PERCENT) {
492
+ throw new Error(`Price impact across bridge too high: ${impactPercent.toFixed(2)}%`);
493
+ }
494
+ const rawResponse = relayQuote;
495
+ const selectedProvider = 'relay'; // Bitcoin only supports relay
496
+ // Create Bitcoin transaction executor
497
+ const bitcoinExecutor = createBitcoinTransactionExecutor(bitcoinConnector, bitcoinConnection);
498
+ setCurrentStep('Executing bridge transaction');
499
+ onStatus?.('Executing bridge transaction');
500
+ // Execute transactions
501
+ if (selectedProvider === 'relay') {
502
+ const relayQuote = rawResponse;
503
+ if (!relayQuote.steps) {
504
+ throw new Error('No steps in relay quote response');
505
+ }
506
+ // Execute each step from relay.link
507
+ for (const step of relayQuote.steps) {
508
+ if (step.kind !== 'transaction') {
509
+ throw new Error(`Unsupported relay step kind: ${step.kind}`);
510
+ }
511
+ if (step.items.length > 1) {
512
+ throw new Error('Multiple items in transaction step not implemented');
513
+ }
514
+ const item = step.items[0];
515
+ const itemData = item.data;
516
+ // Bitcoin transaction (PSBT)
517
+ if ('psbt' in itemData || 'hex' in itemData || 'data' in itemData) {
518
+ setCurrentStep(step.id === 'approve'
519
+ ? 'Requesting approval...'
520
+ : step.id === 'deposit'
521
+ ? 'Requesting deposit...'
522
+ : 'Requesting bridge...');
523
+ onStatus?.(step.id === 'approve'
524
+ ? 'Requesting approval...'
525
+ : step.id === 'deposit'
526
+ ? 'Requesting deposit...'
527
+ : 'Requesting bridge...');
528
+ // Convert relay step to BridgeTransaction
529
+ const bitcoinTx = convertRelayBitcoinStepToTransaction(itemData, N_RELAY_CHAIN_ID_BITCOIN);
530
+ // Execute Bitcoin transaction
531
+ await bitcoinExecutor(bitcoinTx);
532
+ }
533
+ }
534
+ // Find request ID for status monitoring
535
+ const requestId = relayQuote.steps.find((s) => s.requestId)?.requestId;
536
+ if (!requestId) {
537
+ throw new Error('Missing relay.link request ID');
538
+ }
539
+ // Monitor bridge status
540
+ const depositTxHash = await monitorRelayBridgeStatus(requestId, setCurrentStep, onStatus);
541
+ return {
542
+ depositTxHash: depositTxHash,
543
+ provider: 'relay',
544
+ };
545
+ }
546
+ else {
547
+ throw new Error(`Unsupported bridge provider: ${selectedProvider}`);
548
+ }
549
+ }
550
+ catch (error) {
551
+ console.error('Bitcoin bridge execution failed:', {
552
+ sourceAsset,
553
+ sourceAmount,
554
+ usdcAmount,
555
+ bitcoinSenderAddress,
556
+ evmSignerAddress,
557
+ error: error instanceof Error ? error.message : String(error),
558
+ stack: error instanceof Error ? error.stack : undefined,
559
+ });
560
+ throw error;
561
+ }
562
+ }, [bitcoinConnector, bitcoinConnection, setCurrentStep, onStatus, depositorAddress]);
384
563
  /**
385
564
  * Execute EVM bridge transaction
386
565
  *
@@ -508,6 +687,7 @@ export function useBridgeExecution(walletClient, connector, solanaConnector, sol
508
687
  }, [walletClient, connector, setCurrentStep, onStatus, depositorAddress]);
509
688
  return {
510
689
  executeSolanaBridge,
690
+ executeBitcoinBridge,
511
691
  executeEvmBridge,
512
692
  };
513
693
  }