@shapeshiftoss/swap-widget 0.0.2 → 0.2.0

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 (158) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +18 -11
  3. package/package.json +77 -28
  4. package/Dockerfile +0 -87
  5. package/dist/assets/PhArrowCircleDown-B305t-Sh.js +0 -14
  6. package/dist/assets/PhArrowClockwise-CJPxQhr_.js +0 -14
  7. package/dist/assets/PhArrowDown-C4hT9BVu.js +0 -14
  8. package/dist/assets/PhArrowLeft-Way4Cu-r.js +0 -14
  9. package/dist/assets/PhArrowRight-Cp5ZME0F.js +0 -14
  10. package/dist/assets/PhArrowSquareOut-NnrK69c5.js +0 -14
  11. package/dist/assets/PhArrowUp-Dph6j8rm.js +0 -14
  12. package/dist/assets/PhArrowUpRight-BpJjRRRh.js +0 -14
  13. package/dist/assets/PhArrowsClockwise-CFUT8X7R.js +0 -14
  14. package/dist/assets/PhArrowsDownUp-BjBI4ckC.js +0 -14
  15. package/dist/assets/PhArrowsLeftRight-CuLpbKRk.js +0 -14
  16. package/dist/assets/PhBank-Dchr4XQJ.js +0 -14
  17. package/dist/assets/PhBrowser-dUu2TrC1.js +0 -14
  18. package/dist/assets/PhCaretDown-78IuKU3V.js +0 -14
  19. package/dist/assets/PhCaretLeft-_aiyH7n1.js +0 -14
  20. package/dist/assets/PhCaretRight-8zJQEuDX.js +0 -14
  21. package/dist/assets/PhCaretUp-CiCxKWwN.js +0 -14
  22. package/dist/assets/PhCheck-CcADx8br.js +0 -14
  23. package/dist/assets/PhCircleHalf-BYzSzd2h.js +0 -14
  24. package/dist/assets/PhClock-UItiXeJq.js +0 -14
  25. package/dist/assets/PhCompass-CUy3uA7t.js +0 -14
  26. package/dist/assets/PhCopy-C-PDgSlZ.js +0 -14
  27. package/dist/assets/PhCreditCard-CWRaUT6F.js +0 -14
  28. package/dist/assets/PhCurrencyDollar-Cqla3AXT.js +0 -14
  29. package/dist/assets/PhDesktop-BBIPrYLl.js +0 -14
  30. package/dist/assets/PhDeviceMobile-BdsPaATg.js +0 -14
  31. package/dist/assets/PhDotsThree-BMXCQl-0.js +0 -14
  32. package/dist/assets/PhEnvelope-yUsis6yU.js +0 -14
  33. package/dist/assets/PhFunnelSimple-Ba7BbWTp.js +0 -14
  34. package/dist/assets/PhGlobe-DgLxBk5U.js +0 -14
  35. package/dist/assets/PhIdentificationCard-Czjmq9Z3.js +0 -14
  36. package/dist/assets/PhImage-BAJ_erz6.js +0 -14
  37. package/dist/assets/PhInfo-Cr4DhVUE.js +0 -14
  38. package/dist/assets/PhLightbulb-BX_7duTr.js +0 -14
  39. package/dist/assets/PhMagnifyingGlass-Mygfryvz.js +0 -14
  40. package/dist/assets/PhPaperPlaneRight-BeuCA660.js +0 -14
  41. package/dist/assets/PhPlus-BMjcHjsT.js +0 -14
  42. package/dist/assets/PhPower-CGffGmdb.js +0 -14
  43. package/dist/assets/PhPuzzlePiece-wm_GpVSi.js +0 -14
  44. package/dist/assets/PhQrCode-DRwRzXCH.js +0 -14
  45. package/dist/assets/PhQuestion-M09j2U0T.js +0 -14
  46. package/dist/assets/PhQuestionMark-BP9EMB6h.js +0 -14
  47. package/dist/assets/PhSealCheck-BfG80yKn.js +0 -14
  48. package/dist/assets/PhSignOut-C5YOWlPT.js +0 -14
  49. package/dist/assets/PhSpinner-BXooDYzU.js +0 -14
  50. package/dist/assets/PhTrash-Biq6rmmL.js +0 -14
  51. package/dist/assets/PhUser-CAgZSpGe.js +0 -14
  52. package/dist/assets/PhVault-l2PHF38a.js +0 -14
  53. package/dist/assets/PhWallet-w25D16It.js +0 -14
  54. package/dist/assets/PhWarning-DPEydIPw.js +0 -14
  55. package/dist/assets/PhWarningCircle-BqjT745H.js +0 -14
  56. package/dist/assets/PhX-s-SmVExa.js +0 -14
  57. package/dist/assets/SwapController-If0bFrNc.js +0 -1
  58. package/dist/assets/ccip-BBB7uCdL.js +0 -1
  59. package/dist/assets/ccip-CkkdfKKI.js +0 -1
  60. package/dist/assets/data-capture-D4gAy-Yc.js +0 -242
  61. package/dist/assets/email-B1AJehis.js +0 -90
  62. package/dist/assets/embedded-wallet-Q6DQopDk.js +0 -410
  63. package/dist/assets/features-B6JAPAFb.js +0 -5
  64. package/dist/assets/index-5jd8RPN9.js +0 -7428
  65. package/dist/assets/index-B4kNtSTk.js +0 -1080
  66. package/dist/assets/index-B753eAob.js +0 -1
  67. package/dist/assets/index-BZoLTv77.js +0 -3
  68. package/dist/assets/index-Bcw9UURX.js +0 -3
  69. package/dist/assets/index-BkrV-ahK.js +0 -2
  70. package/dist/assets/index-CRQzQ_up.js +0 -9
  71. package/dist/assets/index-CSW0Gt5y.js +0 -132
  72. package/dist/assets/index-CZ4jTIsk.js +0 -138
  73. package/dist/assets/index-Cwa8SB3h.js +0 -61
  74. package/dist/assets/index-DEwQgT4r.js +0 -78
  75. package/dist/assets/index-DVfQiQ1d.js +0 -1
  76. package/dist/assets/index-cV3UdaMs.css +0 -1
  77. package/dist/assets/index-hAmu622O.js +0 -43
  78. package/dist/assets/onramp-XcPNCLRU.js +0 -422
  79. package/dist/assets/pay-with-exchange-BeHt4OF4.js +0 -280
  80. package/dist/assets/property-C9JG5tag.js +0 -27
  81. package/dist/assets/receive-7ff4he2S.js +0 -131
  82. package/dist/assets/send-CkMBIRXT.js +0 -554
  83. package/dist/assets/socials-DmmxuLrP.js +0 -250
  84. package/dist/assets/swaps-IHAYRQ0x.js +0 -1050
  85. package/dist/assets/transactions-9Cxdk7o9.js +0 -16
  86. package/dist/assets/v4-BOvFkHkt.js +0 -1
  87. package/dist/assets/w3m-modal-kJzFnBfB.js +0 -891
  88. package/dist/assets/wallet-standard-Dv8LMoUB.js +0 -1
  89. package/dist/favicon.png +0 -0
  90. package/dist/favicon.svg +0 -11
  91. package/dist/icon-192x192.png +0 -0
  92. package/dist/icon-512x512.png +0 -0
  93. package/dist/index.html +0 -33
  94. package/index.html +0 -32
  95. package/public/favicon.png +0 -0
  96. package/public/favicon.svg +0 -11
  97. package/public/icon-192x192.png +0 -0
  98. package/public/icon-512x512.png +0 -0
  99. package/railway.toml +0 -10
  100. package/src/api/client.ts +0 -95
  101. package/src/components/AddressInputModal.css +0 -173
  102. package/src/components/AddressInputModal.tsx +0 -250
  103. package/src/components/ApprovalStep.tsx +0 -92
  104. package/src/components/ExecutionStep.tsx +0 -27
  105. package/src/components/InputStep.tsx +0 -431
  106. package/src/components/QuoteSelector.css +0 -146
  107. package/src/components/QuoteSelector.tsx +0 -137
  108. package/src/components/QuotesModal.css +0 -250
  109. package/src/components/QuotesModal.tsx +0 -182
  110. package/src/components/SettingsModal.css +0 -145
  111. package/src/components/SettingsModal.tsx +0 -180
  112. package/src/components/StatusStep.tsx +0 -139
  113. package/src/components/SwapWidget.css +0 -713
  114. package/src/components/SwapWidget.tsx +0 -607
  115. package/src/components/TokenSelectModal.css +0 -438
  116. package/src/components/TokenSelectModal.tsx +0 -487
  117. package/src/components/WalletProvider.tsx +0 -92
  118. package/src/config/appkit.ts +0 -104
  119. package/src/config/wagmi.ts +0 -27
  120. package/src/constants/chains.ts +0 -72
  121. package/src/constants/defaults.ts +0 -28
  122. package/src/constants/swappers.ts +0 -48
  123. package/src/constants/viemChains.ts +0 -69
  124. package/src/contexts/SwapWalletContext.tsx +0 -25
  125. package/src/demo/App.css +0 -458
  126. package/src/demo/App.tsx +0 -481
  127. package/src/demo/main.tsx +0 -25
  128. package/src/hooks/useAssets.ts +0 -180
  129. package/src/hooks/useBalances.ts +0 -822
  130. package/src/hooks/useBitcoinSigning.ts +0 -276
  131. package/src/hooks/useMarketData.ts +0 -169
  132. package/src/hooks/useSolanaSigning.ts +0 -275
  133. package/src/hooks/useStatusPolling.ts +0 -137
  134. package/src/hooks/useSwapApproval.ts +0 -106
  135. package/src/hooks/useSwapDisplayValues.ts +0 -157
  136. package/src/hooks/useSwapExecution.ts +0 -199
  137. package/src/hooks/useSwapHandlers.ts +0 -119
  138. package/src/hooks/useSwapQuote.ts +0 -71
  139. package/src/hooks/useSwapQuoting.ts +0 -102
  140. package/src/hooks/useSwapRates.ts +0 -61
  141. package/src/index.ts +0 -43
  142. package/src/machines/SwapMachineContext.ts +0 -5
  143. package/src/machines/__tests__/guards.test.ts +0 -228
  144. package/src/machines/__tests__/setup.test.ts +0 -7
  145. package/src/machines/__tests__/swapMachine.test.ts +0 -544
  146. package/src/machines/__tests__/types.test.ts +0 -124
  147. package/src/machines/guards.ts +0 -33
  148. package/src/machines/swapMachine.ts +0 -288
  149. package/src/machines/types.ts +0 -54
  150. package/src/services/transactionStatus.ts +0 -322
  151. package/src/types/index.ts +0 -361
  152. package/src/utils/addressValidation.ts +0 -201
  153. package/src/utils/redirect.ts +0 -45
  154. package/src/vite-env.d.ts +0 -10
  155. package/tsconfig.json +0 -23
  156. package/tsconfig.node.json +0 -10
  157. package/vite.config.ts +0 -101
  158. package/vitest.config.ts +0 -18
