diff --git a/.gitignore b/.gitignore index 86ea3e6c8a..82009a03f8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env.local .env.development.local .env.test.local @@ -45,7 +46,7 @@ package-lock.json .yarn-cache # TypeScript incremental build cache -tsconfig.tsbuildinfo +*.tsbuildinfo # IDE specific .idea diff --git a/.prettierignore b/.prettierignore index 604e47ce2f..f3b974748a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -26,3 +26,5 @@ src/locales/ *.mermaid *.log *.lock +*.patch +.yarnrc diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000000..3bda14ca24 --- /dev/null +++ b/.yarnrc @@ -0,0 +1,6 @@ +# @funkit/connect transitively depends on @solana/addresses, whose `engines` +# field requires node >=20.18 while this repo targets node 18 (.nvmrc). yarn v1 +# re-validates engines on every install, so skip the check here instead of +# passing --ignore-engines by hand each time. The package is browser code; the +# engines field only gates the install. +ignore-engines true diff --git a/package.json b/package.json index 9c6a695686..4693dd622c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "protobufjs": "^7.5.5", "qs": "^6.14.1", "@types/react": "^18.3.30", - "@types/react-dom": "^18.3.7" + "@types/react-dom": "^18.3.7", + "bignumber.js": "^9.3.1" }, "scripts": { "dev": "next dev", @@ -29,6 +30,7 @@ "i18n:extract": "NODE_ENV=development lingui extract --clean --overwrite --locale en", "i18n:compile": "lingui compile", "i18n": "yarn i18n:extract && yarn i18n:compile", + "postinstall": "patch-package", "prepare": "husky install", "test:open": "DOTENV_CONFIG_PATH='.env.local' cypress open", "test:headless": "export DOTENV_CONFIG_PATH='../../../.env.local' && cypress run --config-file './cypress/configs/local/full.config.ts'", @@ -50,6 +52,9 @@ "@emotion/react": "11.10.4", "@emotion/server": "latest", "@emotion/styled": "11.10.4", + "@funkit/api-base": "^4.5.0", + "@funkit/chains": "^2.0.0", + "@funkit/connect": "^9.19.0", "@heroicons/react": "^1.0.6", "@lingui/core": "^4.14.0", "@lingui/react": "^4.14.1", @@ -99,6 +104,7 @@ "remark-gfm": "^3.0.1", "sonner": "^2.0.3", "tiny-invariant": "^1.3.1", + "tronweb": "^6.0.4", "viem": "2.45.1", "wagmi": "^2.15.2", "zustand": "^5.0.2" @@ -140,6 +146,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^13.0.3", + "patch-package": "^8.0.1", "prettier": "^2.8.1", "typescript": "^5.0.4" }, diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 63c44c7ec6..0d86db1a93 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -1,5 +1,8 @@ import '/public/fonts/inter/inter.css'; import '/src/styles/variables.css'; +// Preflight must come before funkit's own styles so funkit rules win source-order ties. +import '/src/ui-config/funkit/funkitPreflight.css'; +import '@funkit/connect/styles.css'; import { AaveClient, AaveProvider } from '@aave/react'; import { CacheProvider, EmotionCache } from '@emotion/react'; @@ -51,6 +54,16 @@ const BridgeModal = dynamic(() => import('src/components/transactions/Bridge/BridgeModal').then((module) => module.BridgeModal) ); +// ssr: false (unlike the other modal hosts) because `@funkit/connect` is a +// client-only, ESM/browser package. +const FunkitCheckout = dynamic( + () => + import('src/components/transactions/FunCheckout/FunkitCheckout').then( + (module) => module.FunkitCheckout + ), + { ssr: false } +); + const BorrowModal = dynamic(() => import('src/components/transactions/Borrow/BorrowModal').then((module) => module.BorrowModal) ); @@ -164,6 +177,7 @@ export default function MyApp(props: MyAppProps) { {getLayout()} + diff --git a/src/components/transactions/FunCheckout/FunkitCheckout.tsx b/src/components/transactions/FunCheckout/FunkitCheckout.tsx new file mode 100644 index 0000000000..ccb0fd71eb --- /dev/null +++ b/src/components/transactions/FunCheckout/FunkitCheckout.tsx @@ -0,0 +1,142 @@ +import { + type FunkitCheckoutConfig, + FunkitProvider, + useActiveTheme, + useFunkitCheckout, +} from '@funkit/connect'; +import { useTheme } from '@mui/material'; +import { useQueryClient } from '@tanstack/react-query'; +import { useModal } from 'connectkit'; +import { useCallback, useEffect, useRef } from 'react'; +import { aaveTheme } from 'src/ui-config/funkit/aaveTheme'; +import { funkitConfig } from 'src/ui-config/funkit/funkitConfig'; +import { queryKeysFactory } from 'src/ui-config/queries'; +import { getAddress } from 'viem'; +import { useAccount } from 'wagmi'; + +import { buildFunSupplyConfig, FunSupplyReserve } from './funSupplyAssets'; +import { registerFunSupply } from './funSupplyBridge'; + +/** + * funkit checkout host. Mounted once in `_app` alongside the app's other modal + * hosts (SupplyModal etc.), as an `ssr: false` island — `@funkit/connect` is + * client-only. `FunkitProvider` is mounted WITHOUT a `wagmiConfig`/`queryClient`, + * so it reuses the interface's existing wagmi + react-query (and the wallet the + * user connected via ConnectKit). + * + * `InnerCheckout` runs inside the provider, owns the single `useFunkitCheckout` + * instance, and registers `beginSupply` on the module bridge (`funSupplyBridge`) + * for the Supply buttons to invoke. Per-asset configs are passed at call time + * via funkit's supported `beginCheckout(configOverride)`. + */ + +// Placeholder config for the hook — never opened directly; every `beginCheckout` +// call passes a full per-asset override built by `buildFunSupplyConfig`. +const PLACEHOLDER_CONFIG: FunkitCheckoutConfig = { + checkoutItemTitle: '', + targetAsset: getAddress('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), + targetAssetTicker: '', + targetChain: '1', +}; + +function InnerCheckout() { + const { address } = useAccount(); + const { setOpen: setConnectModalOpen } = useModal(); + const queryClient = useQueryClient(); + const muiTheme = useTheme(); + const mode = muiTheme.palette.mode; + const { themeColorScheme, toggleTheme } = useActiveTheme(); + + const onSuccess = useCallback(() => { + // Same refresh the native supply flow performs on tx success + // (SupplyActions.tsx / useTransactionHandler), so the dashboard shows the + // new aToken balance immediately after the funkit checkout completes. + queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); + }, [queryClient]); + + // Mid-checkout connection requests (e.g. switching the payment source to a + // wallet) soft-hide the checkout modal and hand us a resume callback — the SDK + // requires `onLoginFinished()` be called after login, or the modal stays + // hidden. Stash it; the address effect below fires it once ConnectKit connects. + const onLoginFinishedRef = useRef<(() => void) | null>(null); + + const { beginCheckout } = useFunkitCheckout({ + config: PLACEHOLDER_CONFIG, + // funkit's own connect modal is unavailable when sharing the host wagmi + // (no funkit wallet list), so route login through the app's ConnectKit modal. + onLoginRequired: useCallback( + ({ onLoginFinished }: { onLoginFinished?: () => void }) => { + onLoginFinishedRef.current = onLoginFinished ?? null; + setConnectModalOpen(true); + }, + [setConnectModalOpen] + ), + onError: useCallback((error: unknown) => console.error('[FunkitCheckout]', error), []), + onSuccess, + }); + + useEffect(() => { + if (address && onLoginFinishedRef.current) { + onLoginFinishedRef.current(); + onLoginFinishedRef.current = null; + } + }, [address]); + + // Keep the funkit modal's active theme in sync with the app's color mode. + // `toggleTheme`'s identity changes on every FunkitThemeProvider render and its + // body always sets state, so the `themeColorScheme !== mode` guard is what makes + // this effect converge instead of update-looping. + useEffect(() => { + if (themeColorScheme !== mode) { + toggleTheme(mode); + } + }, [mode, themeColorScheme, toggleTheme]); + + const beginSupply = async (reserve: FunSupplyReserve) => { + // funkit checkout needs a connected wallet (read-only/watch mode has none); + // open the app's wallet modal first. + if (!address) { + setConnectModalOpen(true); + return; + } + const config = buildFunSupplyConfig(reserve, address); + if (!config) { + return; + } + const { isActivated } = await beginCheckout(config); + if (!isActivated) { + // Checkout can be remotely deactivated per API key; surface it instead + // of failing silently. + console.warn('[FunkitCheckout] checkout is not activated for this API key'); + } + }; + + // Register on the bridge once; the ref keeps the registered wrapper pointing + // at the latest impl without re-registering each render. The catch keeps a + // beginCheckout rejection from surfacing as an unhandled rejection (the bridge + // is fire-and-forget). + const beginSupplyRef = useRef(beginSupply); + useEffect(() => { + beginSupplyRef.current = beginSupply; + }); + useEffect( + () => + registerFunSupply((reserve) => { + beginSupplyRef.current(reserve).catch((error) => console.error('[FunkitCheckout]', error)); + }), + [] + ); + + return null; +} + +export function FunkitCheckout() { + return ( + + + + ); +} + +export default FunkitCheckout; diff --git a/src/components/transactions/FunCheckout/funSupplyAssets.ts b/src/components/transactions/FunCheckout/funSupplyAssets.ts new file mode 100644 index 0000000000..0b46016bf4 --- /dev/null +++ b/src/components/transactions/FunCheckout/funSupplyAssets.ts @@ -0,0 +1,102 @@ +import type { FunkitCheckoutConfig } from '@funkit/connect'; +import { createAaveSupplyCheckoutConfig } from '@funkit/connect/clients/aave'; +import { CustomMarket } from 'src/ui-config/marketsConfig'; +import { type Address, getAddress } from 'viem'; + +/** + * fun checkout only ships on the Core mainnet market. Gating on the market key + * (not chainId) matters: mainnet hosts three markets (Core, Prime/Lido, EtherFi) + * and e.g. USDC exists in all three with a different aToken and pool in each. + * With the market pinned here, every address the reserve hands us (underlying, + * aToken, pool) is consistent by construction. + */ +const FUN_SUPPLY_MARKET = CustomMarket.proto_mainnet_v3; + +/** + * The product allowlist — the underlyings (lowercased) whose Supply button + * routes through funkit's checkout modal instead of the native Aave supply + * modal. Everything else about these assets (symbols, decimals, addresses, + * icons) comes from live app state at click time. + */ +export const FUN_SUPPLY_UNDERLYINGS: ReadonlySet = new Set([ + '0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf', // cbBTC + '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT +]); + +/** True when this market+asset's Supply button should open the funkit modal. */ +export function isFunSupplyAsset(market: CustomMarket, underlyingAsset: string): boolean { + return market === FUN_SUPPLY_MARKET && FUN_SUPPLY_UNDERLYINGS.has(underlyingAsset.toLowerCase()); +} + +/** + * Reserve snapshot the Supply button hands off when opening the fun modal. + * Display fields come from the clicked dashboard reserve; receipt-token fields + * come from the SDK reserve in `useAppDataContext().supplyReserves` (its + * `aToken`/`underlyingToken` are `@aave/graphql` `Currency` objects — the same + * source AddTokenDropdown renders on reserve-overview). + */ +export type FunSupplyReserve = { + underlyingAsset: string; + /** Patched display symbol of the underlying (the list item's `symbol`). */ + symbol: string; + /** + * Ringed aToken icon (Base64Token data URI), generated by the supply row via + * useFunSupplyATokenIcon — the same image the native flow registers via + * wallet_watchAsset. Undefined when generation hasn't completed (or was + * skipped); the hosted `aToken.imageUrl` is the fallback. + */ + aTokenBase64?: string; + /** Aave's `supplyAPY` — a 0–1 fraction string (e.g. "0.0283"). */ + supplyAPY: string | number; + /** The user's collateral toggle for this reserve (`usageAsCollateralEnabledOnUser`). */ + collateralEnabled: boolean; + chainId: number; + /** The market's pool (`currentMarketData.addresses.LENDING_POOL`). */ + poolAddress: string; + /** `sdkReserve.underlyingToken.imageUrl` — absolute URL. */ + underlyingImageUrl: string; + /** `sdkReserve.aToken` — the receipt token's own on-chain metadata. */ + aToken: { address: string; symbol: string; decimals: number; imageUrl: string }; +}; + +// Aave's supplyAPY is a 0–1 fraction; funkit's `display.supplyAPY` wants a +// percent string without the % sign (e.g. "2.83"). +function toPercentString(apy: string | number): string { + const fraction = Number(apy); + if (!Number.isFinite(fraction)) { + return '0'; + } + return (fraction * 100).toFixed(2); +} + +/** + * Builds the per-asset funkit checkout config. The allowlist+market gate lives + * at the click site (`useSupplyButtonAction`); this trusts the vetted reserve. + * Returns `undefined` only when the chain isn't fun-supported + * (createAaveSupplyCheckoutConfig's own signal). + */ +export function buildFunSupplyConfig( + reserve: FunSupplyReserve, + walletAddress: Address | undefined +): FunkitCheckoutConfig | undefined { + return createAaveSupplyCheckoutConfig({ + underlyingAsset: getAddress(reserve.underlyingAsset), + poolAddress: getAddress(reserve.poolAddress), + chainId: reserve.chainId, + walletAddress, + display: { + symbol: reserve.symbol, + supplyAPY: toPercentString(reserve.supplyAPY), + collateralizationEnabled: reserve.collateralEnabled, + iconSrc: reserve.underlyingImageUrl, + }, + receiptToken: { + address: getAddress(reserve.aToken.address), + symbol: reserve.aToken.symbol, + decimals: reserve.aToken.decimals, + iconSrc: reserve.aTokenBase64 ?? reserve.aToken.imageUrl, + }, + }); +} diff --git a/src/components/transactions/FunCheckout/funSupplyBridge.ts b/src/components/transactions/FunCheckout/funSupplyBridge.ts new file mode 100644 index 0000000000..8c2aaaa004 --- /dev/null +++ b/src/components/transactions/FunCheckout/funSupplyBridge.ts @@ -0,0 +1,34 @@ +import type { FunSupplyReserve } from './funSupplyAssets'; + +/** + * Imperative bridge between the Supply buttons (pre-rendered tree) and funkit's + * checkout (an `ssr: false` island — `@funkit/connect` is client-only, and this + * app pre-renders/static-exports). The island registers its `beginSupply` impl + * on mount; buttons invoke it at click time. No context/state on purpose: React + * never renders from this value, so subscription machinery would be dead weight + * (it's also what previously forced the setState + ref-dance layers). + */ +let impl: ((reserve: FunSupplyReserve) => void) | null = null; + +/** Called by FunkitCheckout on mount. Returns an unregister cleanup. */ +export function registerFunSupply(fn: (reserve: FunSupplyReserve) => void): () => void { + impl = fn; + return () => { + if (impl === fn) { + impl = null; + } + }; +} + +/** + * Opens the funkit checkout for `reserve`. Returns false when the island hasn't + * mounted yet (dynamic chunk still loading) — callers fall back to the native + * supply modal instead of dropping the click. + */ +export function beginFunSupply(reserve: FunSupplyReserve): boolean { + if (!impl) { + return false; + } + impl(reserve); + return true; +} diff --git a/src/components/transactions/FunCheckout/useFunSupplyATokenIcon.tsx b/src/components/transactions/FunCheckout/useFunSupplyATokenIcon.tsx new file mode 100644 index 0000000000..b223b385b1 --- /dev/null +++ b/src/components/transactions/FunCheckout/useFunSupplyATokenIcon.tsx @@ -0,0 +1,31 @@ +import { ReactNode, useState } from 'react'; +import { Base64Token } from 'src/components/primitives/TokenIcon'; +import { useRootStore } from 'src/store/root'; + +import { isFunSupplyAsset } from './funSupplyAssets'; + +/** + * Generates the ringed aToken icon (underlying icon wrapped in Aave's gradient + * TokenRing, as a base64 data URI) for fun-routed supply rows — the same image + * the native flow registers via wallet_watchAsset (Success.tsx / + * AddTokenDropdown pattern: hidden Base64Token + state, ready long before the + * click). Returns the icon plus the hidden generator element the row must + * render. Both are undefined/null for rows that aren't fun-routed. + */ +export function useFunSupplyATokenIcon( + underlyingAsset: string, + iconSymbol: string +): { aTokenBase64: string | undefined; generator: ReactNode } { + const currentMarket = useRootStore((store) => store.currentMarket); + const [aTokenBase64, setATokenBase64] = useState(''); + + // Same render condition as the native flows: only fun-routed rows, and + // Base64Token can't compose multi-part symbols (e.g. LP tokens). + const shouldGenerate = isFunSupplyAsset(currentMarket, underlyingAsset) && !/_/.test(iconSymbol); + + const generator = shouldGenerate ? ( + + ) : null; + + return { aTokenBase64: aTokenBase64 || undefined, generator }; +} diff --git a/src/components/transactions/FunCheckout/useSupplyButtonAction.tsx b/src/components/transactions/FunCheckout/useSupplyButtonAction.tsx new file mode 100644 index 0000000000..5f462698b5 --- /dev/null +++ b/src/components/transactions/FunCheckout/useSupplyButtonAction.tsx @@ -0,0 +1,63 @@ +import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; +import { useModalContext } from 'src/hooks/useModal'; +import { useRootStore } from 'src/store/root'; + +import { isFunSupplyAsset } from './funSupplyAssets'; +import { beginFunSupply } from './funSupplyBridge'; + +/** Fields a Supply list item passes when its button is clicked. */ +export type SupplyButtonReserve = { + underlyingAsset: string; + name: string; + symbol: string; + /** Ringed aToken icon data URI from useFunSupplyATokenIcon (fun-routed rows only). */ + aTokenBase64?: string; + /** Aave's `supplyAPY` — a 0–1 fraction. */ + supplyAPY: string | number; + /** `usageAsCollateralEnabledOnUser` from the reserve. */ + collateralEnabled: boolean; +}; + +/** + * Returns the Supply button's click handler. For the allowlisted assets on the + * Core mainnet market it opens the funkit checkout modal; for everything else it + * falls back to the native Aave supply modal (`openSupply`). Shared by all 3 + * Supply list-item variants so the branch lives in one place. + * + * Receipt-token metadata (the aToken's address/symbol/decimals/icon) comes from + * the SDK reserve already in app state — no integrator-owned copies. + */ +export function useSupplyButtonAction(): (reserve: SupplyButtonReserve) => void { + const currentMarket = useRootStore((store) => store.currentMarket); + const currentMarketData = useRootStore((store) => store.currentMarketData); + const { supplyReserves } = useAppDataContext(); + const { openSupply } = useModalContext(); + + return (reserve: SupplyButtonReserve) => { + if (isFunSupplyAsset(currentMarket, reserve.underlyingAsset)) { + const sdkReserve = supplyReserves.find( + (r) => r.underlyingToken.address.toLowerCase() === reserve.underlyingAsset.toLowerCase() + ); + const handled = + !!sdkReserve && + beginFunSupply({ + underlyingAsset: reserve.underlyingAsset, + symbol: reserve.symbol, + aTokenBase64: reserve.aTokenBase64, + supplyAPY: reserve.supplyAPY, + collateralEnabled: reserve.collateralEnabled, + chainId: currentMarketData.chainId, + poolAddress: currentMarketData.addresses.LENDING_POOL, + underlyingImageUrl: sdkReserve.underlyingToken.imageUrl, + aToken: sdkReserve.aToken, + }); + if (handled) { + return; + } + // Fall through to the native modal when the funkit island hasn't mounted + // yet (ssr:false chunk still loading) or the SDK market data isn't in + // yet — instead of dropping the click. + } + openSupply(reserve.underlyingAsset, currentMarket, reserve.name, 'dashboard'); + }; +} diff --git a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx index e59b0b6a7f..12864d5841 100644 --- a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx +++ b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx @@ -9,6 +9,7 @@ import { ListColumn } from 'src/components/lists/ListColumn'; import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle'; import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper'; import { Warning } from 'src/components/primitives/Warning'; +import { isFunSupplyAsset } from 'src/components/transactions/FunCheckout/funSupplyAssets'; import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; import { useCoingeckoCategories } from 'src/hooks/useCoinGeckoCategories'; import { useWrappedTokens } from 'src/hooks/useWrappedTokens'; @@ -202,6 +203,12 @@ export const SupplyAssetsList = () => { ); const filteredSupplyReserves = sortedSupplyReserves.filter((reserve) => { + // fun-routed assets can be supplied from any EVM asset / fiat via the funkit + // checkout, so an empty wallet must not hide them. + if (isFunSupplyAsset(currentMarket, reserve.underlyingAsset)) { + return true; + } + // Filter out dust amounts < $0.01 USD if (reserve.availableToDepositUSD !== '0' && Number(reserve.availableToDepositUSD) >= 0.01) { return true; diff --git a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx index 4b406c59e8..685a463d6a 100644 --- a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx +++ b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx @@ -21,6 +21,9 @@ import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; import { NoData } from 'src/components/primitives/NoData'; import { Row } from 'src/components/primitives/Row'; import { TokenIcon } from 'src/components/primitives/TokenIcon'; +import { isFunSupplyAsset } from 'src/components/transactions/FunCheckout/funSupplyAssets'; +import { useFunSupplyATokenIcon } from 'src/components/transactions/FunCheckout/useFunSupplyATokenIcon'; +import { useSupplyButtonAction } from 'src/components/transactions/FunCheckout/useSupplyButtonAction'; import { WalletBalancesMap } from 'src/hooks/app-data-provider/useWalletBalances'; import { useAssetCaps } from 'src/hooks/useAssetCaps'; import { useModalContext } from 'src/hooks/useModal'; @@ -50,6 +53,7 @@ export const SupplyAssetsListItem = ( const downToXSM = useMediaQuery(theme.breakpoints.down('xsm')); const { supplyCap } = useAssetCaps(); const wrappedTokenReserves = useWrappedTokens(); + const currentMarket = useRootStore((store) => store.currentMarket); const { isActive, isFreezed, walletBalance, underlyingAsset } = params; @@ -61,10 +65,15 @@ export const SupplyAssetsListItem = ( wrappedToken && params.walletBalances[wrappedToken.tokenIn.underlyingAsset.toLowerCase()].amount !== '0'; + // fun-routed assets can be supplied from any EVM asset / fiat via the funkit + // checkout, so an empty wallet doesn't block supplying them (protocol-level + // blocks — inactive/frozen/capped — still apply). + const isFunSupply = isFunSupplyAsset(currentMarket, underlyingAsset); + const disableSupply = !isActive || isFreezed || - (Number(walletBalance) <= 0 && !canSupplyAsWrappedToken) || + (Number(walletBalance) <= 0 && !canSupplyAsWrappedToken && !isFunSupply) || supplyCap.isMaxed; const props: SupplyAssetsListItemProps = { @@ -110,7 +119,13 @@ export const SupplyAssetsListItemDesktop = ({ const currentMarket = useRootStore((store) => store.currentMarket); const wrappedTokenReserves = useWrappedTokens(); - const { openSupply, openSwitch } = useModalContext(); + const { openSwitch } = useModalContext(); + const handleSupplyClick = useSupplyButtonAction(); + // Ringed aToken icon for the fun checkout's add-to-wallet (fun-routed rows only) + const { aTokenBase64, generator: aTokenIconGenerator } = useFunSupplyATokenIcon( + underlyingAsset, + iconSymbol + ); // Disable the asset to prevent it from being supplied if supply cap has been reached const { supplyCap: supplyCapUsage, debtCeiling } = useAssetCaps(); @@ -239,11 +254,19 @@ export const SupplyAssetsListItemDesktop = ({ + {aTokenIconGenerator}