@shapeshiftoss/swap-widget 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/Dockerfile +87 -0
  2. package/README.md +519 -0
  3. package/dist/assets/PhArrowCircleDown-B305t-Sh.js +14 -0
  4. package/dist/assets/PhArrowClockwise-CJPxQhr_.js +14 -0
  5. package/dist/assets/PhArrowDown-C4hT9BVu.js +14 -0
  6. package/dist/assets/PhArrowLeft-Way4Cu-r.js +14 -0
  7. package/dist/assets/PhArrowRight-Cp5ZME0F.js +14 -0
  8. package/dist/assets/PhArrowSquareOut-NnrK69c5.js +14 -0
  9. package/dist/assets/PhArrowUp-Dph6j8rm.js +14 -0
  10. package/dist/assets/PhArrowUpRight-BpJjRRRh.js +14 -0
  11. package/dist/assets/PhArrowsClockwise-CFUT8X7R.js +14 -0
  12. package/dist/assets/PhArrowsDownUp-BjBI4ckC.js +14 -0
  13. package/dist/assets/PhArrowsLeftRight-CuLpbKRk.js +14 -0
  14. package/dist/assets/PhBank-Dchr4XQJ.js +14 -0
  15. package/dist/assets/PhBrowser-dUu2TrC1.js +14 -0
  16. package/dist/assets/PhCaretDown-78IuKU3V.js +14 -0
  17. package/dist/assets/PhCaretLeft-_aiyH7n1.js +14 -0
  18. package/dist/assets/PhCaretRight-8zJQEuDX.js +14 -0
  19. package/dist/assets/PhCaretUp-CiCxKWwN.js +14 -0
  20. package/dist/assets/PhCheck-CcADx8br.js +14 -0
  21. package/dist/assets/PhCircleHalf-BYzSzd2h.js +14 -0
  22. package/dist/assets/PhClock-UItiXeJq.js +14 -0
  23. package/dist/assets/PhCompass-CUy3uA7t.js +14 -0
  24. package/dist/assets/PhCopy-C-PDgSlZ.js +14 -0
  25. package/dist/assets/PhCreditCard-CWRaUT6F.js +14 -0
  26. package/dist/assets/PhCurrencyDollar-Cqla3AXT.js +14 -0
  27. package/dist/assets/PhDesktop-BBIPrYLl.js +14 -0
  28. package/dist/assets/PhDeviceMobile-BdsPaATg.js +14 -0
  29. package/dist/assets/PhDotsThree-BMXCQl-0.js +14 -0
  30. package/dist/assets/PhEnvelope-yUsis6yU.js +14 -0
  31. package/dist/assets/PhFunnelSimple-Ba7BbWTp.js +14 -0
  32. package/dist/assets/PhGlobe-DgLxBk5U.js +14 -0
  33. package/dist/assets/PhIdentificationCard-Czjmq9Z3.js +14 -0
  34. package/dist/assets/PhImage-BAJ_erz6.js +14 -0
  35. package/dist/assets/PhInfo-Cr4DhVUE.js +14 -0
  36. package/dist/assets/PhLightbulb-BX_7duTr.js +14 -0
  37. package/dist/assets/PhMagnifyingGlass-Mygfryvz.js +14 -0
  38. package/dist/assets/PhPaperPlaneRight-BeuCA660.js +14 -0
  39. package/dist/assets/PhPlus-BMjcHjsT.js +14 -0
  40. package/dist/assets/PhPower-CGffGmdb.js +14 -0
  41. package/dist/assets/PhPuzzlePiece-wm_GpVSi.js +14 -0
  42. package/dist/assets/PhQrCode-DRwRzXCH.js +14 -0
  43. package/dist/assets/PhQuestion-M09j2U0T.js +14 -0
  44. package/dist/assets/PhQuestionMark-BP9EMB6h.js +14 -0
  45. package/dist/assets/PhSealCheck-BfG80yKn.js +14 -0
  46. package/dist/assets/PhSignOut-C5YOWlPT.js +14 -0
  47. package/dist/assets/PhSpinner-BXooDYzU.js +14 -0
  48. package/dist/assets/PhTrash-Biq6rmmL.js +14 -0
  49. package/dist/assets/PhUser-CAgZSpGe.js +14 -0
  50. package/dist/assets/PhVault-l2PHF38a.js +14 -0
  51. package/dist/assets/PhWallet-w25D16It.js +14 -0
  52. package/dist/assets/PhWarning-DPEydIPw.js +14 -0
  53. package/dist/assets/PhWarningCircle-BqjT745H.js +14 -0
  54. package/dist/assets/PhX-s-SmVExa.js +14 -0
  55. package/dist/assets/SwapController-If0bFrNc.js +1 -0
  56. package/dist/assets/ccip-BBB7uCdL.js +1 -0
  57. package/dist/assets/ccip-CkkdfKKI.js +1 -0
  58. package/dist/assets/data-capture-D4gAy-Yc.js +242 -0
  59. package/dist/assets/email-B1AJehis.js +90 -0
  60. package/dist/assets/embedded-wallet-Q6DQopDk.js +410 -0
  61. package/dist/assets/features-B6JAPAFb.js +5 -0
  62. package/dist/assets/index-5jd8RPN9.js +7428 -0
  63. package/dist/assets/index-B4kNtSTk.js +1080 -0
  64. package/dist/assets/index-B753eAob.js +1 -0
  65. package/dist/assets/index-BZoLTv77.js +3 -0
  66. package/dist/assets/index-Bcw9UURX.js +3 -0
  67. package/dist/assets/index-BkrV-ahK.js +2 -0
  68. package/dist/assets/index-CRQzQ_up.js +9 -0
  69. package/dist/assets/index-CSW0Gt5y.js +132 -0
  70. package/dist/assets/index-CZ4jTIsk.js +138 -0
  71. package/dist/assets/index-Cwa8SB3h.js +61 -0
  72. package/dist/assets/index-DEwQgT4r.js +78 -0
  73. package/dist/assets/index-DVfQiQ1d.js +1 -0
  74. package/dist/assets/index-cV3UdaMs.css +1 -0
  75. package/dist/assets/index-hAmu622O.js +43 -0
  76. package/dist/assets/onramp-XcPNCLRU.js +422 -0
  77. package/dist/assets/pay-with-exchange-BeHt4OF4.js +280 -0
  78. package/dist/assets/property-C9JG5tag.js +27 -0
  79. package/dist/assets/receive-7ff4he2S.js +131 -0
  80. package/dist/assets/send-CkMBIRXT.js +554 -0
  81. package/dist/assets/socials-DmmxuLrP.js +250 -0
  82. package/dist/assets/swaps-IHAYRQ0x.js +1050 -0
  83. package/dist/assets/transactions-9Cxdk7o9.js +16 -0
  84. package/dist/assets/v4-BOvFkHkt.js +1 -0
  85. package/dist/assets/w3m-modal-kJzFnBfB.js +891 -0
  86. package/dist/assets/wallet-standard-Dv8LMoUB.js +1 -0
  87. package/dist/favicon.png +0 -0
  88. package/dist/favicon.svg +11 -0
  89. package/dist/icon-192x192.png +0 -0
  90. package/dist/icon-512x512.png +0 -0
  91. package/dist/index.html +33 -0
  92. package/index.html +32 -0
  93. package/package.json +59 -0
  94. package/public/favicon.png +0 -0
  95. package/public/favicon.svg +11 -0
  96. package/public/icon-192x192.png +0 -0
  97. package/public/icon-512x512.png +0 -0
  98. package/railway.toml +10 -0
  99. package/src/api/client.ts +95 -0
  100. package/src/components/AddressInputModal.css +173 -0
  101. package/src/components/AddressInputModal.tsx +250 -0
  102. package/src/components/ApprovalStep.tsx +92 -0
  103. package/src/components/ExecutionStep.tsx +27 -0
  104. package/src/components/InputStep.tsx +431 -0
  105. package/src/components/QuoteSelector.css +146 -0
  106. package/src/components/QuoteSelector.tsx +137 -0
  107. package/src/components/QuotesModal.css +250 -0
  108. package/src/components/QuotesModal.tsx +182 -0
  109. package/src/components/SettingsModal.css +145 -0
  110. package/src/components/SettingsModal.tsx +180 -0
  111. package/src/components/StatusStep.tsx +139 -0
  112. package/src/components/SwapWidget.css +713 -0
  113. package/src/components/SwapWidget.tsx +607 -0
  114. package/src/components/TokenSelectModal.css +438 -0
  115. package/src/components/TokenSelectModal.tsx +487 -0
  116. package/src/components/WalletProvider.tsx +92 -0
  117. package/src/config/appkit.ts +104 -0
  118. package/src/config/wagmi.ts +27 -0
  119. package/src/constants/chains.ts +72 -0
  120. package/src/constants/defaults.ts +28 -0
  121. package/src/constants/swappers.ts +48 -0
  122. package/src/constants/viemChains.ts +69 -0
  123. package/src/contexts/SwapWalletContext.tsx +25 -0
  124. package/src/demo/App.css +458 -0
  125. package/src/demo/App.tsx +481 -0
  126. package/src/demo/main.tsx +25 -0
  127. package/src/hooks/useAssets.ts +180 -0
  128. package/src/hooks/useBalances.ts +822 -0
  129. package/src/hooks/useBitcoinSigning.ts +276 -0
  130. package/src/hooks/useMarketData.ts +169 -0
  131. package/src/hooks/useSolanaSigning.ts +275 -0
  132. package/src/hooks/useStatusPolling.ts +137 -0
  133. package/src/hooks/useSwapApproval.ts +106 -0
  134. package/src/hooks/useSwapDisplayValues.ts +157 -0
  135. package/src/hooks/useSwapExecution.ts +199 -0
  136. package/src/hooks/useSwapHandlers.ts +119 -0
  137. package/src/hooks/useSwapQuote.ts +71 -0
  138. package/src/hooks/useSwapQuoting.ts +102 -0
  139. package/src/hooks/useSwapRates.ts +61 -0
  140. package/src/index.ts +43 -0
  141. package/src/machines/SwapMachineContext.ts +5 -0
  142. package/src/machines/__tests__/guards.test.ts +228 -0
  143. package/src/machines/__tests__/setup.test.ts +7 -0
  144. package/src/machines/__tests__/swapMachine.test.ts +544 -0
  145. package/src/machines/__tests__/types.test.ts +124 -0
  146. package/src/machines/guards.ts +33 -0
  147. package/src/machines/swapMachine.ts +288 -0
  148. package/src/machines/types.ts +54 -0
  149. package/src/services/transactionStatus.ts +322 -0
  150. package/src/types/index.ts +361 -0
  151. package/src/utils/addressValidation.ts +201 -0
  152. package/src/utils/redirect.ts +45 -0
  153. package/src/vite-env.d.ts +10 -0
  154. package/tsconfig.json +23 -0
  155. package/tsconfig.node.json +10 -0
  156. package/vite.config.ts +101 -0
  157. package/vitest.config.ts +18 -0
