@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,119 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
4
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
5
+ import type { Asset, TradeRate } from '../types'
6
+ import { parseAmount } from '../types'
7
+
8
+ type UseSwapHandlersParams = {
9
+ onConnectWallet?: () => void
10
+ onAssetSelect?: (type: 'sell' | 'buy', asset: Asset) => void
11
+ }
12
+
13
+ export const useSwapHandlers = ({ onConnectWallet, onAssetSelect }: UseSwapHandlersParams) => {
14
+ const actorRef = SwapMachineCtx.useActorRef()
15
+ const { walletClient, bitcoin, solana } = useSwapWallet()
16
+
17
+ const handleSwapTokens = useCallback(() => {
18
+ const snap = actorRef.getSnapshot()
19
+ actorRef.send({ type: 'SET_SELL_ASSET', asset: snap.context.buyAsset })
20
+ actorRef.send({ type: 'SET_BUY_ASSET', asset: snap.context.sellAsset })
21
+ actorRef.send({ type: 'SET_SELL_AMOUNT', amount: '', amountBaseUnit: undefined })
22
+ }, [actorRef])
23
+
24
+ const handleSellAssetSelect = useCallback(
25
+ (asset: Asset) => {
26
+ actorRef.send({ type: 'SET_SELL_ASSET', asset })
27
+ onAssetSelect?.('sell', asset)
28
+ },
29
+ [actorRef, onAssetSelect],
30
+ )
31
+
32
+ const handleBuyAssetSelect = useCallback(
33
+ (asset: Asset) => {
34
+ actorRef.send({ type: 'SET_BUY_ASSET', asset })
35
+ onAssetSelect?.('buy', asset)
36
+ },
37
+ [actorRef, onAssetSelect],
38
+ )
39
+
40
+ const handleSellAmountChange = useCallback(
41
+ (value: string) => {
42
+ const snap = actorRef.getSnapshot()
43
+ const baseUnit = value ? parseAmount(value, snap.context.sellAsset.precision) : undefined
44
+ actorRef.send({ type: 'SET_SELL_AMOUNT', amount: value, amountBaseUnit: baseUnit })
45
+ },
46
+ [actorRef],
47
+ )
48
+
49
+ const handleSelectRate = useCallback(
50
+ (rate: TradeRate) => {
51
+ actorRef.send({ type: 'SELECT_RATE', rate })
52
+ },
53
+ [actorRef],
54
+ )
55
+
56
+ const handleSlippageChange = useCallback(
57
+ (value: string) => {
58
+ actorRef.send({ type: 'SET_SLIPPAGE', slippage: value })
59
+ },
60
+ [actorRef],
61
+ )
62
+
63
+ const redirectToShapeShift = useCallback(() => {
64
+ const snap = actorRef.getSnapshot()
65
+ const params = new URLSearchParams({
66
+ sellAssetId: snap.context.sellAsset.assetId,
67
+ buyAssetId: snap.context.buyAsset.assetId,
68
+ sellAmount: snap.context.sellAmount,
69
+ })
70
+ window.open(
71
+ `https://app.shapeshift.com/trade?${params.toString()}`,
72
+ '_blank',
73
+ 'noopener,noreferrer',
74
+ )
75
+ }, [actorRef])
76
+
77
+ const handleButtonClick = useCallback(() => {
78
+ const snap = actorRef.getSnapshot()
79
+ if (snap.context.isSellAssetUtxo && !bitcoin.isConnected) {
80
+ return
81
+ }
82
+ if (snap.context.isSellAssetSolana && !solana.isConnected) {
83
+ return
84
+ }
85
+ if (!walletClient && snap.context.isSellAssetEvm && onConnectWallet) {
86
+ onConnectWallet()
87
+ return
88
+ }
89
+ if (
90
+ !snap.context.isSellAssetEvm &&
91
+ !snap.context.isSellAssetUtxo &&
92
+ !snap.context.isSellAssetSolana
93
+ ) {
94
+ const params = new URLSearchParams({
95
+ sellAssetId: snap.context.sellAsset.assetId,
96
+ buyAssetId: snap.context.buyAsset.assetId,
97
+ sellAmount: snap.context.sellAmount,
98
+ })
99
+ window.open(
100
+ `https://app.shapeshift.com/trade?${params.toString()}`,
101
+ '_blank',
102
+ 'noopener,noreferrer',
103
+ )
104
+ return
105
+ }
106
+ actorRef.send({ type: 'FETCH_QUOTE' })
107
+ }, [actorRef, bitcoin.isConnected, solana.isConnected, walletClient, onConnectWallet])
108
+
109
+ return {
110
+ handleSwapTokens,
111
+ handleSellAssetSelect,
112
+ handleBuyAssetSelect,
113
+ handleSellAmountChange,
114
+ handleSelectRate,
115
+ handleSlippageChange,
116
+ redirectToShapeShift,
117
+ handleButtonClick,
118
+ }
119
+ }
@@ -0,0 +1,71 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import type { ApiClient } from '../api/client'
4
+ import type { AssetId, QuoteResponse, SwapperName } from '../types'
5
+
6
+ export type UseSwapQuoteParams = {
7
+ sellAssetId: AssetId | undefined
8
+ buyAssetId: AssetId | undefined
9
+ sellAmountCryptoBaseUnit: string | undefined
10
+ sendAddress: string | undefined
11
+ receiveAddress: string | undefined
12
+ swapperName: SwapperName | undefined
13
+ slippageTolerancePercentageDecimal?: string
14
+ enabled?: boolean
15
+ }
16
+
17
+ export const useSwapQuote = (apiClient: ApiClient, params: UseSwapQuoteParams) => {
18
+ const {
19
+ sellAssetId,
20
+ buyAssetId,
21
+ sellAmountCryptoBaseUnit,
22
+ sendAddress,
23
+ receiveAddress,
24
+ swapperName,
25
+ slippageTolerancePercentageDecimal,
26
+ enabled = true,
27
+ } = params
28
+
29
+ return useQuery({
30
+ queryKey: [
31
+ 'swapQuote',
32
+ sellAssetId,
33
+ buyAssetId,
34
+ sellAmountCryptoBaseUnit,
35
+ sendAddress,
36
+ receiveAddress,
37
+ swapperName,
38
+ slippageTolerancePercentageDecimal,
39
+ ],
40
+ queryFn: (): Promise<QuoteResponse | null> => {
41
+ if (
42
+ !sellAssetId ||
43
+ !buyAssetId ||
44
+ !sellAmountCryptoBaseUnit ||
45
+ !sendAddress ||
46
+ !receiveAddress ||
47
+ !swapperName
48
+ ) {
49
+ return Promise.resolve(null)
50
+ }
51
+ return apiClient.getQuote({
52
+ sellAssetId,
53
+ buyAssetId,
54
+ sellAmountCryptoBaseUnit,
55
+ sendAddress,
56
+ receiveAddress,
57
+ swapperName,
58
+ slippageTolerancePercentageDecimal,
59
+ })
60
+ },
61
+ enabled:
62
+ enabled &&
63
+ !!sellAssetId &&
64
+ !!buyAssetId &&
65
+ !!sellAmountCryptoBaseUnit &&
66
+ !!sendAddress &&
67
+ !!receiveAddress &&
68
+ !!swapperName,
69
+ staleTime: 30_000,
70
+ })
71
+ }
@@ -0,0 +1,102 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import type { ApiClient } from '../api/client'
4
+ import { useSwapWallet } from '../contexts/SwapWalletContext'
5
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
6
+ import type { TradeRate } from '../types'
7
+
8
+ type BalanceData =
9
+ | {
10
+ balance: string
11
+ balanceFormatted: string
12
+ }
13
+ | undefined
14
+
15
+ type UseSwapQuotingParams = {
16
+ apiClient: ApiClient
17
+ rates: TradeRate[] | undefined
18
+ sellAssetBalance: BalanceData
19
+ }
20
+
21
+ export const useSwapQuoting = ({ apiClient, rates, sellAssetBalance }: UseSwapQuotingParams) => {
22
+ const stateValue = SwapMachineCtx.useSelector(s => s.value)
23
+ const context = SwapMachineCtx.useSelector(s => s.context)
24
+ const actorRef = SwapMachineCtx.useActorRef()
25
+
26
+ const { walletAddress, effectiveReceiveAddress, bitcoin, solana } = useSwapWallet()
27
+ const bitcoinAddress = bitcoin.address
28
+ const solanaAddress = solana.address
29
+
30
+ const quotingRef = useRef(false)
31
+
32
+ useEffect(() => {
33
+ const snap = actorRef.getSnapshot()
34
+ if (!snap.matches('quoting') || quotingRef.current) return
35
+ quotingRef.current = true
36
+
37
+ const fetchQuote = async () => {
38
+ try {
39
+ if (sellAssetBalance?.balance && context.sellAmountBaseUnit) {
40
+ const balanceBigInt = BigInt(sellAssetBalance.balance)
41
+ const amountBigInt = BigInt(context.sellAmountBaseUnit)
42
+ if (amountBigInt > balanceBigInt) {
43
+ actorRef.send({ type: 'QUOTE_ERROR', error: 'Insufficient balance' })
44
+ return
45
+ }
46
+ }
47
+
48
+ const parsedSlippage = parseFloat(context.slippage)
49
+ if (isNaN(parsedSlippage) || parsedSlippage < 0) {
50
+ actorRef.send({ type: 'QUOTE_ERROR', error: 'Invalid slippage value' })
51
+ return
52
+ }
53
+ const slippageDecimal = (parsedSlippage / 100).toString()
54
+ const rateToUse = context.selectedRate ?? rates?.[0]
55
+ if (!rateToUse || !context.sellAmountBaseUnit) {
56
+ actorRef.send({ type: 'QUOTE_ERROR', error: 'No rate or amount available' })
57
+ return
58
+ }
59
+
60
+ const sendAddress = context.isSellAssetEvm
61
+ ? walletAddress
62
+ : context.isSellAssetUtxo
63
+ ? bitcoinAddress
64
+ : context.isSellAssetSolana
65
+ ? solanaAddress
66
+ : undefined
67
+
68
+ if (!sendAddress) {
69
+ actorRef.send({ type: 'QUOTE_ERROR', error: 'No wallet address available' })
70
+ return
71
+ }
72
+
73
+ const receiveAddr = effectiveReceiveAddress || sendAddress
74
+
75
+ if (!receiveAddr) {
76
+ actorRef.send({ type: 'QUOTE_ERROR', error: 'No receive address available' })
77
+ return
78
+ }
79
+
80
+ const response = await apiClient.getQuote({
81
+ sellAssetId: context.sellAsset.assetId,
82
+ buyAssetId: context.buyAsset.assetId,
83
+ sellAmountCryptoBaseUnit: context.sellAmountBaseUnit,
84
+ sendAddress,
85
+ receiveAddress: receiveAddr,
86
+ swapperName: rateToUse.swapperName,
87
+ slippageTolerancePercentageDecimal: slippageDecimal,
88
+ })
89
+
90
+ actorRef.send({ type: 'QUOTE_SUCCESS', quote: response })
91
+ } catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : 'Failed to get quote'
93
+ actorRef.send({ type: 'QUOTE_ERROR', error: errorMessage })
94
+ } finally {
95
+ quotingRef.current = false
96
+ }
97
+ }
98
+
99
+ fetchQuote()
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; other deps are stable refs read from snapshot
101
+ }, [stateValue])
102
+ }
@@ -0,0 +1,61 @@
1
+ import { bnOrZero } from '@shapeshiftoss/utils'
2
+ import { useQuery } from '@tanstack/react-query'
3
+
4
+ import type { ApiClient } from '../api/client'
5
+ import type { AssetId, SwapperName, TradeRate } from '../types'
6
+
7
+ export type UseSwapRatesParams = {
8
+ sellAssetId: AssetId | undefined
9
+ buyAssetId: AssetId | undefined
10
+ sellAmountCryptoBaseUnit: string | undefined
11
+ enabled?: boolean
12
+ allowedSwapperNames?: SwapperName[]
13
+ refetchInterval?: number
14
+ }
15
+
16
+ export const useSwapRates = (apiClient: ApiClient, params: UseSwapRatesParams) => {
17
+ const {
18
+ sellAssetId,
19
+ buyAssetId,
20
+ sellAmountCryptoBaseUnit,
21
+ enabled = true,
22
+ allowedSwapperNames,
23
+ refetchInterval = 15_000,
24
+ } = params
25
+
26
+ return useQuery({
27
+ queryKey: ['swapRates', sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, allowedSwapperNames],
28
+ queryFn: async (): Promise<TradeRate[]> => {
29
+ if (!sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit) {
30
+ return []
31
+ }
32
+ const response = await apiClient.getRates({
33
+ sellAssetId,
34
+ buyAssetId,
35
+ sellAmountCryptoBaseUnit,
36
+ })
37
+
38
+ let filteredRates = response.rates.filter(
39
+ rate => !rate.error && rate.buyAmountCryptoBaseUnit !== '0',
40
+ )
41
+
42
+ if (allowedSwapperNames?.length) {
43
+ filteredRates = filteredRates.filter(rate => allowedSwapperNames.includes(rate.swapperName))
44
+ }
45
+
46
+ return filteredRates
47
+ .map((rate, index) => ({
48
+ ...rate,
49
+ id: rate.id ?? `${rate.swapperName}-${index}`,
50
+ }))
51
+ .sort((a, b) => {
52
+ const aAmount = bnOrZero(a.buyAmountCryptoBaseUnit)
53
+ const bAmount = bnOrZero(b.buyAmountCryptoBaseUnit)
54
+ return bAmount.minus(aAmount).toNumber()
55
+ })
56
+ },
57
+ enabled: enabled && !!sellAssetId && !!buyAssetId && !!sellAmountCryptoBaseUnit,
58
+ staleTime: 10_000,
59
+ refetchInterval,
60
+ })
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export { SwapWidget } from './components/SwapWidget'
2
+
3
+ export type {
4
+ Asset,
5
+ AssetId,
6
+ ChainId,
7
+ Chain,
8
+ TradeRate,
9
+ TradeQuote,
10
+ SwapWidgetProps,
11
+ ThemeMode,
12
+ ThemeConfig,
13
+ } from './types'
14
+
15
+ export {
16
+ SwapperName,
17
+ isEvmChainId,
18
+ getEvmNetworkId,
19
+ getChainType,
20
+ formatAmount,
21
+ parseAmount,
22
+ truncateAddress,
23
+ EVM_CHAIN_IDS,
24
+ UTXO_CHAIN_IDS,
25
+ COSMOS_CHAIN_IDS,
26
+ OTHER_CHAIN_IDS,
27
+ } from './types'
28
+
29
+ export {
30
+ getBaseAsset,
31
+ getChainName,
32
+ getChainIcon,
33
+ getChainColor,
34
+ getExplorerTxLink,
35
+ } from './constants/chains'
36
+
37
+ export {
38
+ useAssets,
39
+ useAssetById,
40
+ useChains,
41
+ useAssetsByChainId,
42
+ useAssetSearch,
43
+ } from './hooks/useAssets'
@@ -0,0 +1,5 @@
1
+ import { createActorContext } from '@xstate/react'
2
+
3
+ import { swapMachine } from './swapMachine'
4
+
5
+ export const SwapMachineCtx = createActorContext(swapMachine)
@@ -0,0 +1,228 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { QuoteResponse } from '../../types'
4
+ import {
5
+ canRetry,
6
+ hasQuote,
7
+ hasReceiveAddress,
8
+ hasValidInput,
9
+ hasWallet,
10
+ isApprovalRequired,
11
+ isEvmChain,
12
+ isSolanaChain,
13
+ isUtxoChain,
14
+ } from '../guards'
15
+ import type { SwapMachineContext } from '../types'
16
+
17
+ const createTestContext = (overrides?: Partial<SwapMachineContext>): SwapMachineContext => ({
18
+ sellAsset: {
19
+ assetId: 'eip155:1/slip44:60',
20
+ chainId: 'eip155:1',
21
+ symbol: 'ETH',
22
+ name: 'Ethereum',
23
+ precision: 18,
24
+ },
25
+ buyAsset: {
26
+ assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
27
+ chainId: 'eip155:1',
28
+ symbol: 'USDC',
29
+ name: 'USD Coin',
30
+ precision: 6,
31
+ },
32
+ sellAmount: '1.0',
33
+ sellAmountBaseUnit: '1000000000000000000',
34
+ selectedRate: null,
35
+ quote: null,
36
+ txHash: null,
37
+ approvalTxHash: null,
38
+ error: null,
39
+ errorSource: null,
40
+ retryCount: 0,
41
+ chainType: 'evm',
42
+ slippage: '0.5',
43
+ walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
44
+ effectiveReceiveAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
45
+ isSellAssetEvm: true,
46
+ isSellAssetUtxo: false,
47
+ isSellAssetSolana: false,
48
+ isBuyAssetEvm: true,
49
+ ...overrides,
50
+ })
51
+
52
+ describe('guards', () => {
53
+ describe('hasValidInput', () => {
54
+ it('returns true when all required fields are present', () => {
55
+ expect(hasValidInput(createTestContext())).toBe(true)
56
+ })
57
+
58
+ it('returns false when sellAmountBaseUnit is undefined', () => {
59
+ expect(hasValidInput(createTestContext({ sellAmountBaseUnit: undefined }))).toBe(false)
60
+ })
61
+
62
+ it('returns false when sellAmountBaseUnit is "0"', () => {
63
+ expect(hasValidInput(createTestContext({ sellAmountBaseUnit: '0' }))).toBe(false)
64
+ })
65
+
66
+ it('returns false when sellAmountBaseUnit is empty string', () => {
67
+ expect(hasValidInput(createTestContext({ sellAmountBaseUnit: '' }))).toBe(false)
68
+ })
69
+ })
70
+
71
+ describe('hasQuote', () => {
72
+ it('returns false when quote is null', () => {
73
+ expect(hasQuote(createTestContext())).toBe(false)
74
+ })
75
+
76
+ it('returns true when quote is present', () => {
77
+ expect(hasQuote(createTestContext({ quote: {} as QuoteResponse }))).toBe(true)
78
+ })
79
+
80
+ it('returns true when quote has approval data', () => {
81
+ expect(
82
+ hasQuote(
83
+ createTestContext({
84
+ quote: { approval: { isRequired: true, spender: '0xSpender' } } as QuoteResponse,
85
+ }),
86
+ ),
87
+ ).toBe(true)
88
+ })
89
+ })
90
+
91
+ describe('isApprovalRequired', () => {
92
+ it('returns false when quote is null', () => {
93
+ expect(isApprovalRequired(createTestContext())).toBe(false)
94
+ })
95
+
96
+ it('returns false when approval is not required', () => {
97
+ expect(
98
+ isApprovalRequired(
99
+ createTestContext({
100
+ quote: { approval: { isRequired: false, spender: '0xSpender' } } as QuoteResponse,
101
+ }),
102
+ ),
103
+ ).toBe(false)
104
+ })
105
+
106
+ it('returns true when approval is required for ERC20 on EVM chain', () => {
107
+ expect(
108
+ isApprovalRequired(
109
+ createTestContext({
110
+ sellAsset: {
111
+ assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
112
+ chainId: 'eip155:1',
113
+ symbol: 'USDC',
114
+ name: 'USD Coin',
115
+ precision: 6,
116
+ },
117
+ chainType: 'evm',
118
+ quote: { approval: { isRequired: true, spender: '0xSpender' } } as QuoteResponse,
119
+ }),
120
+ ),
121
+ ).toBe(true)
122
+ })
123
+
124
+ it('returns false when approval is required but sell asset is native (slip44)', () => {
125
+ expect(
126
+ isApprovalRequired(
127
+ createTestContext({
128
+ chainType: 'evm',
129
+ quote: { approval: { isRequired: true, spender: '0xSpender' } } as QuoteResponse,
130
+ }),
131
+ ),
132
+ ).toBe(false)
133
+ })
134
+
135
+ it('returns false when approval is required but chain is not EVM', () => {
136
+ expect(
137
+ isApprovalRequired(
138
+ createTestContext({
139
+ chainType: 'utxo',
140
+ quote: { approval: { isRequired: true, spender: '0xSpender' } } as QuoteResponse,
141
+ }),
142
+ ),
143
+ ).toBe(false)
144
+ })
145
+
146
+ it('returns false when quote has no approval field', () => {
147
+ expect(
148
+ isApprovalRequired(
149
+ createTestContext({
150
+ quote: {} as QuoteResponse,
151
+ }),
152
+ ),
153
+ ).toBe(false)
154
+ })
155
+ })
156
+
157
+ describe('canRetry', () => {
158
+ it('returns true when retryCount is 0', () => {
159
+ expect(canRetry(createTestContext({ retryCount: 0 }))).toBe(true)
160
+ })
161
+
162
+ it('returns true when retryCount is 2', () => {
163
+ expect(canRetry(createTestContext({ retryCount: 2 }))).toBe(true)
164
+ })
165
+
166
+ it('returns false when retryCount is 3', () => {
167
+ expect(canRetry(createTestContext({ retryCount: 3 }))).toBe(false)
168
+ })
169
+
170
+ it('returns false when retryCount is greater than 3', () => {
171
+ expect(canRetry(createTestContext({ retryCount: 5 }))).toBe(false)
172
+ })
173
+ })
174
+
175
+ describe('isEvmChain', () => {
176
+ it('returns true for evm chain', () => {
177
+ expect(isEvmChain(createTestContext({ chainType: 'evm' }))).toBe(true)
178
+ })
179
+
180
+ it('returns false for utxo chain', () => {
181
+ expect(isEvmChain(createTestContext({ chainType: 'utxo' }))).toBe(false)
182
+ })
183
+ })
184
+
185
+ describe('isUtxoChain', () => {
186
+ it('returns true for utxo chain', () => {
187
+ expect(isUtxoChain(createTestContext({ chainType: 'utxo' }))).toBe(true)
188
+ })
189
+
190
+ it('returns false for evm chain', () => {
191
+ expect(isUtxoChain(createTestContext({ chainType: 'evm' }))).toBe(false)
192
+ })
193
+ })
194
+
195
+ describe('isSolanaChain', () => {
196
+ it('returns true for solana chain', () => {
197
+ expect(isSolanaChain(createTestContext({ chainType: 'solana' }))).toBe(true)
198
+ })
199
+
200
+ it('returns false for evm chain', () => {
201
+ expect(isSolanaChain(createTestContext({ chainType: 'evm' }))).toBe(false)
202
+ })
203
+ })
204
+
205
+ describe('hasWallet', () => {
206
+ it('returns true when walletAddress is set', () => {
207
+ expect(hasWallet(createTestContext())).toBe(true)
208
+ })
209
+
210
+ it('returns false when walletAddress is undefined', () => {
211
+ expect(hasWallet(createTestContext({ walletAddress: undefined }))).toBe(false)
212
+ })
213
+
214
+ it('returns false when walletAddress is empty string', () => {
215
+ expect(hasWallet(createTestContext({ walletAddress: '' }))).toBe(false)
216
+ })
217
+ })
218
+
219
+ describe('hasReceiveAddress', () => {
220
+ it('returns true when effectiveReceiveAddress is set', () => {
221
+ expect(hasReceiveAddress(createTestContext())).toBe(true)
222
+ })
223
+
224
+ it('returns false when effectiveReceiveAddress is empty', () => {
225
+ expect(hasReceiveAddress(createTestContext({ effectiveReceiveAddress: '' }))).toBe(false)
226
+ })
227
+ })
228
+ })
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ describe('test infrastructure', () => {
4
+ it('should work', () => {
5
+ expect(true).toBe(true)
6
+ })
7
+ })