@@ -1,822 +0,0 @@
1
- import { useAppKitConnection } from '@reown/appkit-adapter-solana/react'
2
- import { ASSET_NAMESPACE, CHAIN_NAMESPACE, fromAssetId } from '@shapeshiftoss/caip'
3
- import { Connection, PublicKey } from '@solana/web3.js'
4
- import { useQueries, useQueryClient } from '@tanstack/react-query'
5
- import { getBalance, readContract } from '@wagmi/core'
6
- import PQueue from 'p-queue'
7
- import { useCallback, useMemo } from 'react'
8
- import { erc20Abi } from 'viem'
9
- import { useConfig } from 'wagmi'
10
-
11
- import type { SupportedChainId } from '../config/wagmi'
12
- import type { AssetId } from '../types'
13
- import { formatAmount, getChainType, UTXO_CHAIN_IDS } from '../types'
14
-
15
- const CONCURRENCY_LIMIT = 5
16
- const DELAY_BETWEEN_BATCHES_MS = 50
17
-
18
- const balanceQueue = new PQueue({
19
- concurrency: CONCURRENCY_LIMIT,
20
- interval: DELAY_BETWEEN_BATCHES_MS,
21
- intervalCap: CONCURRENCY_LIMIT,
22
- })
23
-
24
- type BalanceResult = {
25
- assetId: AssetId
26
- balance: string
27
- balanceFormatted: string
28
- }
29
-
30
- type BalancesMap = Record<AssetId, BalanceResult>
31
-
32
- type ParsedAssetEvm = {
33
- chainType: 'evm'
34
- evmChainId: number
35
- tokenAddress?: `0x${string}`
36
- }
37
-
38
- type ParsedAssetUtxo = {
39
- chainType: 'utxo'
40
- chainReference: string
41
- }
42
-
43
- type ParsedAssetSolana = {
44
- chainType: 'solana'
45
- tokenAddress?: string
46
- }
47
-
48
- type ParsedAsset = ParsedAssetEvm | ParsedAssetUtxo | ParsedAssetSolana | null
49
-
50
- const MEMPOOL_API_BASE = 'https://mempool.space/api'
51
- const SOLANA_PUBLIC_RPC = 'https://api.mainnet-beta.solana.com'
52
-
53
- const parseAssetIdMultiChain = (assetId: AssetId): ParsedAsset => {
54
- try {
55
- const { chainNamespace, chainReference, assetNamespace, assetReference } = fromAssetId(assetId)
56
- const chainType = getChainType(assetId.split('/')[0] as string)
57
-
58
- if (chainType === 'evm' && chainNamespace === CHAIN_NAMESPACE.Evm) {
59
- const evmChainId = Number(chainReference)
60
- if (assetNamespace === ASSET_NAMESPACE.erc20) {
61
- return {
62
- chainType: 'evm',
63
- evmChainId,
64
- tokenAddress: assetReference as `0x${string}`,
65
- }
66
- }
67
- return { chainType: 'evm', evmChainId }
68
- }
69
-
70
- if (chainType === 'utxo' && chainNamespace === CHAIN_NAMESPACE.Utxo) {
71
- return { chainType: 'utxo', chainReference }
72
- }
73
-
74
- if (chainType === 'solana' && chainNamespace === CHAIN_NAMESPACE.Solana) {
75
- if (assetNamespace === ASSET_NAMESPACE.splToken) {
76
- return { chainType: 'solana', tokenAddress: assetReference }
77
- }
78
- return { chainType: 'solana' }
79
- }
80
-
81
- return null
82
- } catch {
83
- return null
84
- }
85
- }
86
-
87
- const parseAssetId = (
88
- assetId: AssetId,
89
- ): { chainId: number; tokenAddress?: `0x${string}` } | null => {
90
- try {
91
- const { chainNamespace, chainReference, assetNamespace, assetReference } = fromAssetId(assetId)
92
-
93
- if (chainNamespace !== CHAIN_NAMESPACE.Evm) return null
94
-
95
- const evmChainId = Number(chainReference)
96
-
97
- if (assetNamespace === ASSET_NAMESPACE.erc20) {
98
- return {
99
- chainId: evmChainId,
100
- tokenAddress: assetReference as `0x${string}`,
101
- }
102
- }
103
-
104
- return { chainId: evmChainId }
105
- } catch {
106
- return null
107
- }
108
- }
109
-
110
- const fetchBitcoinBalance = async (
111
- address: string,
112
- network: 'mainnet' | 'testnet' = 'mainnet',
113
- ): Promise<string> => {
114
- const baseUrl = network === 'testnet' ? 'https://mempool.space/testnet/api' : MEMPOOL_API_BASE
115
-
116
- const response = await fetch(`${baseUrl}/address/${address}`)
117
- if (!response.ok) {
118
- throw new Error(`Failed to fetch Bitcoin balance: ${response.status}`)
119
- }
120
-
121
- const data = await response.json()
122
- const confirmedFunded = data.chain_stats?.funded_txo_sum ?? 0
123
- const confirmedSpent = data.chain_stats?.spent_txo_sum ?? 0
124
- const mempoolFunded = data.mempool_stats?.funded_txo_sum ?? 0
125
- const mempoolSpent = data.mempool_stats?.spent_txo_sum ?? 0
126
- const totalBalance = confirmedFunded - confirmedSpent + (mempoolFunded - mempoolSpent)
127
-
128
- return totalBalance.toString()
129
- }
130
-
131
- export const useEvmBalances = (
132
- address: string | undefined,
133
- assetIds: AssetId[],
134
- assetPrecisions: Record<AssetId, number>,
135
- ) => {
136
- const config = useConfig()
137
-
138
- const parsedAssets = useMemo(() => {
139
- return assetIds
140
- .map(assetId => {
141
- const parsed = parseAssetId(assetId)
142
- if (!parsed) return null
143
- return {
144
- assetId,
145
- chainId: parsed.chainId as SupportedChainId,
146
- tokenAddress: parsed.tokenAddress,
147
- precision: assetPrecisions[assetId] ?? 18,
148
- isNative: !parsed.tokenAddress,
149
- }
150
- })
151
- .filter(Boolean) as {
152
- assetId: AssetId
153
- chainId: SupportedChainId
154
- tokenAddress?: `0x${string}`
155
- precision: number
156
- isNative: boolean
157
- }[]
158
- }, [assetIds, assetPrecisions])
159
-
160
- const nativeAssets = useMemo(() => parsedAssets.filter(a => a.isNative), [parsedAssets])
161
-
162
- const erc20Assets = useMemo(() => parsedAssets.filter(a => !a.isNative), [parsedAssets])
163
-
164
- const nativeQueries = useQueries({
165
- queries: nativeAssets.map(asset => ({
166
- queryKey: ['nativeBalance', address, asset.chainId],
167
- queryFn: () => {
168
- if (!address) return Promise.resolve(null)
169
- return balanceQueue.add(async () => {
170
- try {
171
- const result = await getBalance(config, {
172
- address: address as `0x${string}`,
173
- // @ts-ignore - swap-widget supports more chains than main app's wagmi type registration
174
- chainId: asset.chainId,
175
- })
176
- return {
177
- assetId: asset.assetId,
178
- balance: result.value.toString(),
179
- precision: asset.precision,
180
- }
181
- } catch {
182
- return null
183
- }
184
- })
185
- },
186
- enabled: !!address,
187
- staleTime: 60_000,
188
- refetchOnWindowFocus: false,
189
- })),
190
- })
191
-
192
- const erc20Queries = useQueries({
193
- queries: erc20Assets.map(asset => ({
194
- queryKey: ['erc20Balance', address, asset.chainId, asset.tokenAddress],
195
- queryFn: () => {
196
- if (!address) return Promise.resolve(null)
197
- return balanceQueue.add(async () => {
198
- try {
199
- if (!asset.tokenAddress) return null
200
- const result = await readContract(config, {
201
- address: asset.tokenAddress,
202
- abi: erc20Abi,
203
- functionName: 'balanceOf',
204
- args: [address as `0x${string}`],
205
- // @ts-ignore - swap-widget supports more chains than main app's wagmi type registration
206
- chainId: asset.chainId,
207
- })
208
- return {
209
- assetId: asset.assetId,
210
- balance: (result as bigint).toString(),
211
- precision: asset.precision,
212
- }
213
- } catch {
214
- return null
215
- }
216
- })
217
- },
218
- enabled: !!address,
219
- staleTime: 60_000,
220
- refetchOnWindowFocus: false,
221
- })),
222
- })
223
-
224
- const isErc20Loading = erc20Queries.some(q => q.isLoading)
225
-
226
- const balances = useMemo((): BalancesMap => {
227
- const result: BalancesMap = {}
228
-
229
- nativeQueries.forEach(query => {
230
- if (query.data) {
231
- const { assetId, balance, precision } = query.data
232
- result[assetId] = {
233
- assetId,
234
- balance,
235
- balanceFormatted: formatAmount(balance, precision),
236
- }
237
- }
238
- })
239
-
240
- erc20Queries.forEach(query => {
241
- if (query.data) {
242
- const { assetId, balance, precision } = query.data
243
- result[assetId] = {
244
- assetId,
245
- balance,
246
- balanceFormatted: formatAmount(balance, precision),
247
- }
248
- }
249
- })
250
-
251
- return result
252
- }, [nativeQueries, erc20Queries])
253
-
254
- const isLoading = nativeQueries.some(q => q.isLoading) || isErc20Loading
255
-
256
- const loadingAssetIds = useMemo(() => {
257
- const loading = new Set<AssetId>()
258
- nativeQueries.forEach((query, index) => {
259
- if (query.isLoading) {
260
- loading.add(nativeAssets[index].assetId)
261
- }
262
- })
263
- erc20Queries.forEach((query, index) => {
264
- if (query.isLoading) {
265
- loading.add(erc20Assets[index].assetId)
266
- }
267
- })
268
- return loading
269
- }, [nativeQueries, nativeAssets, erc20Queries, erc20Assets])
270
-
271
- return { data: balances, isLoading, loadingAssetIds }
272
- }
273
-
274
- export const useBitcoinBalance = (
275
- address: string | undefined,
276
- assetId: AssetId | undefined,
277
- precision: number = 8,
278
- ) => {
279
- const parsed = assetId ? parseAssetIdMultiChain(assetId) : null
280
- const isUtxo = parsed?.chainType === 'utxo'
281
-
282
- const query = useQueries({
283
- queries: [
284
- {
285
- queryKey: ['utxoBalance', address, assetId],
286
- queryFn: async () => {
287
- if (!address || !assetId || !isUtxo) return null
288
- try {
289
- const balance = await fetchBitcoinBalance(address)
290
- return {
291
- assetId,
292
- balance,
293
- precision,
294
- }
295
- } catch {
296
- return null
297
- }
298
- },
299
- enabled: !!address && !!assetId && isUtxo,
300
- staleTime: 60_000,
301
- refetchOnWindowFocus: false,
302
- },
303
- ],
304
- })
305
-
306
- return useMemo(() => {
307
- const queryResult = query[0]
308
- if (!queryResult?.data || !assetId) {
309
- return {
310
- data: undefined,
311
- isLoading: queryResult?.isLoading ?? false,
312
- refetch: queryResult?.refetch,
313
- }
314
- }
315
-
316
- const { balance } = queryResult.data
317
- return {
318
- data: {
319
- assetId,
320
- balance,
321
- balanceFormatted: formatAmount(balance, precision),
322
- },
323
- isLoading: queryResult.isLoading,
324
- refetch: queryResult.refetch,
325
- }
326
- }, [query, assetId, precision])
327
- }
328
-
329
- export const useSolanaBalance = (
330
- address: string | undefined,
331
- assetId: AssetId | undefined,
332
- precision: number = 9,
333
- ) => {
334
- const { connection: walletConnection } = useAppKitConnection()
335
- const parsed = assetId ? parseAssetIdMultiChain(assetId) : null
336
- const isSolana = parsed?.chainType === 'solana'
337
- const isNative = isSolana && !('tokenAddress' in parsed && parsed.tokenAddress)
338
- const tokenAddress = isSolana && 'tokenAddress' in parsed ? parsed.tokenAddress : undefined
339
-
340
- const query = useQueries({
341
- queries: [
342
- {
343
- queryKey: ['solanaBalance', address, assetId, isNative],
344
- queryFn: async () => {
345
- if (!address || !assetId || !isSolana) return null
346
-
347
- const connection = walletConnection ?? new Connection(SOLANA_PUBLIC_RPC, 'confirmed')
348
-
349
- try {
350
- const pubKey = new PublicKey(address)
351
-
352
- if (isNative) {
353
- const balance = await connection.getBalance(pubKey)
354
- return {
355
- assetId,
356
- balance: balance.toString(),
357
- precision,
358
- }
359
- }
360
-
361
- if (tokenAddress) {
362
- const tokenPubKey = new PublicKey(tokenAddress)
363
- const tokenAccounts = await connection.getTokenAccountsByOwner(pubKey, {
364
- mint: tokenPubKey,
365
- })
366
-
367
- if (tokenAccounts.value.length === 0) {
368
- return {
369
- assetId,
370
- balance: '0',
371
- precision,
372
- }
373
- }
374
-
375
- const accountInfo = tokenAccounts.value[0].account.data
376
- const balance = accountInfo.slice(64, 72).readBigUInt64LE()
377
- return {
378
- assetId,
379
- balance: balance.toString(),
380
- precision,
381
- }
382
- }
383
-
384
- return null
385
- } catch {
386
- return null
387
- }
388
- },
389
- enabled: !!address && !!assetId && isSolana,
390
- staleTime: 30_000,
391
- refetchOnWindowFocus: false,
392
- },
393
- ],
394
- })
395
-
396
- return useMemo(() => {
397
- const queryResult = query[0]
398
- if (!queryResult?.data || !assetId) {
399
- return {
400
- data: undefined,
401
- isLoading: queryResult?.isLoading ?? false,
402
- refetch: queryResult?.refetch,
403
- }
404
- }
405
-
406
- const { balance } = queryResult.data
407
- return {
408
- data: {
409
- assetId,
410
- balance,
411
- balanceFormatted: formatAmount(balance, precision),
412
- },
413
- isLoading: queryResult.isLoading,
414
- refetch: queryResult.refetch,
415
- }
416
- }, [query, assetId, precision])
417
- }
418
-
419
- export const useMultiChainBalance = (
420
- evmAddress: string | undefined,
421
- utxoAddress: string | undefined,
422
- solanaAddress: string | undefined,
423
- assetId: AssetId | undefined,
424
- precision: number = 18,
425
- ) => {
426
- const parsed = assetId ? parseAssetIdMultiChain(assetId) : null
427
- const chainType = parsed?.chainType ?? 'other'
428
- const config = useConfig()
429
-
430
- const addressForChain =
431
- chainType === 'evm'
432
- ? evmAddress
433
- : chainType === 'utxo'
434
- ? utxoAddress
435
- : chainType === 'solana'
436
- ? solanaAddress
437
- : undefined
438
-
439
- const evmParsed = chainType === 'evm' && parsed?.chainType === 'evm' ? parsed : null
440
- const isErc20 = evmParsed && evmParsed.tokenAddress
441
-
442
- const evmQuery = useQueries({
443
- queries: [
444
- {
445
- queryKey: [
446
- 'singleEvmBalance',
447
- evmAddress,
448
- assetId,
449
- evmParsed?.evmChainId,
450
- evmParsed?.tokenAddress,
451
- ],
452
- queryFn: async () => {
453
- if (!evmAddress || !assetId || !evmParsed) return null
454
- try {
455
- if (isErc20 && evmParsed.tokenAddress) {
456
- const result = await readContract(config, {
457
- address: evmParsed.tokenAddress,
458
- abi: erc20Abi,
459
- functionName: 'balanceOf',
460
- args: [evmAddress as `0x${string}`],
461
- chainId: evmParsed.evmChainId,
462
- })
463
- return {
464
- assetId,
465
- balance: (result as bigint).toString(),
466
- precision,
467
- }
468
- }
469
- const result = await getBalance(config, {
470
- address: evmAddress as `0x${string}`,
471
- // @ts-ignore
472
- chainId: evmParsed.evmChainId,
473
- })
474
- return {
475
- assetId,
476
- balance: result.value.toString(),
477
- precision,
478
- }
479
- } catch {
480
- return null
481
- }
482
- },
483
- enabled: chainType === 'evm' && !!evmAddress && !!assetId,
484
- staleTime: 60_000,
485
- refetchOnWindowFocus: false,
486
- },
487
- ],
488
- })
489
-
490
- const evmBalance = useMemo(() => {
491
- const queryResult = evmQuery[0]
492
- if (!queryResult?.data || !assetId) {
493
- return {
494
- data: undefined,
495
- isLoading: queryResult?.isLoading ?? false,
496
- refetch: queryResult?.refetch,
497
- }
498
- }
499
-
500
- const { balance } = queryResult.data
501
- return {
502
- data: {
503
- assetId,
504
- balance,
505
- balanceFormatted: formatAmount(balance, precision),
506
- },
507
- isLoading: queryResult.isLoading,
508
- refetch: queryResult.refetch,
509
- }
510
- }, [evmQuery, assetId, precision])
511
-
512
- const utxoBalance = useBitcoinBalance(
513
- chainType === 'utxo' ? addressForChain : undefined,
514
- chainType === 'utxo' ? assetId : undefined,
515
- precision,
516
- )
517
-
518
- const solanaBalance = useSolanaBalance(
519
- chainType === 'solana' ? addressForChain : undefined,
520
- chainType === 'solana' ? assetId : undefined,
521
- precision,
522
- )
523
-
524
- return useMemo(() => {
525
- switch (chainType) {
526
- case 'evm':
527
- return evmBalance
528
- case 'utxo':
529
- return utxoBalance
530
- case 'solana':
531
- return solanaBalance
532
- default:
533
- return { data: undefined, isLoading: false, refetch: undefined }
534
- }
535
- }, [chainType, evmBalance, utxoBalance, solanaBalance])
536
- }
537
-
538
- export const useMultiChainBalances = (
539
- evmAddress: string | undefined,
540
- utxoAddress: string | undefined,
541
- solanaAddress: string | undefined,
542
- assetIds: AssetId[],
543
- assetPrecisions: Record<AssetId, number>,
544
- ) => {
545
- const config = useConfig()
546
- const { connection: walletConnection } = useAppKitConnection()
547
- const queryClient = useQueryClient()
548
-
549
- const groupedAssets = useMemo(() => {
550
- const evm: {
551
- assetId: AssetId
552
- chainId: number
553
- tokenAddress?: `0x${string}`
554
- precision: number
555
- }[] = []
556
- const utxo: { assetId: AssetId; precision: number }[] = []
557
- const solana: { assetId: AssetId; tokenAddress?: string; precision: number }[] = []
558
-
559
- assetIds.forEach(assetId => {
560
- const parsed = parseAssetIdMultiChain(assetId)
561
- const precision = assetPrecisions[assetId] ?? 18
562
-
563
- if (parsed?.chainType === 'evm') {
564
- evm.push({
565
- assetId,
566
- chainId: parsed.evmChainId,
567
- tokenAddress: parsed.tokenAddress,
568
- precision,
569
- })
570
- } else if (
571
- parsed?.chainType === 'utxo' &&
572
- parsed.chainReference === UTXO_CHAIN_IDS.bitcoin.split(':')[1]
573
- ) {
574
- utxo.push({ assetId, precision })
575
- } else if (parsed?.chainType === 'solana') {
576
- solana.push({
577
- assetId,
578
- tokenAddress: parsed.tokenAddress,
579
- precision,
580
- })
581
- }
582
- })
583
-
584
- return { evm, utxo, solana }
585
- }, [assetIds, assetPrecisions])
586
-
587
- const evmQueries = useQueries({
588
- queries: groupedAssets.evm.map(asset => ({
589
- queryKey: ['multiChainBalance', 'evm', evmAddress, asset.chainId, asset.tokenAddress],
590
- queryFn: async () => {
591
- if (!evmAddress) return null
592
- try {
593
- if (asset.tokenAddress) {
594
- const result = await readContract(config, {
595
- address: asset.tokenAddress,
596
- abi: erc20Abi,
597
- functionName: 'balanceOf',
598
- args: [evmAddress as `0x${string}`],
599
- // @ts-ignore - swap-widget supports more chains than main app's wagmi type registration
600
- chainId: asset.chainId,
601
- })
602
- return {
603
- assetId: asset.assetId,
604
- balance: (result as bigint).toString(),
605
- precision: asset.precision,
606
- }
607
- }
608
- const result = await getBalance(config, {
609
- address: evmAddress as `0x${string}`,
610
- // @ts-ignore - swap-widget supports more chains than main app's wagmi type registration
611
- chainId: asset.chainId,
612
- })
613
- return {
614
- assetId: asset.assetId,
615
- balance: result.value.toString(),
616
- precision: asset.precision,
617
- }
618
- } catch {
619
- return null
620
- }
621
- },
622
- enabled: !!evmAddress,
623
- staleTime: 60_000,
624
- refetchOnWindowFocus: false,
625
- })),
626
- })
627
-
628
- const utxoQueries = useQueries({
629
- queries: groupedAssets.utxo.map(asset => ({
630
- queryKey: ['multiChainBalance', 'utxo', utxoAddress, asset.assetId],
631
- queryFn: async () => {
632
- if (!utxoAddress) return null
633
- try {
634
- const balance = await fetchBitcoinBalance(utxoAddress)
635
- return {
636
- assetId: asset.assetId,
637
- balance,
638
- precision: asset.precision,
639
- }
640
- } catch {
641
- return null
642
- }
643
- },
644
- enabled: !!utxoAddress,
645
- staleTime: 60_000,
646
- refetchOnWindowFocus: false,
647
- })),
648
- })
649
-
650
- const solanaQueries = useQueries({
651
- queries: groupedAssets.solana.map(asset => ({
652
- queryKey: ['multiChainBalance', 'solana', solanaAddress, asset.assetId, asset.tokenAddress],
653
- queryFn: async () => {
654
- if (!solanaAddress) return null
655
-
656
- const connection = walletConnection ?? new Connection(SOLANA_PUBLIC_RPC, 'confirmed')
657
-
658
- try {
659
- const pubKey = new PublicKey(solanaAddress)
660
-
661
- if (!asset.tokenAddress) {
662
- const balance = await connection.getBalance(pubKey)
663
- return {
664
- assetId: asset.assetId,
665
- balance: balance.toString(),
666
- precision: asset.precision,
667
- }
668
- }
669
-
670
- const tokenPubKey = new PublicKey(asset.tokenAddress)
671
- const tokenAccounts = await connection.getTokenAccountsByOwner(pubKey, {
672
- mint: tokenPubKey,
673
- })
674
-
675
- if (tokenAccounts.value.length === 0) {
676
- return {
677
- assetId: asset.assetId,
678
- balance: '0',
679
- precision: asset.precision,
680
- }
681
- }
682
-
683
- const accountInfo = tokenAccounts.value[0].account.data
684
- const balance = accountInfo.slice(64, 72).readBigUInt64LE()
685
- return {
686
- assetId: asset.assetId,
687
- balance: balance.toString(),
688
- precision: asset.precision,
689
- }
690
- } catch {
691
- return null
692
- }
693
- },
694
- enabled: !!solanaAddress,
695
- staleTime: 30_000,
696
- refetchOnWindowFocus: false,
697
- })),
698
- })
699
-
700
- const balances = useMemo((): BalancesMap => {
701
- const result: BalancesMap = {}
702
-
703
- ;[...evmQueries, ...utxoQueries, ...solanaQueries].forEach(query => {
704
- if (query.data) {
705
- const { assetId, balance, precision } = query.data
706
- const formatted = formatAmount(balance, precision)
707
- result[assetId] = {
708
- assetId,
709
- balance,
710
- balanceFormatted: formatted,
711
- }
712
- }
713
- })
714
-
715
- return result
716
- }, [evmQueries, utxoQueries, solanaQueries])
717
-
718
- const isLoading =
719
- evmQueries.some(q => q.isLoading) ||
720
- utxoQueries.some(q => q.isLoading) ||
721
- solanaQueries.some(q => q.isLoading)
722
-
723
- const loadingAssetIds = useMemo(() => {
724
- const loading = new Set<AssetId>()
725
-
726
- evmQueries.forEach((query, index) => {
727
- if (query.isLoading) {
728
- loading.add(groupedAssets.evm[index].assetId)
729
- }
730
- })
731
- utxoQueries.forEach((query, index) => {
732
- if (query.isLoading) {
733
- loading.add(groupedAssets.utxo[index].assetId)
734
- }
735
- })
736
- solanaQueries.forEach((query, index) => {
737
- if (query.isLoading) {
738
- loading.add(groupedAssets.solana[index].assetId)
739
- }
740
- })
741
-
742
- return loading
743
- }, [evmQueries, utxoQueries, solanaQueries, groupedAssets])
744
-
745
- const refetch = useCallback(() => {
746
- queryClient.invalidateQueries({
747
- predicate: query => {
748
- const key = query.queryKey
749
- return (
750
- (Array.isArray(key) && key[0] === 'multiChainBalance') ||
751
- key[0] === 'nativeBalance' ||
752
- key[0] === 'erc20Balance' ||
753
- key[0] === 'utxoBalance' ||
754
- key[0] === 'solanaBalance'
755
- )
756
- },
757
- })
758
- }, [queryClient])
759
-
760
- const refetchSpecific = useCallback(
761
- (targetAssetIds: AssetId[]) => {
762
- queryClient.invalidateQueries({
763
- predicate: query => {
764
- const key = query.queryKey as unknown[]
765
- if (!Array.isArray(key) || key[0] !== 'multiChainBalance') return false
766
-
767
- return targetAssetIds.some(assetId => {
768
- const evmAsset = groupedAssets.evm.find(a => a.assetId === assetId)
769
- if (evmAsset) {
770
- const expectedKey = [
771
- 'multiChainBalance',
772
- 'evm',
773
- evmAddress,
774
- evmAsset.chainId,
775
- evmAsset.tokenAddress,
776
- ]
777
- if (
778
- key.length === expectedKey.length &&
779
- key.every((val, idx) => val === expectedKey[idx])
780
- ) {
781
- return true
782
- }
783
- }
784
-
785
- const utxoAsset = groupedAssets.utxo.find(a => a.assetId === assetId)
786
- if (utxoAsset) {
787
- const expectedKey = ['multiChainBalance', 'utxo', utxoAddress, assetId]
788
- if (
789
- key.length === expectedKey.length &&
790
- key.every((val, idx) => val === expectedKey[idx])
791
- ) {
792
- return true
793
- }
794
- }
795
-
796
- const solanaAsset = groupedAssets.solana.find(a => a.assetId === assetId)
797
- if (solanaAsset) {
798
- const expectedKey = [
799
- 'multiChainBalance',
800
- 'solana',
801
- solanaAddress,
802
- assetId,
803
- solanaAsset.tokenAddress,
804
- ]
805
- if (
806
- key.length === expectedKey.length &&
807
- key.every((val, idx) => val === expectedKey[idx])
808
- ) {
809
- return true
810
- }
811
- }
812
-
813
- return false
814
- })
815
- },
816
- })
817
- },
818
- [queryClient, groupedAssets, evmAddress, utxoAddress, solanaAddress],
819
- )
820
-
821
- return { data: balances, isLoading, loadingAssetIds, refetch, refetchSpecific }
822
- }