@@ -0,0 +1,137 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
4
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
5
+ import type { CheckStatusParams } from '../services/transactionStatus'
6
+ import { checkTransactionStatus } from '../services/transactionStatus'
7
+ import { getEvmNetworkId } from '../types'
8
+
9
+ const POLL_INTERVAL_MS = 5000
10
+
11
+ type UseStatusPollingParams = {
12
+ onSwapSuccess?: (txHash: string) => void
13
+ onSwapError?: (error: Error) => void
14
+ refetchSellBalance?: () => void
15
+ refetchBuyBalance?: () => void
16
+ }
17
+
18
+ export const useStatusPolling = ({
19
+ onSwapSuccess,
20
+ onSwapError,
21
+ refetchSellBalance,
22
+ refetchBuyBalance,
23
+ }: UseStatusPollingParams) => {
24
+ const stateValue = SwapMachineCtx.useSelector(s => s.value)
25
+ const context = SwapMachineCtx.useSelector(s => s.context)
26
+ const actorRef = SwapMachineCtx.useActorRef()
27
+
28
+ const { solana } = useSwapWallet()
29
+ const solanaConnection = solana.connection
30
+
31
+ const pollingRef = useRef(false)
32
+ useEffect(() => {
33
+ const snap = actorRef.getSnapshot()
34
+ if (!snap.matches('polling_status')) {
35
+ pollingRef.current = false
36
+ return
37
+ }
38
+ if (pollingRef.current) return
39
+ pollingRef.current = true
40
+
41
+ let stopped = false
42
+
43
+ const poll = async () => {
44
+ if (stopped || !context.txHash) return
45
+
46
+ try {
47
+ let statusParams: CheckStatusParams
48
+
49
+ if (context.isSellAssetEvm) {
50
+ statusParams = {
51
+ txHash: context.txHash,
52
+ chainType: 'evm',
53
+ chainId: getEvmNetworkId(context.sellAsset.chainId),
54
+ }
55
+ } else if (context.isSellAssetUtxo) {
56
+ statusParams = {
57
+ txHash: context.txHash,
58
+ chainType: 'utxo',
59
+ }
60
+ } else if (context.isSellAssetSolana) {
61
+ statusParams = {
62
+ txHash: context.txHash,
63
+ chainType: 'solana',
64
+ connection: solanaConnection as CheckStatusParams['connection'],
65
+ }
66
+ } else {
67
+ console.warn(
68
+ `[swap-widget] Status polling not implemented for chainType "${context.chainType}". Auto-confirming.`,
69
+ )
70
+ actorRef.send({ type: 'STATUS_CONFIRMED' })
71
+ return
72
+ }
73
+
74
+ const result = await checkTransactionStatus(statusParams)
75
+
76
+ if (stopped) return
77
+
78
+ if (result.status === 'confirmed') {
79
+ actorRef.send({ type: 'STATUS_CONFIRMED' })
80
+ return
81
+ }
82
+
83
+ if (result.status === 'failed') {
84
+ actorRef.send({ type: 'STATUS_FAILED', error: result.error ?? 'Transaction failed' })
85
+ return
86
+ }
87
+
88
+ setTimeout(poll, POLL_INTERVAL_MS)
89
+ } catch (err) {
90
+ if (stopped) return
91
+ const errorMessage = err instanceof Error ? err.message : 'Unknown polling error'
92
+ actorRef.send({ type: 'STATUS_FAILED', error: errorMessage })
93
+ }
94
+ }
95
+
96
+ poll()
97
+
98
+ return () => {
99
+ stopped = true
100
+ pollingRef.current = false
101
+ }
102
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; other deps are stable refs or read from snapshot
103
+ }, [stateValue])
104
+
105
+ const completionRef = useRef(false)
106
+ useEffect(() => {
107
+ const snap = actorRef.getSnapshot()
108
+ if (!snap.matches('complete')) {
109
+ completionRef.current = false
110
+ return
111
+ }
112
+ if (completionRef.current) return
113
+ completionRef.current = true
114
+
115
+ if (context.txHash) {
116
+ onSwapSuccess?.(context.txHash)
117
+ }
118
+
119
+ refetchSellBalance?.()
120
+ refetchBuyBalance?.()
121
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; callbacks are stable
122
+ }, [stateValue])
123
+
124
+ const errorRef = useRef(false)
125
+ useEffect(() => {
126
+ const snap = actorRef.getSnapshot()
127
+ if (!snap.matches('error')) {
128
+ errorRef.current = false
129
+ return
130
+ }
131
+ if (errorRef.current) return
132
+ errorRef.current = true
133
+
134
+ onSwapError?.(new Error(context.error ?? 'Unknown error'))
135
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; callbacks are stable
136
+ }, [stateValue])
137
+ }
@@ -0,0 +1,106 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { WalletClient } from 'viem'
3
+ import { createPublicClient, encodeFunctionData, erc20Abi, http } from 'viem'
4
+
5
+ import { getBaseAsset } from '../constants/chains'
6
+ import { switchOrAddChain, VIEM_CHAINS_BY_ID } from '../constants/viemChains'
7
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
8
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
9
+ import { getEvmNetworkId } from '../types'
10
+
11
+ export const useSwapApproval = () => {
12
+ const stateValue = SwapMachineCtx.useSelector(s => s.value)
13
+ const context = SwapMachineCtx.useSelector(s => s.context)
14
+ const actorRef = SwapMachineCtx.useActorRef()
15
+
16
+ const { walletClient, walletAddress } = useSwapWallet()
17
+
18
+ const approvingRef = useRef(false)
19
+
20
+ useEffect(() => {
21
+ const snap = actorRef.getSnapshot()
22
+ if (!snap.matches('approving') || approvingRef.current) return
23
+ approvingRef.current = true
24
+
25
+ const executeApproval = async () => {
26
+ try {
27
+ if (!walletClient || !walletAddress) {
28
+ actorRef.send({ type: 'APPROVAL_ERROR', error: 'No wallet connected' })
29
+ return
30
+ }
31
+
32
+ const quote = context.quote
33
+ if (!quote?.approval?.spender) {
34
+ actorRef.send({ type: 'APPROVAL_ERROR', error: 'No approval data in quote' })
35
+ return
36
+ }
37
+
38
+ const sellAssetAddress = context.sellAsset.assetId.split('/')[1]?.split(':')[1]
39
+ if (!sellAssetAddress || !/^0x[a-fA-F0-9]{40}$/.test(sellAssetAddress)) {
40
+ actorRef.send({
41
+ type: 'APPROVAL_ERROR',
42
+ error: 'Approval not applicable for native assets',
43
+ })
44
+ return
45
+ }
46
+
47
+ const requiredChainId = getEvmNetworkId(context.sellAsset.chainId)
48
+ const client = walletClient as WalletClient
49
+
50
+ const currentChainId = await client.getChainId()
51
+ if (currentChainId !== requiredChainId) {
52
+ await switchOrAddChain(client, requiredChainId)
53
+ }
54
+
55
+ const baseAsset = getBaseAsset(context.sellAsset.chainId)
56
+ const nativeCurrency = baseAsset
57
+ ? { name: baseAsset.name, symbol: baseAsset.symbol, decimals: baseAsset.precision }
58
+ : { name: 'ETH', symbol: 'ETH', decimals: 18 }
59
+
60
+ const viemChain = VIEM_CHAINS_BY_ID[requiredChainId]
61
+ const chain = viemChain ?? {
62
+ id: requiredChainId,
63
+ name: baseAsset?.networkName ?? baseAsset?.name ?? 'Chain',
64
+ nativeCurrency,
65
+ rpcUrls: { default: { http: [] } },
66
+ }
67
+
68
+ if (!context.sellAmountBaseUnit || context.sellAmountBaseUnit === '0') {
69
+ actorRef.send({ type: 'APPROVAL_ERROR', error: 'No sell amount specified' })
70
+ return
71
+ }
72
+
73
+ const approvalData = encodeFunctionData({
74
+ abi: erc20Abi,
75
+ functionName: 'approve',
76
+ args: [quote.approval.spender as `0x${string}`, BigInt(context.sellAmountBaseUnit)],
77
+ })
78
+
79
+ const approvalHash = await client.sendTransaction({
80
+ to: sellAssetAddress as `0x${string}`,
81
+ data: approvalData,
82
+ value: BigInt(0),
83
+ chain,
84
+ account: walletAddress as `0x${string}`,
85
+ })
86
+
87
+ const rpcUrl = chain.rpcUrls?.default?.http?.[0]
88
+ const publicClient = createPublicClient({
89
+ chain,
90
+ transport: rpcUrl ? http(rpcUrl) : http(),
91
+ })
92
+ await publicClient.waitForTransactionReceipt({ hash: approvalHash })
93
+
94
+ actorRef.send({ type: 'APPROVAL_SUCCESS', txHash: approvalHash })
95
+ } catch (error) {
96
+ const errorMessage = error instanceof Error ? error.message : 'Approval failed'
97
+ actorRef.send({ type: 'APPROVAL_ERROR', error: errorMessage })
98
+ } finally {
99
+ approvingRef.current = false
100
+ }
101
+ }
102
+
103
+ executeApproval()
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; other deps are stable refs read from snapshot
105
+ }, [stateValue])
106
+ }
@@ -0,0 +1,157 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import type { ApiClient } from '../api/client'
4
+ import { getBaseAsset } from '../constants/chains'
5
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
6
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
7
+ import { formatAmount, getChainType } from '../types'
8
+ import { useChainInfo } from './useAssets'
9
+ import { useMultiChainBalance } from './useBalances'
10
+ import { formatUsdValue, useMarketData } from './useMarketData'
11
+ import { useSwapRates } from './useSwapRates'
12
+
13
+ type UseSwapDisplayValuesParams = {
14
+ apiClient: ApiClient
15
+ }
16
+
17
+ export const useSwapDisplayValues = ({ apiClient }: UseSwapDisplayValuesParams) => {
18
+ const sellAsset = SwapMachineCtx.useSelector(s => s.context.sellAsset)
19
+ const buyAsset = SwapMachineCtx.useSelector(s => s.context.buyAsset)
20
+ const sellAmountBaseUnit = SwapMachineCtx.useSelector(s => s.context.sellAmountBaseUnit)
21
+ const isSellAssetEvm = SwapMachineCtx.useSelector(s => s.context.isSellAssetEvm)
22
+ const isSellAssetUtxo = SwapMachineCtx.useSelector(s => s.context.isSellAssetUtxo)
23
+ const isSellAssetSolana = SwapMachineCtx.useSelector(s => s.context.isSellAssetSolana)
24
+ const selectedRate = SwapMachineCtx.useSelector(s => s.context.selectedRate)
25
+
26
+ const { walletAddress, effectiveReceiveAddress, bitcoin, solana } = useSwapWallet()
27
+ const bitcoinAddress = bitcoin.address
28
+ const solanaAddress = solana.address
29
+
30
+ const buyChainType = getChainType(buyAsset.chainId)
31
+
32
+ const {
33
+ data: rates,
34
+ isLoading: isLoadingRates,
35
+ error: ratesError,
36
+ } = useSwapRates(apiClient, {
37
+ sellAssetId: sellAsset.assetId,
38
+ buyAssetId: buyAsset.assetId,
39
+ sellAmountCryptoBaseUnit: sellAmountBaseUnit,
40
+ enabled:
41
+ !!sellAmountBaseUnit &&
42
+ sellAmountBaseUnit !== '0' &&
43
+ (isSellAssetEvm || isSellAssetUtxo || isSellAssetSolana),
44
+ })
45
+
46
+ const {
47
+ data: sellAssetBalance,
48
+ isLoading: isSellBalanceLoading,
49
+ refetch: refetchSellBalance,
50
+ } = useMultiChainBalance(
51
+ walletAddress,
52
+ bitcoinAddress,
53
+ solanaAddress,
54
+ sellAsset.assetId,
55
+ sellAsset.precision,
56
+ )
57
+
58
+ const buyAssetAddressForBalance = useMemo(() => {
59
+ if (buyChainType === 'evm') return effectiveReceiveAddress || walletAddress
60
+ if (buyChainType === 'utxo') return effectiveReceiveAddress || bitcoinAddress
61
+ if (buyChainType === 'solana') return effectiveReceiveAddress || solanaAddress
62
+ return effectiveReceiveAddress
63
+ }, [buyChainType, effectiveReceiveAddress, walletAddress, bitcoinAddress, solanaAddress])
64
+
65
+ const {
66
+ data: buyAssetBalance,
67
+ isLoading: isBuyBalanceLoading,
68
+ refetch: refetchBuyBalance,
69
+ } = useMultiChainBalance(
70
+ buyChainType === 'evm' ? buyAssetAddressForBalance : walletAddress,
71
+ buyChainType === 'utxo' ? buyAssetAddressForBalance : bitcoinAddress,
72
+ buyChainType === 'solana' ? buyAssetAddressForBalance : solanaAddress,
73
+ buyAsset.assetId,
74
+ buyAsset.precision,
75
+ )
76
+
77
+ const { data: sellChainInfo } = useChainInfo(sellAsset.chainId)
78
+ const { data: buyChainInfo } = useChainInfo(buyAsset.chainId)
79
+
80
+ const displayRate = useMemo(() => selectedRate ?? rates?.[0], [selectedRate, rates])
81
+ const buyAmount = displayRate?.buyAmountCryptoBaseUnit
82
+
83
+ const sellChainNativeAsset = useMemo(() => getBaseAsset(sellAsset.chainId), [sellAsset.chainId])
84
+
85
+ const assetIdsForPrices = useMemo(() => {
86
+ const ids = [sellAsset.assetId, buyAsset.assetId]
87
+ if (sellChainNativeAsset && sellChainNativeAsset.assetId !== sellAsset.assetId) {
88
+ ids.push(sellChainNativeAsset.assetId)
89
+ }
90
+ return ids
91
+ }, [sellAsset.assetId, buyAsset.assetId, sellChainNativeAsset])
92
+
93
+ const { data: marketData } = useMarketData(assetIdsForPrices)
94
+ const sellAssetUsdPrice = marketData?.[sellAsset.assetId]?.price
95
+ const buyAssetUsdPrice = marketData?.[buyAsset.assetId]?.price
96
+ const nativeAssetUsdPrice = sellChainNativeAsset
97
+ ? marketData?.[sellChainNativeAsset.assetId]?.price
98
+ : undefined
99
+
100
+ const networkFeeDisplay = useMemo(() => {
101
+ const feeBaseUnit = displayRate?.networkFeeCryptoBaseUnit
102
+ if (!feeBaseUnit || feeBaseUnit === '0' || !sellChainNativeAsset) return undefined
103
+ const formatted = formatAmount(feeBaseUnit, sellChainNativeAsset.precision, 6)
104
+ const cryptoPart = `${formatted} ${sellChainNativeAsset.symbol}`
105
+ if (!nativeAssetUsdPrice) return cryptoPart
106
+ const fiatValue = formatUsdValue(
107
+ feeBaseUnit,
108
+ sellChainNativeAsset.precision,
109
+ nativeAssetUsdPrice,
110
+ )
111
+ return `${cryptoPart} (${fiatValue})`
112
+ }, [displayRate?.networkFeeCryptoBaseUnit, sellChainNativeAsset, nativeAssetUsdPrice])
113
+
114
+ const sellUsdValue = useMemo(() => {
115
+ if (!sellAmountBaseUnit || !sellAssetUsdPrice) return '$0.00'
116
+ return formatUsdValue(sellAmountBaseUnit, sellAsset.precision, sellAssetUsdPrice)
117
+ }, [sellAmountBaseUnit, sellAsset.precision, sellAssetUsdPrice])
118
+
119
+ const buyUsdValue = useMemo(() => {
120
+ if (!buyAmount || !buyAssetUsdPrice) return '$0.00'
121
+ return formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
122
+ }, [buyAmount, buyAsset.precision, buyAssetUsdPrice])
123
+
124
+ const sellBalanceFiatValue = useMemo(() => {
125
+ if (!sellAssetBalance?.balance || !sellAssetUsdPrice) return undefined
126
+ return formatUsdValue(sellAssetBalance.balance, sellAsset.precision, sellAssetUsdPrice)
127
+ }, [sellAssetBalance?.balance, sellAsset.precision, sellAssetUsdPrice])
128
+
129
+ const buyBalanceFiatValue = useMemo(() => {
130
+ if (!buyAssetBalance?.balance || !buyAssetUsdPrice) return undefined
131
+ return formatUsdValue(buyAssetBalance.balance, buyAsset.precision, buyAssetUsdPrice)
132
+ }, [buyAssetBalance?.balance, buyAsset.precision, buyAssetUsdPrice])
133
+
134
+ return {
135
+ rates,
136
+ isLoadingRates,
137
+ ratesError,
138
+ sellAssetBalance,
139
+ isSellBalanceLoading,
140
+ refetchSellBalance,
141
+ buyAssetBalance,
142
+ isBuyBalanceLoading,
143
+ refetchBuyBalance,
144
+ sellChainInfo,
145
+ buyChainInfo,
146
+ displayRate,
147
+ buyAmount,
148
+ sellChainNativeAsset,
149
+ networkFeeDisplay,
150
+ sellUsdValue,
151
+ buyUsdValue,
152
+ sellAssetUsdPrice,
153
+ buyAssetUsdPrice,
154
+ sellBalanceFiatValue,
155
+ buyBalanceFiatValue,
156
+ }
157
+ }
@@ -0,0 +1,199 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { WalletClient } from 'viem'
3
+
4
+ import { switchOrAddChain, VIEM_CHAINS_BY_ID } from '../constants/viemChains'
5
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
6
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
7
+ import { getEvmNetworkId } from '../types'
8
+
9
+ export const useSwapExecution = () => {
10
+ const stateValue = SwapMachineCtx.useSelector(s => s.value)
11
+ const context = SwapMachineCtx.useSelector(s => s.context)
12
+ const actorRef = SwapMachineCtx.useActorRef()
13
+
14
+ const { walletClient, walletAddress, bitcoin, solana } = useSwapWallet()
15
+
16
+ const executingRef = useRef(false)
17
+
18
+ useEffect(() => {
19
+ const snap = actorRef.getSnapshot()
20
+ if (!snap.matches('executing') || executingRef.current) return
21
+ executingRef.current = true
22
+
23
+ const executeSwap = async () => {
24
+ try {
25
+ const quote = context.quote
26
+ if (!quote) {
27
+ actorRef.send({ type: 'EXECUTE_ERROR', error: 'No quote available' })
28
+ return
29
+ }
30
+
31
+ if (context.isSellAssetEvm) {
32
+ if (!walletClient || !walletAddress) {
33
+ actorRef.send({ type: 'EXECUTE_ERROR', error: 'No wallet connected' })
34
+ return
35
+ }
36
+
37
+ const requiredChainId = getEvmNetworkId(context.sellAsset.chainId)
38
+ const client = walletClient as WalletClient
39
+
40
+ const currentChainId = await client.getChainId()
41
+ if (currentChainId !== requiredChainId) {
42
+ await switchOrAddChain(client, requiredChainId)
43
+ }
44
+
45
+ const viemChain = VIEM_CHAINS_BY_ID[requiredChainId]
46
+ if (!viemChain) {
47
+ throw new Error(
48
+ `Unsupported EVM chain ID ${requiredChainId}. No RPC configuration available.`,
49
+ )
50
+ }
51
+ const chain = viemChain
52
+
53
+ const outerStep = quote.steps?.[0]
54
+ const innerStep = quote.quote?.steps?.[0]
55
+
56
+ const transactionData =
57
+ quote.transactionData ??
58
+ outerStep?.transactionData ??
59
+ outerStep?.relayTransactionMetadata ??
60
+ outerStep?.butterSwapTransactionMetadata ??
61
+ innerStep?.transactionData ??
62
+ innerStep?.relayTransactionMetadata ??
63
+ innerStep?.butterSwapTransactionMetadata
64
+
65
+ if (!transactionData) {
66
+ throw new Error(
67
+ `No transaction data returned. Response keys: ${Object.keys(quote).join(', ')}`,
68
+ )
69
+ }
70
+
71
+ const txData = transactionData as {
72
+ type?: string
73
+ to: string
74
+ data: string
75
+ value?: string
76
+ gasLimit?: string
77
+ }
78
+
79
+ if (txData.type && txData.type !== 'evm') {
80
+ throw new Error(
81
+ `Expected EVM transaction data but got type '${txData.type}'. Check swapper configuration.`,
82
+ )
83
+ }
84
+
85
+ if (!txData.to || !txData.data) {
86
+ throw new Error(`Invalid EVM transaction data: missing 'to' or 'data' fields.`)
87
+ }
88
+
89
+ const txHash = await client.sendTransaction({
90
+ to: txData.to as `0x${string}`,
91
+ data: txData.data as `0x${string}`,
92
+ value: BigInt(txData.value ?? '0'),
93
+ gas: txData.gasLimit ? BigInt(txData.gasLimit) : undefined,
94
+ chain,
95
+ account: walletAddress as `0x${string}`,
96
+ })
97
+
98
+ actorRef.send({ type: 'EXECUTE_SUCCESS', txHash })
99
+ } else if (context.isSellAssetUtxo) {
100
+ if (!bitcoin.isConnected || !bitcoin.address) {
101
+ actorRef.send({ type: 'EXECUTE_ERROR', error: 'Bitcoin wallet not connected' })
102
+ return
103
+ }
104
+
105
+ bitcoin.reset()
106
+
107
+ const outerStep = quote.steps?.[0]
108
+ const innerStep = quote.quote?.steps?.[0]
109
+ const transactionData =
110
+ quote.transactionData ?? outerStep?.transactionData ?? innerStep?.transactionData
111
+
112
+ if (!transactionData) {
113
+ throw new Error(
114
+ `No transaction data returned. Response keys: ${Object.keys(quote).join(', ')}`,
115
+ )
116
+ }
117
+
118
+ let txid: string
119
+
120
+ if (transactionData.type === 'utxo_deposit') {
121
+ txid = await bitcoin.sendTransfer({
122
+ recipientAddress: transactionData.depositAddress,
123
+ amount: transactionData.value,
124
+ memo: transactionData.memo,
125
+ })
126
+ } else {
127
+ throw new Error(
128
+ `Unsupported UTXO transaction type: ${transactionData.type}. Only utxo_deposit is supported.`,
129
+ )
130
+ }
131
+
132
+ actorRef.send({ type: 'EXECUTE_SUCCESS', txHash: txid })
133
+ } else if (context.isSellAssetSolana) {
134
+ if (!solana.isConnected || !solana.address || !solana.connection) {
135
+ actorRef.send({ type: 'EXECUTE_ERROR', error: 'Solana wallet not connected' })
136
+ return
137
+ }
138
+
139
+ solana.reset()
140
+
141
+ const outerStep = quote.steps?.[0]
142
+ const innerStep = quote.quote?.steps?.[0]
143
+
144
+ const txData = outerStep?.transactionData ?? innerStep?.transactionData
145
+
146
+ if (!txData || txData.type !== 'solana') {
147
+ throw new Error(
148
+ `No Solana transaction metadata returned. Response keys: ${Object.keys(quote).join(
149
+ ', ',
150
+ )}`,
151
+ )
152
+ }
153
+
154
+ const { Transaction, PublicKey, TransactionInstruction } = await import('@solana/web3.js')
155
+
156
+ const instructions = txData.instructions.map(ix => {
157
+ const keys = ix.keys.map(key => ({
158
+ pubkey: new PublicKey(key.pubkey),
159
+ isSigner: key.isSigner,
160
+ isWritable: key.isWritable,
161
+ }))
162
+
163
+ return new TransactionInstruction({
164
+ keys,
165
+ programId: new PublicKey(ix.programId),
166
+ data: Buffer.from(ix.data, 'base64'),
167
+ })
168
+ })
169
+
170
+ const transaction = new Transaction().add(...instructions)
171
+ const conn = solana.connection as {
172
+ getLatestBlockhash: (commitment: string) => Promise<{ blockhash: string }>
173
+ }
174
+ const { blockhash } = await conn.getLatestBlockhash('confirmed')
175
+ transaction.recentBlockhash = blockhash
176
+ transaction.feePayer = new PublicKey(solana.address)
177
+
178
+ const signature = await solana.sendTransaction({ transaction })
179
+ actorRef.send({ type: 'EXECUTE_SUCCESS', txHash: signature })
180
+ } else {
181
+ actorRef.send({ type: 'EXECUTE_ERROR', error: 'Unsupported chain type' })
182
+ }
183
+ } catch (error) {
184
+ const errorMessage =
185
+ error instanceof Error
186
+ ? error.message
187
+ : typeof error === 'string'
188
+ ? error
189
+ : JSON.stringify(error) ?? 'Transaction failed'
190
+ actorRef.send({ type: 'EXECUTE_ERROR', error: errorMessage })
191
+ } finally {
192
+ executingRef.current = false
193
+ }
194
+ }
195
+
196
+ executeSwap()
197
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; other deps are stable refs read from snapshot
198
+ }, [stateValue])
199
+ }