From 6f46aceec34f0f3b869db8a5dc6f7d8cfdc700b8 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Tue, 28 Jul 2026 17:59:49 -0400 Subject: [PATCH 01/45] fix web build on current Node --- apps/web/babel.config.js | 14 +++++++++++--- apps/web/next.config.js | 2 +- apps/web/src/hooks/useCascade.ts | 18 +++++++++++++++++- package.json | 2 +- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/web/babel.config.js b/apps/web/babel.config.js index 201b413..8b30343 100644 --- a/apps/web/babel.config.js +++ b/apps/web/babel.config.js @@ -1,6 +1,14 @@ -// apps/web/babel.config.js +const path = require('path') + module.exports = { presets: ['next/babel'], - plugins: ['@tamagui/babel-plugin'], + plugins: [ + [ + '@tamagui/babel-plugin', + { + config: path.resolve(__dirname, '../../tamagui.config.ts'), + components: ['tamagui'], + }, + ], + ], } - diff --git a/apps/web/next.config.js b/apps/web/next.config.js index b0e5cd5..9228686 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -7,7 +7,7 @@ const tamaguiConfigPath = path.resolve(__dirname, '../../tamagui.config.ts') module.exports = withTamagui({ config: tamaguiConfigPath, - components: ['tamagui', '@lumera-hub/ui'], + components: ['tamagui'], appDir: true, })({ reactStrictMode: true, diff --git a/apps/web/src/hooks/useCascade.ts b/apps/web/src/hooks/useCascade.ts index 68cb1fb..85c7594 100644 --- a/apps/web/src/hooks/useCascade.ts +++ b/apps/web/src/hooks/useCascade.ts @@ -98,7 +98,18 @@ export const FILES_TYPE: FileTypeOption[] = [ const GAS_PRICE = '0.025ulume'; -const client = new IPLocate(process.env.NEXT_PUBLIC_IPAPI_KEY || ''); +let ipLocateClient: IPLocate | undefined; + +const getIpLocateClient = () => { + const apiKey = process.env.NEXT_PUBLIC_IPAPI_KEY; + + if (!apiKey) { + return null; + } + + ipLocateClient ??= new IPLocate(apiKey); + return ipLocateClient; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any const useCascade = ({ lumeraSdk }: { lumeraSdk: any }) => { @@ -164,6 +175,11 @@ const useCascade = ({ lumeraSdk }: { lumeraSdk: any }) => { const fetchLocationFromIpLocate = async (ip: string) => { try { + const client = getIpLocateClient(); + if (!client) { + return null; + } + const result = await client.lookup(ip); return { latitude: result?.latitude || null, diff --git a/package.json b/package.json index 1260492..d84a770 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "typescript": "5.8.3" }, "engines": { - "node": ">=24 <25" + "node": ">=24 <27" }, "version": "0.1.0", "type": "module", From 5ebaa2fa105974e17679a7f7b8bc7d90e87646b0 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Tue, 28 Jul 2026 18:36:12 -0400 Subject: [PATCH 02/45] add selectable network profiles --- README.md | 14 +++++++ apps/web/.env.example | 20 +++++---- apps/web/src/contants/network.ts | 70 +++++++++++++++++++++++++++++--- apps/web/src/utils/helpers.ts | 51 ++++++++++++++++------- 4 files changed, 127 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index d6929e6..bcc4b42 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ pnpm install ## Development +### Select a network + +Copy the web environment template and select one of the local network profiles: + +```bash +cp apps/web/.env.example apps/web/.env.local +``` + +```dotenv +NEXT_PUBLIC_NETWORK_PROFILE=testnet +``` + +Supported profiles are `devnet`, `testnet`, and `mainnet`. Their chain IDs and endpoints are defined together in `apps/web/src/contants/network.ts`. Individual `NEXT_PUBLIC_CHAIN_NAME`, `NEXT_PUBLIC_CHAIN_ID`, `NEXT_PUBLIC_RPC_ENDPOINT`, `NEXT_PUBLIC_REST_AI_URL`, `NEXT_PUBLIC_EVM_RPC_ENDPOINT`, `NEXT_PUBLIC_EVM_WS_ENDPOINT`, and `NEXT_PUBLIC_SNAPI_URL` values can still override the selected profile. + ### Run Dev Servers with Watcher Run all apps in development mode: diff --git a/apps/web/.env.example b/apps/web/.env.example index 8b6f339..a94ee0f 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,15 +1,21 @@ -NEXT_PUBLIC_NODE_ENV=production -NEXT_PUBLIC_CHAIN_NAME=lumera -NEXT_PUBLIC_DENOM=ulume -NEXT_PUBLIC_CHAIN_ID=lumera-testnet-2 -NEXT_PUBLIC_REST_AI_URL=https://lcd.testnet.lumera.io -NEXT_PUBLIC_RPC_ENDPOINT=https://rpc.testnet.lumera.io +# Switch the complete local endpoint set with: devnet, testnet, or mainnet. +NEXT_PUBLIC_NETWORK_PROFILE=testnet + +# Optional per-value overrides. Leave unset to use the selected profile. +# NEXT_PUBLIC_CHAIN_NAME=lumeratestnet +# NEXT_PUBLIC_DENOM=ulume +# NEXT_PUBLIC_CHAIN_ID=lumera-testnet-2 +# NEXT_PUBLIC_REST_AI_URL=https://lcd-testnet.lumeraprotocol.com +# NEXT_PUBLIC_RPC_ENDPOINT=https://rpc-testnet.lumeraprotocol.com +# NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-testnet.lumeraprotocol.com +# NEXT_PUBLIC_EVM_WS_ENDPOINT=https://evm-ws-testnet.lumeraprotocol.com +# NEXT_PUBLIC_SNAPI_URL=http://localhost:3100 + NEXT_PUBLIC_WALLET_CONNECT_PROJECTID=fd049c1154d0886fda615b1c2e08ee28 NEXT_PUBLIC_WALLET_CONNECT_RELAY_URL=wss://relay.walletconnect.org NEXT_PUBLIC_WALLET_CONNECT_NAME=Lumera Hub NEXT_PUBLIC_WALLET_CONNECT_DESCRIPTION=Lumera Hub NEXT_PUBLIC_WALLET_CONNECT_URL=https://hub.testnet.lumera.io NEXT_PUBLIC_WALLET_CONNECT_ICON=https://portal.testnet.lumera.io/assets/logo-5cb73fc7.png -NEXT_PUBLIC_SNAPI_URL=http://localhost:3100 NEXT_PUBLIC_IPAPI_KEY= NEXT_PUBLIC_ABSTRACTAPI_KEY= diff --git a/apps/web/src/contants/network.ts b/apps/web/src/contants/network.ts index a860071..9b40b86 100644 --- a/apps/web/src/contants/network.ts +++ b/apps/web/src/contants/network.ts @@ -1,12 +1,70 @@ -export const CHAIN_NAME = process.env.NEXT_PUBLIC_CHAIN_NAME || 'lumera'; -export const DENOM = process.env.NEXT_PUBLIC_DENOM || 'ulume'; -export const CHAIN_ID = process.env.NEXT_PUBLIC_CHAIN_ID || 'lumera-mainnet-1'; -export const RPC_ENDPOINT = process.env.NEXT_PUBLIC_RPC_ENDPOINT || 'https://rpc.lumera.io'; -export const REST_AI_URL = process.env.NEXT_PUBLIC_REST_AI_URL || 'https://lcd.lumera.io'; +export const NETWORK_PROFILES = { + devnet: { + chainName: 'lumera-devnet', + chainId: 'lumera-devnet-1', + denom: 'ulume', + rpcEndpoint: 'https://rpc.pastel.network', + restEndpoint: 'https://lcd.pastel.network', + evmRpcEndpoint: 'https://evm-rpc.pastel.network', + evmWsEndpoint: null, + snapiUrl: 'http://localhost:3100', + }, + testnet: { + chainName: 'lumera-testnet', + chainId: 'lumera-testnet-2', + denom: 'ulume', + rpcEndpoint: 'https://rpc-testnet.lumeraprotocol.com', + restEndpoint: 'https://lcd-testnet.lumeraprotocol.com', + evmRpcEndpoint: 'https://evm-testnet.lumeraprotocol.com', + evmWsEndpoint: 'https://evm-ws-testnet.lumeraprotocol.com', + snapiUrl: 'http://localhost:3100', + }, + mainnet: { + chainName: 'lumera', + chainId: 'lumera-mainnet-1', + denom: 'ulume', + rpcEndpoint: 'https://rpc.lumera.io', + restEndpoint: 'https://lcd.lumera.io', + evmRpcEndpoint: null, + evmWsEndpoint: null, + snapiUrl: 'http://localhost:3100', + }, +} as const; + +export type NetworkProfile = keyof typeof NETWORK_PROFILES; + +const isNetworkProfile = (value: string): value is NetworkProfile => value in NETWORK_PROFILES; + +const getLegacyNetworkProfile = (): NetworkProfile | undefined => { + if (process.env.NEXT_PUBLIC_NODE_ENV === 'devnet') return 'devnet'; + if (process.env.NEXT_PUBLIC_NODE_ENV === 'dev') return 'testnet'; + return undefined; +}; + +const requestedProfile = process.env.NEXT_PUBLIC_NETWORK_PROFILE || getLegacyNetworkProfile() || 'mainnet'; + +if (!isNetworkProfile(requestedProfile)) { + throw new Error( + `Unknown network profile "${requestedProfile}". Expected one of: ${Object.keys(NETWORK_PROFILES).join(', ')}` + ); +} + +export const NETWORK_PROFILE: NetworkProfile = requestedProfile; + +export const ACTIVE_NETWORK = NETWORK_PROFILES[NETWORK_PROFILE]; + +// Individual overrides are useful for local nodes and private deployments. +export const CHAIN_NAME = process.env.NEXT_PUBLIC_CHAIN_NAME || ACTIVE_NETWORK.chainName; +export const DENOM = process.env.NEXT_PUBLIC_DENOM || ACTIVE_NETWORK.denom; +export const CHAIN_ID = process.env.NEXT_PUBLIC_CHAIN_ID || ACTIVE_NETWORK.chainId; +export const RPC_ENDPOINT = process.env.NEXT_PUBLIC_RPC_ENDPOINT || ACTIVE_NETWORK.rpcEndpoint; +export const REST_AI_URL = process.env.NEXT_PUBLIC_REST_AI_URL || ACTIVE_NETWORK.restEndpoint; +export const EVM_RPC_ENDPOINT = process.env.NEXT_PUBLIC_EVM_RPC_ENDPOINT || ACTIVE_NETWORK.evmRpcEndpoint; +export const EVM_WS_ENDPOINT = process.env.NEXT_PUBLIC_EVM_WS_ENDPOINT || ACTIVE_NETWORK.evmWsEndpoint; export const WALLET_CONNECT_PROJECTID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECTID || 'fd049c1154d0886fda615b1c2e08ee28'; export const WALLET_CONNECT_RELAY_URL = process.env.NEXT_PUBLIC_WALLET_CONNECT_RELAY_URL || 'wss://relay.walletconnect.org'; export const WALLET_CONNECT_NAME = process.env.NEXT_PUBLIC_WALLET_CONNECT_NAME || 'Lumera Hub'; export const WALLET_CONNECT_DESCRIPTION = process.env.NEXT_PUBLIC_WALLET_CONNECT_DESCRIPTION || 'Lumera Hub'; export const WALLET_CONNECT_URL = process.env.NEXT_PUBLIC_WALLET_CONNECT_URL || 'https://hub.testnet.lumera.io/'; export const WALLET_CONNECT_ICON = process.env.NEXT_PUBLIC_WALLET_CONNECT_ICON || 'https://portal.testnet.lumera.io/assets/logo-5cb73fc7.png'; -export const SNAPI_URL = process.env.NEXT_PUBLIC_SNAPI_URL || 'http://localhost:3100'; +export const SNAPI_URL = process.env.NEXT_PUBLIC_SNAPI_URL || ACTIVE_NETWORK.snapiUrl; diff --git a/apps/web/src/utils/helpers.ts b/apps/web/src/utils/helpers.ts index 8758cd4..f48637d 100644 --- a/apps/web/src/utils/helpers.ts +++ b/apps/web/src/utils/helpers.ts @@ -12,6 +12,14 @@ export { parseCoins } from '@cosmjs/stargate'; import { MsgDelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; import { IValidator } from '@/types/validator'; +import { + CHAIN_ID, + CHAIN_NAME, + DENOM, + NETWORK_PROFILE, + REST_AI_URL, + RPC_ENDPOINT, +} from '@/contants/network'; export const getMessages = (msgs: { '@type'?: string; typeUrl?: string }[]) => { if (msgs) { @@ -83,12 +91,12 @@ export const mapAmount = (events:{type: string, attributes: {key: string, value: } export const getChains = () => { - if (process.env.NEXT_PUBLIC_NODE_ENV === 'devnet') { + if (NETWORK_PROFILE === 'devnet') { const lumeraChain = { - chainName: 'lumera-testnet', + chainName: CHAIN_NAME, status: 'live', networkType: 'testnet', - chainId: 'lumera-devnet-1', + chainId: CHAIN_ID, chainType: "cosmos", prettyName: 'Lumera Devnet', chainSymbol: 'lumera-testnet', @@ -100,7 +108,7 @@ export const getChains = () => { fees: { feeTokens: [ { - denom: 'ulume', + denom: DENOM, fixedMinGasPrice: '0.025', lowGasPrice: '0.025', averageGasPrice: '0.025', @@ -114,13 +122,13 @@ export const getChains = () => { apis: { rpc: [ { - address: 'https://rpc.pastel.network', + address: RPC_ENDPOINT, provider: 'lumera', }, ], rest: [ { - address: 'https://lcd.pastel.network', + address: REST_AI_URL, provider: 'lumera', }, ], @@ -135,13 +143,13 @@ export const getChains = () => { features: ['cosmwasm'], }; const lumeraAssets = { - chainName: 'lumera-testnet', + chainName: CHAIN_NAME, assets: [ { description: 'Lumera native token on Lumera Devnet', denomUnits: [ { - denom: 'ulume', + denom: DENOM, exponent: 0, aliases: ['microlume'], }, @@ -169,15 +177,28 @@ export const getChains = () => { chains: [lumeraChain], } } - if (process.env.NEXT_PUBLIC_NODE_ENV === 'dev') { - return { - assetLists: chainTestnet.assetLists, - chains: chainTestnet.chains, - } + + const registry = NETWORK_PROFILE === 'testnet' ? chainTestnet : chainMainnet; + const chain = registry.chains.find(({ chainName }) => chainName === CHAIN_NAME); + const assets = registry.assetLists.find(({ chainName }) => chainName === CHAIN_NAME); + + if (!chain || !assets) { + return { assetLists: [], chains: [] }; } + return { - assetLists: chainMainnet.assetLists, - chains: chainMainnet.chains, + assetLists: [assets], + chains: [ + { + ...chain, + chainId: CHAIN_ID, + apis: { + ...chain.apis, + rpc: [{ address: RPC_ENDPOINT, provider: 'Lumera Hub profile' }], + rest: [{ address: REST_AI_URL, provider: 'Lumera Hub profile' }], + }, + }, + ], } } From fdf68e284525bf18a0a522e8158575fcaf18bf18 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Fri, 31 Jul 2026 10:40:38 -0400 Subject: [PATCH 03/45] add EVM wallet support for EVM-enabled network profiles Profiles that define both an EVM RPC endpoint and EVM chain ID now use an injected EIP-1193 wallet (e.g. MetaMask) for connecting, native LUME balances, and transfers. Staking, governance signing, and transaction history stay on the legacy Cosmos flow and are hidden or disabled on EVM profiles. Co-Authored-By: Claude Fable 5 --- README.md | 4 +- apps/web/.env.example | 1 + apps/web/.tamagui/tamagui.config.json | 927 ------------------ .../src/app/providers/evm-wallet-provider.tsx | 159 +++ .../web/src/app/providers/wallet-provider.tsx | 25 +- apps/web/src/app/wallet/page.tsx | 22 +- apps/web/src/components/ConnectWallet.tsx | 98 +- apps/web/src/components/SendModal.tsx | 13 +- apps/web/src/contants/network.ts | 16 + apps/web/src/hooks/useAccountInfo.ts | 23 +- apps/web/src/hooks/useDelegate.ts | 11 +- apps/web/src/hooks/useSend.ts | 57 +- apps/web/src/hooks/useTransaction.ts | 8 + apps/web/src/hooks/useWalletConnect.ts | 18 +- apps/web/src/types/window.d.ts | 9 +- apps/web/src/utils/evm.ts | 77 ++ apps/web/src/utils/helpers.ts | 11 +- packages/ui/src/screens/WalletScreen.tsx | 58 +- 18 files changed, 523 insertions(+), 1014 deletions(-) create mode 100644 apps/web/src/app/providers/evm-wallet-provider.tsx create mode 100644 apps/web/src/utils/evm.ts diff --git a/README.md b/README.md index bcc4b42..f7cbfb4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ cp apps/web/.env.example apps/web/.env.local NEXT_PUBLIC_NETWORK_PROFILE=testnet ``` -Supported profiles are `devnet`, `testnet`, and `mainnet`. Their chain IDs and endpoints are defined together in `apps/web/src/contants/network.ts`. Individual `NEXT_PUBLIC_CHAIN_NAME`, `NEXT_PUBLIC_CHAIN_ID`, `NEXT_PUBLIC_RPC_ENDPOINT`, `NEXT_PUBLIC_REST_AI_URL`, `NEXT_PUBLIC_EVM_RPC_ENDPOINT`, `NEXT_PUBLIC_EVM_WS_ENDPOINT`, and `NEXT_PUBLIC_SNAPI_URL` values can still override the selected profile. +Supported profiles are `devnet`, `testnet`, and `mainnet`. Their chain IDs and endpoints are defined together in `apps/web/src/contants/network.ts`. Individual `NEXT_PUBLIC_CHAIN_NAME`, `NEXT_PUBLIC_CHAIN_ID`, `NEXT_PUBLIC_RPC_ENDPOINT`, `NEXT_PUBLIC_REST_AI_URL`, `NEXT_PUBLIC_EVM_RPC_ENDPOINT`, `NEXT_PUBLIC_EVM_WS_ENDPOINT`, `NEXT_PUBLIC_EVM_CHAIN_ID`, and `NEXT_PUBLIC_SNAPI_URL` values can still override the selected profile. + +Profiles with both an EVM RPC endpoint and EVM chain ID use an injected EIP-1193 wallet for native LUME balances and transfers. Profiles without those values continue to use the legacy Cosmos/Interchain wallet flow. ### Run Dev Servers with Watcher diff --git a/apps/web/.env.example b/apps/web/.env.example index a94ee0f..b783e4f 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -9,6 +9,7 @@ NEXT_PUBLIC_NETWORK_PROFILE=testnet # NEXT_PUBLIC_RPC_ENDPOINT=https://rpc-testnet.lumeraprotocol.com # NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-testnet.lumeraprotocol.com # NEXT_PUBLIC_EVM_WS_ENDPOINT=https://evm-ws-testnet.lumeraprotocol.com +# NEXT_PUBLIC_EVM_CHAIN_ID=76857769 # NEXT_PUBLIC_SNAPI_URL=http://localhost:3100 NEXT_PUBLIC_WALLET_CONNECT_PROJECTID=fd049c1154d0886fda615b1c2e08ee28 diff --git a/apps/web/.tamagui/tamagui.config.json b/apps/web/.tamagui/tamagui.config.json index d8e53cf..e14ecef 100644 --- a/apps/web/.tamagui/tamagui.config.json +++ b/apps/web/.tamagui/tamagui.config.json @@ -35718,933 +35718,6 @@ } } } - }, - { - "moduleName": "@lumera-hub/ui", - "nameToInfo": { - "H1": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h1", - "unstyled": false - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$10", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } - }, - "validStyles": { - "backfaceVisibility": true, - "borderBottomEndRadius": true, - "borderBottomStartRadius": true, - "borderBottomWidth": true, - "borderLeftWidth": true, - "borderRightWidth": true, - "borderBlockWidth": true, - "borderBlockEndWidth": true, - "borderBlockStartWidth": true, - "borderInlineWidth": true, - "borderInlineEndWidth": true, - "borderInlineStartWidth": true, - "borderStyle": true, - "borderBlockStyle": true, - "borderBlockEndStyle": true, - "borderBlockStartStyle": true, - "borderInlineStyle": true, - "borderInlineEndStyle": true, - "borderInlineStartStyle": true, - "borderTopEndRadius": true, - "borderTopStartRadius": true, - "borderTopWidth": true, - "borderWidth": true, - "transform": true, - "transformOrigin": true, - "alignContent": true, - "alignItems": true, - "alignSelf": true, - "borderEndWidth": true, - "borderStartWidth": true, - "bottom": true, - "display": true, - "end": true, - "flexBasis": true, - "flexDirection": true, - "flexWrap": true, - "gap": true, - "columnGap": true, - "rowGap": true, - "justifyContent": true, - "left": true, - "margin": true, - "marginBlock": true, - "marginBlockEnd": true, - "marginBlockStart": true, - "marginInline": true, - "marginInlineStart": true, - "marginInlineEnd": true, - "marginBottom": true, - "marginEnd": true, - "marginHorizontal": true, - "marginLeft": true, - "marginRight": true, - "marginStart": true, - "marginTop": true, - "marginVertical": true, - "overflow": true, - "padding": true, - "paddingBottom": true, - "paddingInline": true, - "paddingBlock": true, - "paddingBlockStart": true, - "paddingInlineEnd": true, - "paddingInlineStart": true, - "paddingEnd": true, - "paddingHorizontal": true, - "paddingLeft": true, - "paddingRight": true, - "paddingStart": true, - "paddingTop": true, - "paddingVertical": true, - "position": true, - "right": true, - "start": true, - "top": true, - "inset": true, - "insetBlock": true, - "insetBlockEnd": true, - "insetBlockStart": true, - "insetInline": true, - "insetInlineEnd": true, - "insetInlineStart": true, - "direction": true, - "shadowOffset": true, - "shadowRadius": true, - "backgroundColor": true, - "borderColor": true, - "borderBlockStartColor": true, - "borderBlockEndColor": true, - "borderBlockColor": true, - "borderBottomColor": true, - "borderInlineColor": true, - "borderInlineStartColor": true, - "borderInlineEndColor": true, - "borderTopColor": true, - "borderLeftColor": true, - "borderRightColor": true, - "borderEndColor": true, - "borderStartColor": true, - "shadowColor": true, - "color": true, - "textDecorationColor": true, - "textShadowColor": true, - "outlineColor": true, - "caretColor": true, - "borderRadius": true, - "borderTopLeftRadius": true, - "borderTopRightRadius": true, - "borderBottomLeftRadius": true, - "borderBottomRightRadius": true, - "borderStartStartRadius": true, - "borderStartEndRadius": true, - "borderEndStartRadius": true, - "borderEndEndRadius": true, - "width": true, - "height": true, - "minWidth": true, - "minHeight": true, - "maxWidth": true, - "maxHeight": true, - "blockSize": true, - "minBlockSize": true, - "maxBlockSize": true, - "inlineSize": true, - "minInlineSize": true, - "maxInlineSize": true, - "x": true, - "y": true, - "scale": true, - "perspective": true, - "scaleX": true, - "scaleY": true, - "skewX": true, - "skewY": true, - "matrix": true, - "rotate": true, - "rotateY": true, - "rotateX": true, - "rotateZ": true, - "WebkitLineClamp": true, - "animationIterationCount": true, - "aspectRatio": true, - "borderImageOutset": true, - "borderImageSlice": true, - "borderImageWidth": true, - "columnCount": true, - "flex": true, - "flexGrow": true, - "flexOrder": true, - "flexPositive": true, - "flexShrink": true, - "flexNegative": true, - "fontWeight": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowGap": true, - "gridRowStart": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnGap": true, - "gridColumnStart": true, - "gridTemplateColumns": true, - "gridTemplateAreas": true, - "lineClamp": true, - "opacity": true, - "order": true, - "orphans": true, - "tabSize": true, - "widows": true, - "zIndex": true, - "zoom": true, - "scaleZ": true, - "shadowOpacity": true, - "boxShadow": true, - "filter": true, - "transition": true, - "textWrap": true, - "backdropFilter": true, - "WebkitBackdropFilter": true, - "background": true, - "backgroundAttachment": true, - "backgroundBlendMode": true, - "backgroundClip": true, - "backgroundImage": true, - "backgroundOrigin": true, - "backgroundPosition": true, - "backgroundRepeat": true, - "backgroundSize": true, - "borderBottomStyle": true, - "borderImage": true, - "borderLeftStyle": true, - "borderRightStyle": true, - "borderTopStyle": true, - "boxSizing": true, - "clipPath": true, - "contain": true, - "containerType": true, - "content": true, - "cursor": true, - "float": true, - "mask": true, - "maskBorder": true, - "maskBorderMode": true, - "maskBorderOutset": true, - "maskBorderRepeat": true, - "maskBorderSlice": true, - "maskBorderSource": true, - "maskBorderWidth": true, - "maskClip": true, - "maskComposite": true, - "maskImage": true, - "maskMode": true, - "maskOrigin": true, - "maskPosition": true, - "maskRepeat": true, - "maskSize": true, - "maskType": true, - "mixBlendMode": true, - "objectFit": true, - "objectPosition": true, - "outlineOffset": true, - "outlineStyle": true, - "outlineWidth": true, - "overflowBlock": true, - "overflowInline": true, - "overflowX": true, - "overflowY": true, - "pointerEvents": true, - "scrollbarWidth": true, - "textEmphasis": true, - "touchAction": true, - "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "H1", - "isReactNative": false, - "isStyledHOC": false - } - }, - "Paragraph": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "margin": 0, - "fontFamily": "$body", - "unstyled": false, - "tag": "p", - "userSelect": "auto", - "color": "$color", - "size": "$true", - "whiteSpace": "normal" - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$true", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } - }, - "validStyles": { - "backfaceVisibility": true, - "borderBottomEndRadius": true, - "borderBottomStartRadius": true, - "borderBottomWidth": true, - "borderLeftWidth": true, - "borderRightWidth": true, - "borderBlockWidth": true, - "borderBlockEndWidth": true, - "borderBlockStartWidth": true, - "borderInlineWidth": true, - "borderInlineEndWidth": true, - "borderInlineStartWidth": true, - "borderStyle": true, - "borderBlockStyle": true, - "borderBlockEndStyle": true, - "borderBlockStartStyle": true, - "borderInlineStyle": true, - "borderInlineEndStyle": true, - "borderInlineStartStyle": true, - "borderTopEndRadius": true, - "borderTopStartRadius": true, - "borderTopWidth": true, - "borderWidth": true, - "transform": true, - "transformOrigin": true, - "alignContent": true, - "alignItems": true, - "alignSelf": true, - "borderEndWidth": true, - "borderStartWidth": true, - "bottom": true, - "display": true, - "end": true, - "flexBasis": true, - "flexDirection": true, - "flexWrap": true, - "gap": true, - "columnGap": true, - "rowGap": true, - "justifyContent": true, - "left": true, - "margin": true, - "marginBlock": true, - "marginBlockEnd": true, - "marginBlockStart": true, - "marginInline": true, - "marginInlineStart": true, - "marginInlineEnd": true, - "marginBottom": true, - "marginEnd": true, - "marginHorizontal": true, - "marginLeft": true, - "marginRight": true, - "marginStart": true, - "marginTop": true, - "marginVertical": true, - "overflow": true, - "padding": true, - "paddingBottom": true, - "paddingInline": true, - "paddingBlock": true, - "paddingBlockStart": true, - "paddingInlineEnd": true, - "paddingInlineStart": true, - "paddingEnd": true, - "paddingHorizontal": true, - "paddingLeft": true, - "paddingRight": true, - "paddingStart": true, - "paddingTop": true, - "paddingVertical": true, - "position": true, - "right": true, - "start": true, - "top": true, - "inset": true, - "insetBlock": true, - "insetBlockEnd": true, - "insetBlockStart": true, - "insetInline": true, - "insetInlineEnd": true, - "insetInlineStart": true, - "direction": true, - "shadowOffset": true, - "shadowRadius": true, - "backgroundColor": true, - "borderColor": true, - "borderBlockStartColor": true, - "borderBlockEndColor": true, - "borderBlockColor": true, - "borderBottomColor": true, - "borderInlineColor": true, - "borderInlineStartColor": true, - "borderInlineEndColor": true, - "borderTopColor": true, - "borderLeftColor": true, - "borderRightColor": true, - "borderEndColor": true, - "borderStartColor": true, - "shadowColor": true, - "color": true, - "textDecorationColor": true, - "textShadowColor": true, - "outlineColor": true, - "caretColor": true, - "borderRadius": true, - "borderTopLeftRadius": true, - "borderTopRightRadius": true, - "borderBottomLeftRadius": true, - "borderBottomRightRadius": true, - "borderStartStartRadius": true, - "borderStartEndRadius": true, - "borderEndStartRadius": true, - "borderEndEndRadius": true, - "width": true, - "height": true, - "minWidth": true, - "minHeight": true, - "maxWidth": true, - "maxHeight": true, - "blockSize": true, - "minBlockSize": true, - "maxBlockSize": true, - "inlineSize": true, - "minInlineSize": true, - "maxInlineSize": true, - "x": true, - "y": true, - "scale": true, - "perspective": true, - "scaleX": true, - "scaleY": true, - "skewX": true, - "skewY": true, - "matrix": true, - "rotate": true, - "rotateY": true, - "rotateX": true, - "rotateZ": true, - "WebkitLineClamp": true, - "animationIterationCount": true, - "aspectRatio": true, - "borderImageOutset": true, - "borderImageSlice": true, - "borderImageWidth": true, - "columnCount": true, - "flex": true, - "flexGrow": true, - "flexOrder": true, - "flexPositive": true, - "flexShrink": true, - "flexNegative": true, - "fontWeight": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowGap": true, - "gridRowStart": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnGap": true, - "gridColumnStart": true, - "gridTemplateColumns": true, - "gridTemplateAreas": true, - "lineClamp": true, - "opacity": true, - "order": true, - "orphans": true, - "tabSize": true, - "widows": true, - "zIndex": true, - "zoom": true, - "scaleZ": true, - "shadowOpacity": true, - "boxShadow": true, - "filter": true, - "transition": true, - "textWrap": true, - "backdropFilter": true, - "WebkitBackdropFilter": true, - "background": true, - "backgroundAttachment": true, - "backgroundBlendMode": true, - "backgroundClip": true, - "backgroundImage": true, - "backgroundOrigin": true, - "backgroundPosition": true, - "backgroundRepeat": true, - "backgroundSize": true, - "borderBottomStyle": true, - "borderImage": true, - "borderLeftStyle": true, - "borderRightStyle": true, - "borderTopStyle": true, - "boxSizing": true, - "clipPath": true, - "contain": true, - "containerType": true, - "content": true, - "cursor": true, - "float": true, - "mask": true, - "maskBorder": true, - "maskBorderMode": true, - "maskBorderOutset": true, - "maskBorderRepeat": true, - "maskBorderSlice": true, - "maskBorderSource": true, - "maskBorderWidth": true, - "maskClip": true, - "maskComposite": true, - "maskImage": true, - "maskMode": true, - "maskOrigin": true, - "maskPosition": true, - "maskRepeat": true, - "maskSize": true, - "maskType": true, - "mixBlendMode": true, - "objectFit": true, - "objectPosition": true, - "outlineOffset": true, - "outlineStyle": true, - "outlineWidth": true, - "overflowBlock": true, - "overflowInline": true, - "overflowX": true, - "overflowY": true, - "pointerEvents": true, - "scrollbarWidth": true, - "textEmphasis": true, - "touchAction": true, - "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "componentName": "Paragraph", - "isReactNative": false, - "isStyledHOC": false - } - }, - "YStack": { - "staticConfig": { - "acceptsClassName": true, - "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", - "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column" - }, - "validStyles": { - "backfaceVisibility": true, - "borderBottomEndRadius": true, - "borderBottomStartRadius": true, - "borderBottomWidth": true, - "borderLeftWidth": true, - "borderRightWidth": true, - "borderBlockWidth": true, - "borderBlockEndWidth": true, - "borderBlockStartWidth": true, - "borderInlineWidth": true, - "borderInlineEndWidth": true, - "borderInlineStartWidth": true, - "borderStyle": true, - "borderBlockStyle": true, - "borderBlockEndStyle": true, - "borderBlockStartStyle": true, - "borderInlineStyle": true, - "borderInlineEndStyle": true, - "borderInlineStartStyle": true, - "borderTopEndRadius": true, - "borderTopStartRadius": true, - "borderTopWidth": true, - "borderWidth": true, - "transform": true, - "transformOrigin": true, - "alignContent": true, - "alignItems": true, - "alignSelf": true, - "borderEndWidth": true, - "borderStartWidth": true, - "bottom": true, - "display": true, - "end": true, - "flexBasis": true, - "flexDirection": true, - "flexWrap": true, - "gap": true, - "columnGap": true, - "rowGap": true, - "justifyContent": true, - "left": true, - "margin": true, - "marginBlock": true, - "marginBlockEnd": true, - "marginBlockStart": true, - "marginInline": true, - "marginInlineStart": true, - "marginInlineEnd": true, - "marginBottom": true, - "marginEnd": true, - "marginHorizontal": true, - "marginLeft": true, - "marginRight": true, - "marginStart": true, - "marginTop": true, - "marginVertical": true, - "overflow": true, - "padding": true, - "paddingBottom": true, - "paddingInline": true, - "paddingBlock": true, - "paddingBlockStart": true, - "paddingInlineEnd": true, - "paddingInlineStart": true, - "paddingEnd": true, - "paddingHorizontal": true, - "paddingLeft": true, - "paddingRight": true, - "paddingStart": true, - "paddingTop": true, - "paddingVertical": true, - "position": true, - "right": true, - "start": true, - "top": true, - "inset": true, - "insetBlock": true, - "insetBlockEnd": true, - "insetBlockStart": true, - "insetInline": true, - "insetInlineEnd": true, - "insetInlineStart": true, - "direction": true, - "shadowOffset": true, - "shadowRadius": true, - "backgroundColor": true, - "borderColor": true, - "borderBlockStartColor": true, - "borderBlockEndColor": true, - "borderBlockColor": true, - "borderBottomColor": true, - "borderInlineColor": true, - "borderInlineStartColor": true, - "borderInlineEndColor": true, - "borderTopColor": true, - "borderLeftColor": true, - "borderRightColor": true, - "borderEndColor": true, - "borderStartColor": true, - "shadowColor": true, - "color": true, - "textDecorationColor": true, - "textShadowColor": true, - "outlineColor": true, - "caretColor": true, - "borderRadius": true, - "borderTopLeftRadius": true, - "borderTopRightRadius": true, - "borderBottomLeftRadius": true, - "borderBottomRightRadius": true, - "borderStartStartRadius": true, - "borderStartEndRadius": true, - "borderEndStartRadius": true, - "borderEndEndRadius": true, - "width": true, - "height": true, - "minWidth": true, - "minHeight": true, - "maxWidth": true, - "maxHeight": true, - "blockSize": true, - "minBlockSize": true, - "maxBlockSize": true, - "inlineSize": true, - "minInlineSize": true, - "maxInlineSize": true, - "x": true, - "y": true, - "scale": true, - "perspective": true, - "scaleX": true, - "scaleY": true, - "skewX": true, - "skewY": true, - "matrix": true, - "rotate": true, - "rotateY": true, - "rotateX": true, - "rotateZ": true, - "WebkitLineClamp": true, - "animationIterationCount": true, - "aspectRatio": true, - "borderImageOutset": true, - "borderImageSlice": true, - "borderImageWidth": true, - "columnCount": true, - "flex": true, - "flexGrow": true, - "flexOrder": true, - "flexPositive": true, - "flexShrink": true, - "flexNegative": true, - "fontWeight": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowGap": true, - "gridRowStart": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnGap": true, - "gridColumnStart": true, - "gridTemplateColumns": true, - "gridTemplateAreas": true, - "lineClamp": true, - "opacity": true, - "order": true, - "orphans": true, - "tabSize": true, - "widows": true, - "zIndex": true, - "zoom": true, - "scaleZ": true, - "shadowOpacity": true, - "boxShadow": true, - "filter": true, - "transition": true, - "textWrap": true, - "backdropFilter": true, - "WebkitBackdropFilter": true, - "background": true, - "backgroundAttachment": true, - "backgroundBlendMode": true, - "backgroundClip": true, - "backgroundImage": true, - "backgroundOrigin": true, - "backgroundPosition": true, - "backgroundRepeat": true, - "backgroundSize": true, - "borderBottomStyle": true, - "borderImage": true, - "borderLeftStyle": true, - "borderRightStyle": true, - "borderTopStyle": true, - "boxSizing": true, - "clipPath": true, - "contain": true, - "containerType": true, - "content": true, - "cursor": true, - "float": true, - "mask": true, - "maskBorder": true, - "maskBorderMode": true, - "maskBorderOutset": true, - "maskBorderRepeat": true, - "maskBorderSlice": true, - "maskBorderSource": true, - "maskBorderWidth": true, - "maskClip": true, - "maskComposite": true, - "maskImage": true, - "maskMode": true, - "maskOrigin": true, - "maskPosition": true, - "maskRepeat": true, - "maskSize": true, - "maskType": true, - "mixBlendMode": true, - "objectFit": true, - "objectPosition": true, - "outlineOffset": true, - "outlineStyle": true, - "outlineWidth": true, - "overflowBlock": true, - "overflowInline": true, - "overflowX": true, - "overflowY": true, - "pointerEvents": true, - "scrollbarWidth": true, - "textEmphasis": true, - "touchAction": true, - "transformStyle": true, - "userSelect": true - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function" - }, - "isReactNative": false, - "isText": false, - "isStyledHOC": false - } - } - } } ], "nameToPaths": {}, diff --git a/apps/web/src/app/providers/evm-wallet-provider.tsx b/apps/web/src/app/providers/evm-wallet-provider.tsx new file mode 100644 index 0000000..14a2b54 --- /dev/null +++ b/apps/web/src/app/providers/evm-wallet-provider.tsx @@ -0,0 +1,159 @@ +'use client' + +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; + +import { + ACTIVE_NETWORK, + EVM_CHAIN_ID, + EVM_RPC_ENDPOINT, + IS_EVM_NETWORK, +} from '@/contants/network'; +import { toHexChainId } from '@/utils/evm'; +import type { Eip1193Provider } from '@/types/window'; + +interface EvmProviderError extends Error { + code?: number; +} + +interface EvmWalletContextValue { + address: string; + isConnected: boolean; + isConnecting: boolean; + error: string; + provider: Eip1193Provider | null; + connect: () => Promise; + disconnect: () => Promise; + ensureNetwork: () => Promise; +} + +const EvmWalletContext = createContext(null); + +export function EvmWalletProvider({ children }: { children: React.ReactNode }) { + const [address, setAddress] = useState(''); + const [isConnecting, setConnecting] = useState(false); + const [error, setError] = useState(''); + const provider = typeof window === 'undefined' ? null : window.ethereum || null; + + const ensureNetwork = useCallback(async () => { + if (!IS_EVM_NETWORK || !EVM_CHAIN_ID || !EVM_RPC_ENDPOINT) { + throw new Error('The active network does not support EVM wallets.'); + } + if (!provider) { + throw new Error('No EVM wallet was detected. Install MetaMask or another compatible wallet.'); + } + + const chainId = toHexChainId(EVM_CHAIN_ID); + const currentChainId = await provider.request({ method: 'eth_chainId' }); + if (currentChainId.toLowerCase() === chainId.toLowerCase()) return; + + try { + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); + } catch (switchError) { + const typedError = switchError as EvmProviderError; + if (typedError.code !== 4902) throw switchError; + + await provider.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId, + chainName: ACTIVE_NETWORK.displayName, + nativeCurrency: { + name: 'Lumera', + symbol: 'LUME', + decimals: 18, + }, + rpcUrls: [EVM_RPC_ENDPOINT], + }], + }); + } + }, [provider]); + + const connect = useCallback(async () => { + setError(''); + setConnecting(true); + try { + if (!provider) { + throw new Error('No EVM wallet was detected. Install MetaMask or another compatible wallet.'); + } + const accounts = await provider.request({ method: 'eth_requestAccounts' }); + await ensureNetwork(); + setAddress(accounts[0] || ''); + } catch (connectError) { + const message = connectError instanceof Error ? connectError.message : 'Unable to connect EVM wallet.'; + setError(message); + throw new Error(message); + } finally { + setConnecting(false); + } + }, [ensureNetwork, provider]); + + const disconnect = useCallback(async () => { + try { + await provider?.request({ + method: 'wallet_revokePermissions', + params: [{ eth_accounts: {} }], + }); + } catch { + // Some injected wallets do not implement permission revocation. + } + setAddress(''); + setError(''); + }, [provider]); + + useEffect(() => { + if (!IS_EVM_NETWORK || !provider || !EVM_CHAIN_ID) return; + const expectedChainId = EVM_CHAIN_ID; + + const syncAccounts = async () => { + try { + const [accounts, chainId] = await Promise.all([ + provider.request({ method: 'eth_accounts' }), + provider.request({ method: 'eth_chainId' }), + ]); + setAddress( + chainId.toLowerCase() === toHexChainId(expectedChainId).toLowerCase() + ? accounts[0] || '' + : '' + ); + } catch { + setAddress(''); + } + }; + + const handleAccountsChanged = () => void syncAccounts(); + const handleChainChanged = () => void syncAccounts(); + + void syncAccounts(); + provider.on?.('accountsChanged', handleAccountsChanged); + provider.on?.('chainChanged', handleChainChanged); + + return () => { + provider.removeListener?.('accountsChanged', handleAccountsChanged); + provider.removeListener?.('chainChanged', handleChainChanged); + }; + }, [provider]); + + const value = useMemo(() => ({ + address, + isConnected: Boolean(address), + isConnecting, + error, + provider, + connect, + disconnect, + ensureNetwork, + }), [address, connect, disconnect, ensureNetwork, error, isConnecting, provider]); + + return {children}; +} + +export const useEvmWallet = () => { + const context = useContext(EvmWalletContext); + if (!context) { + throw new Error('useEvmWallet must be used within EvmWalletProvider.'); + } + return context; +}; diff --git a/apps/web/src/app/providers/wallet-provider.tsx b/apps/web/src/app/providers/wallet-provider.tsx index 8890132..3489919 100644 --- a/apps/web/src/app/providers/wallet-provider.tsx +++ b/apps/web/src/app/providers/wallet-provider.tsx @@ -23,6 +23,7 @@ import { } from '@/contants/network'; import { getChains } from '@/utils/helpers'; import { RegistryProvider } from "./RegistryContext"; +import { EvmWalletProvider } from './evm-wallet-provider'; import store, { persistor } from '@/store'; export function WebWalletProviders({ children }: { children: React.ReactNode }) { @@ -68,17 +69,19 @@ export function WebWalletProviders({ children }: { children: React.ReactNode }) - {isBrowser && chainData ? ( - - {children} - - - ) : ( - // During SSR or while resolving on client, render app shell without ChainProvider to avoid build-time throws - <> - {children} - - )} + + {isBrowser && chainData ? ( + + {children} + + + ) : ( + // During SSR or while resolving on client, render app shell without ChainProvider to avoid build-time throws + <> + {children} + + )} + diff --git a/apps/web/src/app/wallet/page.tsx b/apps/web/src/app/wallet/page.tsx index bada3e0..052c14a 100644 --- a/apps/web/src/app/wallet/page.tsx +++ b/apps/web/src/app/wallet/page.tsx @@ -9,24 +9,26 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import useTransaction from '@/hooks/useTransaction'; import useDelegate from '@/hooks/useDelegate'; import useSend from '@/hooks/useSend'; +import { IS_EVM_NETWORK } from '@/contants/network'; export default function Page() { const { address } = useWalletConnect(); + const account = useAccountInfo(); const { accountInfo, selectedModal, handleOpenModal, handleCloseModal, - } = useAccountInfo(); + } = account; const { - isLoading, - error, + isLoading: isTransactionLoading, + error: transactionError, transactions, totalTransactions, handlePageClick, } = useTransaction(); const sendOptions = useSend({ - callback: handleCloseModal, + callback: IS_EVM_NETWORK ? account.fetchData : handleCloseModal, customMemo: '', }); const delegate = useDelegate(); @@ -42,9 +44,10 @@ export default function Page() {
{ + sendOptions.handleCloseCongratulationsModal(); + handleCloseModal(); + }, }} delegateOptions={{ isVoteLoading: delegate.isLoading, diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index f8a7410..58a8d9d 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -8,23 +8,22 @@ import { toast } from 'react-toastify'; import { useDispatch } from '@/redux/hooks'; import { formatAddress } from '@/utils/format'; -import { CHAIN_NAME } from '@/contants/network'; +import { CHAIN_NAME, IS_EVM_NETWORK } from '@/contants/network'; import { setAddress, setConnected } from '@/redux/wallet.slice'; +import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; export function WalletModalComponent() { const dispatch = useDispatch(); - const { address } = useChain(CHAIN_NAME); + const { address: cosmosAddress } = useChain(CHAIN_NAME); + const { address: evmAddress } = useEvmWallet(); + const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; useEffect(() => { - if (address) { - dispatch(setAddress({ - address, - })); - dispatch(setConnected({ - status: true, - })); - } - }, [address]) + dispatch(setAddress({ address: address || '' })); + dispatch(setConnected({ status: Boolean(address) })); + }, [address, dispatch]) + + if (IS_EVM_NETWORK) return null; return (
@@ -35,20 +34,46 @@ export function WalletModalComponent() { export function ConnectWallet() { const dispatch = useDispatch(); - const { address, disconnect, openView } = useChain(CHAIN_NAME); + const { + address: cosmosAddress, + disconnect: disconnectCosmos, + openView, + } = useChain(CHAIN_NAME); + const { + address: evmAddress, + connect: connectEvm, + disconnect: disconnectEvm, + isConnecting, + } = useEvmWallet(); + const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; + + const handleDisconnect = async () => { + if (IS_EVM_NETWORK) { + await disconnectEvm(); + } else { + disconnectCosmos(); + } + dispatch(setAddress({ address: '' })); + dispatch(setConnected({ status: false })); + } - const handleDesconnect = () => { - disconnect(); - dispatch(setAddress({ - address: '', - })); - dispatch(setConnected({ - status: false, - })); + const handleConnect = async () => { + if (!IS_EVM_NETWORK) { + openView(); + return; + } + try { + await connectEvm(); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Unable to connect EVM wallet.', { + position: 'bottom-center', + theme: 'dark', + }); + } } const handleCopyAddress = () => { - navigator.clipboard.writeText(address); + void navigator.clipboard.writeText(address); toast('The address has been copied.', { position: "bottom-center", theme: "dark", @@ -59,14 +84,15 @@ export function ConnectWallet() {
{!address ? : <> {formatAddress(address, 5, -4)} - + }
@@ -74,16 +100,34 @@ export function ConnectWallet() { } export function ConnectWalletButton() { - const { address, openView } = useChain(CHAIN_NAME); + const { address: cosmosAddress, openView } = useChain(CHAIN_NAME); + const { address: evmAddress, connect, isConnecting } = useEvmWallet(); + const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; + + const handleConnect = async () => { + if (!IS_EVM_NETWORK) { + openView(); + return; + } + try { + await connect(); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Unable to connect EVM wallet.', { + position: 'bottom-center', + theme: 'dark', + }); + } + }; return (
{!address ? : null }
diff --git a/apps/web/src/components/SendModal.tsx b/apps/web/src/components/SendModal.tsx index 7a3768b..92e1743 100644 --- a/apps/web/src/components/SendModal.tsx +++ b/apps/web/src/components/SendModal.tsx @@ -17,6 +17,7 @@ import { DENOM } from '@/contants/network'; import { RATE_VALUE } from '@/contants'; interface IVoteModal { + isEvm: boolean; isOpen: boolean; isVoteLoading: boolean; error: string | null; @@ -40,6 +41,7 @@ interface IVoteModal { } export default function SendModal({ + isEvm, isOpen, isVoteLoading, error, @@ -103,7 +105,11 @@ export default function SendModal({

Congratulations! send completed successfully.

+ {isEvm ? ( + {transactionHash} + ) : ( View Transaction + )}
@@ -166,6 +172,7 @@ export default function SendModal({ placeholder="Sender" className='input' value={optionsAdvanced.senderAddress} + disabled={isEvm} onChangeText={(newValue) => onInputChange('senderAddress', newValue)} /> @@ -207,7 +214,7 @@ export default function SendModal({ - {showAdvanced ? + {showAdvanced && !isEvm ?
@@ -251,7 +258,7 @@ export default function SendModal({
-
+ {!isEvm ?
Advanced -
+
: null}
diff --git a/apps/web/src/contants/network.ts b/apps/web/src/contants/network.ts index 9b40b86..dbb3769 100644 --- a/apps/web/src/contants/network.ts +++ b/apps/web/src/contants/network.ts @@ -1,5 +1,6 @@ export const NETWORK_PROFILES = { devnet: { + displayName: 'Lumera Devnet', chainName: 'lumera-devnet', chainId: 'lumera-devnet-1', denom: 'ulume', @@ -7,9 +8,11 @@ export const NETWORK_PROFILES = { restEndpoint: 'https://lcd.pastel.network', evmRpcEndpoint: 'https://evm-rpc.pastel.network', evmWsEndpoint: null, + evmChainId: 76857769, snapiUrl: 'http://localhost:3100', }, testnet: { + displayName: 'Lumera Testnet', chainName: 'lumera-testnet', chainId: 'lumera-testnet-2', denom: 'ulume', @@ -17,9 +20,11 @@ export const NETWORK_PROFILES = { restEndpoint: 'https://lcd-testnet.lumeraprotocol.com', evmRpcEndpoint: 'https://evm-testnet.lumeraprotocol.com', evmWsEndpoint: 'https://evm-ws-testnet.lumeraprotocol.com', + evmChainId: 76857769, snapiUrl: 'http://localhost:3100', }, mainnet: { + displayName: 'Lumera Mainnet', chainName: 'lumera', chainId: 'lumera-mainnet-1', denom: 'ulume', @@ -27,6 +32,7 @@ export const NETWORK_PROFILES = { restEndpoint: 'https://lcd.lumera.io', evmRpcEndpoint: null, evmWsEndpoint: null, + evmChainId: null, snapiUrl: 'http://localhost:3100', }, } as const; @@ -61,6 +67,16 @@ export const RPC_ENDPOINT = process.env.NEXT_PUBLIC_RPC_ENDPOINT || ACTIVE_NETWO export const REST_AI_URL = process.env.NEXT_PUBLIC_REST_AI_URL || ACTIVE_NETWORK.restEndpoint; export const EVM_RPC_ENDPOINT = process.env.NEXT_PUBLIC_EVM_RPC_ENDPOINT || ACTIVE_NETWORK.evmRpcEndpoint; export const EVM_WS_ENDPOINT = process.env.NEXT_PUBLIC_EVM_WS_ENDPOINT || ACTIVE_NETWORK.evmWsEndpoint; +export const EVM_CHAIN_ID = process.env.NEXT_PUBLIC_EVM_CHAIN_ID + ? Number(process.env.NEXT_PUBLIC_EVM_CHAIN_ID) + : ACTIVE_NETWORK.evmChainId; +export const EVM_NATIVE_DECIMALS = 18; + +if (EVM_CHAIN_ID !== null && (!Number.isSafeInteger(EVM_CHAIN_ID) || EVM_CHAIN_ID <= 0)) { + throw new Error('NEXT_PUBLIC_EVM_CHAIN_ID must be a positive integer.'); +} + +export const IS_EVM_NETWORK = EVM_RPC_ENDPOINT !== null && EVM_CHAIN_ID !== null; export const WALLET_CONNECT_PROJECTID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECTID || 'fd049c1154d0886fda615b1c2e08ee28'; export const WALLET_CONNECT_RELAY_URL = process.env.NEXT_PUBLIC_WALLET_CONNECT_RELAY_URL || 'wss://relay.walletconnect.org'; export const WALLET_CONNECT_NAME = process.env.NEXT_PUBLIC_WALLET_CONNECT_NAME || 'Lumera Hub'; diff --git a/apps/web/src/hooks/useAccountInfo.ts b/apps/web/src/hooks/useAccountInfo.ts index a35c4a6..6d9ca12 100644 --- a/apps/web/src/hooks/useAccountInfo.ts +++ b/apps/web/src/hooks/useAccountInfo.ts @@ -2,8 +2,9 @@ import { useEffect, useState } from 'react'; import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM } from '@/contants/network'; +import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; +import { evmBalanceToMicroLume, getEvmBalance } from '@/utils/evm'; export interface Coin { denom: string; @@ -90,6 +91,22 @@ const useAccountInfo = () => { setError(null); try { + if (IS_EVM_NETWORK) { + const balance = await getEvmBalance(address); + const _accountInfo: AccountInfoData = { + balances: [{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }], + delegations: [], + rewards: [], + unbonding: [], + }; + setAccountInfo(_accountInfo); + setClaimInfo((current) => ({ + ...current, + totalRewards: '0', + })); + return; + } + const [balanceRes, delegationsRes, rewardsRes, resUnbonding] = await Promise.all([ instance.get(`/cosmos/bank/v1beta1/balances/${address}`), instance.get(`/cosmos/staking/v1beta1/delegations/${address}`), @@ -140,6 +157,10 @@ const useAccountInfo = () => { const handleClaimButtonClick = async () => { setErrorClaim(null); + if (IS_EVM_NETWORK) { + setErrorClaim('Staking rewards require a legacy Cosmos wallet connection.'); + return; + } if (!claimInfo.senderAddress) { return; } diff --git a/apps/web/src/hooks/useDelegate.ts b/apps/web/src/hooks/useDelegate.ts index 9c09e3e..5fa8415 100644 --- a/apps/web/src/hooks/useDelegate.ts +++ b/apps/web/src/hooks/useDelegate.ts @@ -5,7 +5,7 @@ import { import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM } from '@/contants/network'; +import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; import { extractValidNumber } from '@/utils/helpers'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contants'; import { @@ -39,6 +39,11 @@ const useDelegate = (options: UseDepositOptions = {}) => { const [selectedModal, setSelectedModal] = useState(''); const fetchValidator = async () => { + if (IS_EVM_NETWORK) { + setValidators([]); + setTotalValidators('0'); + return; + } setFetchValidatorLoading(true); try { const { data } = await instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=1000&status=BOND_STATUS_BONDED&pagination.count_total=true'); @@ -101,6 +106,10 @@ const useDelegate = (options: UseDepositOptions = {}) => { const handleSendClick = async () => { setError(''); setTransactionHash(''); + if (IS_EVM_NETWORK) { + setError('Staking requires a legacy Cosmos wallet connection.'); + return; + } if (!optionsAdvanced?.amount || Number(optionsAdvanced.amount) <= 0) { setError('Please enter amount.'); return diff --git a/apps/web/src/hooks/useSend.ts b/apps/web/src/hooks/useSend.ts index ec31ebb..96f4a7f 100644 --- a/apps/web/src/hooks/useSend.ts +++ b/apps/web/src/hooks/useSend.ts @@ -3,10 +3,16 @@ import { MsgSend, } from 'cosmjs-types/cosmos/bank/v1beta1/tx'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM } from '@/contants/network'; +import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contants'; import { Coin } from '@/hooks/useAccountInfo'; import { extractValidNumber } from '@/utils/helpers'; +import { + evmBalanceToMicroLume, + getEvmBalance, + isEvmAddress, + parseEvmAmount, +} from '@/utils/evm'; interface UseDepositOptions { callback?: () => void; @@ -14,7 +20,13 @@ interface UseDepositOptions { } const useSend = (options: UseDepositOptions = {}) => { - const { address, getClient, isConnected } = useWalletConnect(); + const { + address, + getClient, + isConnected, + evmProvider, + ensureEvmNetwork, + } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [optionsAdvanced, setOptionsAdvanced] = useState({ senderAddress: address, @@ -37,6 +49,13 @@ const useSend = (options: UseDepositOptions = {}) => { } }, [isConnected]); + useEffect(() => { + setOptionsAdvanced((current) => ({ + ...current, + senderAddress: address, + })); + }, [address]); + useEffect(() => { if (options?.customMemo) { setOptionsAdvanced({ @@ -85,16 +104,39 @@ const useSend = (options: UseDepositOptions = {}) => { setError('Please enter sender.'); return } - if (!optionsAdvanced.fees) { + if (!IS_EVM_NETWORK && !optionsAdvanced.fees) { setError('Please enter fee.'); return } - if (!optionsAdvanced.gas) { + if (!IS_EVM_NETWORK && !optionsAdvanced.gas) { setError('Please enter gas.'); return } setLoading(true); try { + if (IS_EVM_NETWORK) { + if (!isEvmAddress(optionsAdvanced.recipient)) { + throw new Error('Enter a valid EVM recipient address.'); + } + if (!evmProvider) { + throw new Error('No EVM wallet was detected.'); + } + + await ensureEvmNetwork(); + const transactionHash = await evmProvider.request({ + method: 'eth_sendTransaction', + params: [{ + from: address, + to: optionsAdvanced.recipient, + value: parseEvmAmount(optionsAdvanced.amount), + }], + }); + setTransactionHash(transactionHash); + resetData(); + options.callback?.(); + return; + } + const client = await getClient(); const msg = { typeUrl: '/cosmos.bank.v1beta1.MsgSend', @@ -136,6 +178,13 @@ const useSend = (options: UseDepositOptions = {}) => { const queryBalances = async (): Promise => { try { + if (IS_EVM_NETWORK) { + const balance = await getEvmBalance(address); + setBalances([{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }]); + setSelectedDenom(DENOM); + return; + } + const client = await getClient(); if (!client) { return diff --git a/apps/web/src/hooks/useTransaction.ts b/apps/web/src/hooks/useTransaction.ts index b3b93bd..39f936a 100644 --- a/apps/web/src/hooks/useTransaction.ts +++ b/apps/web/src/hooks/useTransaction.ts @@ -4,6 +4,7 @@ import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; import { TLog, TLogEvent, TMessage, TOption, TSignerInfos, TFee } from '@/hooks/useRecentActivity'; import { Coin } from '@/hooks/useAccountInfo'; +import { IS_EVM_NETWORK } from '@/contants/network'; const LIMIT = 20; @@ -49,6 +50,13 @@ const useTransaction = () => { const [totalTransactions, setTotalTransactions] = useState(0); const fetchTransactions = async (offset = 0) => { + if (IS_EVM_NETWORK) { + setTransactions([]); + setTotalTransactions(0); + setError(''); + setLoading(false); + return; + } setLoading(true); setError(''); diff --git a/apps/web/src/hooks/useWalletConnect.ts b/apps/web/src/hooks/useWalletConnect.ts index 4748461..d5ff9eb 100644 --- a/apps/web/src/hooks/useWalletConnect.ts +++ b/apps/web/src/hooks/useWalletConnect.ts @@ -5,13 +5,21 @@ import { useSelector } from '@/redux/hooks'; import { RPC_ENDPOINT, CHAIN_NAME, + IS_EVM_NETWORK, } from '@/contants/network'; +import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; const useWalletConnect = () => { - const { chain, wallet } = useChain(CHAIN_NAME); - const { isConnected, address, walletName, isModalOpen } = useSelector((state) => state.wallet); + const { chain, wallet, address: cosmosAddress } = useChain(CHAIN_NAME); + const evmWallet = useEvmWallet(); + const { walletName, isModalOpen } = useSelector((state) => state.wallet); + const address = IS_EVM_NETWORK ? evmWallet.address : cosmosAddress || ''; + const isConnected = Boolean(address); const getClient = async () => { + if (IS_EVM_NETWORK) { + throw new Error('Cosmos signing is unavailable while using an EVM network profile.'); + } if (!wallet || !chain) { throw new Error('Please connect wallet before using'); } @@ -27,6 +35,9 @@ const useWalletConnect = () => { } const getOfflineSigner = async () => { + if (IS_EVM_NETWORK) { + throw new Error('Cosmos signing is unavailable while using an EVM network profile.'); + } if (!wallet || !chain) { throw new Error('Please connect wallet before using'); } @@ -44,6 +55,9 @@ const useWalletConnect = () => { isConnected, address, walletName, + isEvm: IS_EVM_NETWORK, + evmProvider: evmWallet.provider, + ensureEvmNetwork: evmWallet.ensureNetwork, getClient, getOfflineSigner, } diff --git a/apps/web/src/types/window.d.ts b/apps/web/src/types/window.d.ts index 549e9c7..3454e30 100644 --- a/apps/web/src/types/window.d.ts +++ b/apps/web/src/types/window.d.ts @@ -12,11 +12,18 @@ interface Leap { getOfflineSigner(chainId: string): OfflineSigner; } +export interface Eip1193Provider { + request(args: { method: string; params?: unknown[] | object }): Promise; + on?(event: string, listener: (...args: unknown[]) => void): void; + removeListener?(event: string, listener: (...args: unknown[]) => void): void; +} + declare global { interface Window { keplr?: Keplr; leap?: Leap; + ethereum?: Eip1193Provider; } } -export {}; \ No newline at end of file +export {}; diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts new file mode 100644 index 0000000..63c80f8 --- /dev/null +++ b/apps/web/src/utils/evm.ts @@ -0,0 +1,77 @@ +import { + EVM_NATIVE_DECIMALS, + EVM_RPC_ENDPOINT, +} from '@/contants/network'; + +interface EvmRpcResponse { + result?: T; + error?: { + code: number; + message: string; + }; +} + +export const isEvmAddress = (value: string) => /^0x[0-9a-fA-F]{40}$/.test(value); + +export const toHexChainId = (chainId: number) => `0x${chainId.toString(16)}`; + +export const parseEvmAmount = (value: string, decimals = EVM_NATIVE_DECIMALS) => { + const normalized = value.trim(); + if (!/^\d+(\.\d+)?$/.test(normalized)) { + throw new Error('Enter a valid amount.'); + } + + const [whole, fraction = ''] = normalized.split('.'); + if (fraction.length > decimals) { + throw new Error(`Amount supports at most ${decimals} decimal places.`); + } + + const units = BigInt(whole) * (BigInt(10) ** BigInt(decimals)) + + BigInt(fraction.padEnd(decimals, '0') || '0'); + + if (units <= BigInt(0)) { + throw new Error('Amount must be greater than zero.'); + } + + return `0x${units.toString(16)}`; +}; + +export const evmBalanceToMicroLume = (balance: string) => { + const wei = BigInt(balance); + const microLumeDivisor = BigInt(10) ** BigInt(EVM_NATIVE_DECIMALS - 6); + return (wei / microLumeDivisor).toString(); +}; + +export const requestEvmRpc = async (method: string, params: unknown[] = []): Promise => { + if (!EVM_RPC_ENDPOINT) { + throw new Error('The active network does not define an EVM RPC endpoint.'); + } + + const response = await fetch(EVM_RPC_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: Date.now(), + method, + params, + }), + }); + + if (!response.ok) { + throw new Error(`EVM RPC request failed with status ${response.status}.`); + } + + const payload = await response.json() as EvmRpcResponse; + if (payload.error) { + throw new Error(payload.error.message); + } + if (payload.result === undefined) { + throw new Error(`EVM RPC method ${method} returned no result.`); + } + + return payload.result; +}; + +export const getEvmBalance = (address: string) => + requestEvmRpc('eth_getBalance', [address, 'latest']); diff --git a/apps/web/src/utils/helpers.ts b/apps/web/src/utils/helpers.ts index f48637d..daa5988 100644 --- a/apps/web/src/utils/helpers.ts +++ b/apps/web/src/utils/helpers.ts @@ -179,18 +179,23 @@ export const getChains = () => { } const registry = NETWORK_PROFILE === 'testnet' ? chainTestnet : chainMainnet; - const chain = registry.chains.find(({ chainName }) => chainName === CHAIN_NAME); - const assets = registry.assetLists.find(({ chainName }) => chainName === CHAIN_NAME); + const chain = registry.chains.find(({ chainName, chainId }) => + chainName === CHAIN_NAME || chainId === CHAIN_ID + ); + const assets = chain + ? registry.assetLists.find(({ chainName }) => chainName === chain.chainName) + : undefined; if (!chain || !assets) { return { assetLists: [], chains: [] }; } return { - assetLists: [assets], + assetLists: [{ ...assets, chainName: CHAIN_NAME }], chains: [ { ...chain, + chainName: CHAIN_NAME, chainId: CHAIN_ID, apis: { ...chain.apis, diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index 937e93f..ca728a7 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -42,6 +42,7 @@ import 'react-paginate/theme/basic/react-paginate.css'; interface IWalletScreen { walletAddress: string; + isEvm: boolean; accountInfo: AccountInfoData | null; isLoading: boolean; error: string; @@ -95,6 +96,7 @@ interface IWalletScreen { export const WalletScreen = ({ walletAddress, + isEvm, accountInfo, isLoading, error, @@ -285,6 +287,7 @@ export const WalletScreen = ({ walletAddress={walletAddress} /> - + {!isEvm ? ( + + ) : null}

Total Wallet Balance

@@ -328,6 +333,9 @@ export const WalletScreen = ({ }

+ {error && !isLoading ? ( +

{error}

+ ) : null}
  • Available: @@ -336,27 +344,27 @@ export const WalletScreen = ({ denom: DENOM, }, false, '0,0.[00000]')} LUME
  • -
  • + {!isEvm ?
  • Staking: {formatTokenDisplay({ amount: `${getDelegations()}`, denom: DENOM, }, false, '0,0.[00000]')} LUME -
  • -
  • +
  • : null} + {!isEvm ?
  • Rewards: {formatTokenDisplay({ amount: `${getRewards()}`, denom: DENOM, }, false, '0,0.[00000]')} LUME -
  • -
  • +
  • : null} + {!isEvm ?
  • Unstaking: {formatTokenDisplay({ amount: `${getUnbonding()}`, denom: DENOM, }, false, '0,0.[00000]')} LUME -
  • + : null}
@@ -376,14 +384,14 @@ export const WalletScreen = ({ > Receive - onOpenModal('stake')} disabled={isLoading} > Stake - + : null}
@@ -401,7 +409,7 @@ export const WalletScreen = ({
- + {!isEvm ?

Transaction History

@@ -480,7 +488,7 @@ export const WalletScreen = ({
: null }
- + : null}
); }; From 035ad75f45bdfa7c426b6768c9f3aa6b2affd6d4 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Fri, 31 Jul 2026 11:20:12 -0400 Subject: [PATCH 04/45] add design doc for MetaMask Cosmos signing on EVM profiles Co-Authored-By: Claude Fable 5 --- .../2026-07-31-evm-metamask-cosmos-signing.md | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 docs/design/2026-07-31-evm-metamask-cosmos-signing.md diff --git a/docs/design/2026-07-31-evm-metamask-cosmos-signing.md b/docs/design/2026-07-31-evm-metamask-cosmos-signing.md new file mode 100644 index 0000000..70bd76b --- /dev/null +++ b/docs/design/2026-07-31-evm-metamask-cosmos-signing.md @@ -0,0 +1,175 @@ +# EVM profiles: sign Cosmos transactions with MetaMask (EIP-712) + +- **Date:** 2026-07-31 +- **Status:** Approved design, pending implementation +- **Branch:** `evm-support` +- **First delivery target:** governance (vote, deposit, create proposal) on EVM network profiles + +## Problem + +On EVM-enabled network profiles (currently devnet and testnet), the hub connects an injected +EIP-1193 wallet (MetaMask) instead of a Cosmos wallet. Native LUME balance and transfers work +via EVM JSON-RPC, but every flow that signs a Cosmos SDK message is a dead-end: +`useWalletConnect.getClient()` / `getOfflineSigner()` throw +"Cosmos signing is unavailable while using an EVM network profile." + +Affected flows: governance voting, deposits, and proposal creation; staking +(delegate/undelegate/redelegate/claims); Cascade uploads (custom Lumera messages). +The user only discovers the failure after clicking the action button. + +## Decision + +Adopt the approach proven in `lumera-portal` / `portal-widgets`: **make MetaMask act as a +Cosmos transaction signer via EIP-712 typed-data signing**, and route the existing hooks' +Cosmos messages through it (the only hook change is using the bech32 address in signer fields). Do **not** use the EVM precompiles for module actions, +and do **not** gate governance UI. + +### Why this works (chain evidence) + +Lumera (`lumera` repo, app version 1.20.1) pins `github.com/cosmos/evm v0.6.0` and uses its +standard dual-routing ante handler. In cosmos/evm v0.6.0, signature verification for +`eth_secp256k1` accounts is: + +```go +// crypto/ethsecp256k1/ethsecp256k1.go:213 +func (pubKey PubKey) VerifySignature(msg, sig []byte) bool { + return pubKey.verifySignatureECDSA(msg, sig) || pubKey.verifySignatureAsEIP712(msg, sig) +} +``` + +The fallback reconstructs the Cosmos sign-doc as EIP-712 typed data (current and legacy +encodings) and verifies the ECDSA signature against its hash. This runs inside the standard +SDK `SigVerificationDecorator` — no extension options and no special ante path required. +A Cosmos tx assembled with a signature obtained from MetaMask's `eth_signTypedData_v4` +therefore verifies on-chain. `lumera-portal` uses exactly this mechanism in production +(`portal-widgets/lib/wallet/wallets/MetamaskWallet.ts`). + +### Alternatives considered + +1. **Gov precompile (0x…0805).** Active on devnet/testnet and covers vote/deposit/submitProposal, + but: per-module coverage only (Lumera's custom modules — Cascade, claims — can never work + this way), `submitProposal` requires reworking the create-proposal flow from legacy v1beta1 + content types to gov v1 JSON, and it adds an ABI-encoding dependency. Each future flow needs + its own precompile integration. Rejected: EIP-712 signing covers all of these at once. +2. **Gate governance UI on EVM profiles.** Smallest change, but leaves governance read-only on + the default (testnet) profile. Rejected by product direction: Lumera EVM supports both + Ethereum and Cosmos transactions, so the hub should too. + +## Architecture + +### New: MetaMask Cosmos signer + signing-client adapter + +`apps/web/src/utils/metamask-cosmos-signer.ts` (adapted from portal-widgets `MetamaskWallet`): + +- **Connect-time key discovery.** After `eth_requestAccounts`, request one + `personal_sign("Verify Public Key")` and recover the compressed secp256k1 public key from + the signature. Cache it (localStorage, keyed by 0x address) so the prompt happens once per + account. The bech32 address is derived from the same 20 account bytes as the 0x address + (`toBech32('lumera', fromHex(ethAddress))` — no pubkey needed for the address itself). +- **`MetamaskSigningClient`** — a small class exposing exactly the call surface the hooks + already use on the Cosmos path: + - `simulate(signerAddress, messages, memo)` → LCD `POST /cosmos/tx/v1beta1/simulate` + (unsigned tx bytes with pubkey + sequence) → returns `gas_used`. + - `signAndBroadcast(signerAddress, messages, fee, memo)` → full flow below. + - `getBlock()` → LCD latest block (used by `useGovernances.getBlock`). + + Sign-and-broadcast flow: + 1. Fetch `account_number` / `sequence` from LCD `/cosmos/auth/v1beta1/accounts/{bech32}`. + 2. Convert messages to amino JSON via cosmjs `AminoTypes` (gov v1beta1 vote/deposit/ + submitProposal are covered by `createDefaultAminoConverters`). + 3. Build the EIP-712 typed-data payload with the message-type definitions + (`@tharsis/eip712` `createEIP712` / `generateTypes` / `generateFee` / + `generateMessageWithMultipleTransactions`, as in the portal). + **Domain `chainId` is `EVM_CHAIN_ID` (76857769) taken from the network profile** — not + parsed from the Cosmos chain-id string. (The portal's `extractChainId` expects + evmos-style `name_1234-5` ids and yields 0 for `lumera-*` ids; we deviate deliberately.) + 4. `eth_signTypedData_v4` with the connected 0x address. + 5. Assemble `TxRaw`: proto-encoded `TxBody`; `authInfo` via `makeAuthInfoBytes` with the + pubkey wrapped as **`/cosmos.evm.crypto.v1.ethsecp256k1.PubKey`** (the type cosmos/evm + registers; not the ethermint or plain-cosmos type URL); the recovered signature bytes. + Declared sign mode mirrors the portal (`SIGN_MODE_DIRECT` default). If devnet + verification rejects it, switch the declared mode to `SIGN_MODE_LEGACY_AMINO_JSON` — + a one-line change; the chain's EIP-712 fallback parses both encodings. + 6. Broadcast via LCD `POST /cosmos/tx/v1beta1/txs` (sync mode), then poll + `/cosmos/tx/v1beta1/txs/{hash}` until inclusion and return + `{ transactionHash, code, rawLog }` — matching what the hooks read from cosmjs' + `DeliverTxResponse`. + +### Message-type definitions (EIP-712 types) + +The typed-data `types` come per message type, portal-style +(`EthermintMessageAdapter`). The portal already defines vote, send, delegate, undelegate, +redelegate, and reward/commission claims. We add the two governance gaps: + +- `/cosmos.gov.v1beta1.MsgDeposit` — trivial (proposal_id, depositor, amount coins). +- `/cosmos.gov.v1beta1.MsgSubmitProposal` — the `content` field is an amino-encoded object + per proposal type (Text / ParameterChange / SoftwareUpgrade). Each gets its own type + definition. This is the riskiest encoding in scope; it is validated live on devnet. If the + chain's encoder rejects a specific proposal type, that proposal type alone reports a clear + error and is fixed in a follow-up — vote and deposit do not depend on it. + +### Changed: wallet plumbing + +- `apps/web/src/app/providers/evm-wallet-provider.tsx` — on connect, run pubkey recovery and + expose `{ ethAddress, cosmosAddress, pubkey }` in context. Skip the `personal_sign` prompt + when the pubkey is already cached for that address. +- `apps/web/src/hooks/useWalletConnect.ts` — on EVM profiles `getClient()` returns a + `MetamaskSigningClient` instead of throwing. Additionally expose `cosmosAddress` (bech32; + on Cosmos profiles it equals `address`). `getOfflineSigner()` keeps throwing for now — + its only consumer is Cascade, which is a follow-up. +- Governance hooks (`useProposals`, `useDeposit`, `useGovernances`) — use `cosmosAddress` + for message signer fields (voter / depositor / proposer). No other hook changes: they keep + calling `client.simulate` / `client.signAndBroadcast` exactly as today. + +### Unchanged + +- Native LUME transfer stays on `eth_sendTransaction` (already shipped on this branch). +- Cosmos-profile behavior (interchain-kit wallets) is untouched. +- Governance UI: no gating; existing modal error states surface failures. + +## Scope + +**This iteration:** the signer/client adapter, governance message types, `cosmosAddress` +plumbing, and removal of the governance dead-ends. Verified live on devnet. + +**Explicit follow-ups (same foundation, small diffs each):** +- Staking page (delegate/undelegate/redelegate/claims already have adapters in the portal to copy). +- Wallet page: re-enable staking sections, rewards claim, and Cosmos REST transaction history + on EVM profiles using `cosmosAddress` (removes most `IS_EVM_NETWORK` branches added earlier). +- Cascade uploads (`getOfflineSigner` consumers) — Lumera custom-message amino converters and + proto types exist in portal-widgets (`lib/amino/lumera-amino.ts`, `lib/protobuf/lumera`). +- Send-modal receipt polling and EVM explorer links (pre-existing polish items). + +## Error handling + +- MetaMask rejection (code 4001) → the existing modal error states show a friendly + "Request rejected in wallet." message instead of the raw provider string. +- Broadcast returns `code !== 0` → surface `rawLog` in the modal error state. +- LCD/simulate failures → surfaced through the same try/catch paths the hooks already have. +- Account not found on LCD (never-funded account) → clear error telling the user to fund the + address first. + +## Dependencies + +Added to `apps/web`: `@tharsis/eip712`, `@tharsis/transactions` (typed-data construction — +same packages the portal ships), `@ethersproject/hash`, `@ethersproject/signing-key` +(pubkey recovery). All are already vetted in production via lumera-portal. + +**Risk noted:** the chain-side EIP-712 verification fallback is marked deprecated upstream in +cosmos/evm. It is active in v0.6.0, lumera-portal depends on it in production, and Lumera +controls its chain upgrade cadence. If a future chain upgrade removes the fallback, both the +portal and the hub must migrate together (precompiles or whatever replacement cosmos/evm +ships); this design keeps that migration localized to `metamask-cosmos-signer.ts`. + +## Verification + +1. `pnpm build:web` and typecheck pass. +2. Live on devnet (`https://lcd.pastel.network`, `https://evm-rpc.pastel.network`, EVM chain + id 76857769) with a funded test account in MetaMask: + - Connect → one `personal_sign` prompt → bech32 address derived and shown where relevant. + - Vote on an active proposal → `eth_signTypedData_v4` prompt → tx included → + LCD `/cosmos/gov/v1/proposals/{id}/votes/{voter}` shows the vote. + - Deposit on a deposit-period proposal → LCD deposits query reflects it. + - Create a Text proposal → proposal appears; if the legacy-content encoding fails, the + error is surfaced and logged as the known follow-up (vote/deposit unaffected). +3. Regression: Cosmos profile (mainnet config) governance still works with Keplr/Leap. From 9b0239b786c74a78224cb1dda0f0f3ada19637d2 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 12:03:29 -0400 Subject: [PATCH 05/45] fix EVM wallet safety and governance gating --- apps/web/.env.example | 2 + apps/web/package.json | 4 +- apps/web/src/app/governance/[id]/page.tsx | 6 +- apps/web/src/app/governance/page.tsx | 6 +- .../src/app/providers/evm-wallet-provider.tsx | 39 +- apps/web/src/contants/network.test.ts | 69 ++ apps/web/src/contants/network.ts | 6 + apps/web/src/hooks/useDeposit.ts | 9 +- apps/web/src/hooks/useGovernances.ts | 13 +- apps/web/src/hooks/useProposals.ts | 6 +- apps/web/src/hooks/useSend.ts | 21 +- apps/web/src/hooks/useWalletConnect.ts | 13 + .../web/src/utils/cosmos-transactions.test.ts | 42 + apps/web/src/utils/cosmos-transactions.ts | 18 + apps/web/src/utils/env.test.ts | 20 + apps/web/src/utils/env.ts | 9 + apps/web/src/utils/evm.test.ts | 145 +++ apps/web/src/utils/evm.ts | 52 +- apps/web/vitest.config.ts | 14 + .../2026-07-31-evm-metamask-cosmos-signing.md | 394 +++++--- .../src/screens/GovernanceDetailsScreen.tsx | 15 +- packages/ui/src/screens/GovernanceScreen.tsx | 25 +- pnpm-lock.yaml | 929 +++++++++++++++++- turbo.json | 23 + 24 files changed, 1709 insertions(+), 171 deletions(-) create mode 100644 apps/web/src/contants/network.test.ts create mode 100644 apps/web/src/utils/cosmos-transactions.test.ts create mode 100644 apps/web/src/utils/cosmos-transactions.ts create mode 100644 apps/web/src/utils/env.test.ts create mode 100644 apps/web/src/utils/env.ts create mode 100644 apps/web/src/utils/evm.test.ts create mode 100644 apps/web/vitest.config.ts diff --git a/apps/web/.env.example b/apps/web/.env.example index b783e4f..c96367b 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -10,6 +10,8 @@ NEXT_PUBLIC_NETWORK_PROFILE=testnet # NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-testnet.lumeraprotocol.com # NEXT_PUBLIC_EVM_WS_ENDPOINT=https://evm-ws-testnet.lumeraprotocol.com # NEXT_PUBLIC_EVM_CHAIN_ID=76857769 +# Enable only after the chain and hub MetaMask Cosmos signer are both ready. +# NEXT_PUBLIC_COSMOS_EIP712_ENABLED=false # NEXT_PUBLIC_SNAPI_URL=http://localhost:3100 NEXT_PUBLIC_WALLET_CONNECT_PROJECTID=fd049c1154d0886fda615b1c2e08ee28 diff --git a/apps/web/package.json b/apps/web/package.json index 2e5f5e9..8e5e96d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "test": "vitest run", "start": "next start", "lint": "next lint" }, @@ -60,7 +61,8 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.4.6", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.2.4" }, "browserslist": { "production": [ diff --git a/apps/web/src/app/governance/[id]/page.tsx b/apps/web/src/app/governance/[id]/page.tsx index dd07b04..02415ab 100644 --- a/apps/web/src/app/governance/[id]/page.tsx +++ b/apps/web/src/app/governance/[id]/page.tsx @@ -8,6 +8,7 @@ import useDeposit from '@/hooks/useDeposit'; import useProposals from '@/hooks/useProposals'; import useWalletConnect from '@/hooks/useWalletConnect'; import { GovernanceDetailsScreen } from '@lumera-hub/ui/src/screens/GovernanceDetailsScreen'; +import { GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE } from '@/utils/cosmos-transactions'; interface Props { params: Promise<{ id: string }>; @@ -34,7 +35,7 @@ export default function Page({ params }: Props) { const proposals = useProposals({ customMemo: governance?.title ? `Vote for the ${governance?.title}` : '', }); - const { address } = useWalletConnect(); + const { address, canSignCosmosTransactions } = useWalletConnect(); useEffect(() => { document.title = `${governance?.title || 'Governance Details'} - Lumera Hub`; @@ -48,6 +49,9 @@ export default function Page({ params }: Props) {
( + typeof window === 'undefined' ? null : window.ethereum || null + ); + + useEffect(() => { + const detectProvider = () => setProvider(window.ethereum || null); + detectProvider(); + window.addEventListener('ethereum#initialized', detectProvider, { once: true }); + return () => window.removeEventListener('ethereum#initialized', detectProvider); + }, []); const ensureNetwork = useCallback(async () => { if (!IS_EVM_NETWORK || !EVM_CHAIN_ID || !EVM_RPC_ENDPOINT) { @@ -68,6 +77,15 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { rpcUrls: [EVM_RPC_ENDPOINT], }], }); + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); + } + + const activeChainId = await provider.request({ method: 'eth_chainId' }); + if (activeChainId.toLowerCase() !== chainId.toLowerCase()) { + throw new Error(`Wallet did not switch to ${ACTIVE_NETWORK.displayName}.`); } }, [provider]); @@ -78,9 +96,12 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { if (!provider) { throw new Error('No EVM wallet was detected. Install MetaMask or another compatible wallet.'); } - const accounts = await provider.request({ method: 'eth_requestAccounts' }); + await provider.request({ method: 'eth_requestAccounts' }); await ensureNetwork(); - setAddress(accounts[0] || ''); + if (!EVM_CHAIN_ID) { + throw new Error('The active network does not define an EVM chain ID.'); + } + setAddress(await getEvmAccountForChain(provider, EVM_CHAIN_ID)); } catch (connectError) { const message = connectError instanceof Error ? connectError.message : 'Unable to connect EVM wallet.'; setError(message); @@ -109,15 +130,7 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { const syncAccounts = async () => { try { - const [accounts, chainId] = await Promise.all([ - provider.request({ method: 'eth_accounts' }), - provider.request({ method: 'eth_chainId' }), - ]); - setAddress( - chainId.toLowerCase() === toHexChainId(expectedChainId).toLowerCase() - ? accounts[0] || '' - : '' - ); + setAddress(await getEvmAccountForChain(provider, expectedChainId)); } catch { setAddress(''); } diff --git a/apps/web/src/contants/network.test.ts b/apps/web/src/contants/network.test.ts new file mode 100644 index 0000000..0dbe4ce --- /dev/null +++ b/apps/web/src/contants/network.test.ts @@ -0,0 +1,69 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const ENVIRONMENT_KEYS = [ + 'NEXT_PUBLIC_NETWORK_PROFILE', + 'NEXT_PUBLIC_NODE_ENV', + 'NEXT_PUBLIC_CHAIN_NAME', + 'NEXT_PUBLIC_CHAIN_ID', + 'NEXT_PUBLIC_DENOM', + 'NEXT_PUBLIC_RPC_ENDPOINT', + 'NEXT_PUBLIC_REST_AI_URL', + 'NEXT_PUBLIC_EVM_RPC_ENDPOINT', + 'NEXT_PUBLIC_EVM_WS_ENDPOINT', + 'NEXT_PUBLIC_EVM_CHAIN_ID', + 'NEXT_PUBLIC_COSMOS_EIP712_ENABLED', +] as const; + +const originalEnvironment = Object.fromEntries( + ENVIRONMENT_KEYS.map((key) => [key, process.env[key]]) +); + +beforeEach(() => { + for (const key of ENVIRONMENT_KEYS) delete process.env[key]; + vi.resetModules(); +}); + +afterEach(() => { + for (const key of ENVIRONMENT_KEYS) { + const value = originalEnvironment[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + vi.resetModules(); +}); + +describe('network profiles', () => { + it('defaults to the non-EVM mainnet profile', async () => { + const network = await import('./network'); + expect(network.NETWORK_PROFILE).toBe('mainnet'); + expect(network.IS_EVM_NETWORK).toBe(false); + }); + + it('selects the complete testnet EVM profile', async () => { + process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'testnet'; + const network = await import('./network'); + expect(network.CHAIN_ID).toBe('lumera-testnet-2'); + expect(network.EVM_CHAIN_ID).toBe(76857769); + expect(network.IS_EVM_NETWORK).toBe(true); + }); + + it('keeps Cosmos EIP-712 disabled unless explicitly enabled', async () => { + let network = await import('./network'); + expect(network.COSMOS_EIP712_ENABLED).toBe(false); + + vi.resetModules(); + process.env.NEXT_PUBLIC_COSMOS_EIP712_ENABLED = 'true'; + network = await import('./network'); + expect(network.COSMOS_EIP712_ENABLED).toBe(true); + }); + + it('rejects unknown profiles and malformed chain IDs', async () => { + process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'staging'; + await expect(import('./network')).rejects.toThrow('Unknown network profile'); + + vi.resetModules(); + process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'testnet'; + process.env.NEXT_PUBLIC_EVM_CHAIN_ID = '1.5'; + await expect(import('./network')).rejects.toThrow('positive integer'); + }); +}); diff --git a/apps/web/src/contants/network.ts b/apps/web/src/contants/network.ts index dbb3769..bedc5a1 100644 --- a/apps/web/src/contants/network.ts +++ b/apps/web/src/contants/network.ts @@ -1,3 +1,5 @@ +import { parseBooleanEnvironmentValue } from '@/utils/env'; + export const NETWORK_PROFILES = { devnet: { displayName: 'Lumera Devnet', @@ -71,6 +73,10 @@ export const EVM_CHAIN_ID = process.env.NEXT_PUBLIC_EVM_CHAIN_ID ? Number(process.env.NEXT_PUBLIC_EVM_CHAIN_ID) : ACTIVE_NETWORK.evmChainId; export const EVM_NATIVE_DECIMALS = 18; +export const COSMOS_EIP712_ENABLED = parseBooleanEnvironmentValue( + process.env.NEXT_PUBLIC_COSMOS_EIP712_ENABLED, + 'NEXT_PUBLIC_COSMOS_EIP712_ENABLED' +); if (EVM_CHAIN_ID !== null && (!Number.isSafeInteger(EVM_CHAIN_ID) || EVM_CHAIN_ID <= 0)) { throw new Error('NEXT_PUBLIC_EVM_CHAIN_ID must be a positive integer.'); diff --git a/apps/web/src/hooks/useDeposit.ts b/apps/web/src/hooks/useDeposit.ts index cb8a79c..0aa4f46 100644 --- a/apps/web/src/hooks/useDeposit.ts +++ b/apps/web/src/hooks/useDeposit.ts @@ -8,6 +8,7 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import { DENOM } from '@/contants/network'; import { RATE_VALUE, GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; import { extractValidNumber } from '@/utils/helpers'; +import { assertGovernanceTransactionsAvailable } from '@/utils/cosmos-transactions'; interface UseDepositOptions { callback?: () => void; @@ -15,7 +16,7 @@ interface UseDepositOptions { } const useDeposit = (options: UseDepositOptions = {}) => { - const { address, getClient } = useWalletConnect(); + const { address, canSignCosmosTransactions, getClient } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [depositAdvanced, setDepositAdvanced] = useState({ senderAddress: address, @@ -94,6 +95,12 @@ const useDeposit = (options: UseDepositOptions = {}) => { const handleSendClick = async () => { setError(''); setTransactionHash(''); + try { + assertGovernanceTransactionsAvailable(canSignCosmosTransactions); + } catch (guardError) { + setError(guardError instanceof Error ? guardError.message : 'An unknown error occurred.'); + return; + } if (!depositAdvanced.depositAmount) { setError('Please enter amount.'); return diff --git a/apps/web/src/hooks/useGovernances.ts b/apps/web/src/hooks/useGovernances.ts index d1fb7fc..e9e15cd 100644 --- a/apps/web/src/hooks/useGovernances.ts +++ b/apps/web/src/hooks/useGovernances.ts @@ -11,6 +11,7 @@ import { RATE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; import { IProposal } from '@/hooks/useProposals'; import useWalletConnect from '@/hooks/useWalletConnect'; import { extractValidNumber } from '@/utils/helpers'; +import { assertGovernanceTransactionsAvailable } from '@/utils/cosmos-transactions'; const LIMIT = 20; @@ -66,7 +67,7 @@ export const GOVERNANCE_STATS = { const EXPEDITED_DEPOSIT_REQUIRED = GOVERNANCE_STATS.expeditedDepositRequired; const useGovernances = () => { - const { address, getClient } = useWalletConnect(); + const { address, canSignCosmosTransactions, getClient } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [governances, setGovernances] = useState([]); const [msg, setMsg] = useState({ @@ -261,6 +262,15 @@ const useGovernances = () => { } const handleOpenCreateProposalModal = () => { + try { + assertGovernanceTransactionsAvailable(canSignCosmosTransactions); + } catch (guardError) { + setMsg({ + type: 'error', + message: guardError instanceof Error ? guardError.message : 'An unknown error occurred.', + }); + return; + } resetData(); setSelectedModal('create'); @@ -396,6 +406,7 @@ const useGovernances = () => { }); setCreateProposalLoading(true); try { + assertGovernanceTransactionsAvailable(canSignCosmosTransactions); if (!proposal.title) { setMsg({ type: 'error', diff --git a/apps/web/src/hooks/useProposals.ts b/apps/web/src/hooks/useProposals.ts index 334e44a..17f38dd 100644 --- a/apps/web/src/hooks/useProposals.ts +++ b/apps/web/src/hooks/useProposals.ts @@ -9,6 +9,7 @@ import { REST_AI_URL, DENOM } from '@/contants/network'; import { Coin } from '@/hooks/useAccountInfo' import useWalletConnect from '@/hooks/useWalletConnect'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; +import { assertGovernanceTransactionsAvailable } from '@/utils/cosmos-transactions'; type TMessage = { '@type': string; @@ -76,7 +77,7 @@ interface UseDepositOptions { } const useProposals = (options: UseDepositOptions = {}) => { - const { address, getClient } = useWalletConnect(); + const { address, canSignCosmosTransactions, getClient } = useWalletConnect(); const [proposalsInfo, setProposalsInfo] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -145,9 +146,10 @@ const useProposals = (options: UseDepositOptions = {}) => { if (!item) { return null; } - setVoteLoading(true); setErrorVote(null); try { + assertGovernanceTransactionsAvailable(canSignCosmosTransactions); + setVoteLoading(true); const client = await getClient(); const msg = { typeUrl: '/cosmos.gov.v1.MsgVote', diff --git a/apps/web/src/hooks/useSend.ts b/apps/web/src/hooks/useSend.ts index 96f4a7f..84a26ba 100644 --- a/apps/web/src/hooks/useSend.ts +++ b/apps/web/src/hooks/useSend.ts @@ -3,13 +3,14 @@ import { MsgSend, } from 'cosmjs-types/cosmos/bank/v1beta1/tx'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; +import { DENOM, EVM_CHAIN_ID, IS_EVM_NETWORK } from '@/contants/network'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contants'; import { Coin } from '@/hooks/useAccountInfo'; import { extractValidNumber } from '@/utils/helpers'; import { evmBalanceToMicroLume, getEvmBalance, + assertEvmAccountForChain, isEvmAddress, parseEvmAmount, } from '@/utils/evm'; @@ -44,10 +45,12 @@ const useSend = (options: UseDepositOptions = {}) => { const [transactionHash, setTransactionHash] = useState(''); useEffect(() => { - if (isConnected) { - queryBalances(); + if (!isConnected || !address) { + setBalances([]); + return; } - }, [isConnected]); + void queryBalances(); + }, [address, isConnected]); useEffect(() => { setOptionsAdvanced((current) => ({ @@ -121,12 +124,20 @@ const useSend = (options: UseDepositOptions = {}) => { if (!evmProvider) { throw new Error('No EVM wallet was detected.'); } + if (!EVM_CHAIN_ID) { + throw new Error('The active network does not define an EVM chain ID.'); + } await ensureEvmNetwork(); + const activeAddress = await assertEvmAccountForChain( + evmProvider, + address, + EVM_CHAIN_ID + ); const transactionHash = await evmProvider.request({ method: 'eth_sendTransaction', params: [{ - from: address, + from: activeAddress, to: optionsAdvanced.recipient, value: parseEvmAmount(optionsAdvanced.amount), }], diff --git a/apps/web/src/hooks/useWalletConnect.ts b/apps/web/src/hooks/useWalletConnect.ts index d5ff9eb..68a0bb6 100644 --- a/apps/web/src/hooks/useWalletConnect.ts +++ b/apps/web/src/hooks/useWalletConnect.ts @@ -5,9 +5,11 @@ import { useSelector } from '@/redux/hooks'; import { RPC_ENDPOINT, CHAIN_NAME, + COSMOS_EIP712_ENABLED, IS_EVM_NETWORK, } from '@/contants/network'; import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; +import { canWalletSignCosmosTransactions } from '@/utils/cosmos-transactions'; const useWalletConnect = () => { const { chain, wallet, address: cosmosAddress } = useChain(CHAIN_NAME); @@ -15,9 +17,19 @@ const useWalletConnect = () => { const { walletName, isModalOpen } = useSelector((state) => state.wallet); const address = IS_EVM_NETWORK ? evmWallet.address : cosmosAddress || ''; const isConnected = Boolean(address); + // Phase 2 will source this from the MetaMask Cosmos signer once it is implemented. + const hasEvmCosmosSigner = false; + const canSignCosmosTransactions = canWalletSignCosmosTransactions({ + isEvmNetwork: IS_EVM_NETWORK, + chainEip712Enabled: COSMOS_EIP712_ENABLED, + hasEvmCosmosSigner, + }); const getClient = async () => { if (IS_EVM_NETWORK) { + if (!canSignCosmosTransactions) { + throw new Error('Cosmos transactions are temporarily unavailable with MetaMask on this network.'); + } throw new Error('Cosmos signing is unavailable while using an EVM network profile.'); } if (!wallet || !chain) { @@ -55,6 +67,7 @@ const useWalletConnect = () => { isConnected, address, walletName, + canSignCosmosTransactions, isEvm: IS_EVM_NETWORK, evmProvider: evmWallet.provider, ensureEvmNetwork: evmWallet.ensureNetwork, diff --git a/apps/web/src/utils/cosmos-transactions.test.ts b/apps/web/src/utils/cosmos-transactions.test.ts new file mode 100644 index 0000000..0fae856 --- /dev/null +++ b/apps/web/src/utils/cosmos-transactions.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; + +import { + assertGovernanceTransactionsAvailable, + canWalletSignCosmosTransactions, + GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE, +} from './cosmos-transactions'; + +describe('assertGovernanceTransactionsAvailable', () => { + it('allows Cosmos transaction-capable wallets', () => { + expect(() => assertGovernanceTransactionsAvailable(true)).not.toThrow(); + }); + + it('fails closed with the user-facing governance reason', () => { + expect(() => assertGovernanceTransactionsAvailable(false)).toThrow( + GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE + ); + }); +}); + +describe('canWalletSignCosmosTransactions', () => { + it('keeps Cosmos wallets enabled without EIP-712', () => { + expect(canWalletSignCosmosTransactions({ + isEvmNetwork: false, + chainEip712Enabled: false, + hasEvmCosmosSigner: false, + })).toBe(true); + }); + + it('requires both chain support and an implemented EVM Cosmos signer', () => { + expect(canWalletSignCosmosTransactions({ + isEvmNetwork: true, + chainEip712Enabled: true, + hasEvmCosmosSigner: false, + })).toBe(false); + expect(canWalletSignCosmosTransactions({ + isEvmNetwork: true, + chainEip712Enabled: true, + hasEvmCosmosSigner: true, + })).toBe(true); + }); +}); diff --git a/apps/web/src/utils/cosmos-transactions.ts b/apps/web/src/utils/cosmos-transactions.ts new file mode 100644 index 0000000..6e4c398 --- /dev/null +++ b/apps/web/src/utils/cosmos-transactions.ts @@ -0,0 +1,18 @@ +export const GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE = + 'Governance transactions are temporarily unavailable with MetaMask on this network.'; + +export const canWalletSignCosmosTransactions = ({ + isEvmNetwork, + chainEip712Enabled, + hasEvmCosmosSigner, +}: { + isEvmNetwork: boolean; + chainEip712Enabled: boolean; + hasEvmCosmosSigner: boolean; +}) => !isEvmNetwork || (chainEip712Enabled && hasEvmCosmosSigner); + +export const assertGovernanceTransactionsAvailable = (canSignCosmosTransactions: boolean) => { + if (!canSignCosmosTransactions) { + throw new Error(GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE); + } +}; diff --git a/apps/web/src/utils/env.test.ts b/apps/web/src/utils/env.test.ts new file mode 100644 index 0000000..682588a --- /dev/null +++ b/apps/web/src/utils/env.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { parseBooleanEnvironmentValue } from './env'; + +describe('parseBooleanEnvironmentValue', () => { + it.each([ + [undefined, false], + ['', false], + [' false ', false], + ['TRUE', true], + ])('parses %s as %s', (value, expected) => { + expect(parseBooleanEnvironmentValue(value, 'FLAG')).toBe(expected); + }); + + it('rejects ambiguous values instead of enabling a capability', () => { + expect(() => parseBooleanEnvironmentValue('1', 'FLAG')).toThrow( + 'FLAG must be either "true" or "false".' + ); + }); +}); diff --git a/apps/web/src/utils/env.ts b/apps/web/src/utils/env.ts new file mode 100644 index 0000000..34b2d79 --- /dev/null +++ b/apps/web/src/utils/env.ts @@ -0,0 +1,9 @@ +export const parseBooleanEnvironmentValue = (value: string | undefined, name: string) => { + if (value === undefined || value.trim() === '') return false; + + const normalized = value.trim().toLowerCase(); + if (normalized === 'true') return true; + if (normalized === 'false') return false; + + throw new Error(`${name} must be either "true" or "false".`); +}; diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts new file mode 100644 index 0000000..900c36f --- /dev/null +++ b/apps/web/src/utils/evm.test.ts @@ -0,0 +1,145 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { Eip1193Provider } from '@/types/window'; + +vi.mock('@/contants/network', () => ({ + EVM_NATIVE_DECIMALS: 18, + EVM_RPC_ENDPOINT: 'https://rpc.example.test', +})); + +import { + assertEvmAccountForChain, + evmBalanceToMicroLume, + getEvmAccountForChain, + getEvmBalance, + isEvmAddress, + parseEvmAmount, + requestEvmRpc, + toHexChainId, +} from './evm'; + +const ADDRESS = '0x0123456789abcdef0123456789abcdef01234567'; +const CHAIN_ID = 76857769; + +const createProvider = (accounts = [ADDRESS], chainId = toHexChainId(CHAIN_ID)) => ({ + request: vi.fn(async ({ method }: { method: string }) => { + if (method === 'eth_accounts') return accounts; + if (method === 'eth_chainId') return chainId; + throw new Error(`Unexpected method ${method}`); + }), +}) as unknown as Eip1193Provider; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('EVM value helpers', () => { + it('validates addresses without accepting malformed values', () => { + expect(isEvmAddress(ADDRESS)).toBe(true); + expect(isEvmAddress(`${ADDRESS}00`)).toBe(false); + expect(isEvmAddress('lumera1abc')).toBe(false); + }); + + it('converts decimal LUME amounts to exact wei hex values', () => { + expect(parseEvmAmount('1')).toBe('0xde0b6b3a7640000'); + expect(parseEvmAmount('0.000000000000000001')).toBe('0x1'); + expect(parseEvmAmount('1.25', 2)).toBe('0x7d'); + }); + + it.each(['0', '-1', '1e3', '1.0000000000000000001'])( + 'rejects invalid amount %s', + (amount) => expect(() => parseEvmAmount(amount)).toThrow() + ); + + it('rejects invalid token decimal metadata', () => { + expect(() => parseEvmAmount('1', -1)).toThrow('non-negative integer'); + expect(() => parseEvmAmount('1', 1.5)).toThrow('non-negative integer'); + }); + + it('converts wei balances to the existing micro-LUME display unit', () => { + expect(evmBalanceToMicroLume('0xde0b6b3a7640000')).toBe('1000000'); + expect(evmBalanceToMicroLume('0xe8d4a50fff')).toBe('0'); + }); + + it('rejects malformed RPC balance quantities', () => { + expect(() => evmBalanceToMicroLume('not-a-quantity')).toThrow('invalid balance'); + expect(() => evmBalanceToMicroLume('-1')).toThrow('invalid balance'); + }); +}); + +describe('EVM account validation', () => { + it('returns the connected account only on the expected chain', async () => { + await expect(getEvmAccountForChain(createProvider(), CHAIN_ID)).resolves.toBe(ADDRESS); + }); + + it('rejects the wrong chain and missing accounts', async () => { + await expect(getEvmAccountForChain(createProvider([ADDRESS], '0x1'), CHAIN_ID)).rejects.toThrow( + 'different network' + ); + await expect(getEvmAccountForChain(createProvider([]), CHAIN_ID)).rejects.toThrow( + 'No EVM wallet account' + ); + }); + + it('detects an account change before sending', async () => { + await expect( + assertEvmAccountForChain(createProvider(), '0x1111111111111111111111111111111111111111', CHAIN_ID) + ).rejects.toThrow('active wallet account changed'); + }); +}); + +describe('requestEvmRpc', () => { + it('posts a JSON-RPC request and returns its result', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ jsonrpc: '2.0', id: 1, result: '0x2a' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await expect(requestEvmRpc('eth_blockNumber')).resolves.toBe('0x2a'); + expect(fetchMock).toHaveBeenCalledWith( + 'https://rpc.example.test', + expect.objectContaining({ method: 'POST' }) + ); + const request = fetchMock.mock.calls[0][1] as RequestInit; + expect(JSON.parse(request.body as string)).toMatchObject({ + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }); + }); + + it('surfaces HTTP, RPC, and malformed-response failures', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 503 })); + await expect(requestEvmRpc('eth_blockNumber')).rejects.toThrow('status 503'); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ error: { code: -32000, message: 'upstream failure' } }), + })); + await expect(requestEvmRpc('eth_blockNumber')).rejects.toThrow('upstream failure'); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({}), + })); + await expect(requestEvmRpc('eth_blockNumber')).rejects.toThrow('returned no result'); + }); +}); + +describe('getEvmBalance', () => { + it('validates the address before making a request', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + await expect(getEvmBalance('invalid')).rejects.toThrow('invalid EVM address'); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('rejects a malformed balance result', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ result: null }), + })); + await expect(getEvmBalance(ADDRESS)).rejects.toThrow('invalid balance'); + }); +}); diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index 63c80f8..c7afb89 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -2,6 +2,7 @@ import { EVM_NATIVE_DECIMALS, EVM_RPC_ENDPOINT, } from '@/contants/network'; +import type { Eip1193Provider } from '@/types/window'; interface EvmRpcResponse { result?: T; @@ -15,7 +16,43 @@ export const isEvmAddress = (value: string) => /^0x[0-9a-fA-F]{40}$/.test(value) export const toHexChainId = (chainId: number) => `0x${chainId.toString(16)}`; +export const getEvmAccountForChain = async ( + provider: Eip1193Provider, + expectedChainId: number +) => { + const [accounts, chainId] = await Promise.all([ + provider.request({ method: 'eth_accounts' }), + provider.request({ method: 'eth_chainId' }), + ]); + + if (chainId.toLowerCase() !== toHexChainId(expectedChainId).toLowerCase()) { + throw new Error('The wallet is connected to a different network.'); + } + + const account = accounts[0] || ''; + if (!isEvmAddress(account)) { + throw new Error('No EVM wallet account is connected.'); + } + + return account; +}; + +export const assertEvmAccountForChain = async ( + provider: Eip1193Provider, + expectedAddress: string, + expectedChainId: number +) => { + const account = await getEvmAccountForChain(provider, expectedChainId); + if (account.toLowerCase() !== expectedAddress.toLowerCase()) { + throw new Error('The active wallet account changed. Please retry the transaction.'); + } + return account; +}; + export const parseEvmAmount = (value: string, decimals = EVM_NATIVE_DECIMALS) => { + if (!Number.isSafeInteger(decimals) || decimals < 0) { + throw new Error('Token decimals must be a non-negative integer.'); + } const normalized = value.trim(); if (!/^\d+(\.\d+)?$/.test(normalized)) { throw new Error('Enter a valid amount.'); @@ -37,6 +74,9 @@ export const parseEvmAmount = (value: string, decimals = EVM_NATIVE_DECIMALS) => }; export const evmBalanceToMicroLume = (balance: string) => { + if (!/^0x[0-9a-fA-F]+$/.test(balance)) { + throw new Error('EVM RPC returned an invalid balance.'); + } const wei = BigInt(balance); const microLumeDivisor = BigInt(10) ** BigInt(EVM_NATIVE_DECIMALS - 6); return (wei / microLumeDivisor).toString(); @@ -73,5 +113,13 @@ export const requestEvmRpc = async (method: string, params: unknown[] = []): return payload.result; }; -export const getEvmBalance = (address: string) => - requestEvmRpc('eth_getBalance', [address, 'latest']); +export const getEvmBalance = async (address: string) => { + if (!isEvmAddress(address)) { + throw new Error('Cannot query the balance of an invalid EVM address.'); + } + const balance = await requestEvmRpc('eth_getBalance', [address, 'latest']); + if (typeof balance !== 'string' || !/^0x[0-9a-fA-F]+$/.test(balance)) { + throw new Error('EVM RPC returned an invalid balance.'); + } + return balance; +}; diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 0000000..bf5fa08 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,14 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/docs/design/2026-07-31-evm-metamask-cosmos-signing.md b/docs/design/2026-07-31-evm-metamask-cosmos-signing.md index 70bd76b..12f6fdb 100644 --- a/docs/design/2026-07-31-evm-metamask-cosmos-signing.md +++ b/docs/design/2026-07-31-evm-metamask-cosmos-signing.md @@ -1,175 +1,293 @@ -# EVM profiles: sign Cosmos transactions with MetaMask (EIP-712) +# EVM profiles: staged governance support with MetaMask (EIP-712) - **Date:** 2026-07-31 -- **Status:** Approved design, pending implementation +- **Status:** Approved staged design, pending implementation - **Branch:** `evm-support` -- **First delivery target:** governance (vote, deposit, create proposal) on EVM network profiles +- **First delivery target:** safe read-only governance on EVM profiles, followed by vote, + deposit, and proposal creation after Lumera enables Cosmos EIP-712 verification ## Problem On EVM-enabled network profiles (currently devnet and testnet), the hub connects an injected EIP-1193 wallet (MetaMask) instead of a Cosmos wallet. Native LUME balance and transfers work -via EVM JSON-RPC, but every flow that signs a Cosmos SDK message is a dead-end: +via EVM JSON-RPC, but every flow that signs a Cosmos SDK message is currently a dead-end: `useWalletConnect.getClient()` / `getOfflineSigner()` throw "Cosmos signing is unavailable while using an EVM network profile." -Affected flows: governance voting, deposits, and proposal creation; staking -(delegate/undelegate/redelegate/claims); Cascade uploads (custom Lumera messages). -The user only discovers the failure after clicking the action button. +Affected flows include governance voting, deposits, and proposal creation; staking +(delegate/undelegate/redelegate/claims); and Cascade uploads (custom Lumera messages). +Today the user only discovers the failure after starting an action. + +The planned MetaMask signer depends on Lumera's cosmos/evm EIP-712 signature-verification +fallback. Lumera v1.20.1 includes that fallback in its dependency, but does not yet initialize +the EIP-712 codecs in the production app. The hub must therefore support both network states: + +1. EVM is enabled, but Cosmos EIP-712 signing is not enabled on the chain. +2. EVM and Cosmos EIP-712 signing are both enabled. ## Decision -Adopt the approach proven in `lumera-portal` / `portal-widgets`: **make MetaMask act as a -Cosmos transaction signer via EIP-712 typed-data signing**, and route the existing hooks' -Cosmos messages through it (the only hook change is using the bech32 address in signer fields). Do **not** use the EVM precompiles for module actions, -and do **not** gate governance UI. +Use a two-phase rollout that fails closed. + +### Phase 1: chain does not support Cosmos EIP-712 signing + +- Keep governance pages readable on EVM profiles. +- Disable vote, deposit, and create-proposal controls for MetaMask users. Show a specific + explanation: "Governance transactions are temporarily unavailable with MetaMask on this + network." +- Add the same guard in the governance hooks so a stale UI or direct invocation cannot reach + a signing dead-end. +- Leave `useWalletConnect.getClient()` throwing on EVM profiles while the capability is off. + +The gate is controlled by an explicit deployment capability flag, +`NEXT_PUBLIC_COSMOS_EIP712_ENABLED`, which defaults to `false`. Do not infer support from the +presence of an EVM RPC endpoint, the EVM chain ID, or a Lumera version string. There is no +reliable read-only chain query for this verifier state. + +Expose one derived capability from the wallet layer: + +```ts +canSignCosmosTransactions = !IS_EVM_NETWORK || COSMOS_EIP712_ENABLED +``` + +Cosmos profiles therefore retain their current behavior. An EVM deployment may set the flag +to `true` only after the matching chain has passed the Phase 2 readiness checks below. + +### Phase 2: chain supports Cosmos EIP-712 signing -### Why this works (chain evidence) +Adopt the approach used by `lumera-portal` / `portal-widgets`: make MetaMask act as a Cosmos +transaction signer via EIP-712 typed-data signing and route the existing hooks' Cosmos +messages through it. Do not use EVM precompiles for module actions. -Lumera (`lumera` repo, app version 1.20.1) pins `github.com/cosmos/evm v0.6.0` and uses its -standard dual-routing ante handler. In cosmos/evm v0.6.0, signature verification for -`eth_secp256k1` accounts is: +Once this phase is enabled, the governance mutation controls use the signer instead of the +Phase 1 gate. + +## Chain readiness requirement + +Lumera pins `github.com/cosmos/evm v0.6.0`. Its `eth_secp256k1` public key accepts either a +normal ECDSA signature or an EIP-712 representation of the Cosmos sign doc: ```go -// crypto/ethsecp256k1/ethsecp256k1.go:213 func (pubKey PubKey) VerifySignature(msg, sig []byte) bool { return pubKey.verifySignatureECDSA(msg, sig) || pubKey.verifySignatureAsEIP712(msg, sig) } ``` -The fallback reconstructs the Cosmos sign-doc as EIP-712 typed data (current and legacy -encodings) and verifies the ECDSA signature against its hash. This runs inside the standard -SDK `SigVerificationDecorator` — no extension options and no special ante path required. -A Cosmos tx assembled with a signature obtained from MetaMask's `eth_signTypedData_v4` -therefore verifies on-chain. `lumera-portal` uses exactly this mechanism in production -(`portal-widgets/lib/wallet/wallets/MetamaskWallet.ts`). - -### Alternatives considered - -1. **Gov precompile (0x…0805).** Active on devnet/testnet and covers vote/deposit/submitProposal, - but: per-module coverage only (Lumera's custom modules — Cascade, claims — can never work - this way), `submitProposal` requires reworking the create-proposal flow from legacy v1beta1 - content types to gov v1 JSON, and it adds an ABI-encoding dependency. Each future flow needs - its own precompile integration. Rejected: EIP-712 signing covers all of these at once. -2. **Gate governance UI on EVM profiles.** Smallest change, but leaves governance read-only on - the default (testnet) profile. Rejected by product direction: Lumera EVM supports both - Ethereum and Cosmos transactions, so the hub should too. - -## Architecture - -### New: MetaMask Cosmos signer + signing-client adapter - -`apps/web/src/utils/metamask-cosmos-signer.ts` (adapted from portal-widgets `MetamaskWallet`): - -- **Connect-time key discovery.** After `eth_requestAccounts`, request one - `personal_sign("Verify Public Key")` and recover the compressed secp256k1 public key from - the signature. Cache it (localStorage, keyed by 0x address) so the prompt happens once per - account. The bech32 address is derived from the same 20 account bytes as the 0x address - (`toBech32('lumera', fromHex(ethAddress))` — no pubkey needed for the address itself). -- **`MetamaskSigningClient`** — a small class exposing exactly the call surface the hooks - already use on the Cosmos path: - - `simulate(signerAddress, messages, memo)` → LCD `POST /cosmos/tx/v1beta1/simulate` - (unsigned tx bytes with pubkey + sequence) → returns `gas_used`. - - `signAndBroadcast(signerAddress, messages, fee, memo)` → full flow below. - - `getBlock()` → LCD latest block (used by `useGovernances.getBlock`). - - Sign-and-broadcast flow: - 1. Fetch `account_number` / `sequence` from LCD `/cosmos/auth/v1beta1/accounts/{bech32}`. - 2. Convert messages to amino JSON via cosmjs `AminoTypes` (gov v1beta1 vote/deposit/ - submitProposal are covered by `createDefaultAminoConverters`). - 3. Build the EIP-712 typed-data payload with the message-type definitions - (`@tharsis/eip712` `createEIP712` / `generateTypes` / `generateFee` / - `generateMessageWithMultipleTransactions`, as in the portal). - **Domain `chainId` is `EVM_CHAIN_ID` (76857769) taken from the network profile** — not - parsed from the Cosmos chain-id string. (The portal's `extractChainId` expects - evmos-style `name_1234-5` ids and yields 0 for `lumera-*` ids; we deviate deliberately.) - 4. `eth_signTypedData_v4` with the connected 0x address. - 5. Assemble `TxRaw`: proto-encoded `TxBody`; `authInfo` via `makeAuthInfoBytes` with the - pubkey wrapped as **`/cosmos.evm.crypto.v1.ethsecp256k1.PubKey`** (the type cosmos/evm - registers; not the ethermint or plain-cosmos type URL); the recovered signature bytes. - Declared sign mode mirrors the portal (`SIGN_MODE_DIRECT` default). If devnet - verification rejects it, switch the declared mode to `SIGN_MODE_LEGACY_AMINO_JSON` — - a one-line change; the chain's EIP-712 fallback parses both encodings. - 6. Broadcast via LCD `POST /cosmos/tx/v1beta1/txs` (sync mode), then poll - `/cosmos/tx/v1beta1/txs/{hash}` until inclusion and return - `{ transactionHash, code, rawLog }` — matching what the hooks read from cosmjs' - `DeliverTxResponse`. - -### Message-type definitions (EIP-712 types) - -The typed-data `types` come per message type, portal-style -(`EthermintMessageAdapter`). The portal already defines vote, send, delegate, undelegate, -redelegate, and reward/commission claims. We add the two governance gaps: - -- `/cosmos.gov.v1beta1.MsgDeposit` — trivial (proposal_id, depositor, amount coins). -- `/cosmos.gov.v1beta1.MsgSubmitProposal` — the `content` field is an amino-encoded object - per proposal type (Text / ParameterChange / SoftwareUpgrade). Each gets its own type - definition. This is the riskiest encoding in scope; it is validated live on devnet. If the - chain's encoder rejects a specific proposal type, that proposal type alone reports a clear - error and is fixed in a follow-up — vote and deposit do not depend on it. - -### Changed: wallet plumbing - -- `apps/web/src/app/providers/evm-wallet-provider.tsx` — on connect, run pubkey recovery and - expose `{ ethAddress, cosmosAddress, pubkey }` in context. Skip the `personal_sign` prompt - when the pubkey is already cached for that address. -- `apps/web/src/hooks/useWalletConnect.ts` — on EVM profiles `getClient()` returns a - `MetamaskSigningClient` instead of throwing. Additionally expose `cosmosAddress` (bech32; - on Cosmos profiles it equals `address`). `getOfflineSigner()` keeps throwing for now — - its only consumer is Cascade, which is a follow-up. -- Governance hooks (`useProposals`, `useDeposit`, `useGovernances`) — use `cosmosAddress` - for message signer fields (voter / depositor / proposer). No other hook changes: they keep - calling `client.simulate` / `client.signAndBroadcast` exactly as today. - -### Unchanged - -- Native LUME transfer stays on `eth_sendTransaction` (already shipped on this branch). -- Cosmos-profile behavior (interchain-kit wallets) is untouched. -- Governance UI: no gating; existing modal error states surface failures. +The EIP-712 fallback requires the chain to initialize its global codecs and EVM chain ID with +`eip712.SetEncodingConfig(...)`. Merely using the cosmos/evm ante handler is insufficient. +Before the hub flag can be enabled, the Lumera production app must: + +1. Call `eip712.SetEncodingConfig(app.legacyAmino, app.interfaceRegistry, 76857769)` after all + module Amino types and interfaces have been registered. +2. Include a chain integration test that signs and delivers at least one Cosmos message with + an `eth_secp256k1` key through the same standard ante path used in production. +3. Confirm the deployed EVM chain ID is 76857769 and the deployment's Cosmos chain ID matches + the hub profile. + +The fallback reconstructs typed data from the Cosmos sign doc and verifies the ECDSA signature +against its hash. A Cosmos `TxRaw` containing a MetaMask `eth_signTypedData_v4` signature can +then verify inside the SDK `SigVerificationDecorator`; no extension option is required. + +## Alternatives considered + +1. **Gov precompile (0x…0805).** It covers vote/deposit/submitProposal but only for that + module. Lumera custom modules still need another signing mechanism, and proposal creation + would require a second encoding path. Rejected in favor of one EIP-712 foundation. +2. **Permanently gate governance UI on EVM profiles.** Rejected because Lumera intends to + support Cosmos transactions from Ethereum wallets. A temporary, capability-controlled gate + is accepted for Phase 1 so unsupported transactions cannot be attempted before the chain is + ready. + +## Phase 1 architecture: governance capability gate + +- `apps/web/src/contants/network.ts` — parse + `NEXT_PUBLIC_COSMOS_EIP712_ENABLED`; default to `false` and expose + `COSMOS_EIP712_ENABLED`. +- `apps/web/src/hooks/useWalletConnect.ts` — expose `canSignCosmosTransactions`. On EVM + profiles, `getClient()` continues to throw a clear capability error until the flag is on. +- Governance screens/modals — keep proposal lists and details visible, but disable vote, + deposit, and create-proposal controls when an EVM wallet is in use and the capability is off. + Keep the reason visible next to or inside the disabled action area; do not rely only on a + tooltip. +- Governance hooks (`useProposals`, `useDeposit`, `useGovernances`) — check + `canSignCosmosTransactions` before simulation or broadcast and return the same clear error. + +The UI gate and hook guard intentionally duplicate enforcement: the UI explains availability, +while the hook guard preserves correctness during stale renders or future reuse. + +## Phase 2 architecture: MetaMask Cosmos signer + +### Signer and signing-client adapter + +`apps/web/src/utils/metamask-cosmos-signer.ts` is adapted from portal-widgets +`MetamaskWallet`: + +- **Account hydration and key discovery.** After `eth_requestAccounts`, request one + `personal_sign("Verify Public Key")` and recover the compressed secp256k1 public key. Cache + it in localStorage, keyed by the normalized 0x address. A cached key is accepted only after + deriving its Ethereum address and comparing it with the active account. +- The same hydration path must run for initial `eth_accounts` restoration and every + `accountsChanged` event, not only explicit connect. Account and chain changes clear the + complete previous identity atomically before loading the new one. +- Derive the bech32 address from the same 20 account bytes as the 0x address: + `toBech32('lumera', fromHex(ethAddress))`. +- **`MetamaskSigningClient`** exposes the call surface used by existing hooks: + - `simulate(signerAddress, messages, memo)` posts a tx with pubkey, sequence, and one empty + signature to `POST /cosmos/tx/v1beta1/simulate`, returning numeric `gas_used`. + - `signAndBroadcast(signerAddress, messages, fee, memo)` performs the full flow below. + - `getBlock()` fetches the LCD latest block but normalizes `header.height` from its JSON + string representation to a validated safe integer, matching CosmJS' caller contract. +- Every adapter method validates that `signerAddress` is the hydrated bech32 address for the + active MetaMask account. It never silently ignores or normalizes an unrelated signer. + +Sign-and-broadcast flow: + +1. Fetch `account_number` / `sequence` from + `/cosmos/auth/v1beta1/accounts/{bech32}`, handling the EVM account wrapper explicitly. +2. Convert each message to its exact Amino JSON representation using the converters described + below. +3. Build the EIP-712 typed-data payload with `@tharsis/eip712` helpers. Domain `chainId` is + `EVM_CHAIN_ID` (76857769) from the network profile, while the message's `chain_id` is the + full Cosmos chain ID from the same profile. +4. Re-check the active account and network, then call `eth_signTypedData_v4` with the connected + 0x address. +5. Assemble `TxRaw`: proto-encoded `TxBody`; `authInfo` via `makeAuthInfoBytes` with the pubkey + wrapped as `/cosmos.evm.crypto.v1.ethsecp256k1.PubKey`; and the recovered 65-byte signature. + Declare `SIGN_MODE_DIRECT`; cosmos/evm reconstructs the EIP-712 representation from the + protobuf sign doc. +6. Broadcast in sync mode. Throw with `rawLog` immediately for a nonzero CheckTx code. For a + successful CheckTx, poll the tx query with a bounded timeout; throw with `rawLog` for a + nonzero DeliverTx code. Only return `{ transactionHash, code: 0, rawLog }` after successful + inclusion, because existing hooks treat any returned hash as success. + +### Governance message encodings + +The current hooks use governance v1 for vote and deposit. CosmJS 0.36.1's default Amino +converters do not cover these v1 type URLs, and `@tharsis/eip712`'s built-in vote adapter uses +the legacy v1beta1 Amino type. Do not reuse either blindly. + +Add explicit converters and EIP-712 message builders for: + +- `/cosmos.gov.v1.MsgVote` → Amino type `cosmos-sdk/v1/MsgVote` + (`proposal_id`, `voter`, `option`). +- `/cosmos.gov.v1.MsgDeposit` → Amino type `cosmos-sdk/v1/MsgDeposit` + (`proposal_id`, `depositor`, `amount`). + +Proposal creation currently remains on `/cosmos.gov.v1beta1.MsgSubmitProposal` because the +existing flow wraps legacy proposal content. Its adapter must cover every proposal type that +the UI enables: + +- `/cosmos.gov.v1beta1.TextProposal` +- `/cosmos.params.v1beta1.ParameterChangeProposal` +- `/cosmos.distribution.v1beta1.CommunityPoolSpendProposal` +- `/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal` + +Each content type gets an exact Amino converter and matching EIP-712 nested type definition. +An unsupported proposal type is disabled before submission; it is not offered and allowed to +fail after the wallet prompt. + +### Wallet plumbing + +- `apps/web/src/app/providers/evm-wallet-provider.tsx` — expose the atomically hydrated + `{ ethAddress, cosmosAddress, pubkey }` identity. +- `apps/web/src/hooks/useWalletConnect.ts` — when both `IS_EVM_NETWORK` and + `COSMOS_EIP712_ENABLED` are true, `getClient()` returns `MetamaskSigningClient`. Expose + `cosmosAddress`; on Cosmos profiles it equals `address`. `getOfflineSigner()` remains out of + scope for Cascade. +- Governance hooks — use `cosmosAddress` consistently for message fields and for every + `simulate` / `signAndBroadcast` signer argument. Do not pass the 0x display address into a + Cosmos client method. + +## Unchanged + +- Native LUME transfer stays on `eth_sendTransaction`. +- Cosmos-profile behavior with Keplr/Leap is untouched. +- Governance queries and pages remain available in both phases. +- Staking and Cascade remain gated on EVM profiles until their own Phase 2 adapters are added. ## Scope -**This iteration:** the signer/client adapter, governance message types, `cosmosAddress` -plumbing, and removal of the governance dead-ends. Verified live on devnet. +### This iteration (Phase 1) + +- Capability flag and derived `canSignCosmosTransactions` state. +- Read-only governance with disabled vote, deposit, and create-proposal actions on EVM + deployments where the flag is false. +- Hook-level guards and clear user-facing messaging. +- Cosmos-profile regression coverage. + +### After Lumera chain readiness (Phase 2) + +- MetaMask signer/client adapter and account hydration. +- Governance v1 vote and deposit converters. +- v1beta1 submit-proposal converter covering all four enabled legacy content types. +- `cosmosAddress` plumbing and removal of the Phase 1 governance action gate when the flag is + true. -**Explicit follow-ups (same foundation, small diffs each):** -- Staking page (delegate/undelegate/redelegate/claims already have adapters in the portal to copy). -- Wallet page: re-enable staking sections, rewards claim, and Cosmos REST transaction history - on EVM profiles using `cosmosAddress` (removes most `IS_EVM_NETWORK` branches added earlier). -- Cascade uploads (`getOfflineSigner` consumers) — Lumera custom-message amino converters and - proto types exist in portal-widgets (`lib/amino/lumera-amino.ts`, `lib/protobuf/lumera`). -- Send-modal receipt polling and EVM explorer links (pre-existing polish items). +### Follow-ups + +- Staking page: delegate/undelegate/redelegate/claims. +- Wallet page: staking sections, rewards claim, and Cosmos REST transaction history using + `cosmosAddress`. +- Cascade uploads: `getOfflineSigner` consumers plus Lumera custom-message converters/types. +- Send-modal receipt polling and EVM explorer links. ## Error handling -- MetaMask rejection (code 4001) → the existing modal error states show a friendly - "Request rejected in wallet." message instead of the raw provider string. -- Broadcast returns `code !== 0` → surface `rawLog` in the modal error state. -- LCD/simulate failures → surfaced through the same try/catch paths the hooks already have. -- Account not found on LCD (never-funded account) → clear error telling the user to fund the - address first. +### Phase 1 + +- Disabled actions show the explicit temporary-unavailability message. +- Hook guards return the same message if invoked despite the UI gate. +- No `personal_sign` or `eth_signTypedData_v4` prompt is triggered. + +### Phase 2 + +- MetaMask rejection (code 4001) becomes "Request rejected in wallet." +- Nonzero CheckTx or DeliverTx code throws with `rawLog`; callers never receive a success hash. +- Polling has a bounded timeout and a retryable timeout message. +- LCD/simulation failures are surfaced through existing modal error states. +- Account not found tells the user to fund the address first. +- Account/network changes during signing cancel the operation and require a retry. ## Dependencies -Added to `apps/web`: `@tharsis/eip712`, `@tharsis/transactions` (typed-data construction — -same packages the portal ships), `@ethersproject/hash`, `@ethersproject/signing-key` -(pubkey recovery). All are already vetted in production via lumera-portal. +Phase 1 adds no runtime dependencies. + +Phase 2 adds pinned compatible versions of `@tharsis/eip712`, `@tharsis/transactions`, +`@ethersproject/hash`, and `@ethersproject/signing-key`. The old Tharsis governance helper is +v1beta1-specific, so the hub supplies and tests its own gov v1 definitions rather than treating +the dependency as the source of truth. -**Risk noted:** the chain-side EIP-712 verification fallback is marked deprecated upstream in -cosmos/evm. It is active in v0.6.0, lumera-portal depends on it in production, and Lumera -controls its chain upgrade cadence. If a future chain upgrade removes the fallback, both the -portal and the hub must migrate together (precompiles or whatever replacement cosmos/evm -ships); this design keeps that migration localized to `metamask-cosmos-signer.ts`. +The cosmos/evm fallback is deprecated upstream. Lumera controls its chain upgrade cadence, but +the capability flag must be turned off before deploying any future chain release that removes +or changes the verifier. The signer implementation remains localized so it can later be +replaced by the chain-supported successor. ## Verification -1. `pnpm build:web` and typecheck pass. -2. Live on devnet (`https://lcd.pastel.network`, `https://evm-rpc.pastel.network`, EVM chain - id 76857769) with a funded test account in MetaMask: - - Connect → one `personal_sign` prompt → bech32 address derived and shown where relevant. - - Vote on an active proposal → `eth_signTypedData_v4` prompt → tx included → - LCD `/cosmos/gov/v1/proposals/{id}/votes/{voter}` shows the vote. - - Deposit on a deposit-period proposal → LCD deposits query reflects it. - - Create a Text proposal → proposal appears; if the legacy-content encoding fails, the - error is surfaced and logged as the known follow-up (vote/deposit unaffected). -3. Regression: Cosmos profile (mainnet config) governance still works with Keplr/Leap. +### Phase 1 + +1. `pnpm build:web` and typecheck pass with the flag absent/false. +2. On devnet/testnet EVM profiles: + - Proposal lists and details remain readable. + - Vote, deposit, and create-proposal actions are disabled with the explicit explanation. + - Directly invoking each governance hook returns the same capability error without a wallet + signing prompt or broadcast request. +3. Cosmos profile regression: governance vote, deposit, and proposal creation still work with + Keplr/Leap. + +### Phase 2 readiness and activation + +1. Lumera production-style chain test proves an EIP-712-signed Cosmos tx passes the normal + ante handler after `SetEncodingConfig` initialization. +2. Golden-vector tests compare the browser-produced typed-data hash with cosmos/evm's + reconstructed hash for: + - gov v1 vote; + - gov v1 deposit; + - v1beta1 submit proposal with Text, Parameter Change, Community Pool Spend, and Software + Upgrade content. +3. Adapter tests cover restored sessions, `accountsChanged`, invalid/stale cached pubkeys, + signer-address mismatch, nonzero CheckTx/DeliverTx codes, polling timeout, and numeric block + height normalization. +4. Enable `NEXT_PUBLIC_COSMOS_EIP712_ENABLED=true` only on a chain that passed steps 1–3, then + verify live vote, deposit, and all enabled proposal types before rollout. +5. Re-run the Phase 1 flag-off checks as a rollback test. diff --git a/packages/ui/src/screens/GovernanceDetailsScreen.tsx b/packages/ui/src/screens/GovernanceDetailsScreen.tsx index d53caff..da35419 100644 --- a/packages/ui/src/screens/GovernanceDetailsScreen.tsx +++ b/packages/ui/src/screens/GovernanceDetailsScreen.tsx @@ -27,6 +27,7 @@ import { VoteModal } from './HomeScreen'; import 'react-paginate/theme/basic/react-paginate.css'; interface IGovernanceDetailsScreen { + transactionUnavailableReason: string; isLoading: boolean; isVoteLoading: boolean; governance: IProposal | null; @@ -92,6 +93,7 @@ interface IVoteChartOptions { const COLORS = ['#2dd4bf', '#f87171', '#fb923c', '#9ca3af']; export const GovernanceDetailsScreen = ({ + transactionUnavailableReason, isLoading, governance, pool, @@ -213,10 +215,19 @@ export const GovernanceDetailsScreen = ({
{governance.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? - : null + : null } - +
+ {transactionUnavailableReason ? ( +

{transactionUnavailableReason}

+ ) : null}
); } diff --git a/packages/ui/src/screens/GovernanceScreen.tsx b/packages/ui/src/screens/GovernanceScreen.tsx index 7a71fa0..b1360f2 100644 --- a/packages/ui/src/screens/GovernanceScreen.tsx +++ b/packages/ui/src/screens/GovernanceScreen.tsx @@ -28,6 +28,7 @@ import { formatNumber, formatToken } from '@/utils/format'; import { VoteModal } from './HomeScreen'; interface IGovernanceScreen { + transactionUnavailableReason: string; selectedItem: IProposal | null; setSelectedItem: (item: IProposal) => void; isLoading: boolean, @@ -135,6 +136,7 @@ interface IGovernanceScreen { } export const GovernanceScreen = ({ + transactionUnavailableReason, isLoading, governances, sumary, @@ -234,10 +236,19 @@ export const GovernanceScreen = ({
{item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? - : null + : null } - +
+ {transactionUnavailableReason ? ( +

{transactionUnavailableReason}

+ ) : null}
); } @@ -263,11 +274,19 @@ export const GovernanceScreen = ({

Governance

-
+ {transactionUnavailableReason ? ( +
+ {transactionUnavailableReason} +
+ ) : null}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 412a06a..364b77e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,6 +250,9 @@ importers: typescript: specifier: ^5 version: 5.8.3 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) packages/core: dependencies: @@ -957,156 +960,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.8': resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.8': resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.8': resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.8': resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.8': resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.8': resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.8': resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.8': resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.8': resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.8': resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.8': resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.8': resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.8': resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.8': resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.8': resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.8': resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.8': resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.8': resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.8': resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.8': resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.8': resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.8': resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.8': resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.8': resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.8': resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1753,6 +1912,12 @@ packages: resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==} engines: {node: '>= 18'} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2580,6 +2745,131 @@ packages: react-redux: optional: true + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -3633,9 +3923,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3645,6 +3941,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -3904,6 +4203,35 @@ packages: peerDependencies: '@vanilla-extract/css': ^1.0.0 + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@walletconnect/core@2.21.8': resolution: {integrity: sha512-MD1SY7KAeHWvufiBK8C1MwP9/pxxI7SnKi/rHYfjco2Xvke+M+Bbm2OzvuSN7dYZvwLTkZCiJmBccTNVPCpSUQ==} engines: {node: '>=18'} @@ -4023,6 +4351,7 @@ packages: '@xmldom/xmldom@0.8.10': resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4204,6 +4533,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -4447,6 +4780,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -4497,6 +4834,10 @@ packages: resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} hasBin: true + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chain-registry@2.0.42: resolution: {integrity: sha512-Magth+y5yLEvyKLgOMJBkxtvaXufyE8/0KhLfyKZfG2Fqy+R9AuKoffr99n8ViSSAXf0kZCYDMZ8bh27P3Vo3A==} @@ -4517,6 +4858,10 @@ packages: engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4706,6 +5051,7 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} @@ -4813,6 +5159,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -5065,6 +5415,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5212,6 +5567,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5246,6 +5604,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + expo-asset@11.1.7: resolution: {integrity: sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg==} peerDependencies: @@ -5367,6 +5729,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -5558,11 +5929,12 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -6018,6 +6390,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -6321,6 +6696,9 @@ packages: lossless-json@4.3.0: resolution: {integrity: sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -6619,6 +6997,7 @@ packages: next@15.4.6: resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -6876,6 +7255,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6891,6 +7277,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -7016,6 +7406,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -7488,6 +7879,11 @@ packages: resolution: {integrity: sha512-PD6U2PGk6Vq2spfgiWZdomLvRGDreBLxi5jv5M8EpRo3pU6VEm31KO+HFxE18Q3vgqfDrQ9pZA3FP95rkijNKw==} hasBin: true + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rpc-websockets@9.2.0: resolution: {integrity: sha512-DS/XHdPxplQTtNRKiBCRWGBJfjOk56W7fyFUpiYi9fSTWTzoEMbUkn3J4gB0IMniIEVeAGR1/rzFQogzD5MxvQ==} @@ -7635,6 +8031,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -7719,6 +8118,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -7738,6 +8140,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -7820,6 +8225,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -7911,6 +8319,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} @@ -7968,10 +8377,32 @@ packages: resolution: {integrity: sha512-eb+F6NabSnjbLwNoC+2o5ItbmP1kg7HliWue71JgLegQt6A5mTN8YbvTLCazdlg6e5SV6A+r8OGvZYskdlmhqQ==} engines: {node: '>=6.0.0'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -8298,10 +8729,12 @@ packages: uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true validate-npm-package-name@5.0.1: @@ -8320,6 +8753,79 @@ packages: typescript: optional: true + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} @@ -8397,6 +8903,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wif@2.0.6: resolution: {integrity: sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==} @@ -9361,81 +9872,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.8': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.25.8': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.25.8': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.25.8': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.25.8': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.25.8': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.25.8': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.25.8': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.25.8': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.25.8': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.25.8': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.25.8': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.25.8': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.25.8': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.25.8': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.25.8': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.25.8': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.25.8': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.25.8': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.25.8': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.25.8': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.25.8': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.25.8': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.25.8': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.25.8': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.25.8': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))': dependencies: eslint: 9.33.0(jiti@2.5.1) @@ -11017,6 +11606,9 @@ snapshots: '@msgpack/msgpack@3.1.2': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.4.5 @@ -13276,6 +13868,81 @@ snapshots: react: 19.1.0 react-redux: 9.2.0(@types/react@19.0.14)(react@19.1.0)(redux@5.0.1) + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} @@ -16091,10 +16758,17 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 20.19.10 + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -16107,6 +16781,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 20.19.10 @@ -16374,6 +17050,48 @@ snapshots: dependencies: '@vanilla-extract/css': 1.17.4 + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@walletconnect/core@2.21.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -16943,6 +17661,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} astral-regex@2.0.0: {} @@ -17250,6 +17970,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -17297,6 +18019,14 @@ snapshots: ansicolors: 0.3.2 redeyed: 2.1.1 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chain-registry@2.0.42: dependencies: '@chain-registry/types': 2.0.42 @@ -17326,6 +18056,8 @@ snapshots: table: 6.9.0 type-fest: 4.41.0 + check-error@2.1.3: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -17628,6 +18360,8 @@ snapshots: dedent@1.6.0: {} + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -17972,6 +18706,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.8 '@esbuild/win32-x64': 0.25.8 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -18188,6 +18951,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -18223,6 +18990,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.4.0: {} + expo-asset@11.1.7(expo@53.0.20(@babel/core@7.28.0)(bufferutil@4.0.9)(react-native@0.79.5(@babel/core@7.28.0)(@types/react@19.0.14)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.5(@babel/core@7.28.0)(@types/react@19.0.14)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0): dependencies: '@expo/image-utils': 0.7.6 @@ -18374,6 +19143,14 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -19151,6 +19928,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -19392,6 +20171,8 @@ snapshots: lossless-json@4.3.0: {} + loupe@3.2.1: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -20184,6 +20965,10 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -20192,6 +20977,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.5: {} + pify@2.3.0: {} pino-abstract-transport@0.5.0: @@ -21224,6 +22011,38 @@ snapshots: rlp@3.0.0: {} + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + rpc-websockets@9.2.0: dependencies: '@swc/helpers': 0.5.17 @@ -21455,6 +22274,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -21524,6 +22345,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + stackframe@1.3.4: {} stacktrace-parser@0.1.11: @@ -21547,6 +22370,8 @@ snapshots: statuses@2.0.1: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -21652,6 +22477,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + structured-headers@0.4.1: {} style-value-types@5.0.0: @@ -21943,11 +22772,26 @@ snapshots: elliptic: 6.6.1 nan: 2.23.0 + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tmpl@1.0.5: {} to-buffer@1.2.1: @@ -22243,6 +23087,84 @@ snapshots: - utf-8-validate - zod + vite-node@3.2.4(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.10 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + terser: 5.43.1 + yaml: 2.8.1 + + vitest@3.2.7(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.1 + expect-type: 1.4.0 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.10 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vlq@1.0.1: {} w-json@1.3.10: {} @@ -22365,6 +23287,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wif@2.0.6: dependencies: bs58check: 2.1.2 diff --git a/turbo.json b/turbo.json index 1ad3375..804d97d 100644 --- a/turbo.json +++ b/turbo.json @@ -5,6 +5,29 @@ "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], + "env": [ + "NEXT_OUTPUT", + "NEXT_PUBLIC_ABSTRACTAPI_KEY", + "NEXT_PUBLIC_CHAIN_ID", + "NEXT_PUBLIC_CHAIN_NAME", + "NEXT_PUBLIC_COSMOS_EIP712_ENABLED", + "NEXT_PUBLIC_DENOM", + "NEXT_PUBLIC_EVM_CHAIN_ID", + "NEXT_PUBLIC_EVM_RPC_ENDPOINT", + "NEXT_PUBLIC_EVM_WS_ENDPOINT", + "NEXT_PUBLIC_IPAPI_KEY", + "NEXT_PUBLIC_NETWORK_PROFILE", + "NEXT_PUBLIC_NODE_ENV", + "NEXT_PUBLIC_REST_AI_URL", + "NEXT_PUBLIC_RPC_ENDPOINT", + "NEXT_PUBLIC_SNAPI_URL", + "NEXT_PUBLIC_WALLET_CONNECT_DESCRIPTION", + "NEXT_PUBLIC_WALLET_CONNECT_ICON", + "NEXT_PUBLIC_WALLET_CONNECT_NAME", + "NEXT_PUBLIC_WALLET_CONNECT_PROJECTID", + "NEXT_PUBLIC_WALLET_CONNECT_RELAY_URL", + "NEXT_PUBLIC_WALLET_CONNECT_URL" + ], "outputs": ["dist/**", ".next/**", "out/**"] }, "lint": { From d2b6d4477e03b816f4996f17b60fd142484793d0 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 13:56:52 -0400 Subject: [PATCH 06/45] improve network startup and EVM query safety --- Makefile | 121 ++++++++++++++++++ apps/web/babel.config.js | 14 -- apps/web/src/hooks/useDeposit.ts | 21 ++- .../web/src/utils/cosmos-transactions.test.ts | 21 +++ apps/web/src/utils/cosmos-transactions.ts | 8 ++ apps/web/src/utils/helpers.test.ts | 54 ++++++++ apps/web/src/utils/helpers.ts | 18 +-- 7 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 Makefile delete mode 100644 apps/web/babel.config.js create mode 100644 apps/web/src/utils/helpers.test.ts diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8ae65f --- /dev/null +++ b/Makefile @@ -0,0 +1,121 @@ +SHELL := /bin/bash + +PNPM ?= pnpm +PORT ?= 3000 +WEB_ENV_FILE := apps/web/.env.local +WEB_ENV_EXAMPLE := apps/web/.env.example + +# Set complete profiles explicitly so stale local overrides cannot route a run +# to another network. Cosmos EIP-712 writes remain fail-closed. +COMMON_NETWORK_ENV := \ + NEXT_PUBLIC_DENOM=ulume \ + NEXT_PUBLIC_COSMOS_EIP712_ENABLED=false \ + NEXT_PUBLIC_SNAPI_URL=http://localhost:3100 + +DEVNET_ENV := \ + $(COMMON_NETWORK_ENV) \ + NEXT_PUBLIC_NETWORK_PROFILE=devnet \ + NEXT_PUBLIC_CHAIN_NAME=lumera-devnet \ + NEXT_PUBLIC_CHAIN_ID=lumera-devnet-1 \ + NEXT_PUBLIC_REST_AI_URL=https://lcd.pastel.network \ + NEXT_PUBLIC_RPC_ENDPOINT=https://rpc.pastel.network \ + NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-rpc.pastel.network \ + NEXT_PUBLIC_EVM_WS_ENDPOINT= \ + NEXT_PUBLIC_EVM_CHAIN_ID=76857769 + +TESTNET_ENV := \ + $(COMMON_NETWORK_ENV) \ + NEXT_PUBLIC_NETWORK_PROFILE=testnet \ + NEXT_PUBLIC_CHAIN_NAME=lumera-testnet \ + NEXT_PUBLIC_CHAIN_ID=lumera-testnet-2 \ + NEXT_PUBLIC_REST_AI_URL=https://lcd-testnet.lumeraprotocol.com \ + NEXT_PUBLIC_RPC_ENDPOINT=https://rpc-testnet.lumeraprotocol.com \ + NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-testnet.lumeraprotocol.com \ + NEXT_PUBLIC_EVM_WS_ENDPOINT=https://evm-ws-testnet.lumeraprotocol.com \ + NEXT_PUBLIC_EVM_CHAIN_ID=76857769 + +MAINNET_ENV := \ + $(COMMON_NETWORK_ENV) \ + NEXT_PUBLIC_NETWORK_PROFILE=mainnet \ + NEXT_PUBLIC_CHAIN_NAME=lumera \ + NEXT_PUBLIC_CHAIN_ID=lumera-mainnet-1 \ + NEXT_PUBLIC_REST_AI_URL=https://lcd.lumera.io \ + NEXT_PUBLIC_RPC_ENDPOINT=https://rpc.lumera.io \ + NEXT_PUBLIC_EVM_RPC_ENDPOINT= \ + NEXT_PUBLIC_EVM_WS_ENDPOINT= \ + NEXT_PUBLIC_EVM_CHAIN_ID= + +.DEFAULT_GOAL := help + +.PHONY: help deps local-env setup \ + devnet devnet-build devnet-check devnet-preview \ + testnet testnet-build testnet-check testnet-preview \ + mainnet mainnet-build mainnet-check mainnet-preview \ + test typecheck + +help: ## Show available commands. + @printf '%s\n' \ + 'make devnet Install, configure, and run the devnet development server' \ + 'make testnet Install, configure, and run the testnet development server' \ + 'make mainnet Install, configure, and run the mainnet development server' \ + 'make -check Run tests, type checking, and a production build' \ + 'make -preview Build and serve a production bundle' \ + 'make -build Build the web app for devnet, testnet, or mainnet' \ + 'make setup Install dependencies and create .env.local when missing' \ + 'make test Run web unit tests' \ + 'make typecheck Run web TypeScript checks' + +deps: + $(PNPM) install --frozen-lockfile + +local-env: + @if [[ -f "$(WEB_ENV_FILE)" ]]; then \ + printf 'Keeping existing %s\n' "$(WEB_ENV_FILE)"; \ + else \ + cp "$(WEB_ENV_EXAMPLE)" "$(WEB_ENV_FILE)"; \ + printf 'Created %s from %s\n' "$(WEB_ENV_FILE)" "$(WEB_ENV_EXAMPLE)"; \ + fi + +setup: deps local-env + +devnet: setup ## Run the development server against Lumera devnet. + @printf 'Starting devnet at http://localhost:%s (the first request can take about a minute to compile)\n' "$(PORT)" + $(DEVNET_ENV) $(PNPM) --filter web dev --port $(PORT) + +testnet: setup ## Run the development server against Lumera testnet. + @printf 'Starting testnet at http://localhost:%s (the first request can take about a minute to compile)\n' "$(PORT)" + $(TESTNET_ENV) $(PNPM) --filter web dev --port $(PORT) + +mainnet: setup ## Run the development server against Lumera mainnet. + @printf 'Starting mainnet at http://localhost:%s (the first request can take about a minute to compile)\n' "$(PORT)" + $(MAINNET_ENV) $(PNPM) --filter web dev --port $(PORT) + +test: deps ## Run web unit tests. + $(PNPM) --filter web test + +typecheck: deps ## Run web TypeScript checks. + $(PNPM) --filter web exec tsc --noEmit + +devnet-build: setup ## Create a production build configured for devnet. + $(DEVNET_ENV) $(PNPM) build:web + +testnet-build: setup ## Create a production build configured for testnet. + $(TESTNET_ENV) $(PNPM) build:web + +mainnet-build: setup ## Create a production build configured for mainnet. + $(MAINNET_ENV) $(PNPM) build:web + +devnet-check: test typecheck devnet-build ## Run all automated devnet checks. + +testnet-check: test typecheck testnet-build ## Run all automated testnet checks. + +mainnet-check: test typecheck mainnet-build ## Run all automated mainnet checks. + +devnet-preview: devnet-build ## Build and serve the devnet production bundle. + $(DEVNET_ENV) $(PNPM) --filter web start --port $(PORT) + +testnet-preview: testnet-build ## Build and serve the testnet production bundle. + $(TESTNET_ENV) $(PNPM) --filter web start --port $(PORT) + +mainnet-preview: mainnet-build ## Build and serve the mainnet production bundle. + $(MAINNET_ENV) $(PNPM) --filter web start --port $(PORT) diff --git a/apps/web/babel.config.js b/apps/web/babel.config.js deleted file mode 100644 index 8b30343..0000000 --- a/apps/web/babel.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const path = require('path') - -module.exports = { - presets: ['next/babel'], - plugins: [ - [ - '@tamagui/babel-plugin', - { - config: path.resolve(__dirname, '../../tamagui.config.ts'), - components: ['tamagui'], - }, - ], - ], -} diff --git a/apps/web/src/hooks/useDeposit.ts b/apps/web/src/hooks/useDeposit.ts index 0aa4f46..a5899bf 100644 --- a/apps/web/src/hooks/useDeposit.ts +++ b/apps/web/src/hooks/useDeposit.ts @@ -8,7 +8,10 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import { DENOM } from '@/contants/network'; import { RATE_VALUE, GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; import { extractValidNumber } from '@/utils/helpers'; -import { assertGovernanceTransactionsAvailable } from '@/utils/cosmos-transactions'; +import { + assertGovernanceTransactionsAvailable, + canQueryCosmosAccountData, +} from '@/utils/cosmos-transactions'; interface UseDepositOptions { callback?: () => void; @@ -16,7 +19,7 @@ interface UseDepositOptions { } const useDeposit = (options: UseDepositOptions = {}) => { - const { address, canSignCosmosTransactions, getClient } = useWalletConnect(); + const { address, canSignCosmosTransactions, getClient, isEvm } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [depositAdvanced, setDepositAdvanced] = useState({ senderAddress: address, @@ -33,6 +36,10 @@ const useDeposit = (options: UseDepositOptions = {}) => { const [transactionHash, setTransactionHash] = useState(''); const fetchData = async () => { + if (!canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { + setAvailableAmount(0); + return; + } try { const { data } = await instance.get(`/cosmos/bank/v1beta1/balances/${address}`); let total = 0; @@ -42,16 +49,18 @@ const useDeposit = (options: UseDepositOptions = {}) => { } } setAvailableAmount(Number((total / RATE_VALUE).toFixed(6))); - } catch (e) { - console.error('API Error:', e); + } catch { + setAvailableAmount(0); } }; useEffect(() => { - if (address) { + if (canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { fetchData(); + } else { + setAvailableAmount(0); } - }, [address]); + }, [address, isEvm]); useEffect(() => { if (options?.customMemo) { diff --git a/apps/web/src/utils/cosmos-transactions.test.ts b/apps/web/src/utils/cosmos-transactions.test.ts index 0fae856..5b495a0 100644 --- a/apps/web/src/utils/cosmos-transactions.test.ts +++ b/apps/web/src/utils/cosmos-transactions.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { assertGovernanceTransactionsAvailable, + canQueryCosmosAccountData, canWalletSignCosmosTransactions, GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE, } from './cosmos-transactions'; @@ -40,3 +41,23 @@ describe('canWalletSignCosmosTransactions', () => { })).toBe(true); }); }); + +describe('canQueryCosmosAccountData', () => { + it('requires a connected Cosmos account', () => { + expect(canQueryCosmosAccountData({ + address: '', + isEvmNetwork: false, + })).toBe(false); + expect(canQueryCosmosAccountData({ + address: 'lumera1account', + isEvmNetwork: false, + })).toBe(true); + }); + + it('never sends an EVM address to Cosmos account endpoints', () => { + expect(canQueryCosmosAccountData({ + address: '0x1234567890abcdef', + isEvmNetwork: true, + })).toBe(false); + }); +}); diff --git a/apps/web/src/utils/cosmos-transactions.ts b/apps/web/src/utils/cosmos-transactions.ts index 6e4c398..fcab7d0 100644 --- a/apps/web/src/utils/cosmos-transactions.ts +++ b/apps/web/src/utils/cosmos-transactions.ts @@ -11,6 +11,14 @@ export const canWalletSignCosmosTransactions = ({ hasEvmCosmosSigner: boolean; }) => !isEvmNetwork || (chainEip712Enabled && hasEvmCosmosSigner); +export const canQueryCosmosAccountData = ({ + address, + isEvmNetwork, +}: { + address: string; + isEvmNetwork: boolean; +}) => Boolean(address) && !isEvmNetwork; + export const assertGovernanceTransactionsAvailable = (canSignCosmosTransactions: boolean) => { if (!canSignCosmosTransactions) { throw new Error(GOVERNANCE_TRANSACTION_UNAVAILABLE_MESSAGE); diff --git a/apps/web/src/utils/helpers.test.ts b/apps/web/src/utils/helpers.test.ts new file mode 100644 index 0000000..95e4ae1 --- /dev/null +++ b/apps/web/src/utils/helpers.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const originalProfile = process.env.NEXT_PUBLIC_NETWORK_PROFILE; + +beforeEach(() => { + delete process.env.NEXT_PUBLIC_NETWORK_PROFILE; + vi.resetModules(); +}); + +afterEach(() => { + if (originalProfile === undefined) delete process.env.NEXT_PUBLIC_NETWORK_PROFILE; + else process.env.NEXT_PUBLIC_NETWORK_PROFILE = originalProfile; + vi.resetModules(); +}); + +describe('getChains', () => { + it('returns only the configured Lumera testnet chain and assets', async () => { + process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'testnet'; + const { getChains } = await import('./helpers'); + + const { chains, assetLists } = getChains(); + + expect(chains).toHaveLength(1); + expect(assetLists).toHaveLength(1); + expect(chains[0]).toMatchObject({ + chainName: 'lumera-testnet', + chainId: 'lumera-testnet-2', + apis: { + rpc: [{ address: 'https://rpc-testnet.lumeraprotocol.com' }], + rest: [{ address: 'https://lcd-testnet.lumeraprotocol.com' }], + }, + }); + expect(assetLists[0].chainName).toBe('lumera-testnet'); + }); + + it('returns only the configured Lumera mainnet chain and assets', async () => { + process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'mainnet'; + const { getChains } = await import('./helpers'); + + const { chains, assetLists } = getChains(); + + expect(chains).toHaveLength(1); + expect(assetLists).toHaveLength(1); + expect(chains[0]).toMatchObject({ + chainName: 'lumera', + chainId: 'lumera-mainnet-1', + apis: { + rpc: [{ address: 'https://rpc.lumera.io' }], + rest: [{ address: 'https://lcd.lumera.io' }], + }, + }); + expect(assetLists[0].chainName).toBe('lumera'); + }); +}); diff --git a/apps/web/src/utils/helpers.ts b/apps/web/src/utils/helpers.ts index daa5988..3c7cdb9 100644 --- a/apps/web/src/utils/helpers.ts +++ b/apps/web/src/utils/helpers.ts @@ -6,8 +6,8 @@ import { toHex, } from '@cosmjs/encoding'; import { Ripemd160, sha256 } from '@cosmjs/crypto'; -import chainMainnet from 'chain-registry/mainnet' -import chainTestnet from 'chain-registry/testnet'; +import { assetList as mainnetAssets, chain as mainnetChain } from 'chain-registry/mainnet/lumera'; +import { assetList as testnetAssets, chain as testnetChain } from 'chain-registry/testnet/lumeratestnet'; export { parseCoins } from '@cosmjs/stargate'; import { MsgDelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; @@ -178,17 +178,9 @@ export const getChains = () => { } } - const registry = NETWORK_PROFILE === 'testnet' ? chainTestnet : chainMainnet; - const chain = registry.chains.find(({ chainName, chainId }) => - chainName === CHAIN_NAME || chainId === CHAIN_ID - ); - const assets = chain - ? registry.assetLists.find(({ chainName }) => chainName === chain.chainName) - : undefined; - - if (!chain || !assets) { - return { assetLists: [], chains: [] }; - } + const { chain, assets } = NETWORK_PROFILE === 'testnet' + ? { chain: testnetChain, assets: testnetAssets } + : { chain: mainnetChain, assets: mainnetAssets }; return { assetLists: [{ ...assets, chainName: CHAIN_NAME }], From 9a04de562dff68e406baeb4f8caa2f14083aeda0 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 13:57:00 -0400 Subject: [PATCH 07/45] support selectable MetaMask and Keplr wallets --- .../src/app/providers/evm-wallet-provider.tsx | 31 +- apps/web/src/app/staking/page.tsx | 4 +- apps/web/src/app/wallet/page.tsx | 7 +- apps/web/src/components/ConnectWallet.tsx | 284 ++++++++++++++---- apps/web/src/hooks/useAccountInfo.ts | 12 +- apps/web/src/hooks/useDelegate.ts | 12 +- apps/web/src/hooks/useSend.ts | 13 +- apps/web/src/hooks/useStaking.ts | 39 ++- apps/web/src/hooks/useTransaction.ts | 7 +- apps/web/src/hooks/useWalletConnect.ts | 28 +- apps/web/src/types/window.d.ts | 2 + apps/web/src/utils/evm.test.ts | 34 +++ apps/web/src/utils/evm.ts | 9 + apps/web/src/utils/wallet-selection.test.ts | 50 +++ apps/web/src/utils/wallet-selection.ts | 35 +++ 15 files changed, 459 insertions(+), 108 deletions(-) create mode 100644 apps/web/src/utils/wallet-selection.test.ts create mode 100644 apps/web/src/utils/wallet-selection.ts diff --git a/apps/web/src/app/providers/evm-wallet-provider.tsx b/apps/web/src/app/providers/evm-wallet-provider.tsx index 316abca..128e207 100644 --- a/apps/web/src/app/providers/evm-wallet-provider.tsx +++ b/apps/web/src/app/providers/evm-wallet-provider.tsx @@ -8,13 +8,17 @@ import { EVM_RPC_ENDPOINT, IS_EVM_NETWORK, } from '@/contants/network'; -import { getEvmAccountForChain, toHexChainId } from '@/utils/evm'; +import { getEvmAccountForChain, getMetaMaskProvider, toHexChainId } from '@/utils/evm'; import type { Eip1193Provider } from '@/types/window'; interface EvmProviderError extends Error { code?: number; } +interface Eip6963ProviderDetail { + provider?: Eip1193Provider; +} + interface EvmWalletContextValue { address: string; isConnected: boolean; @@ -33,14 +37,29 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { const [isConnecting, setConnecting] = useState(false); const [error, setError] = useState(''); const [provider, setProvider] = useState( - typeof window === 'undefined' ? null : window.ethereum || null + typeof window === 'undefined' ? null : getMetaMaskProvider(window.ethereum) ); useEffect(() => { - const detectProvider = () => setProvider(window.ethereum || null); + const detectProvider = () => { + const detectedProvider = getMetaMaskProvider(window.ethereum); + setProvider((currentProvider) => detectedProvider || currentProvider); + }; + const handleProviderAnnouncement = (event: Event) => { + const announcedProvider = getMetaMaskProvider( + (event as CustomEvent).detail?.provider + ); + if (announcedProvider) setProvider(announcedProvider); + }; + detectProvider(); window.addEventListener('ethereum#initialized', detectProvider, { once: true }); - return () => window.removeEventListener('ethereum#initialized', detectProvider); + window.addEventListener('eip6963:announceProvider', handleProviderAnnouncement); + window.dispatchEvent(new Event('eip6963:requestProvider')); + return () => { + window.removeEventListener('ethereum#initialized', detectProvider); + window.removeEventListener('eip6963:announceProvider', handleProviderAnnouncement); + }; }, []); const ensureNetwork = useCallback(async () => { @@ -48,7 +67,7 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { throw new Error('The active network does not support EVM wallets.'); } if (!provider) { - throw new Error('No EVM wallet was detected. Install MetaMask or another compatible wallet.'); + throw new Error('MetaMask was not detected. Install or enable the MetaMask extension.'); } const chainId = toHexChainId(EVM_CHAIN_ID); @@ -94,7 +113,7 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { setConnecting(true); try { if (!provider) { - throw new Error('No EVM wallet was detected. Install MetaMask or another compatible wallet.'); + throw new Error('MetaMask was not detected. Install or enable the MetaMask extension.'); } await provider.request({ method: 'eth_requestAccounts' }); await ensureNetwork(); diff --git a/apps/web/src/app/staking/page.tsx b/apps/web/src/app/staking/page.tsx index b3f5d1b..62a8e63 100644 --- a/apps/web/src/app/staking/page.tsx +++ b/apps/web/src/app/staking/page.tsx @@ -12,8 +12,8 @@ import useUnbond from '@/hooks/useUnbond'; import useRedelegate from '@/hooks/useRedelegate'; export default function Page() { - const { address } = useWalletConnect(); - const staking = useStaking(address); + const { address, isEvm } = useWalletConnect(); + const staking = useStaking(address, isEvm); const { loading, accountInfo, diff --git a/apps/web/src/app/wallet/page.tsx b/apps/web/src/app/wallet/page.tsx index 052c14a..2469624 100644 --- a/apps/web/src/app/wallet/page.tsx +++ b/apps/web/src/app/wallet/page.tsx @@ -9,10 +9,9 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import useTransaction from '@/hooks/useTransaction'; import useDelegate from '@/hooks/useDelegate'; import useSend from '@/hooks/useSend'; -import { IS_EVM_NETWORK } from '@/contants/network'; export default function Page() { - const { address } = useWalletConnect(); + const { address, isEvm } = useWalletConnect(); const account = useAccountInfo(); const { accountInfo, @@ -28,7 +27,7 @@ export default function Page() { handlePageClick, } = useTransaction(); const sendOptions = useSend({ - callback: IS_EVM_NETWORK ? account.fetchData : handleCloseModal, + callback: isEvm ? account.fetchData : handleCloseModal, customMemo: '', }); const delegate = useDelegate(); @@ -44,7 +43,7 @@ export default function Page() {
{ + toast.error(error instanceof Error ? error.message : fallback, { + position: 'bottom-center', + theme: 'dark', + }); +}; + +function WalletChoiceModal() { + const dispatch = useDispatch(); + const isModalOpen = useSelector((state) => state.wallet.isModalOpen); + const evmWallet = useEvmWallet(); + const keplrWallet = useChainWallet(CHAIN_NAME, KEPLR_WALLET_NAME); + const [isKeplrInstalled, setKeplrInstalled] = useState(false); + const [connectingWallet, setConnectingWallet] = useState(''); + + useEffect(() => { + if (isModalOpen) setKeplrInstalled(Boolean(window.keplr)); + }, [isModalOpen]); + + if (!isModalOpen) return null; + + const close = () => dispatch(setModalOpen({ status: false })); + + const connectMetaMask = async () => { + setConnectingWallet(METAMASK_WALLET_NAME); + try { + await evmWallet.connect(); + dispatch(setWalletName({ walletName: METAMASK_WALLET_NAME })); + close(); + } catch (error) { + showWalletError(error, 'Unable to connect MetaMask.'); + } finally { + setConnectingWallet(''); + } + }; + + const connectKeplr = async () => { + setConnectingWallet(KEPLR_WALLET_NAME); + try { + await keplrWallet.connect(); + dispatch(setWalletName({ walletName: KEPLR_WALLET_NAME })); + close(); + } catch (error) { + showWalletError(error, 'Unable to connect Keplr.'); + } finally { + setConnectingWallet(''); + } + }; + + const walletButton = ( + name: string, + description: string, + installed: boolean, + onClick: () => Promise, + walletName: string + ) => ( + + ); + + return ( +
{ + if (event.target === event.currentTarget) close(); + }} + style={{ + position: 'fixed', + inset: 0, + zIndex: 1000, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 20, + background: 'rgba(5, 7, 15, 0.72)', + }} + > +
+
+
+

+ Choose wallet +

+

+ Select how you want to use Lumera Hub. +

+
+ +
+
+ {walletButton( + 'MetaMask', + 'EVM address and native LUME transfers', + Boolean(evmWallet.provider), + connectMetaMask, + METAMASK_WALLET_NAME + )} + {walletButton( + 'Keplr', + 'Cosmos transfers, staking, and governance', + isKeplrInstalled, + connectKeplr, + KEPLR_WALLET_NAME + )} +
+
+
+ ); +} export function WalletModalComponent() { const dispatch = useDispatch(); const { address: cosmosAddress } = useChain(CHAIN_NAME); const { address: evmAddress } = useEvmWallet(); - const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; + const walletName = useSelector((state) => state.wallet.walletName); + const walletMode = getActiveWalletMode({ selectedWallet: walletName, isEvmNetwork: IS_EVM_NETWORK }); + const address = getActiveWalletAddress({ mode: walletMode, evmAddress, cosmosAddress }); useEffect(() => { - dispatch(setAddress({ address: address || '' })); + dispatch(setAddress({ address })); dispatch(setConnected({ status: Boolean(address) })); - }, [address, dispatch]) + }, [address, dispatch]); - if (IS_EVM_NETWORK) return null; + if (IS_EVM_NETWORK) return ; return (
@@ -34,88 +205,80 @@ export function WalletModalComponent() { export function ConnectWallet() { const dispatch = useDispatch(); - const { - address: cosmosAddress, - disconnect: disconnectCosmos, - openView, - } = useChain(CHAIN_NAME); - const { - address: evmAddress, - connect: connectEvm, - disconnect: disconnectEvm, - isConnecting, - } = useEvmWallet(); - const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; + const { disconnect: disconnectCosmos, openView } = useChain(CHAIN_NAME); + const keplrWallet = useChainWallet(CHAIN_NAME, KEPLR_WALLET_NAME); + const { disconnect: disconnectEvm } = useEvmWallet(); + const { address, walletName } = useWalletConnect(); const handleDisconnect = async () => { - if (IS_EVM_NETWORK) { + if (IS_EVM_NETWORK && walletName === METAMASK_WALLET_NAME) { await disconnectEvm(); + } else if (IS_EVM_NETWORK && walletName === KEPLR_WALLET_NAME) { + await keplrWallet.disconnect(); } else { - disconnectCosmos(); + await disconnectCosmos(); } + dispatch(setWalletName({ walletName: '' })); dispatch(setAddress({ address: '' })); dispatch(setConnected({ status: false })); - } + }; - const handleConnect = async () => { - if (!IS_EVM_NETWORK) { + const handleConnect = () => { + if (IS_EVM_NETWORK) { + dispatch(setModalOpen({ status: true })); + } else { openView(); - return; } - try { - await connectEvm(); - } catch (error) { - toast.error(error instanceof Error ? error.message : 'Unable to connect EVM wallet.', { - position: 'bottom-center', - theme: 'dark', - }); - } - } + }; const handleCopyAddress = () => { void navigator.clipboard.writeText(address); toast('The address has been copied.', { - position: "bottom-center", - theme: "dark", - }) - } + position: 'bottom-center', + theme: 'dark', + }); + }; + + const walletLabel = walletName === METAMASK_WALLET_NAME ? 'MetaMask' : 'Keplr'; return (
{!address ? : <> + {IS_EVM_NETWORK && ( + + )} {formatAddress(address, 5, -4)} - + }
- ) + ); } export function ConnectWalletButton() { - const { address: cosmosAddress, openView } = useChain(CHAIN_NAME); - const { address: evmAddress, connect, isConnecting } = useEvmWallet(); - const address = IS_EVM_NETWORK ? evmAddress : cosmosAddress; + const dispatch = useDispatch(); + const { openView } = useChain(CHAIN_NAME); + const { address } = useWalletConnect(); - const handleConnect = async () => { - if (!IS_EVM_NETWORK) { + const handleConnect = () => { + if (IS_EVM_NETWORK) { + dispatch(setModalOpen({ status: true })); + } else { openView(); - return; - } - try { - await connect(); - } catch (error) { - toast.error(error instanceof Error ? error.message : 'Unable to connect EVM wallet.', { - position: 'bottom-center', - theme: 'dark', - }); } }; @@ -124,12 +287,11 @@ export function ConnectWalletButton() { {!address ? : null }
- ) + ); } diff --git a/apps/web/src/hooks/useAccountInfo.ts b/apps/web/src/hooks/useAccountInfo.ts index 6d9ca12..e3fc7a5 100644 --- a/apps/web/src/hooks/useAccountInfo.ts +++ b/apps/web/src/hooks/useAccountInfo.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; +import { DENOM } from '@/contants/network'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; import { evmBalanceToMicroLume, getEvmBalance } from '@/utils/evm'; @@ -62,7 +62,7 @@ export const getTotalRewards = (accountInfo: AccountInfoData | null) => { } const useAccountInfo = () => { - const { address, getClient } = useWalletConnect(); + const { address, getClient, isEvm } = useWalletConnect(); const [accountInfo, setAccountInfo] = useState({ balances: [], @@ -91,7 +91,7 @@ const useAccountInfo = () => { setError(null); try { - if (IS_EVM_NETWORK) { + if (isEvm) { const balance = await getEvmBalance(address); const _accountInfo: AccountInfoData = { balances: [{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }], @@ -153,12 +153,12 @@ const useAccountInfo = () => { }); } fetchData(); - }, [address]); + }, [address, isEvm]); const handleClaimButtonClick = async () => { setErrorClaim(null); - if (IS_EVM_NETWORK) { - setErrorClaim('Staking rewards require a legacy Cosmos wallet connection.'); + if (isEvm) { + setErrorClaim('Staking rewards require a Keplr wallet connection.'); return; } if (!claimInfo.senderAddress) { diff --git a/apps/web/src/hooks/useDelegate.ts b/apps/web/src/hooks/useDelegate.ts index 5fa8415..4c104a8 100644 --- a/apps/web/src/hooks/useDelegate.ts +++ b/apps/web/src/hooks/useDelegate.ts @@ -5,7 +5,7 @@ import { import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM, IS_EVM_NETWORK } from '@/contants/network'; +import { DENOM } from '@/contants/network'; import { extractValidNumber } from '@/utils/helpers'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contants'; import { @@ -19,7 +19,7 @@ interface UseDepositOptions { } const useDelegate = (options: UseDepositOptions = {}) => { - const { address, getClient } = useWalletConnect(); + const { address, getClient, isEvm } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [optionsAdvanced, setOptionsAdvanced] = useState({ senderAddress: address, @@ -39,7 +39,7 @@ const useDelegate = (options: UseDepositOptions = {}) => { const [selectedModal, setSelectedModal] = useState(''); const fetchValidator = async () => { - if (IS_EVM_NETWORK) { + if (isEvm) { setValidators([]); setTotalValidators('0'); return; @@ -57,7 +57,7 @@ const useDelegate = (options: UseDepositOptions = {}) => { useEffect(() => { fetchValidator(); - }, []); + }, [isEvm]); useEffect(() => { if (options?.customMemo) { @@ -106,8 +106,8 @@ const useDelegate = (options: UseDepositOptions = {}) => { const handleSendClick = async () => { setError(''); setTransactionHash(''); - if (IS_EVM_NETWORK) { - setError('Staking requires a legacy Cosmos wallet connection.'); + if (isEvm) { + setError('Staking requires a Keplr wallet connection.'); return; } if (!optionsAdvanced?.amount || Number(optionsAdvanced.amount) <= 0) { diff --git a/apps/web/src/hooks/useSend.ts b/apps/web/src/hooks/useSend.ts index 84a26ba..3cbef3d 100644 --- a/apps/web/src/hooks/useSend.ts +++ b/apps/web/src/hooks/useSend.ts @@ -3,7 +3,7 @@ import { MsgSend, } from 'cosmjs-types/cosmos/bank/v1beta1/tx'; import useWalletConnect from '@/hooks/useWalletConnect'; -import { DENOM, EVM_CHAIN_ID, IS_EVM_NETWORK } from '@/contants/network'; +import { DENOM, EVM_CHAIN_ID } from '@/contants/network'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contants'; import { Coin } from '@/hooks/useAccountInfo'; import { extractValidNumber } from '@/utils/helpers'; @@ -25,6 +25,7 @@ const useSend = (options: UseDepositOptions = {}) => { address, getClient, isConnected, + isEvm, evmProvider, ensureEvmNetwork, } = useWalletConnect(); @@ -50,7 +51,7 @@ const useSend = (options: UseDepositOptions = {}) => { return; } void queryBalances(); - }, [address, isConnected]); + }, [address, isConnected, isEvm]); useEffect(() => { setOptionsAdvanced((current) => ({ @@ -107,17 +108,17 @@ const useSend = (options: UseDepositOptions = {}) => { setError('Please enter sender.'); return } - if (!IS_EVM_NETWORK && !optionsAdvanced.fees) { + if (!isEvm && !optionsAdvanced.fees) { setError('Please enter fee.'); return } - if (!IS_EVM_NETWORK && !optionsAdvanced.gas) { + if (!isEvm && !optionsAdvanced.gas) { setError('Please enter gas.'); return } setLoading(true); try { - if (IS_EVM_NETWORK) { + if (isEvm) { if (!isEvmAddress(optionsAdvanced.recipient)) { throw new Error('Enter a valid EVM recipient address.'); } @@ -189,7 +190,7 @@ const useSend = (options: UseDepositOptions = {}) => { const queryBalances = async (): Promise => { try { - if (IS_EVM_NETWORK) { + if (isEvm) { const balance = await getEvmBalance(address); setBalances([{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }]); setSelectedDenom(DENOM); diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index a0df92d..b76fe15 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -5,11 +5,12 @@ import * as instance from '@/utils/api'; import { DENOM } from '@/contants/network'; import { useSelector, useDispatch } from '@/redux/hooks'; import { isNumber } from '@/utils/helpers'; +import { canQueryCosmosAccountData } from '@/utils/cosmos-transactions'; import { setCurrentTab, setValidatorTab, setSubTab } from '@/redux/app.slice'; import { IValidator } from '@/types/validator'; import { TUnbondingDelegation } from '@/types'; -const useStaking = (address = '') => { +const useStaking = (address = '', isEvm = false) => { const dispatch = useDispatch(); const { currentTab, validatorTab, subTab } = useSelector((state) => state.app); const [isLoading, setLoading] = useState(false); @@ -84,15 +85,25 @@ const useStaking = (address = '') => { } const fetchRewards = async () => { + if (!canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { + setRewards([]); + return; + } try { const { data } = await instance.get(`/cosmos/distribution/v1beta1/delegators/${address}/rewards`); setRewards(data.rewards); - } catch (error) { - console.error(error instanceof Error ? error.message : 'An unknown error occurred.'); + } catch { + setRewards([]); } } const fetchActivities = useCallback(async () => { + if (!canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { + setActivities([]); + setActivitiesLoading(false); + setActivitiesError(''); + return; + } setActivitiesLoading(true); setActivitiesError(''); try { @@ -102,9 +113,15 @@ const useStaking = (address = '') => { setActivitiesError(error instanceof Error ? error.message : 'An unknown error occurred.'); } setActivitiesLoading(false); - }, []) + }, [address, isEvm]) const fetchUnbondingDelegations = useCallback(async () => { + if (!canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { + setUnbondingDelegations([]); + setUnbondingDelegationsLoading(false); + setUnbondingDelegationsError(''); + return; + } setUnbondingDelegationsLoading(true); setUnbondingDelegationsError(''); try { @@ -140,7 +157,7 @@ const useStaking = (address = '') => { setUnbondingDelegationsError(error instanceof Error ? error.message : 'An unknown error occurred.'); } setUnbondingDelegationsLoading(false); - }, []); + }, [address, isEvm]); const fetchDataForAPR = async () => { setAPRLoading(true); @@ -192,13 +209,21 @@ const useStaking = (address = '') => { }, [validatorTab]); useEffect(() => { - if (address) { + if (canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { if (validatorTab === 'my') { handleFetchDataForSubTab(subTab); } fetchRewards(); + } else { + setRewards([]); + setActivities([]); + setActivitiesLoading(false); + setActivitiesError(''); + setUnbondingDelegations([]); + setUnbondingDelegationsLoading(false); + setUnbondingDelegationsError(''); } - }, [address, validatorTab, subTab]); + }, [address, isEvm, validatorTab, subTab]); const handleTabChange = (tab: string) => { dispatch(setCurrentTab({ diff --git a/apps/web/src/hooks/useTransaction.ts b/apps/web/src/hooks/useTransaction.ts index 39f936a..1bc438f 100644 --- a/apps/web/src/hooks/useTransaction.ts +++ b/apps/web/src/hooks/useTransaction.ts @@ -4,7 +4,6 @@ import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; import { TLog, TLogEvent, TMessage, TOption, TSignerInfos, TFee } from '@/hooks/useRecentActivity'; import { Coin } from '@/hooks/useAccountInfo'; -import { IS_EVM_NETWORK } from '@/contants/network'; const LIMIT = 20; @@ -43,14 +42,14 @@ export interface ITransaction { } const useTransaction = () => { - const { address } = useWalletConnect(); + const { address, isEvm } = useWalletConnect(); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(''); const [transactions, setTransactions] = useState([]); const [totalTransactions, setTotalTransactions] = useState(0); const fetchTransactions = async (offset = 0) => { - if (IS_EVM_NETWORK) { + if (isEvm) { setTransactions([]); setTotalTransactions(0); setError(''); @@ -75,7 +74,7 @@ const useTransaction = () => { if (address) { fetchTransactions(); } - }, [address]); + }, [address, isEvm]); const handlePageClick = ({ selected }: { selected: number }) => { const offset = selected * LIMIT; diff --git a/apps/web/src/hooks/useWalletConnect.ts b/apps/web/src/hooks/useWalletConnect.ts index 68a0bb6..3506442 100644 --- a/apps/web/src/hooks/useWalletConnect.ts +++ b/apps/web/src/hooks/useWalletConnect.ts @@ -10,23 +10,35 @@ import { } from '@/contants/network'; import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; import { canWalletSignCosmosTransactions } from '@/utils/cosmos-transactions'; +import { getActiveWalletAddress, getActiveWalletMode } from '@/utils/wallet-selection'; const useWalletConnect = () => { const { chain, wallet, address: cosmosAddress } = useChain(CHAIN_NAME); const evmWallet = useEvmWallet(); const { walletName, isModalOpen } = useSelector((state) => state.wallet); - const address = IS_EVM_NETWORK ? evmWallet.address : cosmosAddress || ''; + const walletMode = getActiveWalletMode({ + selectedWallet: walletName, + isEvmNetwork: IS_EVM_NETWORK, + }); + const address = getActiveWalletAddress({ + mode: walletMode, + evmAddress: evmWallet.address, + cosmosAddress, + }); const isConnected = Boolean(address); // Phase 2 will source this from the MetaMask Cosmos signer once it is implemented. const hasEvmCosmosSigner = false; const canSignCosmosTransactions = canWalletSignCosmosTransactions({ - isEvmNetwork: IS_EVM_NETWORK, + isEvmNetwork: walletMode === 'evm', chainEip712Enabled: COSMOS_EIP712_ENABLED, hasEvmCosmosSigner, }); const getClient = async () => { - if (IS_EVM_NETWORK) { + if (walletMode === 'none') { + throw new Error('Please connect wallet before using'); + } + if (walletMode === 'evm') { if (!canSignCosmosTransactions) { throw new Error('Cosmos transactions are temporarily unavailable with MetaMask on this network.'); } @@ -47,8 +59,11 @@ const useWalletConnect = () => { } const getOfflineSigner = async () => { - if (IS_EVM_NETWORK) { - throw new Error('Cosmos signing is unavailable while using an EVM network profile.'); + if (walletMode === 'none') { + throw new Error('Please connect wallet before using'); + } + if (walletMode === 'evm') { + throw new Error('Cosmos signing is unavailable while using MetaMask.'); } if (!wallet || !chain) { throw new Error('Please connect wallet before using'); @@ -67,8 +82,9 @@ const useWalletConnect = () => { isConnected, address, walletName, + walletMode, canSignCosmosTransactions, - isEvm: IS_EVM_NETWORK, + isEvm: walletMode === 'evm', evmProvider: evmWallet.provider, ensureEvmNetwork: evmWallet.ensureNetwork, getClient, diff --git a/apps/web/src/types/window.d.ts b/apps/web/src/types/window.d.ts index 3454e30..388876b 100644 --- a/apps/web/src/types/window.d.ts +++ b/apps/web/src/types/window.d.ts @@ -13,6 +13,8 @@ interface Leap { } export interface Eip1193Provider { + isMetaMask?: boolean; + providers?: Eip1193Provider[]; request(args: { method: string; params?: unknown[] | object }): Promise; on?(event: string, listener: (...args: unknown[]) => void): void; removeListener?(event: string, listener: (...args: unknown[]) => void): void; diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts index 900c36f..0649139 100644 --- a/apps/web/src/utils/evm.test.ts +++ b/apps/web/src/utils/evm.test.ts @@ -12,6 +12,7 @@ import { evmBalanceToMicroLume, getEvmAccountForChain, getEvmBalance, + getMetaMaskProvider, isEvmAddress, parseEvmAmount, requestEvmRpc, @@ -29,6 +30,39 @@ const createProvider = (accounts = [ADDRESS], chainId = toHexChainId(CHAIN_ID)) }), }) as unknown as Eip1193Provider; +describe('MetaMask provider selection', () => { + it('finds MetaMask in a multi-provider browser', () => { + const keplrProvider = { request: vi.fn(), isMetaMask: false } as unknown as Eip1193Provider; + const metaMaskProvider = { request: vi.fn(), isMetaMask: true } as unknown as Eip1193Provider; + const aggregateProvider = { + request: vi.fn(), + providers: [keplrProvider, metaMaskProvider], + } as unknown as Eip1193Provider; + + expect(getMetaMaskProvider(aggregateProvider)).toBe(metaMaskProvider); + }); + + it('accepts a direct MetaMask provider and rejects other injected providers', () => { + const metaMaskProvider = { request: vi.fn(), isMetaMask: true } as unknown as Eip1193Provider; + const otherProvider = { request: vi.fn() } as unknown as Eip1193Provider; + + expect(getMetaMaskProvider(metaMaskProvider)).toBe(metaMaskProvider); + expect(getMetaMaskProvider(otherProvider)).toBeNull(); + expect(getMetaMaskProvider()).toBeNull(); + }); + + it('falls back to an aggregate provider that identifies itself as MetaMask', () => { + const otherProvider = { request: vi.fn() } as unknown as Eip1193Provider; + const aggregateMetaMask = { + request: vi.fn(), + isMetaMask: true, + providers: [otherProvider], + } as unknown as Eip1193Provider; + + expect(getMetaMaskProvider(aggregateMetaMask)).toBe(aggregateMetaMask); + }); +}); + afterEach(() => { vi.unstubAllGlobals(); }); diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index c7afb89..a4af091 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -12,6 +12,15 @@ interface EvmRpcResponse { }; } +export const getMetaMaskProvider = (provider?: Eip1193Provider | null) => { + if (!provider) return null; + if (provider.providers?.length) { + return provider.providers.find((candidate) => candidate.isMetaMask) + || (provider.isMetaMask ? provider : null); + } + return provider.isMetaMask ? provider : null; +}; + export const isEvmAddress = (value: string) => /^0x[0-9a-fA-F]{40}$/.test(value); export const toHexChainId = (chainId: number) => `0x${chainId.toString(16)}`; diff --git a/apps/web/src/utils/wallet-selection.test.ts b/apps/web/src/utils/wallet-selection.test.ts new file mode 100644 index 0000000..8ec199e --- /dev/null +++ b/apps/web/src/utils/wallet-selection.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; + +import { + getActiveWalletAddress, + getActiveWalletMode, + KEPLR_WALLET_NAME, + METAMASK_WALLET_NAME, +} from './wallet-selection'; + +describe('active wallet selection', () => { + it('requires an explicit selection on EVM-enabled network profiles', () => { + const mode = getActiveWalletMode({ selectedWallet: '', isEvmNetwork: true }); + expect(mode).toBe('none'); + expect(getActiveWalletAddress({ + mode, + evmAddress: '0xabc', + cosmosAddress: 'lumera1abc', + })).toBe(''); + }); + + it('uses the MetaMask EVM address when MetaMask is selected', () => { + const mode = getActiveWalletMode({ + selectedWallet: METAMASK_WALLET_NAME, + isEvmNetwork: true, + }); + expect(mode).toBe('evm'); + expect(getActiveWalletAddress({ mode, evmAddress: '0xabc', cosmosAddress: 'lumera1abc' })) + .toBe('0xabc'); + }); + + it('uses the Keplr Cosmos address when Keplr is selected', () => { + const mode = getActiveWalletMode({ + selectedWallet: KEPLR_WALLET_NAME, + isEvmNetwork: true, + }); + expect(mode).toBe('cosmos'); + expect(getActiveWalletAddress({ mode, evmAddress: '0xabc', cosmosAddress: 'lumera1abc' })) + .toBe('lumera1abc'); + }); + + it('keeps legacy Cosmos wallet behavior on non-EVM profiles', () => { + const mode = getActiveWalletMode({ + selectedWallet: METAMASK_WALLET_NAME, + isEvmNetwork: false, + }); + expect(mode).toBe('cosmos'); + expect(getActiveWalletAddress({ mode, evmAddress: '0xabc', cosmosAddress: 'lumera1abc' })) + .toBe('lumera1abc'); + }); +}); diff --git a/apps/web/src/utils/wallet-selection.ts b/apps/web/src/utils/wallet-selection.ts new file mode 100644 index 0000000..9f8ebe7 --- /dev/null +++ b/apps/web/src/utils/wallet-selection.ts @@ -0,0 +1,35 @@ +export const METAMASK_WALLET_NAME = 'metamask'; +export const KEPLR_WALLET_NAME = 'keplr-extension'; + +export type ActiveWalletMode = 'none' | 'evm' | 'cosmos'; + +interface ActiveWalletModeOptions { + selectedWallet: string; + isEvmNetwork: boolean; +} + +export const getActiveWalletMode = ({ + selectedWallet, + isEvmNetwork, +}: ActiveWalletModeOptions): ActiveWalletMode => { + if (!isEvmNetwork) return 'cosmos'; + if (selectedWallet === METAMASK_WALLET_NAME) return 'evm'; + if (selectedWallet === KEPLR_WALLET_NAME) return 'cosmos'; + return 'none'; +}; + +interface ActiveWalletAddressOptions { + mode: ActiveWalletMode; + evmAddress?: string; + cosmosAddress?: string; +} + +export const getActiveWalletAddress = ({ + mode, + evmAddress, + cosmosAddress, +}: ActiveWalletAddressOptions) => { + if (mode === 'evm') return evmAddress || ''; + if (mode === 'cosmos') return cosmosAddress || ''; + return ''; +}; From 4c1f81b207d8d18aace9767590a5e0c4502bffa2 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 16:53:10 -0400 Subject: [PATCH 08/45] upgrade CosmJS for EVM account keys --- apps/web/package.json | 10 +- apps/web/src/utils/cosmjs-evm-account.test.ts | 39 ++++ package.json | 9 + packages/ui/package.json | 4 +- pnpm-lock.yaml | 194 ++++++++++++++++-- 5 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/utils/cosmjs-evm-account.test.ts diff --git a/apps/web/package.json b/apps/web/package.json index 8e5e96d..4d6105f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,10 +10,10 @@ "lint": "next lint" }, "dependencies": { - "@cosmjs/crypto": "^0.36.1", - "@cosmjs/encoding": "^0.36.1", - "@cosmjs/proto-signing": "^0.36.1", - "@cosmjs/stargate": "^0.36.1", + "@cosmjs/crypto": "^0.39.0", + "@cosmjs/encoding": "^0.39.0", + "@cosmjs/proto-signing": "^0.39.0", + "@cosmjs/stargate": "^0.39.0", "@interchain-kit/core": "^0.3.43", "@interchain-kit/cosmostation-extension": "^0.3.55", "@interchain-kit/keplr-extension": "^0.3.55", @@ -29,7 +29,7 @@ "@tamagui/lucide-icons": "^1.132.20", "axios": "^1.12.0", "chain-registry": "^2.0.42", - "cosmjs-types": "^0.10.1", + "cosmjs-types": "^0.11.0", "dayjs": "^1.11.18", "jszip": "^3.10.1", "lucide-react": "^0.545.0", diff --git a/apps/web/src/utils/cosmjs-evm-account.test.ts b/apps/web/src/utils/cosmjs-evm-account.test.ts new file mode 100644 index 0000000..3635768 --- /dev/null +++ b/apps/web/src/utils/cosmjs-evm-account.test.ts @@ -0,0 +1,39 @@ +import { accountFromAny } from '@cosmjs/stargate'; +import { encodePubkey } from '@cosmjs/proto-signing'; +import { describe, expect, it } from 'vitest'; +import { BaseAccount } from 'cosmjs-types/cosmos/auth/v1beta1/auth'; +import { PubKey } from 'cosmjs-types/cosmos/crypto/secp256k1/keys'; +import { Any } from 'cosmjs-types/google/protobuf/any'; + +const EVM_PUBKEY_TYPE = '/cosmos.evm.crypto.v1.ethsecp256k1.PubKey'; +const ADDRESS = 'lumera17rxl2anj94mppyqunkch08p3h8a32zcj7p633x'; +const PUBKEY = Uint8Array.from([ + 3, 89, 169, 171, 153, 132, 128, 104, 177, 142, 142, + 247, 8, 146, 82, 195, 85, 149, 78, 69, 222, 15, 42, + 3, 102, 203, 69, 227, 80, 211, 114, 170, 37, +]); + +describe('CosmJS Lumera EVM account support', () => { + it('round-trips the Lumera EVM public-key type used by existing accounts', () => { + const input = Any.fromPartial({ + typeUrl: '/cosmos.auth.v1beta1.BaseAccount', + value: BaseAccount.encode(BaseAccount.fromPartial({ + address: ADDRESS, + pubKey: Any.fromPartial({ + typeUrl: EVM_PUBKEY_TYPE, + value: PubKey.encode(PubKey.fromPartial({ key: PUBKEY })).finish(), + }), + accountNumber: BigInt(9143), + sequence: BigInt(4), + })).finish(), + }); + + const account = accountFromAny(input); + + expect(account.address).toBe(ADDRESS); + expect(account.accountNumber).toBe(BigInt(9143)); + expect(account.sequence).toBe(4); + expect(account.pubkey?.type).toBe('os/PubKeyEthSecp256k1'); + expect(encodePubkey(account.pubkey!)).toEqual(BaseAccount.decode(input.value).pubKey); + }); +}); diff --git a/package.json b/package.json index d84a770..71f565b 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,14 @@ "version": "0.1.0", "type": "module", "dependencies": { + }, + "pnpm": { + "packageExtensions": { + "@interchainjs/utils@*": { + "dependencies": { + "@noble/hashes": "^1.8.0" + } + } + } } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 00207cc..3d9880c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,8 +13,8 @@ "module": "./src/index.ts", "types": "./src/index.ts", "dependencies": { - "@cosmjs/encoding": "^0.36.1", - "@cosmjs/proto-signing": "^0.36.1", + "@cosmjs/encoding": "^0.39.0", + "@cosmjs/proto-signing": "^0.39.0", "@headlessui/react": "^2.2.9", "@react-jvectormap/core": "^1.0.4", "@react-jvectormap/world": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 364b77e..72d04f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,8 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +packageExtensionsChecksum: dfe194370da362b6b6629d88d617e24e + importers: .: @@ -100,17 +102,17 @@ importers: apps/web: dependencies: '@cosmjs/crypto': - specifier: ^0.36.1 - version: 0.36.1 + specifier: ^0.39.0 + version: 0.39.0 '@cosmjs/encoding': - specifier: ^0.36.1 - version: 0.36.1 + specifier: ^0.39.0 + version: 0.39.0 '@cosmjs/proto-signing': - specifier: ^0.36.1 - version: 0.36.1 + specifier: ^0.39.0 + version: 0.39.0 '@cosmjs/stargate': - specifier: ^0.36.1 - version: 0.36.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: ^0.39.0 + version: 0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@interchain-kit/core': specifier: ^0.3.43 version: 0.3.43(@chain-registry/v2-types@0.53.146)(@chain-registry/v2@1.71.237)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -157,8 +159,8 @@ importers: specifier: ^2.0.42 version: 2.0.42 cosmjs-types: - specifier: ^0.10.1 - version: 0.10.1 + specifier: ^0.11.0 + version: 0.11.0 dayjs: specifier: ^1.11.18 version: 1.11.18 @@ -266,11 +268,11 @@ importers: packages/ui: dependencies: '@cosmjs/encoding': - specifier: ^0.36.1 - version: 0.36.1 + specifier: ^0.39.0 + version: 0.39.0 '@cosmjs/proto-signing': - specifier: ^0.36.1 - version: 0.36.1 + specifier: ^0.39.0 + version: 0.39.0 '@headlessui/react': specifier: ^2.2.9 version: 2.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -887,51 +889,84 @@ packages: '@cosmjs/amino@0.37.0': resolution: {integrity: sha512-Qjg3vx0V907ICYr9wTFuF55+P2F7FuVuVdV8WsBJC6G9ekS5nbi7Z4+YsoJf3JEp5WApVgcX3HmAWZhziayxzw==} + '@cosmjs/amino@0.39.0': + resolution: {integrity: sha512-56WqsZ4xbdJE4KWJTPbw7hUzePflRxRLuxoBlUsa8qIiFumpzZRW0Ycb7IloEF8wAmdgsdfrG1hSJSBlqdFE7A==} + '@cosmjs/crypto@0.36.1': resolution: {integrity: sha512-7vx9rZAuboyMxs9zv3hZhNA0JAFMpaW+fFgRDQzZzfIVj0z4h8RW4dJu0xx5cv3KBQXmoRUzWklpnFuMQYiGdg==} '@cosmjs/crypto@0.37.0': resolution: {integrity: sha512-rjnU7SEgNTUQAUotG686m7ahYSWgHh3J6n2JXoWoHJz0uVv4o4P+pbAFklyQ1PcPIR7u6LezCKDB5tP5Y5PeYQ==} + '@cosmjs/crypto@0.39.0': + resolution: {integrity: sha512-ATRhSXN8w3fvUkj9xzLHwvzylDvvn4f3cC1CQwhQc2OxyzpEEFACS3wHS6iwdJJS99acV8dq+oVOYflUqI0brQ==} + '@cosmjs/encoding@0.36.1': resolution: {integrity: sha512-i5dTiOdSAfyU76lmOm0+VLEIDEtmINtpOeAuzzBJP1er5fDJvpBysgY9MefVwXNBY/P46W10Uta9zQc98ehBXg==} '@cosmjs/encoding@0.37.0': resolution: {integrity: sha512-xtdC0w+iVFOrod9a5RLJULUECv+6AvZr5FkD8AFr2vD853n7Z89/AVuEiJzd4GdUwlPzxcaamhAtmI+IB9DYvg==} + '@cosmjs/encoding@0.39.0': + resolution: {integrity: sha512-+poEaeM8YjGNVtrHLQNWqkhEeDxapjrdpnPCT+JCRh8YNbeHEdftzZLCH5VCBSOtvo7PF0gK1B7sbQbBl6q5pQ==} + '@cosmjs/json-rpc@0.36.1': resolution: {integrity: sha512-7vpTw3r3vVaiMY9L0hDh2QP5RnmSim00DhQuqtrPz/qd740Q2xtxhO7UxnWOzL85gG1Tyr9KshE46QIhZcTU/Q==} + '@cosmjs/json-rpc@0.39.0': + resolution: {integrity: sha512-slyo76IYkTuSxrzxvF1s1ScFPIGnW7PbNSx8lr++NzdyPluqe053+cSqsxLUioXb5qI9Nxny2fWxOK8isSn+cg==} + '@cosmjs/math@0.36.1': resolution: {integrity: sha512-ML5X5iupmTUV6bik+YEShrmK49ikB8jMQfgzdQV7qeBMN2Rc4ijd1/sFya5AiyJzxtBlYe7yv0i5NyNzZPqKpQ==} '@cosmjs/math@0.37.0': resolution: {integrity: sha512-FI+Tq8mhW0tuDawRvPdyX3K7qDZD2v1keRhiK/zHisvtQVzqoRRoOS1g5P9Pc7gWLQ1jPS15gDMYBu5+UYJ1+g==} + '@cosmjs/math@0.39.0': + resolution: {integrity: sha512-FSLy/oDF+BtOP/J60RsjL5W4MCKiCfBjSoeV5xj6qg2g8N884Ue853iuWanjqGkQJwkXCp+JbeK8Mv9j6AaYHw==} + '@cosmjs/proto-signing@0.36.1': resolution: {integrity: sha512-aWGJW4gwVEf/HaVe7Bf8K3Vc7J8vOMvChDhUr2FT8NG7REJ2CztJ0jcYgaJLRzoEj6rLJoN97eAf2hUjFQxWTw==} '@cosmjs/proto-signing@0.37.0': resolution: {integrity: sha512-Ir/nPyrKIlFKsNAfUPVAfgNj0NBEHyryzIPJbCebTK9fBuAyQ3LXn+QfiFYR58/kVp63gEdIlADMQGGBS5ZF+w==} + '@cosmjs/proto-signing@0.39.0': + resolution: {integrity: sha512-jthQ2PA092fvrbFaZRRKuHV7awOsayjDLIBACaGIblFj9yJCcrvhFp+8YV87ia990DjKmWJc3sstHvQZ3yK/EQ==} + '@cosmjs/socket@0.36.1': resolution: {integrity: sha512-p6KgnQmlz1MYJjWi66xyLLYk0NqXFQ/OE5LlC6+Pj6tv7sxjhqsQiQYwfgKJjSwTbduXv2vOHrqEnKZ892E5Xw==} + '@cosmjs/socket@0.39.0': + resolution: {integrity: sha512-mvA+/ycMn7dnjjdooSPq516poFpSARRXhFhKjCXxGUmBZA+74vLDLbtD6VGzHd08A2JSZBSiAhVOZpjYqgV78A==} + '@cosmjs/stargate@0.36.1': resolution: {integrity: sha512-MCvZCginWI/2crnf7xYNsVNPzXLRgjuXUYF7t00JSOeif3MBM3YUHKQjjMsFNVys4eEDkfbptfn9bDghFy1nag==} + '@cosmjs/stargate@0.39.0': + resolution: {integrity: sha512-dQtucU9czF2NUUbEKs19PMfv+EusDFKWHMVK6Hinytmpq9sdk5JUIln+g/8k+UjKcV4HPv6S0AywbhE+tl84kQ==} + '@cosmjs/stream@0.36.1': resolution: {integrity: sha512-q8TrUt7iiWGf6N5x7vWGqWIhtgPzTMkZlM73vkE4F3rcWbybJHgNu7inOhwnuJpR+gFZd9JNq+jAtjsbptZGag==} + '@cosmjs/stream@0.39.0': + resolution: {integrity: sha512-h56it+ku5F6nlO94VyVAyz3pQqYsGRJ8v+5Y5erXYlQ0Qmr+rLrlSMCiViFEMK9INRSilAypdI5hKPCzotiqRg==} + '@cosmjs/tendermint-rpc@0.36.1': resolution: {integrity: sha512-9EN84YVqdgDLB97xFq+aqsolbMCyKwg6NcB/CR1s3DrquzJEx9/ZNEBJak/VVfb+croDFgvQd4X7CzqkW4N2ag==} + '@cosmjs/tendermint-rpc@0.39.0': + resolution: {integrity: sha512-KowP/6P6Tx+OnGoimYnxuSBIfhB+vWqWER2pZpEIOHId8ZfcjQ9JS2F+Ah+WmZ9D+bKgn4pJ5ImAS13TeShIEw==} + '@cosmjs/utils@0.36.1': resolution: {integrity: sha512-kjdDD6t7dMLRUtbRCRskP7sNpyNf6cxVgaM2z7n64e6upXwE+bsoKfKrG+iY2ABT57oH6UxJYgcB+7ACmPxZCg==} '@cosmjs/utils@0.37.0': resolution: {integrity: sha512-j46yZg+cLBpANGc5sCxtQHJgPGBt5zSKlU+KX9FlDS6JU6q2vZYCpKgucFFpjekEiMagU1eu8cDSCRjTthEM6w==} + '@cosmjs/utils@0.39.0': + resolution: {integrity: sha512-h7fy7Tbcl9v8ABntp8+kqw2VmUus2HbnRJFyzTkM7byRktLtECHYNMsztwyl1rdHkzc8Nc2xs6K/56d7a+75aw==} + '@egjs/hammerjs@2.0.17': resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} @@ -1979,6 +2014,10 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} + '@noble/ciphers@2.3.0': + resolution: {integrity: sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw==} + engines: {node: '>= 20.19.0'} + '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} @@ -2006,6 +2045,10 @@ packages: resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@2.3.0': + resolution: {integrity: sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.3.2': resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} @@ -2022,6 +2065,10 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2891,6 +2938,9 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@scure/bip39@2.3.0': + resolution: {integrity: sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==} + '@scure/starknet@1.1.0': resolution: {integrity: sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==} @@ -5030,6 +5080,10 @@ packages: cosmjs-types@0.10.1: resolution: {integrity: sha512-CENXb4O5GN+VyB68HYXFT2SOhv126Z59631rZC56m8uMWa6/cSlFeai8BwZGT1NMepw0Ecf+U8XSOnBzZUWh9Q==} + cosmjs-types@0.11.0: + resolution: {integrity: sha512-kDSkgHpRTrg1413jCNehT3P21+EBxZWFMBr9JEzVfmPiNdtuwAoLAkCYo7c7i/pTakAwyHsXbxOg8kkD+AN33w==} + engines: {node: '>=20.19'} + cosmjs-types@0.9.0: resolution: {integrity: sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==} @@ -7744,6 +7798,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readonly-date-esm@2.0.0: + resolution: {integrity: sha512-adlyzz144ofU22kjnnRIN0HPPqIbc5IZvMmMuVtEgMY4mKgNyKqOVb4Fa2GUzE2By4TEPOYoGqDoKRyqmeEuPQ==} + readonly-date@1.0.0: resolution: {integrity: sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==} @@ -9732,6 +9789,13 @@ snapshots: '@cosmjs/math': 0.37.0 '@cosmjs/utils': 0.37.0 + '@cosmjs/amino@0.39.0': + dependencies: + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + '@cosmjs/crypto@0.36.1': dependencies: '@cosmjs/encoding': 0.36.1 @@ -9753,6 +9817,16 @@ snapshots: '@scure/bip39': 1.6.0 hash-wasm: 4.12.0 + '@cosmjs/crypto@0.39.0': + dependencies: + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + '@noble/ciphers': 2.3.0 + '@noble/curves': 2.3.0 + '@noble/hashes': 2.3.0 + '@scure/bip39': 2.3.0 + '@cosmjs/encoding@0.36.1': dependencies: base64-js: 1.5.1 @@ -9765,15 +9839,28 @@ snapshots: base64-js: 1.5.1 readonly-date: 1.0.0 + '@cosmjs/encoding@0.39.0': + dependencies: + '@scure/base': 2.0.0 + base64-js: 1.5.1 + readonly-date-esm: 2.0.0 + '@cosmjs/json-rpc@0.36.1': dependencies: '@cosmjs/stream': 0.36.1 xstream: 11.14.0 + '@cosmjs/json-rpc@0.39.0': + dependencies: + '@cosmjs/stream': 0.39.0 + xstream: 11.14.0 + '@cosmjs/math@0.36.1': {} '@cosmjs/math@0.37.0': {} + '@cosmjs/math@0.39.0': {} + '@cosmjs/proto-signing@0.36.1': dependencies: '@cosmjs/amino': 0.36.1 @@ -9792,6 +9879,15 @@ snapshots: '@cosmjs/utils': 0.37.0 cosmjs-types: 0.10.1 + '@cosmjs/proto-signing@0.39.0': + dependencies: + '@cosmjs/amino': 0.39.0 + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/utils': 0.39.0 + cosmjs-types: 0.11.0 + '@cosmjs/socket@0.36.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/stream': 0.36.1 @@ -9802,6 +9898,16 @@ snapshots: - bufferutil - utf-8-validate + '@cosmjs/socket@0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/stream': 0.39.0 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@cosmjs/stargate@0.36.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/amino': 0.36.1 @@ -9816,10 +9922,28 @@ snapshots: - bufferutil - utf-8-validate + '@cosmjs/stargate@0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/amino': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/proto-signing': 0.39.0 + '@cosmjs/stream': 0.39.0 + '@cosmjs/tendermint-rpc': 0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/utils': 0.39.0 + cosmjs-types: 0.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@cosmjs/stream@0.36.1': dependencies: xstream: 11.14.0 + '@cosmjs/stream@0.39.0': + dependencies: + xstream: 11.14.0 + '@cosmjs/tendermint-rpc@0.36.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/crypto': 0.36.1 @@ -9835,10 +9959,27 @@ snapshots: - bufferutil - utf-8-validate + '@cosmjs/tendermint-rpc@0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/crypto': 0.39.0 + '@cosmjs/encoding': 0.39.0 + '@cosmjs/json-rpc': 0.39.0 + '@cosmjs/math': 0.39.0 + '@cosmjs/socket': 0.39.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/stream': 0.39.0 + '@cosmjs/utils': 0.39.0 + readonly-date-esm: 2.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@cosmjs/utils@0.36.1': {} '@cosmjs/utils@0.37.0': {} + '@cosmjs/utils@0.39.0': {} + '@egjs/hammerjs@2.0.17': dependencies: '@types/hammerjs': 2.0.46 @@ -11301,6 +11442,7 @@ snapshots: '@chain-registry/v2': 1.71.237 '@chain-registry/v2-types': 0.53.146 '@interchainjs/types': 1.11.11 + '@noble/hashes': 1.8.0 bech32: 2.0.0 decimal.js: 10.6.0 @@ -11309,6 +11451,7 @@ snapshots: '@chain-registry/v2': 1.71.237 '@chain-registry/v2-types': 0.53.146 '@interchainjs/types': 1.17.3 + '@noble/hashes': 1.8.0 bech32: 2.0.0 decimal.js: 10.6.0 @@ -11317,6 +11460,7 @@ snapshots: '@chain-registry/v2': 1.71.237 '@chain-registry/v2-types': 0.53.146 '@interchainjs/types': 1.18.0 + '@noble/hashes': 1.8.0 bech32: 2.0.0 decimal.js: 10.6.0 @@ -11648,6 +11792,8 @@ snapshots: '@noble/ciphers@1.3.0': {} + '@noble/ciphers@2.3.0': {} + '@noble/curves@1.2.0': dependencies: '@noble/hashes': 1.3.2 @@ -11676,6 +11822,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@2.3.0': + dependencies: + '@noble/hashes': 2.3.0 + '@noble/hashes@1.3.2': {} '@noble/hashes@1.6.0': {} @@ -11684,6 +11834,8 @@ snapshots: '@noble/hashes@1.8.0': {} + '@noble/hashes@2.3.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13964,6 +14116,10 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@scure/bip39@2.3.0': + dependencies: + '@noble/hashes': 2.3.0 + '@scure/starknet@1.1.0': dependencies: '@noble/curves': 1.7.0 @@ -16772,11 +16928,11 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} @@ -18231,6 +18387,8 @@ snapshots: cosmjs-types@0.10.1: {} + cosmjs-types@0.11.0: {} + cosmjs-types@0.9.0: {} create-hash@1.2.0: @@ -21878,6 +22036,8 @@ snapshots: readdirp@4.1.2: {} + readonly-date-esm@2.0.0: {} + readonly-date@1.0.0: {} real-require@0.1.0: {} @@ -23200,7 +23360,7 @@ snapshots: webpack@5.101.0(esbuild@0.25.8): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 From 76316f2415e8abde5743350236c430271ddba81c Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:06:23 -0400 Subject: [PATCH 09/45] match wallet dialog to portal design --- apps/web/public/metamask.svg | 18 ++ .../src/components/ConnectWallet.module.css | 206 +++++++++++++++ apps/web/src/components/ConnectWallet.tsx | 236 +++++++++--------- apps/web/src/utils/wallet-selection.test.ts | 32 +++ apps/web/src/utils/wallet-selection.ts | 22 ++ 5 files changed, 392 insertions(+), 122 deletions(-) create mode 100644 apps/web/public/metamask.svg create mode 100644 apps/web/src/components/ConnectWallet.module.css diff --git a/apps/web/public/metamask.svg b/apps/web/public/metamask.svg new file mode 100644 index 0000000..5203d36 --- /dev/null +++ b/apps/web/public/metamask.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/ConnectWallet.module.css b/apps/web/src/components/ConnectWallet.module.css new file mode 100644 index 0000000..c373c03 --- /dev/null +++ b/apps/web/src/components/ConnectWallet.module.css @@ -0,0 +1,206 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(2, 9, 20, 0.7); + backdrop-filter: blur(2px); +} + +.dialog { + position: relative; + width: min(610px, 100%); + padding: 28px 24px 16px; + border: 1px solid rgba(160, 174, 202, 0.12); + border-radius: 8px; + background: #29344f; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5); +} + +.title { + margin: 0; + color: #b8c0d0; + font-size: 26px; + font-weight: 700; + line-height: 1.25; +} + +.closeButton { + position: absolute; + top: 16px; + right: 16px; + display: grid; + width: 32px; + height: 32px; + place-items: center; + border: 0; + border-radius: 50%; + background: transparent; + color: #b8c0d0; + cursor: pointer; + font-size: 25px; + line-height: 1; +} + +.closeButton:hover:not(:disabled), +.closeButton:focus-visible { + background: rgba(255, 255, 255, 0.08); + color: #fff; + outline: none; +} + +.walletList { + display: grid; + gap: 4px; + margin: 22px 0 0; + padding: 16px 14px; + border-radius: 10px; + background: #0d1727; + list-style: none; +} + +.walletOption { + width: 100%; + display: flex; + min-height: 76px; + align-items: center; + gap: 18px; + padding: 10px 12px; + border: 1px solid transparent; + border-radius: 9px; + background: transparent; + color: #d7dce7; + cursor: pointer; + text-align: left; + transition: background-color 150ms ease, border-color 150ms ease; +} + +.walletOption:hover:not(:disabled), +.walletOption:focus-visible, +.walletOptionSelected { + border-color: rgba(112, 202, 213, 0.2); + background: rgba(255, 255, 255, 0.055); + outline: none; +} + +.walletOption:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +.walletLogo { + width: 50px; + height: 50px; + flex: 0 0 50px; + border-radius: 50%; + object-fit: contain; +} + +.walletName { + flex: 1; + font-size: 20px; + font-weight: 700; +} + +.walletStatus { + margin-left: auto; + color: #9da8bd; + font-size: 12px; +} + +.selectedMark { + display: grid; + width: 20px; + height: 20px; + margin-right: 16px; + place-items: center; + border-radius: 50%; + background: #baf5d2; + color: #15915a; + font-size: 15px; + font-weight: 900; + line-height: 1; +} + +.error { + margin: 12px 4px 0; + color: #ff8f8f; + font-size: 14px; +} + +.connectButton { + width: 100%; + min-height: 60px; + margin-top: 40px; + border: 0; + border-radius: 10px; + background: #10999d; + color: #fff; + cursor: pointer; + font-size: 18px; + font-weight: 800; + text-transform: uppercase; + transition: background-color 150ms ease, opacity 150ms ease; +} + +.connectButton:hover:not(:disabled), +.connectButton:focus-visible { + background: #0dafb4; + outline: 2px solid rgba(112, 225, 230, 0.55); + outline-offset: 2px; +} + +.connectButton:disabled, +.closeButton:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +@media (max-width: 640px) { + .overlay { + align-items: flex-end; + padding: 12px; + } + + .dialog { + padding: 24px 16px 16px; + border-radius: 14px; + } + + .title { + font-size: 23px; + } + + .walletList { + margin-top: 18px; + padding: 10px 8px; + } + + .walletOption { + min-height: 70px; + gap: 14px; + padding: 9px 10px; + } + + .walletLogo { + width: 44px; + height: 44px; + flex-basis: 44px; + } + + .walletName { + font-size: 18px; + } + + .selectedMark { + margin-right: 4px; + } + + .connectButton { + min-height: 54px; + margin-top: 24px; + } +} diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index 2952bcd..6ba12fe 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -1,6 +1,8 @@ 'use client' import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { createPortal } from 'react-dom'; import { Wallet, LogOut } from '@tamagui/lucide-icons'; import { InterchainWalletModal, useChain, useChainWallet } from '@interchain-kit/react'; import { toast } from 'react-toastify'; @@ -19,165 +21,155 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import { getActiveWalletAddress, getActiveWalletMode, + getPreferredWalletSelection, KEPLR_WALLET_NAME, METAMASK_WALLET_NAME, } from '@/utils/wallet-selection'; - -const showWalletError = (error: unknown, fallback: string) => { - toast.error(error instanceof Error ? error.message : fallback, { - position: 'bottom-center', - theme: 'dark', - }); -}; +import styles from './ConnectWallet.module.css'; function WalletChoiceModal() { const dispatch = useDispatch(); - const isModalOpen = useSelector((state) => state.wallet.isModalOpen); + const { isModalOpen, walletName } = useSelector((state) => state.wallet); const evmWallet = useEvmWallet(); const keplrWallet = useChainWallet(CHAIN_NAME, KEPLR_WALLET_NAME); const [isKeplrInstalled, setKeplrInstalled] = useState(false); + const [selectedWallet, setSelectedWallet] = useState(''); const [connectingWallet, setConnectingWallet] = useState(''); + const [walletError, setWalletError] = useState(''); + const isMetaMaskInstalled = Boolean(evmWallet.provider); + + useEffect(() => { + if (!isModalOpen) return; + + const keplrInstalled = Boolean(window.keplr); + setKeplrInstalled(keplrInstalled); + setSelectedWallet(getPreferredWalletSelection({ + currentSelection: walletName, + isKeplrInstalled: keplrInstalled, + isMetaMaskInstalled, + })); + setWalletError(''); + }, [isMetaMaskInstalled, isModalOpen, walletName]); useEffect(() => { - if (isModalOpen) setKeplrInstalled(Boolean(window.keplr)); - }, [isModalOpen]); + if (!isModalOpen) return; + + const previousOverflow = document.body.style.overflow; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && !connectingWallet) { + dispatch(setModalOpen({ status: false })); + } + }; + document.body.style.overflow = 'hidden'; + window.addEventListener('keydown', handleKeyDown); + + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener('keydown', handleKeyDown); + }; + }, [connectingWallet, dispatch, isModalOpen]); - if (!isModalOpen) return null; + if (!isModalOpen || typeof document === 'undefined') return null; const close = () => dispatch(setModalOpen({ status: false })); - const connectMetaMask = async () => { - setConnectingWallet(METAMASK_WALLET_NAME); - try { - await evmWallet.connect(); - dispatch(setWalletName({ walletName: METAMASK_WALLET_NAME })); - close(); - } catch (error) { - showWalletError(error, 'Unable to connect MetaMask.'); - } finally { - setConnectingWallet(''); - } - }; + const connectSelectedWallet = async () => { + if (!selectedWallet) return; - const connectKeplr = async () => { - setConnectingWallet(KEPLR_WALLET_NAME); + setConnectingWallet(selectedWallet); + setWalletError(''); try { - await keplrWallet.connect(); - dispatch(setWalletName({ walletName: KEPLR_WALLET_NAME })); + if (selectedWallet === METAMASK_WALLET_NAME) { + if (!isMetaMaskInstalled) throw new Error('MetaMask was not detected.'); + await evmWallet.connect(); + } else { + if (!isKeplrInstalled) throw new Error('Keplr was not detected.'); + await keplrWallet.connect(); + } + dispatch(setWalletName({ walletName: selectedWallet })); close(); } catch (error) { - showWalletError(error, 'Unable to connect Keplr.'); + setWalletError(error instanceof Error ? error.message : 'Unable to connect wallet.'); } finally { setConnectingWallet(''); } }; - const walletButton = ( - name: string, - description: string, - installed: boolean, - onClick: () => Promise, - walletName: string - ) => ( - - ); + const walletOptions = [ + { + name: 'Keplr', + walletName: KEPLR_WALLET_NAME, + logo: '/keplr.svg', + installed: isKeplrInstalled, + }, + { + name: 'MetaMask', + walletName: METAMASK_WALLET_NAME, + logo: '/metamask.svg', + installed: isMetaMaskInstalled, + }, + ]; - return ( + return createPortal(
{ - if (event.target === event.currentTarget) close(); - }} - style={{ - position: 'fixed', - inset: 0, - zIndex: 1000, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: 20, - background: 'rgba(5, 7, 15, 0.72)', + if (event.target === event.currentTarget && !connectingWallet) close(); }} >
-
-
-

- Choose wallet -

-

- Select how you want to use Lumera Hub. -

-
- -
-
- {walletButton( - 'MetaMask', - 'EVM address and native LUME transfers', - Boolean(evmWallet.provider), - connectMetaMask, - METAMASK_WALLET_NAME - )} - {walletButton( - 'Keplr', - 'Cosmos transfers, staking, and governance', - isKeplrInstalled, - connectKeplr, - KEPLR_WALLET_NAME - )} -
+

Connect Wallet

+ +
    + {walletOptions.map((option) => { + const isSelected = option.walletName === selectedWallet; + return ( +
  • + +
  • + ); + })} +
+ {walletError &&

{walletError}

} +
-
+
, + document.body ); } diff --git a/apps/web/src/utils/wallet-selection.test.ts b/apps/web/src/utils/wallet-selection.test.ts index 8ec199e..115d0a5 100644 --- a/apps/web/src/utils/wallet-selection.test.ts +++ b/apps/web/src/utils/wallet-selection.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { getActiveWalletAddress, getActiveWalletMode, + getPreferredWalletSelection, KEPLR_WALLET_NAME, METAMASK_WALLET_NAME, } from './wallet-selection'; @@ -48,3 +49,34 @@ describe('active wallet selection', () => { .toBe('lumera1abc'); }); }); + +describe('preferred wallet selection', () => { + it('keeps the active installed wallet selected when reopening the dialog', () => { + expect(getPreferredWalletSelection({ + currentSelection: METAMASK_WALLET_NAME, + isKeplrInstalled: true, + isMetaMaskInstalled: true, + })).toBe(METAMASK_WALLET_NAME); + }); + + it('defaults to Keplr when both wallets are installed', () => { + expect(getPreferredWalletSelection({ + currentSelection: '', + isKeplrInstalled: true, + isMetaMaskInstalled: true, + })).toBe(KEPLR_WALLET_NAME); + }); + + it('selects the only installed wallet and leaves none selected otherwise', () => { + expect(getPreferredWalletSelection({ + currentSelection: '', + isKeplrInstalled: false, + isMetaMaskInstalled: true, + })).toBe(METAMASK_WALLET_NAME); + expect(getPreferredWalletSelection({ + currentSelection: '', + isKeplrInstalled: false, + isMetaMaskInstalled: false, + })).toBe(''); + }); +}); diff --git a/apps/web/src/utils/wallet-selection.ts b/apps/web/src/utils/wallet-selection.ts index 9f8ebe7..90ee0a2 100644 --- a/apps/web/src/utils/wallet-selection.ts +++ b/apps/web/src/utils/wallet-selection.ts @@ -33,3 +33,25 @@ export const getActiveWalletAddress = ({ if (mode === 'cosmos') return cosmosAddress || ''; return ''; }; + +interface PreferredWalletSelectionOptions { + currentSelection: string; + isKeplrInstalled: boolean; + isMetaMaskInstalled: boolean; +} + +export const getPreferredWalletSelection = ({ + currentSelection, + isKeplrInstalled, + isMetaMaskInstalled, +}: PreferredWalletSelectionOptions) => { + if (currentSelection === KEPLR_WALLET_NAME && isKeplrInstalled) { + return KEPLR_WALLET_NAME; + } + if (currentSelection === METAMASK_WALLET_NAME && isMetaMaskInstalled) { + return METAMASK_WALLET_NAME; + } + if (isKeplrInstalled) return KEPLR_WALLET_NAME; + if (isMetaMaskInstalled) return METAMASK_WALLET_NAME; + return ''; +}; From 50f9df8a3a4274fc8f2cf3253759f70d6bfacdfd Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:07:30 -0400 Subject: [PATCH 10/45] use portal MetaMask wallet icon --- apps/web/public/metamask.png | Bin 0 -> 59719 bytes apps/web/public/metamask.svg | 18 ------------------ apps/web/src/components/ConnectWallet.tsx | 2 +- 3 files changed, 1 insertion(+), 19 deletions(-) create mode 100644 apps/web/public/metamask.png delete mode 100644 apps/web/public/metamask.svg diff --git a/apps/web/public/metamask.png b/apps/web/public/metamask.png new file mode 100644 index 0000000000000000000000000000000000000000..78269463ca4dcbea22c984d6a927315740633719 GIT binary patch literal 59719 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 literal 0 HcmV?d00001 diff --git a/apps/web/public/metamask.svg b/apps/web/public/metamask.svg deleted file mode 100644 index 5203d36..0000000 --- a/apps/web/public/metamask.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index 6ba12fe..99e14cf 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -105,7 +105,7 @@ function WalletChoiceModal() { { name: 'MetaMask', walletName: METAMASK_WALLET_NAME, - logo: '/metamask.svg', + logo: '/metamask.png', installed: isMetaMaskInstalled, }, ]; From 88f1363491cbaae09450929fd28ec3b008ea110c Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:20:07 -0400 Subject: [PATCH 11/45] add connected wallet account menu --- .../src/components/ConnectWallet.module.css | 166 ++++++++++++++++++ apps/web/src/components/ConnectWallet.tsx | 150 +++++++++++++--- apps/web/src/utils/evm.test.ts | 7 + apps/web/src/utils/evm.ts | 8 + apps/web/src/utils/wallet-selection.test.ts | 29 +++ apps/web/src/utils/wallet-selection.ts | 20 +++ 6 files changed, 357 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/ConnectWallet.module.css b/apps/web/src/components/ConnectWallet.module.css index c373c03..e337ef0 100644 --- a/apps/web/src/components/ConnectWallet.module.css +++ b/apps/web/src/components/ConnectWallet.module.css @@ -159,6 +159,168 @@ opacity: 0.55; } +.accountControls { + display: flex; + gap: 8px; +} + +.accountMenuRoot { + position: relative; +} + +.accountMenuTrigger { + display: flex; + min-height: 42px; + align-items: center; + gap: 8px; + padding: 8px 12px 8px 14px; + border: 0; + border-radius: 9px; + background: #4f46e5; + color: #fff; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: background-color 150ms ease; +} + +.accountMenuTrigger:hover, +.accountMenuTrigger:focus-visible { + background: #6366f1; + outline: none; +} + +.triggerChevron { + transition: transform 150ms ease; +} + +.triggerChevronOpen { + transform: rotate(180deg); +} + +.accountMenu { + position: absolute; + top: calc(100% + 10px); + right: 0; + z-index: 1100; + width: 430px; + max-width: calc(100vw - 24px); + overflow: hidden; + border: 1px solid rgba(160, 174, 202, 0.18); + border-radius: 12px; + background: #111a2a; + box-shadow: 0 18px 55px rgba(0, 0, 0, 0.55); +} + +.accountMenuHeading { + padding: 16px 16px 10px; + color: #f2f4f8; + font-size: 15px; + font-weight: 700; +} + +.addressList { + display: grid; + gap: 6px; + padding: 0 10px 12px; +} + +.addressItem { + display: flex; + width: 100%; + align-items: center; + gap: 12px; + padding: 11px 10px; + border: 1px solid transparent; + border-radius: 8px; + background: #0b1423; + color: #d9dfeb; + cursor: pointer; + text-align: left; + transition: border-color 150ms ease, background-color 150ms ease; +} + +.addressItem:hover, +.addressItem:focus-visible { + border-color: rgba(82, 205, 211, 0.4); + background: #101e30; + outline: none; +} + +.addressContent { + min-width: 0; + flex: 1; +} + +.addressLabel { + display: block; + margin-bottom: 5px; + color: #99a6ba; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.fullAddress { + display: block; + color: #e8ecf4; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.menuIcon { + flex: 0 0 auto; + color: #8f9bb0; +} + +.addressItem:hover .menuIcon, +.addressItem:focus-visible .menuIcon { + color: #52cdd3; +} + +.menuActions { + display: grid; + gap: 3px; + padding: 8px 10px 10px; + border-top: 1px solid rgba(160, 174, 202, 0.14); +} + +.menuAction { + display: flex; + width: 100%; + align-items: center; + gap: 10px; + padding: 11px 10px; + border: 0; + border-radius: 8px; + background: transparent; + color: #d9dfeb; + cursor: pointer; + font-size: 14px; + font-weight: 600; + text-align: left; +} + +.menuAction:hover, +.menuAction:focus-visible { + background: rgba(255, 255, 255, 0.06); + color: #fff; + outline: none; +} + +.disconnectAction { + color: #ff8585; +} + +.disconnectAction:hover, +.disconnectAction:focus-visible { + background: rgba(239, 68, 68, 0.12); + color: #ff9b9b; +} + @media (max-width: 640px) { .overlay { align-items: flex-end; @@ -203,4 +365,8 @@ min-height: 54px; margin-top: 24px; } + + .accountMenu { + max-width: calc(100vw - 16px); + } } diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index 99e14cf..c380745 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -1,14 +1,14 @@ 'use client' -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import { createPortal } from 'react-dom'; -import { Wallet, LogOut } from '@tamagui/lucide-icons'; +import { Wallet } from '@tamagui/lucide-icons'; import { InterchainWalletModal, useChain, useChainWallet } from '@interchain-kit/react'; +import { ChevronDown, Copy, LogOut, RefreshCw } from 'lucide-react'; import { toast } from 'react-toastify'; import { useDispatch, useSelector } from '@/redux/hooks'; -import { formatAddress } from '@/utils/format'; import { CHAIN_NAME, IS_EVM_NETWORK } from '@/contants/network'; import { setAddress, @@ -18,9 +18,11 @@ import { } from '@/redux/wallet.slice'; import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; import useWalletConnect from '@/hooks/useWalletConnect'; +import { evmAddressToCosmosAddress } from '@/utils/evm'; import { getActiveWalletAddress, getActiveWalletMode, + getAlternativeWalletName, getPreferredWalletSelection, KEPLR_WALLET_NAME, METAMASK_WALLET_NAME, @@ -199,12 +201,39 @@ export function ConnectWallet() { const dispatch = useDispatch(); const { disconnect: disconnectCosmos, openView } = useChain(CHAIN_NAME); const keplrWallet = useChainWallet(CHAIN_NAME, KEPLR_WALLET_NAME); - const { disconnect: disconnectEvm } = useEvmWallet(); + const evmWallet = useEvmWallet(); const { address, walletName } = useWalletConnect(); + const menuRef = useRef(null); + const [isMenuOpen, setMenuOpen] = useState(false); + const [isKeplrInstalled, setKeplrInstalled] = useState(false); + const isMetaMaskInstalled = Boolean(evmWallet.provider); + + useEffect(() => { + if (!isMenuOpen) return; + + setKeplrInstalled(Boolean(window.keplr)); + const handlePointerDown = (event: PointerEvent) => { + if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false); + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setMenuOpen(false); + }; + document.addEventListener('pointerdown', handlePointerDown); + window.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('pointerdown', handlePointerDown); + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isMenuOpen]); + + useEffect(() => { + if (!address) setMenuOpen(false); + }, [address]); const handleDisconnect = async () => { + setMenuOpen(false); if (IS_EVM_NETWORK && walletName === METAMASK_WALLET_NAME) { - await disconnectEvm(); + await evmWallet.disconnect(); } else if (IS_EVM_NETWORK && walletName === KEPLR_WALLET_NAME) { await keplrWallet.disconnect(); } else { @@ -223,39 +252,114 @@ export function ConnectWallet() { } }; - const handleCopyAddress = () => { - void navigator.clipboard.writeText(address); - toast('The address has been copied.', { - position: 'bottom-center', - theme: 'dark', - }); + const handleCopyAddress = async (value: string, label: string) => { + try { + await navigator.clipboard.writeText(value); + toast(`${label} copied.`, { + position: 'bottom-center', + theme: 'dark', + }); + } catch { + toast.error('Unable to copy the address.', { + position: 'bottom-center', + theme: 'dark', + }); + } }; const walletLabel = walletName === METAMASK_WALLET_NAME ? 'MetaMask' : 'Keplr'; + const alternativeWallet = IS_EVM_NETWORK ? getAlternativeWalletName({ + currentWallet: walletName, + isKeplrInstalled, + isMetaMaskInstalled, + }) : ''; + const metaMaskCosmosAddress = walletName === METAMASK_WALLET_NAME && evmWallet.address + ? evmAddressToCosmosAddress(evmWallet.address) + : ''; + const addressItems = walletName === METAMASK_WALLET_NAME + ? [ + { label: 'Cosmos-style EVM address', value: metaMaskCosmosAddress }, + { label: 'ETH hex address', value: evmWallet.address }, + ] + : [{ label: 'Lumera address', value: address }]; return ( -
+
{!address ? : - <> - {IS_EVM_NETWORK && ( + : ( +
- )} - {formatAddress(address, 5, -4)} - - + + {isMenuOpen && ( +
+
{walletLabel} wallet
+
+ {addressItems.filter((item) => item.value).map((item) => ( + + ))} +
+ +
+ {alternativeWallet && ( + + )} + +
+
+ )} +
+ ) }
); diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts index 0649139..96defd9 100644 --- a/apps/web/src/utils/evm.test.ts +++ b/apps/web/src/utils/evm.test.ts @@ -9,6 +9,7 @@ vi.mock('@/contants/network', () => ({ import { assertEvmAccountForChain, + evmAddressToCosmosAddress, evmBalanceToMicroLume, getEvmAccountForChain, getEvmBalance, @@ -74,6 +75,12 @@ describe('EVM value helpers', () => { expect(isEvmAddress('lumera1abc')).toBe(false); }); + it('converts an EVM account to its Lumera Bech32 representation', () => { + expect(evmAddressToCosmosAddress(ADDRESS)) + .toBe('lumera1qy352euf40x77qfrg4ncn27dauqjx3t83egcev'); + expect(() => evmAddressToCosmosAddress('not-an-address')).toThrow('invalid EVM address'); + }); + it('converts decimal LUME amounts to exact wei hex values', () => { expect(parseEvmAmount('1')).toBe('0xde0b6b3a7640000'); expect(parseEvmAmount('0.000000000000000001')).toBe('0x1'); diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index a4af091..d8147dc 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -2,6 +2,7 @@ import { EVM_NATIVE_DECIMALS, EVM_RPC_ENDPOINT, } from '@/contants/network'; +import { fromHex, toBech32 } from '@cosmjs/encoding'; import type { Eip1193Provider } from '@/types/window'; interface EvmRpcResponse { @@ -23,6 +24,13 @@ export const getMetaMaskProvider = (provider?: Eip1193Provider | null) => { export const isEvmAddress = (value: string) => /^0x[0-9a-fA-F]{40}$/.test(value); +export const evmAddressToCosmosAddress = (address: string, prefix = 'lumera') => { + if (!isEvmAddress(address)) { + throw new Error('Cannot convert an invalid EVM address.'); + } + return toBech32(prefix, fromHex(address.slice(2))); +}; + export const toHexChainId = (chainId: number) => `0x${chainId.toString(16)}`; export const getEvmAccountForChain = async ( diff --git a/apps/web/src/utils/wallet-selection.test.ts b/apps/web/src/utils/wallet-selection.test.ts index 115d0a5..3dfd69f 100644 --- a/apps/web/src/utils/wallet-selection.test.ts +++ b/apps/web/src/utils/wallet-selection.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { getActiveWalletAddress, getActiveWalletMode, + getAlternativeWalletName, getPreferredWalletSelection, KEPLR_WALLET_NAME, METAMASK_WALLET_NAME, @@ -80,3 +81,31 @@ describe('preferred wallet selection', () => { })).toBe(''); }); }); + +describe('alternative wallet selection', () => { + it('offers MetaMask to a Keplr user only when MetaMask is installed', () => { + expect(getAlternativeWalletName({ + currentWallet: KEPLR_WALLET_NAME, + isKeplrInstalled: true, + isMetaMaskInstalled: true, + })).toBe(METAMASK_WALLET_NAME); + expect(getAlternativeWalletName({ + currentWallet: KEPLR_WALLET_NAME, + isKeplrInstalled: true, + isMetaMaskInstalled: false, + })).toBe(''); + }); + + it('offers Keplr to a MetaMask user only when Keplr is installed', () => { + expect(getAlternativeWalletName({ + currentWallet: METAMASK_WALLET_NAME, + isKeplrInstalled: true, + isMetaMaskInstalled: true, + })).toBe(KEPLR_WALLET_NAME); + expect(getAlternativeWalletName({ + currentWallet: METAMASK_WALLET_NAME, + isKeplrInstalled: false, + isMetaMaskInstalled: true, + })).toBe(''); + }); +}); diff --git a/apps/web/src/utils/wallet-selection.ts b/apps/web/src/utils/wallet-selection.ts index 90ee0a2..26b36b2 100644 --- a/apps/web/src/utils/wallet-selection.ts +++ b/apps/web/src/utils/wallet-selection.ts @@ -55,3 +55,23 @@ export const getPreferredWalletSelection = ({ if (isMetaMaskInstalled) return METAMASK_WALLET_NAME; return ''; }; + +interface AlternativeWalletOptions { + currentWallet: string; + isKeplrInstalled: boolean; + isMetaMaskInstalled: boolean; +} + +export const getAlternativeWalletName = ({ + currentWallet, + isKeplrInstalled, + isMetaMaskInstalled, +}: AlternativeWalletOptions) => { + if (currentWallet === KEPLR_WALLET_NAME && isMetaMaskInstalled) { + return METAMASK_WALLET_NAME; + } + if (currentWallet === METAMASK_WALLET_NAME && isKeplrInstalled) { + return KEPLR_WALLET_NAME; + } + return ''; +}; From 24a030b0cd6144d20dd93ba9fb72eb162656a1cf Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:25:05 -0400 Subject: [PATCH 12/45] suppress extension body hydration mismatch --- apps/web/src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 87cef7e..bc58f77 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -20,7 +20,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - + {children} From 9f68cf19f82b88ce6f1a54b9de4059beeee81a3e Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:32:31 -0400 Subject: [PATCH 13/45] show complete wallet page addresses --- packages/ui/src/screens/WalletScreen.tsx | 95 ++++++++++++++++-------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index ca728a7..f86d213 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -37,6 +37,7 @@ import { formatAddress, formatTokenDisplay } from '@/utils/format'; import { getMessages } from '@/utils/helpers'; import { IValidator } from '@/types/validator'; import { DENOM } from '@/contants/network'; +import { evmAddressToCosmosAddress, isEvmAddress } from '@/utils/evm'; import 'react-paginate/theme/basic/react-paginate.css'; @@ -109,7 +110,7 @@ export const WalletScreen = ({ onOpenModal, onCloseModal, }: IWalletScreen) => { - const [isCopied, setCopied] = useState(false); + const [copiedAddress, setCopiedAddress] = useState(''); const getTxIcon = (type: string) => { switch(type) { @@ -240,24 +241,23 @@ export const WalletScreen = ({ return total; } - const handleCopyAddress = () => { - navigator.clipboard.writeText(walletAddress) - setCopied(true); - setTimeout(() => { - setCopied(false); - }, 3000); - toast('The address has been copied.', { - position: "bottom-center", - theme: "dark", - }); - } - - const handleCopyAddress2 = () => { - navigator.clipboard.writeText(walletAddress) - toast('The address has been copied.', { - position: "bottom-center", - theme: "dark", - }); + const handleCopyAddress = async (address: string, label: string) => { + try { + await navigator.clipboard.writeText(address); + setCopiedAddress(address); + setTimeout(() => { + setCopiedAddress((currentAddress) => currentAddress === address ? '' : currentAddress); + }, 3000); + toast(`${label} copied.`, { + position: "bottom-center", + theme: "dark", + }); + } catch { + toast.error('Unable to copy the address.', { + position: "bottom-center", + theme: "dark", + }); + } } if (!walletAddress) { @@ -279,6 +279,16 @@ export const WalletScreen = ({ ); } + const cosmosStyleEvmAddress = isEvm && isEvmAddress(walletAddress) + ? evmAddressToCosmosAddress(walletAddress) + : ''; + const displayedAddresses = isEvm + ? [ + { label: 'Cosmos-style EVM address', value: cosmosStyleEvmAddress }, + { label: 'ETH hex address', value: walletAddress }, + ] + : [{ label: 'Lumera address', value: walletAddress }]; + return (
-

Your Address

-
- {walletAddress} - +

{isEvm ? 'Your Addresses' : 'Your Address'}

+
+ {displayedAddresses.filter(({ value }) => value).map(({ label, value }) => ( +
+ + +
+ ))}
-

This is your unique address. Use it to receive LUME and other assets.

+

+ {isEvm + ? 'These formats identify the same MetaMask account. Click either address to copy it.' + : 'This is your unique address. Use it to receive LUME and other assets.'} +

From f15af52e90b6b3bac0f605838f975eb9a3227c15 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:38:38 -0400 Subject: [PATCH 14/45] fit wallet address card to content --- packages/ui/src/screens/WalletScreen.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index f86d213..6367554 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -328,8 +328,8 @@ export const WalletScreen = ({ onCloseCongratulationsModal={delegateOptions.onCloseCongratulationsModal} /> ) : null} -
- +
+

Total Wallet Balance

@@ -404,7 +404,7 @@ export const WalletScreen = ({ : null}

- +

{isEvm ? 'Your Addresses' : 'Your Address'}

{displayedAddresses.filter(({ value }) => value).map(({ label, value }) => ( @@ -420,7 +420,7 @@ export const WalletScreen = ({ {label} ) : null} - + {value} From 453cfc82555580ca7d5b1e5fe3f50adf63c16d7d Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 17:45:31 -0400 Subject: [PATCH 15/45] keep validators visible with MetaMask --- apps/web/src/app/staking/page.tsx | 1 + apps/web/src/hooks/useDelegate.ts | 18 ++++++-------- apps/web/src/utils/staking-validators.test.ts | 20 ++++++++++++++++ apps/web/src/utils/staking-validators.ts | 24 +++++++++++++++++++ .../components/AllValidators.tsx | 4 +++- .../components/RewardsCalculator.tsx | 8 ++++--- .../ui/src/screens/StakingScreen/index.tsx | 2 ++ 7 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/utils/staking-validators.test.ts create mode 100644 apps/web/src/utils/staking-validators.ts diff --git a/apps/web/src/app/staking/page.tsx b/apps/web/src/app/staking/page.tsx index 62a8e63..8ba2203 100644 --- a/apps/web/src/app/staking/page.tsx +++ b/apps/web/src/app/staking/page.tsx @@ -63,6 +63,7 @@ export default function Page() { isAccountInfoLoading={loading} onRefreshBalance={fetchData} delegateOptions={{ + canDelegate: delegate.canDelegate, isVoteLoading: delegate.isLoading, error: delegate.error, optionsAdvanced: delegate.optionsAdvanced, diff --git a/apps/web/src/hooks/useDelegate.ts b/apps/web/src/hooks/useDelegate.ts index 4c104a8..27185f0 100644 --- a/apps/web/src/hooks/useDelegate.ts +++ b/apps/web/src/hooks/useDelegate.ts @@ -1,9 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { MsgDelegate, } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; -import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; import { DENOM } from '@/contants/network'; import { extractValidNumber } from '@/utils/helpers'; @@ -11,6 +10,7 @@ import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO, RATE_VALUE } from '@/contan import { IValidator, } from '@/types'; +import { fetchBondedValidators } from '@/utils/staking-validators'; interface UseDepositOptions { callback?: () => void; @@ -38,26 +38,21 @@ const useDelegate = (options: UseDepositOptions = {}) => { const [transactionHash, setTransactionHash] = useState(''); const [selectedModal, setSelectedModal] = useState(''); - const fetchValidator = async () => { - if (isEvm) { - setValidators([]); - setTotalValidators('0'); - return; - } + const fetchValidator = useCallback(async () => { setFetchValidatorLoading(true); try { - const { data } = await instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=1000&status=BOND_STATUS_BONDED&pagination.count_total=true'); + const data = await fetchBondedValidators(); setValidators(data.validators); setTotalValidators(data.pagination.total); } catch (e) { console.error('API Error:', e); } setFetchValidatorLoading(false); - } + }, []); useEffect(() => { fetchValidator(); - }, [isEvm]); + }, [fetchValidator]); useEffect(() => { if (options?.customMemo) { @@ -228,6 +223,7 @@ const useDelegate = (options: UseDepositOptions = {}) => { } return { + canDelegate: !isEvm, error, showAdvanced, isLoading, diff --git a/apps/web/src/utils/staking-validators.test.ts b/apps/web/src/utils/staking-validators.test.ts new file mode 100644 index 0000000..c8a6ab8 --- /dev/null +++ b/apps/web/src/utils/staking-validators.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + BONDED_VALIDATORS_PATH, + fetchBondedValidators, +} from './staking-validators'; + +describe('fetchBondedValidators', () => { + it('loads public bonded-validator data without requiring wallet context', async () => { + const response = { + validators: [{ operator_address: 'lumeravaloper1active', jailed: false }], + pagination: { total: '1' }, + }; + const request = vi.fn().mockResolvedValue({ data: response }); + + await expect(fetchBondedValidators(request)).resolves.toBe(response); + expect(request).toHaveBeenCalledOnce(); + expect(request).toHaveBeenCalledWith(BONDED_VALIDATORS_PATH); + }); +}); diff --git a/apps/web/src/utils/staking-validators.ts b/apps/web/src/utils/staking-validators.ts new file mode 100644 index 0000000..c2c53b7 --- /dev/null +++ b/apps/web/src/utils/staking-validators.ts @@ -0,0 +1,24 @@ +import * as instance from '@/utils/api'; +import type { IValidator } from '@/types/validator'; + +export const BONDED_VALIDATORS_PATH = + '/cosmos/staking/v1beta1/validators?pagination.limit=1000&status=BOND_STATUS_BONDED&pagination.count_total=true'; + +interface BondedValidatorsResponse { + validators: IValidator[]; + pagination: { + total: string; + }; +} + +type ValidatorRequest = (path: string) => Promise<{ + data: BondedValidatorsResponse; +}>; + +// Validator metadata is public chain data and must not depend on the connected wallet type. +export const fetchBondedValidators = async ( + request: ValidatorRequest = instance.get +) => { + const { data } = await request(BONDED_VALIDATORS_PATH); + return data; +}; diff --git a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx index 9bd2411..d9cdd7e 100644 --- a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx +++ b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx @@ -43,6 +43,7 @@ interface IAllValidators { totalPower: number; getUptime: (validator: IValidator) => number; delegateOptions: { + canDelegate: boolean; onOpenModal: (validator: string, customMemo?: string) => void; validators: IValidator[]; onSelectValidator: (validator: string) => void; @@ -300,9 +301,10 @@ export default function AllValidators({
:
} diff --git a/packages/ui/src/screens/StakingScreen/components/RewardsCalculator.tsx b/packages/ui/src/screens/StakingScreen/components/RewardsCalculator.tsx index 3238121..419a54a 100644 --- a/packages/ui/src/screens/StakingScreen/components/RewardsCalculator.tsx +++ b/packages/ui/src/screens/StakingScreen/components/RewardsCalculator.tsx @@ -19,6 +19,7 @@ import { formatTokenDisplay } from '@/utils/format'; interface IRewardsCalculator { apr: number; availableAmount: number; + canDelegate: boolean; isLoading: boolean; onStakingButtonClick: (amount: string) => void; onRefreshBalance: () => void; @@ -27,6 +28,7 @@ interface IRewardsCalculator { export default function RewardsCalculator({ apr, availableAmount, + canDelegate, isLoading, onStakingButtonClick, onRefreshBalance, @@ -119,9 +121,9 @@ export default function RewardsCalculator({ {error ?
{error}
: null } -
-
diff --git a/packages/ui/src/screens/StakingScreen/index.tsx b/packages/ui/src/screens/StakingScreen/index.tsx index 3169486..be08480 100644 --- a/packages/ui/src/screens/StakingScreen/index.tsx +++ b/packages/ui/src/screens/StakingScreen/index.tsx @@ -47,6 +47,7 @@ import Activities from './components/Activities'; interface IStakingScreen { address: string; delegateOptions: { + canDelegate: boolean; isVoteLoading: boolean; error: string | null; optionsAdvanced: { @@ -376,6 +377,7 @@ export const StakingScreen = ({ Date: Thu, 13 Aug 2026 18:02:05 -0400 Subject: [PATCH 16/45] replace validator wallet labels with staking warning --- apps/web/src/app/staking/page.tsx | 8 ++++++ apps/web/src/components/ConnectWallet.tsx | 6 ++--- apps/web/src/redux/wallet.slice.test.ts | 20 ++++++++++++++ apps/web/src/redux/wallet.slice.ts | 6 ++++- .../components/AllValidators.tsx | 26 ++++++++++++++++--- .../ui/src/screens/StakingScreen/index.tsx | 1 + 6 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/redux/wallet.slice.test.ts diff --git a/apps/web/src/app/staking/page.tsx b/apps/web/src/app/staking/page.tsx index 8ba2203..ca9e1ec 100644 --- a/apps/web/src/app/staking/page.tsx +++ b/apps/web/src/app/staking/page.tsx @@ -10,8 +10,12 @@ import useStaking from '@/hooks/useStaking'; import useAccountInfo from '@/hooks/useAccountInfo'; import useUnbond from '@/hooks/useUnbond'; import useRedelegate from '@/hooks/useRedelegate'; +import { useDispatch } from '@/redux/hooks'; +import { setModalOpen } from '@/redux/wallet.slice'; +import { KEPLR_WALLET_NAME } from '@/utils/wallet-selection'; export default function Page() { + const dispatch = useDispatch(); const { address, isEvm } = useWalletConnect(); const staking = useStaking(address, isEvm); const { @@ -84,6 +88,10 @@ export default function Page() { onCloseContinueToStakingModal: delegate.handleCloseContinueToStakingModal, onSelectValidator: delegate.handleSelectValidator, onStakingAmountChange: delegate.handleStakingAmountChange, + onSwitchWallet: () => dispatch(setModalOpen({ + status: true, + preferredWalletName: KEPLR_WALLET_NAME, + })), }} staking={{ totalValidators: staking.totalValidators, diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index c380745..79c6d47 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -31,7 +31,7 @@ import styles from './ConnectWallet.module.css'; function WalletChoiceModal() { const dispatch = useDispatch(); - const { isModalOpen, walletName } = useSelector((state) => state.wallet); + const { isModalOpen, preferredWalletName, walletName } = useSelector((state) => state.wallet); const evmWallet = useEvmWallet(); const keplrWallet = useChainWallet(CHAIN_NAME, KEPLR_WALLET_NAME); const [isKeplrInstalled, setKeplrInstalled] = useState(false); @@ -46,12 +46,12 @@ function WalletChoiceModal() { const keplrInstalled = Boolean(window.keplr); setKeplrInstalled(keplrInstalled); setSelectedWallet(getPreferredWalletSelection({ - currentSelection: walletName, + currentSelection: preferredWalletName || walletName, isKeplrInstalled: keplrInstalled, isMetaMaskInstalled, })); setWalletError(''); - }, [isMetaMaskInstalled, isModalOpen, walletName]); + }, [isMetaMaskInstalled, isModalOpen, preferredWalletName, walletName]); useEffect(() => { if (!isModalOpen) return; diff --git a/apps/web/src/redux/wallet.slice.test.ts b/apps/web/src/redux/wallet.slice.test.ts new file mode 100644 index 0000000..1c75201 --- /dev/null +++ b/apps/web/src/redux/wallet.slice.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import walletReducer, { setModalOpen } from './wallet.slice'; +import { KEPLR_WALLET_NAME } from '@/utils/wallet-selection'; + +describe('wallet modal selection', () => { + it('stores a requested wallet while opening and clears it when closing', () => { + const opened = walletReducer(undefined, setModalOpen({ + status: true, + preferredWalletName: KEPLR_WALLET_NAME, + })); + + expect(opened.isModalOpen).toBe(true); + expect(opened.preferredWalletName).toBe(KEPLR_WALLET_NAME); + + const closed = walletReducer(opened, setModalOpen({ status: false })); + expect(closed.isModalOpen).toBe(false); + expect(closed.preferredWalletName).toBe(''); + }); +}); diff --git a/apps/web/src/redux/wallet.slice.ts b/apps/web/src/redux/wallet.slice.ts index dabf150..e79355e 100644 --- a/apps/web/src/redux/wallet.slice.ts +++ b/apps/web/src/redux/wallet.slice.ts @@ -5,6 +5,7 @@ interface IWalletState { address: string; isConnected: boolean; walletName: string; + preferredWalletName: string; isModalOpen: boolean; } @@ -13,6 +14,7 @@ const initialState: IWalletState = { isConnected: false, isModalOpen: false, walletName: '', + preferredWalletName: '', }; type TAddressAction = { @@ -25,6 +27,7 @@ type TConnectedAction = { type TModalOpenAction = { status: boolean; + preferredWalletName?: string; }; type TWalletnameAction = { @@ -46,9 +49,10 @@ export const walletSlice = createSlice({ }, setModalOpen: (state, { payload }: PayloadAction) => { state.isModalOpen = payload.status; + state.preferredWalletName = payload.status ? payload.preferredWalletName || '' : ''; }, }, }); export const { setAddress, setConnected, setWalletName, setModalOpen } = walletSlice.actions; -export default walletSlice.reducer; \ No newline at end of file +export default walletSlice.reducer; diff --git a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx index d9cdd7e..a94f537 100644 --- a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx +++ b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx @@ -47,6 +47,7 @@ interface IAllValidators { onOpenModal: (validator: string, customMemo?: string) => void; validators: IValidator[]; onSelectValidator: (validator: string) => void; + onSwitchWallet: () => void; } } @@ -171,6 +172,24 @@ export default function AllValidators({
+ {!delegateOptions.canDelegate ? ( +
+
+

Staking is not currently supported with MetaMask.

+

Switch to a Keplr wallet to delegate LUME.

+
+ +
+ ) : null} {staking.isLoading || !staking?.params?.bond_denom ? (
@@ -298,15 +317,14 @@ export default function AllValidators({ {validator.jailed ?
-
: +
: delegateOptions.canDelegate ?
-
+
: null }
diff --git a/packages/ui/src/screens/StakingScreen/index.tsx b/packages/ui/src/screens/StakingScreen/index.tsx index be08480..2a0176d 100644 --- a/packages/ui/src/screens/StakingScreen/index.tsx +++ b/packages/ui/src/screens/StakingScreen/index.tsx @@ -75,6 +75,7 @@ interface IStakingScreen { onCloseContinueToStakingModal: () => void; onSelectValidator: (validator: string) => void; onStakingAmountChange: (amount: string) => void; + onSwitchWallet: () => void; }; staking: { validators: IValidator[]; From ce2c1a65ce5cd1d55324b5e6cbb737905f5690ea Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 18:20:24 -0400 Subject: [PATCH 17/45] show current governance votes --- apps/web/src/app/governance/[id]/page.tsx | 6 ++ apps/web/src/app/governance/page.tsx | 1 + apps/web/src/app/page.tsx | 2 +- apps/web/src/components/layout/AppShell.tsx | 30 +++++---- apps/web/src/hooks/useGovernanceDetails.ts | 1 + apps/web/src/hooks/useProposals.ts | 60 ++++++++++++++++++ apps/web/src/utils/governance-votes.test.ts | 45 ++++++++++++++ apps/web/src/utils/governance-votes.ts | 49 +++++++++++++++ .../src/screens/GovernanceDetailsScreen.tsx | 38 +++++++++--- packages/ui/src/screens/GovernanceScreen.tsx | 40 +++++++++--- packages/ui/src/screens/HomeScreen.tsx | 62 ++++++++++++++----- 11 files changed, 288 insertions(+), 46 deletions(-) create mode 100644 apps/web/src/utils/governance-votes.test.ts create mode 100644 apps/web/src/utils/governance-votes.ts diff --git a/apps/web/src/app/governance/[id]/page.tsx b/apps/web/src/app/governance/[id]/page.tsx index 02415ab..66b46bf 100644 --- a/apps/web/src/app/governance/[id]/page.tsx +++ b/apps/web/src/app/governance/[id]/page.tsx @@ -27,6 +27,7 @@ export default function Page({ params }: Props) { isVoteLoading, handlePageClick, fetchGovernanceDetail, + fetchVotes, } = useGovernanceDetails(id); const deposit = useDeposit({ callback: () => fetchGovernanceDetail(id), @@ -34,6 +35,10 @@ export default function Page({ params }: Props) { }); const proposals = useProposals({ customMemo: governance?.title ? `Vote for the ${governance?.title}` : '', + callback: () => { + fetchGovernanceDetail(id); + fetchVotes(); + }, }); const { address, canSignCosmosTransactions } = useWalletConnect(); @@ -84,6 +89,7 @@ export default function Page({ params }: Props) { error: proposals.errorVote, voteAdvanced: proposals.voteAdvanced, transactionHash: proposals.transactionHash, + currentVote: proposals.userVotes[id], handleVoteAdvancedChange: proposals.handleVoteAdvancedChange, handleResetError: proposals.handleResetError, isVoteOpen: proposals.isVoteOpen, diff --git a/apps/web/src/app/governance/page.tsx b/apps/web/src/app/governance/page.tsx index 9ed8f7f..f973644 100644 --- a/apps/web/src/app/governance/page.tsx +++ b/apps/web/src/app/governance/page.tsx @@ -95,6 +95,7 @@ export default function Page() { isVoteOpen={proposals.isVoteOpen} setVoteOpen={proposals.setVoteOpen} voteTransactionHash={proposals.transactionHash} + userVotes={proposals.userVotes} onCloseVoteCongratulationsModal={proposals.handleCloseCongratulationsModal} deposit={{ isOpen: deposit.isModalOpen, diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 14a6a25..5109ea3 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -47,6 +47,7 @@ export default function Page() { loading={loading} accountInfo={accountInfo} proposals={proposals.proposalsInfo} + userVotes={proposals.userVotes} isProposalLoading={proposals.loading} recentActivities={recentActivityData.recentActivity} isRecentActivityLoading={recentActivityData.loading} @@ -73,4 +74,3 @@ export default function Page() { ) } - diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx index 3c4f082..14f8c24 100644 --- a/apps/web/src/components/layout/AppShell.tsx +++ b/apps/web/src/components/layout/AppShell.tsx @@ -13,6 +13,7 @@ import { } from '@tamagui/lucide-icons'; import Image from 'next/image'; import { Layers } from 'lucide-react'; +import { usePathname } from 'next/navigation'; import { ConnectWallet, WalletModalComponent } from '@/components/ConnectWallet' import AppLink from '@/components/AppLink'; @@ -52,22 +53,34 @@ const VIEW_TITLES: Record = { block: "Block Details", } +function isActive(currentUrl: string, url: string) { + if (currentUrl === '/' && currentUrl === url) { + return true; + } + return url !== NAV_ITEMS[0].url && currentUrl.indexOf(url) !== -1; +} + export default function AppShell({ children }: { children: React.ReactNode }) { const dispatch = useDispatch(); const { activeView, currentPath, viewTitle } = useSelector((state) => state.app); + const pathname = usePathname(); const [isSidebarOpen, setSidebarOpen] = useState(false) useEffect(() => { - if (window?.location?.pathname) { + if (pathname) { dispatch(setCurrentPath({ - currentPath: window.location.pathname, + currentPath: pathname, })); - const navItem = NAV_ITEMS.find((item) => isActive(currentPath, item.url)); + const navItem = NAV_ITEMS.find((item) => isActive(pathname, item.url)); dispatch(setActiveView({ activeView: navItem?.id || "dashboard", })); } - }, []) + }, [dispatch, pathname]) + + const isContextualRoute = pathname.startsWith('/tx/') || pathname.startsWith('/block/'); + const routeNavItem = NAV_ITEMS.find((item) => isActive(pathname, item.url)); + const shellTitle = isContextualRoute ? viewTitle : routeNavItem?.label || VIEW_TITLES[activeView]; const onNavClick = (id: ViewId) => { dispatch(setActiveView({ @@ -76,13 +89,6 @@ export default function AppShell({ children }: { children: React.ReactNode }) { setSidebarOpen(false) } - const isActive = (currentUrl: string, url: string) => { - if (currentUrl === '/' && currentUrl === url) { - return true; - } - return url !== NAV_ITEMS[0].url && currentUrl.indexOf(url) !== -1; - } - const handleMenuItemClick = (item: TNaxItems) => { onNavClick(item.id); dispatch(setViewTitle({ @@ -200,7 +206,7 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
-

{viewTitle || VIEW_TITLES[activeView]}

+

{shellTitle}

{/* Placeholder for wallet actions */} diff --git a/apps/web/src/hooks/useGovernanceDetails.ts b/apps/web/src/hooks/useGovernanceDetails.ts index 826c5ef..0d10857 100644 --- a/apps/web/src/hooks/useGovernanceDetails.ts +++ b/apps/web/src/hooks/useGovernanceDetails.ts @@ -139,6 +139,7 @@ const useGovernanceDetails = (id: string) => { totalVotes, errorVote, fetchGovernanceDetail, + fetchVotes, handlePageClick, } } diff --git a/apps/web/src/hooks/useProposals.ts b/apps/web/src/hooks/useProposals.ts index 17f38dd..f7e819c 100644 --- a/apps/web/src/hooks/useProposals.ts +++ b/apps/web/src/hooks/useProposals.ts @@ -10,6 +10,7 @@ import { Coin } from '@/hooks/useAccountInfo' import useWalletConnect from '@/hooks/useWalletConnect'; import { GAS_LIMIT, FEE_VALUE, GAS_RATIO, FEE_RATIO } from '@/contants'; import { assertGovernanceTransactionsAvailable } from '@/utils/cosmos-transactions'; +import { GovernanceVote } from '@/utils/governance-votes'; type TMessage = { '@type': string; @@ -92,6 +93,31 @@ const useProposals = (options: UseDepositOptions = {}) => { }); const [isVoteOpen, setVoteOpen] = useState(false); const [transactionHash, setTransactionHash] = useState(''); + const [userVotes, setUserVotes] = useState>({}); + + const refreshUserVote = async (proposalId: string) => { + if (!address) { + return; + } + + try { + const { data } = await axios.get( + `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposalId}/votes/${address}`, + ); + setUserVotes((current) => ({ + ...current, + [proposalId]: data.vote, + })); + } catch (queryError) { + if (axios.isAxiosError(queryError) && queryError.response?.status === 404) { + setUserVotes((current) => { + const next = { ...current }; + delete next[proposalId]; + return next; + }); + } + } + }; const fetchData = async () => { setLoading(true); @@ -115,6 +141,38 @@ const useProposals = (options: UseDepositOptions = {}) => { fetchData(); }, []); + useEffect(() => { + let cancelled = false; + + if (!address) { + setUserVotes({}); + return; + } + + const fetchUserVotes = async () => { + const voteEntries = await Promise.all(proposalsInfo.map(async (proposal) => { + try { + const { data } = await axios.get( + `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposal.id}/votes/${address}`, + ); + return [proposal.id, data.vote] as const; + } catch { + return null; + } + })); + + if (!cancelled) { + setUserVotes(Object.fromEntries(voteEntries.filter((entry) => entry !== null))); + } + }; + + fetchUserVotes(); + + return () => { + cancelled = true; + }; + }, [address, proposalsInfo]); + useEffect(() => { if (options?.customMemo) { setAdvanced({ @@ -176,6 +234,7 @@ const useProposals = (options: UseDepositOptions = {}) => { const result = await client.signAndBroadcast(address, [msg], fee, voteAdvanced.memo); if (result?.transactionHash) { setTransactionHash(result?.transactionHash); + await refreshUserVote(item.id); // setVoteOpen(false); fetchData(); if (options?.callback) { @@ -215,6 +274,7 @@ const useProposals = (options: UseDepositOptions = {}) => { voteAdvanced, isVoteOpen, transactionHash, + userVotes, handleCloseCongratulationsModal, setVoteOpen, handleResetError, diff --git a/apps/web/src/utils/governance-votes.test.ts b/apps/web/src/utils/governance-votes.test.ts new file mode 100644 index 0000000..ce9f5ea --- /dev/null +++ b/apps/web/src/utils/governance-votes.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { + formatGovernanceVote, + getGovernanceVoteValue, + GovernanceVote, +} from './governance-votes'; + +const vote = (options: GovernanceVote['options']): GovernanceVote => ({ + proposal_id: '15', + voter: 'lumera1voter', + options, +}); + +describe('formatGovernanceVote', () => { + it('formats a standard vote', () => { + expect(formatGovernanceVote(vote([ + { option: 'VOTE_OPTION_YES', weight: '1.000000000000000000' }, + ]))).toBe('Yes'); + }); + + it('formats a weighted vote', () => { + expect(formatGovernanceVote(vote([ + { option: 'VOTE_OPTION_YES', weight: '0.750000000000000000' }, + { option: 'VOTE_OPTION_ABSTAIN', weight: '0.250000000000000000' }, + ]))).toBe('Yes 75%, Abstain 25%'); + }); + + it('returns an empty label when no vote is available', () => { + expect(formatGovernanceVote()).toBe(''); + }); + + it('maps a standard chain vote back to the form value', () => { + expect(getGovernanceVoteValue(vote([ + { option: 'VOTE_OPTION_NO_WITH_VETO', weight: '1.000000000000000000' }, + ]))).toBe('4'); + }); + + it('does not map a weighted vote to the single-choice form', () => { + expect(getGovernanceVoteValue(vote([ + { option: 'VOTE_OPTION_YES', weight: '0.500000000000000000' }, + { option: 'VOTE_OPTION_NO', weight: '0.500000000000000000' }, + ]))).toBe(''); + }); +}); diff --git a/apps/web/src/utils/governance-votes.ts b/apps/web/src/utils/governance-votes.ts new file mode 100644 index 0000000..ba70f18 --- /dev/null +++ b/apps/web/src/utils/governance-votes.ts @@ -0,0 +1,49 @@ +export interface GovernanceVoteOption { + option: string; + weight: string; +} + +export interface GovernanceVote { + proposal_id: string; + voter: string; + options: GovernanceVoteOption[]; + metadata?: string; +} + +const VOTE_OPTION_LABELS: Record = { + VOTE_OPTION_YES: 'Yes', + VOTE_OPTION_ABSTAIN: 'Abstain', + VOTE_OPTION_NO: 'No', + VOTE_OPTION_NO_WITH_VETO: 'No With Veto', +}; + +const VOTE_OPTION_VALUES: Record = { + VOTE_OPTION_YES: '1', + VOTE_OPTION_ABSTAIN: '2', + VOTE_OPTION_NO: '3', + VOTE_OPTION_NO_WITH_VETO: '4', +}; + +export const getGovernanceVoteValue = (vote?: GovernanceVote | null) => { + if (vote?.options?.length !== 1 || Number(vote.options[0].weight) !== 1) { + return ''; + } + + return VOTE_OPTION_VALUES[vote.options[0].option] || ''; +}; + +export const formatGovernanceVote = (vote?: GovernanceVote | null) => { + const options = vote?.options?.filter(({ weight }) => Number(weight) > 0) || []; + + if (options.length === 0) { + return ''; + } + + if (options.length === 1 && Number(options[0].weight) === 1) { + return VOTE_OPTION_LABELS[options[0].option] || options[0].option; + } + + return options + .map(({ option, weight }) => `${VOTE_OPTION_LABELS[option] || option} ${Number(weight) * 100}%`) + .join(', '); +}; diff --git a/packages/ui/src/screens/GovernanceDetailsScreen.tsx b/packages/ui/src/screens/GovernanceDetailsScreen.tsx index da35419..1999a60 100644 --- a/packages/ui/src/screens/GovernanceDetailsScreen.tsx +++ b/packages/ui/src/screens/GovernanceDetailsScreen.tsx @@ -21,6 +21,11 @@ import { IProposal } from '@/hooks/useProposals'; import { VOTE_LIMIT } from '@/hooks/useGovernanceDetails'; import { IBlock, IVote } from '@/hooks/useGovernanceDetails'; import { formatAddress, formatToken } from '@/utils/format'; +import { + formatGovernanceVote, + getGovernanceVoteValue, + GovernanceVote, +} from '@/utils/governance-votes'; import { DENOM } from '@/contants/network'; import { VoteModal } from './HomeScreen'; @@ -75,6 +80,7 @@ interface IGovernanceDetailsScreen { setVoteOpen: (status: boolean) => void; handleResetError: () => void; transactionHash: string; + currentVote?: GovernanceVote; handleCloseCongratulationsModal: () => void; }; block: IBlock | null; @@ -197,6 +203,10 @@ export const GovernanceDetailsScreen = ({ const handleVotePress = () => { vote.handleResetError(); + const currentVoteValue = getGovernanceVoteValue(vote.currentVote); + if (currentVoteValue) { + vote.onOptionChange(currentVoteValue); + } vote.setVoteOpen(true); } @@ -211,19 +221,28 @@ export const GovernanceDetailsScreen = ({ if (['PROPOSAL_STATUS_FAILED', 'PROPOSAL_STATUS_REJECTED'].includes(governance?.status) || (isExpired && governance.status !== 'PROPOSAL_STATUS_VOTING_PERIOD')) { return null; } + const currentVoteLabel = formatGovernanceVote(vote.currentVote); + return (
-
- {governance.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? +
+ {currentVoteLabel ? ( + Your vote: {currentVoteLabel} + ) : null} + {governance.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? ( +
+ +
+ ) : null} +
: null - } - + onPress={() => handleDepositClick(governance)} + >Deposit +
{transactionUnavailableReason ? (

{transactionUnavailableReason}

@@ -673,6 +692,7 @@ export const GovernanceDetailsScreen = ({ voteAdvanced={vote.voteAdvanced} handleVoteAdvancedChange={vote.handleVoteAdvancedChange} transactionHash={vote.transactionHash} + currentVote={vote.currentVote} onCloseCongratulationsModal={vote.handleCloseCongratulationsModal} /> void; }; voteTransactionHash?: string; + userVotes: Record; onCloseVoteCongratulationsModal?: () => void; createProposal: { step: number; @@ -150,6 +156,7 @@ export const GovernanceScreen = ({ isSumaryLoading, nextKey, voteTransactionHash, + userVotes, selectedItem, createProposal, setSelectedItem, @@ -232,19 +239,29 @@ export const GovernanceScreen = ({ if (['PROPOSAL_STATUS_FAILED', 'PROPOSAL_STATUS_REJECTED'].includes(item?.status) || (isExpired && item.status !== 'PROPOSAL_STATUS_VOTING_PERIOD')) { return null; } + const currentVote = userVotes[item.id]; + const currentVoteLabel = formatGovernanceVote(currentVote); + return (
-
- {item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? +
+ {currentVoteLabel ? ( + Your vote: {currentVoteLabel} + ) : null} + {item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? ( +
+ +
+ ) : null} +
: null - } - + onPress={() => handleDepositClick(item)} + >Deposit +
{transactionUnavailableReason ? (

{transactionUnavailableReason}

@@ -265,6 +282,10 @@ export const GovernanceScreen = ({ const handleVotePress = (item: IProposal) => { handleResetError(); + const currentVoteValue = getGovernanceVoteValue(userVotes[item.id]); + if (currentVoteValue) { + onOptionChange(currentVoteValue); + } setVoteOpen(true); setSelectedItem(item); } @@ -509,6 +530,7 @@ export const GovernanceScreen = ({ voteAdvanced={voteAdvanced} handleVoteAdvancedChange={handleVoteAdvancedChange} transactionHash={voteTransactionHash} + currentVote={selectedItem ? userVotes[selectedItem.id] : undefined} onCloseCongratulationsModal={onCloseVoteCongratulationsModal} /> ; isProposalLoading: boolean; recentActivities: IRecentActivity[]; isRecentActivityLoading: boolean; @@ -112,6 +118,7 @@ interface IVoteModal { }; handleVoteAdvancedChange: (name: string, value: string) => void; transactionHash?: string; + currentVote?: GovernanceVote; onCloseCongratulationsModal?: () => void; } @@ -216,6 +223,7 @@ export const VoteModal = ({ voteAdvanced, handleVoteAdvancedChange, transactionHash, + currentVote, onCloseCongratulationsModal, }: IVoteModal) => { if (!isOpen) { @@ -227,6 +235,9 @@ export const VoteModal = ({ setShowAdvanced(checked); } + const currentVoteLabel = formatGovernanceVote(currentVote); + const currentVoteValue = getGovernanceVoteValue(currentVote); + if (transactionHash) { return (
- + {currentVoteLabel ? ( +

+ Current vote: {currentVoteLabel}. Submitting a new vote replaces it. +

+ ) : null} +
{VOTE_OPTIONS?.map((item) => (
@@ -639,6 +655,7 @@ export const HomeScreen = ({ loading, accountInfo, proposals, + userVotes, isProposalLoading, recentActivities, isRecentActivityLoading, @@ -804,6 +821,10 @@ export const HomeScreen = ({ const handleVotePress = (item: IProposal) => { handleResetError(); + const currentVoteValue = getGovernanceVoteValue(userVotes[item.id]); + if (currentVoteValue) { + onOptionChange(currentVoteValue); + } setVoteOpen(true); setSelectedItem(item); } @@ -950,21 +971,31 @@ export const HomeScreen = ({

No active proposals

: null } - {proposals?.map((item) => ( -
-
- - {item.title} - - {item.proposer} + {proposals?.map((item) => { + const currentVote = userVotes[item.id]; + const currentVoteLabel = formatGovernanceVote(currentVote); + + return ( +
+
+ + {item.title} + + {item.proposer} + {currentVoteLabel ? ( + + Your vote: {currentVoteLabel} + + ) : null} +
+ {item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? +
+ +
: null + }
- {item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ? -
- -
: null - } -
- ))} + ); + })} } @@ -1001,6 +1032,7 @@ export const HomeScreen = ({ voteAdvanced={voteAdvanced} handleVoteAdvancedChange={handleVoteAdvancedChange} transactionHash={voteTransactionHash} + currentVote={selectedItem ? userVotes[selectedItem.id] : undefined} onCloseCongratulationsModal={onCloseVoteCongratulationsModal} /> Date: Thu, 13 Aug 2026 18:30:33 -0400 Subject: [PATCH 18/45] show both EVM account address formats --- apps/web/src/app/wallet/page.tsx | 4 ++- apps/web/src/components/ConnectWallet.tsx | 14 +++------ apps/web/src/hooks/useWalletConnect.ts | 4 +++ apps/web/src/utils/evm.test.ts | 37 +++++++++++++++++++++++ apps/web/src/utils/evm.ts | 34 ++++++++++++++++++++- packages/ui/src/screens/WalletScreen.tsx | 25 +++++++-------- 6 files changed, 95 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/wallet/page.tsx b/apps/web/src/app/wallet/page.tsx index 2469624..872a9c2 100644 --- a/apps/web/src/app/wallet/page.tsx +++ b/apps/web/src/app/wallet/page.tsx @@ -11,7 +11,7 @@ import useDelegate from '@/hooks/useDelegate'; import useSend from '@/hooks/useSend'; export default function Page() { - const { address, isEvm } = useWalletConnect(); + const { address, bech32Address, ethAddress, isEvm } = useWalletConnect(); const account = useAccountInfo(); const { accountInfo, @@ -43,6 +43,8 @@ export default function Page() {
(null); const [isMenuOpen, setMenuOpen] = useState(false); const [isKeplrInstalled, setKeplrInstalled] = useState(false); @@ -273,15 +272,12 @@ export function ConnectWallet() { isKeplrInstalled, isMetaMaskInstalled, }) : ''; - const metaMaskCosmosAddress = walletName === METAMASK_WALLET_NAME && evmWallet.address - ? evmAddressToCosmosAddress(evmWallet.address) - : ''; - const addressItems = walletName === METAMASK_WALLET_NAME + const addressItems = IS_EVM_NETWORK ? [ - { label: 'Cosmos-style EVM address', value: metaMaskCosmosAddress }, - { label: 'ETH hex address', value: evmWallet.address }, + { label: 'Bech32 address', value: bech32Address }, + { label: 'ETH hex address', value: ethAddress }, ] - : [{ label: 'Lumera address', value: address }]; + : [{ label: 'Bech32 address', value: address }]; return (
diff --git a/apps/web/src/hooks/useWalletConnect.ts b/apps/web/src/hooks/useWalletConnect.ts index 3506442..b5dc907 100644 --- a/apps/web/src/hooks/useWalletConnect.ts +++ b/apps/web/src/hooks/useWalletConnect.ts @@ -11,6 +11,7 @@ import { import { useEvmWallet } from '@/app/providers/evm-wallet-provider'; import { canWalletSignCosmosTransactions } from '@/utils/cosmos-transactions'; import { getActiveWalletAddress, getActiveWalletMode } from '@/utils/wallet-selection'; +import { getEvmAddressFormats } from '@/utils/evm'; const useWalletConnect = () => { const { chain, wallet, address: cosmosAddress } = useChain(CHAIN_NAME); @@ -25,6 +26,7 @@ const useWalletConnect = () => { evmAddress: evmWallet.address, cosmosAddress, }); + const { bech32Address, ethAddress } = getEvmAddressFormats(address, IS_EVM_NETWORK); const isConnected = Boolean(address); // Phase 2 will source this from the MetaMask Cosmos signer once it is implemented. const hasEvmCosmosSigner = false; @@ -81,6 +83,8 @@ const useWalletConnect = () => { isModalOpen, isConnected, address, + bech32Address, + ethAddress, walletName, walletMode, canSignCosmosTransactions, diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts index 96defd9..92a3e71 100644 --- a/apps/web/src/utils/evm.test.ts +++ b/apps/web/src/utils/evm.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; +import { toBech32 } from '@cosmjs/encoding'; import type { Eip1193Provider } from '@/types/window'; @@ -9,10 +10,12 @@ vi.mock('@/contants/network', () => ({ import { assertEvmAccountForChain, + cosmosAddressToEvmAddress, evmAddressToCosmosAddress, evmBalanceToMicroLume, getEvmAccountForChain, getEvmBalance, + getEvmAddressFormats, getMetaMaskProvider, isEvmAddress, parseEvmAmount, @@ -64,6 +67,40 @@ describe('MetaMask provider selection', () => { }); }); +describe('EVM account address formats', () => { + const bech32Address = 'lumera1qy352euf40x77qfrg4ncn27dauqjx3t83egcev'; + + it('converts between the Bech32 and ETH hex representations', () => { + expect(evmAddressToCosmosAddress(ADDRESS)).toBe(bech32Address); + expect(cosmosAddressToEvmAddress(bech32Address)).toBe(ADDRESS); + }); + + it('returns both formats for either wallet mode on an EVM-enabled chain', () => { + expect(getEvmAddressFormats(ADDRESS, true)).toEqual({ + bech32Address, + ethAddress: ADDRESS, + }); + expect(getEvmAddressFormats(bech32Address, true)).toEqual({ + bech32Address, + ethAddress: ADDRESS, + }); + }); + + it('does not invent an ETH address on a non-EVM chain', () => { + expect(getEvmAddressFormats(bech32Address, false)).toEqual({ + bech32Address, + ethAddress: '', + }); + }); + + it('rejects malformed and non-account Bech32 values', () => { + expect(() => cosmosAddressToEvmAddress('not-an-address')) + .toThrow('invalid Bech32 address'); + expect(() => cosmosAddressToEvmAddress(toBech32('lumera', new Uint8Array([1])))) + .toThrow('not 20 bytes'); + }); +}); + afterEach(() => { vi.unstubAllGlobals(); }); diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index d8147dc..7094751 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -2,7 +2,7 @@ import { EVM_NATIVE_DECIMALS, EVM_RPC_ENDPOINT, } from '@/contants/network'; -import { fromHex, toBech32 } from '@cosmjs/encoding'; +import { fromBech32, fromHex, toBech32, toHex } from '@cosmjs/encoding'; import type { Eip1193Provider } from '@/types/window'; interface EvmRpcResponse { @@ -31,6 +31,38 @@ export const evmAddressToCosmosAddress = (address: string, prefix = 'lumera') => return toBech32(prefix, fromHex(address.slice(2))); }; +export const cosmosAddressToEvmAddress = (address: string) => { + let decoded: ReturnType; + try { + decoded = fromBech32(address); + } catch { + throw new Error('Cannot convert an invalid Bech32 address.'); + } + if (decoded.data.length !== 20) { + throw new Error('Cannot convert a Bech32 address that is not 20 bytes.'); + } + return `0x${toHex(decoded.data)}`; +}; + +export const getEvmAddressFormats = (address: string, isEvmNetwork: boolean) => { + if (!address) { + return { bech32Address: '', ethAddress: '' }; + } + if (!isEvmNetwork) { + return { bech32Address: address, ethAddress: '' }; + } + if (isEvmAddress(address)) { + return { + bech32Address: evmAddressToCosmosAddress(address), + ethAddress: address, + }; + } + return { + bech32Address: address, + ethAddress: cosmosAddressToEvmAddress(address), + }; +}; + export const toHexChainId = (chainId: number) => `0x${chainId.toString(16)}`; export const getEvmAccountForChain = async ( diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index 6367554..627688c 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -37,12 +37,13 @@ import { formatAddress, formatTokenDisplay } from '@/utils/format'; import { getMessages } from '@/utils/helpers'; import { IValidator } from '@/types/validator'; import { DENOM } from '@/contants/network'; -import { evmAddressToCosmosAddress, isEvmAddress } from '@/utils/evm'; import 'react-paginate/theme/basic/react-paginate.css'; interface IWalletScreen { walletAddress: string; + bech32Address: string; + ethAddress: string; isEvm: boolean; accountInfo: AccountInfoData | null; isLoading: boolean; @@ -97,6 +98,8 @@ interface IWalletScreen { export const WalletScreen = ({ walletAddress, + bech32Address, + ethAddress, isEvm, accountInfo, isLoading, @@ -279,15 +282,13 @@ export const WalletScreen = ({ ); } - const cosmosStyleEvmAddress = isEvm && isEvmAddress(walletAddress) - ? evmAddressToCosmosAddress(walletAddress) - : ''; - const displayedAddresses = isEvm + const hasEvmAddressFormats = Boolean(ethAddress); + const displayedAddresses = hasEvmAddressFormats ? [ - { label: 'Cosmos-style EVM address', value: cosmosStyleEvmAddress }, - { label: 'ETH hex address', value: walletAddress }, + { label: 'Bech32 address', value: bech32Address }, + { label: 'ETH hex address', value: ethAddress }, ] - : [{ label: 'Lumera address', value: walletAddress }]; + : [{ label: 'Bech32 address', value: bech32Address || walletAddress }]; return (
@@ -405,7 +406,7 @@ export const WalletScreen = ({
-

{isEvm ? 'Your Addresses' : 'Your Address'}

+

{hasEvmAddressFormats ? 'Your Addresses' : 'Your Address'}

{displayedAddresses.filter(({ value }) => value).map(({ label, value }) => (
@@ -415,7 +416,7 @@ export const WalletScreen = ({ aria-label={`Copy ${label}`} onClick={() => void handleCopyAddress(value, label)} > - {isEvm ? ( + {hasEvmAddressFormats ? ( {label} @@ -439,8 +440,8 @@ export const WalletScreen = ({ ))}

- {isEvm - ? 'These formats identify the same MetaMask account. Click either address to copy it.' + {hasEvmAddressFormats + ? 'These formats identify the same account. Click either address to copy it.' : 'This is your unique address. Use it to receive LUME and other assets.'}

From 67b1ff810df06444eeb0ec29d7ac4cf26d8a1662 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 18:34:10 -0400 Subject: [PATCH 19/45] query governance votes by bech32 address --- apps/web/src/hooks/useProposals.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/web/src/hooks/useProposals.ts b/apps/web/src/hooks/useProposals.ts index f7e819c..9c0de55 100644 --- a/apps/web/src/hooks/useProposals.ts +++ b/apps/web/src/hooks/useProposals.ts @@ -78,7 +78,12 @@ interface UseDepositOptions { } const useProposals = (options: UseDepositOptions = {}) => { - const { address, canSignCosmosTransactions, getClient } = useWalletConnect(); + const { + address, + bech32Address, + canSignCosmosTransactions, + getClient, + } = useWalletConnect(); const [proposalsInfo, setProposalsInfo] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -96,13 +101,13 @@ const useProposals = (options: UseDepositOptions = {}) => { const [userVotes, setUserVotes] = useState>({}); const refreshUserVote = async (proposalId: string) => { - if (!address) { + if (!bech32Address) { return; } try { const { data } = await axios.get( - `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposalId}/votes/${address}`, + `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposalId}/votes/${bech32Address}`, ); setUserVotes((current) => ({ ...current, @@ -144,7 +149,7 @@ const useProposals = (options: UseDepositOptions = {}) => { useEffect(() => { let cancelled = false; - if (!address) { + if (!bech32Address) { setUserVotes({}); return; } @@ -153,7 +158,7 @@ const useProposals = (options: UseDepositOptions = {}) => { const voteEntries = await Promise.all(proposalsInfo.map(async (proposal) => { try { const { data } = await axios.get( - `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposal.id}/votes/${address}`, + `${REST_AI_URL}/cosmos/gov/v1/proposals/${proposal.id}/votes/${bech32Address}`, ); return [proposal.id, data.vote] as const; } catch { @@ -171,7 +176,7 @@ const useProposals = (options: UseDepositOptions = {}) => { return () => { cancelled = true; }; - }, [address, proposalsInfo]); + }, [bech32Address, proposalsInfo]); useEffect(() => { if (options?.customMemo) { From c50b6503a7c4afa7bdfa24a4c89ffff2e7715de0 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 18:41:00 -0400 Subject: [PATCH 20/45] show active proposal voting countdown --- apps/web/src/components/CountDown.tsx | 37 +++++++------------- apps/web/src/utils/countdown.test.ts | 28 +++++++++++++++ apps/web/src/utils/countdown.ts | 30 ++++++++++++++++ packages/ui/src/screens/GovernanceScreen.tsx | 6 ++++ packages/ui/src/screens/HomeScreen.tsx | 6 ++++ 5 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/utils/countdown.test.ts create mode 100644 apps/web/src/utils/countdown.ts diff --git a/apps/web/src/components/CountDown.tsx b/apps/web/src/components/CountDown.tsx index c494e97..fc0cfc1 100644 --- a/apps/web/src/components/CountDown.tsx +++ b/apps/web/src/components/CountDown.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { getCountdownTimeLeft } from '@/utils/countdown'; interface CountdownProps { targetDate: Date; @@ -6,42 +7,28 @@ interface CountdownProps { className?: string; } -interface TimeLeft { - days: number; - hours: number; - minutes: number; - seconds: number; -} - const CountDown: React.FC = ({ targetDate, className = '' }) => { - const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + const targetTime = targetDate.getTime(); + // Keep the server and first client render deterministic; update after hydration. + const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); useEffect(() => { + const updateTimeLeft = () => setTimeLeft(getCountdownTimeLeft(new Date(targetTime))); + updateTimeLeft(); const timer = setInterval(() => { - const now = new Date().getTime(); - const distance = targetDate.getTime() - now; - - if (distance < 0) { - setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 }); - return; - } - - setTimeLeft({ - days: Math.floor(distance / (1000 * 60 * 60 * 24)), - hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), - minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)), - seconds: Math.floor((distance % (1000 * 60)) / 1000), - }); + updateTimeLeft(); }, 1000); return () => clearInterval(timer); - }, [targetDate]); + }, [targetTime]); return ( - {timeLeft.days} days {timeLeft.hours} hours {timeLeft.minutes} minutes {timeLeft.seconds} seconds + {timeLeft.days > 0 ? <>{timeLeft.days} days : null} + {timeLeft.hours > 0 ? <>{timeLeft.hours} hours : null} + {timeLeft.minutes} minutes {timeLeft.seconds} seconds ) }; -export default CountDown; \ No newline at end of file +export default CountDown; diff --git a/apps/web/src/utils/countdown.test.ts b/apps/web/src/utils/countdown.test.ts new file mode 100644 index 0000000..6576f2e --- /dev/null +++ b/apps/web/src/utils/countdown.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { getCountdownTimeLeft } from './countdown'; + +describe('getCountdownTimeLeft', () => { + it('calculates the remaining voting time', () => { + const now = Date.UTC(2026, 7, 13, 18, 0, 0); + const target = new Date(now + (((2 * 24 + 3) * 60 + 4) * 60 + 5) * 1000); + + expect(getCountdownTimeLeft(target, now)).toEqual({ + days: 2, + hours: 3, + minutes: 4, + seconds: 5, + }); + }); + + it('stays at zero after voting has ended', () => { + const now = Date.UTC(2026, 7, 13, 18, 0, 0); + + expect(getCountdownTimeLeft(new Date(now - 1), now)).toEqual({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }); + }); +}); diff --git a/apps/web/src/utils/countdown.ts b/apps/web/src/utils/countdown.ts new file mode 100644 index 0000000..839666e --- /dev/null +++ b/apps/web/src/utils/countdown.ts @@ -0,0 +1,30 @@ +export interface CountdownTimeLeft { + days: number; + hours: number; + minutes: number; + seconds: number; +} + +const EMPTY_COUNTDOWN: CountdownTimeLeft = { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, +}; + +export const getCountdownTimeLeft = ( + targetDate: Date, + now = Date.now(), +): CountdownTimeLeft => { + const distance = targetDate.getTime() - now; + if (!Number.isFinite(distance) || distance <= 0) { + return EMPTY_COUNTDOWN; + } + + return { + days: Math.floor(distance / (1000 * 60 * 60 * 24)), + hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), + minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)), + seconds: Math.floor((distance % (1000 * 60)) / 1000), + }; +}; diff --git a/packages/ui/src/screens/GovernanceScreen.tsx b/packages/ui/src/screens/GovernanceScreen.tsx index 2876230..fd5b423 100644 --- a/packages/ui/src/screens/GovernanceScreen.tsx +++ b/packages/ui/src/screens/GovernanceScreen.tsx @@ -23,6 +23,7 @@ import Loading from '@/components/Loading'; import DepositModal from '@/components/DepositModal'; import CreateProposalModal from '@/components/CreateProposalModal'; import Skeleton from '@/components/Skeleton'; +import CountDown from '@/components/CountDown'; import { IProposal } from '@/hooks/useProposals'; import { formatNumber, formatToken } from '@/utils/format'; import { @@ -482,6 +483,11 @@ export const GovernanceScreen = ({
{item.summary}
+ {item.status === 'PROPOSAL_STATUS_VOTING_PERIOD' && item.voting_end_time ? ( +
+ Voting ends in +
+ ) : null}
diff --git a/packages/ui/src/screens/HomeScreen.tsx b/packages/ui/src/screens/HomeScreen.tsx index 57ba761..f91a9fa 100644 --- a/packages/ui/src/screens/HomeScreen.tsx +++ b/packages/ui/src/screens/HomeScreen.tsx @@ -37,6 +37,7 @@ import Loading from '@/components/Loading'; import AppLink from '@/components/AppLink'; import { ConnectWalletButton } from '@/components/ConnectWallet'; import Skeleton from '@/components/Skeleton'; +import CountDown from '@/components/CountDown'; import { AccountInfoData, getTotalRewards } from '@/hooks/useAccountInfo'; import useAppRouter from '@/hooks/useAppRouter'; import { IRecentActivity, TMessage } from '@/hooks/useRecentActivity'; @@ -982,6 +983,11 @@ export const HomeScreen = ({ {item.title} {item.proposer} + {item.voting_end_time ? ( + + Voting ends in + + ) : null} {currentVoteLabel ? ( Your vote: {currentVoteLabel} From bd7b77e9943b953b1ae60c32edc82047865cead6 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 18:44:54 -0400 Subject: [PATCH 21/45] correct countdown unit plurals --- apps/web/src/components/CountDown.tsx | 13 +++++++++---- apps/web/src/utils/countdown.test.ts | 19 ++++++++++++++++++- apps/web/src/utils/countdown.ts | 5 +++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/CountDown.tsx b/apps/web/src/components/CountDown.tsx index fc0cfc1..c711d0a 100644 --- a/apps/web/src/components/CountDown.tsx +++ b/apps/web/src/components/CountDown.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { getCountdownTimeLeft } from '@/utils/countdown'; +import { getCountdownTimeLeft, getCountdownUnitLabel } from '@/utils/countdown'; interface CountdownProps { targetDate: Date; @@ -24,9 +24,14 @@ const CountDown: React.FC = ({ targetDate, className = '' }) => return ( - {timeLeft.days > 0 ? <>{timeLeft.days} days : null} - {timeLeft.hours > 0 ? <>{timeLeft.hours} hours : null} - {timeLeft.minutes} minutes {timeLeft.seconds} seconds + {timeLeft.days > 0 ? ( + <>{timeLeft.days} {getCountdownUnitLabel(timeLeft.days, 'day')} + ) : null} + {timeLeft.hours > 0 ? ( + <>{timeLeft.hours} {getCountdownUnitLabel(timeLeft.hours, 'hour')} + ) : null} + {timeLeft.minutes} {getCountdownUnitLabel(timeLeft.minutes, 'minute')}{' '} + {timeLeft.seconds} {getCountdownUnitLabel(timeLeft.seconds, 'second')} ) }; diff --git a/apps/web/src/utils/countdown.test.ts b/apps/web/src/utils/countdown.test.ts index 6576f2e..53a5d3a 100644 --- a/apps/web/src/utils/countdown.test.ts +++ b/apps/web/src/utils/countdown.test.ts @@ -1,6 +1,23 @@ import { describe, expect, it } from 'vitest'; -import { getCountdownTimeLeft } from './countdown'; +import { getCountdownTimeLeft, getCountdownUnitLabel, type CountdownUnit } from './countdown'; + +describe('getCountdownUnitLabel', () => { + it.each(['day', 'hour', 'minute', 'second'])( + 'uses the singular form for one %s', + (unit) => { + expect(getCountdownUnitLabel(1, unit)).toBe(unit); + }, + ); + + it.each(['day', 'hour', 'minute', 'second'])( + 'uses the plural form for zero and values above one: %s', + (unit) => { + expect(getCountdownUnitLabel(0, unit)).toBe(`${unit}s`); + expect(getCountdownUnitLabel(2, unit)).toBe(`${unit}s`); + }, + ); +}); describe('getCountdownTimeLeft', () => { it('calculates the remaining voting time', () => { diff --git a/apps/web/src/utils/countdown.ts b/apps/web/src/utils/countdown.ts index 839666e..17850e4 100644 --- a/apps/web/src/utils/countdown.ts +++ b/apps/web/src/utils/countdown.ts @@ -5,6 +5,11 @@ export interface CountdownTimeLeft { seconds: number; } +export type CountdownUnit = 'day' | 'hour' | 'minute' | 'second'; + +export const getCountdownUnitLabel = (value: number, unit: CountdownUnit): string => + value === 1 ? unit : `${unit}s`; + const EMPTY_COUNTDOWN: CountdownTimeLeft = { days: 0, hours: 0, From 874c7ad8fa8397cce03c4720aa4a73deecde64a9 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 18:57:23 -0400 Subject: [PATCH 22/45] cache staking overview data --- apps/web/src/app/staking/page.tsx | 8 + apps/web/src/hooks/useDelegate.ts | 18 +- apps/web/src/hooks/useStaking.ts | 255 ++++++++++++------ .../src/utils/staking-overview-cache.test.ts | 77 ++++++ apps/web/src/utils/staking-overview-cache.ts | 91 +++++++ .../components/AllValidators.tsx | 55 +++- .../ui/src/screens/StakingScreen/index.tsx | 38 ++- 7 files changed, 441 insertions(+), 101 deletions(-) create mode 100644 apps/web/src/utils/staking-overview-cache.test.ts create mode 100644 apps/web/src/utils/staking-overview-cache.ts diff --git a/apps/web/src/app/staking/page.tsx b/apps/web/src/app/staking/page.tsx index ca9e1ec..6437195 100644 --- a/apps/web/src/app/staking/page.tsx +++ b/apps/web/src/app/staking/page.tsx @@ -37,6 +37,9 @@ export default function Page() { const delegate = useDelegate({ availableAmount: `${getTotalBalances(accountInfo)}`, callback: fetchData, + validators: staking.activeValidators, + totalValidators: staking.totalValidators, + isValidatorDataLoading: staking.isLoading, }); const unbond = useUnbond({ callback: () => { @@ -99,6 +102,10 @@ export default function Page() { currentTab: staking.currentTab, params: staking.params, isLoading: staking.isLoading, + isRefreshing: staking.isRefreshing, + refreshProgress: staking.refreshProgress, + lastUpdated: staking.lastUpdated, + refreshError: staking.error, slashingParams: staking.slashingParams, signingInfos: staking.signingInfos, validatorTab: staking.validatorTab, @@ -115,6 +122,7 @@ export default function Page() { handleOpenModal: staking.handleOpenModal, handleCloseModal: staking.handleCloseModal, handleShowConfirmModal: staking.handleShowConfirmModal, + onRefresh: staking.refreshOverview, }} claim={{ onClaimButtonClick: handleClaimButtonClick, diff --git a/apps/web/src/hooks/useDelegate.ts b/apps/web/src/hooks/useDelegate.ts index 27185f0..a740d1a 100644 --- a/apps/web/src/hooks/useDelegate.ts +++ b/apps/web/src/hooks/useDelegate.ts @@ -16,6 +16,9 @@ interface UseDepositOptions { callback?: () => void; customMemo?: string; availableAmount?: string; + validators?: IValidator[]; + totalValidators?: string; + isValidatorDataLoading?: boolean; } const useDelegate = (options: UseDepositOptions = {}) => { @@ -37,6 +40,7 @@ const useDelegate = (options: UseDepositOptions = {}) => { const [isFetchValidatorLoading, setFetchValidatorLoading] = useState(false); const [transactionHash, setTransactionHash] = useState(''); const [selectedModal, setSelectedModal] = useState(''); + const effectiveValidators = options.validators ?? validators; const fetchValidator = useCallback(async () => { setFetchValidatorLoading(true); @@ -51,8 +55,10 @@ const useDelegate = (options: UseDepositOptions = {}) => { }, []); useEffect(() => { - fetchValidator(); - }, [fetchValidator]); + if (options.validators === undefined) { + fetchValidator(); + } + }, [fetchValidator, options.validators]); useEffect(() => { if (options?.customMemo) { @@ -80,7 +86,7 @@ const useDelegate = (options: UseDepositOptions = {}) => { const handleInputChange = (name: string, value: string) => { let newOptionsAdvanced = optionsAdvanced; if (name === 'validator') { - const item = validators.find((v) => v.operator_address === value); + const item = effectiveValidators.find((v) => v.operator_address === value); if (item) { newOptionsAdvanced = { ...newOptionsAdvanced, @@ -228,10 +234,10 @@ const useDelegate = (options: UseDepositOptions = {}) => { showAdvanced, isLoading, optionsAdvanced, - validators, + validators: effectiveValidators, isOpenModal, - totalValidators, - isFetchValidatorLoading, + totalValidators: options.totalValidators ?? totalValidators, + isFetchValidatorLoading: options.isValidatorDataLoading ?? isFetchValidatorLoading, transactionHash, selectedModal, handleStakingAmountChange, diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index b76fe15..0186b71 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -1,38 +1,68 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; import dayjs from 'dayjs'; import * as instance from '@/utils/api'; -import { DENOM } from '@/contants/network'; +import { CHAIN_ID, DENOM } from '@/contants/network'; import { useSelector, useDispatch } from '@/redux/hooks'; import { isNumber } from '@/utils/helpers'; import { canQueryCosmosAccountData } from '@/utils/cosmos-transactions'; import { setCurrentTab, setValidatorTab, setSubTab } from '@/redux/app.slice'; import { IValidator } from '@/types/validator'; import { TUnbondingDelegation } from '@/types'; +import { BONDED_VALIDATORS_PATH } from '@/utils/staking-validators'; +import { + readStakingOverviewCache, + writeStakingOverviewCache, + getStakingRefreshProgress, + type StakingOverviewCache, + type StakingParams, + type SlashingParams, +} from '@/utils/staking-overview-cache'; + +const DEFAULT_STAKING_PARAMS: StakingParams = { + bond_denom: DENOM, + historical_entries: 0, + max_entries: 0, + max_validators: 0, + min_commission_rate: '0', + unbonding_time: '0', +}; + +const DEFAULT_SLASHING_PARAMS: SlashingParams = { + signed_blocks_window: '0', + min_signed_per_window: '0', + downtime_jail_duration: '0s', + slash_fraction_double_sign: '0', + slash_fraction_downtime: '0', +}; + +const PUBLIC_STAKING_REQUEST_COUNT = 10; + +interface ApiResponse { + data: T; +} + +interface ValidatorsResponse { + validators: IValidator[]; + pagination?: { total?: string }; +} const useStaking = (address = '', isEvm = false) => { const dispatch = useDispatch(); const { currentTab, validatorTab, subTab } = useSelector((state) => state.app); - const [isLoading, setLoading] = useState(false); const [error, setError] = useState(''); + const [activeValidators, setActiveValidators] = useState([]); const [validators, setValidators] = useState([]); const [totalValidators, setTotalValidators] = useState('0'); - const [params, setParams] = useState({ - bond_denom: "ulume", - historical_entries: 0, - max_entries: 0, - max_validators: 0, - min_commission_rate: '0', - unbonding_time: '0', - }); - const [slashingParams, setSlashingParams] = useState({ - signed_blocks_window: "0", - min_signed_per_window: "0", - downtime_jail_duration: "0s", - slash_fraction_double_sign: "0", - slash_fraction_downtime: "0" - }); - const [signingInfos, setSigningInfos] = useState([]); + const [params, setParams] = useState(DEFAULT_STAKING_PARAMS); + const [slashingParams, setSlashingParams] = useState(DEFAULT_SLASHING_PARAMS); + const [signingInfos, setSigningInfos] = useState([]); + const [hasLoadedOverview, setHasLoadedOverview] = useState(false); + const [isRefreshing, setRefreshing] = useState(false); + const [refreshProgress, setRefreshProgress] = useState(0); + const [lastUpdated, setLastUpdated] = useState(null); + const refreshingRef = useRef(false); + const initializedRef = useRef(false); const [rewards, setRewards] = useState([]); const [isActivitiesLoading, setActivitiesLoading] = useState(false); const [activities, setActivities] = useState([]); @@ -41,7 +71,6 @@ const useStaking = (address = '', isEvm = false) => { const [unbondingDelegations, setUnbondingDelegations] = useState([]); const [unbondingDelegationsError, setUnbondingDelegationsError] = useState(''); const [apr, setAPR] = useState(0); - const [isAPRLoading, setAPRLoading] = useState(false); const [bondedTokens, setBondedTokens] = useState(0); const [selectedModal, setSelectedModal] = useState(''); const [selectedData, setSelectedData] = useState({ @@ -51,38 +80,118 @@ const useStaking = (address = '', isEvm = false) => { rewards: '', }) - const fetchValidator = async () => { - setLoading(true); - try { - const [undondingRes, unbondedRes] = await Promise.all([ - instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=1000&status=BOND_STATUS_UNBONDING&pagination.count_total=true'), - instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=300&status=BOND_STATUS_UNBONDED'), - ]); - const allValidators = [...undondingRes.data.validators, ...unbondedRes.data.validators] as IValidator[]; - setValidators(allValidators); - setTotalValidators(`${allValidators.length}`); - } catch (error) { - setError(error instanceof Error ? error.message : 'An unknown error occurred.'); - } - setLoading(false); - } + const applyOverview = useCallback((overview: StakingOverviewCache) => { + setActiveValidators(overview.activeValidators); + setValidators(overview.inactiveValidators); + setTotalValidators(overview.activeValidatorTotal); + setParams(overview.params); + setSlashingParams(overview.slashingParams); + setSigningInfos(overview.signingInfos); + setAPR(overview.apr); + setBondedTokens(overview.bondedTokens); + setLastUpdated(overview.updatedAt); + setHasLoadedOverview(true); + }, []); + + const refreshOverview = useCallback(async () => { + if (refreshingRef.current) return; + + refreshingRef.current = true; + setRefreshing(true); + setRefreshProgress(0); + setError(''); + let completedRequests = 0; + const track = async (request: Promise): Promise => { + try { + return await request; + } finally { + completedRequests += 1; + setRefreshProgress(getStakingRefreshProgress( + completedRequests, + PUBLIC_STAKING_REQUEST_COUNT, + )); + } + }; - const fetchParams = async () => { - setLoading(true); try { - const [stakingParamsRes, slashingParamsRes, signingInfosRes] = await Promise.all([ - instance.get('/cosmos/staking/v1beta1/params'), - instance.get('/cosmos/slashing/v1beta1/params'), - instance.get('/cosmos/slashing/v1beta1/signing_infos?pagination.limit=300'), + const results = await Promise.allSettled([ + track(instance.get(BONDED_VALIDATORS_PATH)), + track(instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=1000&status=BOND_STATUS_UNBONDING&pagination.count_total=true')), + track(instance.get('/cosmos/staking/v1beta1/validators?pagination.limit=300&status=BOND_STATUS_UNBONDED')), + track(instance.get('/cosmos/staking/v1beta1/params')), + track(instance.get('/cosmos/slashing/v1beta1/params')), + track(instance.get('/cosmos/slashing/v1beta1/signing_infos?pagination.limit=300')), + track(instance.get('/cosmos/mint/v1beta1/inflation')), + track(instance.get('/cosmos/staking/v1beta1/pool')), + track(instance.get('/cosmos/bank/v1beta1/supply')), + track(instance.get('/cosmos/distribution/v1beta1/params')), ]); - setParams(stakingParamsRes.data.params); - setSlashingParams(slashingParamsRes.data.params); - setSigningInfos(signingInfosRes.data.info); - } catch (error) { - console.error(error instanceof Error ? error.message : 'An unknown error occurred.'); + const failedRequest = results.find((result): result is PromiseRejectedResult => result.status === 'rejected'); + if (failedRequest) throw failedRequest.reason; + + const responses = results.map((result) => ( + result as PromiseFulfilledResult> + ).value); + const [ + activeRes, + unbondingRes, + unbondedRes, + stakingParamsRes, + slashingParamsRes, + signingInfosRes, + inflationRes, + poolRes, + supplyRes, + distributionParamsRes, + ] = responses as [ + ApiResponse, + ApiResponse, + ApiResponse, + ApiResponse<{ params: StakingParams }>, + ApiResponse<{ params: SlashingParams }>, + ApiResponse<{ info: StakingOverviewCache['signingInfos'] }>, + ApiResponse<{ inflation: string }>, + ApiResponse<{ pool: { bonded_tokens: string } }>, + ApiResponse<{ supply: { denom: string; amount: string }[] }>, + ApiResponse<{ params: { community_tax: string } }>, + ]; + const inactiveValidators = [ + ...unbondingRes.data.validators, + ...unbondedRes.data.validators, + ] as IValidator[]; + const totalSupply = supplyRes.data.supply.reduce( + (total: number, coin: { denom: string; amount: string }) => + coin.denom === DENOM ? total + Number(coin.amount) : total, + 0, + ); + const bondedTokens = Number(poolRes.data.pool.bonded_tokens); + const bondedRatio = bondedTokens / totalSupply; + const aprValue = Number(inflationRes.data.inflation) / bondedRatio + * (1 - Number(distributionParamsRes.data.params.community_tax)); + const updatedOverview: StakingOverviewCache = { + version: 1, + updatedAt: Date.now(), + activeValidators: activeRes.data.validators, + inactiveValidators, + activeValidatorTotal: activeRes.data.pagination?.total + ?? `${activeRes.data.validators.length}`, + params: stakingParamsRes.data.params, + slashingParams: slashingParamsRes.data.params, + signingInfos: signingInfosRes.data.info, + apr: isNumber(aprValue) ? aprValue * 100 : 0, + bondedTokens, + }; + + applyOverview(updatedOverview); + writeStakingOverviewCache(window.localStorage, CHAIN_ID, updatedOverview); + } catch (refreshError) { + setError(refreshError instanceof Error ? refreshError.message : 'Unable to update staking data.'); + setHasLoadedOverview(true); + } finally { + setRefreshing(false); + refreshingRef.current = false; } - setLoading(false); - } + }, [applyOverview]); const fetchRewards = async () => { if (!canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { @@ -159,34 +268,6 @@ const useStaking = (address = '', isEvm = false) => { setUnbondingDelegationsLoading(false); }, [address, isEvm]); - const fetchDataForAPR = async () => { - setAPRLoading(true); - try { - const [resInflation, resPool, resSupply, resParams] = await Promise.all([ - instance.get('/cosmos/mint/v1beta1/inflation'), - instance.get('/cosmos/staking/v1beta1/pool'), - instance.get('/cosmos/bank/v1beta1/supply'), - instance.get('/cosmos/distribution/v1beta1/params'), - ]); - let totalSupply = 0; - for (const item of resSupply.data.supply) { - if (item.denom === DENOM) { - totalSupply += Number(item.amount); - } - } - const inflation = Number(resInflation.data.inflation); - const communityTax = Number(resParams.data.params.community_tax); - const bondedTokens = Number(resPool.data.pool.bonded_tokens); - const bondedRatio = bondedTokens / totalSupply; - const aprVal = inflation / bondedRatio * (1 - communityTax); - setAPR(isNumber(aprVal) ? aprVal * 100 : 0); - setBondedTokens(bondedTokens); - } catch (error) { - console.error('fetchDataForAPR', error); - } - setAPRLoading(false); - } - const handleFetchDataForSubTab = useCallback((_subTab: string) => { switch (_subTab) { case 'activities': @@ -201,12 +282,13 @@ const useStaking = (address = '', isEvm = false) => { }, [fetchActivities, fetchUnbondingDelegations]); useEffect(() => { - fetchValidator(); - if (validatorTab === 'all') { - fetchParams(); - fetchDataForAPR(); - } - }, [validatorTab]); + if (initializedRef.current) return; + initializedRef.current = true; + + const cachedOverview = readStakingOverviewCache(window.localStorage, CHAIN_ID); + if (cachedOverview) applyOverview(cachedOverview); + void refreshOverview(); + }, [applyOverview, refreshOverview]); useEffect(() => { if (canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { @@ -275,8 +357,12 @@ const useStaking = (address = '', isEvm = false) => { } return { - isLoading, + isLoading: !hasLoadedOverview, + isRefreshing, + refreshProgress, + lastUpdated, error, + activeValidators, validators, totalValidators, currentTab, @@ -293,11 +379,12 @@ const useStaking = (address = '', isEvm = false) => { unbondingDelegations, unbondingDelegationsError, apr, - isAPRLoading, + isAPRLoading: !hasLoadedOverview, bondedTokens, selectedModal, selectedData, fetchUnbondingDelegations, + refreshOverview, handleShowConfirmModal, handleOpenModal, handleCloseModal, diff --git a/apps/web/src/utils/staking-overview-cache.test.ts b/apps/web/src/utils/staking-overview-cache.test.ts new file mode 100644 index 0000000..35f5a34 --- /dev/null +++ b/apps/web/src/utils/staking-overview-cache.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + getStakingOverviewCacheKey, + getStakingRefreshProgress, + readStakingOverviewCache, + writeStakingOverviewCache, + type StakingOverviewCache, +} from './staking-overview-cache'; + +const cache: StakingOverviewCache = { + version: 1, + updatedAt: 1_786_640_000_000, + activeValidators: [], + inactiveValidators: [], + activeValidatorTotal: '0', + params: { + bond_denom: 'ulume', + historical_entries: 0, + max_entries: 7, + max_validators: 100, + min_commission_rate: '0.05', + unbonding_time: '1814400s', + }, + slashingParams: { + signed_blocks_window: '10000', + min_signed_per_window: '0.5', + downtime_jail_duration: '600s', + slash_fraction_double_sign: '0.05', + slash_fraction_downtime: '0.0001', + }, + signingInfos: [], + apr: 12.5, + bondedTokens: 123, +}; + +describe('staking overview cache', () => { + it('uses a chain-specific versioned key', () => { + expect(getStakingOverviewCacheKey('lumera-testnet-2')) + .toBe('lumera-hub:staking-overview:lumera-testnet-2:v1'); + }); + + it('round-trips a valid cache entry', () => { + let serialized = ''; + const storage = { + getItem: vi.fn(() => serialized || null), + setItem: vi.fn((_key: string, value: string) => { serialized = value; }), + }; + + writeStakingOverviewCache(storage, 'lumera-testnet-2', cache); + + expect(storage.setItem).toHaveBeenCalledWith( + getStakingOverviewCacheKey('lumera-testnet-2'), + JSON.stringify(cache), + ); + expect(readStakingOverviewCache(storage, 'lumera-testnet-2')).toEqual(cache); + }); + + it('ignores malformed and incompatible entries', () => { + expect(readStakingOverviewCache({ getItem: () => 'not-json' }, 'chain')).toBeNull(); + expect(readStakingOverviewCache({ getItem: () => '{"version":2}' }, 'chain')).toBeNull(); + }); + + it('does not fail when storage rejects a write', () => { + expect(() => writeStakingOverviewCache({ + setItem: () => { throw new Error('quota exceeded'); }, + }, 'chain', cache)).not.toThrow(); + }); + + it('reports bounded refresh progress as a percentage', () => { + expect(getStakingRefreshProgress(0, 10)).toBe(0); + expect(getStakingRefreshProgress(3, 10)).toBe(30); + expect(getStakingRefreshProgress(10, 10)).toBe(100); + expect(getStakingRefreshProgress(11, 10)).toBe(100); + expect(getStakingRefreshProgress(1, 0)).toBe(0); + }); +}); diff --git a/apps/web/src/utils/staking-overview-cache.ts b/apps/web/src/utils/staking-overview-cache.ts new file mode 100644 index 0000000..71bc756 --- /dev/null +++ b/apps/web/src/utils/staking-overview-cache.ts @@ -0,0 +1,91 @@ +import type { TSigningInfos } from '@/types'; +import type { IValidator } from '@/types/validator'; + +export interface StakingParams { + bond_denom: string; + historical_entries: number; + max_entries: number; + max_validators: number; + min_commission_rate: string; + unbonding_time: string; +} + +export interface SlashingParams { + signed_blocks_window: string; + min_signed_per_window: string; + downtime_jail_duration: string; + slash_fraction_double_sign: string; + slash_fraction_downtime: string; +} + +export interface StakingOverviewCache { + version: 1; + updatedAt: number; + activeValidators: IValidator[]; + inactiveValidators: IValidator[]; + activeValidatorTotal: string; + params: StakingParams; + slashingParams: SlashingParams; + signingInfos: TSigningInfos[]; + apr: number; + bondedTokens: number; +} + +interface CacheStorage { + getItem: (key: string) => string | null; + setItem: (key: string, value: string) => void; +} + +export const getStakingOverviewCacheKey = (chainId: string) => + `lumera-hub:staking-overview:${chainId}:v1`; + +export const getStakingRefreshProgress = (completed: number, total: number): number => { + if (!Number.isFinite(completed) || !Number.isFinite(total) || total <= 0) return 0; + return Math.min(100, Math.max(0, Math.round((completed / total) * 100))); +}; + +export const isStakingOverviewCache = (value: unknown): value is StakingOverviewCache => { + if (!value || typeof value !== 'object') return false; + + const cache = value as Partial; + return cache.version === 1 + && typeof cache.updatedAt === 'number' + && Number.isFinite(cache.updatedAt) + && Array.isArray(cache.activeValidators) + && Array.isArray(cache.inactiveValidators) + && typeof cache.activeValidatorTotal === 'string' + && Boolean(cache.params && typeof cache.params.bond_denom === 'string') + && Boolean(cache.slashingParams && typeof cache.slashingParams.signed_blocks_window === 'string') + && Array.isArray(cache.signingInfos) + && typeof cache.apr === 'number' + && Number.isFinite(cache.apr) + && typeof cache.bondedTokens === 'number' + && Number.isFinite(cache.bondedTokens); +}; + +export const readStakingOverviewCache = ( + storage: Pick, + chainId: string, +): StakingOverviewCache | null => { + try { + const serialized = storage.getItem(getStakingOverviewCacheKey(chainId)); + if (!serialized) return null; + + const parsed: unknown = JSON.parse(serialized); + return isStakingOverviewCache(parsed) ? parsed : null; + } catch { + return null; + } +}; + +export const writeStakingOverviewCache = ( + storage: Pick, + chainId: string, + cache: StakingOverviewCache, +) => { + try { + storage.setItem(getStakingOverviewCacheKey(chainId), JSON.stringify(cache)); + } catch { + // Rendering fresh data should still succeed when browser storage is unavailable or full. + } +}; diff --git a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx index a94f537..97ead6d 100644 --- a/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx +++ b/packages/ui/src/screens/StakingScreen/components/AllValidators.tsx @@ -12,6 +12,7 @@ import { Search } from '@tamagui/lucide-icons'; import { ArrowUp, ArrowDown, + RefreshCw, } from 'lucide-react'; import AppLink from '@/components/AppLink'; @@ -28,6 +29,10 @@ import { calculatePercent } from '@/utils/helpers'; interface IAllValidators { staking: { isLoading: boolean; + isRefreshing: boolean; + refreshProgress: number; + lastUpdated: number | null; + refreshError: string; params: { bond_denom: string; historical_entries: number; @@ -38,6 +43,7 @@ interface IAllValidators { }; currentTab: string; onTabChange: (tab: string) => void; + onRefresh: () => Promise; validators: IValidator[]; } totalPower: number; @@ -61,6 +67,7 @@ export default function AllValidators({ const [keyword, setKeyword] = useState(''); const [sortBy, setSortBy] = useState('uptime'); const [sort, setSort] = useState('DESC'); + const refreshProgress = Math.min(100, Math.max(0, staking.refreshProgress)); useEffect(() => { setSortBy('uptime'); @@ -103,7 +110,7 @@ export default function AllValidators({ if (keyword) { validators = validators.filter((validator) => validator.description.moniker.toLowerCase().indexOf(keyword.toLowerCase()) !== -1); } - return [...validators.sort((a, b) => sortFunc(a, b))]; + return [...validators].sort((a, b) => sortFunc(a, b)); } const handleInputChange = (text: string) => { @@ -156,6 +163,52 @@ export default function AllValidators({

All Validators

Delegate your stake to a validator to earn rewards.
+
+
+
+ Last updated: {staking.lastUpdated + ? new Date(staking.lastUpdated).toLocaleString() + : 'Not yet updated'} +
+ {staking.isRefreshing ? ( +
Updating {refreshProgress}%
+ ) : null} + {!staking.isRefreshing && staking.refreshError ? ( +
+ {staking.lastUpdated + ? 'Update failed. Showing cached data.' + : 'Unable to load staking data.'} +
+ ) : null} +
+ +
+
+ {staking.isRefreshing ? ( +
+
+
+ ) : null} +
void; + onRefresh: () => Promise; }; accountInfo: AccountInfoData | null; claim: { @@ -244,6 +250,27 @@ export const StakingScreen = ({ const totalPower = calculateTotalPower(getValidators()); + const validatorUptime = useMemo(() => { + const signingByAddress = new Map( + staking.signingInfos.map((item) => [valconsToBase64(item.address), item]), + ); + const window = Number(staking.slashingParams.signed_blocks_window || 0); + const uptimeByOperator = new Map(); + + for (const validator of [...delegateOptions.validators, ...staking.validators]) { + const hex = consensusPubkeyToHexAddress(validator.consensus_pubkey); + const signing = hex ? signingByAddress.get(toBase64(fromHex(hex))) : undefined; + uptimeByOperator.set( + validator.operator_address, + signing && window > 0 + ? (window - Number(signing.missed_blocks_counter)) / window + : 0, + ); + } + + return uptimeByOperator; + }, [delegateOptions.validators, staking.signingInfos, staking.slashingParams.signed_blocks_window, staking.validators]); + const getMyTotalStaked = () => { if (staking.validatorTab === 'my') { return accountInfo?.delegations?.reduce((total, item) => Number(item.balance.amount) + total, 0) || 0; @@ -252,16 +279,7 @@ export const StakingScreen = ({ } const getUptime = (validator: IValidator) => { - const slashingParams = staking.slashingParams; - const signingInfos = staking.signingInfos; - const hex = consensusPubkeyToHexAddress(validator.consensus_pubkey); - const window = Number(slashingParams.signed_blocks_window || 0); - const signing = signingInfos.find((item) => { - return toBase64(fromHex(hex)) === valconsToBase64(item.address) - }); - return signing && window > 0 - ? (window - Number(signing.missed_blocks_counter)) / window - : 0 + return validatorUptime.get(validator.operator_address) ?? 0; } const getTotalRewards = () => { From cdb6e761516f51d767a4d4721096bdea47dbec9c Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 19:00:38 -0400 Subject: [PATCH 23/45] update project configuration --- README.md | 2 +- .../.tamagui/tamagui-components.config.cjs | 378 +++++++++--------- apps/web/.tamagui/tamagui.config.cjs | 262 ++++++------ 3 files changed, 321 insertions(+), 321 deletions(-) diff --git a/README.md b/README.md index f7cbfb4..64b5b55 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ cp apps/web/.env.example apps/web/.env.local ``` ```dotenv -NEXT_PUBLIC_NETWORK_PROFILE=testnet +q ``` Supported profiles are `devnet`, `testnet`, and `mainnet`. Their chain IDs and endpoints are defined together in `apps/web/src/contants/network.ts`. Individual `NEXT_PUBLIC_CHAIN_NAME`, `NEXT_PUBLIC_CHAIN_ID`, `NEXT_PUBLIC_RPC_ENDPOINT`, `NEXT_PUBLIC_REST_AI_URL`, `NEXT_PUBLIC_EVM_RPC_ENDPOINT`, `NEXT_PUBLIC_EVM_WS_ENDPOINT`, `NEXT_PUBLIC_EVM_CHAIN_ID`, and `NEXT_PUBLIC_SNAPI_URL` values can still override the selected profile. diff --git a/apps/web/.tamagui/tamagui-components.config.cjs b/apps/web/.tamagui/tamagui-components.config.cjs index 9efe5cf..3603175 100644 --- a/apps/web/.tamagui/tamagui-components.config.cjs +++ b/apps/web/.tamagui/tamagui-components.config.cjs @@ -21193,7 +21193,7 @@ var require_dist = __commonJS({ } }); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/index.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/index.mjs var esm_exports = {}; __export(esm_exports, { ACTIONS: () => ACTIONS, @@ -21530,7 +21530,7 @@ __export(esm_exports, { }); module.exports = __toCommonJS(esm_exports); -// ../../node_modules/.pnpm/@tamagui+constants@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/constants/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+constants@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_nivd3v3e2nsflxlvuvlf4fa5p4/node_modules/@tamagui/constants/dist/esm/constants.mjs var import_react = require("react"); var import_react2 = require("react"); var isWeb = true; @@ -21555,10 +21555,10 @@ function useForceUpdate() { } __name(useForceUpdate, "useForceUpdate"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_zj6msapkgdk7h5sfeyjzj5huze/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs var import_react6 = require("react"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/LayoutGroupContext.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_zj6msapkgdk7h5sfeyjzj5huze/node_modules/@tamagui/animate-presence/dist/esm/LayoutGroupContext.mjs var import_react4 = __toESM(require("react"), 1); var LayoutGroupContext = import_react4.default.createContext({}); @@ -21573,7 +21573,7 @@ function useConstant(fn) { } __name(useConstant, "useConstant"); -// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_hwowyxiuo2sdaztcnn2obknkua/node_modules/@tamagui/use-presence/dist/esm/PresenceContext.mjs +// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_xftudic26c2isw4ocxvma5w3ha/node_modules/@tamagui/use-presence/dist/esm/PresenceContext.mjs var React4 = __toESM(require("react"), 1); var import_jsx_runtime = require("react/jsx-runtime"); var PresenceContext = React4.createContext(null); @@ -21585,7 +21585,7 @@ var ResetPresence = /* @__PURE__ */ __name((props) => { }); }, "ResetPresence"); -// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_hwowyxiuo2sdaztcnn2obknkua/node_modules/@tamagui/use-presence/dist/esm/usePresence.mjs +// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_xftudic26c2isw4ocxvma5w3ha/node_modules/@tamagui/use-presence/dist/esm/usePresence.mjs var React5 = __toESM(require("react"), 1); function usePresence() { const context2 = React5.useContext(PresenceContext); @@ -21608,7 +21608,7 @@ function isPresent(context2) { } __name(isPresent, "isPresent"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/PresenceChild.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_zj6msapkgdk7h5sfeyjzj5huze/node_modules/@tamagui/animate-presence/dist/esm/PresenceChild.mjs var React6 = __toESM(require("react"), 1); var import_react5 = require("react"); var import_jsx_runtime2 = require("react/jsx-runtime"); @@ -21661,7 +21661,7 @@ function newChildrenMap() { } __name(newChildrenMap, "newChildrenMap"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_zj6msapkgdk7h5sfeyjzj5huze/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs var import_jsx_runtime3 = require("react/jsx-runtime"); var getChildKey = /* @__PURE__ */ __name((child) => child.key || "", "getChildKey"); function updateChildLookup(children, allChildren) { @@ -21795,13 +21795,13 @@ function isValidCSSCharCode(code) { } __name(isValidCSSCharCode, "isValidCSSCharCode"); -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/clamp.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/clamp.mjs function clamp(value, [min2, max2]) { return Math.min(max2, Math.max(min2, value)); } __name(clamp, "clamp"); -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/composeEventHandlers.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/composeEventHandlers.mjs function composeEventHandlers(og, next, { checkDefaultPrevented = true } = {}) { @@ -21812,14 +21812,14 @@ function composeEventHandlers(og, next, { } __name(composeEventHandlers, "composeEventHandlers"); -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/types.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/types.mjs var StyleObjectProperty = 0; var StyleObjectValue = 1; var StyleObjectIdentifier = 2; var StyleObjectPseudo = 3; var StyleObjectRules = 4; -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/shouldRenderNativePlatform.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/shouldRenderNativePlatform.mjs var ALL_PLATFORMS = ["web", "android", "ios"]; function shouldRenderNativePlatform(nativeProp) { if (!nativeProp) return null; @@ -21834,7 +21834,7 @@ function resolvePlatformNames(nativeProp) { } __name(resolvePlatformNames, "resolvePlatformNames"); -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/validStyleProps.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/validStyleProps.mjs var textColors = { color: true, textDecorationColor: true, @@ -22150,7 +22150,7 @@ var validPseudoKeys = { }; var validStyles = stylePropsView; -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/withStaticProperties.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_toml3zfnk3gjkibfesbwernkay/node_modules/@tamagui/helpers/dist/esm/withStaticProperties.mjs var import_react7 = __toESM(require("react"), 1); var Decorated = Symbol(); var withStaticProperties = /* @__PURE__ */ __name((component, staticProps) => { @@ -22172,7 +22172,7 @@ var withStaticProperties = /* @__PURE__ */ __name((component, staticProps) => { return Object.assign(next, staticProps), next[Decorated] = true, next; }, "withStaticProperties"); -// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/use-event/dist/esm/useGet.mjs +// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_coxfms2uluc4vi7yxenwrpnlh4/node_modules/@tamagui/use-event/dist/esm/useGet.mjs var React8 = __toESM(require("react"), 1); function useGet(currentValue, initialValue2, forwardToFunction) { const curRef = React8.useRef(initialValue2 ?? currentValue); @@ -22182,7 +22182,7 @@ function useGet(currentValue, initialValue2, forwardToFunction) { } __name(useGet, "useGet"); -// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/use-event/dist/esm/useEvent.mjs +// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_coxfms2uluc4vi7yxenwrpnlh4/node_modules/@tamagui/use-event/dist/esm/useEvent.mjs function useEvent(callback) { return useGet(callback, defaultValue, true); } @@ -22191,7 +22191,7 @@ var defaultValue = /* @__PURE__ */ __name(() => { throw new Error("Cannot call an event handler while rendering."); }, "defaultValue"); -// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_mfcgnw6hqeenclbq4ofkf3hyxq/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs +// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_2jzdyh5msyhvsqlr577nd3gubi/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs var React9 = __toESM(require("react"), 1); // ../../node_modules/.pnpm/@tamagui+start-transition@1.132.17_react@19.1.0/node_modules/@tamagui/start-transition/dist/esm/index.mjs @@ -22200,7 +22200,7 @@ var startTransition = /* @__PURE__ */ __name((callback) => { (0, import_react8.startTransition)(callback); }, "startTransition"); -// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_mfcgnw6hqeenclbq4ofkf3hyxq/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs +// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_2jzdyh5msyhvsqlr577nd3gubi/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs var emptyCallbackFn = /* @__PURE__ */ __name((_) => _(), "emptyCallbackFn"); function useControllableState({ prop, @@ -22232,7 +22232,7 @@ __name(useControllableState, "useControllableState"); var idFn2 = /* @__PURE__ */ __name(() => { }, "idFn"); -// ../../node_modules/.pnpm/@tamagui+collapsible@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_yk3652m4gdjla6kpiyzgiolv7m/node_modules/@tamagui/collapsible/dist/esm/Collapsible.mjs +// ../../node_modules/.pnpm/@tamagui+collapsible@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_ixty7mii2qk4cfbh4qxol3dka4/node_modules/@tamagui/collapsible/dist/esm/Collapsible.mjs var import_web = require("@tamagui/core"); var React10 = __toESM(require("react"), 1); var import_jsx_runtime4 = require("react/jsx-runtime"); @@ -22343,7 +22343,7 @@ function useComposedRefs(...refs) { } __name(useComposedRefs, "useComposedRefs"); -// ../../node_modules/.pnpm/@tamagui+collection@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7_zmxqlusikhank74xsa43fjfdze/node_modules/@tamagui/collection/dist/esm/Collection.mjs +// ../../node_modules/.pnpm/@tamagui+collection@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7_sbj7nlwehvtm6cdxdwdzqivxdu/node_modules/@tamagui/collection/dist/esm/Collection.mjs var import_core = require("@tamagui/core"); var import_react9 = __toESM(require("react"), 1); var import_jsx_runtime5 = require("react/jsx-runtime"); @@ -22415,13 +22415,13 @@ function createCollection(name) { } __name(createCollection, "createCollection"); -// ../../node_modules/.pnpm/@tamagui+accordion@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._bvk3fg6tsila3nnwdfjhwrm4fi/node_modules/@tamagui/accordion/dist/esm/Accordion.mjs +// ../../node_modules/.pnpm/@tamagui+accordion@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._jag3swyrnf4ryglqt4amxzikcq/node_modules/@tamagui/accordion/dist/esm/Accordion.mjs var import_core6 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs var import_core3 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/getElevation.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/getElevation.mjs var import_core2 = require("@tamagui/core"); var getElevation = /* @__PURE__ */ __name((size4, extras) => { if (!size4) return; @@ -22454,7 +22454,7 @@ var getSizedElevation = /* @__PURE__ */ __name((val, { }; }, "getSizedElevation"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs var fullscreenStyle = { position: "absolute", top: 0, @@ -22496,10 +22496,10 @@ var ZStack = (0, import_core3.styled)(YStack, { }); ZStack.displayName = "ZStack"; -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/SizableStack.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/SizableStack.mjs var import_core4 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+get-token@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._zt3ejghu7v62ucxsmck4ym7fmu/node_modules/@tamagui/get-token/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+get-token@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._fsvrv5walafl4wiorbunxeczuu/node_modules/@tamagui/get-token/dist/esm/index.mjs var import_web2 = require("@tamagui/core"); var defaultOptions = { shift: 0, @@ -22530,7 +22530,7 @@ var stepTokenUpOrDown = /* @__PURE__ */ __name((type, current, options = default }, "stepTokenUpOrDown"); var getTokenRelative = stepTokenUpOrDown; -// ../../node_modules/.pnpm/@tamagui+get-button-sized@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_gzqf5iq7vkxwkkqkj3ftfud7mi/node_modules/@tamagui/get-button-sized/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+get-button-sized@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_ro2wpk3wusu6twaxk5uytpsntq/node_modules/@tamagui/get-button-sized/dist/esm/index.mjs var getButtonSized = /* @__PURE__ */ __name((val, { tokens, props @@ -22549,7 +22549,7 @@ var getButtonSized = /* @__PURE__ */ __name((val, { }; }, "getButtonSized"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/variants.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/variants.mjs var elevate = { true: /* @__PURE__ */ __name((_, extras) => getElevation(extras.props.size, extras), "true") }; @@ -22648,7 +22648,7 @@ var focusTheme = { false: {} }; -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/SizableStack.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/SizableStack.mjs var SizableStack = (0, import_core4.styled)(XStack, { name: "SizableStack", variants: { @@ -22673,7 +22673,7 @@ var SizableStack = (0, import_core4.styled)(XStack, { } }); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/ThemeableStack.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/ThemeableStack.mjs var import_core5 = require("@tamagui/core"); var chromelessStyle = { backgroundColor: "transparent", @@ -22716,11 +22716,11 @@ var ThemeableStack = (0, import_core5.styled)(YStack, { variants: themeableVariants }); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/NestingContext.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qbhaj2j3nixhholzziviciuanm/node_modules/@tamagui/stacks/dist/esm/NestingContext.mjs var import_react10 = __toESM(require("react"), 1); var ButtonNestingContext = import_react10.default.createContext(false); -// ../../node_modules/.pnpm/@tamagui+get-font-sized@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_rbbvgapwpxd4conn2yxogzeuyu/node_modules/@tamagui/get-font-sized/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+get-font-sized@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_mwbhyq2xjkbmrslowkmnnf4e3e/node_modules/@tamagui/get-font-sized/dist/esm/index.mjs var import_web3 = require("@tamagui/core"); var getFontSized = /* @__PURE__ */ __name((sizeTokenIn = "$true", { font, @@ -22760,7 +22760,7 @@ function getDefaultSizeToken(font) { } __name(getDefaultSizeToken, "getDefaultSizeToken"); -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/SizableText.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__35i3yn3egcazkzjixosyqx6mmm/node_modules/@tamagui/text/dist/esm/SizableText.mjs var import_web4 = require("@tamagui/core"); var SizableText2 = (0, import_web4.styled)(import_web4.Text, { name: "SizableText", @@ -22785,7 +22785,7 @@ SizableText2.staticConfig.variants.fontFamily = { }, "...") }; -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/Paragraph.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__35i3yn3egcazkzjixosyqx6mmm/node_modules/@tamagui/text/dist/esm/Paragraph.mjs var import_web5 = require("@tamagui/core"); var Paragraph = (0, import_web5.styled)(SizableText2, { name: "Paragraph", @@ -22796,7 +22796,7 @@ var Paragraph = (0, import_web5.styled)(SizableText2, { whiteSpace: "normal" }); -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/Headings.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__35i3yn3egcazkzjixosyqx6mmm/node_modules/@tamagui/text/dist/esm/Headings.mjs var import_web6 = require("@tamagui/core"); var Heading = (0, import_web6.styled)(Paragraph, { tag: "span", @@ -22891,7 +22891,7 @@ var H6 = (0, import_web6.styled)(Heading, { } }); -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/wrapChildrenInText.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__35i3yn3egcazkzjixosyqx6mmm/node_modules/@tamagui/text/dist/esm/wrapChildrenInText.mjs var import_react11 = __toESM(require("react"), 1); var import_jsx_runtime6 = require("react/jsx-runtime"); function wrapChildrenInText(TextComponent, propsIn, extraProps) { @@ -22934,7 +22934,7 @@ function useDirection(localDir) { } __name(useDirection, "useDirection"); -// ../../node_modules/.pnpm/@tamagui+accordion@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._bvk3fg6tsila3nnwdfjhwrm4fi/node_modules/@tamagui/accordion/dist/esm/Accordion.mjs +// ../../node_modules/.pnpm/@tamagui+accordion@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._jag3swyrnf4ryglqt4amxzikcq/node_modules/@tamagui/accordion/dist/esm/Accordion.mjs var React16 = __toESM(require("react"), 1); var import_jsx_runtime8 = require("react/jsx-runtime"); var ACCORDION_NAME = "Accordion"; @@ -23239,7 +23239,7 @@ var Accordion = withStaticProperties(AccordionComponent, { HeightAnimator }); -// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_l3lpzlqd7gjells3fjeiwokuti/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs +// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_c7zmbqwar3idlbt2gioq7uclxe/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs var import_core7 = require("@tamagui/core"); // ../../node_modules/.pnpm/@tamagui+polyfill-dev@1.132.20/node_modules/@tamagui/polyfill-dev/index.js @@ -23316,11 +23316,11 @@ var StackZIndexContext = /* @__PURE__ */ __name(({ })), content; }, "StackZIndexContext"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/Portal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/Portal.mjs var React17 = __toESM(require("react"), 1); var import_react_dom = require("react-dom"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/helpers.mjs var import_web7 = require("@tamagui/core"); var getStackedZIndexProps = /* @__PURE__ */ __name((propsIn) => ({ stackZIndex: propsIn.stackZIndex, @@ -23328,7 +23328,7 @@ var getStackedZIndexProps = /* @__PURE__ */ __name((propsIn) => ({ }), "getStackedZIndexProps"); var resolveViewZIndex = /* @__PURE__ */ __name((zIndex) => typeof zIndex > "u" || zIndex === "unset" ? void 0 : typeof zIndex == "number" ? zIndex : (0, import_web7.getTokenValue)(zIndex, "zIndex"), "resolveViewZIndex"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/Portal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/Portal.mjs var import_jsx_runtime10 = require("react/jsx-runtime"); var Portal = React17.memo((propsIn) => { if (isServer) return null; @@ -23350,16 +23350,16 @@ var Portal = React17.memo((propsIn) => { }), body); }); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs var import_react15 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/constants.mjs var IS_FABRIC = typeof global < "u" && !!(global._IS_FABRIC ?? global.nativeFabricUIManager); var USE_NATIVE_PORTAL = process.env.TAMAGUI_USE_NATIVE_PORTAL && process.env.TAMAGUI_USE_NATIVE_PORTAL !== "false" ? true : !isAndroid && !IS_FABRIC; var allPortalHosts = /* @__PURE__ */ new Map(); var portalListeners = {}; -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs var import_jsx_runtime11 = require("react/jsx-runtime"); var ACTIONS = /* @__PURE__ */ ((ACTIONS2) => (ACTIONS2[ACTIONS2.REGISTER_HOST = 0] = "REGISTER_HOST", ACTIONS2[ACTIONS2.DEREGISTER_HOST = 1] = "DEREGISTER_HOST", ACTIONS2[ACTIONS2.ADD_UPDATE_PORTAL = 2] = "ADD_UPDATE_PORTAL", ACTIONS2[ACTIONS2.REMOVE_PORTAL = 3] = "REMOVE_PORTAL", ACTIONS2))(ACTIONS || {}); var INITIAL_STATE = {}; @@ -23517,7 +23517,7 @@ function PortalHostNonNative(props) { } __name(PortalHostNonNative, "PortalHostNonNative"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortalItem.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._u2adqyzc6tgelqq2glsezbmzt4/node_modules/@tamagui/portal/dist/esm/GorhomPortalItem.mjs var import_react16 = require("react"); var import_react_dom2 = require("react-dom"); var GorhomPortalItem = /* @__PURE__ */ __name((props) => { @@ -23534,7 +23534,7 @@ var GorhomPortalItem = /* @__PURE__ */ __name((props) => { }, [node]), props.passThrough ? props.children : node ? (0, import_react_dom2.createPortal)(props.children, node) : null; }, "GorhomPortalItem"); -// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_l3lpzlqd7gjells3fjeiwokuti/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs +// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_c7zmbqwar3idlbt2gioq7uclxe/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs var import_react17 = __toESM(require("react"), 1); var import_jsx_runtime12 = require("react/jsx-runtime"); var AdaptContext = (0, import_core7.createStyledContext)({ @@ -23666,10 +23666,10 @@ var useAdaptIsActive = /* @__PURE__ */ __name((scope) => { return useAdaptIsActiveGiven(props); }, "useAdaptIsActive"); -// ../../node_modules/.pnpm/@tamagui+alert-dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_kj47tbhahgbt52qvsv5cahqrdi/node_modules/@tamagui/alert-dialog/dist/esm/AlertDialog.mjs +// ../../node_modules/.pnpm/@tamagui+alert-dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_bh6qd7phhthrtoi2seazkyqigi/node_modules/@tamagui/alert-dialog/dist/esm/AlertDialog.mjs var import_core15 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kgepzgc4yinsbqld47gdum6wgy/node_modules/@tamagui/dialog/dist/esm/Dialog.mjs +// ../../node_modules/.pnpm/@tamagui+dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._x3o3esu5gdeupigd7752grk6xe/node_modules/@tamagui/dialog/dist/esm/Dialog.mjs var import_core14 = require("@tamagui/core"); // ../../node_modules/.pnpm/@tamagui+create-context@1.132.17_react@19.1.0/node_modules/@tamagui/create-context/dist/esm/create-context.mjs @@ -23795,7 +23795,7 @@ function useEscapeKeydown(onEscapeKeyDownProp, ownerDocument = globalThis?.docum } __name(useEscapeKeydown, "useEscapeKeydown"); -// ../../node_modules/.pnpm/@tamagui+dismissable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_dn7bn7b2e5vk77fn2bquvgxsua/node_modules/@tamagui/dismissable/dist/esm/Dismissable.mjs +// ../../node_modules/.pnpm/@tamagui+dismissable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_vdwzjrlb52tu5jqdqypoy7gv2m/node_modules/@tamagui/dismissable/dist/esm/Dismissable.mjs var React23 = __toESM(require("react"), 1); var ReactDOM = __toESM(require("react-dom"), 1); var import_jsx_runtime14 = require("react/jsx-runtime"); @@ -24022,10 +24022,10 @@ var fullyIdle = /* @__PURE__ */ __name(async (signal) => { } }, "fullyIdle"); -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_vamii7q4iay6rmo3svc62y3wqa/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs var React25 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScopeController.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_vamii7q4iay6rmo3svc62y3wqa/node_modules/@tamagui/focus-scope/dist/esm/FocusScopeController.mjs var React24 = __toESM(require("react"), 1); var import_jsx_runtime15 = require("react/jsx-runtime"); var FOCUS_SCOPE_CONTROLLER_NAME = "FocusScopeController"; @@ -24060,7 +24060,7 @@ function FocusScopeController(props) { __name(FocusScopeController, "FocusScopeController"); var FocusScopeControllerComponent = FocusScopeController; -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_vamii7q4iay6rmo3svc62y3wqa/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs var import_jsx_runtime16 = require("react/jsx-runtime"); var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount"; var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount"; @@ -24302,15 +24302,15 @@ var useDisableBodyScroll = /* @__PURE__ */ __name((enabled) => { // ../../node_modules/.pnpm/@tamagui+remove-scroll@1.132.17_react@19.1.0/node_modules/@tamagui/remove-scroll/dist/esm/RemoveScroll.mjs var RemoveScroll = /* @__PURE__ */ __name((props) => (useDisableBodyScroll(!!props.enabled), props.children), "RemoveScroll"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs var import_core12 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/constants.mjs var SHEET_NAME = "Sheet"; var SHEET_HANDLE_NAME = "SheetHandle"; var SHEET_OVERLAY_NAME = "SheetOverlay"; -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_core11 = require("@tamagui/core"); // ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.1.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/index.mjs @@ -24329,27 +24329,27 @@ __name(useDidFinishSSR, "useDidFinishSSR"); var subscribe = /* @__PURE__ */ __name(() => () => { }, "subscribe"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_react27 = require("react"); var import_react_native_web3 = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetContext.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetContext.mjs var [createSheetContext, createSheetScope] = createContextScope(SHEET_NAME); var [SheetProvider, useSheetContext] = createSheetContext(SHEET_NAME, {}); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs var import_core9 = require("@tamagui/core"); var import_react25 = __toESM(require("react"), 1); var import_react_native_web = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/contexts.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/contexts.mjs var import_react22 = __toESM(require("react"), 1); var ParentSheetContext = import_react22.default.createContext({ zIndex: 1e5 }); var SheetInsideSheetContext = import_react22.default.createContext(null); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/helpers.mjs function resisted(y, minY, maxOverflow = 25) { if (y >= minY) return y; const pastBoundary = minY - y, resistedDistance = Math.sqrt(pastBoundary) * 2; @@ -24357,7 +24357,7 @@ function resisted(y, minY, maxOverflow = 25) { } __name(resisted, "resisted"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetController.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/useSheetController.mjs var import_react23 = __toESM(require("react"), 1); var useSheetController = /* @__PURE__ */ __name(() => { const controller = import_react23.default.useContext(SheetControllerContext), isHidden2 = controller?.hidden, isShowingNonSheet = isHidden2 && controller?.open; @@ -24370,7 +24370,7 @@ var useSheetController = /* @__PURE__ */ __name(() => { }, "useSheetController"); var SheetControllerContext = import_react23.default.createContext(null); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetOpenState.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/useSheetOpenState.mjs var useSheetOpenState = /* @__PURE__ */ __name((props) => { const { isHidden: isHidden2, @@ -24391,7 +24391,7 @@ var useSheetOpenState = /* @__PURE__ */ __name((props) => { }; }, "useSheetOpenState"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetProviderProps.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/useSheetProviderProps.mjs var import_react24 = __toESM(require("react"), 1); var import_core8 = require("@tamagui/core"); function useSheetProviderProps(props, state, options = {}) { @@ -24476,7 +24476,7 @@ function useSheetProviderProps(props, state, options = {}) { } __name(useSheetProviderProps, "useSheetProviderProps"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs var import_jsx_runtime18 = require("react/jsx-runtime"); var hiddenSize = 10000.1; var sheetHiddenStyleSheet = null; @@ -24762,10 +24762,10 @@ function getYPositions(mode, point, screenSize, frameSize) { } __name(getYPositions, "getYPositions"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs var import_core10 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+scroll-view@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_m2y4hsuo7z4hxvlznc2ixtzv5m/node_modules/@tamagui/scroll-view/dist/esm/ScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+scroll-view@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_oankwinygng2nuwx46yeftjdje/node_modules/@tamagui/scroll-view/dist/esm/ScrollView.mjs var import_web8 = require("@tamagui/core"); var import_react_native_web2 = __toESM(require_cjs(), 1); var ScrollView = (0, import_web8.styled)(import_react_native_web2.ScrollView, { @@ -24782,7 +24782,7 @@ var ScrollView = (0, import_web8.styled)(import_react_native_web2.ScrollView, { } }); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs var import_react26 = __toESM(require("react"), 1); var import_jsx_runtime19 = require("react/jsx-runtime"); var SHEET_SCROLL_VIEW_NAME = "SheetScrollView"; @@ -24902,7 +24902,7 @@ var SheetScrollView = import_react26.default.forwardRef(({ }); }); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetOffscreenSize.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/useSheetOffscreenSize.mjs var useSheetOffscreenSize = /* @__PURE__ */ __name(({ snapPoints, position, @@ -24925,7 +24925,7 @@ var useSheetOffscreenSize = /* @__PURE__ */ __name(({ return Number.isNaN(offscreenSize) ? 0 : offscreenSize; }, "useSheetOffscreenSize"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_jsx_runtime20 = require("react/jsx-runtime"); function createSheet({ Handle: Handle2, @@ -25039,7 +25039,7 @@ function createSheet({ } __name(createSheet, "createSheet"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs var Handle = (0, import_core12.styled)(XStack, { name: SHEET_HANDLE_NAME, variants: { @@ -25124,10 +25124,10 @@ var Sheet = createSheet({ var SheetOverlayFrame = Overlay; var SheetHandleFrame = Handle; -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/useSheet.mjs var useSheet = /* @__PURE__ */ __name(() => useSheetContext("", void 0), "useSheet"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetController.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/SheetController.mjs var import_react28 = __toESM(require("react"), 1); var import_core13 = require("@tamagui/core"); var import_jsx_runtime21 = require("react/jsx-runtime"); @@ -25151,7 +25151,7 @@ var SheetController = /* @__PURE__ */ __name(({ }); }, "SheetController"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/nativeSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_grvw4acp77rjgp65wb4mjv745m/node_modules/@tamagui/sheet/dist/esm/nativeSheet.mjs var import_react29 = require("react"); var import_react_native_web4 = __toESM(require_cjs(), 1); var import_jsx_runtime22 = require("react/jsx-runtime"); @@ -25211,7 +25211,7 @@ __name(setupNativeSheet, "setupNativeSheet"); var emptyFn = /* @__PURE__ */ __name(() => { }, "emptyFn"); -// ../../node_modules/.pnpm/@tamagui+dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kgepzgc4yinsbqld47gdum6wgy/node_modules/@tamagui/dialog/dist/esm/Dialog.mjs +// ../../node_modules/.pnpm/@tamagui+dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._x3o3esu5gdeupigd7752grk6xe/node_modules/@tamagui/dialog/dist/esm/Dialog.mjs var React33 = __toESM(require("react"), 1); var import_jsx_runtime23 = require("react/jsx-runtime"); var DialogContext = (0, import_core14.createStyledContext)( @@ -25681,7 +25681,7 @@ var DialogSheetController = /* @__PURE__ */ __name((props) => { }); }, "DialogSheetController"); -// ../../node_modules/.pnpm/@tamagui+alert-dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_kj47tbhahgbt52qvsv5cahqrdi/node_modules/@tamagui/alert-dialog/dist/esm/AlertDialog.mjs +// ../../node_modules/.pnpm/@tamagui+alert-dialog@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_bh6qd7phhthrtoi2seazkyqigi/node_modules/@tamagui/alert-dialog/dist/esm/AlertDialog.mjs var React34 = __toESM(require("react"), 1); var import_jsx_runtime24 = require("react/jsx-runtime"); var AlertScopePrefix = "Alert__"; @@ -25879,10 +25879,10 @@ var AlertDialog = withStaticProperties(AlertDialogInner, { }); AlertDialog.displayName = ROOT_NAME; -// ../../node_modules/.pnpm/@tamagui+avatar@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4bikkshj45gwygaryw4nvi6dlu/node_modules/@tamagui/avatar/dist/esm/Avatar.mjs +// ../../node_modules/.pnpm/@tamagui+avatar@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._v65pqkv2kfwknipmtqhzcxh7vq/node_modules/@tamagui/avatar/dist/esm/Avatar.mjs var import_core17 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+image@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_iumev7dhrclqs2v6kl4tqruwr4/node_modules/@tamagui/image/dist/esm/Image.mjs +// ../../node_modules/.pnpm/@tamagui+image@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_zm7lpzyuwxoxu3lptsexlox7f4/node_modules/@tamagui/image/dist/esm/Image.mjs var import_react30 = __toESM(require("react"), 1); var import_core16 = require("@tamagui/core"); var import_react_native_web5 = __toESM(require_cjs(), 1); @@ -25933,10 +25933,10 @@ Image.prefetchWithMetadata = import_react_native_web5.Image.prefetchWithMetadata Image.abortPrefetch = import_react_native_web5.Image.abortPrefetch; Image.queryCache = import_react_native_web5.Image.queryCache; -// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._a544qn4p5jbj52bjw4cz2snsgy/node_modules/@tamagui/shapes/dist/esm/Square.mjs +// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._noci2xtt4fabzrh7klsmzxfjni/node_modules/@tamagui/shapes/dist/esm/Square.mjs var import_web9 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._a544qn4p5jbj52bjw4cz2snsgy/node_modules/@tamagui/shapes/dist/esm/getShapeSize.mjs +// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._noci2xtt4fabzrh7klsmzxfjni/node_modules/@tamagui/shapes/dist/esm/getShapeSize.mjs var getShapeSize = /* @__PURE__ */ __name((size4, { tokens }) => { @@ -25951,7 +25951,7 @@ var getShapeSize = /* @__PURE__ */ __name((size4, { }; }, "getShapeSize"); -// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._a544qn4p5jbj52bjw4cz2snsgy/node_modules/@tamagui/shapes/dist/esm/Square.mjs +// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._noci2xtt4fabzrh7klsmzxfjni/node_modules/@tamagui/shapes/dist/esm/Square.mjs var Square = (0, import_web9.styled)(ThemeableStack, { name: "Square", alignItems: "center", @@ -25966,14 +25966,14 @@ var Square = (0, import_web9.styled)(ThemeableStack, { memo: true }); -// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._a544qn4p5jbj52bjw4cz2snsgy/node_modules/@tamagui/shapes/dist/esm/Circle.mjs +// ../../node_modules/.pnpm/@tamagui+shapes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._noci2xtt4fabzrh7klsmzxfjni/node_modules/@tamagui/shapes/dist/esm/Circle.mjs var import_web10 = require("@tamagui/core"); var Circle = (0, import_web10.styled)(Square, { name: "Circle", circular: true }); -// ../../node_modules/.pnpm/@tamagui+avatar@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4bikkshj45gwygaryw4nvi6dlu/node_modules/@tamagui/avatar/dist/esm/Avatar.mjs +// ../../node_modules/.pnpm/@tamagui+avatar@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._v65pqkv2kfwknipmtqhzcxh7vq/node_modules/@tamagui/avatar/dist/esm/Avatar.mjs var React36 = __toESM(require("react"), 1); var import_jsx_runtime26 = require("react/jsx-runtime"); var AVATAR_NAME = "Avatar"; @@ -26072,7 +26072,7 @@ var Avatar = withStaticProperties(React36.forwardRef((props, forwardedRef) => { }); Avatar.displayName = AVATAR_NAME; -// ../../node_modules/.pnpm/@tamagui+font-size@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._qa3m3qlkzht77r5o72xlwraxiq/node_modules/@tamagui/font-size/dist/esm/getFontSize.mjs +// ../../node_modules/.pnpm/@tamagui+font-size@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._rpwexwgy724j2362m3umoj3rru/node_modules/@tamagui/font-size/dist/esm/getFontSize.mjs var import_core18 = require("@tamagui/core"); var getFontSize = /* @__PURE__ */ __name((inSize, opts) => { const res = getFontSizeVariable(inSize, opts); @@ -26094,17 +26094,17 @@ var getFontSizeToken = /* @__PURE__ */ __name((inSize, opts) => { return sizeTokens[tokenIndex] ?? size4; }, "getFontSizeToken"); -// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_kljsf33l6eqdaroxv47tnl47ge/node_modules/@tamagui/helpers-tamagui/dist/esm/prevent.mjs +// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_ctsxu7meetgjxvckfx6sls363u/node_modules/@tamagui/helpers-tamagui/dist/esm/prevent.mjs var prevent = /* @__PURE__ */ __name((e) => [e.preventDefault(), e.stopPropagation()], "prevent"); -// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_kljsf33l6eqdaroxv47tnl47ge/node_modules/@tamagui/helpers-tamagui/dist/esm/useCurrentColor.mjs +// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_ctsxu7meetgjxvckfx6sls363u/node_modules/@tamagui/helpers-tamagui/dist/esm/useCurrentColor.mjs var import_web11 = require("@tamagui/core"); var useCurrentColor = /* @__PURE__ */ __name((colorProp) => { const theme = (0, import_web11.useTheme)(); return colorProp ? (0, import_web11.getVariable)(colorProp) : theme[colorProp]?.get() || theme.color?.get(); }, "useCurrentColor"); -// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_kljsf33l6eqdaroxv47tnl47ge/node_modules/@tamagui/helpers-tamagui/dist/esm/useGetThemedIcon.mjs +// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_ctsxu7meetgjxvckfx6sls363u/node_modules/@tamagui/helpers-tamagui/dist/esm/useGetThemedIcon.mjs var import_react31 = __toESM(require("react"), 1); var useGetThemedIcon = /* @__PURE__ */ __name((props) => { const color = useCurrentColor(props.color); @@ -26116,7 +26116,7 @@ var useGetThemedIcon = /* @__PURE__ */ __name((props) => { }) : import_react31.default.createElement(el, props)); }, "useGetThemedIcon"); -// ../../node_modules/.pnpm/@tamagui+button@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._mdyteqltamz5tkbmxtghimmvjm/node_modules/@tamagui/button/dist/esm/Button.mjs +// ../../node_modules/.pnpm/@tamagui+button@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._hzqjxi7jjgbhdl6lcycs5ctwnm/node_modules/@tamagui/button/dist/esm/Button.mjs var import_web12 = require("@tamagui/core"); var import_react32 = require("react"); var import_jsx_runtime27 = require("react/jsx-runtime"); @@ -26331,7 +26331,7 @@ function useButton({ } __name(useButton, "useButton"); -// ../../node_modules/.pnpm/@tamagui+card@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__kytirxynxyqyd22lovfqigkoxm/node_modules/@tamagui/card/dist/esm/Card.mjs +// ../../node_modules/.pnpm/@tamagui+card@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__sswlwpm7nzqhauudvb7cacfwzi/node_modules/@tamagui/card/dist/esm/Card.mjs var import_web13 = require("@tamagui/core"); var CardContext = (0, import_web13.createStyledContext)({ size: "$true" @@ -26421,17 +26421,17 @@ var Card = (0, import_web13.withStaticProperties)(CardFrame, { Background: CardBackground }); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/Checkbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/Checkbox.mjs var import_core20 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/CheckboxStyledContext.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/CheckboxStyledContext.mjs var import_core19 = require("@tamagui/core"); var CheckboxStyledContext = (0, import_core19.createStyledContext)({ size: "$true", scaleIcon: 1 }); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/Checkbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/Checkbox.mjs var INDICATOR_NAME = "CheckboxIndicator"; var CheckboxIndicatorFrame = (0, import_core20.styled)(ThemeableStack, { // use Checkbox for easier themes @@ -26504,16 +26504,16 @@ var CheckboxFrame = (0, import_core20.styled)(ThemeableStack, { } }); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/createCheckbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/createCheckbox.mjs var import_react35 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+focusable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._iesbmiavrn4nm463rb44h54koy/node_modules/@tamagui/focusable/dist/esm/registerFocusable.mjs +// ../../node_modules/.pnpm/@tamagui+focusable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._zlkhhojtjznelaa6k2wccvl2hq/node_modules/@tamagui/focusable/dist/esm/registerFocusable.mjs var registerFocusable = /* @__PURE__ */ __name((id, input) => () => { }, "registerFocusable"); var focusFocusable = /* @__PURE__ */ __name((id) => { }, "focusFocusable"); -// ../../node_modules/.pnpm/@tamagui+focusable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._iesbmiavrn4nm463rb44h54koy/node_modules/@tamagui/focusable/dist/esm/focusableInputHOC.mjs +// ../../node_modules/.pnpm/@tamagui+focusable@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._zlkhhojtjznelaa6k2wccvl2hq/node_modules/@tamagui/focusable/dist/esm/focusableInputHOC.mjs var import_web14 = require("@tamagui/core"); var import_react33 = __toESM(require("react"), 1); function useFocusable({ @@ -26549,7 +26549,7 @@ function useFocusable({ } __name(useFocusable, "useFocusable"); -// ../../node_modules/.pnpm/@tamagui+label@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_2y4ppqpnkncbg5ztevghupdxsy/node_modules/@tamagui/label/dist/esm/Label.mjs +// ../../node_modules/.pnpm/@tamagui+label@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_mueibzbsxgne33p5bvdrlt3fny/node_modules/@tamagui/label/dist/esm/Label.mjs var import_web15 = require("@tamagui/core"); var React39 = __toESM(require("react"), 1); var import_jsx_runtime28 = require("react/jsx-runtime"); @@ -26642,7 +26642,7 @@ var useLabelContext = /* @__PURE__ */ __name((element) => { }, [element, controlRef]), context2.id; }, "useLabelContext"); -// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_5hltsb4n6nuhadh4fbh3aubexu/node_modules/@tamagui/checkbox-headless/dist/esm/useCheckbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_g5q5nfmor6vjb7kt7g37moha7e/node_modules/@tamagui/checkbox-headless/dist/esm/useCheckbox.mjs var import_react34 = __toESM(require("react"), 1); // ../../node_modules/.pnpm/@tamagui+use-previous@1.132.17_react@19.1.0/node_modules/@tamagui/use-previous/dist/esm/index.mjs @@ -26656,10 +26656,10 @@ function usePrevious(value) { } __name(usePrevious, "usePrevious"); -// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_5hltsb4n6nuhadh4fbh3aubexu/node_modules/@tamagui/checkbox-headless/dist/esm/BubbleInput.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_g5q5nfmor6vjb7kt7g37moha7e/node_modules/@tamagui/checkbox-headless/dist/esm/BubbleInput.mjs var React41 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_5hltsb4n6nuhadh4fbh3aubexu/node_modules/@tamagui/checkbox-headless/dist/esm/utils.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_g5q5nfmor6vjb7kt7g37moha7e/node_modules/@tamagui/checkbox-headless/dist/esm/utils.mjs function isIndeterminate(checked) { return checked === "indeterminate"; } @@ -26669,7 +26669,7 @@ function getState4(checked) { } __name(getState4, "getState"); -// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_5hltsb4n6nuhadh4fbh3aubexu/node_modules/@tamagui/checkbox-headless/dist/esm/BubbleInput.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_g5q5nfmor6vjb7kt7g37moha7e/node_modules/@tamagui/checkbox-headless/dist/esm/BubbleInput.mjs var import_jsx_runtime29 = require("react/jsx-runtime"); var BubbleInput = /* @__PURE__ */ __name((props) => { const { @@ -26710,7 +26710,7 @@ var BubbleInput = /* @__PURE__ */ __name((props) => { }); }, "BubbleInput"); -// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_5hltsb4n6nuhadh4fbh3aubexu/node_modules/@tamagui/checkbox-headless/dist/esm/useCheckbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel_g5q5nfmor6vjb7kt7g37moha7e/node_modules/@tamagui/checkbox-headless/dist/esm/useCheckbox.mjs var import_jsx_runtime30 = require("react/jsx-runtime"); function useCheckbox(props, [checked, setChecked], ref) { const { @@ -26757,7 +26757,7 @@ function useCheckbox(props, [checked, setChecked], ref) { } __name(useCheckbox, "useCheckbox"); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/createCheckbox.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/createCheckbox.mjs var import_core21 = require("@tamagui/core"); var import_jsx_runtime31 = require("react/jsx-runtime"); var CheckboxContext = import_react35.default.createContext({ @@ -26877,13 +26877,13 @@ function createCheckbox(createProps) { } __name(createCheckbox, "createCheckbox"); -// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_3stj5attwarklynhm7b4sjki3e/node_modules/@tamagui/checkbox/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+checkbox@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ydecpxjon3q4gmhb4idgkuvuey/node_modules/@tamagui/checkbox/dist/esm/index.mjs var Checkbox = createCheckbox({ Frame: CheckboxFrame, Indicator: CheckboxIndicatorFrame }); -// ../../node_modules/.pnpm/@tamagui+form@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__vdtvcf24fqddeonywiiuxcju7e/node_modules/@tamagui/form/dist/esm/Form.mjs +// ../../node_modules/.pnpm/@tamagui+form@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__yuvoma3ehtkytmqqtnoayxjheq/node_modules/@tamagui/form/dist/esm/Form.mjs var import_core22 = require("@tamagui/core"); var import_jsx_runtime32 = require("react/jsx-runtime"); var FORM_NAME = "Form"; @@ -26929,12 +26929,12 @@ var Form2 = withStaticProperties(FormComponent, { Trigger: FormTrigger }); -// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_7gjpm7jr2l5dayr6xnsmhgs7ja/node_modules/@tamagui/group/dist/esm/Group.mjs +// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_vacxztjqfrt5ew5khkimjtfp2y/node_modules/@tamagui/group/dist/esm/Group.mjs var import_core23 = require("@tamagui/core"); var import_react36 = __toESM(require("react"), 1); var import_react_native_web6 = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_7gjpm7jr2l5dayr6xnsmhgs7ja/node_modules/@tamagui/group/dist/esm/useIndexedChildren.mjs +// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_vacxztjqfrt5ew5khkimjtfp2y/node_modules/@tamagui/group/dist/esm/useIndexedChildren.mjs var React44 = __toESM(require("react"), 1); var import_jsx_runtime33 = require("react/jsx-runtime"); var MaxIndexContext = React44.createContext([]); @@ -26974,7 +26974,7 @@ function parseIndexPath(indexPathString) { } __name(parseIndexPath, "parseIndexPath"); -// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_7gjpm7jr2l5dayr6xnsmhgs7ja/node_modules/@tamagui/group/dist/esm/Group.mjs +// ../../node_modules/.pnpm/@tamagui+group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_vacxztjqfrt5ew5khkimjtfp2y/node_modules/@tamagui/group/dist/esm/Group.mjs var import_jsx_runtime34 = require("react/jsx-runtime"); var GROUP_NAME = "Group"; var [createGroupContext, createGroupScope] = createContextScope(GROUP_NAME); @@ -27131,19 +27131,19 @@ var cloneElementWithPropOrder = /* @__PURE__ */ __name((child, props) => import_ ...props }), "cloneElementWithPropOrder"); -// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._z6c5mwqiwvhdcwiat43xjqg4eu/node_modules/@tamagui/react-native-media-driver/dist/esm/createMedia.mjs +// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._eoybv2dedjsw5ochcxn2nsvmyq/node_modules/@tamagui/react-native-media-driver/dist/esm/createMedia.mjs var import_web16 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._z6c5mwqiwvhdcwiat43xjqg4eu/node_modules/@tamagui/react-native-media-driver/dist/esm/matchMedia.mjs +// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._eoybv2dedjsw5ochcxn2nsvmyq/node_modules/@tamagui/react-native-media-driver/dist/esm/matchMedia.mjs var matchMedia = globalThis.matchMedia; -// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._z6c5mwqiwvhdcwiat43xjqg4eu/node_modules/@tamagui/react-native-media-driver/dist/esm/createMedia.mjs +// ../../node_modules/.pnpm/@tamagui+react-native-media-driver@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79._eoybv2dedjsw5ochcxn2nsvmyq/node_modules/@tamagui/react-native-media-driver/dist/esm/createMedia.mjs function createMedia(media) { return (0, import_web16.setupMatchMedia)(matchMedia), media; } __name(createMedia, "createMedia"); -// ../../node_modules/.pnpm/@tamagui+elements@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_qbsraf2p3vrhgk5oprlvc65vde/node_modules/@tamagui/elements/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+elements@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_x6vful6e3yaj2xr6kh5kvqaemu/node_modules/@tamagui/elements/dist/esm/index.mjs var import_core24 = require("@tamagui/core"); var Section = (0, import_core24.styled)(import_core24.View, { name: "Section", @@ -27186,7 +27186,7 @@ var Nav = (0, import_core24.styled)(import_core24.View, { // accessibilityRole: 'navigation', }); -// ../../node_modules/.pnpm/@tamagui+list-item@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._bm6g7oqpcdaqlpomwam2p4dveu/node_modules/@tamagui/list-item/dist/esm/ListItem.mjs +// ../../node_modules/.pnpm/@tamagui+list-item@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._gtfjnu2f63msyzy2qefkqpchu4/node_modules/@tamagui/list-item/dist/esm/ListItem.mjs var import_web17 = require("@tamagui/core"); var import_jsx_runtime35 = require("react/jsx-runtime"); var NAME2 = "ListItem"; @@ -27383,7 +27383,7 @@ if (typeof globalThis["__DEV__"] === "undefined") { globalThis["__DEV__"] = process.env.NODE_ENV === "development"; } -// ../../node_modules/.pnpm/@tamagui+animate@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_rsxx5lmis5xxgt34b6o5dtbbcy/node_modules/@tamagui/animate/dist/esm/Animate.mjs +// ../../node_modules/.pnpm/@tamagui+animate@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_iypvtq2f4bue3j64hjvc2mwmae/node_modules/@tamagui/animate/dist/esm/Animate.mjs var import_react37 = require("react"); var import_jsx_runtime36 = require("react/jsx-runtime"); function Animate({ @@ -27426,7 +27426,7 @@ function Animate({ } __name(Animate, "Animate"); -// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_amwouagyqvrw6raqqmswatkufy/node_modules/@tamagui/popover/dist/esm/Popover.mjs +// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_wxvs6mqlzqftc7flpdvzjozdbu/node_modules/@tamagui/popover/dist/esm/Popover.mjs var import_core27 = require("@tamagui/core"); // ../../node_modules/.pnpm/@floating-ui+utils@0.2.10/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs @@ -29254,7 +29254,7 @@ var arrow3 = /* @__PURE__ */ __name((options, deps) => ({ options: [options, deps] }), "arrow"); -// ../../node_modules/.pnpm/@tamagui+floating@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_etb7p3eoy3zuvptrotsoedv24u/node_modules/@tamagui/floating/dist/esm/useFloating.mjs +// ../../node_modules/.pnpm/@tamagui+floating@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_xw4jhbcjkfo5dxni3yezbdvaw4/node_modules/@tamagui/floating/dist/esm/useFloating.mjs var import_react39 = __toESM(require("react"), 1); var FloatingOverrideContext = import_react39.default.createContext(null); var useFloating2 = /* @__PURE__ */ __name((props) => (import_react39.default.useContext(FloatingOverrideContext) || useFloating)?.({ @@ -29277,7 +29277,7 @@ var useFloating2 = /* @__PURE__ */ __name((props) => (import_react39.default.use ] }), "useFloating"); -// ../../node_modules/.pnpm/@tamagui+popper@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._wenoqgspeusjfrldacakq6wen4/node_modules/@tamagui/popper/dist/esm/Popper.mjs +// ../../node_modules/.pnpm/@tamagui+popper@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._3lcurmi7bisher5maoojdct2u4/node_modules/@tamagui/popper/dist/esm/Popper.mjs var import_core26 = require("@tamagui/core"); var React48 = __toESM(require("react"), 1); var import_jsx_runtime37 = require("react/jsx-runtime"); @@ -29625,10 +29625,10 @@ var PopperArrow = React48.forwardRef(function(propsIn, forwardedRef) { }); }); -// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_amwouagyqvrw6raqqmswatkufy/node_modules/@tamagui/popover/dist/esm/Popover.mjs +// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_wxvs6mqlzqftc7flpdvzjozdbu/node_modules/@tamagui/popover/dist/esm/Popover.mjs var React52 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_amwouagyqvrw6raqqmswatkufy/node_modules/@tamagui/popover/dist/esm/useFloatingContext.mjs +// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_wxvs6mqlzqftc7flpdvzjozdbu/node_modules/@tamagui/popover/dist/esm/useFloatingContext.mjs var import_react41 = __toESM(require("react"), 1); // ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@floating-ui/react/dist/floating-ui.react.mjs @@ -33466,7 +33466,7 @@ function safePolygon(options) { } __name(safePolygon, "safePolygon"); -// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_amwouagyqvrw6raqqmswatkufy/node_modules/@tamagui/popover/dist/esm/useFloatingContext.mjs +// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_wxvs6mqlzqftc7flpdvzjozdbu/node_modules/@tamagui/popover/dist/esm/useFloatingContext.mjs var useFloatingContext = /* @__PURE__ */ __name(({ open, setOpen, @@ -33510,7 +33510,7 @@ var useFloatingContext = /* @__PURE__ */ __name(({ }; }, [open, setOpen, disable, disableFocus, hoverable]), "useFloatingContext"); -// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_amwouagyqvrw6raqqmswatkufy/node_modules/@tamagui/popover/dist/esm/Popover.mjs +// ../../node_modules/.pnpm/@tamagui+popover@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_wxvs6mqlzqftc7flpdvzjozdbu/node_modules/@tamagui/popover/dist/esm/Popover.mjs var import_jsx_runtime39 = require("react/jsx-runtime"); var needsRepropagation2 = isAndroid || isIos && !USE_NATIVE_PORTAL; var PopoverContext = (0, import_core27.createStyledContext)( @@ -33917,7 +33917,7 @@ var useShowPopoverSheet = /* @__PURE__ */ __name((context2) => { return context2.open === false ? false : isAdapted; }, "useShowPopoverSheet"); -// ../../node_modules/.pnpm/@tamagui+progress@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_5hxgsfhjeyucsxbbzedffwshgy/node_modules/@tamagui/progress/dist/esm/Progress.mjs +// ../../node_modules/.pnpm/@tamagui+progress@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.2_ubj2orjnsl4spirkt2h2egzcmy/node_modules/@tamagui/progress/dist/esm/Progress.mjs var import_core28 = require("@tamagui/core"); var React53 = __toESM(require("react"), 1); var import_jsx_runtime40 = require("react/jsx-runtime"); @@ -34045,7 +34045,7 @@ var Progress = withStaticProperties(ProgressFrame.styleable(function(props, forw Indicator: ProgressIndicator }); -// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_3acpvlcn3q3vsaxywfdgxnkwjy/node_modules/@tamagui/radio-group/dist/esm/RadioGroup.mjs +// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_zaxn7arbzrqrjk6mmsr2mfnx5e/node_modules/@tamagui/radio-group/dist/esm/RadioGroup.mjs var import_core29 = require("@tamagui/core"); var RADIO_GROUP_ITEM_NAME = "RadioGroupItem"; var RadioGroupItemFrame = (0, import_core29.styled)(ThemeableStack, { @@ -34150,14 +34150,14 @@ var RadioGroupFrame = (0, import_core29.styled)(ThemeableStack, { } }); -// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_3acpvlcn3q3vsaxywfdgxnkwjy/node_modules/@tamagui/radio-group/dist/esm/createRadioGroup.mjs +// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_zaxn7arbzrqrjk6mmsr2mfnx5e/node_modules/@tamagui/radio-group/dist/esm/createRadioGroup.mjs var import_react45 = __toESM(require("react"), 1); var import_core31 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_bltk6jkej4zzfciyl2iiwuv6oa/node_modules/@tamagui/radio-headless/dist/esm/useRadioGroup.mjs +// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_cvlzoc63cpniilmf25y7lqgbxu/node_modules/@tamagui/radio-headless/dist/esm/useRadioGroup.mjs var import_react44 = require("react"); -// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_bltk6jkej4zzfciyl2iiwuv6oa/node_modules/@tamagui/radio-headless/dist/esm/BubbleInput.mjs +// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_cvlzoc63cpniilmf25y7lqgbxu/node_modules/@tamagui/radio-headless/dist/esm/BubbleInput.mjs var import_react43 = __toESM(require("react"), 1); var import_jsx_runtime41 = require("react/jsx-runtime"); var BubbleInput2 = /* @__PURE__ */ __name((props) => { @@ -34200,13 +34200,13 @@ var BubbleInput2 = /* @__PURE__ */ __name((props) => { }); }, "BubbleInput"); -// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_bltk6jkej4zzfciyl2iiwuv6oa/node_modules/@tamagui/radio-headless/dist/esm/utils.mjs +// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_cvlzoc63cpniilmf25y7lqgbxu/node_modules/@tamagui/radio-headless/dist/esm/utils.mjs function getState6(checked) { return checked ? "checked" : "unchecked"; } __name(getState6, "getState"); -// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_bltk6jkej4zzfciyl2iiwuv6oa/node_modules/@tamagui/radio-headless/dist/esm/useRadioGroup.mjs +// ../../node_modules/.pnpm/@tamagui+radio-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_cvlzoc63cpniilmf25y7lqgbxu/node_modules/@tamagui/radio-headless/dist/esm/useRadioGroup.mjs var import_jsx_runtime42 = require("react/jsx-runtime"); function useRadioGroup(params) { const { @@ -34352,7 +34352,7 @@ function useRadioGroupItemIndicator(params) { } __name(useRadioGroupItemIndicator, "useRadioGroupItemIndicator"); -// ../../node_modules/.pnpm/@tamagui+roving-focus@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_qqlqfvp3qncq2kees7wp6mqm4m/node_modules/@tamagui/roving-focus/dist/esm/RovingFocusGroup.mjs +// ../../node_modules/.pnpm/@tamagui+roving-focus@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_peaqo2q4bf3fynkpw3oorqo76y/node_modules/@tamagui/roving-focus/dist/esm/RovingFocusGroup.mjs var import_core30 = require("@tamagui/core"); var React55 = __toESM(require("react"), 1); var import_jsx_runtime43 = require("react/jsx-runtime"); @@ -34518,7 +34518,7 @@ function wrapArray(array, startIndex) { } __name(wrapArray, "wrapArray"); -// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_3acpvlcn3q3vsaxywfdgxnkwjy/node_modules/@tamagui/radio-group/dist/esm/createRadioGroup.mjs +// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_zaxn7arbzrqrjk6mmsr2mfnx5e/node_modules/@tamagui/radio-group/dist/esm/createRadioGroup.mjs var import_jsx_runtime44 = require("react/jsx-runtime"); var ensureContext2 = /* @__PURE__ */ __name((x) => { x.context || (x.context = RadioGroupContext); @@ -34642,24 +34642,24 @@ function createRadioGroup(createProps) { } __name(createRadioGroup, "createRadioGroup"); -// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_3acpvlcn3q3vsaxywfdgxnkwjy/node_modules/@tamagui/radio-group/dist/esm/RadioGroupStyledContext.mjs +// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_zaxn7arbzrqrjk6mmsr2mfnx5e/node_modules/@tamagui/radio-group/dist/esm/RadioGroupStyledContext.mjs var import_core32 = require("@tamagui/core"); var RadioGroupStyledContext = (0, import_core32.createStyledContext)({ size: "$true", scaleIcon: 1 }, "RadioGroup"); -// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_3acpvlcn3q3vsaxywfdgxnkwjy/node_modules/@tamagui/radio-group/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+radio-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_zaxn7arbzrqrjk6mmsr2mfnx5e/node_modules/@tamagui/radio-group/dist/esm/index.mjs var RadioGroup = createRadioGroup({ Frame: RadioGroupFrame, Indicator: RadioGroupIndicatorFrame, Item: RadioGroupItemFrame }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/Select.mjs var import_core41 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+separator@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._ssqfrg2dxpzomet2g6vcbmqupe/node_modules/@tamagui/separator/dist/esm/Separator.mjs +// ../../node_modules/.pnpm/@tamagui+separator@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._jfwbfthbj7rs5lzgxixqha67nu/node_modules/@tamagui/separator/dist/esm/Separator.mjs var import_core33 = require("@tamagui/core"); var Separator = (0, import_core33.styled)(import_core33.Stack, { name: "Separator", @@ -34730,10 +34730,10 @@ function useDebounceValue(val, amt = 0) { } __name(useDebounceValue, "useDebounceValue"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/Select.mjs var React64 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/context.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/context.mjs var import_core34 = require("@tamagui/core"); var import_jsx_runtime45 = require("react/jsx-runtime"); var { @@ -34759,17 +34759,17 @@ var ForwardSelectContext = /* @__PURE__ */ __name(({ }) }), "ForwardSelectContext"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectContent.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectContent.mjs var import_core35 = require("@tamagui/core"); var import_react47 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/useSelectBreakpointActive.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/useSelectBreakpointActive.mjs var useShowSelectSheet = /* @__PURE__ */ __name((context2) => { const breakpointActive = useAdaptIsActive(context2.adaptScope); return context2.open === false ? false : breakpointActive; }, "useShowSelectSheet"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectContent.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectContent.mjs var import_jsx_runtime46 = require("react/jsx-runtime"); var SelectContent = /* @__PURE__ */ __name(({ children, @@ -34804,16 +34804,16 @@ var SelectContent = /* @__PURE__ */ __name(({ }); }, "SelectContent"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs var import_core36 = require("@tamagui/core"); var React59 = __toESM(require("react"), 1); var import_react_dom5 = require("react-dom"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/constants.mjs var SCROLL_ARROW_THRESHOLD = 8; var VIEWPORT_NAME = "SelectViewport"; -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs var import_jsx_runtime47 = require("react/jsx-runtime"); var SelectInlineImpl = /* @__PURE__ */ __name((props) => { const { @@ -35018,7 +35018,7 @@ var SelectInlineImpl = /* @__PURE__ */ __name((props) => { }); }, "SelectInlineImpl"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectItem.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectItem.mjs var import_core37 = require("@tamagui/core"); var React60 = __toESM(require("react"), 1); var import_jsx_runtime48 = require("react/jsx-runtime"); @@ -35136,7 +35136,7 @@ var SelectItem = ListItemFrame.styleable(function(props, forwardedRef) { disableTheme: true }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectItemText.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectItemText.mjs var import_core38 = require("@tamagui/core"); var React61 = __toESM(require("react"), 1); var import_jsx_runtime49 = require("react/jsx-runtime"); @@ -35179,7 +35179,7 @@ var SelectItemText = SelectItemTextFrame.styleable(function(props, forwardedRef) }); }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectScrollButton.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectScrollButton.mjs var React62 = __toESM(require("react"), 1); var import_react_dom6 = require("react-dom"); var import_jsx_runtime50 = require("react/jsx-runtime"); @@ -35267,7 +35267,7 @@ var SelectScrollButtonImpl = React62.memo(React62.forwardRef((props, forwardedRe }); })); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectTrigger.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectTrigger.mjs var import_core39 = require("@tamagui/core"); var React63 = __toESM(require("react"), 1); var import_jsx_runtime51 = require("react/jsx-runtime"); @@ -35326,7 +35326,7 @@ var SelectTrigger = React63.forwardRef(function(props, forwardedRef) { }); }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectViewport.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/SelectViewport.mjs var import_core40 = require("@tamagui/core"); var import_jsx_runtime52 = require("react/jsx-runtime"); var SelectViewportFrame = (0, import_core40.styled)(ThemeableStack, { @@ -35431,7 +35431,7 @@ var selectViewportCSS = ` } `; -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._yug4mhvj6m5pzx3vjyiwvy7jni/node_modules/@tamagui/select/dist/esm/Select.mjs var import_jsx_runtime53 = require("react/jsx-runtime"); var VALUE_NAME = "SelectValue"; var SelectValueFrame = (0, import_core41.styled)(SizableText2, { @@ -35742,11 +35742,11 @@ function SelectInner(props) { } __name(SelectInner, "SelectInner"); -// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._sckfhnobnlpdlrv25dxcoix3ny/node_modules/@tamagui/slider/dist/esm/Slider.mjs +// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._dic233gj7zelnzmpxtwep6n4ki/node_modules/@tamagui/slider/dist/esm/Slider.mjs var import_core44 = require("@tamagui/core"); var React66 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._sckfhnobnlpdlrv25dxcoix3ny/node_modules/@tamagui/slider/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._dic233gj7zelnzmpxtwep6n4ki/node_modules/@tamagui/slider/dist/esm/constants.mjs var import_core42 = require("@tamagui/core"); var SLIDER_NAME = "Slider"; var SliderContext = (0, import_core42.createStyledContext)({ @@ -35776,7 +35776,7 @@ var BACK_KEYS = { rtl: ["ArrowDown", "Home", "ArrowRight", "PageDown"] }; -// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._sckfhnobnlpdlrv25dxcoix3ny/node_modules/@tamagui/slider/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._dic233gj7zelnzmpxtwep6n4ki/node_modules/@tamagui/slider/dist/esm/helpers.mjs function getNextSortedValues(prevValues = [], nextValue, atIndex) { const nextValues = [...prevValues]; return nextValues[atIndex] = nextValue, nextValues.sort((a, b) => a - b); @@ -35832,7 +35832,7 @@ function roundValue(value, decimalCount) { } __name(roundValue, "roundValue"); -// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._sckfhnobnlpdlrv25dxcoix3ny/node_modules/@tamagui/slider/dist/esm/SliderImpl.mjs +// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._dic233gj7zelnzmpxtwep6n4ki/node_modules/@tamagui/slider/dist/esm/SliderImpl.mjs var import_core43 = require("@tamagui/core"); var React65 = __toESM(require("react"), 1); var import_jsx_runtime54 = require("react/jsx-runtime"); @@ -35898,7 +35898,7 @@ var SliderImpl = React65.forwardRef((props, forwardedRef) => { }); }); -// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._sckfhnobnlpdlrv25dxcoix3ny/node_modules/@tamagui/slider/dist/esm/Slider.mjs +// ../../node_modules/.pnpm/@tamagui+slider@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._dic233gj7zelnzmpxtwep6n4ki/node_modules/@tamagui/slider/dist/esm/Slider.mjs var import_jsx_runtime55 = require("react/jsx-runtime"); var activeSliderMeasureListeners = /* @__PURE__ */ new Set(); isWeb && isClient && (process.env.TAMAGUI_DISABLE_SLIDER_INTERVAL || setInterval?.( @@ -36326,10 +36326,10 @@ var Track = SliderTrack; var Range = SliderTrackActive; var Thumb = SliderThumb; -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs var import_core47 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+switch-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_ftjegsvodpdynnqg65isu4pxne/node_modules/@tamagui/switch-headless/dist/esm/useSwitch.mjs +// ../../node_modules/.pnpm/@tamagui+switch-headless@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_3u2tk2cwc5zav352hdufsjdjna/node_modules/@tamagui/switch-headless/dist/esm/useSwitch.mjs var React67 = __toESM(require("react"), 1); var import_jsx_runtime56 = require("react/jsx-runtime"); function getState7(checked) { @@ -36412,18 +36412,18 @@ function useSwitch(props, [checked, setChecked], ref) { } __name(useSwitch, "useSwitch"); -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs var React68 = __toESM(require("react"), 1); var import_react_native_web7 = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/StyledContext.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/StyledContext.mjs var import_core45 = require("@tamagui/core"); var SwitchStyledContext = (0, import_core45.createStyledContext)({ size: void 0, unstyled: process.env.TAMAGUI_HEADLESS === "1" }); -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/Switch.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/Switch.mjs var import_core46 = require("@tamagui/core"); var SwitchThumb = (0, import_core46.styled)(ThemeableStack, { name: "SwitchThumb", @@ -36491,7 +36491,7 @@ var SwitchFrame = (0, import_core46.styled)(YStack, { } }); -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/createSwitch.mjs var import_jsx_runtime57 = require("react/jsx-runtime"); var SwitchContext = React68.createContext({ checked: false, @@ -36621,17 +36621,17 @@ var measureContainerStyle = { flex: 1 }; -// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._cpqlksit3ganl2zuawhah4wpkm/node_modules/@tamagui/switch/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+switch@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._qtjr3an6i7a7v57xzqnjs3u64i/node_modules/@tamagui/switch/dist/esm/index.mjs var Switch = createSwitch({ Frame: SwitchFrame, Thumb: SwitchThumb }); -// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__buqxvwg6nlofjzah25azhcseuy/node_modules/@tamagui/tabs/dist/esm/createTabs.mjs +// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__4etm7aznztrwlkkrlterljziqa/node_modules/@tamagui/tabs/dist/esm/createTabs.mjs var import_web18 = require("@tamagui/core"); var React69 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__buqxvwg6nlofjzah25azhcseuy/node_modules/@tamagui/tabs/dist/esm/Tabs.mjs +// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__4etm7aznztrwlkkrlterljziqa/node_modules/@tamagui/tabs/dist/esm/Tabs.mjs var import_core48 = require("@tamagui/core"); var TABS_NAME = "Tabs"; var DefaultTabsFrame = (0, import_core48.styled)(SizableStack, { @@ -36691,14 +36691,14 @@ var DefaultTabsContentFrame = (0, import_core48.styled)(ThemeableStack, { name: CONTENT_NAME4 }); -// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__buqxvwg6nlofjzah25azhcseuy/node_modules/@tamagui/tabs/dist/esm/StyledContext.mjs +// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__4etm7aznztrwlkkrlterljziqa/node_modules/@tamagui/tabs/dist/esm/StyledContext.mjs var import_core49 = require("@tamagui/core"); var { Provider: TabsProvider, useStyledContext: useTabsContext } = (0, import_core49.createStyledContext)(); -// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__buqxvwg6nlofjzah25azhcseuy/node_modules/@tamagui/tabs/dist/esm/createTabs.mjs +// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__4etm7aznztrwlkkrlterljziqa/node_modules/@tamagui/tabs/dist/esm/createTabs.mjs var import_jsx_runtime58 = require("react/jsx-runtime"); function createTabs(createProps) { const { @@ -36891,14 +36891,14 @@ function makeContentId(baseId, value) { } __name(makeContentId, "makeContentId"); -// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__buqxvwg6nlofjzah25azhcseuy/node_modules/@tamagui/tabs/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+tabs@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__4etm7aznztrwlkkrlterljziqa/node_modules/@tamagui/tabs/dist/esm/index.mjs var Tabs = createTabs({ ContentFrame: DefaultTabsContentFrame, TabFrame: DefaultTabsTabFrame, TabsFrame: DefaultTabsFrame }); -// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_d5bcisok27fiddsxni6gfy7b44/node_modules/@tamagui/theme/dist/esm/_mutateTheme.mjs +// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_xassilaeacoydyxgknjdvz53ay/node_modules/@tamagui/theme/dist/esm/_mutateTheme.mjs var import_web19 = require("@tamagui/core"); function mutateThemes({ themes, @@ -37003,7 +37003,7 @@ function updateStyle(id, rules) { } __name(updateStyle, "updateStyle"); -// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_d5bcisok27fiddsxni6gfy7b44/node_modules/@tamagui/theme/dist/esm/addTheme.mjs +// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_xassilaeacoydyxgknjdvz53ay/node_modules/@tamagui/theme/dist/esm/addTheme.mjs function addTheme(props) { return _mutateTheme({ ...props, @@ -37013,7 +37013,7 @@ function addTheme(props) { } __name(addTheme, "addTheme"); -// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_d5bcisok27fiddsxni6gfy7b44/node_modules/@tamagui/theme/dist/esm/updateTheme.mjs +// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_xassilaeacoydyxgknjdvz53ay/node_modules/@tamagui/theme/dist/esm/updateTheme.mjs function updateTheme({ name, theme @@ -37027,7 +37027,7 @@ function updateTheme({ } __name(updateTheme, "updateTheme"); -// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_d5bcisok27fiddsxni6gfy7b44/node_modules/@tamagui/theme/dist/esm/replaceTheme.mjs +// ../../node_modules/.pnpm/@tamagui+theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_xassilaeacoydyxgknjdvz53ay/node_modules/@tamagui/theme/dist/esm/replaceTheme.mjs function replaceTheme({ name, theme @@ -37041,11 +37041,11 @@ function replaceTheme({ } __name(replaceTheme, "replaceTheme"); -// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_yzlipsddpizwpgkb7bodm7dzy4/node_modules/@tamagui/toggle-group/dist/esm/ToggleGroup.mjs +// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_xfgesax4rhts5xtqsd7dojlngy/node_modules/@tamagui/toggle-group/dist/esm/ToggleGroup.mjs var import_web21 = require("@tamagui/core"); var import_react51 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_yzlipsddpizwpgkb7bodm7dzy4/node_modules/@tamagui/toggle-group/dist/esm/Toggle.mjs +// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_xfgesax4rhts5xtqsd7dojlngy/node_modules/@tamagui/toggle-group/dist/esm/Toggle.mjs var import_web20 = require("@tamagui/core"); var React70 = __toESM(require("react"), 1); var import_jsx_runtime59 = require("react/jsx-runtime"); @@ -37142,7 +37142,7 @@ var Toggle = React70.forwardRef(function(props, forwardedRef) { }); }); -// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_yzlipsddpizwpgkb7bodm7dzy4/node_modules/@tamagui/toggle-group/dist/esm/ToggleGroup.mjs +// ../../node_modules/.pnpm/@tamagui+toggle-group@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_xfgesax4rhts5xtqsd7dojlngy/node_modules/@tamagui/toggle-group/dist/esm/ToggleGroup.mjs var import_jsx_runtime60 = require("react/jsx-runtime"); var TOGGLE_GROUP_NAME = "ToggleGroup"; var TOGGLE_GROUP_ITEM_NAME = "ToggleGroupItem"; @@ -37367,7 +37367,7 @@ var ToggleGroupImpl = ToggleGroupImplElementFrame.extractable(import_react51.def }); })); -// ../../node_modules/.pnpm/@tamagui+tooltip@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_yzfd5pde36krappj37es45hspe/node_modules/@tamagui/tooltip/dist/esm/Tooltip.mjs +// ../../node_modules/.pnpm/@tamagui+tooltip@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_yv5mci6cvhcoxqk3yhpei3dw7y/node_modules/@tamagui/tooltip/dist/esm/Tooltip.mjs var import_core50 = require("@tamagui/core"); var React72 = __toESM(require("react"), 1); var import_jsx_runtime61 = require("react/jsx-runtime"); @@ -37529,7 +37529,7 @@ var Tooltip2 = withStaticProperties(TooltipComponent, { var voidFn = /* @__PURE__ */ __name(() => { }, "voidFn"); -// ../../node_modules/.pnpm/@tamagui+tooltip@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_yzfd5pde36krappj37es45hspe/node_modules/@tamagui/tooltip/dist/esm/TooltipSimple.mjs +// ../../node_modules/.pnpm/@tamagui+tooltip@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28_yv5mci6cvhcoxqk3yhpei3dw7y/node_modules/@tamagui/tooltip/dist/esm/TooltipSimple.mjs var React73 = __toESM(require("react"), 1); var import_jsx_runtime62 = require("react/jsx-runtime"); var TooltipSimple = React73.forwardRef(({ @@ -37595,10 +37595,10 @@ var TooltipSimple = React73.forwardRef(({ }) : children; }); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_x76qyok4m7wbzu752h2lgmn3jm/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs var import_react53 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/initialValue.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_x76qyok4m7wbzu752h2lgmn3jm/node_modules/@tamagui/use-window-dimensions/dist/esm/initialValue.mjs var initialValue = { width: 800, height: 600, @@ -37610,7 +37610,7 @@ function configureInitialWindowDimensions(next) { } __name(configureInitialWindowDimensions, "configureInitialWindowDimensions"); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_x76qyok4m7wbzu752h2lgmn3jm/node_modules/@tamagui/use-window-dimensions/dist/esm/helpers.mjs var lastSize = initialValue; var docEl = null; function getWindowSize() { @@ -37644,7 +37644,7 @@ function subscribe2(cb) { } __name(subscribe2, "subscribe"); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_x76qyok4m7wbzu752h2lgmn3jm/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs function useWindowDimensions({ serverValue = initialValue } = {}) { @@ -37652,7 +37652,7 @@ function useWindowDimensions({ } __name(useWindowDimensions, "useWindowDimensions"); -// ../../node_modules/.pnpm/@tamagui+visually-hidden@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_pighlp4jtbtzs54j4sp4nbiqia/node_modules/@tamagui/visually-hidden/dist/esm/VisuallyHidden.mjs +// ../../node_modules/.pnpm/@tamagui+visually-hidden@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_lyetvdozouqqycalcnmqftl5zi/node_modules/@tamagui/visually-hidden/dist/esm/VisuallyHidden.mjs var import_web22 = require("@tamagui/core"); var VisuallyHidden = (0, import_web22.styled)(import_web22.Text, { position: "absolute", @@ -37687,7 +37687,7 @@ var VisuallyHidden = (0, import_web22.styled)(import_web22.Text, { }); VisuallyHidden.isVisuallyHidden = true; -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/createTamagui.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/createTamagui.mjs var import_core51 = require("@tamagui/core"); var createTamagui = process.env.NODE_ENV !== "development" ? import_core51.createTamagui : (conf) => { const sizeTokenKeys = ["$true"], hasKeys = /* @__PURE__ */ __name((expectedKeys, obj) => expectedKeys.every((k) => typeof obj[k] < "u"), "hasKeys"), tamaguiConfig = (0, import_core51.createTamagui)(conf); @@ -37730,7 +37730,7 @@ Expected a subset of: ${expected.join(", ")} return tamaguiConfig; }; -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/TamaguiProvider.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/TamaguiProvider.mjs var import_core52 = require("@tamagui/core"); var import_jsx_runtime63 = require("react/jsx-runtime"); var TamaguiProvider = /* @__PURE__ */ __name(({ @@ -37747,7 +37747,7 @@ var TamaguiProvider = /* @__PURE__ */ __name(({ }) }), "TamaguiProvider"); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Anchor.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Anchor.mjs var import_core53 = require("@tamagui/core"); var import_react_native_web8 = __toESM(require_cjs(), 1); var import_jsx_runtime64 = require("react/jsx-runtime"); @@ -37773,7 +37773,7 @@ var Anchor = AnchorFrame.styleable(({ ref })); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/EnsureFlexed.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/EnsureFlexed.mjs var import_core54 = require("@tamagui/core"); var EnsureFlexed = (0, import_core54.styled)(import_core54.Text, { opacity: 0, @@ -37786,7 +37786,7 @@ var EnsureFlexed = (0, import_core54.styled)(import_core54.Text, { }); EnsureFlexed.isVisuallyHidden = true; -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Fieldset.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Fieldset.mjs var import_core55 = require("@tamagui/core"); var Fieldset = (0, import_core55.styled)(YStack, { name: "Fieldset", @@ -37803,12 +37803,12 @@ var Fieldset = (0, import_core55.styled)(YStack, { } }); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Input.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Input.mjs var import_react54 = __toESM(require("react"), 1); var import_core57 = require("@tamagui/core"); var import_react_native_web9 = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/helpers/inputHelpers.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/helpers/inputHelpers.mjs var import_core56 = require("@tamagui/core"); var inputSizeVariant = /* @__PURE__ */ __name((val = "$true", extras) => { if (extras.props.multiline || extras.props.numberOfLines > 1) return textAreaSizeVariant(val, extras); @@ -37841,7 +37841,7 @@ var textAreaSizeVariant = /* @__PURE__ */ __name((val = "$true", extras) => { }; }, "textAreaSizeVariant"); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Input.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Input.mjs var import_jsx_runtime65 = require("react/jsx-runtime"); var defaultStyles = { size: "$true", @@ -37918,7 +37918,7 @@ function useInputProps(props, ref) { } __name(useInputProps, "useInputProps"); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Spinner.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Spinner.mjs var import_core58 = require("@tamagui/core"); var React76 = __toESM(require("react"), 1); var import_react_native_web10 = __toESM(require_cjs(), 1); @@ -37942,7 +37942,7 @@ var Spinner = YStack.extractable((0, import_core58.themeable)(React76.forwardRef componentName: "Spinner" })); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/TextArea.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/TextArea.mjs var import_react55 = __toESM(require("react"), 1); var import_core59 = require("@tamagui/core"); var import_jsx_runtime67 = require("react/jsx-runtime"); @@ -37977,7 +37977,7 @@ var TextArea = TextAreaFrame.styleable((propsIn, forwardedRef) => { }); }); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/views/Text.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/views/Text.mjs var import_core60 = require("@tamagui/core"); var Text5 = (0, import_core60.styled)(import_core60.Text, { variants: { @@ -37992,7 +37992,7 @@ var Text5 = (0, import_core60.styled)(import_core60.Text, { } }); -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/index.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_ivr2tdbffxrql3vuafy64my3rq/node_modules/tamagui/dist/esm/index.mjs var import_core61 = require("@tamagui/core"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { diff --git a/apps/web/.tamagui/tamagui.config.cjs b/apps/web/.tamagui/tamagui.config.cjs index 5b992d8..a3a4b2e 100644 --- a/apps/web/.tamagui/tamagui.config.cjs +++ b/apps/web/.tamagui/tamagui.config.cjs @@ -21200,7 +21200,7 @@ __export(tamagui_config_exports, { }); module.exports = __toCommonJS(tamagui_config_exports); -// ../../node_modules/.pnpm/@tamagui+constants@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/constants/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+constants@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_f7wculpfgcwfs73brrragb3cnu/node_modules/@tamagui/constants/dist/esm/constants.mjs var import_react = require("react"); var import_react2 = require("react"); var isWeb = true; @@ -21215,7 +21215,7 @@ var isAndroid = false; var isIos = process.env.TEST_NATIVE_PLATFORM === "ios"; var currentPlatform = "web"; -// ../../node_modules/.pnpm/@tamagui+use-force-update@1.132.17_react@19.1.0/node_modules/@tamagui/use-force-update/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-force-update@1.132.17_react@19.2.0/node_modules/@tamagui/use-force-update/dist/esm/index.mjs var import_react3 = __toESM(require("react"), 1); var isServerSide = typeof window > "u"; var idFn = /* @__PURE__ */ __name(() => { @@ -21225,14 +21225,14 @@ function useForceUpdate() { } __name(useForceUpdate, "useForceUpdate"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+_7uje4x5muh7kbgijgp3xgg4d2y/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs var import_react6 = require("react"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/LayoutGroupContext.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+_7uje4x5muh7kbgijgp3xgg4d2y/node_modules/@tamagui/animate-presence/dist/esm/LayoutGroupContext.mjs var import_react4 = __toESM(require("react"), 1); var LayoutGroupContext = import_react4.default.createContext({}); -// ../../node_modules/.pnpm/@tamagui+use-constant@1.132.17_react@19.1.0/node_modules/@tamagui/use-constant/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-constant@1.132.17_react@19.2.0/node_modules/@tamagui/use-constant/dist/esm/index.mjs var React3 = __toESM(require("react"), 1); function useConstant(fn) { if (typeof document > "u") return React3.useMemo(() => fn(), []); @@ -21243,7 +21243,7 @@ function useConstant(fn) { } __name(useConstant, "useConstant"); -// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_hwowyxiuo2sdaztcnn2obknkua/node_modules/@tamagui/use-presence/dist/esm/PresenceContext.mjs +// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_s3z7qyqgds2teden7hrekyssri/node_modules/@tamagui/use-presence/dist/esm/PresenceContext.mjs var React4 = __toESM(require("react"), 1); var import_jsx_runtime = require("react/jsx-runtime"); var PresenceContext = React4.createContext(null); @@ -21255,7 +21255,7 @@ var ResetPresence = /* @__PURE__ */ __name((props) => { }); }, "ResetPresence"); -// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_hwowyxiuo2sdaztcnn2obknkua/node_modules/@tamagui/use-presence/dist/esm/usePresence.mjs +// ../../node_modules/.pnpm/@tamagui+use-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_s3z7qyqgds2teden7hrekyssri/node_modules/@tamagui/use-presence/dist/esm/usePresence.mjs var React5 = __toESM(require("react"), 1); function usePresence() { const context = React5.useContext(PresenceContext); @@ -21270,7 +21270,7 @@ function usePresence() { } __name(usePresence, "usePresence"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/PresenceChild.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+_7uje4x5muh7kbgijgp3xgg4d2y/node_modules/@tamagui/animate-presence/dist/esm/PresenceChild.mjs var React6 = __toESM(require("react"), 1); var import_react5 = require("react"); var import_jsx_runtime2 = require("react/jsx-runtime"); @@ -21323,7 +21323,7 @@ function newChildrenMap() { } __name(newChildrenMap, "newChildrenMap"); -// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+_aukm5wo6dlzju2a7gie6ostrsy/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs +// ../../node_modules/.pnpm/@tamagui+animate-presence@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+_7uje4x5muh7kbgijgp3xgg4d2y/node_modules/@tamagui/animate-presence/dist/esm/AnimatePresence.mjs var import_jsx_runtime3 = require("react/jsx-runtime"); var getChildKey = /* @__PURE__ */ __name((child) => child.key || "", "getChildKey"); function updateChildLookup(children, allChildren) { @@ -21418,7 +21418,7 @@ var AnimatePresence = /* @__PURE__ */ __name(({ }, "AnimatePresence"); AnimatePresence.displayName = "AnimatePresence"; -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/composeEventHandlers.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_lxu3d2hmpxgns7hahcobgmlrlu/node_modules/@tamagui/helpers/dist/esm/composeEventHandlers.mjs function composeEventHandlers(og, next, { checkDefaultPrevented = true } = {}) { @@ -21429,7 +21429,7 @@ function composeEventHandlers(og, next, { } __name(composeEventHandlers, "composeEventHandlers"); -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/validStyleProps.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_lxu3d2hmpxgns7hahcobgmlrlu/node_modules/@tamagui/helpers/dist/esm/validStyleProps.mjs var textColors = { color: true, textDecorationColor: true, @@ -21733,7 +21733,7 @@ var stylePropsText = { ...stylePropsTextOnly }; -// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/helpers/dist/esm/withStaticProperties.mjs +// ../../node_modules/.pnpm/@tamagui+helpers@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_bufferu_lxu3d2hmpxgns7hahcobgmlrlu/node_modules/@tamagui/helpers/dist/esm/withStaticProperties.mjs var import_react7 = __toESM(require("react"), 1); var Decorated = Symbol(); var withStaticProperties = /* @__PURE__ */ __name((component, staticProps) => { @@ -21755,7 +21755,7 @@ var withStaticProperties = /* @__PURE__ */ __name((component, staticProps) => { return Object.assign(next, staticProps), next[Decorated] = true, next; }, "withStaticProperties"); -// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/use-event/dist/esm/useGet.mjs +// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_dvgbo5w3goap7ckwjz2qbdk3aq/node_modules/@tamagui/use-event/dist/esm/useGet.mjs var React8 = __toESM(require("react"), 1); function useGet(currentValue, initialValue2, forwardToFunction) { const curRef = React8.useRef(initialValue2 ?? currentValue); @@ -21765,7 +21765,7 @@ function useGet(currentValue, initialValue2, forwardToFunction) { } __name(useGet, "useGet"); -// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/use-event/dist/esm/useEvent.mjs +// ../../node_modules/.pnpm/@tamagui+use-event@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buffe_dvgbo5w3goap7ckwjz2qbdk3aq/node_modules/@tamagui/use-event/dist/esm/useEvent.mjs function useEvent(callback) { return useGet(callback, defaultValue, true); } @@ -21774,16 +21774,16 @@ var defaultValue = /* @__PURE__ */ __name(() => { throw new Error("Cannot call an event handler while rendering."); }, "defaultValue"); -// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_mfcgnw6hqeenclbq4ofkf3hyxq/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs +// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_pirnyg3kkkvr7cw6zeesngf6mi/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs var React9 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+start-transition@1.132.17_react@19.1.0/node_modules/@tamagui/start-transition/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+start-transition@1.132.17_react@19.2.0/node_modules/@tamagui/start-transition/dist/esm/index.mjs var import_react8 = require("react"); var startTransition = /* @__PURE__ */ __name((callback) => { (0, import_react8.startTransition)(callback); }, "startTransition"); -// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_mfcgnw6hqeenclbq4ofkf3hyxq/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs +// ../../node_modules/.pnpm/@tamagui+use-controllable-state@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@_pirnyg3kkkvr7cw6zeesngf6mi/node_modules/@tamagui/use-controllable-state/dist/esm/useControllableState.mjs var emptyCallbackFn = /* @__PURE__ */ __name((_) => _(), "emptyCallbackFn"); function useControllableState({ prop, @@ -21815,7 +21815,7 @@ __name(useControllableState, "useControllableState"); var idFn2 = /* @__PURE__ */ __name(() => { }, "idFn"); -// ../../node_modules/.pnpm/@tamagui+compose-refs@1.132.17_react@19.1.0/node_modules/@tamagui/compose-refs/dist/esm/compose-refs.mjs +// ../../node_modules/.pnpm/@tamagui+compose-refs@1.132.17_react@19.2.0/node_modules/@tamagui/compose-refs/dist/esm/compose-refs.mjs var React10 = __toESM(require("react"), 1); function setRef(ref, value) { typeof ref == "function" ? ref(value) : ref && (ref.current = value); @@ -21830,10 +21830,10 @@ function useComposedRefs(...refs) { } __name(useComposedRefs, "useComposedRefs"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._sc2mw763pep3fbj62rngpmekcq/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs var import_core2 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/getElevation.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._sc2mw763pep3fbj62rngpmekcq/node_modules/@tamagui/stacks/dist/esm/getElevation.mjs var import_core = require("@tamagui/core"); var getElevation = /* @__PURE__ */ __name((size5, extras) => { if (!size5) return; @@ -21866,7 +21866,7 @@ var getSizedElevation = /* @__PURE__ */ __name((val, { }; }, "getSizedElevation"); -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._sc2mw763pep3fbj62rngpmekcq/node_modules/@tamagui/stacks/dist/esm/Stacks.mjs var fullscreenStyle = { position: "absolute", top: 0, @@ -21908,7 +21908,7 @@ var ZStack = (0, import_core2.styled)(YStack, { }); ZStack.displayName = "ZStack"; -// ../../node_modules/.pnpm/@tamagui+get-token@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._zt3ejghu7v62ucxsmck4ym7fmu/node_modules/@tamagui/get-token/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+get-token@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7._etx6glo6ji6oy3cubd6scfna74/node_modules/@tamagui/get-token/dist/esm/index.mjs var import_web = require("@tamagui/core"); var defaultOptions = { shift: 0, @@ -21939,7 +21939,7 @@ var stepTokenUpOrDown = /* @__PURE__ */ __name((type, current, options = default }, "stepTokenUpOrDown"); var getTokenRelative = stepTokenUpOrDown; -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/variants.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._sc2mw763pep3fbj62rngpmekcq/node_modules/@tamagui/stacks/dist/esm/variants.mjs var elevate = { true: /* @__PURE__ */ __name((_, extras) => getElevation(extras.props.size, extras), "true") }; @@ -22038,7 +22038,7 @@ var focusTheme = { false: {} }; -// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._khhd5fpw4uhzc5llmmsdrmkhpa/node_modules/@tamagui/stacks/dist/esm/ThemeableStack.mjs +// ../../node_modules/.pnpm/@tamagui+stacks@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._sc2mw763pep3fbj62rngpmekcq/node_modules/@tamagui/stacks/dist/esm/ThemeableStack.mjs var import_core3 = require("@tamagui/core"); var chromelessStyle = { backgroundColor: "transparent", @@ -22081,7 +22081,7 @@ var ThemeableStack = (0, import_core3.styled)(YStack, { variants: themeableVariants }); -// ../../node_modules/.pnpm/@tamagui+get-font-sized@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_rbbvgapwpxd4conn2yxogzeuyu/node_modules/@tamagui/get-font-sized/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+get-font-sized@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+co_rfg6i5g7czh7ysc3kcjr35ga5q/node_modules/@tamagui/get-font-sized/dist/esm/index.mjs var import_web2 = require("@tamagui/core"); var getFontSized = /* @__PURE__ */ __name((sizeTokenIn = "$true", { font, @@ -22121,7 +22121,7 @@ function getDefaultSizeToken(font) { } __name(getDefaultSizeToken, "getDefaultSizeToken"); -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/SizableText.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0__rkywgup2cmrxcdkbdnvshdr6ga/node_modules/@tamagui/text/dist/esm/SizableText.mjs var import_web3 = require("@tamagui/core"); var SizableText2 = (0, import_web3.styled)(import_web3.Text, { name: "SizableText", @@ -22146,7 +22146,7 @@ SizableText2.staticConfig.variants.fontFamily = { }, "...") }; -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/Paragraph.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0__rkywgup2cmrxcdkbdnvshdr6ga/node_modules/@tamagui/text/dist/esm/Paragraph.mjs var import_web4 = require("@tamagui/core"); var Paragraph = (0, import_web4.styled)(SizableText2, { name: "Paragraph", @@ -22157,7 +22157,7 @@ var Paragraph = (0, import_web4.styled)(SizableText2, { whiteSpace: "normal" }); -// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0__xytshenq4354n5nkv6cd6koc2a/node_modules/@tamagui/text/dist/esm/wrapChildrenInText.mjs +// ../../node_modules/.pnpm/@tamagui+text@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0__rkywgup2cmrxcdkbdnvshdr6ga/node_modules/@tamagui/text/dist/esm/wrapChildrenInText.mjs var import_react9 = __toESM(require("react"), 1); var import_jsx_runtime4 = require("react/jsx-runtime"); function wrapChildrenInText(TextComponent, propsIn, extraProps) { @@ -22190,7 +22190,7 @@ function wrapChildrenInText(TextComponent, propsIn, extraProps) { } __name(wrapChildrenInText, "wrapChildrenInText"); -// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_l3lpzlqd7gjells3fjeiwokuti/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs +// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_bojwnfyqhhntj4fpsz2bshv44e/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs var import_core4 = require("@tamagui/core"); // ../../node_modules/.pnpm/@tamagui+polyfill-dev@1.132.20/node_modules/@tamagui/polyfill-dev/index.js @@ -22198,15 +22198,15 @@ if (typeof globalThis["__DEV__"] === "undefined") { globalThis["__DEV__"] = process.env.NODE_ENV === "development"; } -// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.1.0/node_modules/@tamagui/z-index-stack/dist/esm/useStackedZIndex.mjs +// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.2.0/node_modules/@tamagui/z-index-stack/dist/esm/useStackedZIndex.mjs var import_react11 = require("react"); -// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.1.0/node_modules/@tamagui/z-index-stack/dist/esm/context.mjs +// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.2.0/node_modules/@tamagui/z-index-stack/dist/esm/context.mjs var import_react10 = require("react"); var ZIndexStackContext = (0, import_react10.createContext)(1); var ZIndexHardcodedContext = (0, import_react10.createContext)(void 0); -// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.1.0/node_modules/@tamagui/z-index-stack/dist/esm/useStackedZIndex.mjs +// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.2.0/node_modules/@tamagui/z-index-stack/dist/esm/useStackedZIndex.mjs var ZIndicesByContext = {}; var CurrentPortalZIndices = {}; var useStackedZIndex = /* @__PURE__ */ __name((props) => { @@ -22249,7 +22249,7 @@ var useStackedZIndex = /* @__PURE__ */ __name((props) => { } }, "useStackedZIndex"); -// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.1.0/node_modules/@tamagui/z-index-stack/dist/esm/StackZIndex.mjs +// ../../node_modules/.pnpm/@tamagui+z-index-stack@1.132.17_react@19.2.0/node_modules/@tamagui/z-index-stack/dist/esm/StackZIndex.mjs var import_react12 = require("react"); var import_jsx_runtime5 = require("react/jsx-runtime"); var StackZIndexContext = /* @__PURE__ */ __name(({ @@ -22267,11 +22267,11 @@ var StackZIndexContext = /* @__PURE__ */ __name(({ })), content; }, "StackZIndexContext"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/Portal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/Portal.mjs var React12 = __toESM(require("react"), 1); var import_react_dom = require("react-dom"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/helpers.mjs var import_web5 = require("@tamagui/core"); var getStackedZIndexProps = /* @__PURE__ */ __name((propsIn) => ({ stackZIndex: propsIn.stackZIndex, @@ -22279,7 +22279,7 @@ var getStackedZIndexProps = /* @__PURE__ */ __name((propsIn) => ({ }), "getStackedZIndexProps"); var resolveViewZIndex = /* @__PURE__ */ __name((zIndex2) => typeof zIndex2 > "u" || zIndex2 === "unset" ? void 0 : typeof zIndex2 == "number" ? zIndex2 : (0, import_web5.getTokenValue)(zIndex2, "zIndex"), "resolveViewZIndex"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/Portal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/Portal.mjs var import_jsx_runtime6 = require("react/jsx-runtime"); var Portal = React12.memo((propsIn) => { if (isServer) return null; @@ -22301,16 +22301,16 @@ var Portal = React12.memo((propsIn) => { }), body); }); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs var import_react13 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/constants.mjs var IS_FABRIC = typeof global < "u" && !!(global._IS_FABRIC ?? global.nativeFabricUIManager); var USE_NATIVE_PORTAL = process.env.TAMAGUI_USE_NATIVE_PORTAL && process.env.TAMAGUI_USE_NATIVE_PORTAL !== "false" ? true : !isAndroid && !IS_FABRIC; var allPortalHosts = /* @__PURE__ */ new Map(); var portalListeners = {}; -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/GorhomPortal.mjs var import_jsx_runtime7 = require("react/jsx-runtime"); var INITIAL_STATE = {}; var registerHost = /* @__PURE__ */ __name((state, hostName) => (hostName in state || (state[hostName] = []), state), "registerHost"); @@ -22467,7 +22467,7 @@ function PortalHostNonNative(props) { } __name(PortalHostNonNative, "PortalHostNonNative"); -// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._aprmjdaoonavrifuvpbepovj74/node_modules/@tamagui/portal/dist/esm/GorhomPortalItem.mjs +// ../../node_modules/.pnpm/@tamagui+portal@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._7s5vk57b2gnc5x63zohjbpzzh4/node_modules/@tamagui/portal/dist/esm/GorhomPortalItem.mjs var import_react14 = require("react"); var import_react_dom2 = require("react-dom"); var GorhomPortalItem = /* @__PURE__ */ __name((props) => { @@ -22484,7 +22484,7 @@ var GorhomPortalItem = /* @__PURE__ */ __name((props) => { }, [node]), props.passThrough ? props.children : node ? (0, import_react_dom2.createPortal)(props.children, node) : null; }, "GorhomPortalItem"); -// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_l3lpzlqd7gjells3fjeiwokuti/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs +// ../../node_modules/.pnpm/@tamagui+adapt@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_bojwnfyqhhntj4fpsz2bshv44e/node_modules/@tamagui/adapt/dist/esm/Adapt.mjs var import_react15 = __toESM(require("react"), 1); var import_jsx_runtime8 = require("react/jsx-runtime"); var AdaptContext = (0, import_core4.createStyledContext)({ @@ -22616,7 +22616,7 @@ var useAdaptIsActive = /* @__PURE__ */ __name((scope) => { return useAdaptIsActiveGiven(props); }, "useAdaptIsActive"); -// ../../node_modules/.pnpm/@tamagui+create-context@1.132.17_react@19.1.0/node_modules/@tamagui/create-context/dist/esm/create-context.mjs +// ../../node_modules/.pnpm/@tamagui+create-context@1.132.17_react@19.2.0/node_modules/@tamagui/create-context/dist/esm/create-context.mjs var React15 = __toESM(require("react"), 1); var import_jsx_runtime9 = require("react/jsx-runtime"); function createContextScope(scopeName, createContextScopeDeps = []) { @@ -22691,10 +22691,10 @@ function composeContextScopes(...scopes) { } __name(composeContextScopes, "composeContextScopes"); -// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.1.0/node_modules/@tamagui/use-async/dist/esm/useAsyncEffect.mjs +// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.2.0/node_modules/@tamagui/use-async/dist/esm/useAsyncEffect.mjs var import_react16 = require("react"); -// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.1.0/node_modules/@tamagui/use-async/dist/esm/errors.mjs +// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.2.0/node_modules/@tamagui/use-async/dist/esm/errors.mjs var AbortError = class extends Error { static { __name(this, "AbortError"); @@ -22704,7 +22704,7 @@ var AbortError = class extends Error { } }; -// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.1.0/node_modules/@tamagui/use-async/dist/esm/useAsyncEffect.mjs +// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.2.0/node_modules/@tamagui/use-async/dist/esm/useAsyncEffect.mjs var DEBUG_LEVEL = 0; function useAsyncEffect(cb, deps = []) { useAsyncEffectOfType(import_react16.useEffect, cb, deps); @@ -22737,12 +22737,12 @@ function useAsyncEffectOfType(type, cb, deps = []) { } __name(useAsyncEffectOfType, "useAsyncEffectOfType"); -// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.1.0/node_modules/@tamagui/use-async/dist/esm/sleep.mjs +// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.2.0/node_modules/@tamagui/use-async/dist/esm/sleep.mjs var sleep = /* @__PURE__ */ __name(async (ms, signal) => { if (await new Promise((res) => setTimeout(res, ms)), signal?.aborted) throw new AbortError(); }, "sleep"); -// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.1.0/node_modules/@tamagui/use-async/dist/esm/idle.mjs +// ../../node_modules/.pnpm/@tamagui+use-async@1.132.17_react@19.2.0/node_modules/@tamagui/use-async/dist/esm/idle.mjs var idleCb = typeof requestIdleCallback > "u" ? (cb) => setTimeout(cb, 1) : requestIdleCallback; var idleAsync = /* @__PURE__ */ __name(() => new Promise((res) => { idleCb(res); @@ -22763,10 +22763,10 @@ var fullyIdle = /* @__PURE__ */ __name(async (signal) => { } }, "fullyIdle"); -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_zxauqexafmrzcylomtrm7tac5u/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs var React17 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScopeController.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_zxauqexafmrzcylomtrm7tac5u/node_modules/@tamagui/focus-scope/dist/esm/FocusScopeController.mjs var React16 = __toESM(require("react"), 1); var import_jsx_runtime10 = require("react/jsx-runtime"); var FOCUS_SCOPE_CONTROLLER_NAME = "FocusScopeController"; @@ -22801,7 +22801,7 @@ function FocusScopeController(props) { __name(FocusScopeController, "FocusScopeController"); var FocusScopeControllerComponent = FocusScopeController; -// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_react@19.1.0__react@19.1.0/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs +// ../../node_modules/.pnpm/@tamagui+focus-scope@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@19.0.14_buf_zxauqexafmrzcylomtrm7tac5u/node_modules/@tamagui/focus-scope/dist/esm/FocusScope.mjs var import_jsx_runtime11 = require("react/jsx-runtime"); var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount"; var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount"; @@ -23024,7 +23024,7 @@ function removeLinks(items) { } __name(removeLinks, "removeLinks"); -// ../../node_modules/.pnpm/@tamagui+remove-scroll@1.132.17_react@19.1.0/node_modules/@tamagui/remove-scroll/dist/esm/useDisableScroll.mjs +// ../../node_modules/.pnpm/@tamagui+remove-scroll@1.132.17_react@19.2.0/node_modules/@tamagui/remove-scroll/dist/esm/useDisableScroll.mjs var import_react17 = require("react"); var canUseDOM = /* @__PURE__ */ __name(() => typeof window < "u" && !!window.document && !!window.document.createElement, "canUseDOM"); var useDisableBodyScroll = /* @__PURE__ */ __name((enabled) => { @@ -23040,29 +23040,29 @@ var useDisableBodyScroll = /* @__PURE__ */ __name((enabled) => { }, [enabled]); }, "useDisableBodyScroll"); -// ../../node_modules/.pnpm/@tamagui+remove-scroll@1.132.17_react@19.1.0/node_modules/@tamagui/remove-scroll/dist/esm/RemoveScroll.mjs +// ../../node_modules/.pnpm/@tamagui+remove-scroll@1.132.17_react@19.2.0/node_modules/@tamagui/remove-scroll/dist/esm/RemoveScroll.mjs var RemoveScroll = /* @__PURE__ */ __name((props) => (useDisableBodyScroll(!!props.enabled), props.children), "RemoveScroll"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs var import_core9 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/constants.mjs var SHEET_NAME = "Sheet"; var SHEET_HANDLE_NAME = "SheetHandle"; var SHEET_OVERLAY_NAME = "SheetOverlay"; -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_core8 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.1.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.2.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/index.mjs var React18 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.1.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/ClientOnly.mjs +// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.2.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/ClientOnly.mjs var import_react18 = require("react"); var import_jsx_runtime12 = require("react/jsx-runtime"); var ClientOnlyContext = (0, import_react18.createContext)(false); -// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.1.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-did-finish-ssr@1.132.17_react@19.2.0/node_modules/@tamagui/use-did-finish-ssr/dist/esm/index.mjs function useDidFinishSSR() { return React18.useContext(ClientOnlyContext) ? true : React18.useSyncExternalStore(subscribe, () => true, () => false); } @@ -23070,27 +23070,27 @@ __name(useDidFinishSSR, "useDidFinishSSR"); var subscribe = /* @__PURE__ */ __name(() => () => { }, "subscribe"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_react24 = require("react"); var import_react_native_web3 = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetContext.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetContext.mjs var [createSheetContext, createSheetScope] = createContextScope(SHEET_NAME); var [SheetProvider, useSheetContext] = createSheetContext(SHEET_NAME, {}); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs var import_core6 = require("@tamagui/core"); var import_react22 = __toESM(require("react"), 1); var import_react_native_web = __toESM(require_cjs(), 1); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/contexts.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/contexts.mjs var import_react19 = __toESM(require("react"), 1); var ParentSheetContext = import_react19.default.createContext({ zIndex: 1e5 }); var SheetInsideSheetContext = import_react19.default.createContext(null); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/helpers.mjs function resisted(y, minY, maxOverflow = 25) { if (y >= minY) return y; const pastBoundary = minY - y, resistedDistance = Math.sqrt(pastBoundary) * 2; @@ -23098,7 +23098,7 @@ function resisted(y, minY, maxOverflow = 25) { } __name(resisted, "resisted"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetController.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/useSheetController.mjs var import_react20 = __toESM(require("react"), 1); var useSheetController = /* @__PURE__ */ __name(() => { const controller = import_react20.default.useContext(SheetControllerContext), isHidden2 = controller?.hidden, isShowingNonSheet = isHidden2 && controller?.open; @@ -23111,7 +23111,7 @@ var useSheetController = /* @__PURE__ */ __name(() => { }, "useSheetController"); var SheetControllerContext = import_react20.default.createContext(null); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetOpenState.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/useSheetOpenState.mjs var useSheetOpenState = /* @__PURE__ */ __name((props) => { const { isHidden: isHidden2, @@ -23132,7 +23132,7 @@ var useSheetOpenState = /* @__PURE__ */ __name((props) => { }; }, "useSheetOpenState"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetProviderProps.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/useSheetProviderProps.mjs var import_react21 = __toESM(require("react"), 1); var import_core5 = require("@tamagui/core"); function useSheetProviderProps(props, state, options = {}) { @@ -23217,7 +23217,7 @@ function useSheetProviderProps(props, state, options = {}) { } __name(useSheetProviderProps, "useSheetProviderProps"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetImplementationCustom.mjs var import_jsx_runtime13 = require("react/jsx-runtime"); var hiddenSize = 10000.1; var sheetHiddenStyleSheet = null; @@ -23503,10 +23503,10 @@ function getYPositions(mode, point, screenSize, frameSize) { } __name(getYPositions, "getYPositions"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs var import_core7 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+scroll-view@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@_m2y4hsuo7z4hxvlznc2ixtzv5m/node_modules/@tamagui/scroll-view/dist/esm/ScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+scroll-view@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@_idh45gqi2qxtb7czkumciemwtu/node_modules/@tamagui/scroll-view/dist/esm/ScrollView.mjs var import_web6 = require("@tamagui/core"); var import_react_native_web2 = __toESM(require_cjs(), 1); var ScrollView = (0, import_web6.styled)(import_react_native_web2.ScrollView, { @@ -23523,7 +23523,7 @@ var ScrollView = (0, import_web6.styled)(import_react_native_web2.ScrollView, { } }); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetScrollView.mjs var import_react23 = __toESM(require("react"), 1); var import_jsx_runtime14 = require("react/jsx-runtime"); var SHEET_SCROLL_VIEW_NAME = "SheetScrollView"; @@ -23643,7 +23643,7 @@ var SheetScrollView = import_react23.default.forwardRef(({ }); }); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/useSheetOffscreenSize.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/useSheetOffscreenSize.mjs var useSheetOffscreenSize = /* @__PURE__ */ __name(({ snapPoints, position, @@ -23666,7 +23666,7 @@ var useSheetOffscreenSize = /* @__PURE__ */ __name(({ return Number.isNaN(offscreenSize) ? 0 : offscreenSize; }, "useSheetOffscreenSize"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/createSheet.mjs var import_jsx_runtime15 = require("react/jsx-runtime"); function createSheet({ Handle: Handle2, @@ -23780,7 +23780,7 @@ function createSheet({ } __name(createSheet, "createSheet"); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/Sheet.mjs var Handle = (0, import_core9.styled)(XStack, { name: SHEET_HANDLE_NAME, variants: { @@ -23863,7 +23863,7 @@ var Sheet = createSheet({ Overlay }); -// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_nilek5me44it5wzk5rej5mjloy/node_modules/@tamagui/sheet/dist/esm/SheetController.mjs +// ../../node_modules/.pnpm/@tamagui+sheet@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_222sne7asnkopuiznd6yntdniq/node_modules/@tamagui/sheet/dist/esm/SheetController.mjs var import_react25 = __toESM(require("react"), 1); var import_core10 = require("@tamagui/core"); var import_jsx_runtime16 = require("react/jsx-runtime"); @@ -23887,7 +23887,7 @@ var SheetController = /* @__PURE__ */ __name(({ }); }, "SheetController"); -// ../../node_modules/.pnpm/@tamagui+font-size@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._qa3m3qlkzht77r5o72xlwraxiq/node_modules/@tamagui/font-size/dist/esm/getFontSize.mjs +// ../../node_modules/.pnpm/@tamagui+font-size@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7._b4shdti63vxqhkg5zexrnpfcv4/node_modules/@tamagui/font-size/dist/esm/getFontSize.mjs var import_core11 = require("@tamagui/core"); var getFontSize = /* @__PURE__ */ __name((inSize, opts) => { const res = getFontSizeVariable(inSize, opts); @@ -23909,14 +23909,14 @@ var getFontSizeToken = /* @__PURE__ */ __name((inSize, opts) => { return sizeTokens[tokenIndex] ?? size5; }, "getFontSizeToken"); -// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_kljsf33l6eqdaroxv47tnl47ge/node_modules/@tamagui/helpers-tamagui/dist/esm/useCurrentColor.mjs +// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+c_n4himsnm3g2dc63l4p62ba2f2e/node_modules/@tamagui/helpers-tamagui/dist/esm/useCurrentColor.mjs var import_web7 = require("@tamagui/core"); var useCurrentColor = /* @__PURE__ */ __name((colorProp) => { const theme = (0, import_web7.useTheme)(); return colorProp ? (0, import_web7.getVariable)(colorProp) : theme[colorProp]?.get() || theme.color?.get(); }, "useCurrentColor"); -// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+c_kljsf33l6eqdaroxv47tnl47ge/node_modules/@tamagui/helpers-tamagui/dist/esm/useGetThemedIcon.mjs +// ../../node_modules/.pnpm/@tamagui+helpers-tamagui@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+c_n4himsnm3g2dc63l4p62ba2f2e/node_modules/@tamagui/helpers-tamagui/dist/esm/useGetThemedIcon.mjs var import_react26 = __toESM(require("react"), 1); var useGetThemedIcon = /* @__PURE__ */ __name((props) => { const color = useCurrentColor(props.color); @@ -23928,7 +23928,7 @@ var useGetThemedIcon = /* @__PURE__ */ __name((props) => { }) : import_react26.default.createElement(el, props)); }, "useGetThemedIcon"); -// ../../node_modules/.pnpm/@tamagui+list-item@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._bm6g7oqpcdaqlpomwam2p4dveu/node_modules/@tamagui/list-item/dist/esm/ListItem.mjs +// ../../node_modules/.pnpm/@tamagui+list-item@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7._jt6wzec3w6ksf4fihrscp6hhda/node_modules/@tamagui/list-item/dist/esm/ListItem.mjs var import_web8 = require("@tamagui/core"); var import_jsx_runtime17 = require("react/jsx-runtime"); var NAME = "ListItem"; @@ -25346,7 +25346,7 @@ var computePosition2 = /* @__PURE__ */ __name((reference, floating, options) => }); }, "computePosition"); -// ../../node_modules/.pnpm/@floating-ui+react-dom@2.1.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.mjs +// ../../node_modules/.pnpm/@floating-ui+react-dom@2.1.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.mjs var React26 = __toESM(require("react"), 1); var import_react27 = require("react"); var ReactDOM = __toESM(require("react-dom"), 1); @@ -25584,10 +25584,10 @@ var size3 = /* @__PURE__ */ __name((options, deps) => ({ options: [options, deps] }), "size"); -// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@floating-ui/react/dist/floating-ui.react.mjs +// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react/dist/floating-ui.react.mjs var React28 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@floating-ui/react/dist/floating-ui.react.utils.mjs +// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react/dist/floating-ui.react.utils.mjs var React27 = __toESM(require("react"), 1); var import_react28 = require("react"); var import_tabbable = __toESM(require_dist(), 1); @@ -26105,7 +26105,7 @@ function enableFocusInside(container) { } __name(enableFocusInside, "enableFocusInside"); -// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@floating-ui/react/dist/floating-ui.react.mjs +// ../../node_modules/.pnpm/@floating-ui+react@0.27.15_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react/dist/floating-ui.react.mjs var import_jsx_runtime18 = require("react/jsx-runtime"); var import_tabbable2 = __toESM(require_dist(), 1); var ReactDOM2 = __toESM(require("react-dom"), 1); @@ -28606,10 +28606,10 @@ function useInnerOffset(context, props) { } __name(useInnerOffset, "useInnerOffset"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/Select.mjs var import_core21 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+separator@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7._ssqfrg2dxpzomet2g6vcbmqupe/node_modules/@tamagui/separator/dist/esm/Separator.mjs +// ../../node_modules/.pnpm/@tamagui+separator@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7._dllvy5f2tmeq7wriwlhcb2vruu/node_modules/@tamagui/separator/dist/esm/Separator.mjs var import_core13 = require("@tamagui/core"); var Separator = (0, import_core13.styled)(import_core13.Stack, { name: "Separator", @@ -28640,7 +28640,7 @@ var Separator = (0, import_core13.styled)(import_core13.Stack, { } }); -// ../../node_modules/.pnpm/@tamagui+use-debounce@1.132.17_react@19.1.0/node_modules/@tamagui/use-debounce/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-debounce@1.132.17_react@19.2.0/node_modules/@tamagui/use-debounce/dist/esm/index.mjs var React29 = __toESM(require("react"), 1); function debounce(func, wait, leading) { let timeout, isCancelled = false; @@ -28668,10 +28668,10 @@ function useDebounce(fn, wait, options = defaultOpts, mountArgs = [fn]) { } __name(useDebounce, "useDebounce"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/Select.mjs var React36 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/context.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/context.mjs var import_core14 = require("@tamagui/core"); var import_jsx_runtime19 = require("react/jsx-runtime"); var { @@ -28697,17 +28697,17 @@ var ForwardSelectContext = /* @__PURE__ */ __name(({ }) }), "ForwardSelectContext"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectContent.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectContent.mjs var import_core15 = require("@tamagui/core"); var import_react30 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/useSelectBreakpointActive.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/useSelectBreakpointActive.mjs var useShowSelectSheet = /* @__PURE__ */ __name((context) => { const breakpointActive = useAdaptIsActive(context.adaptScope); return context.open === false ? false : breakpointActive; }, "useShowSelectSheet"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectContent.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectContent.mjs var import_jsx_runtime20 = require("react/jsx-runtime"); var SelectContent = /* @__PURE__ */ __name(({ children, @@ -28742,16 +28742,16 @@ var SelectContent = /* @__PURE__ */ __name(({ }); }, "SelectContent"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs var import_core16 = require("@tamagui/core"); var React31 = __toESM(require("react"), 1); var import_react_dom5 = require("react-dom"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/constants.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/constants.mjs var SCROLL_ARROW_THRESHOLD = 8; var VIEWPORT_NAME = "SelectViewport"; -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectImpl.mjs var import_jsx_runtime21 = require("react/jsx-runtime"); var SelectInlineImpl = /* @__PURE__ */ __name((props) => { const { @@ -28956,7 +28956,7 @@ var SelectInlineImpl = /* @__PURE__ */ __name((props) => { }); }, "SelectInlineImpl"); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectItem.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectItem.mjs var import_core17 = require("@tamagui/core"); var React32 = __toESM(require("react"), 1); var import_jsx_runtime22 = require("react/jsx-runtime"); @@ -29074,7 +29074,7 @@ var SelectItem = ListItemFrame.styleable(function(props, forwardedRef) { disableTheme: true }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectItemText.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectItemText.mjs var import_core18 = require("@tamagui/core"); var React33 = __toESM(require("react"), 1); var import_jsx_runtime23 = require("react/jsx-runtime"); @@ -29117,7 +29117,7 @@ var SelectItemText = SelectItemTextFrame.styleable(function(props, forwardedRef) }); }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectScrollButton.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectScrollButton.mjs var React34 = __toESM(require("react"), 1); var import_react_dom6 = require("react-dom"); var import_jsx_runtime24 = require("react/jsx-runtime"); @@ -29205,7 +29205,7 @@ var SelectScrollButtonImpl = React34.memo(React34.forwardRef((props, forwardedRe }); })); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectTrigger.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectTrigger.mjs var import_core19 = require("@tamagui/core"); var React35 = __toESM(require("react"), 1); var import_jsx_runtime25 = require("react/jsx-runtime"); @@ -29264,7 +29264,7 @@ var SelectTrigger = React35.forwardRef(function(props, forwardedRef) { }); }); -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/SelectViewport.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/SelectViewport.mjs var import_core20 = require("@tamagui/core"); var import_jsx_runtime26 = require("react/jsx-runtime"); var SelectViewportFrame = (0, import_core20.styled)(ThemeableStack, { @@ -29369,7 +29369,7 @@ var selectViewportCSS = ` } `; -// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._kegfyago4sxesgu3n5wbsv3zou/node_modules/@tamagui/select/dist/esm/Select.mjs +// ../../node_modules/.pnpm/@tamagui+select@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._fl2zpvjeoqjb2qbgncntwzr23m/node_modules/@tamagui/select/dist/esm/Select.mjs var import_jsx_runtime27 = require("react/jsx-runtime"); var VALUE_NAME = "SelectValue"; var SelectValueFrame = (0, import_core21.styled)(SizableText2, { @@ -29680,10 +29680,10 @@ function SelectInner(props) { } __name(SelectInner, "SelectInner"); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_gqelx6ov7to6vickvrsmksulwq/node_modules/@tamagui/use-window-dimensions/dist/esm/index.mjs var import_react34 = __toESM(require("react"), 1); -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/initialValue.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_gqelx6ov7to6vickvrsmksulwq/node_modules/@tamagui/use-window-dimensions/dist/esm/initialValue.mjs var initialValue = { width: 800, height: 600, @@ -29691,7 +29691,7 @@ var initialValue = { fontScale: 1 }; -// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_oatllpdiqvnj4lgq3nynh6l74e/node_modules/@tamagui/use-window-dimensions/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+use-window-dimensions@1.132.17_react-native@0.79.5_@babel+core@7.28.0_@types+react@1_gqelx6ov7to6vickvrsmksulwq/node_modules/@tamagui/use-window-dimensions/dist/esm/helpers.mjs var lastSize = initialValue; var docEl = null; function getWindowSize() { @@ -29721,7 +29721,7 @@ if (isClient) { window.addEventListener("resize", onResize); } -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/createTamagui.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_@types_hi244uh6njxlycilikgiz74u6i/node_modules/tamagui/dist/esm/createTamagui.mjs var import_core22 = require("@tamagui/core"); var createTamagui = process.env.NODE_ENV !== "development" ? import_core22.createTamagui : (conf) => { const sizeTokenKeys = ["$true"], hasKeys = /* @__PURE__ */ __name((expectedKeys, obj) => expectedKeys.every((k) => typeof obj[k] < "u"), "hasKeys"), tamaguiConfig = (0, import_core22.createTamagui)(conf); @@ -29764,10 +29764,10 @@ Expected a subset of: ${expected.join(", ")} return tamaguiConfig; }; -// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28.0_@types_626seodotpt5tlymih577qaarq/node_modules/tamagui/dist/esm/index.mjs +// ../../node_modules/.pnpm/tamagui@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28.0_@types_hi244uh6njxlycilikgiz74u6i/node_modules/tamagui/dist/esm/index.mjs var import_core23 = require("@tamagui/core"); -// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.1.0_react@19.1.0__react-native-reanimated@3.17.5_@babel_o2wgg4cddvy3p37awo2nze5wqy/node_modules/@tamagui/config/dist/esm/v4.mjs +// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.2.0_react@19.2.0__react-native-reanimated@3.17.5_@babel_ylztudjig24qv4fl7hrumgsbaq/node_modules/@tamagui/config/dist/esm/v4.mjs var v4_exports = {}; __export(v4_exports, { animations: () => animationsCSS, @@ -29786,7 +29786,7 @@ __export(v4_exports, { tokens: () => tokens }); -// ../../node_modules/.pnpm/@tamagui+shorthands@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7_ymljrg5nxqugvrz7m3ktjvksse/node_modules/@tamagui/shorthands/dist/esm/v4.mjs +// ../../node_modules/.pnpm/@tamagui+shorthands@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7_ptfmdzrrnmkpwqk6pfdejoxhjq/node_modules/@tamagui/shorthands/dist/esm/v4.mjs var shorthands = { // text text: "textAlign", @@ -29864,13 +29864,13 @@ var nonCompilerShorthands = [ ]; Object.assign(shorthands, Object.fromEntries(nonCompilerShorthands)); -// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4w6nvesoibw5rto7vui5ou2zya/node_modules/@tamagui/themes/dist/esm/utils.mjs +// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._hlfsj4wnejftqxdw5c6se7oczy/node_modules/@tamagui/themes/dist/esm/utils.mjs function sizeToSpace(v) { return v === 0 ? 0 : v === 2 ? 0.5 : v === 4 ? 1 : v === 8 ? 1.5 : v <= 16 ? Math.round(v * 0.333) : Math.floor(v * 0.7 - 12); } __name(sizeToSpace, "sizeToSpace"); -// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4w6nvesoibw5rto7vui5ou2zya/node_modules/@tamagui/themes/dist/esm/v4-tokens.mjs +// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._hlfsj4wnejftqxdw5c6se7oczy/node_modules/@tamagui/themes/dist/esm/v4-tokens.mjs var size4 = { $0: 0, "$0.25": 2, @@ -29939,13 +29939,13 @@ var tokens = { size: size4 }; -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/isMinusZero.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/isMinusZero.mjs function isMinusZero(value) { return 1 / value === Number.NEGATIVE_INFINITY; } __name(isMinusZero, "isMinusZero"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/themeInfo.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/themeInfo.mjs var THEME_INFO = /* @__PURE__ */ new Map(); var getThemeInfo = /* @__PURE__ */ __name((theme, name) => THEME_INFO.get(name || JSON.stringify(theme)), "getThemeInfo"); var setThemeInfo = /* @__PURE__ */ __name((theme, info) => { @@ -29956,7 +29956,7 @@ var setThemeInfo = /* @__PURE__ */ __name((theme, info) => { THEME_INFO.set(info.name || JSON.stringify(theme), next), THEME_INFO.set(JSON.stringify(info.definition), next); }, "setThemeInfo"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/createTheme.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/createTheme.mjs var identityCache = /* @__PURE__ */ new Map(); function createThemeWithPalettes(palettes, defaultPalette, definition, options, name, skipCache = false) { if (!palettes[defaultPalette]) throw new Error(`No pallete: ${defaultPalette}`); @@ -29998,7 +29998,7 @@ var getValue = /* @__PURE__ */ __name((palette, value) => { return palette[index3]; }, "getValue"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/helpers.mjs function objectEntries(obj) { return Object.entries(obj); } @@ -30008,7 +30008,7 @@ function objectFromEntries(arr) { } __name(objectFromEntries, "objectFromEntries"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/masks.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/masks.mjs var createMask = /* @__PURE__ */ __name((createMask2) => typeof createMask2 == "function" ? { name: createMask2.name || "unnamed", mask: createMask2 @@ -30093,7 +30093,7 @@ var createStrengthenMask = /* @__PURE__ */ __name((defaultOptions2) => ({ }, defaultOptions2).mask }), "createStrengthenMask"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/applyMask.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/applyMask.mjs function applyMask(theme, mask, options = {}, parentName, nextName) { const info = getThemeInfo(theme, parentName); if (!info) throw new Error(process.env.NODE_ENV !== "production" ? "No info found for theme, you must pass the theme created by createThemeFromPalette directly to extendTheme" : "\u274C Err2"); @@ -30125,7 +30125,7 @@ function applyMaskStateless(info, mask, options = {}, parentName) { } __name(applyMaskStateless, "applyMaskStateless"); -// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core_jstjn4anqtzyfnhyed72zgjjyi/node_modules/@tamagui/create-theme/dist/esm/combineMasks.mjs +// ../../node_modules/.pnpm/@tamagui+create-theme@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core_q3vw5bletfxq2fpkeiekwkvwh4/node_modules/@tamagui/create-theme/dist/esm/combineMasks.mjs var combineMasks = /* @__PURE__ */ __name((...masks2) => ({ name: "combine-mask", mask: /* @__PURE__ */ __name((template, opts) => { @@ -30139,7 +30139,7 @@ var combineMasks = /* @__PURE__ */ __name((...masks2) => ({ }, "mask") }), "combineMasks"); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/ThemeBuilder.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/ThemeBuilder.mjs var ThemeBuilder = class { static { __name(this, "ThemeBuilder"); @@ -30413,7 +30413,7 @@ function hsla(hue, saturation, lightness, alpha) { } __name(hsla, "hsla"); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/defaultComponentThemes.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/defaultComponentThemes.mjs var defaultComponentThemes = { ListItem: { template: "surface1" @@ -30471,14 +30471,14 @@ var defaultComponentThemes = { } }; -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/helpers.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/helpers.mjs var objectKeys = /* @__PURE__ */ __name((obj) => Object.keys(obj), "objectKeys"); function objectFromEntries2(arr) { return Object.fromEntries(arr); } __name(objectFromEntries2, "objectFromEntries"); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplates.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplates.mjs var getTemplates = /* @__PURE__ */ __name(() => { const lightTemplates = getBaseTemplates("light"), darkTemplates = getBaseTemplates("dark"); return { @@ -30588,7 +30588,7 @@ var getBaseTemplates = /* @__PURE__ */ __name((scheme) => { }, "getBaseTemplates"); var defaultTemplates = getTemplates(); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/getThemeSuitePalettes.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/getThemeSuitePalettes.mjs var paletteSize = 12; var PALETTE_BACKGROUND_OFFSET = 6; var generateColorPalette = /* @__PURE__ */ __name(({ @@ -30634,7 +30634,7 @@ function getThemeSuitePalettes(palette) { } __name(getThemeSuitePalettes, "getThemeSuitePalettes"); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/createThemes.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/createThemes.mjs function createThemes(props) { const { accent, @@ -30828,7 +30828,7 @@ function createPalettes(palettes) { } __name(createPalettes, "createPalettes"); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplatesStronger.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplatesStronger.mjs var getTemplates2 = /* @__PURE__ */ __name(() => { const lightTemplates = getBaseTemplates2("light"), darkTemplates = getBaseTemplates2("dark"); return { @@ -30938,7 +30938,7 @@ var getBaseTemplates2 = /* @__PURE__ */ __name((scheme) => { }, "getBaseTemplates"); var defaultTemplatesStronger = getTemplates2(); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplatesStrongest.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/defaultTemplatesStrongest.mjs var getTemplates3 = /* @__PURE__ */ __name(() => { const lightTemplates = getBaseTemplates3("light"), darkTemplates = getBaseTemplates3("dark"); return { @@ -31048,7 +31048,7 @@ var getBaseTemplates3 = /* @__PURE__ */ __name((scheme) => { }, "getBaseTemplates"); var defaultTemplatesStrongest = getTemplates3(); -// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+cor_wg3g4jbnhjqr5gc2vlupnjypzy/node_modules/@tamagui/theme-builder/dist/esm/masks.mjs +// ../../node_modules/.pnpm/@tamagui+theme-builder@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+cor_se4tpyp5huasfmxkkcf5hzftuu/node_modules/@tamagui/theme-builder/dist/esm/masks.mjs var masks = { identity: createIdentityMask(), soften: createSoftenMask(), @@ -31134,7 +31134,7 @@ var masks = { }) }; -// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4w6nvesoibw5rto7vui5ou2zya/node_modules/@tamagui/themes/dist/esm/generated-v4-tamagui.mjs +// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._hlfsj4wnejftqxdw5c6se7oczy/node_modules/@tamagui/themes/dist/esm/generated-v4-tamagui.mjs function t(a) { let res = {}; for (const [ki, vi] of a) res[ks[ki]] = colors[vi]; @@ -31916,7 +31916,7 @@ var themes = { dark_tan_ProgressIndicator: n156 }; -// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+core@7.28._4w6nvesoibw5rto7vui5ou2zya/node_modules/@tamagui/themes/dist/esm/generated-v4.mjs +// ../../node_modules/.pnpm/@tamagui+themes@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+core@7.28._hlfsj4wnejftqxdw5c6se7oczy/node_modules/@tamagui/themes/dist/esm/generated-v4.mjs function t2(a) { let res = {}; for (const [ki, vi] of a) res[ks2[ki]] = colors2[vi]; @@ -32298,7 +32298,7 @@ var themes2 = { dark_green_ProgressIndicator: n662 }; -// ../../node_modules/.pnpm/@tamagui+animations-css@1.132.17_react-dom@19.1.0_react@19.1.0__react-native@0.79.5_@babel+co_yasy2xr5dd4frhxbyhpez3ohti/node_modules/@tamagui/animations-css/dist/esm/createAnimations.mjs +// ../../node_modules/.pnpm/@tamagui+animations-css@1.132.17_react-dom@19.2.0_react@19.2.0__react-native@0.79.5_@babel+co_g4knpifb7dmixrjrjm6z57h6ba/node_modules/@tamagui/animations-css/dist/esm/createAnimations.mjs var import_web9 = require("@tamagui/core"); var import_react35 = __toESM(require("react"), 1); function extractDuration(animation) { @@ -32390,7 +32390,7 @@ function createAnimations(animations) { } __name(createAnimations, "createAnimations"); -// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.1.0_react@19.1.0__react-native-reanimated@3.17.5_@babel_o2wgg4cddvy3p37awo2nze5wqy/node_modules/@tamagui/config/dist/esm/animationsCSS.mjs +// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.2.0_react@19.2.0__react-native-reanimated@3.17.5_@babel_ylztudjig24qv4fl7hrumgsbaq/node_modules/@tamagui/config/dist/esm/animationsCSS.mjs var smoothBezier = "cubic-bezier(0.215, 0.610, 0.355, 1.000)"; var animationsCSS = createAnimations({ "75ms": "ease-in 75ms", @@ -32407,7 +32407,7 @@ var animationsCSS = createAnimations({ tooltip: "ease-in 400ms" }); -// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.1.0_react@19.1.0__react-native-reanimated@3.17.5_@babel_o2wgg4cddvy3p37awo2nze5wqy/node_modules/@tamagui/config/dist/esm/v4-fonts.mjs +// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.2.0_react@19.2.0__react-native-reanimated@3.17.5_@babel_ylztudjig24qv4fl7hrumgsbaq/node_modules/@tamagui/config/dist/esm/v4-fonts.mjs var import_core24 = require("@tamagui/core"); var createSystemFont = /* @__PURE__ */ __name(({ font = {}, @@ -32457,7 +32457,7 @@ var fonts = { }) }; -// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.1.0_react@19.1.0__react-native-reanimated@3.17.5_@babel_o2wgg4cddvy3p37awo2nze5wqy/node_modules/@tamagui/config/dist/esm/v4-media.mjs +// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.2.0_react@19.2.0__react-native-reanimated@3.17.5_@babel_ylztudjig24qv4fl7hrumgsbaq/node_modules/@tamagui/config/dist/esm/v4-media.mjs var breakpoints = { "2xl": 1536, xl: 1280, @@ -32522,7 +32522,7 @@ var mediaQueryDefaultActive = { "2xs": true }; -// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.1.0_react@19.1.0__react-native-reanimated@3.17.5_@babel_o2wgg4cddvy3p37awo2nze5wqy/node_modules/@tamagui/config/dist/esm/v4.mjs +// ../../node_modules/.pnpm/@tamagui+config@1.132.17_react-dom@19.2.0_react@19.2.0__react-native-reanimated@3.17.5_@babel_ylztudjig24qv4fl7hrumgsbaq/node_modules/@tamagui/config/dist/esm/v4.mjs var selectionStyles = /* @__PURE__ */ __name((theme) => theme.color5 ? { backgroundColor: theme.color5, color: theme.color11 From 5bc8bffebdb8998dbaaf7fc24f232a67dab9f9a6 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 19:05:32 -0400 Subject: [PATCH 24/45] defer fresh staking cache refresh --- apps/web/src/hooks/useStaking.ts | 16 ++++++++++++++-- .../src/utils/staking-overview-cache.test.ts | 12 ++++++++++++ apps/web/src/utils/staking-overview-cache.ts | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index 0186b71..b579a13 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -14,6 +14,7 @@ import { readStakingOverviewCache, writeStakingOverviewCache, getStakingRefreshProgress, + getStakingAutoRefreshDelay, type StakingOverviewCache, type StakingParams, type SlashingParams, @@ -61,6 +62,7 @@ const useStaking = (address = '', isEvm = false) => { const [isRefreshing, setRefreshing] = useState(false); const [refreshProgress, setRefreshProgress] = useState(0); const [lastUpdated, setLastUpdated] = useState(null); + const [isCacheReady, setCacheReady] = useState(false); const refreshingRef = useRef(false); const initializedRef = useRef(false); const [rewards, setRewards] = useState([]); @@ -287,8 +289,18 @@ const useStaking = (address = '', isEvm = false) => { const cachedOverview = readStakingOverviewCache(window.localStorage, CHAIN_ID); if (cachedOverview) applyOverview(cachedOverview); - void refreshOverview(); - }, [applyOverview, refreshOverview]); + setCacheReady(true); + }, [applyOverview]); + + useEffect(() => { + if (!isCacheReady) return; + + const refreshTimer = window.setTimeout(() => { + void refreshOverview(); + }, getStakingAutoRefreshDelay(lastUpdated)); + + return () => window.clearTimeout(refreshTimer); + }, [isCacheReady, lastUpdated, refreshOverview]); useEffect(() => { if (canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { diff --git a/apps/web/src/utils/staking-overview-cache.test.ts b/apps/web/src/utils/staking-overview-cache.test.ts index 35f5a34..3584753 100644 --- a/apps/web/src/utils/staking-overview-cache.test.ts +++ b/apps/web/src/utils/staking-overview-cache.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { getStakingOverviewCacheKey, + getStakingAutoRefreshDelay, getStakingRefreshProgress, readStakingOverviewCache, writeStakingOverviewCache, @@ -74,4 +75,15 @@ describe('staking overview cache', () => { expect(getStakingRefreshProgress(11, 10)).toBe(100); expect(getStakingRefreshProgress(1, 0)).toBe(0); }); + + it('waits until cached data is stale before refreshing automatically', () => { + const now = 1_786_640_000_000; + const fiveMinutes = 5 * 60 * 1000; + + expect(getStakingAutoRefreshDelay(now, now, fiveMinutes)).toBe(fiveMinutes); + expect(getStakingAutoRefreshDelay(now - 60_000, now, fiveMinutes)) + .toBe(fiveMinutes - 60_000); + expect(getStakingAutoRefreshDelay(now - fiveMinutes, now, fiveMinutes)).toBe(0); + expect(getStakingAutoRefreshDelay(null, now, fiveMinutes)).toBe(0); + }); }); diff --git a/apps/web/src/utils/staking-overview-cache.ts b/apps/web/src/utils/staking-overview-cache.ts index 71bc756..88edf2d 100644 --- a/apps/web/src/utils/staking-overview-cache.ts +++ b/apps/web/src/utils/staking-overview-cache.ts @@ -31,6 +31,8 @@ export interface StakingOverviewCache { bondedTokens: number; } +export const STAKING_AUTO_REFRESH_INTERVAL_MS = 5 * 60 * 1000; + interface CacheStorage { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; @@ -44,6 +46,22 @@ export const getStakingRefreshProgress = (completed: number, total: number): num return Math.min(100, Math.max(0, Math.round((completed / total) * 100))); }; +export const getStakingAutoRefreshDelay = ( + lastUpdated: number | null, + now = Date.now(), + refreshInterval = STAKING_AUTO_REFRESH_INTERVAL_MS, +): number => { + if (lastUpdated === null + || !Number.isFinite(lastUpdated) + || !Number.isFinite(now) + || !Number.isFinite(refreshInterval) + || refreshInterval <= 0) { + return 0; + } + + return Math.max(0, lastUpdated + refreshInterval - now); +}; + export const isStakingOverviewCache = (value: unknown): value is StakingOverviewCache => { if (!value || typeof value !== 'object') return false; From 46f57f740588595da8dd9357376431cb335cda06 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 19:16:41 -0400 Subject: [PATCH 25/45] make WalletConnect initialization idempotent --- .../web/src/app/providers/wallet-provider.tsx | 11 ++++-- apps/web/src/utils/wallet-connect.test.ts | 38 +++++++++++++++++++ apps/web/src/utils/wallet-connect.ts | 33 ++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/utils/wallet-connect.test.ts create mode 100644 apps/web/src/utils/wallet-connect.ts diff --git a/apps/web/src/app/providers/wallet-provider.tsx b/apps/web/src/app/providers/wallet-provider.tsx index 3489919..abd774a 100644 --- a/apps/web/src/app/providers/wallet-provider.tsx +++ b/apps/web/src/app/providers/wallet-provider.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { Provider } from 'react-redux'; -import { WCWallet } from '@interchain-kit/core'; import { PersistGate } from 'redux-persist/integration/react'; import { ChainProvider } from '@interchain-kit/react'; import { keplrWallet } from '@interchain-kit/keplr-extension'; @@ -22,6 +21,7 @@ import { WALLET_CONNECT_ICON, } from '@/contants/network'; import { getChains } from '@/utils/helpers'; +import { getWalletConnectWallet } from '@/utils/wallet-connect'; import { RegistryProvider } from "./RegistryContext"; import { EvmWalletProvider } from './evm-wallet-provider'; import store, { persistor } from '@/store'; @@ -49,7 +49,7 @@ export function WebWalletProviders({ children }: { children: React.ReactNode }) setChainData({ chain: foundChain, assets: foundAssets }); }, [isBrowser, chains, assetLists]); // Setup WalletConnect with custom metadata - const walletConnect = React.useMemo(() => new WCWallet(undefined, { + const walletConnect = getWalletConnectWallet({ projectId: WALLET_CONNECT_PROJECTID, relayUrl: WALLET_CONNECT_RELAY_URL, metadata: { @@ -58,10 +58,13 @@ export function WebWalletProviders({ children }: { children: React.ReactNode }) url: WALLET_CONNECT_URL, icons: [WALLET_CONNECT_ICON], }, - }), []); + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const walletAdapters: any = [keplrWallet, leapWallet, cosmostationWallet, walletConnect]; + const walletAdapters: any = React.useMemo( + () => [keplrWallet, leapWallet, cosmostationWallet, walletConnect], + [walletConnect], + ); return ( diff --git a/apps/web/src/utils/wallet-connect.test.ts b/apps/web/src/utils/wallet-connect.test.ts new file mode 100644 index 0000000..5bc0cd5 --- /dev/null +++ b/apps/web/src/utils/wallet-connect.test.ts @@ -0,0 +1,38 @@ +import { WCWallet } from '@interchain-kit/core'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { IdempotentWCWallet } from './wallet-connect'; + +describe('IdempotentWCWallet', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('shares one initialization across concurrent callers', async () => { + let finishInitialization: (() => void) | undefined; + const init = vi.spyOn(WCWallet.prototype, 'init').mockImplementation(() => ( + new Promise((resolve) => { finishInitialization = resolve; }) + )); + const wallet = new IdempotentWCWallet(); + + const first = wallet.init(); + const second = wallet.init(); + + expect(first).toBe(second); + expect(init).toHaveBeenCalledOnce(); + + finishInitialization?.(); + await Promise.all([first, second]); + }); + + it('allows initialization to be retried after a failure', async () => { + const init = vi.spyOn(WCWallet.prototype, 'init') + .mockRejectedValueOnce(new Error('temporary failure')) + .mockResolvedValueOnce(); + const wallet = new IdempotentWCWallet(); + + await expect(wallet.init()).rejects.toThrow('temporary failure'); + await expect(wallet.init()).resolves.toBeUndefined(); + expect(init).toHaveBeenCalledTimes(2); + }); +}); diff --git a/apps/web/src/utils/wallet-connect.ts b/apps/web/src/utils/wallet-connect.ts new file mode 100644 index 0000000..e46af7b --- /dev/null +++ b/apps/web/src/utils/wallet-connect.ts @@ -0,0 +1,33 @@ +import { WCWallet } from '@interchain-kit/core'; + +type WalletConnectOptions = ConstructorParameters[1]; + +export class IdempotentWCWallet extends WCWallet { + private initialization: Promise | null = null; + + override init(): Promise { + if (!this.initialization) { + this.initialization = super.init().catch((error) => { + this.initialization = null; + throw error; + }); + } + + return this.initialization; + } +} + +type WalletConnectGlobal = typeof globalThis & { + __lumeraHubWalletConnect?: IdempotentWCWallet; +}; + +export const getWalletConnectWallet = ( + options: WalletConnectOptions, +): IdempotentWCWallet => { + const globalState = globalThis as WalletConnectGlobal; + if (!globalState.__lumeraHubWalletConnect) { + globalState.__lumeraHubWalletConnect = new IdempotentWCWallet(undefined, options); + } + + return globalState.__lumeraHubWalletConnect; +}; From f04e3d3bd56bda6c0d676a299cbfbed0ff86c7d5 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Thu, 13 Aug 2026 19:46:43 -0400 Subject: [PATCH 26/45] fix MetaMask wallet account data --- apps/web/src/components/SendModal.tsx | 2 +- apps/web/src/components/layout/AppShell.tsx | 8 +- apps/web/src/hooks/useAccountInfo.test.ts | 78 +++++++++++++++++++ apps/web/src/hooks/useAccountInfo.ts | 68 +++++++++++++--- apps/web/src/hooks/useSend.ts | 8 +- apps/web/src/hooks/useTransaction.ts | 19 +++-- apps/web/src/utils/evm.test.ts | 15 ++++ apps/web/src/utils/evm.ts | 17 ++++ apps/web/src/utils/portfolio.test.ts | 45 +++++++++++ apps/web/src/utils/portfolio.ts | 30 +++++++ .../web/src/utils/transaction-history.test.ts | 34 ++++++++ apps/web/src/utils/transaction-history.ts | 20 +++++ packages/ui/src/screens/HomeScreen.tsx | 24 +----- packages/ui/src/screens/WalletScreen.tsx | 9 ++- 14 files changed, 326 insertions(+), 51 deletions(-) create mode 100644 apps/web/src/hooks/useAccountInfo.test.ts create mode 100644 apps/web/src/utils/portfolio.test.ts create mode 100644 apps/web/src/utils/portfolio.ts create mode 100644 apps/web/src/utils/transaction-history.test.ts create mode 100644 apps/web/src/utils/transaction-history.ts diff --git a/apps/web/src/components/SendModal.tsx b/apps/web/src/components/SendModal.tsx index 92e1743..e0006fc 100644 --- a/apps/web/src/components/SendModal.tsx +++ b/apps/web/src/components/SendModal.tsx @@ -182,7 +182,7 @@ export default function SendModal({
onInputChange('recipient', newValue)} diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx index 14f8c24..8bf89ca 100644 --- a/apps/web/src/components/layout/AppShell.tsx +++ b/apps/web/src/components/layout/AppShell.tsx @@ -109,8 +109,8 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
-
- Lumera +
+ Lumera
@@ -152,8 +152,8 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
-
- Lumera +
+ Lumera
diff --git a/apps/web/src/hooks/useAccountInfo.test.ts b/apps/web/src/hooks/useAccountInfo.test.ts new file mode 100644 index 0000000..aa2bd15 --- /dev/null +++ b/apps/web/src/hooks/useAccountInfo.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { fetchEvmAccountInfo, getTotalRewards } from './useAccountInfo'; + +describe('fetchEvmAccountInfo', () => { + it('combines the EVM balance with staking data queried by Bech32 address', async () => { + const getBalance = vi.fn().mockResolvedValue('0xde0b6b3a7640000'); + const delegation = { + delegation: { + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + shares: '2500000.000000000000000000', + }, + balance: { denom: 'ulume', amount: '2500000' }, + }; + const reward = { + validator_address: 'lumeravaloper1validator', + reward: [{ denom: 'ulume', amount: '125000.5' }], + }; + const rewardTotal = [{ denom: 'ulume', amount: '124999.75' }]; + const unbonding = { + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + entries: [], + }; + const get = vi.fn() + .mockResolvedValueOnce({ data: { delegation_responses: [delegation] } }) + .mockResolvedValueOnce({ data: { rewards: [reward], total: rewardTotal } }) + .mockResolvedValueOnce({ data: { unbonding_responses: [unbonding] } }); + + const accountInfo = await fetchEvmAccountInfo({ + ethAddress: '0x0123456789012345678901234567890123456789', + bech32Address: 'lumera1account', + getBalance, + get, + }); + + expect(getBalance).toHaveBeenCalledWith('0x0123456789012345678901234567890123456789'); + expect(get.mock.calls.map(([path]) => path)).toEqual([ + '/cosmos/staking/v1beta1/delegations/lumera1account', + '/cosmos/distribution/v1beta1/delegators/lumera1account/rewards', + '/cosmos/staking/v1beta1/delegators/lumera1account/unbonding_delegations', + ]); + expect(accountInfo).toEqual({ + balances: [{ denom: 'ulume', amount: '1000000' }], + delegations: [delegation], + rewards: [reward], + rewardTotal, + unbonding: [unbonding], + }); + }); + + it('does not query the EVM address through Cosmos staking endpoints', async () => { + const get = vi.fn(); + + await expect(fetchEvmAccountInfo({ + ethAddress: '0x0123456789012345678901234567890123456789', + bech32Address: '', + getBalance: vi.fn(), + get, + })).rejects.toThrow('Cannot query staking data without a Bech32 address.'); + + expect(get).not.toHaveBeenCalled(); + }); + + it('uses the chain-provided aggregate for claimable rewards', () => { + expect(getTotalRewards({ + balances: [], + delegations: [], + rewards: [{ + validator_address: 'lumeravaloper1validator', + reward: [{ denom: 'ulume', amount: '125000.5' }], + }], + rewardTotal: [{ denom: 'ulume', amount: '124999.75' }], + unbonding: [], + })).toBe(124999.75); + }); +}); diff --git a/apps/web/src/hooks/useAccountInfo.ts b/apps/web/src/hooks/useAccountInfo.ts index e3fc7a5..53fdc35 100644 --- a/apps/web/src/hooks/useAccountInfo.ts +++ b/apps/web/src/hooks/useAccountInfo.ts @@ -44,10 +44,60 @@ export interface AccountInfoData { balances: Coin[]; delegations: DelegationResponse[]; rewards: ValidatorRewards[]; + rewardTotal?: Coin[]; unbonding: ValidatorUnbonding[]; } +interface AccountInfoApiResponse { + data: T; +} + +interface FetchEvmAccountInfoOptions { + ethAddress: string; + bech32Address: string; + getBalance?: (address: string) => Promise; + get?: (path: string) => Promise>; +} + +export const fetchEvmAccountInfo = async ({ + ethAddress, + bech32Address, + getBalance = getEvmBalance, + get = instance.get, +}: FetchEvmAccountInfoOptions): Promise => { + if (!bech32Address) { + throw new Error('Cannot query staking data without a Bech32 address.'); + } + + const [balance, delegationsRes, rewardsRes, unbondingRes] = await Promise.all([ + getBalance(ethAddress), + get(`/cosmos/staking/v1beta1/delegations/${bech32Address}`), + get(`/cosmos/distribution/v1beta1/delegators/${bech32Address}/rewards`), + get(`/cosmos/staking/v1beta1/delegators/${bech32Address}/unbonding_delegations`), + ]); + const delegationsData = delegationsRes.data as { delegation_responses?: DelegationResponse[] }; + const rewardsData = rewardsRes.data as { rewards?: ValidatorRewards[]; total?: Coin[] }; + const unbondingData = unbondingRes.data as { unbonding_responses?: ValidatorUnbonding[] }; + + return { + balances: [{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }], + delegations: delegationsData.delegation_responses || [], + rewards: rewardsData.rewards || [], + rewardTotal: rewardsData.total || [], + unbonding: unbondingData.unbonding_responses || [], + }; +}; + export const getTotalRewards = (accountInfo: AccountInfoData | null) => { + if (accountInfo?.rewardTotal) { + return accountInfo.rewardTotal.reduce((total, reward) => { + if (reward.denom === DENOM) { + return total + Number(reward.amount); + } + return total; + }, 0); + } + let total = 0; if (accountInfo?.rewards?.length) { for (const item of accountInfo?.rewards) { @@ -62,7 +112,7 @@ export const getTotalRewards = (accountInfo: AccountInfoData | null) => { } const useAccountInfo = () => { - const { address, getClient, isEvm } = useWalletConnect(); + const { address, bech32Address, getClient, isEvm } = useWalletConnect(); const [accountInfo, setAccountInfo] = useState({ balances: [], @@ -92,17 +142,14 @@ const useAccountInfo = () => { try { if (isEvm) { - const balance = await getEvmBalance(address); - const _accountInfo: AccountInfoData = { - balances: [{ denom: DENOM, amount: evmBalanceToMicroLume(balance) }], - delegations: [], - rewards: [], - unbonding: [], - }; + const _accountInfo = await fetchEvmAccountInfo({ + ethAddress: address, + bech32Address, + }); setAccountInfo(_accountInfo); setClaimInfo((current) => ({ ...current, - totalRewards: '0', + totalRewards: `${getTotalRewards(_accountInfo)}`, })); return; } @@ -121,6 +168,7 @@ const useAccountInfo = () => { balances: balanceData.balances, delegations: delegationsData.delegation_responses, rewards: rewardsData.rewards, + rewardTotal: rewardsData.total, unbonding: resUnbonding.unbonding_responses, } setAccountInfo(_accountInfo); @@ -153,7 +201,7 @@ const useAccountInfo = () => { }); } fetchData(); - }, [address, isEvm]); + }, [address, bech32Address, isEvm]); const handleClaimButtonClick = async () => { setErrorClaim(null); diff --git a/apps/web/src/hooks/useSend.ts b/apps/web/src/hooks/useSend.ts index 3cbef3d..52ba5ec 100644 --- a/apps/web/src/hooks/useSend.ts +++ b/apps/web/src/hooks/useSend.ts @@ -11,7 +11,7 @@ import { evmBalanceToMicroLume, getEvmBalance, assertEvmAccountForChain, - isEvmAddress, + normalizeEvmRecipientAddress, parseEvmAmount, } from '@/utils/evm'; @@ -119,9 +119,7 @@ const useSend = (options: UseDepositOptions = {}) => { setLoading(true); try { if (isEvm) { - if (!isEvmAddress(optionsAdvanced.recipient)) { - throw new Error('Enter a valid EVM recipient address.'); - } + const recipient = normalizeEvmRecipientAddress(optionsAdvanced.recipient); if (!evmProvider) { throw new Error('No EVM wallet was detected.'); } @@ -139,7 +137,7 @@ const useSend = (options: UseDepositOptions = {}) => { method: 'eth_sendTransaction', params: [{ from: activeAddress, - to: optionsAdvanced.recipient, + to: recipient, value: parseEvmAmount(optionsAdvanced.amount), }], }); diff --git a/apps/web/src/hooks/useTransaction.ts b/apps/web/src/hooks/useTransaction.ts index 1bc438f..cb5ed11 100644 --- a/apps/web/src/hooks/useTransaction.ts +++ b/apps/web/src/hooks/useTransaction.ts @@ -4,6 +4,7 @@ import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; import { TLog, TLogEvent, TMessage, TOption, TSignerInfos, TFee } from '@/hooks/useRecentActivity'; import { Coin } from '@/hooks/useAccountInfo'; +import { getTransactionHistoryAddress } from '@/utils/transaction-history'; const LIMIT = 20; @@ -42,14 +43,15 @@ export interface ITransaction { } const useTransaction = () => { - const { address, isEvm } = useWalletConnect(); + const { address, bech32Address, isEvm } = useWalletConnect(); + const transactionAddress = getTransactionHistoryAddress({ address, bech32Address, isEvm }); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(''); const [transactions, setTransactions] = useState([]); const [totalTransactions, setTotalTransactions] = useState(0); const fetchTransactions = async (offset = 0) => { - if (isEvm) { + if (!transactionAddress) { setTransactions([]); setTotalTransactions(0); setError(''); @@ -60,9 +62,9 @@ const useTransaction = () => { setError(''); try { - const { data } = await instance.get(`/cosmos/tx/v1beta1/txs?query=message.sender=%27${address}%27&pagination.limit=${LIMIT}&pagination.offset=${offset}&order_by=ORDER_BY_DESC`); + const { data } = await instance.get(`/cosmos/tx/v1beta1/txs?query=message.sender=%27${transactionAddress}%27&pagination.limit=${LIMIT}&pagination.offset=${offset}&order_by=ORDER_BY_DESC`); setTotalTransactions(Math.ceil(Number(data.total) / LIMIT)); - setTransactions(data.tx_responses); + setTransactions(data.tx_responses || []); } catch (e) { setError(e instanceof Error ? e.message : 'An unknown error occurred.'); } finally { @@ -71,10 +73,15 @@ const useTransaction = () => { } useEffect(() => { - if (address) { + if (transactionAddress) { fetchTransactions(); + } else { + setTransactions([]); + setTotalTransactions(0); + setError(''); + setLoading(false); } - }, [address, isEvm]); + }, [transactionAddress]); const handlePageClick = ({ selected }: { selected: number }) => { const offset = selected * LIMIT; diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts index 92a3e71..6d8f6e1 100644 --- a/apps/web/src/utils/evm.test.ts +++ b/apps/web/src/utils/evm.test.ts @@ -18,6 +18,7 @@ import { getEvmAddressFormats, getMetaMaskProvider, isEvmAddress, + normalizeEvmRecipientAddress, parseEvmAmount, requestEvmRpc, toHexChainId, @@ -99,6 +100,20 @@ describe('EVM account address formats', () => { expect(() => cosmosAddressToEvmAddress(toBech32('lumera', new Uint8Array([1])))) .toThrow('not 20 bytes'); }); + + it('normalizes Lumera Bech32 and EVM transaction recipients', () => { + expect(normalizeEvmRecipientAddress(bech32Address)).toBe(ADDRESS); + expect(normalizeEvmRecipientAddress(` ${ADDRESS} `)).toBe(ADDRESS); + }); + + it('rejects malformed and foreign-prefix transaction recipients', () => { + const cosmosAddress = toBech32('cosmos', new Uint8Array(20)); + + expect(() => normalizeEvmRecipientAddress('not-an-address')) + .toThrow('valid lumera Bech32 or EVM recipient address'); + expect(() => normalizeEvmRecipientAddress(cosmosAddress)) + .toThrow('valid lumera Bech32 or EVM recipient address'); + }); }); afterEach(() => { diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index 7094751..05f81b9 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -44,6 +44,23 @@ export const cosmosAddressToEvmAddress = (address: string) => { return `0x${toHex(decoded.data)}`; }; +export const normalizeEvmRecipientAddress = (address: string, prefix = 'lumera') => { + const normalized = address.trim(); + if (isEvmAddress(normalized)) { + return normalized; + } + + try { + const decoded = fromBech32(normalized); + if (decoded.prefix !== prefix || decoded.data.length !== 20) { + throw new Error('Invalid account address.'); + } + return `0x${toHex(decoded.data)}`; + } catch { + throw new Error(`Enter a valid ${prefix} Bech32 or EVM recipient address.`); + } +}; + export const getEvmAddressFormats = (address: string, isEvmNetwork: boolean) => { if (!address) { return { bech32Address: '', ethAddress: '' }; diff --git a/apps/web/src/utils/portfolio.test.ts b/apps/web/src/utils/portfolio.test.ts new file mode 100644 index 0000000..8dd6ee5 --- /dev/null +++ b/apps/web/src/utils/portfolio.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { getPortfolioData } from './portfolio'; + +describe('getPortfolioData', () => { + it('returns raw numeric LUME amounts suitable for chart values', () => { + const result = getPortfolioData({ + balances: [ + { denom: 'ulume', amount: '152539032533' }, + { denom: 'other', amount: '999999999999' }, + ], + delegations: [{ + delegation: { + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + shares: '10001000000.000000000000000000', + }, + balance: { denom: 'ulume', amount: '9971027013' }, + }], + rewards: [], + unbonding: [], + }); + + expect(result).toEqual({ + stacked: 9971027013, + liquid: 152539032533, + }); + expect(Number.isFinite(result.stacked)).toBe(true); + expect(Number.isFinite(result.liquid)).toBe(true); + }); + + it('normalizes display-denom balances and ignores unrelated tokens', () => { + const result = getPortfolioData({ + balances: [ + { denom: 'lume', amount: '1.5' }, + { denom: 'ibc/token', amount: '4000000' }, + ], + delegations: [], + rewards: [], + unbonding: [], + }); + + expect(result).toEqual({ stacked: 0, liquid: 1500000 }); + }); +}); diff --git a/apps/web/src/utils/portfolio.ts b/apps/web/src/utils/portfolio.ts new file mode 100644 index 0000000..180f80a --- /dev/null +++ b/apps/web/src/utils/portfolio.ts @@ -0,0 +1,30 @@ +import { RATE_VALUE } from '@/contants'; +import { DENOM } from '@/contants/network'; +import type { AccountInfoData, Coin } from '@/hooks/useAccountInfo'; + +const toMicroLume = (coin: Coin) => { + if (coin.denom === DENOM) { + return Number(coin.amount); + } + if (coin.denom === 'lume') { + return Number(coin.amount) * RATE_VALUE; + } + return 0; +}; + +export const getPortfolioData = (accountInfo: AccountInfoData | null) => { + if (!accountInfo) { + return { stacked: 0, liquid: 0 }; + } + + return { + stacked: accountInfo.delegations.reduce( + (total, item) => total + toMicroLume(item.balance), + 0, + ), + liquid: accountInfo.balances.reduce( + (total, balance) => total + toMicroLume(balance), + 0, + ), + }; +}; diff --git a/apps/web/src/utils/transaction-history.test.ts b/apps/web/src/utils/transaction-history.test.ts new file mode 100644 index 0000000..c61ecd8 --- /dev/null +++ b/apps/web/src/utils/transaction-history.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { + getTransactionHistoryAddress, + isTransactionSuccessful, +} from './transaction-history'; + +describe('transaction history', () => { + it('queries the equivalent Bech32 account in MetaMask mode', () => { + expect(getTransactionHistoryAddress({ + address: '0x0123456789012345678901234567890123456789', + bech32Address: 'lumera1account', + isEvm: true, + })).toBe('lumera1account'); + + expect(getTransactionHistoryAddress({ + address: 'lumera1account', + bech32Address: 'lumera1account', + isEvm: false, + })).toBe('lumera1account'); + }); + + it('uses the indexed Cosmos result as the Wallet transaction status', () => { + expect(isTransactionSuccessful({ + code: 0, + events: [{ + type: 'ethereum_tx', + attributes: [{ key: 'ethereumTxFailed', value: 'execution reverted' }], + }], + })).toBe(true); + expect(isTransactionSuccessful({ code: 5 })).toBe(false); + }); + +}); diff --git a/apps/web/src/utils/transaction-history.ts b/apps/web/src/utils/transaction-history.ts new file mode 100644 index 0000000..4224e10 --- /dev/null +++ b/apps/web/src/utils/transaction-history.ts @@ -0,0 +1,20 @@ +interface TransactionHistoryAddressOptions { + address: string; + bech32Address: string; + isEvm: boolean; +} + +interface IndexedTransactionStatus { + code: number; + events?: unknown; +} + +export const getTransactionHistoryAddress = ({ + address, + bech32Address, + isEvm, +}: TransactionHistoryAddressOptions) => isEvm ? bech32Address : address; + +export const isTransactionSuccessful = (transaction: IndexedTransactionStatus) => ( + transaction.code === 0 +); diff --git a/packages/ui/src/screens/HomeScreen.tsx b/packages/ui/src/screens/HomeScreen.tsx index f91a9fa..adfd59a 100644 --- a/packages/ui/src/screens/HomeScreen.tsx +++ b/packages/ui/src/screens/HomeScreen.tsx @@ -45,6 +45,7 @@ import { IProposal, VOTE_OPTIONS, broadcastModeOptions } from '@/hooks/usePropos import { formatToken, formatTokenDisplay } from '@/utils/format'; import { NAV_ITEMS } from '@/components/layout/AppShell'; import { DENOM } from '@/contants/network'; +import { getPortfolioData } from '@/utils/portfolio'; import { formatGovernanceVote, getGovernanceVoteValue, @@ -173,19 +174,6 @@ const getOption = (data: IPortfolioOverviewChart) => { } } -const getPortfolioData = (accountInfo: AccountInfoData | null) => { - let stacked = 0; - let liquid = 0; - if (accountInfo) { - stacked = accountInfo.delegations.reduce((total, item) => Number(item.balance.amount) + total, 0) - liquid = accountInfo.balances.reduce((total, item) => Number(item.amount) + total, 0) - } - return { - stacked, - liquid, - } -} - const formatMessage = (msgs: TMessage[]) => { if (msgs) { const sum: Record = msgs @@ -861,14 +849,8 @@ export const HomeScreen = ({
diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index 627688c..600746e 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -37,6 +37,7 @@ import { formatAddress, formatTokenDisplay } from '@/utils/format'; import { getMessages } from '@/utils/helpers'; import { IValidator } from '@/types/validator'; import { DENOM } from '@/contants/network'; +import { isTransactionSuccessful } from '@/utils/transaction-history'; import 'react-paginate/theme/basic/react-paginate.css'; @@ -447,7 +448,7 @@ export const WalletScreen = ({
- {!isEvm ? +

Transaction History

@@ -493,8 +494,8 @@ export const WalletScreen = ({
Transaction Status:
- - {tx?.code === 0 ? 'Success' : 'Failed'} + + {isTransactionSuccessful(tx) ? 'Success' : 'Failed'}
@@ -526,7 +527,7 @@ export const WalletScreen = ({
: null }
- : null} +
); }; From d005f62a6b52bc4109408431a2a81494d46a34af Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Fri, 14 Aug 2026 14:09:26 -0400 Subject: [PATCH 27/45] add global search, account inspector, and dashboard versions block - Global search dialog in the header accepting block height, tx hash, or account address (bech32 / ETH hex, case-insensitive), routing to the matching inspector - Account inspector page at /account/[address]: balances, delegations, both address formats, sent/received transaction history - Dashboard versions footer: network chain-id and node version, EVM chain ID, and Hub build version from git tag/commit - Extract shared portfolio aggregation helpers and TransactionHistory component from WalletScreen; parameterize account/tx fetchers by address - Fix wallet Unstaking total always reading zero (misread unbonding response) - EVM wallet provider refinements and named EVM network profiles - Add CHANGELOG.md and document Makefile targets in README Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + CHANGELOG.md | 70 + Makefile | 3 + README.md | 37 +- apps/web/.tamagui/tamagui.config.json | 8790 ++++++++--------- apps/web/next.config.js | 15 + apps/web/package.json | 2 + apps/web/src/app/account/[address]/page.tsx | 54 + apps/web/src/app/account/[validator]/page.tsx | 23 - .../src/app/providers/evm-wallet-provider.tsx | 81 +- .../web/src/app/providers/wallet-provider.tsx | 103 +- apps/web/src/app/wallet/page.tsx | 50 +- apps/web/src/components/ConnectWallet.tsx | 6 +- apps/web/src/components/SearchDialog.tsx | 133 + .../web/src/components/TransactionHistory.tsx | 162 + apps/web/src/components/VersionsInfo.tsx | 33 + apps/web/src/components/layout/AppShell.tsx | 8 +- apps/web/src/contants/network.test.ts | 3 + apps/web/src/contants/network.ts | 4 + apps/web/src/hooks/useAccount.ts | 55 + apps/web/src/hooks/useAccountInfo.test.ts | 55 +- apps/web/src/hooks/useAccountInfo.ts | 46 +- apps/web/src/hooks/useNodeInfo.ts | 32 + apps/web/src/hooks/useSend.ts | 6 +- apps/web/src/hooks/useTransaction.ts | 49 +- apps/web/src/utils/account.test.ts | 44 + apps/web/src/utils/account.ts | 33 + apps/web/src/utils/evm.test.ts | 173 + apps/web/src/utils/evm.ts | 162 + apps/web/src/utils/node-info.test.ts | 32 + apps/web/src/utils/node-info.ts | 18 + apps/web/src/utils/portfolio.test.ts | 77 +- apps/web/src/utils/portfolio.ts | 21 + apps/web/src/utils/search.test.ts | 75 + apps/web/src/utils/search.ts | 37 + .../web/src/utils/transaction-history.test.ts | 36 + apps/web/src/utils/transaction-history.ts | 39 + apps/web/src/utils/wallet-selection.test.ts | 70 + apps/web/src/utils/wallet-selection.ts | 68 + packages/ui/src/screens/AccountScreen.tsx | 244 +- packages/ui/src/screens/HomeScreen.tsx | 2 + packages/ui/src/screens/WalletScreen.tsx | 255 +- pnpm-lock.yaml | 6 + 43 files changed, 6442 insertions(+), 4773 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 apps/web/src/app/account/[address]/page.tsx delete mode 100644 apps/web/src/app/account/[validator]/page.tsx create mode 100644 apps/web/src/components/SearchDialog.tsx create mode 100644 apps/web/src/components/TransactionHistory.tsx create mode 100644 apps/web/src/components/VersionsInfo.tsx create mode 100644 apps/web/src/hooks/useAccount.ts create mode 100644 apps/web/src/hooks/useNodeInfo.ts create mode 100644 apps/web/src/utils/account.test.ts create mode 100644 apps/web/src/utils/account.ts create mode 100644 apps/web/src/utils/node-info.test.ts create mode 100644 apps/web/src/utils/node-info.ts create mode 100644 apps/web/src/utils/search.test.ts create mode 100644 apps/web/src/utils/search.ts diff --git a/.gitignore b/.gitignore index 118e909..c742479 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ yarn-error.log* *.pem #.tamagui/ + +# Superdesign local design context and temporary templates +.superdesign/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1fcd2fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to Lumera Hub are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-08-14 + +First public release of Lumera Hub — a web portal for the Lumera network covering +wallet management, staking, governance, and chain exploration. + +### Added + +**Dashboard** +- Network overview with recent activity, claimable staking rewards, and active + governance proposals with voting countdowns. +- Versions footer showing the connected network's chain ID and node version, + the EVM chain ID (on EVM-enabled networks), and the Hub build version taken + from the git tag or commit. + +**Wallet** +- Keplr and MetaMask wallet support with a selectable wallet picker. +- Send, receive (QR code), and stake flows with advanced fee/gas/memo options. +- Balance overview: available, staking, rewards, and unstaking totals. +- Both address formats (Bech32 and ETH hex) with click-to-copy. +- Paginated transaction history with block and transaction links. + +**EVM support** +- Selectable network profiles (mainnet, testnet, devnet) with EVM-enabled + profiles served via MetaMask: EVM balance queries, `eth_sendTransaction` + transfers, and Bech32 ↔ ETH hex address conversion throughout the app. + +**Staking** +- Validator list with sorting, filtering, and delegation totals. +- Delegate, undelegate, redelegate, and claim-rewards flows. +- "My Staking" view with sortable positions and unbonding entries. +- Validator details page: statistics, uptime over the last 100 blocks, + commission, and delegator list. +- Staking rewards calculator. + +**Governance** +- Proposal list with status, results, and voting countdowns. +- Vote flow with current-vote display, queried by Bech32 address. +- Proposal creation (text proposals) with deposit validation. + +**Explorers** +- Global search in the header accepting a block height, transaction hash, or + account address (Bech32 or ETH hex, case-insensitive) and routing to the + matching inspector. +- Block details page with proposer and transaction list. +- Transaction details page with decoded messages and events. +- Account inspector: balances, delegations, both address formats, and + sent/received transaction history for any address. +- Validator inspector linked from delegator lists. + +**Services** +- Cascade page with storage metrics, charts, and file browsing. +- Sense, Inference, and NFTs sections. + +### Fixed + +- Wallet "Unstaking" total always showing zero due to a misread unbonding + response. +- MetaMask account data not refreshing correctly on wallet changes. +- WalletConnect initialization running more than once per session. +- Governance votes not recorded when queried with an EVM address format. +- Countdown unit plurals (for example "1 days" → "1 day"). + +[1.0.0]: https://github.com/LumeraProtocol/lumera-hub/releases/tag/v1.0.0 diff --git a/Makefile b/Makefile index a8ae65f..8f795a5 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ DEVNET_ENV := \ NEXT_PUBLIC_CHAIN_ID=lumera-devnet-1 \ NEXT_PUBLIC_REST_AI_URL=https://lcd.pastel.network \ NEXT_PUBLIC_RPC_ENDPOINT=https://rpc.pastel.network \ + NEXT_PUBLIC_EVM_PROFILE_NAME=lumera-devnet-evm \ NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-rpc.pastel.network \ NEXT_PUBLIC_EVM_WS_ENDPOINT= \ NEXT_PUBLIC_EVM_CHAIN_ID=76857769 @@ -30,6 +31,7 @@ TESTNET_ENV := \ NEXT_PUBLIC_CHAIN_ID=lumera-testnet-2 \ NEXT_PUBLIC_REST_AI_URL=https://lcd-testnet.lumeraprotocol.com \ NEXT_PUBLIC_RPC_ENDPOINT=https://rpc-testnet.lumeraprotocol.com \ + NEXT_PUBLIC_EVM_PROFILE_NAME=lumera-testnet-evm \ NEXT_PUBLIC_EVM_RPC_ENDPOINT=https://evm-testnet.lumeraprotocol.com \ NEXT_PUBLIC_EVM_WS_ENDPOINT=https://evm-ws-testnet.lumeraprotocol.com \ NEXT_PUBLIC_EVM_CHAIN_ID=76857769 @@ -41,6 +43,7 @@ MAINNET_ENV := \ NEXT_PUBLIC_CHAIN_ID=lumera-mainnet-1 \ NEXT_PUBLIC_REST_AI_URL=https://lcd.lumera.io \ NEXT_PUBLIC_RPC_ENDPOINT=https://rpc.lumera.io \ + NEXT_PUBLIC_EVM_PROFILE_NAME= \ NEXT_PUBLIC_EVM_RPC_ENDPOINT= \ NEXT_PUBLIC_EVM_WS_ENDPOINT= \ NEXT_PUBLIC_EVM_CHAIN_ID= diff --git a/README.md b/README.md index 64b5b55..334c713 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,37 @@ A monorepo for Lumera Hub applications: web (Next.js), desktop (Tauri), and mobi - For desktop: Rust toolchain - For mobile: Xcode (macOS) or Android Studio +### Quick Start (Make) + +The fastest way to run the web app is through the Makefile. Each network target +installs dependencies, creates `apps/web/.env.local` from the template when it +is missing, and pins the complete network profile — no manual env editing needed: + +```bash +make devnet # run the dev server against Lumera devnet +make testnet # run the dev server against Lumera testnet +make mainnet # run the dev server against Lumera mainnet +``` + +All targets (replace `` with `devnet`, `testnet`, or `mainnet`): + +| Target | What it does | +| --- | --- | +| `make ` | Install, configure, and run the development server | +| `make -build` | Create a production build configured for that network | +| `make -check` | Run tests, type checking, and a production build | +| `make -preview` | Build and serve a production bundle | +| `make setup` | Install dependencies and create `.env.local` when missing | +| `make test` | Run web unit tests | +| `make typecheck` | Run web TypeScript checks | +| `make help` | List available commands (also the default target) | + +The dev and preview servers listen on port 3000; override it with `PORT`: + +```bash +make testnet PORT=3001 +``` + ### Installation ```bash @@ -21,14 +52,16 @@ pnpm install ### Select a network -Copy the web environment template and select one of the local network profiles: +The Make targets above set the network profile for you. When running the app +directly with pnpm instead, copy the web environment template and select one of +the local network profiles: ```bash cp apps/web/.env.example apps/web/.env.local ``` ```dotenv -q +NEXT_PUBLIC_NETWORK_PROFILE=testnet ``` Supported profiles are `devnet`, `testnet`, and `mainnet`. Their chain IDs and endpoints are defined together in `apps/web/src/contants/network.ts`. Individual `NEXT_PUBLIC_CHAIN_NAME`, `NEXT_PUBLIC_CHAIN_ID`, `NEXT_PUBLIC_RPC_ENDPOINT`, `NEXT_PUBLIC_REST_AI_URL`, `NEXT_PUBLIC_EVM_RPC_ENDPOINT`, `NEXT_PUBLIC_EVM_WS_ENDPOINT`, `NEXT_PUBLIC_EVM_CHAIN_ID`, and `NEXT_PUBLIC_SNAPI_URL` values can still override the selected profile. diff --git a/apps/web/.tamagui/tamagui.config.json b/apps/web/.tamagui/tamagui.config.json index e14ecef..21e21f6 100644 --- a/apps/web/.tamagui/tamagui.config.json +++ b/apps/web/.tamagui/tamagui.config.json @@ -1093,11 +1093,20 @@ { "moduleName": "tamagui", "nameToInfo": { - "Spacer": { + "AlertDialogAction": { "staticConfig": { "acceptsClassName": true, - "memo": true, - "componentName": "Spacer", + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexDirection": "column", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0 + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -1333,44 +1342,15 @@ "transformStyle": true, "userSelect": true }, - "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexDirection": "column", - "flexBasis": "auto", - "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "span", - "size": true, - "pointerEvents": "none" - }, - "variants": { - "size": { - "...": "Function" - }, - "flex": { - "true": { - "flexGrow": 1 - } - }, - "direction": { - "horizontal": { - "height": 0, - "minHeight": 0 - }, - "vertical": { - "width": 0, - "minWidth": 0 - }, - "both": {} - } - } + "componentName": "AlertDialogAction", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "Stack": { + "AlertDialogCancel": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -1618,10 +1598,16 @@ "touchAction": true, "transformStyle": true, "userSelect": true - } + }, + "componentName": "AlertDialogCancel", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "View": { + "AlertDialogDescription": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -1869,22 +1855,29 @@ "touchAction": true, "transformStyle": true, "userSelect": true - } + }, + "componentName": "AlertDialogDescription", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "AlertDialogAction": { + "AlertDialogOverlay": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "flexShrink": 0 + "flexShrink": 0, + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -2121,7 +2114,130 @@ "transformStyle": true, "userSelect": true }, - "componentName": "AlertDialogAction", + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "open": { + "true": { + "pointerEvents": "auto" + }, + "false": { + "pointerEvents": "none" + } + }, + "unstyled": { + "false": { + "fullscreen": true, + "position": "absolute", + "backgrounded": true, + "zIndex": 99999, + "pointerEvents": "auto" + } + } + }, + "componentName": "AlertDialogOverlay", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -2129,7 +2245,7 @@ "isHOC": true } }, - "AlertDialogCancel": { + "AlertDialogTitle": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -2378,7 +2494,7 @@ "transformStyle": true, "userSelect": true }, - "componentName": "AlertDialogCancel", + "componentName": "AlertDialogTitle", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -2386,7 +2502,7 @@ "isHOC": true } }, - "AlertDialogDescription": { + "AlertDialogTrigger": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -2635,7 +2751,7 @@ "transformStyle": true, "userSelect": true }, - "componentName": "AlertDialogDescription", + "componentName": "AlertDialogTrigger", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -2643,20 +2759,68 @@ "isHOC": true } }, - "AlertDialogOverlay": { + "Anchor": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "fontFamily": "$body", + "unstyled": false, + "tag": "a", + "accessibilityRole": "link" + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -2891,152 +3055,48 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "open": { - "true": { - "pointerEvents": "auto" - }, - "false": { - "pointerEvents": "none" - } - }, - "unstyled": { - "false": { - "fullscreen": true, - "position": "absolute", - "backgrounded": true, - "zIndex": 99999, - "pointerEvents": "auto" - } - } + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "AlertDialogOverlay", + "componentName": "Anchor", "isReactNative": false, - "isText": false, "isStyledHOC": false, "neverFlatten": true, "isHOC": true } }, - "AlertDialogTitle": { + "Article": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "flexShrink": 0 + "flexShrink": 0, + "tag": "article", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -3273,27 +3333,26 @@ "transformStyle": true, "userSelect": true }, - "componentName": "AlertDialogTitle", + "componentName": "Article", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "AlertDialogTrigger": { + "Aside": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "flexShrink": 0 + "flexShrink": 0, + "tag": "aside", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -3530,12 +3589,10 @@ "transformStyle": true, "userSelect": true }, - "componentName": "AlertDialogTrigger", + "componentName": "Aside", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, "AvatarFallback": { @@ -8699,20 +8756,21 @@ "neverFlatten": true } }, - "DialogClose": { + "Circle": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", - "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "tag": "button" + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center", + "circular": true }, "validStyles": { "backfaceVisibility": true, @@ -8949,29 +9007,396 @@ "transformStyle": true, "userSelect": true }, - "componentName": "DialogClose", + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "size": { + "...size": "Function", + ":number": "Function" + } + }, + "componentName": "Circle", "isReactNative": false, "isText": false, "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "memo": true } }, - "DialogContent": { + "DialogClose": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "size": "$true", - "unstyled": false + "tag": "button" + }, + "validStyles": { + "backfaceVisibility": true, + "borderBottomEndRadius": true, + "borderBottomStartRadius": true, + "borderBottomWidth": true, + "borderLeftWidth": true, + "borderRightWidth": true, + "borderBlockWidth": true, + "borderBlockEndWidth": true, + "borderBlockStartWidth": true, + "borderInlineWidth": true, + "borderInlineEndWidth": true, + "borderInlineStartWidth": true, + "borderStyle": true, + "borderBlockStyle": true, + "borderBlockEndStyle": true, + "borderBlockStartStyle": true, + "borderInlineStyle": true, + "borderInlineEndStyle": true, + "borderInlineStartStyle": true, + "borderTopEndRadius": true, + "borderTopStartRadius": true, + "borderTopWidth": true, + "borderWidth": true, + "transform": true, + "transformOrigin": true, + "alignContent": true, + "alignItems": true, + "alignSelf": true, + "borderEndWidth": true, + "borderStartWidth": true, + "bottom": true, + "display": true, + "end": true, + "flexBasis": true, + "flexDirection": true, + "flexWrap": true, + "gap": true, + "columnGap": true, + "rowGap": true, + "justifyContent": true, + "left": true, + "margin": true, + "marginBlock": true, + "marginBlockEnd": true, + "marginBlockStart": true, + "marginInline": true, + "marginInlineStart": true, + "marginInlineEnd": true, + "marginBottom": true, + "marginEnd": true, + "marginHorizontal": true, + "marginLeft": true, + "marginRight": true, + "marginStart": true, + "marginTop": true, + "marginVertical": true, + "overflow": true, + "padding": true, + "paddingBottom": true, + "paddingInline": true, + "paddingBlock": true, + "paddingBlockStart": true, + "paddingInlineEnd": true, + "paddingInlineStart": true, + "paddingEnd": true, + "paddingHorizontal": true, + "paddingLeft": true, + "paddingRight": true, + "paddingStart": true, + "paddingTop": true, + "paddingVertical": true, + "position": true, + "right": true, + "start": true, + "top": true, + "inset": true, + "insetBlock": true, + "insetBlockEnd": true, + "insetBlockStart": true, + "insetInline": true, + "insetInlineEnd": true, + "insetInlineStart": true, + "direction": true, + "shadowOffset": true, + "shadowRadius": true, + "backgroundColor": true, + "borderColor": true, + "borderBlockStartColor": true, + "borderBlockEndColor": true, + "borderBlockColor": true, + "borderBottomColor": true, + "borderInlineColor": true, + "borderInlineStartColor": true, + "borderInlineEndColor": true, + "borderTopColor": true, + "borderLeftColor": true, + "borderRightColor": true, + "borderEndColor": true, + "borderStartColor": true, + "shadowColor": true, + "color": true, + "textDecorationColor": true, + "textShadowColor": true, + "outlineColor": true, + "caretColor": true, + "borderRadius": true, + "borderTopLeftRadius": true, + "borderTopRightRadius": true, + "borderBottomLeftRadius": true, + "borderBottomRightRadius": true, + "borderStartStartRadius": true, + "borderStartEndRadius": true, + "borderEndStartRadius": true, + "borderEndEndRadius": true, + "width": true, + "height": true, + "minWidth": true, + "minHeight": true, + "maxWidth": true, + "maxHeight": true, + "blockSize": true, + "minBlockSize": true, + "maxBlockSize": true, + "inlineSize": true, + "minInlineSize": true, + "maxInlineSize": true, + "x": true, + "y": true, + "scale": true, + "perspective": true, + "scaleX": true, + "scaleY": true, + "skewX": true, + "skewY": true, + "matrix": true, + "rotate": true, + "rotateY": true, + "rotateX": true, + "rotateZ": true, + "WebkitLineClamp": true, + "animationIterationCount": true, + "aspectRatio": true, + "borderImageOutset": true, + "borderImageSlice": true, + "borderImageWidth": true, + "columnCount": true, + "flex": true, + "flexGrow": true, + "flexOrder": true, + "flexPositive": true, + "flexShrink": true, + "flexNegative": true, + "fontWeight": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowGap": true, + "gridRowStart": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnGap": true, + "gridColumnStart": true, + "gridTemplateColumns": true, + "gridTemplateAreas": true, + "lineClamp": true, + "opacity": true, + "order": true, + "orphans": true, + "tabSize": true, + "widows": true, + "zIndex": true, + "zoom": true, + "scaleZ": true, + "shadowOpacity": true, + "boxShadow": true, + "filter": true, + "transition": true, + "textWrap": true, + "backdropFilter": true, + "WebkitBackdropFilter": true, + "background": true, + "backgroundAttachment": true, + "backgroundBlendMode": true, + "backgroundClip": true, + "backgroundImage": true, + "backgroundOrigin": true, + "backgroundPosition": true, + "backgroundRepeat": true, + "backgroundSize": true, + "borderBottomStyle": true, + "borderImage": true, + "borderLeftStyle": true, + "borderRightStyle": true, + "borderTopStyle": true, + "boxSizing": true, + "clipPath": true, + "contain": true, + "containerType": true, + "content": true, + "cursor": true, + "float": true, + "mask": true, + "maskBorder": true, + "maskBorderMode": true, + "maskBorderOutset": true, + "maskBorderRepeat": true, + "maskBorderSlice": true, + "maskBorderSource": true, + "maskBorderWidth": true, + "maskClip": true, + "maskComposite": true, + "maskImage": true, + "maskMode": true, + "maskOrigin": true, + "maskPosition": true, + "maskRepeat": true, + "maskSize": true, + "maskType": true, + "mixBlendMode": true, + "objectFit": true, + "objectPosition": true, + "outlineOffset": true, + "outlineStyle": true, + "outlineWidth": true, + "overflowBlock": true, + "overflowInline": true, + "overflowX": true, + "overflowY": true, + "pointerEvents": true, + "scrollbarWidth": true, + "textEmphasis": true, + "touchAction": true, + "transformStyle": true, + "userSelect": true + }, + "componentName": "DialogClose", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "DialogContent": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "size": "$true", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -11303,20 +11728,61 @@ "isHOC": true } }, - "Form": { + "EnsureFlexed": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexDirection": "column", - "flexBasis": "auto", + "fontFamily": "unset", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "form" + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "opacity": 0, + "lineHeight": 0, + "height": 0, + "display": "flex", + "fontSize": 200, + "children": "wwwwwwwwwwwwwwwwwww", + "pointerEvents": "none" + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + } }, "validStyles": { "backfaceVisibility": true, @@ -11551,30 +12017,47 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "Form", "isReactNative": false, - "isText": false, "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "neverFlatten": true } }, - "FormFrame": { + "Fieldset": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "tag": "form" + "flexDirection": "column", + "tag": "fieldset", + "borderWidth": 0 }, "validStyles": { "backfaceVisibility": true, @@ -11811,25 +12294,48 @@ "transformStyle": true, "userSelect": true }, - "componentName": "Form", + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "horizontal": { + "true": { + "flexDirection": "row", + "alignItems": "center" + } + } + }, + "componentName": "Fieldset", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "FormTrigger": { + "Footer": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "flexShrink": 0 + "flexShrink": 0, + "tag": "footer", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -12066,28 +12572,26 @@ "transformStyle": true, "userSelect": true }, - "componentName": "FormTrigger", + "componentName": "Footer", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "Group": { + "Form": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "tag": "form" }, "validStyles": { "backfaceVisibility": true, @@ -12324,122 +12828,7 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true" - } - }, - "size": "Function" - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "GroupFrame", + "componentName": "Form", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -12447,20 +12836,20 @@ "isHOC": true } }, - "GroupFrame": { + "FormFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "tag": "form" }, "validStyles": { "backfaceVisibility": true, @@ -12697,141 +13086,25 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true" - } - }, - "size": "Function" - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "GroupFrame", + "componentName": "Form", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "XGroup": { + "FormTrigger": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "flexShrink": 0 }, "validStyles": { "backfaceVisibility": true, @@ -13068,122 +13341,7 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true" - } - }, - "size": "Function" - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "GroupFrame", + "componentName": "FormTrigger", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -13191,7 +13349,7 @@ "isHOC": true } }, - "YGroup": { + "Frame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -13456,115 +13614,28 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, "unstyled": { "false": { - "size": "$true" + "flex": 1, + "backgroundColor": "$background", + "borderTopLeftRadius": "$true", + "borderTopRightRadius": "$true", + "width": "100%", + "maxHeight": "100%", + "overflow": "hidden" } - }, - "size": "Function" + } }, "defaultVariants": { "unstyled": false }, - "componentName": "GroupFrame", + "componentName": "Sheet", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "Article": { + "Group": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -13576,8 +13647,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "tag": "article", - "flexDirection": "column" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -13814,269 +13885,130 @@ "transformStyle": true, "userSelect": true }, - "componentName": "Article", - "isReactNative": false, - "isText": false, - "isStyledHOC": false - } - }, - "Aside": { - "staticConfig": { - "acceptsClassName": true, - "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", - "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "aside", - "flexDirection": "column" + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "size": "$true" + } + }, + "size": "Function" }, - "validStyles": { - "backfaceVisibility": true, - "borderBottomEndRadius": true, - "borderBottomStartRadius": true, - "borderBottomWidth": true, - "borderLeftWidth": true, - "borderRightWidth": true, - "borderBlockWidth": true, - "borderBlockEndWidth": true, - "borderBlockStartWidth": true, - "borderInlineWidth": true, - "borderInlineEndWidth": true, - "borderInlineStartWidth": true, - "borderStyle": true, - "borderBlockStyle": true, - "borderBlockEndStyle": true, - "borderBlockStartStyle": true, - "borderInlineStyle": true, - "borderInlineEndStyle": true, - "borderInlineStartStyle": true, - "borderTopEndRadius": true, - "borderTopStartRadius": true, - "borderTopWidth": true, - "borderWidth": true, - "transform": true, - "transformOrigin": true, - "alignContent": true, - "alignItems": true, - "alignSelf": true, - "borderEndWidth": true, - "borderStartWidth": true, - "bottom": true, - "display": true, - "end": true, - "flexBasis": true, - "flexDirection": true, - "flexWrap": true, - "gap": true, - "columnGap": true, - "rowGap": true, - "justifyContent": true, - "left": true, - "margin": true, - "marginBlock": true, - "marginBlockEnd": true, - "marginBlockStart": true, - "marginInline": true, - "marginInlineStart": true, - "marginInlineEnd": true, - "marginBottom": true, - "marginEnd": true, - "marginHorizontal": true, - "marginLeft": true, - "marginRight": true, - "marginStart": true, - "marginTop": true, - "marginVertical": true, - "overflow": true, - "padding": true, - "paddingBottom": true, - "paddingInline": true, - "paddingBlock": true, - "paddingBlockStart": true, - "paddingInlineEnd": true, - "paddingInlineStart": true, - "paddingEnd": true, - "paddingHorizontal": true, - "paddingLeft": true, - "paddingRight": true, - "paddingStart": true, - "paddingTop": true, - "paddingVertical": true, - "position": true, - "right": true, - "start": true, - "top": true, - "inset": true, - "insetBlock": true, - "insetBlockEnd": true, - "insetBlockStart": true, - "insetInline": true, - "insetInlineEnd": true, - "insetInlineStart": true, - "direction": true, - "shadowOffset": true, - "shadowRadius": true, - "backgroundColor": true, - "borderColor": true, - "borderBlockStartColor": true, - "borderBlockEndColor": true, - "borderBlockColor": true, - "borderBottomColor": true, - "borderInlineColor": true, - "borderInlineStartColor": true, - "borderInlineEndColor": true, - "borderTopColor": true, - "borderLeftColor": true, - "borderRightColor": true, - "borderEndColor": true, - "borderStartColor": true, - "shadowColor": true, - "color": true, - "textDecorationColor": true, - "textShadowColor": true, - "outlineColor": true, - "caretColor": true, - "borderRadius": true, - "borderTopLeftRadius": true, - "borderTopRightRadius": true, - "borderBottomLeftRadius": true, - "borderBottomRightRadius": true, - "borderStartStartRadius": true, - "borderStartEndRadius": true, - "borderEndStartRadius": true, - "borderEndEndRadius": true, - "width": true, - "height": true, - "minWidth": true, - "minHeight": true, - "maxWidth": true, - "maxHeight": true, - "blockSize": true, - "minBlockSize": true, - "maxBlockSize": true, - "inlineSize": true, - "minInlineSize": true, - "maxInlineSize": true, - "x": true, - "y": true, - "scale": true, - "perspective": true, - "scaleX": true, - "scaleY": true, - "skewX": true, - "skewY": true, - "matrix": true, - "rotate": true, - "rotateY": true, - "rotateX": true, - "rotateZ": true, - "WebkitLineClamp": true, - "animationIterationCount": true, - "aspectRatio": true, - "borderImageOutset": true, - "borderImageSlice": true, - "borderImageWidth": true, - "columnCount": true, - "flex": true, - "flexGrow": true, - "flexOrder": true, - "flexPositive": true, - "flexShrink": true, - "flexNegative": true, - "fontWeight": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowGap": true, - "gridRowStart": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnGap": true, - "gridColumnStart": true, - "gridTemplateColumns": true, - "gridTemplateAreas": true, - "lineClamp": true, - "opacity": true, - "order": true, - "orphans": true, - "tabSize": true, - "widows": true, - "zIndex": true, - "zoom": true, - "scaleZ": true, - "shadowOpacity": true, - "boxShadow": true, - "filter": true, - "transition": true, - "textWrap": true, - "backdropFilter": true, - "WebkitBackdropFilter": true, - "background": true, - "backgroundAttachment": true, - "backgroundBlendMode": true, - "backgroundClip": true, - "backgroundImage": true, - "backgroundOrigin": true, - "backgroundPosition": true, - "backgroundRepeat": true, - "backgroundSize": true, - "borderBottomStyle": true, - "borderImage": true, - "borderLeftStyle": true, - "borderRightStyle": true, - "borderTopStyle": true, - "boxSizing": true, - "clipPath": true, - "contain": true, - "containerType": true, - "content": true, - "cursor": true, - "float": true, - "mask": true, - "maskBorder": true, - "maskBorderMode": true, - "maskBorderOutset": true, - "maskBorderRepeat": true, - "maskBorderSlice": true, - "maskBorderSource": true, - "maskBorderWidth": true, - "maskClip": true, - "maskComposite": true, - "maskImage": true, - "maskMode": true, - "maskOrigin": true, - "maskPosition": true, - "maskRepeat": true, - "maskSize": true, - "maskType": true, - "mixBlendMode": true, - "objectFit": true, - "objectPosition": true, - "outlineOffset": true, - "outlineStyle": true, - "outlineWidth": true, - "overflowBlock": true, - "overflowInline": true, - "overflowX": true, - "overflowY": true, - "pointerEvents": true, - "scrollbarWidth": true, - "textEmphasis": true, - "touchAction": true, - "transformStyle": true, - "userSelect": true + "defaultVariants": { + "unstyled": false }, - "componentName": "Aside", + "componentName": "GroupFrame", "isReactNative": false, "isText": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "Footer": { + "GroupFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -14088,8 +14020,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "tag": "footer", - "flexDirection": "column" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -14326,27 +14258,192 @@ "transformStyle": true, "userSelect": true }, - "componentName": "Footer", + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "size": "$true" + } + }, + "size": "Function" + }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "GroupFrame", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Header": { + "H1": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "header", + "wordWrap": "break-word", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", "accessibilityRole": "header", - "flexDirection": "column" + "fontFamily": "$heading", + "size": "$8", + "margin": 0, + "tag": "h1", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$10", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -14581,28 +14678,100 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "Header", + "defaultVariants": { + "unstyled": false + }, + "componentName": "H1", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "Main": { + "H2": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "main", - "flexDirection": "column" + "wordWrap": "break-word", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", + "margin": 0, + "tag": "h2", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$9", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -14837,28 +15006,100 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "Main", + "defaultVariants": { + "unstyled": false + }, + "componentName": "H2", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "Nav": { + "H3": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "nav", - "flexDirection": "column" + "wordWrap": "break-word", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", + "margin": 0, + "tag": "h3", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$8", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -15093,29 +15334,100 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "Nav", + "defaultVariants": { + "unstyled": false + }, + "componentName": "H3", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "Section": { + "H4": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "tag": "section", - "flexDirection": "column", - "accessibilityRole": "summary" + "wordWrap": "break-word", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", + "margin": 0, + "tag": "h4", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$7", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -15350,28 +15662,36 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "Section", + "defaultVariants": { + "unstyled": false + }, + "componentName": "H4", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "Image": { - "staticConfig": { - "defaultProps": {}, - "componentName": "Image", - "isReactNative": true, - "isText": false, - "acceptsClassName": true, - "inlineProps": {}, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true - } - }, - "Label": { + "H5": { "staticConfig": { "acceptsClassName": true, "isText": true, @@ -15379,10 +15699,14 @@ "display": "inline", "boxSizing": "border-box", "wordWrap": "break-word", - "whiteSpace": "pre-wrap", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", "margin": 0, - "fontFamily": "$body", - "tag": "label", + "tag": "h5", "unstyled": false }, "inlineWhenUnflattened": {}, @@ -15424,21 +15748,11 @@ }, "unstyled": { "false": { - "size": "$true", - "color": "$color", - "backgroundColor": "transparent", - "display": "flex", - "alignItems": "center", - "userSelect": "none", - "cursor": "default", - "pressStyle": { - "color": "$colorPress" - } + "size": "$6", + "color": "$color" } }, - "size": { - "...size": "Function" - }, + "size": "Function", "fontFamily": { "...": "Function" } @@ -15700,14 +16014,12 @@ "defaultVariants": { "unstyled": false }, - "componentName": "Label", + "componentName": "H5", "isReactNative": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "LabelFrame": { + "H6": { "staticConfig": { "acceptsClassName": true, "isText": true, @@ -15715,10 +16027,14 @@ "display": "inline", "boxSizing": "border-box", "wordWrap": "break-word", - "whiteSpace": "pre-wrap", + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", "margin": 0, - "fontFamily": "$body", - "tag": "label", + "tag": "h6", "unstyled": false }, "inlineWhenUnflattened": {}, @@ -15760,21 +16076,11 @@ }, "unstyled": { "false": { - "size": "$true", - "color": "$color", - "backgroundColor": "transparent", - "display": "flex", - "alignItems": "center", - "userSelect": "none", - "cursor": "default", - "pressStyle": { - "color": "$colorPress" - } + "size": "$5", + "color": "$color" } }, - "size": { - "...size": "Function" - }, + "size": "Function", "fontFamily": { "...": "Function" } @@ -16036,12 +16342,12 @@ "defaultVariants": { "unstyled": false }, - "componentName": "Label", + "componentName": "H6", "isReactNative": false, "isStyledHOC": false } }, - "ListItem": { + "Handle": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -16053,8 +16359,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "tag": "li", + "flexDirection": "row", "unstyled": false }, "validStyles": { @@ -16307,140 +16612,41 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { + "open": { "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } + "opacity": 1, + "pointerEvents": "auto" }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } + "false": { + "opacity": 0, + "pointerEvents": "none" } }, "unstyled": { "false": { - "size": "$true", - "alignItems": "center", - "justifyContent": "space-between", - "flexWrap": "nowrap", - "width": "100%", - "borderColor": "$borderColor", - "maxWidth": "100%", - "overflow": "hidden", - "flexDirection": "row", + "height": 10, + "borderRadius": 100, "backgroundColor": "$background", - "cursor": "default" - } - }, - "size": { - "...size": "Function" - }, - "active": { - "true": { + "zIndex": 10, + "marginHorizontal": "35%", + "marginBottom": "$2", + "opacity": 0.5, "hoverStyle": { - "backgroundColor": "$background" + "opacity": 0.7 } } - }, - "disabled": { - "true": { - "opacity": 0.5, - "pointerEvents": "none" - } } }, "defaultVariants": { "unstyled": false }, - "componentName": "ListItem", + "componentName": "SheetHandle", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "ListItemFrame": { + "Header": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -16452,9 +16658,9 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "tag": "li", - "unstyled": false + "tag": "header", + "accessibilityRole": "header", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -16691,153 +16897,13 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true", - "alignItems": "center", - "justifyContent": "space-between", - "flexWrap": "nowrap", - "width": "100%", - "borderColor": "$borderColor", - "maxWidth": "100%", - "overflow": "hidden", - "flexDirection": "row", - "backgroundColor": "$background", - "cursor": "default" - } - }, - "size": { - "...size": "Function" - }, - "active": { - "true": { - "hoverStyle": { - "backgroundColor": "$background" - } - } - }, - "disabled": { - "true": { - "opacity": 0.5, - "pointerEvents": "none" - } - } - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "ListItem", + "componentName": "Header", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "ListItemSubtitle": { + "Heading": { "staticConfig": { "acceptsClassName": true, "isText": true, @@ -16845,10 +16911,15 @@ "display": "inline", "boxSizing": "border-box", "wordWrap": "break-word", - "whiteSpace": "pre-wrap", - "margin": 0, - "fontFamily": "$body", - "unstyled": false + "unstyled": false, + "userSelect": "auto", + "color": "$color", + "whiteSpace": "normal", + "tag": "span", + "accessibilityRole": "header", + "fontFamily": "$heading", + "size": "$8", + "margin": 0 }, "inlineWhenUnflattened": {}, "variants": { @@ -16890,18 +16961,10 @@ "unstyled": { "false": { "size": "$true", - "color": "$color", - "flexGrow": 1, - "flexShrink": 1, - "ellipse": true, - "cursor": "inherit", - "opacity": 0.6, - "maxWidth": "100%" + "color": "$color" } }, - "size": { - "...size": "Function" - }, + "size": "Function", "fontFamily": { "...": "Function" } @@ -17160,15 +17223,131 @@ "textDecorationDistance": true, "WebkitBoxOrient": true }, + "componentName": "Heading", + "isReactNative": false, + "isStyledHOC": false + } + }, + "Image": { + "staticConfig": { + "defaultProps": {}, + "componentName": "Image", + "isReactNative": true, + "isText": false, + "acceptsClassName": true, + "inlineProps": {}, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "Input": { + "staticConfig": { + "isInput": true, + "accept": { + "placeholderTextColor": "color", + "selectionColor": "color" + }, + "variants": { + "unstyled": { + "false": { + "size": "$true", + "fontFamily": "$body", + "borderWidth": 1, + "outlineWidth": 0, + "color": "$color", + "tabIndex": 0, + "borderColor": "$borderColor", + "backgroundColor": "$background", + "minWidth": 0, + "hoverStyle": { + "borderColor": "$borderColorHover" + }, + "focusStyle": { + "borderColor": "$borderColorFocus" + }, + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineWidth": 2, + "outlineStyle": "solid" + } + } + }, + "size": { + "...size": "Function" + }, + "disabled": { + "true": {} + } + }, + "defaultProps": { + "unstyled": false + }, "defaultVariants": { "unstyled": false }, - "componentName": "ListItemSubtitle", - "isReactNative": false, + "componentName": "Input", + "isReactNative": true, + "isText": true, + "acceptsClassName": true, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "InputFrame": { + "staticConfig": { + "isInput": true, + "accept": { + "placeholderTextColor": "color", + "selectionColor": "color" + }, + "variants": { + "unstyled": { + "false": { + "size": "$true", + "fontFamily": "$body", + "borderWidth": 1, + "outlineWidth": 0, + "color": "$color", + "tabIndex": 0, + "borderColor": "$borderColor", + "backgroundColor": "$background", + "minWidth": 0, + "hoverStyle": { + "borderColor": "$borderColorHover" + }, + "focusStyle": { + "borderColor": "$borderColorFocus" + }, + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineWidth": 2, + "outlineStyle": "solid" + } + } + }, + "size": { + "...size": "Function" + }, + "disabled": { + "true": {} + } + }, + "defaultProps": { + "unstyled": false + }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "Input", + "isReactNative": true, + "isText": true, + "acceptsClassName": true, "isStyledHOC": false } }, - "ListItemText": { + "Label": { "staticConfig": { "acceptsClassName": true, "isText": true, @@ -17179,6 +17358,7 @@ "whiteSpace": "pre-wrap", "margin": 0, "fontFamily": "$body", + "tag": "label", "unstyled": false }, "inlineWhenUnflattened": {}, @@ -17222,13 +17402,19 @@ "false": { "size": "$true", "color": "$color", - "flexGrow": 1, - "flexShrink": 1, - "ellipse": true, - "cursor": "inherit" + "backgroundColor": "transparent", + "display": "flex", + "alignItems": "center", + "userSelect": "none", + "cursor": "default", + "pressStyle": { + "color": "$colorPress" + } } }, - "size": "Function", + "size": { + "...size": "Function" + }, "fontFamily": { "...": "Function" } @@ -17490,12 +17676,14 @@ "defaultVariants": { "unstyled": false }, - "componentName": "ListItemText", + "componentName": "Label", "isReactNative": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "ListItemTitle": { + "LabelFrame": { "staticConfig": { "acceptsClassName": true, "isText": true, @@ -17506,6 +17694,7 @@ "whiteSpace": "pre-wrap", "margin": 0, "fontFamily": "$body", + "tag": "label", "unstyled": false }, "inlineWhenUnflattened": {}, @@ -17549,13 +17738,19 @@ "false": { "size": "$true", "color": "$color", - "flexGrow": 1, - "flexShrink": 1, - "ellipse": true, - "cursor": "inherit" + "backgroundColor": "transparent", + "display": "flex", + "alignItems": "center", + "userSelect": "none", + "cursor": "default", + "pressStyle": { + "color": "$colorPress" + } } }, - "size": "Function", + "size": { + "...size": "Function" + }, "fontFamily": { "...": "Function" } @@ -17814,12 +18009,15 @@ "textDecorationDistance": true, "WebkitBoxOrient": true }, - "componentName": "ListItemTitle", + "defaultVariants": { + "unstyled": false + }, + "componentName": "Label", "isReactNative": false, "isStyledHOC": false } }, - "PopoverArrow": { + "ListItem": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -17832,6 +18030,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", + "tag": "li", "unstyled": false }, "validStyles": { @@ -18084,18 +18283,132 @@ ":number": "Function" }, "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, "unstyled": { "false": { + "size": "$true", + "alignItems": "center", + "justifyContent": "space-between", + "flexWrap": "nowrap", + "width": "100%", "borderColor": "$borderColor", + "maxWidth": "100%", + "overflow": "hidden", + "flexDirection": "row", "backgroundColor": "$background", - "position": "relative" + "cursor": "default" + } + }, + "size": { + "...size": "Function" + }, + "active": { + "true": { + "hoverStyle": { + "backgroundColor": "$background" + } + } + }, + "disabled": { + "true": { + "opacity": 0.5, + "pointerEvents": "none" } } }, "defaultVariants": { "unstyled": false }, - "componentName": "PopperArrow", + "componentName": "ListItem", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -18103,7 +18416,7 @@ "isHOC": true } }, - "PopoverContent": { + "ListItemFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -18116,6 +18429,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", + "tag": "li", "unstyled": false }, "validStyles": { @@ -18461,36 +18775,112 @@ "unstyled": { "false": { "size": "$true", - "backgroundColor": "$background", "alignItems": "center", - "radiused": true + "justifyContent": "space-between", + "flexWrap": "nowrap", + "width": "100%", + "borderColor": "$borderColor", + "maxWidth": "100%", + "overflow": "hidden", + "flexDirection": "row", + "backgroundColor": "$background", + "cursor": "default" } }, "size": { "...size": "Function" + }, + "active": { + "true": { + "hoverStyle": { + "backgroundColor": "$background" + } + } + }, + "disabled": { + "true": { + "opacity": 0.5, + "pointerEvents": "none" + } } }, - "componentName": "Popover", + "defaultVariants": { + "unstyled": false + }, + "componentName": "ListItem", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "PopperAnchor": { + "ListItemSubtitle": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column" + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "fontFamily": "$body", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color", + "flexGrow": 1, + "flexShrink": 1, + "ellipse": true, + "cursor": "inherit", + "opacity": 0.6, + "maxWidth": "100%" + } + }, + "size": { + "...size": "Function" + }, + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -18725,46 +19115,100 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function" + "defaultVariants": { + "unstyled": false }, + "componentName": "ListItemSubtitle", "isReactNative": false, - "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "PopperArrowFrame": { + "ListItemText": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "fontFamily": "$body", "unstyled": false }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color", + "flexGrow": 1, + "flexShrink": 1, + "ellipse": true, + "cursor": "inherit" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -18998,55 +19442,100 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "unstyled": { - "false": { - "borderColor": "$borderColor", - "backgroundColor": "$background", - "position": "relative" - } - } + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, "defaultVariants": { "unstyled": false }, - "componentName": "PopperArrow", + "componentName": "ListItemText", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "PopperContentFrame": { + "ListItemTitle": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "fontFamily": "$body", "unstyled": false }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color", + "flexGrow": 1, + "flexShrink": 1, + "ellipse": true, + "cursor": "inherit" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -19280,135 +19769,33 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true", - "backgroundColor": "$background", - "alignItems": "center", - "radiused": true - } - }, - "size": { - "...size": "Function" - } - }, - "defaultVariants": { - "unstyled": false + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "PopperContent", + "componentName": "ListItemTitle", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "Progress": { + "Main": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -19420,8 +19807,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "tag": "main", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -19658,134 +20045,13 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "borderRadius": 100000, - "overflow": "hidden", - "backgrounded": true - } - }, - "size": { - "...size": "Function" - } - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "Progress", + "componentName": "Main", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "ProgressFrame": { + "Nav": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -19797,8 +20063,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "tag": "nav", + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -20035,132 +20301,13 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "borderRadius": 100000, - "overflow": "hidden", - "backgrounded": true - } - }, - "size": { - "...size": "Function" - } - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "Progress", + "componentName": "Nav", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "ProgressIndicator": { + "Overlay": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -20515,39 +20662,97 @@ } } }, + "open": { + "true": { + "pointerEvents": "auto" + }, + "false": { + "pointerEvents": "none" + } + }, "unstyled": { "false": { - "height": "100%", - "width": "100%", - "backgrounded": true + "fullscreen": true, + "position": "absolute", + "backgrounded": true, + "zIndex": 99999, + "pointerEvents": "auto" } } }, "defaultVariants": { "unstyled": false }, - "componentName": "ProgressIndicator", + "componentName": "SheetOverlay", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "ProgressIndicatorFrame": { + "Paragraph": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "wordWrap": "break-word", + "margin": 0, + "fontFamily": "$body", + "unstyled": false, + "tag": "p", + "userSelect": "auto", + "color": "$color", + "size": "$true", + "whiteSpace": "normal" + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -20782,131 +20987,33 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "height": "100%", - "width": "100%", - "backgrounded": true - } - } - }, - "defaultVariants": { - "unstyled": false + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "componentName": "ProgressIndicator", + "componentName": "Paragraph", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "RadioGroup": { + "PopoverArrow": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -20918,7 +21025,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -21170,108 +21278,18 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "orientation": { - "horizontal": { - "flexDirection": "row", - "spaceDirection": "horizontal" - }, - "vertical": { - "flexDirection": "column", - "spaceDirection": "vertical" + "unstyled": { + "false": { + "borderColor": "$borderColor", + "backgroundColor": "$background", + "position": "relative" } } }, - "componentName": "RadioGroup", + "defaultVariants": { + "unstyled": false + }, + "componentName": "PopperArrow", "isReactNative": false, "isText": false, "isStyledHOC": false, @@ -21279,7 +21297,7 @@ "isHOC": true } }, - "RadioGroupFrame": { + "PopoverContent": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -21291,7 +21309,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -21633,24 +21652,27 @@ } } }, - "orientation": { - "horizontal": { - "flexDirection": "row", - "spaceDirection": "horizontal" - }, - "vertical": { - "flexDirection": "column", - "spaceDirection": "vertical" + "unstyled": { + "false": { + "size": "$true", + "backgroundColor": "$background", + "alignItems": "center", + "radiused": true } + }, + "size": { + "...size": "Function" } }, - "componentName": "RadioGroup", + "componentName": "Popover", "isReactNative": false, "isText": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "RadioGroupIndicatorFrame": { + "PopperAnchor": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -21662,8 +21684,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -21914,117 +21935,16 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "width": "33%", - "height": "33%", - "borderRadius": 1000, - "backgroundColor": "$color", - "pressTheme": true - } - } - }, - "defaultVariants": { - "unstyled": false + "inset": "Function" }, - "componentName": "RadioGroupIndicator", "isReactNative": false, "isText": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "RadioGroupItemFrame": { + "PopperArrowFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -22037,7 +21957,6 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "tag": "button", "unstyled": false }, "validStyles": { @@ -22290,183 +22209,24 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, "unstyled": { "false": { - "size": "$true", - "borderRadius": 1000, - "backgroundColor": "$background", - "alignItems": "center", - "justifyContent": "center", - "borderWidth": 1, "borderColor": "$borderColor", - "padding": 0, - "hoverStyle": { - "borderColor": "$borderColorHover", - "backgroundColor": "$backgroundHover" - }, - "focusStyle": { - "borderColor": "$borderColorHover", - "backgroundColor": "$backgroundHover" - }, - "focusVisibleStyle": { - "outlineStyle": "solid", - "outlineWidth": 2, - "outlineColor": "$outlineColor" - }, - "pressStyle": { - "borderColor": "$borderColorFocus", - "backgroundColor": "$backgroundFocus" - } - } - }, - "disabled": { - "true": { - "pointerEvents": "none", - "userSelect": "none", - "cursor": "not-allowed", - "hoverStyle": { - "borderColor": "$borderColor", - "backgroundColor": "$background" - }, - "pressStyle": { - "borderColor": "$borderColor", - "backgroundColor": "$background" - }, - "focusVisibleStyle": { - "outlineWidth": 0 - } + "backgroundColor": "$background", + "position": "relative" } - }, - "size": { - "...size": "Function" } }, "defaultVariants": { "unstyled": false }, - "componentName": "RadioGroupItem", + "componentName": "PopperArrow", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "ScrollView": { - "staticConfig": { - "accept": { - "contentContainerStyle": "style" - }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - } - }, - "defaultProps": { - "scrollEnabled": true - }, - "componentName": "ScrollView", - "isReactNative": true, - "isText": false, - "acceptsClassName": true, - "isStyledHOC": false - } - }, - "SelectGroupFrame": { + "PopperContentFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -22479,7 +22239,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "width": "100%" + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -22730,15 +22490,119 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function" + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "size": "$true", + "backgroundColor": "$background", + "alignItems": "center", + "radiused": true + } + }, + "size": { + "...size": "Function" + } }, - "componentName": "SelectGroup", + "defaultVariants": { + "unstyled": false + }, + "componentName": "PopperContent", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SelectIcon": { + "Progress": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -22750,9 +22614,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "row", - "aria-hidden": true, - "children": "Component" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -23003,35 +22866,133 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function" + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "borderRadius": 100000, + "overflow": "hidden", + "backgrounded": true + } + }, + "size": { + "...size": "Function" + } }, - "componentName": "SelectIcon", + "defaultVariants": { + "unstyled": false + }, + "componentName": "Progress", "isReactNative": false, "isText": false, "isStyledHOC": false, - "neverFlatten": true + "neverFlatten": true, + "isHOC": true } }, - "SelectSeparator": { + "ProgressFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "borderColor": "$borderColor", "flexShrink": 0, - "borderWidth": 0, - "flex": 1, - "height": 0, - "maxHeight": 0, - "borderBottomWidth": 1, - "y": -0.5 + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -23269,45 +23230,144 @@ "userSelect": true }, "variants": { - "vertical": { + "fullscreen": { "true": { - "y": 0, - "x": -0.5, - "height": "initial", - "maxHeight": "initial", - "width": 0, - "maxWidth": 0, - "borderBottomWidth": 0, - "borderRightWidth": 1 + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "borderRadius": 100000, + "overflow": "hidden", + "backgrounded": true } + }, + "size": { + "...size": "Function" } }, - "componentName": "SelectSeparator", + "defaultVariants": { + "unstyled": false + }, + "componentName": "Progress", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Separator": { + "ProgressIndicator": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", - "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, - "borderColor": "$borderColor", "flexShrink": 0, - "borderWidth": 0, - "flex": 1, - "height": 0, - "maxHeight": 0, - "borderBottomWidth": 1, - "y": -0.5 + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -23545,30 +23605,135 @@ "userSelect": true }, "variants": { - "vertical": { + "fullscreen": { "true": { - "y": 0, - "x": -0.5, - "height": "initial", - "maxHeight": "initial", - "width": 0, - "maxWidth": 0, - "borderBottomWidth": 0, - "borderRightWidth": 1 + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "unstyled": { + "false": { + "height": "100%", + "width": "100%", + "backgrounded": true } } }, - "componentName": "Separator", + "defaultVariants": { + "unstyled": false + }, + "componentName": "ProgressIndicator", "isReactNative": false, "isText": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "Square": { + "ProgressIndicatorFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", + "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", @@ -23576,8 +23741,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "alignItems": "center", - "justifyContent": "center" + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -23919,33 +24083,36 @@ } } }, - "size": { - "...size": "Function", - ":number": "Function" + "unstyled": { + "false": { + "height": "100%", + "width": "100%", + "backgrounded": true + } } }, - "componentName": "Square", + "defaultVariants": { + "unstyled": false + }, + "componentName": "ProgressIndicator", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "memo": true + "isStyledHOC": false } }, - "Circle": { + "RadioGroup": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", + "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "alignItems": "center", - "justifyContent": "center", - "circular": true + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -24287,19 +24454,26 @@ } } }, - "size": { - "...size": "Function", - ":number": "Function" + "orientation": { + "horizontal": { + "flexDirection": "row", + "spaceDirection": "horizontal" + }, + "vertical": { + "flexDirection": "column", + "spaceDirection": "vertical" + } } }, - "componentName": "Circle", + "componentName": "RadioGroup", "isReactNative": false, "isText": false, "isStyledHOC": false, - "memo": true + "neverFlatten": true, + "isHOC": true } }, - "Frame": { + "RadioGroupFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -24311,8 +24485,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -24564,28 +24737,114 @@ ":number": "Function" }, "inset": "Function", - "unstyled": { - "false": { - "flex": 1, - "backgroundColor": "$background", - "borderTopLeftRadius": "$true", - "borderTopRightRadius": "$true", - "width": "100%", - "maxHeight": "100%", - "overflow": "hidden" + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "orientation": { + "horizontal": { + "flexDirection": "row", + "spaceDirection": "horizontal" + }, + "vertical": { + "flexDirection": "column", + "spaceDirection": "vertical" } } }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "Sheet", + "componentName": "RadioGroup", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Handle": { + "RadioGroupIndicatorFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -24597,7 +24856,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "row", + "flexDirection": "column", "unstyled": false }, "validStyles": { @@ -24850,41 +25109,116 @@ ":number": "Function" }, "inset": "Function", - "open": { + "backgrounded": { "true": { - "opacity": 1, - "pointerEvents": "auto" + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } }, - "false": { - "opacity": 0, - "pointerEvents": "none" + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" } }, - "unstyled": { - "false": { - "height": 10, - "borderRadius": 100, - "backgroundColor": "$background", - "zIndex": 10, - "marginHorizontal": "35%", - "marginBottom": "$2", - "opacity": 0.5, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", "hoverStyle": { - "opacity": 0.7 + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } } } + }, + "unstyled": { + "false": { + "width": "33%", + "height": "33%", + "borderRadius": 1000, + "backgroundColor": "$color", + "pressTheme": true + } } }, "defaultVariants": { "unstyled": false }, - "componentName": "SheetHandle", + "componentName": "RadioGroupIndicator", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Overlay": { + "RadioGroupItemFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -24897,6 +25231,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", + "tag": "button", "unstyled": false }, "validStyles": { @@ -25239,34 +25574,93 @@ } } }, - "open": { - "true": { - "pointerEvents": "auto" - }, + "unstyled": { "false": { - "pointerEvents": "none" + "size": "$true", + "borderRadius": 1000, + "backgroundColor": "$background", + "alignItems": "center", + "justifyContent": "center", + "borderWidth": 1, + "borderColor": "$borderColor", + "padding": 0, + "hoverStyle": { + "borderColor": "$borderColorHover", + "backgroundColor": "$backgroundHover" + }, + "focusStyle": { + "borderColor": "$borderColorHover", + "backgroundColor": "$backgroundHover" + }, + "focusVisibleStyle": { + "outlineStyle": "solid", + "outlineWidth": 2, + "outlineColor": "$outlineColor" + }, + "pressStyle": { + "borderColor": "$borderColorFocus", + "backgroundColor": "$backgroundFocus" + } } }, - "unstyled": { - "false": { - "fullscreen": true, - "position": "absolute", - "backgrounded": true, - "zIndex": 99999, - "pointerEvents": "auto" + "disabled": { + "true": { + "pointerEvents": "none", + "userSelect": "none", + "cursor": "not-allowed", + "hoverStyle": { + "borderColor": "$borderColor", + "backgroundColor": "$background" + }, + "pressStyle": { + "borderColor": "$borderColor", + "backgroundColor": "$background" + }, + "focusVisibleStyle": { + "outlineWidth": 0 + } } + }, + "size": { + "...size": "Function" } }, "defaultVariants": { "unstyled": false }, - "componentName": "SheetOverlay", + "componentName": "RadioGroupItem", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SheetHandleFrame": { + "ScrollView": { + "staticConfig": { + "accept": { + "contentContainerStyle": "style" + }, + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + } + }, + "defaultProps": { + "scrollEnabled": true + }, + "componentName": "ScrollView", + "isReactNative": true, + "isText": false, + "acceptsClassName": true, + "isStyledHOC": false + } + }, + "Section": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -25278,8 +25672,9 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "row", - "unstyled": false + "tag": "section", + "flexDirection": "column", + "accessibilityRole": "summary" }, "validStyles": { "backfaceVisibility": true, @@ -25516,56 +25911,13 @@ "transformStyle": true, "userSelect": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "open": { - "true": { - "opacity": 1, - "pointerEvents": "auto" - }, - "false": { - "opacity": 0, - "pointerEvents": "none" - } - }, - "unstyled": { - "false": { - "height": 10, - "borderRadius": 100, - "backgroundColor": "$background", - "zIndex": 10, - "marginHorizontal": "35%", - "marginBottom": "$2", - "opacity": 0.5, - "hoverStyle": { - "opacity": 0.7 - } - } - } - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "SheetHandle", + "componentName": "Section", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SheetOverlayFrame": { + "SelectGroupFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -25578,7 +25930,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "unstyled": false + "width": "100%" }, "validStyles": { "backfaceVisibility": true, @@ -25829,125 +26181,15 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "open": { - "true": { - "pointerEvents": "auto" - }, - "false": { - "pointerEvents": "none" - } - }, - "unstyled": { - "false": { - "fullscreen": true, - "position": "absolute", - "backgrounded": true, - "zIndex": 99999, - "pointerEvents": "auto" - } - } - }, - "defaultVariants": { - "unstyled": false + "inset": "Function" }, - "componentName": "SheetOverlay", + "componentName": "SelectGroup", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SliderFrame": { + "SelectIcon": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -25955,11 +26197,13 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", + "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "position": "relative" + "flexDirection": "row", + "aria-hidden": true, + "children": "Component" }, "validStyles": { "backfaceVisibility": true, @@ -26210,32 +26454,35 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function", - "orientation": { - "horizontal": {}, - "vertical": {} - }, - "size": "Function" + "inset": "Function" }, + "componentName": "SelectIcon", "isReactNative": false, "isText": false, - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true } }, - "SliderThumb": { + "SelectSeparator": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, + "borderColor": "$borderColor", "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "borderWidth": 0, + "flex": 1, + "height": 0, + "maxHeight": 0, + "borderBottomWidth": 1, + "y": -0.5 }, "validStyles": { "backfaceVisibility": true, @@ -26473,151 +26720,45 @@ "userSelect": true }, "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { + "vertical": { "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "size": { - "...size": "Function" - }, - "unstyled": { - "false": { - "position": "absolute", - "bordered": 2, - "borderWidth": 2, - "backgrounded": true, - "pressTheme": true, - "focusTheme": true, - "hoverTheme": true + "y": 0, + "x": -0.5, + "height": "initial", + "maxHeight": "initial", + "width": 0, + "maxWidth": 0, + "borderBottomWidth": 0, + "borderRightWidth": 1 } } }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "SliderThumb", + "componentName": "SelectSeparator", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "memo": true, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "SliderThumbFrame": { + "Separator": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", "alignItems": "stretch", + "flexDirection": "column", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, + "borderColor": "$borderColor", "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "borderWidth": 0, + "flex": 1, + "height": 0, + "maxHeight": 0, + "borderBottomWidth": 1, + "y": -0.5 }, "validStyles": { "backfaceVisibility": true, @@ -26855,135 +26996,26 @@ "userSelect": true }, "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { + "vertical": { "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "size": { - "...size": "Function" - }, - "unstyled": { - "false": { - "position": "absolute", - "bordered": 2, - "borderWidth": 2, - "backgrounded": true, - "pressTheme": true, - "focusTheme": true, - "hoverTheme": true + "y": 0, + "x": -0.5, + "height": "initial", + "maxHeight": "initial", + "width": 0, + "maxWidth": 0, + "borderBottomWidth": 0, + "borderRightWidth": 1 } } }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "SliderThumb", + "componentName": "Separator", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SliderTrackActiveFrame": { + "SheetHandleFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -26991,13 +27023,12 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", + "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "backgroundColor": "$background", - "position": "absolute", - "pointerEvents": "box-none" + "flexDirection": "row", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -27249,19 +27280,41 @@ ":number": "Function" }, "inset": "Function", - "orientation": { - "horizontal": {}, - "vertical": {} + "open": { + "true": { + "opacity": 1, + "pointerEvents": "auto" + }, + "false": { + "opacity": 0, + "pointerEvents": "none" + } }, - "size": "Function" + "unstyled": { + "false": { + "height": 10, + "borderRadius": 100, + "backgroundColor": "$background", + "zIndex": 10, + "marginHorizontal": "35%", + "marginBottom": "$2", + "opacity": 0.5, + "hoverStyle": { + "opacity": 0.7 + } + } + } }, - "componentName": "SliderTrackActive", + "defaultVariants": { + "unstyled": false + }, + "componentName": "SheetHandle", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "SliderTrackFrame": { + "SheetOverlayFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -27269,11 +27322,11 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", + "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "position": "relative", "unstyled": false }, "validStyles": { @@ -27526,32 +27579,124 @@ ":number": "Function" }, "inset": "Function", - "orientation": { - "horizontal": {}, - "vertical": {} + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "open": { + "true": { + "pointerEvents": "auto" + }, + "false": { + "pointerEvents": "none" + } }, - "size": "Function", "unstyled": { "false": { - "height": "100%", - "width": "100%", - "backgroundColor": "$background", - "position": "relative", - "borderRadius": 100000, - "overflow": "hidden" + "fullscreen": true, + "position": "absolute", + "backgrounded": true, + "zIndex": 99999, + "pointerEvents": "auto" } } }, "defaultVariants": { "unstyled": false }, - "componentName": "SliderTrack", + "componentName": "SheetOverlay", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Thumb": { + "SizableStack": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -27563,8 +27708,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "flexDirection": "row" }, "validStyles": { "backfaceVisibility": true, @@ -27816,14 +27960,15 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { + "unstyled": { "true": { - "backgroundColor": "$background" + "hoverTheme": false, + "pressTheme": false, + "focusTheme": false, + "elevate": false, + "bordered": false } }, - "radiused": { - "true": "Function" - }, "hoverTheme": { "true": { "hoverStyle": { @@ -27855,97 +28000,80 @@ "circular": { "true": "Function" }, - "padded": { - "true": "Function" - }, "elevate": { "true": "Function" }, "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, "size": { "...size": "Function" - }, - "unstyled": { - "false": { - "position": "absolute", - "bordered": 2, - "borderWidth": 2, - "backgrounded": true, - "pressTheme": true, - "focusTheme": true, - "hoverTheme": true - } } }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "SliderThumb", + "componentName": "SizableStack", "isReactNative": false, "isText": false, - "isStyledHOC": false, - "memo": true, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "XStack": { + "SizableText": { "staticConfig": { "acceptsClassName": true, + "isText": true, "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", + "display": "inline", "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "row" + "wordWrap": "break-word", + "whiteSpace": "pre-wrap", + "margin": 0, + "fontFamily": "$body", + "unstyled": false + }, + "inlineWhenUnflattened": {}, + "variants": { + "numberOfLines": { + "1": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + }, + ":number": "Function" + }, + "selectable": { + "true": { + "userSelect": "text", + "cursor": "text" + }, + "false": { + "userSelect": "none", + "cursor": "default" + } + }, + "ellipse": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "ellipsis": { + "true": { + "maxWidth": "100%", + "overflow": "hidden", + "textOverflow": "ellipsis", + "whiteSpace": "nowrap" + } + }, + "unstyled": { + "false": { + "size": "$true", + "color": "$color" + } + }, + "size": "Function", + "fontFamily": { + "...": "Function" + } }, "validStyles": { "backfaceVisibility": true, @@ -28180,30 +28308,36 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true + "userSelect": true, + "fontFamily": true, + "fontSize": true, + "fontStyle": true, + "fontVariant": true, + "letterSpacing": true, + "lineHeight": true, + "textTransform": true, + "textAlign": true, + "textDecorationLine": true, + "textDecorationStyle": true, + "textShadowOffset": true, + "textShadowRadius": true, + "selectable": true, + "verticalAlign": true, + "whiteSpace": true, + "wordWrap": true, + "textOverflow": true, + "textDecorationDistance": true, + "WebkitBoxOrient": true }, - "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" - }, - "inset": "Function" + "defaultVariants": { + "unstyled": false }, + "componentName": "SizableText", "isReactNative": false, - "isText": false, "isStyledHOC": false } }, - "YStack": { + "SliderFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -28211,11 +28345,11 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", - "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column" + "flexDirection": "column", + "position": "relative" }, "validStyles": { "backfaceVisibility": true, @@ -28466,14 +28600,19 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function" + "inset": "Function", + "orientation": { + "horizontal": {}, + "vertical": {} + }, + "size": "Function" }, "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "ZStack": { + "SliderThumb": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -28481,11 +28620,12 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", + "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "position": "relative" + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -28736,16 +28876,125 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function" + "inset": "Function", + "backgrounded": { + "true": { + "backgroundColor": "$background" + } + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, + "size": { + "...size": "Function" + }, + "unstyled": { + "false": { + "position": "absolute", + "bordered": 2, + "borderWidth": 2, + "backgrounded": true, + "pressTheme": true, + "focusTheme": true, + "hoverTheme": true + } + } + }, + "defaultVariants": { + "unstyled": false }, + "componentName": "SliderThumb", "isReactNative": false, "isText": false, "isStyledHOC": false, + "memo": true, "neverFlatten": true, - "isZStack": true + "isHOC": true } }, - "SizableStack": { + "SliderThumbFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -28757,7 +29006,8 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "row" + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -29009,15 +29259,14 @@ ":number": "Function" }, "inset": "Function", - "unstyled": { + "backgrounded": { "true": { - "hoverTheme": false, - "pressTheme": false, - "focusTheme": false, - "elevate": false, - "bordered": false + "backgroundColor": "$background" } }, + "radiused": { + "true": "Function" + }, "hoverTheme": { "true": { "hoverStyle": { @@ -29049,21 +29298,82 @@ "circular": { "true": "Function" }, + "padded": { + "true": "Function" + }, "elevate": { "true": "Function" }, "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, "size": { "...size": "Function" + }, + "unstyled": { + "false": { + "position": "absolute", + "bordered": 2, + "borderWidth": 2, + "backgrounded": true, + "pressTheme": true, + "focusTheme": true, + "hoverTheme": true + } } }, - "componentName": "SizableStack", + "defaultVariants": { + "unstyled": false + }, + "componentName": "SliderThumb", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "ThemeableStack": { + "SliderTrackActiveFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -29071,11 +29381,13 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", - "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column" + "flexDirection": "column", + "backgroundColor": "$background", + "position": "absolute", + "pointerEvents": "box-none" }, "validStyles": { "backfaceVisibility": true, @@ -29327,103 +29639,19 @@ ":number": "Function" }, "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } + "orientation": { + "horizontal": {}, + "vertical": {} }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - } + "size": "Function" }, + "componentName": "SliderTrackActive", "isReactNative": false, "isText": false, "isStyledHOC": false } }, - "Switch": { + "SliderTrackFrame": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -29431,13 +29659,11 @@ "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", - "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "tag": "button", - "tabIndex": 0, + "position": "relative", "unstyled": false }, "validStyles": { @@ -29690,55 +29916,36 @@ ":number": "Function" }, "inset": "Function", + "orientation": { + "horizontal": {}, + "vertical": {} + }, + "size": "Function", "unstyled": { "false": { - "borderRadius": 1000, + "height": "100%", + "width": "100%", "backgroundColor": "$background", - "borderWidth": 2, - "borderColor": "$background", - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineStyle": "solid", - "outlineWidth": 2 - } + "position": "relative", + "borderRadius": 100000, + "overflow": "hidden" } - }, - "checked": { - "true": {} - }, - "size": { - "...size": "Function" } }, "defaultVariants": { "unstyled": false }, - "componentName": "Switch", + "componentName": "SliderTrack", "isReactNative": false, "isText": false, - "context": "Component", - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "isStyledHOC": false } }, - "SwitchFrame": { + "Spacer": { "staticConfig": { "acceptsClassName": true, - "defaultProps": { - "display": "flex", - "alignItems": "stretch", - "flexBasis": "auto", - "boxSizing": "border-box", - "position": "relative", - "minHeight": 0, - "minWidth": 0, - "flexShrink": 0, - "flexDirection": "column", - "tag": "button", - "tabIndex": 0, - "unstyled": false - }, + "memo": true, + "componentName": "Spacer", "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -29974,52 +30181,44 @@ "transformStyle": true, "userSelect": true }, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexDirection": "column", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "tag": "span", + "size": true, + "pointerEvents": "none" + }, "variants": { - "fullscreen": { - "true": { - "position": "absolute", - "top": 0, - "left": 0, - "right": 0, - "bottom": 0 - } - }, - "elevation": { - "...size": "Function", - ":number": "Function" + "size": { + "...": "Function" }, - "inset": "Function", - "unstyled": { - "false": { - "borderRadius": 1000, - "backgroundColor": "$background", - "borderWidth": 2, - "borderColor": "$background", - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineStyle": "solid", - "outlineWidth": 2 - } + "flex": { + "true": { + "flexGrow": 1 } }, - "checked": { - "true": {} - }, - "size": { - "...size": "Function" + "direction": { + "horizontal": { + "height": 0, + "minHeight": 0 + }, + "vertical": { + "width": 0, + "minWidth": 0 + }, + "both": {} } - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "Switch", - "isReactNative": false, - "isText": false, - "context": "Component", - "isStyledHOC": false + } } }, - "SwitchThumb": { + "Spinner": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -30031,8 +30230,7 @@ "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "column", - "unstyled": false + "flexDirection": "column" }, "validStyles": { "backfaceVisibility": true, @@ -30283,134 +30481,29 @@ "...size": "Function", ":number": "Function" }, - "inset": "Function", - "backgrounded": { - "true": { - "backgroundColor": "$background" - } - }, - "radiused": { - "true": "Function" - }, - "hoverTheme": { - "true": { - "hoverStyle": { - "backgroundColor": "$backgroundHover", - "borderColor": "$borderColorHover" - } - }, - "false": {} - }, - "pressTheme": { - "true": { - "cursor": "pointer", - "pressStyle": { - "backgroundColor": "$backgroundPress", - "borderColor": "$borderColorPress" - } - }, - "false": {} - }, - "focusTheme": { - "true": { - "focusStyle": { - "backgroundColor": "$backgroundFocus", - "borderColor": "$borderColorFocus" - } - }, - "false": {} - }, - "circular": { - "true": "Function" - }, - "padded": { - "true": "Function" - }, - "elevate": { - "true": "Function" - }, - "bordered": "Function", - "transparent": { - "true": { - "backgroundColor": "transparent" - } - }, - "chromeless": { - "true": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "all": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "pressStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - }, - "focusStyle": { - "backgroundColor": "transparent", - "borderColor": "transparent", - "shadowColor": "transparent", - "hoverStyle": { - "borderColor": "transparent" - } - } - } - }, - "unstyled": { - "false": { - "size": "$true", - "backgroundColor": "$background", - "borderRadius": 1000 - } - }, - "checked": { - "true": {} - }, - "size": { - "...size": "Function" - } - }, - "defaultVariants": { - "unstyled": false + "inset": "Function" }, - "componentName": "SwitchThumb", "isReactNative": false, "isText": false, - "context": "Component", - "isStyledHOC": false + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "Tabs": { + "Square": { "staticConfig": { "acceptsClassName": true, "defaultProps": { "display": "flex", - "alignItems": "stretch", "flexBasis": "auto", "boxSizing": "border-box", "position": "relative", "minHeight": 0, "minWidth": 0, "flexShrink": 0, - "flexDirection": "row" + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center" }, "validStyles": { "backfaceVisibility": true, @@ -30662,15 +30755,14 @@ ":number": "Function" }, "inset": "Function", - "unstyled": { + "backgrounded": { "true": { - "hoverTheme": false, - "pressTheme": false, - "focusTheme": false, - "elevate": false, - "bordered": false + "backgroundColor": "$background" } }, + "radiused": { + "true": "Function" + }, "hoverTheme": { "true": { "hoverStyle": { @@ -30702,82 +30794,82 @@ "circular": { "true": "Function" }, + "padded": { + "true": "Function" + }, "elevate": { "true": "Function" }, "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } + }, "size": { - "...size": "Function" + "...size": "Function", + ":number": "Function" } }, - "componentName": "Tabs", + "componentName": "Square", "isReactNative": false, "isText": false, "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true + "memo": true } }, - "SizableText": { + "Stack": { "staticConfig": { "acceptsClassName": true, - "isText": true, "defaultProps": { - "display": "inline", + "display": "flex", + "alignItems": "stretch", + "flexDirection": "column", + "flexBasis": "auto", "boxSizing": "border-box", - "wordWrap": "break-word", - "whiteSpace": "pre-wrap", - "margin": 0, - "fontFamily": "$body", - "unstyled": false - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$true", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0 }, "validStyles": { "backfaceVisibility": true, @@ -31012,99 +31104,26 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "SizableText", - "isReactNative": false, - "isStyledHOC": false + "userSelect": true + } } }, - "Paragraph": { + "Switch": { "staticConfig": { "acceptsClassName": true, - "isText": true, "defaultProps": { - "display": "inline", + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", "boxSizing": "border-box", - "wordWrap": "break-word", - "margin": 0, - "fontFamily": "$body", - "unstyled": false, - "tag": "p", - "userSelect": "auto", - "color": "$color", - "size": "$true", - "whiteSpace": "normal" - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$true", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "tag": "button", + "tabIndex": 0, + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -31339,98 +31358,72 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "componentName": "Paragraph", - "isReactNative": false, - "isStyledHOC": false - } - }, - "H1": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h1", - "unstyled": false + "userSelect": true }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { + "fullscreen": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 } }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } + "elevation": { + "...size": "Function", + ":number": "Function" }, + "inset": "Function", "unstyled": { "false": { - "size": "$10", - "color": "$color" + "borderRadius": 1000, + "backgroundColor": "$background", + "borderWidth": 2, + "borderColor": "$background", + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineStyle": "solid", + "outlineWidth": 2 + } } }, - "size": "Function", - "fontFamily": { - "...": "Function" + "checked": { + "true": {} + }, + "size": { + "...size": "Function" } }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "Switch", + "isReactNative": false, + "isText": false, + "context": "Component", + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "SwitchFrame": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "tag": "button", + "tabIndex": 0, + "unstyled": false + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -31664,101 +31657,68 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true + "userSelect": true + }, + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "unstyled": { + "false": { + "borderRadius": 1000, + "backgroundColor": "$background", + "borderWidth": 2, + "borderColor": "$background", + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineStyle": "solid", + "outlineWidth": 2 + } + } + }, + "checked": { + "true": {} + }, + "size": { + "...size": "Function" + } }, "defaultVariants": { "unstyled": false }, - "componentName": "H1", + "componentName": "Switch", "isReactNative": false, + "isText": false, + "context": "Component", "isStyledHOC": false } }, - "H2": { + "SwitchThumb": { "staticConfig": { "acceptsClassName": true, - "isText": true, "defaultProps": { - "display": "inline", + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h2", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", "unstyled": false }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$9", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } - }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -31992,101 +31952,151 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "H2", - "isReactNative": false, - "isStyledHOC": false - } - }, - "H3": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h3", - "unstyled": false + "userSelect": true }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", ":number": "Function" }, - "selectable": { + "inset": "Function", + "backgrounded": { "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" + "backgroundColor": "$background" } }, - "ellipse": { + "radiused": { + "true": "Function" + }, + "hoverTheme": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" } }, - "ellipsis": { + "chromeless": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } } }, "unstyled": { "false": { - "size": "$8", - "color": "$color" + "size": "$true", + "backgroundColor": "$background", + "borderRadius": 1000 } }, - "size": "Function", - "fontFamily": { - "...": "Function" + "checked": { + "true": {} + }, + "size": { + "...size": "Function" } }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "SwitchThumb", + "isReactNative": false, + "isText": false, + "context": "Component", + "isStyledHOC": false + } + }, + "Tabs": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "row" + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -32320,51 +32330,90 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true + "userSelect": true }, - "defaultVariants": { - "unstyled": false + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function", + "unstyled": { + "true": { + "hoverTheme": false, + "pressTheme": false, + "focusTheme": false, + "elevate": false, + "bordered": false + } + }, + "hoverTheme": { + "true": { + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "size": { + "...size": "Function" + } }, - "componentName": "H3", + "componentName": "Tabs", "isReactNative": false, - "isStyledHOC": false + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "H4": { + "Text": { "staticConfig": { "acceptsClassName": true, "isText": true, "defaultProps": { + "fontFamily": "unset", "display": "inline", "boxSizing": "border-box", "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", + "whiteSpace": "pre-wrap", "margin": 0, - "tag": "h4", "unstyled": false }, "inlineWhenUnflattened": {}, @@ -32406,13 +32455,8 @@ }, "unstyled": { "false": { - "size": "$7", "color": "$color" } - }, - "size": "Function", - "fontFamily": { - "...": "Function" } }, "validStyles": { @@ -32672,77 +32716,136 @@ "defaultVariants": { "unstyled": false }, - "componentName": "H4", "isReactNative": false, "isStyledHOC": false } }, - "H5": { + "TextArea": { "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h5", - "unstyled": false + "isInput": true, + "accept": { + "placeholderTextColor": "color", + "selectionColor": "color" }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, + "unstyled": { "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "size": "$true", + "fontFamily": "$body", + "borderWidth": 1, + "outlineWidth": 0, + "color": "$color", + "tabIndex": 0, + "borderColor": "$borderColor", + "backgroundColor": "$background", + "minWidth": 0, + "hoverStyle": { + "borderColor": "$borderColorHover" + }, + "focusStyle": { + "borderColor": "$borderColorFocus" + }, + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineWidth": 2, + "outlineStyle": "solid" + }, + "height": "auto" } }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } + "size": { + "...size": "Function" }, + "disabled": { + "true": {} + } + }, + "defaultProps": { + "multiline": true, + "whiteSpace": "pre-wrap", + "unstyled": false + }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "TextArea", + "isReactNative": true, + "isText": true, + "acceptsClassName": true, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "TextAreaFrame": { + "staticConfig": { + "isInput": true, + "accept": { + "placeholderTextColor": "color", + "selectionColor": "color" + }, + "variants": { "unstyled": { "false": { - "size": "$6", - "color": "$color" + "size": "$true", + "fontFamily": "$body", + "borderWidth": 1, + "outlineWidth": 0, + "color": "$color", + "tabIndex": 0, + "borderColor": "$borderColor", + "backgroundColor": "$background", + "minWidth": 0, + "hoverStyle": { + "borderColor": "$borderColorHover" + }, + "focusStyle": { + "borderColor": "$borderColorFocus" + }, + "focusVisibleStyle": { + "outlineColor": "$outlineColor", + "outlineWidth": 2, + "outlineStyle": "solid" + }, + "height": "auto" } }, - "size": "Function", - "fontFamily": { - "...": "Function" + "size": { + "...size": "Function" + }, + "disabled": { + "true": {} } }, + "defaultProps": { + "multiline": true, + "whiteSpace": "pre-wrap", + "unstyled": false + }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "TextArea", + "isReactNative": true, + "isText": true, + "acceptsClassName": true, + "isStyledHOC": false + } + }, + "ThemeableStack": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column" + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -32976,101 +33079,134 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "H5", - "isReactNative": false, - "isStyledHOC": false - } - }, - "H6": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0, - "tag": "h6", - "unstyled": false + "userSelect": true }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", ":number": "Function" }, - "selectable": { + "inset": "Function", + "backgrounded": { "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" + "backgroundColor": "$background" } }, - "ellipse": { + "radiused": { + "true": "Function" + }, + "hoverTheme": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} }, - "ellipsis": { + "pressTheme": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} }, - "unstyled": { - "false": { - "size": "$5", - "color": "$color" + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" } }, - "size": "Function", - "fontFamily": { - "...": "Function" + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } + } } }, + "isReactNative": false, + "isText": false, + "isStyledHOC": false + } + }, + "Thumb": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "unstyled": false + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -33304,101 +33440,154 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "H6", - "isReactNative": false, - "isStyledHOC": false - } - }, - "Heading": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "display": "inline", - "boxSizing": "border-box", - "wordWrap": "break-word", - "unstyled": false, - "userSelect": "auto", - "color": "$color", - "whiteSpace": "normal", - "tag": "span", - "accessibilityRole": "header", - "fontFamily": "$heading", - "size": "$8", - "margin": 0 + "userSelect": true }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", ":number": "Function" }, - "selectable": { + "inset": "Function", + "backgrounded": { "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" + "backgroundColor": "$background" } }, - "ellipse": { + "radiused": { + "true": "Function" + }, + "hoverTheme": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" } }, - "ellipsis": { + "chromeless": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } } }, + "size": { + "...size": "Function" + }, "unstyled": { "false": { - "size": "$true", - "color": "$color" + "position": "absolute", + "bordered": 2, + "borderWidth": 2, + "backgrounded": true, + "pressTheme": true, + "focusTheme": true, + "hoverTheme": true } - }, - "size": "Function", - "fontFamily": { - "...": "Function" } }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "SliderThumb", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "memo": true, + "neverFlatten": true, + "isHOC": true + } + }, + "View": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexDirection": "column", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0 + }, "validStyles": { "backfaceVisibility": true, "borderBottomEndRadius": true, @@ -33632,30 +33821,8 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "componentName": "Heading", - "isReactNative": false, - "isStyledHOC": false + "userSelect": true + } } }, "VisuallyHidden": { @@ -33992,68 +34159,20 @@ "isStyledHOC": false } }, - "Anchor": { + "XGroup": { "staticConfig": { "acceptsClassName": true, - "isText": true, "defaultProps": { - "display": "inline", + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", "boxSizing": "border-box", - "wordWrap": "break-word", - "whiteSpace": "pre-wrap", - "margin": 0, - "fontFamily": "$body", - "unstyled": false, - "tag": "a", - "accessibilityRole": "link" - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "size": "$true", - "color": "$color" - } - }, - "size": "Function", - "fontFamily": { - "...": "Function" - } + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -34288,89 +34407,144 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true - }, - "componentName": "Anchor", - "isReactNative": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true - } - }, - "EnsureFlexed": { - "staticConfig": { - "acceptsClassName": true, - "isText": true, - "defaultProps": { - "fontFamily": "unset", - "boxSizing": "border-box", - "wordWrap": "break-word", - "whiteSpace": "pre-wrap", - "margin": 0, - "opacity": 0, - "lineHeight": 0, - "height": 0, - "display": "flex", - "fontSize": 200, - "children": "wwwwwwwwwwwwwwwwwww", - "pointerEvents": "none" + "userSelect": true }, - "inlineWhenUnflattened": {}, "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", ":number": "Function" }, - "selectable": { + "inset": "Function", + "backgrounded": { "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" + "backgroundColor": "$background" } }, - "ellipse": { + "radiused": { + "true": "Function" + }, + "hoverTheme": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "hoverStyle": { + "backgroundColor": "$backgroundHover", + "borderColor": "$borderColorHover" + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { + "focusStyle": { + "backgroundColor": "$backgroundFocus", + "borderColor": "$borderColorFocus" + } + }, + "false": {} + }, + "circular": { + "true": "Function" + }, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" } }, - "ellipsis": { + "chromeless": { "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + }, + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } + } } - } + }, + "unstyled": { + "false": { + "size": "$true" + } + }, + "size": "Function" + }, + "defaultVariants": { + "unstyled": false + }, + "componentName": "GroupFrame", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true + } + }, + "XStack": { + "staticConfig": { + "acceptsClassName": true, + "defaultProps": { + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", + "boxSizing": "border-box", + "position": "relative", + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "row" }, "validStyles": { "backfaceVisibility": true, @@ -34605,33 +34779,30 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true + "userSelect": true + }, + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function" }, "isReactNative": false, - "isStyledHOC": false, - "neverFlatten": true + "isText": false, + "isStyledHOC": false } }, - "Fieldset": { + "YGroup": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -34644,8 +34815,7 @@ "minWidth": 0, "flexShrink": 0, "flexDirection": "column", - "tag": "fieldset", - "borderWidth": 0 + "unstyled": false }, "validStyles": { "backfaceVisibility": true, @@ -34897,126 +35067,115 @@ ":number": "Function" }, "inset": "Function", - "horizontal": { + "backgrounded": { "true": { - "flexDirection": "row", - "alignItems": "center" + "backgroundColor": "$background" } - } - }, - "componentName": "Fieldset", - "isReactNative": false, - "isText": false, - "isStyledHOC": false - } - }, - "Input": { - "staticConfig": { - "isInput": true, - "accept": { - "placeholderTextColor": "color", - "selectionColor": "color" - }, - "variants": { - "unstyled": { - "false": { - "size": "$true", - "fontFamily": "$body", - "borderWidth": 1, - "outlineWidth": 0, - "color": "$color", - "tabIndex": 0, - "borderColor": "$borderColor", - "backgroundColor": "$background", - "minWidth": 0, + }, + "radiused": { + "true": "Function" + }, + "hoverTheme": { + "true": { "hoverStyle": { + "backgroundColor": "$backgroundHover", "borderColor": "$borderColorHover" - }, + } + }, + "false": {} + }, + "pressTheme": { + "true": { + "cursor": "pointer", + "pressStyle": { + "backgroundColor": "$backgroundPress", + "borderColor": "$borderColorPress" + } + }, + "false": {} + }, + "focusTheme": { + "true": { "focusStyle": { + "backgroundColor": "$backgroundFocus", "borderColor": "$borderColorFocus" - }, - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineWidth": 2, - "outlineStyle": "solid" } - } + }, + "false": {} }, - "size": { - "...size": "Function" + "circular": { + "true": "Function" }, - "disabled": { - "true": {} - } - }, - "defaultProps": { - "unstyled": false - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "Input", - "isReactNative": true, - "isText": true, - "acceptsClassName": true, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true - } - }, - "InputFrame": { - "staticConfig": { - "isInput": true, - "accept": { - "placeholderTextColor": "color", - "selectionColor": "color" - }, - "variants": { - "unstyled": { - "false": { - "size": "$true", - "fontFamily": "$body", - "borderWidth": 1, - "outlineWidth": 0, - "color": "$color", - "tabIndex": 0, - "borderColor": "$borderColor", - "backgroundColor": "$background", - "minWidth": 0, + "padded": { + "true": "Function" + }, + "elevate": { + "true": "Function" + }, + "bordered": "Function", + "transparent": { + "true": { + "backgroundColor": "transparent" + } + }, + "chromeless": { + "true": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", "hoverStyle": { - "borderColor": "$borderColorHover" + "borderColor": "transparent" + } + }, + "all": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } }, - "focusStyle": { - "borderColor": "$borderColorFocus" + "pressStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } }, - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineWidth": 2, - "outlineStyle": "solid" + "focusStyle": { + "backgroundColor": "transparent", + "borderColor": "transparent", + "shadowColor": "transparent", + "hoverStyle": { + "borderColor": "transparent" + } } } }, - "size": { - "...size": "Function" + "unstyled": { + "false": { + "size": "$true" + } }, - "disabled": { - "true": {} - } - }, - "defaultProps": { - "unstyled": false + "size": "Function" }, "defaultVariants": { "unstyled": false }, - "componentName": "Input", - "isReactNative": true, - "isText": true, - "acceptsClassName": true, - "isStyledHOC": false + "componentName": "GroupFrame", + "isReactNative": false, + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isHOC": true } }, - "Spinner": { + "YStack": { "staticConfig": { "acceptsClassName": true, "defaultProps": { @@ -35283,178 +35442,22 @@ }, "isReactNative": false, "isText": false, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true - } - }, - "TextArea": { - "staticConfig": { - "isInput": true, - "accept": { - "placeholderTextColor": "color", - "selectionColor": "color" - }, - "variants": { - "unstyled": { - "false": { - "size": "$true", - "fontFamily": "$body", - "borderWidth": 1, - "outlineWidth": 0, - "color": "$color", - "tabIndex": 0, - "borderColor": "$borderColor", - "backgroundColor": "$background", - "minWidth": 0, - "hoverStyle": { - "borderColor": "$borderColorHover" - }, - "focusStyle": { - "borderColor": "$borderColorFocus" - }, - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineWidth": 2, - "outlineStyle": "solid" - }, - "height": "auto" - } - }, - "size": { - "...size": "Function" - }, - "disabled": { - "true": {} - } - }, - "defaultProps": { - "multiline": true, - "whiteSpace": "pre-wrap", - "unstyled": false - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "TextArea", - "isReactNative": true, - "isText": true, - "acceptsClassName": true, - "isStyledHOC": false, - "neverFlatten": true, - "isHOC": true - } - }, - "TextAreaFrame": { - "staticConfig": { - "isInput": true, - "accept": { - "placeholderTextColor": "color", - "selectionColor": "color" - }, - "variants": { - "unstyled": { - "false": { - "size": "$true", - "fontFamily": "$body", - "borderWidth": 1, - "outlineWidth": 0, - "color": "$color", - "tabIndex": 0, - "borderColor": "$borderColor", - "backgroundColor": "$background", - "minWidth": 0, - "hoverStyle": { - "borderColor": "$borderColorHover" - }, - "focusStyle": { - "borderColor": "$borderColorFocus" - }, - "focusVisibleStyle": { - "outlineColor": "$outlineColor", - "outlineWidth": 2, - "outlineStyle": "solid" - }, - "height": "auto" - } - }, - "size": { - "...size": "Function" - }, - "disabled": { - "true": {} - } - }, - "defaultProps": { - "multiline": true, - "whiteSpace": "pre-wrap", - "unstyled": false - }, - "defaultVariants": { - "unstyled": false - }, - "componentName": "TextArea", - "isReactNative": true, - "isText": true, - "acceptsClassName": true, "isStyledHOC": false } }, - "Text": { + "ZStack": { "staticConfig": { "acceptsClassName": true, - "isText": true, "defaultProps": { - "fontFamily": "unset", - "display": "inline", + "display": "flex", + "alignItems": "stretch", + "flexBasis": "auto", "boxSizing": "border-box", - "wordWrap": "break-word", - "whiteSpace": "pre-wrap", - "margin": 0, - "unstyled": false - }, - "inlineWhenUnflattened": {}, - "variants": { - "numberOfLines": { - "1": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - }, - ":number": "Function" - }, - "selectable": { - "true": { - "userSelect": "text", - "cursor": "text" - }, - "false": { - "userSelect": "none", - "cursor": "default" - } - }, - "ellipse": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "ellipsis": { - "true": { - "maxWidth": "100%", - "overflow": "hidden", - "textOverflow": "ellipsis", - "whiteSpace": "nowrap" - } - }, - "unstyled": { - "false": { - "color": "$color" - } - } + "minHeight": 0, + "minWidth": 0, + "flexShrink": 0, + "flexDirection": "column", + "position": "relative" }, "validStyles": { "backfaceVisibility": true, @@ -35689,32 +35692,29 @@ "textEmphasis": true, "touchAction": true, "transformStyle": true, - "userSelect": true, - "fontFamily": true, - "fontSize": true, - "fontStyle": true, - "fontVariant": true, - "letterSpacing": true, - "lineHeight": true, - "textTransform": true, - "textAlign": true, - "textDecorationLine": true, - "textDecorationStyle": true, - "textShadowOffset": true, - "textShadowRadius": true, - "selectable": true, - "verticalAlign": true, - "whiteSpace": true, - "wordWrap": true, - "textOverflow": true, - "textDecorationDistance": true, - "WebkitBoxOrient": true + "userSelect": true }, - "defaultVariants": { - "unstyled": false + "variants": { + "fullscreen": { + "true": { + "position": "absolute", + "top": 0, + "left": 0, + "right": 0, + "bottom": 0 + } + }, + "elevation": { + "...size": "Function", + ":number": "Function" + }, + "inset": "Function" }, "isReactNative": false, - "isStyledHOC": false + "isText": false, + "isStyledHOC": false, + "neverFlatten": true, + "isZStack": true } } } diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 9228686..7a1bbb4 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,16 +1,31 @@ /* eslint-disable @typescript-eslint/no-require-imports */ const path = require('path') +const { execSync } = require('child_process') const { withTamagui } = require('@tamagui/next-plugin') const isDesktopExport = process.env.NEXT_OUTPUT === 'export' const tamaguiConfigPath = path.resolve(__dirname, '../../tamagui.config.ts') +// Git tag if one exists, otherwise the short commit hash. +const getHubVersion = () => { + try { + return execSync('git describe --tags --always', { cwd: __dirname }) + .toString() + .trim() + } catch { + return 'unknown' + } +} + module.exports = withTamagui({ config: tamaguiConfigPath, components: ['tamagui'], appDir: true, })({ reactStrictMode: true, + env: { + NEXT_PUBLIC_HUB_VERSION: getHubVersion(), + }, transpilePackages: [ 'tamagui', '@tamagui', diff --git a/apps/web/package.json b/apps/web/package.json index 4d6105f..d0f8a01 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,7 @@ "react-native": "0.79.5", "react-native-svg": "^15.12.1", "react-native-web": "^0.21.0", + "react-paginate": "^8.3.0", "react-qr-code": "^2.0.18", "react-redux": "^9.2.0", "react-toastify": "^11.0.5", @@ -59,6 +60,7 @@ "@types/numeral": "^2.0.5", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-paginate": "^7.1.4", "eslint": "^9", "eslint-config-next": "15.4.6", "typescript": "^5", diff --git a/apps/web/src/app/account/[address]/page.tsx b/apps/web/src/app/account/[address]/page.tsx new file mode 100644 index 0000000..cba1d8c --- /dev/null +++ b/apps/web/src/app/account/[address]/page.tsx @@ -0,0 +1,54 @@ +// apps/web/src/app/account/[address]/page.tsx +'use client' +import { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; + +import { useDispatch } from '@/redux/hooks'; +import { setCurrentPath, setViewTitle } from '@/redux/app.slice'; +import useAccount from '@/hooks/useAccount'; +import useTransaction from '@/hooks/useTransaction'; +import { AccountScreen } from '@lumera-hub/ui/src/screens/AccountScreen'; + +export default function Page() { + const dispatch = useDispatch(); + + useEffect(() => { + document.title = 'Account - Lumera Hub'; + dispatch(setCurrentPath({ + currentPath: '/account', + })); + dispatch(setViewTitle({ + viewTitle: 'Account', + })); + }, []); + + const { addressFormats, isValidAddress, accountInfo, isLoading, error } = useAccount(); + const sentTransactions = useTransaction({ + address: addressFormats?.bech32Address ?? '', + direction: 'sent', + }); + const receivedTransactions = useTransaction({ + address: addressFormats?.bech32Address ?? '', + direction: 'received', + }); + + return ( + <> + + Account - Lumera Hub + +
+ +
+ + ) +} diff --git a/apps/web/src/app/account/[validator]/page.tsx b/apps/web/src/app/account/[validator]/page.tsx deleted file mode 100644 index aa9ed07..0000000 --- a/apps/web/src/app/account/[validator]/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// apps/web/src/app/inference/page.tsx -'use client' -import { useEffect } from "react"; -import { Helmet } from "react-helmet-async"; - -import { AccountScreen } from '@lumera-hub/ui/src/screens/AccountScreen'; - -export default function Page() { - useEffect(() => { - document.title = 'Account - Lumera Hub'; - }, []); - - return ( - <> - - Account - Lumera Hub - -
- -
- - ) -} diff --git a/apps/web/src/app/providers/evm-wallet-provider.tsx b/apps/web/src/app/providers/evm-wallet-provider.tsx index 128e207..53eefd6 100644 --- a/apps/web/src/app/providers/evm-wallet-provider.tsx +++ b/apps/web/src/app/providers/evm-wallet-provider.tsx @@ -5,16 +5,19 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS import { ACTIVE_NETWORK, EVM_CHAIN_ID, + EVM_PROFILE_NAME, EVM_RPC_ENDPOINT, IS_EVM_NETWORK, } from '@/contants/network'; -import { getEvmAccountForChain, getMetaMaskProvider, toHexChainId } from '@/utils/evm'; +import { + assertEvmProviderMatchesRpc, + ensureEvmWalletNetwork, + getEvmAccountForChain, + getEvmConnectionErrorMessage, + getMetaMaskProvider, +} from '@/utils/evm'; import type { Eip1193Provider } from '@/types/window'; -interface EvmProviderError extends Error { - code?: number; -} - interface Eip6963ProviderDetail { provider?: Eip1193Provider; } @@ -62,52 +65,27 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { }; }, []); - const ensureNetwork = useCallback(async () => { - if (!IS_EVM_NETWORK || !EVM_CHAIN_ID || !EVM_RPC_ENDPOINT) { + const ensureConfiguredNetwork = useCallback(async (suggestProfileOnMismatch: boolean) => { + if (!IS_EVM_NETWORK || !EVM_CHAIN_ID || !EVM_PROFILE_NAME || !EVM_RPC_ENDPOINT) { throw new Error('The active network does not support EVM wallets.'); } if (!provider) { throw new Error('MetaMask was not detected. Install or enable the MetaMask extension.'); } - const chainId = toHexChainId(EVM_CHAIN_ID); - const currentChainId = await provider.request({ method: 'eth_chainId' }); - if (currentChainId.toLowerCase() === chainId.toLowerCase()) return; - - try { - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId }], - }); - } catch (switchError) { - const typedError = switchError as EvmProviderError; - if (typedError.code !== 4902) throw switchError; - - await provider.request({ - method: 'wallet_addEthereumChain', - params: [{ - chainId, - chainName: ACTIVE_NETWORK.displayName, - nativeCurrency: { - name: 'Lumera', - symbol: 'LUME', - decimals: 18, - }, - rpcUrls: [EVM_RPC_ENDPOINT], - }], - }); - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId }], - }); - } - - const activeChainId = await provider.request({ method: 'eth_chainId' }); - if (activeChainId.toLowerCase() !== chainId.toLowerCase()) { - throw new Error(`Wallet did not switch to ${ACTIVE_NETWORK.displayName}.`); - } + await ensureEvmWalletNetwork(provider, { + chainId: EVM_CHAIN_ID, + chainName: EVM_PROFILE_NAME, + rpcEndpoint: EVM_RPC_ENDPOINT, + suggestProfileOnMismatch, + }); }, [provider]); + const ensureNetwork = useCallback( + () => ensureConfiguredNetwork(false), + [ensureConfiguredNetwork], + ); + const connect = useCallback(async () => { setError(''); setConnecting(true); @@ -116,19 +94,22 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { throw new Error('MetaMask was not detected. Install or enable the MetaMask extension.'); } await provider.request({ method: 'eth_requestAccounts' }); - await ensureNetwork(); + await ensureConfiguredNetwork(true); if (!EVM_CHAIN_ID) { throw new Error('The active network does not define an EVM chain ID.'); } setAddress(await getEvmAccountForChain(provider, EVM_CHAIN_ID)); } catch (connectError) { - const message = connectError instanceof Error ? connectError.message : 'Unable to connect EVM wallet.'; + const message = getEvmConnectionErrorMessage( + connectError, + ACTIVE_NETWORK.displayName, + ); setError(message); throw new Error(message); } finally { setConnecting(false); } - }, [ensureNetwork, provider]); + }, [ensureConfiguredNetwork, provider]); const disconnect = useCallback(async () => { try { @@ -149,9 +130,13 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { const syncAccounts = async () => { try { - setAddress(await getEvmAccountForChain(provider, expectedChainId)); - } catch { + const activeAddress = await getEvmAccountForChain(provider, expectedChainId); + await assertEvmProviderMatchesRpc(provider, { rpcEndpoint: EVM_RPC_ENDPOINT || undefined }); + setAddress(activeAddress); + setError(''); + } catch (syncError) { setAddress(''); + setError(syncError instanceof Error ? syncError.message : 'Unable to verify the MetaMask network.'); } }; diff --git a/apps/web/src/app/providers/wallet-provider.tsx b/apps/web/src/app/providers/wallet-provider.tsx index abd774a..2240389 100644 --- a/apps/web/src/app/providers/wallet-provider.tsx +++ b/apps/web/src/app/providers/wallet-provider.tsx @@ -4,7 +4,8 @@ import React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; -import { ChainProvider } from '@interchain-kit/react'; +import { ChainProvider, useWalletManager } from '@interchain-kit/react'; +import { WalletState } from '@interchain-kit/core'; import { keplrWallet } from '@interchain-kit/keplr-extension'; import { leapWallet } from '@interchain-kit/leap-extension'; import { cosmostationWallet } from '@interchain-kit/cosmostation-extension'; @@ -13,6 +14,7 @@ import '@interchain-ui/react/styles'; import { CHAIN_NAME, + IS_EVM_NETWORK, WALLET_CONNECT_PROJECTID, WALLET_CONNECT_RELAY_URL, WALLET_CONNECT_NAME, @@ -22,17 +24,70 @@ import { } from '@/contants/network'; import { getChains } from '@/utils/helpers'; import { getWalletConnectWallet } from '@/utils/wallet-connect'; +import { + KEPLR_WALLET_NAME, + METAMASK_WALLET_NAME, + suppressPersistedKeplrConnection, +} from '@/utils/wallet-selection'; +import { useSelector } from '@/redux/hooks'; import { RegistryProvider } from "./RegistryContext"; import { EvmWalletProvider } from './evm-wallet-provider'; import store, { persistor } from '@/store'; -export function WebWalletProviders({ children }: { children: React.ReactNode }) { +function InterchainWalletModeSynchronizer() { + const walletName = useSelector((state) => state.wallet.walletName); + const { + currentWalletName, + getChainWalletState, + setCurrentChainName, + setCurrentWalletName, + updateChainWalletState, + } = useWalletManager(); + const keplrState = getChainWalletState(KEPLR_WALLET_NAME, CHAIN_NAME); + + React.useLayoutEffect(() => { + if (!IS_EVM_NETWORK || walletName !== METAMASK_WALLET_NAME) return; + + if ( + keplrState + && (keplrState.walletState !== WalletState.Disconnected || keplrState.account) + ) { + updateChainWalletState(KEPLR_WALLET_NAME, CHAIN_NAME, { + walletState: WalletState.Disconnected, + account: undefined, + errorMessage: '', + }); + } + if (currentWalletName === KEPLR_WALLET_NAME) { + setCurrentWalletName(''); + setCurrentChainName(''); + } + }, [ + currentWalletName, + keplrState, + setCurrentChainName, + setCurrentWalletName, + updateChainWalletState, + walletName, + ]); + + return null; +} + +function WalletRuntimeProviders({ children }: { children: React.ReactNode }) { + const walletName = useSelector((state) => state.wallet.walletName); const { chains, assetLists } = getChains(); const isBrowser = typeof window !== 'undefined'; // Resolve chain & assets only in the browser to avoid throwing during Next.js prerender/export // Use loose typing to avoid importing chain-registry types; runtime values come from the registry data. // eslint-disable-next-line @typescript-eslint/no-explicit-any const [chainData, setChainData] = React.useState<{ chain: any; assets: any } | null>(null); + React.useEffect(() => { + if (isBrowser && IS_EVM_NETWORK && walletName === METAMASK_WALLET_NAME) { + suppressPersistedKeplrConnection(window.localStorage); + } + }, [isBrowser, walletName]); + React.useEffect(() => { if (!isBrowser || chainData) return; const foundChain = chains.find(({ chainName }) => chainName === CHAIN_NAME); @@ -47,7 +102,7 @@ export function WebWalletProviders({ children }: { children: React.ReactNode }) return; } setChainData({ chain: foundChain, assets: foundAssets }); - }, [isBrowser, chains, assetLists]); + }, [isBrowser, chains, assetLists, chainData]); // Setup WalletConnect with custom metadata const walletConnect = getWalletConnectWallet({ projectId: WALLET_CONNECT_PROJECTID, @@ -66,29 +121,31 @@ export function WebWalletProviders({ children }: { children: React.ReactNode }) [walletConnect], ); + return ( + + + + + {isBrowser && chainData ? ( + + + {children} + + + ) : null} + + + + + ) +} + +export function WebWalletProviders({ children }: { children: React.ReactNode }) { return ( - - - - - {isBrowser && chainData ? ( - - {children} - - - ) : ( - // During SSR or while resolving on client, render app shell without ChainProvider to avoid build-time throws - <> - {children} - - )} - - - - + {children} - ) + ); } diff --git a/apps/web/src/app/wallet/page.tsx b/apps/web/src/app/wallet/page.tsx index 872a9c2..f63f4fd 100644 --- a/apps/web/src/app/wallet/page.tsx +++ b/apps/web/src/app/wallet/page.tsx @@ -1,6 +1,6 @@ // apps/web/src/app/wallet/page.tsx 'use client' -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { WalletScreen } from '@lumera-hub/ui/src/screens/WalletScreen' @@ -9,8 +9,12 @@ import useWalletConnect from '@/hooks/useWalletConnect'; import useTransaction from '@/hooks/useTransaction'; import useDelegate from '@/hooks/useDelegate'; import useSend from '@/hooks/useSend'; +import { hasEthereumTransactionHash } from '@/utils/transaction-history'; + +const EVM_HISTORY_REFRESH_DELAYS = [0, 2_000, 5_000, 10_000, 20_000]; export default function Page() { + const [submittedEvmTransactionHash, setSubmittedEvmTransactionHash] = useState(''); const { address, bech32Address, ethAddress, isEvm } = useWalletConnect(); const account = useAccountInfo(); const { @@ -25,9 +29,17 @@ export default function Page() { transactions, totalTransactions, handlePageClick, + refreshTransactions, } = useTransaction(); const sendOptions = useSend({ - callback: isEvm ? account.fetchData : handleCloseModal, + callback: (transactionHash) => { + if (!isEvm) { + handleCloseModal(); + return; + } + void account.fetchData(); + setSubmittedEvmTransactionHash(transactionHash || ''); + }, customMemo: '', }); const delegate = useDelegate(); @@ -35,6 +47,40 @@ export default function Page() { document.title = 'Wallet - Lumera Hub'; }, []); + useEffect(() => { + if (!submittedEvmTransactionHash) { + return; + } + + let cancelled = false; + let timeout: ReturnType | undefined; + + const refresh = async (attempt: number) => { + const refreshedTransactions = await refreshTransactions(); + if (cancelled) return; + + if (hasEthereumTransactionHash(refreshedTransactions, submittedEvmTransactionHash)) { + setSubmittedEvmTransactionHash(''); + return; + } + + const nextAttempt = attempt + 1; + if (nextAttempt < EVM_HISTORY_REFRESH_DELAYS.length) { + timeout = setTimeout( + () => void refresh(nextAttempt), + EVM_HISTORY_REFRESH_DELAYS[nextAttempt], + ); + } + }; + + timeout = setTimeout(() => void refresh(0), EVM_HISTORY_REFRESH_DELAYS[0]); + + return () => { + cancelled = true; + if (timeout) clearTimeout(timeout); + }; + }, [refreshTransactions, submittedEvmTransactionHash]); + return ( <> diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index 90ee2d7..3cc0e1b 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -4,7 +4,11 @@ import { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import { createPortal } from 'react-dom'; import { Wallet } from '@tamagui/lucide-icons'; -import { InterchainWalletModal, useChain, useChainWallet } from '@interchain-kit/react'; +import { + InterchainWalletModal, + useChain, + useChainWallet, +} from '@interchain-kit/react'; import { ChevronDown, Copy, LogOut, RefreshCw } from 'lucide-react'; import { toast } from 'react-toastify'; diff --git a/apps/web/src/components/SearchDialog.tsx b/apps/web/src/components/SearchDialog.tsx new file mode 100644 index 0000000..275185a --- /dev/null +++ b/apps/web/src/components/SearchDialog.tsx @@ -0,0 +1,133 @@ +'use client' + +import { useState } from 'react'; +import { + H3, + Button, + Dialog, + Input, + VisuallyHidden, +} from 'tamagui'; +import { CircleX, Search } from '@tamagui/lucide-icons'; + +import useAppRouter from '@/hooks/useAppRouter'; +import { parseSearchQuery } from '@/utils/search'; + +export default function SearchDialog() { + const { redirect } = useAppRouter(); + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(''); + const [error, setError] = useState(null); + + const closeDialog = () => { + setIsOpen(false); + setQuery(''); + setError(null); + }; + + const onConfirm = () => { + const url = parseSearchQuery(query); + if (!url) { + setError('Enter a valid height, transaction hash, or account address.'); + return; + } + closeDialog(); + redirect(url); + }; + + return ( + <> + + + { + if (!open) closeDialog(); + }} + modal + > + + + + + + + + + Search + +
+
+
+

Search

+ + Height/Transaction/Account Address + +
+ +
+
+
+ { + setQuery(newValue); + setError(null); + }} + onKeyPress={(event) => { + if (event.nativeEvent.key === 'Enter') { + event.preventDefault(); + onConfirm(); + } + }} + /> +
+
+ {error ? +
{error}
: null + } +
+ +
+
+
+
+
+ + ) +} diff --git a/apps/web/src/components/TransactionHistory.tsx b/apps/web/src/components/TransactionHistory.tsx new file mode 100644 index 0000000..5df5478 --- /dev/null +++ b/apps/web/src/components/TransactionHistory.tsx @@ -0,0 +1,162 @@ +import { + XCircle, + ArrowUpRight, + ArrowLeftRight, + ArrowDownLeft, + Layers, + ClockPlus, + Unlink, + Star, +} from 'lucide-react'; +import { H3 } from 'tamagui'; +import ReactPaginate from 'react-paginate'; +import dayjs from 'dayjs'; + +import AppLink from '@/components/AppLink'; +import Loading from '@/components/Loading'; +import PastTime from '@/components/PastTime'; +import { ITransaction } from '@/hooks/useTransaction'; +import { getMessages } from '@/utils/helpers'; +import { isTransactionSuccessful } from '@/utils/transaction-history'; + +import 'react-paginate/theme/basic/react-paginate.css'; + +interface ITransactionHistory { + transactions: ITransaction[]; + totalTransactions: number; + isLoading: boolean; + handlePageClick: ({ selected }: { selected: number }) => void; +} + +const getTxIcon = (type: string) => { + switch(type) { + case 'Send': + return ; + case 'Received': + return ; + case 'BeginRedelegate': + return ; + case 'Delegate': + return ; + case 'Failed': + return ; + case 'Undelegate': + return ; + default: + if (type.indexOf('WithdrawDelegatorReward') !== -1) { + return ; + } + return ; + } +}; + +const getColor = (type: string) => { + switch(type) { + case 'Send': + case 'Failed': + return 'bg-red-500/20'; + case 'Delegate': + case 'BeginRedelegate': + return 'bg-green-400/20'; + case 'Received': + return 'bg-green-500/20'; + default: + if (type.indexOf('WithdrawDelegatorReward') !== -1) { + return 'recent-activity-icon'; + } + return 'bg-red-500/20'; + } +} + +export default function TransactionHistory({ + transactions, + totalTransactions, + isLoading, + handlePageClick, +}: ITransactionHistory) { + return ( +
+ +
+
+
+
+ Block Height +
+
+ TX Hash +
+
+ TX Type +
+
+ TX Status +
+
+ Time +
+
+ {transactions.map((tx) => ( +
+
+
Block Height:
+
+
+ {getTxIcon(getMessages(tx.tx.body.messages))} +
+ {tx.height} +
+
+
+
TX Hash:
+ + {tx.txhash} + +
+
+
TX Type:
+ {getMessages(tx.tx.body.messages)} +
+
+
TX Status:
+ + {isTransactionSuccessful(tx) ? 'Success' : 'Failed'} + +
+
+
Time:
+ {dayjs(tx.timestamp).format('MMMM DD, YYYY')} at {dayjs(tx.timestamp).format('HH:mm:ss')} + () +
+
+ ))} + {!transactions?.length && !isLoading ? +
+

No Transactions

+
: null + } +
+
+ {totalTransactions > 1 ? +
+ +
: null + } +
+ ); +} diff --git a/apps/web/src/components/VersionsInfo.tsx b/apps/web/src/components/VersionsInfo.tsx new file mode 100644 index 0000000..fb4d4b9 --- /dev/null +++ b/apps/web/src/components/VersionsInfo.tsx @@ -0,0 +1,33 @@ +'use client' + +import useNodeInfo from '@/hooks/useNodeInfo'; +import { EVM_CHAIN_ID, IS_EVM_NETWORK } from '@/contants/network'; + +const HUB_VERSION = process.env.NEXT_PUBLIC_HUB_VERSION || 'unknown'; + +export default function VersionsInfo() { + const { nodeInfo } = useNodeInfo(); + + const networkValue = nodeInfo?.network + ? `${nodeInfo.network}${nodeInfo.appVersion ? `, ${nodeInfo.appVersion}` : ''}` + : '—'; + + const rows = [ + { label: 'Lumera Network', value: networkValue }, + ...(IS_EVM_NETWORK ? [{ label: 'EVM Chain ID', value: `${EVM_CHAIN_ID}` }] : []), + { label: 'Lumera Hub', value: HUB_VERSION }, + ]; + + return ( +
+
    + {rows.map(({ label, value }) => ( +
  • + {label}: + {value} +
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx index 8bf89ca..24c41d5 100644 --- a/apps/web/src/components/layout/AppShell.tsx +++ b/apps/web/src/components/layout/AppShell.tsx @@ -17,6 +17,7 @@ import { usePathname } from 'next/navigation'; import { ConnectWallet, WalletModalComponent } from '@/components/ConnectWallet' import AppLink from '@/components/AppLink'; +import SearchDialog from '@/components/SearchDialog'; import { useSelector, useDispatch } from '@/redux/hooks'; import { setActiveView, setCurrentPath, setViewTitle } from '@/redux/app.slice'; @@ -78,7 +79,7 @@ export default function AppShell({ children }: { children: React.ReactNode }) { } }, [dispatch, pathname]) - const isContextualRoute = pathname.startsWith('/tx/') || pathname.startsWith('/block/'); + const isContextualRoute = pathname.startsWith('/tx/') || pathname.startsWith('/block/') || pathname.startsWith('/account/'); const routeNavItem = NAV_ITEMS.find((item) => isActive(pathname, item.url)); const shellTitle = isContextualRoute ? viewTitle : routeNavItem?.label || VIEW_TITLES[activeView]; @@ -105,7 +106,7 @@ export default function AppShell({ children }: { children: React.ReactNode }) { return (
{/* Sidebar (desktop) */} -
+
@@ -188,7 +189,7 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
{/* Content area */} -
+
{/* Top bar */}
+ {/* Placeholder for wallet actions */} diff --git a/apps/web/src/contants/network.test.ts b/apps/web/src/contants/network.test.ts index 0dbe4ce..bd8c23f 100644 --- a/apps/web/src/contants/network.test.ts +++ b/apps/web/src/contants/network.test.ts @@ -10,6 +10,7 @@ const ENVIRONMENT_KEYS = [ 'NEXT_PUBLIC_REST_AI_URL', 'NEXT_PUBLIC_EVM_RPC_ENDPOINT', 'NEXT_PUBLIC_EVM_WS_ENDPOINT', + 'NEXT_PUBLIC_EVM_PROFILE_NAME', 'NEXT_PUBLIC_EVM_CHAIN_ID', 'NEXT_PUBLIC_COSMOS_EIP712_ENABLED', ] as const; @@ -44,6 +45,8 @@ describe('network profiles', () => { const network = await import('./network'); expect(network.CHAIN_ID).toBe('lumera-testnet-2'); expect(network.EVM_CHAIN_ID).toBe(76857769); + expect(network.EVM_PROFILE_NAME).toBe('lumera-testnet-evm'); + expect(network.EVM_RPC_ENDPOINT).toBe('https://evm-testnet.lumeraprotocol.com'); expect(network.IS_EVM_NETWORK).toBe(true); }); diff --git a/apps/web/src/contants/network.ts b/apps/web/src/contants/network.ts index bedc5a1..8597fa6 100644 --- a/apps/web/src/contants/network.ts +++ b/apps/web/src/contants/network.ts @@ -4,6 +4,7 @@ export const NETWORK_PROFILES = { devnet: { displayName: 'Lumera Devnet', chainName: 'lumera-devnet', + evmProfileName: 'lumera-devnet-evm', chainId: 'lumera-devnet-1', denom: 'ulume', rpcEndpoint: 'https://rpc.pastel.network', @@ -16,6 +17,7 @@ export const NETWORK_PROFILES = { testnet: { displayName: 'Lumera Testnet', chainName: 'lumera-testnet', + evmProfileName: 'lumera-testnet-evm', chainId: 'lumera-testnet-2', denom: 'ulume', rpcEndpoint: 'https://rpc-testnet.lumeraprotocol.com', @@ -28,6 +30,7 @@ export const NETWORK_PROFILES = { mainnet: { displayName: 'Lumera Mainnet', chainName: 'lumera', + evmProfileName: null, chainId: 'lumera-mainnet-1', denom: 'ulume', rpcEndpoint: 'https://rpc.lumera.io', @@ -69,6 +72,7 @@ export const RPC_ENDPOINT = process.env.NEXT_PUBLIC_RPC_ENDPOINT || ACTIVE_NETWO export const REST_AI_URL = process.env.NEXT_PUBLIC_REST_AI_URL || ACTIVE_NETWORK.restEndpoint; export const EVM_RPC_ENDPOINT = process.env.NEXT_PUBLIC_EVM_RPC_ENDPOINT || ACTIVE_NETWORK.evmRpcEndpoint; export const EVM_WS_ENDPOINT = process.env.NEXT_PUBLIC_EVM_WS_ENDPOINT || ACTIVE_NETWORK.evmWsEndpoint; +export const EVM_PROFILE_NAME = process.env.NEXT_PUBLIC_EVM_PROFILE_NAME || ACTIVE_NETWORK.evmProfileName; export const EVM_CHAIN_ID = process.env.NEXT_PUBLIC_EVM_CHAIN_ID ? Number(process.env.NEXT_PUBLIC_EVM_CHAIN_ID) : ACTIVE_NETWORK.evmChainId; diff --git a/apps/web/src/hooks/useAccount.ts b/apps/web/src/hooks/useAccount.ts new file mode 100644 index 0000000..b8249b1 --- /dev/null +++ b/apps/web/src/hooks/useAccount.ts @@ -0,0 +1,55 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useParams } from 'next/navigation'; + +import { fetchAccountInfo, AccountInfoData } from '@/hooks/useAccountInfo'; +import { parseAccountAddress } from '@/utils/account'; + +const useAccount = () => { + const params = useParams(); + const rawAddress = typeof params?.address === 'string' ? params.address : ''; + const addressFormats = useMemo(() => parseAccountAddress(rawAddress), [rawAddress]); + + const [accountInfo, setAccountInfo] = useState(null); + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + if (!addressFormats) { + setAccountInfo(null); + return; + } + let cancelled = false; + const fetchData = async () => { + setLoading(true); + setError(''); + try { + const info = await fetchAccountInfo(addressFormats.bech32Address); + if (!cancelled) { + setAccountInfo(info); + } + } catch (e) { + if (!cancelled) { + setError(e instanceof Error ? e.message : 'Unable to load account data.'); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + fetchData(); + return () => { + cancelled = true; + }; + }, [addressFormats]); + + return { + addressFormats, + isValidAddress: Boolean(addressFormats), + accountInfo, + isLoading, + error, + }; +}; + +export default useAccount; diff --git a/apps/web/src/hooks/useAccountInfo.test.ts b/apps/web/src/hooks/useAccountInfo.test.ts index aa2bd15..5cad7ce 100644 --- a/apps/web/src/hooks/useAccountInfo.test.ts +++ b/apps/web/src/hooks/useAccountInfo.test.ts @@ -1,6 +1,59 @@ import { describe, expect, it, vi } from 'vitest'; -import { fetchEvmAccountInfo, getTotalRewards } from './useAccountInfo'; +import { fetchAccountInfo, fetchEvmAccountInfo, getTotalRewards } from './useAccountInfo'; + +describe('fetchAccountInfo', () => { + it('queries balances and staking data for the given address', async () => { + const delegation = { + delegation: { + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + shares: '2500000.000000000000000000', + }, + balance: { denom: 'ulume', amount: '2500000' }, + }; + const reward = { + validator_address: 'lumeravaloper1validator', + reward: [{ denom: 'ulume', amount: '125000.5' }], + }; + const rewardTotal = [{ denom: 'ulume', amount: '124999.75' }]; + const get = vi.fn() + .mockResolvedValueOnce({ data: { balances: [{ denom: 'ulume', amount: '7000000' }] } }) + .mockResolvedValueOnce({ data: { delegation_responses: [delegation] } }) + .mockResolvedValueOnce({ data: { rewards: [reward], total: rewardTotal } }) + .mockResolvedValueOnce({ data: { unbonding_responses: [] } }); + + const accountInfo = await fetchAccountInfo('lumera1account', { get }); + + expect(get.mock.calls.map(([path]) => path)).toEqual([ + '/cosmos/bank/v1beta1/balances/lumera1account', + '/cosmos/staking/v1beta1/delegations/lumera1account', + '/cosmos/distribution/v1beta1/delegators/lumera1account/rewards', + '/cosmos/staking/v1beta1/delegators/lumera1account/unbonding_delegations', + ]); + expect(accountInfo).toEqual({ + balances: [{ denom: 'ulume', amount: '7000000' }], + delegations: [delegation], + rewards: [reward], + rewardTotal, + unbonding: [], + }); + }); + + it('defaults missing response collections to empty arrays', async () => { + const get = vi.fn().mockResolvedValue({ data: {} }); + + const accountInfo = await fetchAccountInfo('lumera1account', { get }); + + expect(accountInfo).toEqual({ + balances: [], + delegations: [], + rewards: [], + rewardTotal: [], + unbonding: [], + }); + }); +}); describe('fetchEvmAccountInfo', () => { it('combines the EVM balance with staking data queried by Bech32 address', async () => { diff --git a/apps/web/src/hooks/useAccountInfo.ts b/apps/web/src/hooks/useAccountInfo.ts index 53fdc35..185212d 100644 --- a/apps/web/src/hooks/useAccountInfo.ts +++ b/apps/web/src/hooks/useAccountInfo.ts @@ -88,6 +88,34 @@ export const fetchEvmAccountInfo = async ({ }; }; +interface FetchAccountInfoOptions { + get?: (path: string) => Promise>; +} + +export const fetchAccountInfo = async ( + address: string, + { get = instance.get }: FetchAccountInfoOptions = {}, +): Promise => { + const [balanceRes, delegationsRes, rewardsRes, unbondingRes] = await Promise.all([ + get(`/cosmos/bank/v1beta1/balances/${address}`), + get(`/cosmos/staking/v1beta1/delegations/${address}`), + get(`/cosmos/distribution/v1beta1/delegators/${address}/rewards`), + get(`/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations`), + ]); + const balanceData = balanceRes.data as { balances?: Coin[] }; + const delegationsData = delegationsRes.data as { delegation_responses?: DelegationResponse[] }; + const rewardsData = rewardsRes.data as { rewards?: ValidatorRewards[]; total?: Coin[] }; + const unbondingData = unbondingRes.data as { unbonding_responses?: ValidatorUnbonding[] }; + + return { + balances: balanceData.balances || [], + delegations: delegationsData.delegation_responses || [], + rewards: rewardsData.rewards || [], + rewardTotal: rewardsData.total || [], + unbonding: unbondingData.unbonding_responses || [], + }; +}; + export const getTotalRewards = (accountInfo: AccountInfoData | null) => { if (accountInfo?.rewardTotal) { return accountInfo.rewardTotal.reduce((total, reward) => { @@ -154,23 +182,7 @@ const useAccountInfo = () => { return; } - const [balanceRes, delegationsRes, rewardsRes, resUnbonding] = await Promise.all([ - instance.get(`/cosmos/bank/v1beta1/balances/${address}`), - instance.get(`/cosmos/staking/v1beta1/delegations/${address}`), - instance.get(`/cosmos/distribution/v1beta1/delegators/${address}/rewards`), - instance.get(`/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations`), - ]); - - const balanceData = balanceRes.data; - const delegationsData = delegationsRes.data; - const rewardsData = rewardsRes.data; - const _accountInfo = { - balances: balanceData.balances, - delegations: delegationsData.delegation_responses, - rewards: rewardsData.rewards, - rewardTotal: rewardsData.total, - unbonding: resUnbonding.unbonding_responses, - } + const _accountInfo = await fetchAccountInfo(address); setAccountInfo(_accountInfo); setClaimInfo({ ...claimInfo, diff --git a/apps/web/src/hooks/useNodeInfo.ts b/apps/web/src/hooks/useNodeInfo.ts new file mode 100644 index 0000000..d583d09 --- /dev/null +++ b/apps/web/src/hooks/useNodeInfo.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; + +import * as instance from '@/utils/api'; +import { parseNodeInfo, NodeVersionInfo } from '@/utils/node-info'; + +const useNodeInfo = () => { + const [nodeInfo, setNodeInfo] = useState(null); + + useEffect(() => { + let cancelled = false; + const fetchData = async () => { + try { + const { data } = await instance.get('/cosmos/base/tendermint/v1beta1/node_info'); + if (!cancelled) { + setNodeInfo(parseNodeInfo(data)); + } + } catch { + if (!cancelled) { + setNodeInfo(null); + } + } + }; + fetchData(); + return () => { + cancelled = true; + }; + }, []); + + return { nodeInfo }; +}; + +export default useNodeInfo; diff --git a/apps/web/src/hooks/useSend.ts b/apps/web/src/hooks/useSend.ts index 52ba5ec..765048d 100644 --- a/apps/web/src/hooks/useSend.ts +++ b/apps/web/src/hooks/useSend.ts @@ -16,7 +16,7 @@ import { } from '@/utils/evm'; interface UseDepositOptions { - callback?: () => void; + callback?: (transactionHash?: string) => void; customMemo?: string; } @@ -143,7 +143,7 @@ const useSend = (options: UseDepositOptions = {}) => { }); setTransactionHash(transactionHash); resetData(); - options.callback?.(); + options.callback?.(transactionHash); return; } @@ -177,7 +177,7 @@ const useSend = (options: UseDepositOptions = {}) => { setTransactionHash(result?.transactionHash); resetData(); if (options?.callback) { - options.callback(); + options.callback(result.transactionHash); } } } catch (error) { diff --git a/apps/web/src/hooks/useTransaction.ts b/apps/web/src/hooks/useTransaction.ts index cb5ed11..d8977db 100644 --- a/apps/web/src/hooks/useTransaction.ts +++ b/apps/web/src/hooks/useTransaction.ts @@ -1,10 +1,14 @@ -import { useState, useEffect } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import * as instance from '@/utils/api'; import useWalletConnect from '@/hooks/useWalletConnect'; import { TLog, TLogEvent, TMessage, TOption, TSignerInfos, TFee } from '@/hooks/useRecentActivity'; import { Coin } from '@/hooks/useAccountInfo'; -import { getTransactionHistoryAddress } from '@/utils/transaction-history'; +import { + buildTxHistoryPath, + getTransactionHistoryAddress, + TxHistoryDirection, +} from '@/utils/transaction-history'; const LIMIT = 20; @@ -42,35 +46,52 @@ export interface ITransaction { txhash: string; } -const useTransaction = () => { +interface UseTransactionOptions { + address?: string; + direction?: TxHistoryDirection; +} + +const useTransaction = ({ address: addressOverride, direction }: UseTransactionOptions = {}) => { const { address, bech32Address, isEvm } = useWalletConnect(); - const transactionAddress = getTransactionHistoryAddress({ address, bech32Address, isEvm }); + const transactionAddress = addressOverride + ?? getTransactionHistoryAddress({ address, bech32Address, isEvm }); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(''); const [transactions, setTransactions] = useState([]); const [totalTransactions, setTotalTransactions] = useState(0); - const fetchTransactions = async (offset = 0) => { + const fetchTransactions = useCallback(async (offset = 0, showLoading = true) => { if (!transactionAddress) { setTransactions([]); setTotalTransactions(0); setError(''); setLoading(false); - return; + return []; + } + if (showLoading) { + setLoading(true); } - setLoading(true); setError(''); try { - const { data } = await instance.get(`/cosmos/tx/v1beta1/txs?query=message.sender=%27${transactionAddress}%27&pagination.limit=${LIMIT}&pagination.offset=${offset}&order_by=ORDER_BY_DESC`); + const { data } = await instance.get(buildTxHistoryPath({ + address: transactionAddress, + direction, + limit: LIMIT, + offset, + })); setTotalTransactions(Math.ceil(Number(data.total) / LIMIT)); setTransactions(data.tx_responses || []); + return data.tx_responses || []; } catch (e) { setError(e instanceof Error ? e.message : 'An unknown error occurred.'); + return []; } finally { - setLoading(false); + if (showLoading) { + setLoading(false); + } } - } + }, [transactionAddress, direction]); useEffect(() => { if (transactionAddress) { @@ -81,19 +102,25 @@ const useTransaction = () => { setError(''); setLoading(false); } - }, [transactionAddress]); + }, [fetchTransactions, transactionAddress]); const handlePageClick = ({ selected }: { selected: number }) => { const offset = selected * LIMIT; fetchTransactions(offset); } + const refreshTransactions = useCallback( + () => fetchTransactions(0, false), + [fetchTransactions], + ); + return { isLoading, error, transactions, totalTransactions, handlePageClick, + refreshTransactions, } } diff --git a/apps/web/src/utils/account.test.ts b/apps/web/src/utils/account.test.ts new file mode 100644 index 0000000..d483ce7 --- /dev/null +++ b/apps/web/src/utils/account.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { toBech32, fromHex } from '@cosmjs/encoding'; + +import { parseAccountAddress } from './account'; + +const HEX_20_BYTES = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; +const ACCOUNT_ADDRESS = toBech32('lumera', fromHex(HEX_20_BYTES)); + +describe('parseAccountAddress', () => { + it('returns both formats for a bech32 account address', () => { + expect(parseAccountAddress(ACCOUNT_ADDRESS)).toEqual({ + bech32Address: ACCOUNT_ADDRESS, + ethAddress: `0x${HEX_20_BYTES}`, + }); + }); + + it('accepts an uppercase bech32 address', () => { + expect(parseAccountAddress(ACCOUNT_ADDRESS.toUpperCase())).toEqual({ + bech32Address: ACCOUNT_ADDRESS, + ethAddress: `0x${HEX_20_BYTES}`, + }); + }); + + it('accepts an EVM hex address in any case', () => { + expect(parseAccountAddress(`0x${HEX_20_BYTES.toUpperCase()}`)).toEqual({ + bech32Address: ACCOUNT_ADDRESS, + ethAddress: `0x${HEX_20_BYTES}`, + }); + }); + + it('rejects a bech32 address with a foreign prefix', () => { + expect(parseAccountAddress(toBech32('cosmos', fromHex(HEX_20_BYTES)))).toBeNull(); + }); + + it('rejects a validator operator address', () => { + expect(parseAccountAddress(toBech32('lumeravaloper', fromHex(HEX_20_BYTES)))).toBeNull(); + }); + + it('rejects malformed input', () => { + expect(parseAccountAddress('lumera1notarealaddress')).toBeNull(); + expect(parseAccountAddress('')).toBeNull(); + expect(parseAccountAddress('0xabc')).toBeNull(); + }); +}); diff --git a/apps/web/src/utils/account.ts b/apps/web/src/utils/account.ts new file mode 100644 index 0000000..ae2af3e --- /dev/null +++ b/apps/web/src/utils/account.ts @@ -0,0 +1,33 @@ +import { fromBech32 } from '@cosmjs/encoding'; + +import { cosmosAddressToEvmAddress, evmAddressToCosmosAddress, isEvmAddress } from './evm'; + +const BECH32_PREFIX = 'lumera'; + +export interface AccountAddressFormats { + bech32Address: string; + ethAddress: string; +} + +export const parseAccountAddress = (input: string): AccountAddressFormats | null => { + const lowered = input.trim().toLowerCase(); + if (!lowered) return null; + + if (isEvmAddress(lowered)) { + return { + bech32Address: evmAddressToCosmosAddress(lowered, BECH32_PREFIX), + ethAddress: lowered, + }; + } + + try { + const { prefix, data } = fromBech32(lowered); + if (prefix !== BECH32_PREFIX || data.length !== 20) return null; + return { + bech32Address: lowered, + ethAddress: cosmosAddressToEvmAddress(lowered), + }; + } catch { + return null; + } +}; diff --git a/apps/web/src/utils/evm.test.ts b/apps/web/src/utils/evm.test.ts index 6d8f6e1..c5ee348 100644 --- a/apps/web/src/utils/evm.test.ts +++ b/apps/web/src/utils/evm.test.ts @@ -10,11 +10,14 @@ vi.mock('@/contants/network', () => ({ import { assertEvmAccountForChain, + assertEvmProviderMatchesRpc, cosmosAddressToEvmAddress, evmAddressToCosmosAddress, evmBalanceToMicroLume, + ensureEvmWalletNetwork, getEvmAccountForChain, getEvmBalance, + getEvmConnectionErrorMessage, getEvmAddressFormats, getMetaMaskProvider, isEvmAddress, @@ -68,6 +71,28 @@ describe('MetaMask provider selection', () => { }); }); +describe('MetaMask connection errors', () => { + it('explains browser and gateway failures as an RPC outage', () => { + const expected = 'Lumera Testnet is temporarily unavailable. Your wallet is fine—please try again in a few minutes.'; + + expect(getEvmConnectionErrorMessage( + new TypeError('Failed to fetch'), + 'Lumera Testnet', + )).toBe(expected); + expect(getEvmConnectionErrorMessage( + new Error('EVM RPC request failed with status 502.'), + 'Lumera Testnet', + )).toBe(expected); + }); + + it('preserves wallet-specific errors such as user rejection', () => { + expect(getEvmConnectionErrorMessage( + new Error('User rejected the request.'), + 'Lumera Testnet', + )).toBe('User rejected the request.'); + }); +}); + describe('EVM account address formats', () => { const bech32Address = 'lumera1qy352euf40x77qfrg4ncn27dauqjx3t83egcev'; @@ -181,6 +206,154 @@ describe('EVM account validation', () => { }); }); +describe('EVM RPC identity validation', () => { + const createBlockProvider = (blockNumber: string, blockHash: string) => ({ + request: vi.fn(async ({ method }: { method: string }) => { + if (method === 'eth_blockNumber') return blockNumber; + if (method === 'eth_getBlockByNumber') return { hash: blockHash }; + throw new Error(`Unexpected method ${method}`); + }), + }) as unknown as Eip1193Provider; + + it('accepts matching configured and MetaMask RPC chains', async () => { + const requestRpc = vi.fn(async (method: string) => { + if (method === 'eth_blockNumber') return '0x102'; + if (method === 'eth_getBlockByNumber') return { hash: '0xabc' }; + throw new Error(`Unexpected method ${method}`); + }); + + await expect(assertEvmProviderMatchesRpc( + createBlockProvider('0x100', '0xAbC'), + { requestRpc: requestRpc as never, rpcEndpoint: 'https://rpc.example.test' }, + )).resolves.toBeUndefined(); + }); + + it('rejects a different network that reuses the configured chain ID', async () => { + const requestRpc = vi.fn(async (method: string) => { + if (method === 'eth_blockNumber') return '0x1000'; + throw new Error(`Unexpected method ${method}`); + }); + + await expect(assertEvmProviderMatchesRpc( + createBlockProvider('0x100', '0xabc'), + { requestRpc: requestRpc as never, rpcEndpoint: 'https://testnet.example.test' }, + )).rejects.toThrow('set the RPC URL to https://testnet.example.test'); + }); + + it('rejects divergent block hashes when heights are close', async () => { + const requestRpc = vi.fn(async (method: string) => { + if (method === 'eth_blockNumber') return '0x100'; + if (method === 'eth_getBlockByNumber') return { hash: '0xdef' }; + throw new Error(`Unexpected method ${method}`); + }); + + await expect(assertEvmProviderMatchesRpc( + createBlockProvider('0x100', '0xabc'), + { requestRpc: requestRpc as never }, + )).rejects.toThrow('different Lumera network'); + }); +}); + +describe('MetaMask network profile setup', () => { + const options = { + chainId: CHAIN_ID, + chainName: 'lumera-testnet-evm', + rpcEndpoint: 'https://evm-testnet.lumeraprotocol.com', + }; + const matchingRpc = vi.fn(async (method: string) => { + if (method === 'eth_blockNumber') return '0x100'; + if (method === 'eth_getBlockByNumber') return { hash: '0xtestnet' }; + throw new Error(`Unexpected method ${method}`); + }); + + it('suggests the named testnet profile when MetaMask does not know the chain', async () => { + let activeChainId = '0x1'; + const provider = { + request: vi.fn(async ({ method, params }: { method: string; params?: unknown[] }) => { + if (method === 'eth_chainId') return activeChainId; + if (method === 'wallet_switchEthereumChain') { + if (activeChainId === '0x1') { + const error = new Error('Unrecognized chain') as Error & { code: number }; + error.code = 4902; + throw error; + } + return null; + } + if (method === 'wallet_addEthereumChain') { + const profile = params?.[0] as { chainId: string }; + activeChainId = profile.chainId; + return null; + } + if (method === 'eth_blockNumber') return '0x100'; + if (method === 'eth_getBlockByNumber') return { hash: '0xtestnet' }; + throw new Error(`Unexpected method ${method}`); + }), + } as unknown as Eip1193Provider; + + await expect(ensureEvmWalletNetwork(provider, { + ...options, + requestRpc: matchingRpc as never, + suggestProfileOnMismatch: true, + })).resolves.toBeUndefined(); + + expect(provider.request).toHaveBeenCalledWith({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: '0x494c1a9', + chainName: 'lumera-testnet-evm', + nativeCurrency: { name: 'LUME', symbol: 'LUME', decimals: 18 }, + rpcUrls: ['https://evm-testnet.lumeraprotocol.com'], + }], + }); + }); + + it('suggests testnet when an active devnet profile reuses its chain ID', async () => { + let configuredTestnet = false; + const provider = { + request: vi.fn(async ({ method }: { method: string }) => { + if (method === 'eth_chainId') return '0x494c1a9'; + if (method === 'eth_blockNumber') return configuredTestnet ? '0x100' : '0x1000'; + if (method === 'eth_getBlockByNumber') return { hash: '0xtestnet' }; + if (method === 'wallet_addEthereumChain') { + configuredTestnet = true; + return null; + } + if (method === 'wallet_switchEthereumChain') return null; + throw new Error(`Unexpected method ${method}`); + }), + } as unknown as Eip1193Provider; + + await expect(ensureEvmWalletNetwork(provider, { + ...options, + requestRpc: matchingRpc as never, + suggestProfileOnMismatch: true, + })).resolves.toBeUndefined(); + + expect(provider.request).toHaveBeenCalledWith(expect.objectContaining({ + method: 'wallet_addEthereumChain', + })); + }); + + it('does not open a profile prompt during background or pre-send validation', async () => { + const provider = { + request: vi.fn(async ({ method }: { method: string }) => { + if (method === 'eth_chainId') return '0x494c1a9'; + if (method === 'eth_blockNumber') return '0x1000'; + throw new Error(`Unexpected method ${method}`); + }), + } as unknown as Eip1193Provider; + + await expect(ensureEvmWalletNetwork(provider, { + ...options, + requestRpc: matchingRpc as never, + })).rejects.toThrow('different Lumera network'); + + expect(provider.request).not.toHaveBeenCalledWith(expect.objectContaining({ + method: 'wallet_addEthereumChain', + })); + }); +}); + describe('requestEvmRpc', () => { it('posts a JSON-RPC request and returns its result', async () => { const fetchMock = vi.fn().mockResolvedValue({ diff --git a/apps/web/src/utils/evm.ts b/apps/web/src/utils/evm.ts index 05f81b9..484c4c6 100644 --- a/apps/web/src/utils/evm.ts +++ b/apps/web/src/utils/evm.ts @@ -13,6 +13,46 @@ interface EvmRpcResponse { }; } +interface EvmBlockIdentity { + hash?: string | null; +} + +type EvmRpcRequester = (method: string, params?: unknown[]) => Promise; + +interface EvmRpcIdentityOptions { + requestRpc?: EvmRpcRequester; + rpcEndpoint?: string; + maxBlockDrift?: number; +} + +interface EvmWalletNetworkOptions extends EvmRpcIdentityOptions { + chainId: number; + chainName: string; + rpcEndpoint: string; + suggestProfileOnMismatch?: boolean; +} + +export class EvmNetworkMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'EvmNetworkMismatchError'; + } +} + +export const getEvmConnectionErrorMessage = ( + error: unknown, + networkName: string, +) => { + const message = error instanceof Error ? error.message : 'Unable to connect EVM wallet.'; + if ( + /failed to fetch|network request failed|could not fetch chain id|rpc request failed with status (?:429|5\d\d)/i + .test(message) + ) { + return `${networkName} is temporarily unavailable. Your wallet is fine—please try again in a few minutes.`; + } + return message; +}; + export const getMetaMaskProvider = (provider?: Eip1193Provider | null) => { if (!provider) return null; if (provider.providers?.length) { @@ -179,6 +219,128 @@ export const requestEvmRpc = async (method: string, params: unknown[] = []): return payload.result; }; +const parseRpcBlockNumber = (value: string) => { + if (!/^0x[0-9a-fA-F]+$/.test(value)) { + throw new Error('EVM RPC returned an invalid block number.'); + } + return Number.parseInt(value, 16); +}; + +export const assertEvmProviderMatchesRpc = async ( + provider: Eip1193Provider, + { + requestRpc = requestEvmRpc, + rpcEndpoint = EVM_RPC_ENDPOINT || 'the configured network RPC', + maxBlockDrift = 100, + }: EvmRpcIdentityOptions = {}, +) => { + const networkMismatch = () => new EvmNetworkMismatchError( + `MetaMask is connected to a different Lumera network. In MetaMask, set the RPC URL to ${rpcEndpoint}.`, + ); + const [providerBlockValue, configuredBlockValue] = await Promise.all([ + provider.request({ method: 'eth_blockNumber' }), + requestRpc('eth_blockNumber'), + ]); + const providerBlock = parseRpcBlockNumber(providerBlockValue); + const configuredBlock = parseRpcBlockNumber(configuredBlockValue); + + if (Math.abs(providerBlock - configuredBlock) > maxBlockDrift) { + throw networkMismatch(); + } + + const comparisonBlock = Math.max(0, Math.min(providerBlock, configuredBlock) - 12); + const blockTag = `0x${comparisonBlock.toString(16)}`; + const [providerIdentity, configuredIdentity] = await Promise.all([ + provider.request({ method: 'eth_getBlockByNumber', params: [blockTag, false] }), + requestRpc('eth_getBlockByNumber', [blockTag, false]), + ]); + + if ( + !providerIdentity?.hash + || !configuredIdentity?.hash + || providerIdentity.hash.toLowerCase() !== configuredIdentity.hash.toLowerCase() + ) { + throw networkMismatch(); + } +}; + +const isUnrecognizedChainError = (error: unknown) => { + const providerError = error as { code?: number; data?: { originalError?: { code?: number } } }; + return providerError.code === 4902 || providerError.data?.originalError?.code === 4902; +}; + +export const ensureEvmWalletNetwork = async ( + provider: Eip1193Provider, + { + chainId, + chainName, + rpcEndpoint, + suggestProfileOnMismatch = false, + requestRpc, + maxBlockDrift, + }: EvmWalletNetworkOptions, +) => { + const expectedChainId = toHexChainId(chainId); + const addProfile = () => provider.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: expectedChainId, + chainName, + nativeCurrency: { + name: 'LUME', + symbol: 'LUME', + decimals: EVM_NATIVE_DECIMALS, + }, + rpcUrls: [rpcEndpoint], + }], + }); + const switchProfile = () => provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: expectedChainId }], + }); + + const currentChainId = await provider.request({ method: 'eth_chainId' }); + if (currentChainId.toLowerCase() !== expectedChainId.toLowerCase()) { + try { + await switchProfile(); + } catch (switchError) { + if (!isUnrecognizedChainError(switchError)) throw switchError; + await addProfile(); + await switchProfile(); + } + } + + const activeChainId = await provider.request({ method: 'eth_chainId' }); + if (activeChainId.toLowerCase() !== expectedChainId.toLowerCase()) { + throw new Error(`Wallet did not switch to ${chainName}.`); + } + + const verifyIdentity = () => assertEvmProviderMatchesRpc(provider, { + requestRpc, + rpcEndpoint, + maxBlockDrift, + }); + + try { + await verifyIdentity(); + } catch (identityError) { + if (!(identityError instanceof EvmNetworkMismatchError) || !suggestProfileOnMismatch) { + throw identityError; + } + + try { + await addProfile(); + await switchProfile(); + } catch (profileError) { + const detail = profileError instanceof Error ? ` ${profileError.message}` : ''; + throw new Error( + `MetaMask could not configure ${chainName}. Remove or update the conflicting Lumera network, then add ${chainName} with RPC URL ${rpcEndpoint}.${detail}`, + ); + } + await verifyIdentity(); + } +}; + export const getEvmBalance = async (address: string) => { if (!isEvmAddress(address)) { throw new Error('Cannot query the balance of an invalid EVM address.'); diff --git a/apps/web/src/utils/node-info.test.ts b/apps/web/src/utils/node-info.test.ts new file mode 100644 index 0000000..0ace9f3 --- /dev/null +++ b/apps/web/src/utils/node-info.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { parseNodeInfo } from './node-info'; + +describe('parseNodeInfo', () => { + it('extracts the network and v-prefixed app version', () => { + expect(parseNodeInfo({ + default_node_info: { network: 'lumera-testnet-2' }, + application_version: { version: '1.20.2-rc1' }, + })).toEqual({ + network: 'lumera-testnet-2', + appVersion: 'v1.20.2-rc1', + }); + }); + + it('keeps an existing v prefix', () => { + expect(parseNodeInfo({ + default_node_info: { network: 'lumera-mainnet-1' }, + application_version: { version: 'v2.0.0' }, + })).toEqual({ + network: 'lumera-mainnet-1', + appVersion: 'v2.0.0', + }); + }); + + it('returns empty fields when data is missing', () => { + expect(parseNodeInfo({})).toEqual({ network: '', appVersion: '' }); + expect(parseNodeInfo({ + default_node_info: { network: 'lumera-testnet-2' }, + })).toEqual({ network: 'lumera-testnet-2', appVersion: '' }); + }); +}); diff --git a/apps/web/src/utils/node-info.ts b/apps/web/src/utils/node-info.ts new file mode 100644 index 0000000..67942b7 --- /dev/null +++ b/apps/web/src/utils/node-info.ts @@ -0,0 +1,18 @@ +export interface NodeInfoResponse { + default_node_info?: { network?: string }; + application_version?: { version?: string }; +} + +export interface NodeVersionInfo { + network: string; + appVersion: string; +} + +export const parseNodeInfo = (data: NodeInfoResponse): NodeVersionInfo => { + const network = data.default_node_info?.network || ''; + const version = data.application_version?.version || ''; + return { + network, + appVersion: version && !version.startsWith('v') ? `v${version}` : version, + }; +}; diff --git a/apps/web/src/utils/portfolio.test.ts b/apps/web/src/utils/portfolio.test.ts index 8dd6ee5..1fb0274 100644 --- a/apps/web/src/utils/portfolio.test.ts +++ b/apps/web/src/utils/portfolio.test.ts @@ -1,6 +1,81 @@ import { describe, expect, it } from 'vitest'; -import { getPortfolioData } from './portfolio'; +import { + getPortfolioData, + getAvailableBalances, + getDelegations, + getRewards, + getUnbonding, + getTotalBalances, +} from './portfolio'; +import type { AccountInfoData } from '@/hooks/useAccountInfo'; + +const unbondingEntry = (balance: string) => ({ + balance, + completion_time: '2026-08-21T00:00:00Z', + creation_height: '100', + initial_balance: balance, + unbonding_id: '1', + unbonding_on_hold_ref_count: '0', +}); + +const ACCOUNT_INFO: AccountInfoData = { + balances: [ + { denom: 'ulume', amount: '5000000' }, + { denom: 'lume', amount: '2' }, + { denom: 'ibc/other', amount: '999' }, + ], + delegations: [{ + delegation: { + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + shares: '3000000.000000000000000000', + }, + balance: { denom: 'ulume', amount: '3000000' }, + }], + rewards: [{ + validator_address: 'lumeravaloper1validator', + reward: [ + { denom: 'ulume', amount: '250000' }, + { denom: 'ibc/other', amount: '999' }, + ], + }], + unbonding: [{ + delegator_address: 'lumera1account', + validator_address: 'lumeravaloper1validator', + entries: [unbondingEntry('100000'), unbondingEntry('50000')], + }], +}; + +describe('balance aggregation helpers', () => { + it('sums available balances in micro denom, converting display denom', () => { + expect(getAvailableBalances(ACCOUNT_INFO)).toBe(7000000); + }); + + it('sums delegated balances', () => { + expect(getDelegations(ACCOUNT_INFO)).toBe(3000000); + }); + + it('sums rewards across validators, ignoring foreign denoms', () => { + expect(getRewards(ACCOUNT_INFO)).toBe(250000); + }); + + it('sums unbonding entries', () => { + expect(getUnbonding(ACCOUNT_INFO)).toBe(150000); + }); + + it('totals available plus delegated balances', () => { + expect(getTotalBalances(ACCOUNT_INFO)).toBe(10000000); + }); + + it('returns zero for null account info', () => { + expect(getAvailableBalances(null)).toBe(0); + expect(getDelegations(null)).toBe(0); + expect(getRewards(null)).toBe(0); + expect(getUnbonding(null)).toBe(0); + expect(getTotalBalances(null)).toBe(0); + }); +}); describe('getPortfolioData', () => { it('returns raw numeric LUME amounts suitable for chart values', () => { diff --git a/apps/web/src/utils/portfolio.ts b/apps/web/src/utils/portfolio.ts index 180f80a..1923312 100644 --- a/apps/web/src/utils/portfolio.ts +++ b/apps/web/src/utils/portfolio.ts @@ -12,6 +12,27 @@ const toMicroLume = (coin: Coin) => { return 0; }; +export const getAvailableBalances = (accountInfo: AccountInfoData | null) => + (accountInfo?.balances || []).reduce((total, coin) => total + toMicroLume(coin), 0); + +export const getDelegations = (accountInfo: AccountInfoData | null) => + (accountInfo?.delegations || []).reduce((total, item) => total + toMicroLume(item.balance), 0); + +export const getRewards = (accountInfo: AccountInfoData | null) => + (accountInfo?.rewards || []).reduce( + (total, item) => total + item.reward.reduce((sum, coin) => sum + toMicroLume(coin), 0), + 0, + ); + +export const getUnbonding = (accountInfo: AccountInfoData | null) => + (accountInfo?.unbonding || []).reduce( + (total, item) => total + item.entries.reduce((sum, entry) => sum + Number(entry.balance), 0), + 0, + ); + +export const getTotalBalances = (accountInfo: AccountInfoData | null) => + getAvailableBalances(accountInfo) + getDelegations(accountInfo); + export const getPortfolioData = (accountInfo: AccountInfoData | null) => { if (!accountInfo) { return { stacked: 0, liquid: 0 }; diff --git a/apps/web/src/utils/search.test.ts b/apps/web/src/utils/search.test.ts new file mode 100644 index 0000000..7dddf65 --- /dev/null +++ b/apps/web/src/utils/search.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { toBech32, fromHex } from '@cosmjs/encoding'; +import { parseSearchQuery } from './search'; + +const HEX_20_BYTES = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; +const ACCOUNT_ADDRESS = toBech32('lumera', fromHex(HEX_20_BYTES)); +const VALOPER_ADDRESS = toBech32('lumeravaloper', fromHex(HEX_20_BYTES)); +const TX_HASH = 'ab'.repeat(32); + +describe('parseSearchQuery', () => { + it('routes a block height to the block inspector', () => { + expect(parseSearchQuery('123456')).toBe('/block/123456'); + }); + + it('routes a transaction hash to the transaction inspector uppercased', () => { + expect(parseSearchQuery(TX_HASH)).toBe(`/tx/${TX_HASH.toUpperCase()}`); + }); + + it('accepts a transaction hash with a 0x prefix', () => { + expect(parseSearchQuery(`0x${TX_HASH}`)).toBe(`/tx/${TX_HASH.toUpperCase()}`); + }); + + it('accepts a mixed-case transaction hash', () => { + expect(parseSearchQuery('aB'.repeat(32))).toBe(`/tx/${TX_HASH.toUpperCase()}`); + }); + + it('converts an EVM hex address to a bech32 account route', () => { + expect(parseSearchQuery(`0x${HEX_20_BYTES}`)).toBe(`/account/${ACCOUNT_ADDRESS}`); + }); + + it('accepts an EVM hex address in any case', () => { + expect(parseSearchQuery(`0x${HEX_20_BYTES.toUpperCase()}`)).toBe( + `/account/${ACCOUNT_ADDRESS}`, + ); + }); + + it('routes a bech32 account address to the account inspector', () => { + expect(parseSearchQuery(ACCOUNT_ADDRESS)).toBe(`/account/${ACCOUNT_ADDRESS}`); + }); + + it('accepts an uppercase bech32 account address', () => { + expect(parseSearchQuery(ACCOUNT_ADDRESS.toUpperCase())).toBe( + `/account/${ACCOUNT_ADDRESS}`, + ); + }); + + it('routes a validator operator address to the staking inspector', () => { + expect(parseSearchQuery(VALOPER_ADDRESS)).toBe(`/staking/${VALOPER_ADDRESS}`); + }); + + it('trims surrounding whitespace before classifying', () => { + expect(parseSearchQuery(' 123456 ')).toBe('/block/123456'); + }); + + it('returns null for an empty query', () => { + expect(parseSearchQuery(' ')).toBeNull(); + }); + + it('returns null for a bech32 address with a foreign prefix', () => { + const foreign = toBech32('cosmos', fromHex(HEX_20_BYTES)); + expect(parseSearchQuery(foreign)).toBeNull(); + }); + + it('returns null for a malformed bech32 address', () => { + expect(parseSearchQuery('lumera1notarealaddress')).toBeNull(); + }); + + it('returns null for hex of the wrong length', () => { + expect(parseSearchQuery('0xabc123')).toBeNull(); + }); + + it('returns null for arbitrary text', () => { + expect(parseSearchQuery('hello world')).toBeNull(); + }); +}); diff --git a/apps/web/src/utils/search.ts b/apps/web/src/utils/search.ts new file mode 100644 index 0000000..30ea547 --- /dev/null +++ b/apps/web/src/utils/search.ts @@ -0,0 +1,37 @@ +import { fromBech32 } from '@cosmjs/encoding'; +import { evmAddressToCosmosAddress, isEvmAddress } from './evm'; + +const BECH32_PREFIX = 'lumera'; +const VALOPER_PREFIX = 'lumeravaloper'; + +export const parseSearchQuery = (input: string): string | null => { + const query = input.trim(); + if (!query) return null; + + if (/^\d+$/.test(query)) { + return `/block/${query}`; + } + + const hash = query.startsWith('0x') || query.startsWith('0X') + ? query.slice(2) + : query; + if (/^[0-9a-fA-F]{64}$/.test(hash)) { + return `/tx/${hash.toUpperCase()}`; + } + + const lowered = query.toLowerCase(); + if (isEvmAddress(lowered)) { + return `/account/${evmAddressToCosmosAddress(lowered, BECH32_PREFIX)}`; + } + + try { + const { prefix, data } = fromBech32(lowered); + if (data.length !== 20) return null; + if (prefix === VALOPER_PREFIX) return `/staking/${lowered}`; + if (prefix === BECH32_PREFIX) return `/account/${lowered}`; + } catch { + return null; + } + + return null; +}; diff --git a/apps/web/src/utils/transaction-history.test.ts b/apps/web/src/utils/transaction-history.test.ts index c61ecd8..3632018 100644 --- a/apps/web/src/utils/transaction-history.test.ts +++ b/apps/web/src/utils/transaction-history.test.ts @@ -1,10 +1,31 @@ import { describe, expect, it } from 'vitest'; import { + buildTxHistoryPath, getTransactionHistoryAddress, + hasEthereumTransactionHash, isTransactionSuccessful, } from './transaction-history'; +describe('buildTxHistoryPath', () => { + it('builds a sender query by default', () => { + expect(buildTxHistoryPath({ address: 'lumera1account', limit: 20, offset: 0 })).toBe( + "/cosmos/tx/v1beta1/txs?query=message.sender=%27lumera1account%27&pagination.limit=20&pagination.offset=0&order_by=ORDER_BY_DESC", + ); + }); + + it('builds a recipient query for received transactions', () => { + expect(buildTxHistoryPath({ + address: 'lumera1account', + direction: 'received', + limit: 20, + offset: 40, + })).toBe( + "/cosmos/tx/v1beta1/txs?query=transfer.recipient=%27lumera1account%27&pagination.limit=20&pagination.offset=40&order_by=ORDER_BY_DESC", + ); + }); +}); + describe('transaction history', () => { it('queries the equivalent Bech32 account in MetaMask mode', () => { expect(getTransactionHistoryAddress({ @@ -31,4 +52,19 @@ describe('transaction history', () => { expect(isTransactionSuccessful({ code: 5 })).toBe(false); }); + it('finds a submitted Ethereum hash in indexed transaction events', () => { + const transactions = [{ + events: [{ + type: 'ethereum_tx', + attributes: [ + { key: 'txGasUsed', value: '21000' }, + { key: 'ethereumTxHash', value: '0xAbCd' }, + ], + }], + }]; + + expect(hasEthereumTransactionHash(transactions, '0xabcd')).toBe(true); + expect(hasEthereumTransactionHash(transactions, '0x1234')).toBe(false); + }); + }); diff --git a/apps/web/src/utils/transaction-history.ts b/apps/web/src/utils/transaction-history.ts index 4224e10..406a4b7 100644 --- a/apps/web/src/utils/transaction-history.ts +++ b/apps/web/src/utils/transaction-history.ts @@ -9,6 +9,32 @@ interface IndexedTransactionStatus { events?: unknown; } +interface IndexedTransactionEvents { + events?: Array<{ + type: string; + attributes?: Array<{ key: string; value: string }>; + }>; +} + +export type TxHistoryDirection = 'sent' | 'received'; + +interface TxHistoryPathOptions { + address: string; + direction?: TxHistoryDirection; + limit: number; + offset: number; +} + +export const buildTxHistoryPath = ({ + address, + direction = 'sent', + limit, + offset, +}: TxHistoryPathOptions) => { + const event = direction === 'received' ? 'transfer.recipient' : 'message.sender'; + return `/cosmos/tx/v1beta1/txs?query=${event}=%27${address}%27&pagination.limit=${limit}&pagination.offset=${offset}&order_by=ORDER_BY_DESC`; +}; + export const getTransactionHistoryAddress = ({ address, bech32Address, @@ -18,3 +44,16 @@ export const getTransactionHistoryAddress = ({ export const isTransactionSuccessful = (transaction: IndexedTransactionStatus) => ( transaction.code === 0 ); + +export const hasEthereumTransactionHash = ( + transactions: IndexedTransactionEvents[], + transactionHash: string, +) => { + const normalizedHash = transactionHash.toLowerCase(); + return transactions.some((transaction) => transaction.events?.some((event) => ( + event.type === 'ethereum_tx' + && event.attributes?.some(({ key, value }) => ( + key === 'ethereumTxHash' && value.toLowerCase() === normalizedHash + )) + ))); +}; diff --git a/apps/web/src/utils/wallet-selection.test.ts b/apps/web/src/utils/wallet-selection.test.ts index 3dfd69f..7745a6d 100644 --- a/apps/web/src/utils/wallet-selection.test.ts +++ b/apps/web/src/utils/wallet-selection.test.ts @@ -5,6 +5,7 @@ import { getActiveWalletMode, getAlternativeWalletName, getPreferredWalletSelection, + disconnectPersistedInterchainWallet, KEPLR_WALLET_NAME, METAMASK_WALLET_NAME, } from './wallet-selection'; @@ -109,3 +110,72 @@ describe('alternative wallet selection', () => { })).toBe(''); }); }); + +describe('persisted Cosmos wallet isolation', () => { + it('disconnects a restored Keplr session when MetaMask is the active wallet', () => { + const persisted = JSON.stringify({ + state: { + chainWalletState: [ + { + chainName: 'lumera-testnet', + walletName: KEPLR_WALLET_NAME, + walletState: 'Connected', + account: { address: 'lumera1abc' }, + }, + { + chainName: 'lumera-testnet', + walletName: 'leap-extension', + walletState: 'Connected', + account: { address: 'lumera1def' }, + }, + ], + currentWalletName: KEPLR_WALLET_NAME, + currentChainName: 'lumera-testnet', + }, + version: 0, + }); + + expect(JSON.parse(disconnectPersistedInterchainWallet( + persisted, + KEPLR_WALLET_NAME, + ))).toEqual({ + state: { + chainWalletState: [ + { + chainName: 'lumera-testnet', + walletName: KEPLR_WALLET_NAME, + walletState: 'Disconnected', + account: null, + }, + { + chainName: 'lumera-testnet', + walletName: 'leap-extension', + walletState: 'Connected', + account: { address: 'lumera1def' }, + }, + ], + currentWalletName: '', + currentChainName: '', + }, + version: 0, + }); + }); + + it('leaves malformed and already-disconnected state untouched', () => { + expect(disconnectPersistedInterchainWallet('{invalid', KEPLR_WALLET_NAME)) + .toBe('{invalid'); + + const disconnected = JSON.stringify({ + state: { + chainWalletState: [{ + walletName: KEPLR_WALLET_NAME, + walletState: 'Disconnected', + account: null, + }], + currentWalletName: '', + }, + }); + expect(disconnectPersistedInterchainWallet(disconnected, KEPLR_WALLET_NAME)) + .toBe(disconnected); + }); +}); diff --git a/apps/web/src/utils/wallet-selection.ts b/apps/web/src/utils/wallet-selection.ts index 26b36b2..2e53c4a 100644 --- a/apps/web/src/utils/wallet-selection.ts +++ b/apps/web/src/utils/wallet-selection.ts @@ -1,5 +1,6 @@ export const METAMASK_WALLET_NAME = 'metamask'; export const KEPLR_WALLET_NAME = 'keplr-extension'; +export const INTERCHAIN_WALLET_STORAGE_KEY = 'interchain-kit-store'; export type ActiveWalletMode = 'none' | 'evm' | 'cosmos'; @@ -75,3 +76,70 @@ export const getAlternativeWalletName = ({ } return ''; }; + +interface PersistedInterchainWalletState { + state?: { + chainWalletState?: Array<{ + walletName?: string; + walletState?: string; + account?: unknown; + [key: string]: unknown; + }>; + currentWalletName?: string; + currentChainName?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export const disconnectPersistedInterchainWallet = ( + serializedState: string, + walletName: string, +) => { + let persisted: PersistedInterchainWalletState; + try { + persisted = JSON.parse(serializedState) as PersistedInterchainWalletState; + } catch { + return serializedState; + } + + const chainWalletState = persisted.state?.chainWalletState; + if (!persisted.state || !Array.isArray(chainWalletState)) return serializedState; + + let changed = false; + persisted.state.chainWalletState = chainWalletState.map((walletState) => { + if (walletState.walletName !== walletName) return walletState; + if (walletState.walletState === 'Disconnected' && walletState.account == null) { + return walletState; + } + changed = true; + return { + ...walletState, + walletState: 'Disconnected', + account: null, + }; + }); + + if (persisted.state.currentWalletName === walletName) { + persisted.state.currentWalletName = ''; + persisted.state.currentChainName = ''; + changed = true; + } + + return changed ? JSON.stringify(persisted) : serializedState; +}; + +export const suppressPersistedKeplrConnection = ( + storage: Pick, +) => { + const serializedState = storage.getItem(INTERCHAIN_WALLET_STORAGE_KEY); + if (!serializedState) return; + + const disconnectedState = disconnectPersistedInterchainWallet( + serializedState, + KEPLR_WALLET_NAME, + ); + if (disconnectedState !== serializedState) { + storage.setItem(INTERCHAIN_WALLET_STORAGE_KEY, disconnectedState); + } +}; diff --git a/packages/ui/src/screens/AccountScreen.tsx b/packages/ui/src/screens/AccountScreen.tsx index 3ba1efd..a66d5c8 100644 --- a/packages/ui/src/screens/AccountScreen.tsx +++ b/packages/ui/src/screens/AccountScreen.tsx @@ -1,17 +1,239 @@ -import { H2, Card } from 'tamagui'; -import { Construction } from '@tamagui/lucide-icons'; +import { useState } from 'react'; +import { Copy, Check, SearchX } from 'lucide-react'; +import { YStack, H2, Paragraph, Card as TamaguiCard } from 'tamagui'; +import { toast } from 'react-toastify'; + +import Card from '@/components/Card'; +import AppLink from '@/components/AppLink'; +import Skeleton from '@/components/Skeleton'; +import TransactionHistory from '@/components/TransactionHistory'; +import { AccountInfoData } from '@/hooks/useAccountInfo'; +import { ITransaction } from '@/hooks/useTransaction'; +import { formatTokenDisplay } from '@/utils/format'; +import { DENOM } from '@/contants/network'; +import { + getAvailableBalances, + getDelegations, + getRewards, + getUnbonding, + getTotalBalances, +} from '@/utils/portfolio'; + +interface ITransactionList { + transactions: ITransaction[]; + totalTransactions: number; + isLoading: boolean; + error: string; + handlePageClick: ({ selected }: { selected: number }) => void; +} + +interface IAccountScreen { + bech32Address: string; + ethAddress: string; + isValidAddress: boolean; + accountInfo: AccountInfoData | null; + isLoading: boolean; + error: string; + sentTransactions: ITransactionList; + receivedTransactions: ITransactionList; +} + +export const AccountScreen = ({ + bech32Address, + ethAddress, + isValidAddress, + accountInfo, + isLoading, + error, + sentTransactions, + receivedTransactions, +}: IAccountScreen) => { + const [copiedAddress, setCopiedAddress] = useState(''); + const [activeTab, setActiveTab] = useState<'sent' | 'received'>('sent'); + + const handleCopyAddress = async (address: string, label: string) => { + try { + await navigator.clipboard.writeText(address); + setCopiedAddress(address); + setTimeout(() => { + setCopiedAddress((currentAddress) => currentAddress === address ? '' : currentAddress); + }, 3000); + toast(`${label} copied.`, { + position: "bottom-center", + theme: "dark", + }); + } catch { + toast.error('Unable to copy the address.', { + position: "bottom-center", + theme: "dark", + }); + } + } + + if (!isValidAddress) { + return ( + + +
+
+ +
+

Invalid Account Address

+ The address in the URL is not a valid Lumera account address. +
+
+
+ ); + } + + const displayedAddresses = [ + { label: 'Bech32 address', value: bech32Address }, + { label: 'ETH hex address', value: ethAddress }, + ]; + const activeTransactions = activeTab === 'sent' ? sentTransactions : receivedTransactions; -export const AccountScreen = () => { return (
- -
-
- +
+ +

Total Balance

+
+

+ {isLoading ? + : <> + {formatTokenDisplay({ + amount: `${getTotalBalances(accountInfo)}`, + denom: DENOM, + }, false, '0,0.[00000]')} LUME + + } +

-

Coming soon

-
+ {error && !isLoading ? ( +

{error}

+ ) : null} +
    +
  • + Available: + {formatTokenDisplay({ + amount: `${getAvailableBalances(accountInfo)}`, + denom: DENOM, + }, false, '0,0.[00000]')} LUME +
  • +
  • + Staking: + {formatTokenDisplay({ + amount: `${getDelegations(accountInfo)}`, + denom: DENOM, + }, false, '0,0.[00000]')} LUME +
  • +
  • + Rewards: + {formatTokenDisplay({ + amount: `${getRewards(accountInfo)}`, + denom: DENOM, + }, false, '0,0.[00000]')} LUME +
  • +
  • + Unstaking: + {formatTokenDisplay({ + amount: `${getUnbonding(accountInfo)}`, + denom: DENOM, + }, false, '0,0.[00000]')} LUME +
  • +
+ + +

Addresses

+
+ {displayedAddresses.filter(({ value }) => value).map(({ label, value }) => ( +
+ + +
+ ))} +
+

+ These formats identify the same account. Click either address to copy it. +

+
+
+ + {accountInfo?.delegations?.length ? ( + +

Delegations

+
+
+
+
Validator
+
Amount
+
+ {accountInfo.delegations.map((item) => ( +
+
+
Validator:
+ + {item.delegation.validator_address} + +
+
+
Amount:
+ + {formatTokenDisplay(item.balance, false, '0,0.[00000]')} LUME + +
+
+ ))} +
+
+
+ ) : null} + + +

Transactions

+
    +
  • + +
  • +
  • + +
  • +
+ {activeTransactions.error && !activeTransactions.isLoading ? ( +

{activeTransactions.error}

+ ) : null} +
- ) -} + ); +}; diff --git a/packages/ui/src/screens/HomeScreen.tsx b/packages/ui/src/screens/HomeScreen.tsx index adfd59a..7a869c8 100644 --- a/packages/ui/src/screens/HomeScreen.tsx +++ b/packages/ui/src/screens/HomeScreen.tsx @@ -35,6 +35,7 @@ import { import Loading from '@/components/Loading'; import AppLink from '@/components/AppLink'; +import VersionsInfo from '@/components/VersionsInfo'; import { ConnectWalletButton } from '@/components/ConnectWallet'; import Skeleton from '@/components/Skeleton'; import CountDown from '@/components/CountDown'; @@ -1045,6 +1046,7 @@ export const HomeScreen = ({ /> } + ) } diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index 600746e..3c81b1a 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -1,45 +1,36 @@ import { useState } from 'react'; import { - XCircle, - ArrowUpRight, Copy, - Coins, Send, ArrowDown, - ArrowLeftRight, - ArrowDownLeft, Check, Layers, - ClockPlus, - Unlink, - Star, } from 'lucide-react'; -import { YStack, H2, Paragraph, Card as TamaguiCard, H3 } from 'tamagui'; -import ReactPaginate from 'react-paginate'; -import dayjs from 'dayjs'; +import { YStack, H2, Paragraph, Card as TamaguiCard } from 'tamagui'; import { Wallet } from '@tamagui/lucide-icons'; import { toast } from 'react-toastify'; -import AppLink from '@/components/AppLink'; import { ConnectWalletButton } from '@/components/ConnectWallet'; -import Loading from '@/components/Loading'; -import PastTime from '@/components/PastTime'; import Card from '@/components/Card'; import ReceiveModal from '@/components/ReceiveModal'; import DelegateModal from '@/components/DelegateModal'; import SendModal from '@/components/SendModal'; import Skeleton from '@/components/Skeleton'; import AppButton from '@/components/AppButton'; +import TransactionHistory from '@/components/TransactionHistory'; import { AccountInfoData } from '@/hooks/useAccountInfo'; import { RATE_VALUE } from '@/contants'; import { ITransaction } from '@/hooks/useTransaction'; -import { formatAddress, formatTokenDisplay } from '@/utils/format'; -import { getMessages } from '@/utils/helpers'; +import { formatTokenDisplay } from '@/utils/format'; import { IValidator } from '@/types/validator'; import { DENOM } from '@/contants/network'; -import { isTransactionSuccessful } from '@/utils/transaction-history'; - -import 'react-paginate/theme/basic/react-paginate.css'; +import { + getAvailableBalances, + getDelegations, + getRewards, + getUnbonding, + getTotalBalances, +} from '@/utils/portfolio'; interface IWalletScreen { walletAddress: string; @@ -116,135 +107,6 @@ export const WalletScreen = ({ }: IWalletScreen) => { const [copiedAddress, setCopiedAddress] = useState(''); - const getTxIcon = (type: string) => { - switch(type) { - case 'Send': - return ; - case 'Received': - return ; - case 'BeginRedelegate': - return ; - case 'Delegate': - return ; - case 'Failed': - return ; - case 'Undelegate': - return ; - default: - if (type.indexOf('WithdrawDelegatorReward') !== -1) { - return ; - } - return ; - } - }; - - const getColor = (type: string) => { - switch(type) { - case 'Send': - case 'Failed': - return 'bg-red-500/20'; - case 'Delegate': - case 'BeginRedelegate': - return 'bg-green-400/20'; - case 'Received': - return 'bg-green-500/20'; - default: - if (type.indexOf('WithdrawDelegatorReward') !== -1) { - return 'recent-activity-icon'; - } - return 'bg-red-500/20'; - } - } - - const getTotalBalances = () => { - let total = 0; - if (accountInfo?.balances?.length) { - for (const item of accountInfo?.balances) { - if (item.denom === DENOM) { - total += Number(item.amount); - } - if (item.denom === 'lume') { - total += Number(item.amount) * RATE_VALUE; - } - } - } - if (accountInfo?.delegations?.length) { - for (const item of accountInfo?.delegations) { - if (item.balance.denom === DENOM) { - total += Number(item.balance.amount); - } - if (item.balance.denom === 'lume') { - total += Number(item.balance.amount) * RATE_VALUE; - } - } - } - - return total; - } - - const getAvailableBalances = () => { - let total = 0; - if (accountInfo?.balances?.length) { - for (const item of accountInfo?.balances) { - if (item.denom === DENOM) { - total += Number(item.amount); - } - if (item.denom === 'lume') { - total += Number(item.amount) * RATE_VALUE; - } - } - } - - return total; - } - - const getDelegations = () => { - let total = 0; - if (accountInfo?.delegations?.length) { - for (const item of accountInfo?.delegations) { - if (item.balance.denom === DENOM) { - total += Number(item.balance.amount); - } - if (item.balance.denom === 'lume') { - total += Number(item.balance.amount) * RATE_VALUE; - } - } - } - - return total; - } - - const getRewards = () => { - let total = 0; - if (accountInfo?.rewards?.length) { - for (const item of accountInfo?.rewards) { - for (const reward of item.reward) { - if (reward.denom === DENOM) { - total += Number(reward.amount); - } - if (reward.denom === 'lume') { - total += Number(reward.amount) * RATE_VALUE; - } - } - } - } - - return total; - } - - const getUnbonding = () => { - let total = 0; - if (accountInfo?.unbonding?.length) { - for (const item of accountInfo?.unbonding) { - for (const reward of item.entries) { - total += Number(reward.balance); - } - } - } - - return total; - } - const handleCopyAddress = async (address: string, label: string) => { try { await navigator.clipboard.writeText(address); @@ -301,7 +163,7 @@ export const WalletScreen = ({ : <> {formatTokenDisplay({ - amount: `${getTotalBalances()}`, + amount: `${getTotalBalances(accountInfo)}`, denom: DENOM, }, false, '0,0.[00000]')} LUME @@ -352,28 +214,28 @@ export const WalletScreen = ({
  • Available: {formatTokenDisplay({ - amount: `${getAvailableBalances()}`, + amount: `${getAvailableBalances(accountInfo)}`, denom: DENOM, }, false, '0,0.[00000]')} LUME
  • {!isEvm ?
  • Staking: {formatTokenDisplay({ - amount: `${getDelegations()}`, + amount: `${getDelegations(accountInfo)}`, denom: DENOM, }, false, '0,0.[00000]')} LUME
  • : null} {!isEvm ?
  • Rewards: {formatTokenDisplay({ - amount: `${getRewards()}`, + amount: `${getRewards(accountInfo)}`, denom: DENOM, }, false, '0,0.[00000]')} LUME
  • : null} {!isEvm ?
  • Unstaking: {formatTokenDisplay({ - amount: `${getUnbonding()}`, + amount: `${getUnbonding(accountInfo)}`, denom: DENOM, }, false, '0,0.[00000]')} LUME
  • : null} @@ -450,83 +312,12 @@ export const WalletScreen = ({

    Transaction History

    -
    - -
    -
    -
    -
    - Block Height -
    -
    - TX Hash -
    -
    - Transaction Type -
    -
    - Transaction Status -
    -
    - Time -
    -
    - {transactions.map((tx) => ( -
    -
    -
    Block Height:
    -
    -
    - {getTxIcon(getMessages(tx.tx.body.messages))} -
    - {tx.height} -
    -
    -
    -
    TX Hash:
    - - {formatAddress(tx.txhash, 10, -4)} - -
    -
    -
    Transaction Type:
    - {getMessages(tx.tx.body.messages)} -
    -
    -
    Transaction Status:
    - - {isTransactionSuccessful(tx) ? 'Success' : 'Failed'} - -
    -
    -
    Time:
    - {dayjs(tx.timestamp).format('MMMM DD, YYYY')} at {dayjs(tx.timestamp).format('HH:mm:ss')} - () -
    -
    - ))} - {!transactions?.length && !isLoading ? -
    -

    No Transactions

    -
    : null - } -
    -
    - {totalTransactions > 1 ? -
    - -
    : null - } -
    +
    ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72d04f8..68caf29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,9 @@ importers: react-native-web: specifier: ^0.21.0 version: 0.21.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-paginate: + specifier: ^8.3.0 + version: 8.3.0(react@19.1.0) react-qr-code: specifier: ^2.0.18 version: 2.0.18(react@19.1.0) @@ -243,6 +246,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.1.7(@types/react@19.0.14) + '@types/react-paginate': + specifier: ^7.1.4 + version: 7.1.4 eslint: specifier: ^9 version: 9.33.0(jiti@2.5.1) From 1c06974e31412f0c2df3980f0a9f46f893ab5653 Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:28:31 -0400 Subject: [PATCH 28/45] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- turbo.json | 1 + 1 file changed, 1 insertion(+) diff --git a/turbo.json b/turbo.json index 804d97d..3666338 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,7 @@ "NEXT_PUBLIC_COSMOS_EIP712_ENABLED", "NEXT_PUBLIC_DENOM", "NEXT_PUBLIC_EVM_CHAIN_ID", + "NEXT_PUBLIC_EVM_PROFILE_NAME", "NEXT_PUBLIC_EVM_RPC_ENDPOINT", "NEXT_PUBLIC_EVM_WS_ENDPOINT", "NEXT_PUBLIC_IPAPI_KEY", From c27821a1d0d6f646835b626c60a3804f9a63755c Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:14:20 -0400 Subject: [PATCH 29/45] Update packages/ui/src/screens/HomeScreen.tsx Co-authored-by: Kullat Nunu --- packages/ui/src/screens/HomeScreen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/screens/HomeScreen.tsx b/packages/ui/src/screens/HomeScreen.tsx index 7a869c8..7d26ebd 100644 --- a/packages/ui/src/screens/HomeScreen.tsx +++ b/packages/ui/src/screens/HomeScreen.tsx @@ -812,9 +812,7 @@ export const HomeScreen = ({ const handleVotePress = (item: IProposal) => { handleResetError(); const currentVoteValue = getGovernanceVoteValue(userVotes[item.id]); - if (currentVoteValue) { - onOptionChange(currentVoteValue); - } + onOptionChange(currentVoteValue || '1'); setVoteOpen(true); setSelectedItem(item); } From 6160acc6369672951e9406cda9a789338648179f Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:14:33 -0400 Subject: [PATCH 30/45] Update packages/ui/src/screens/GovernanceScreen.tsx Co-authored-by: Kullat Nunu --- packages/ui/src/screens/GovernanceScreen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/screens/GovernanceScreen.tsx b/packages/ui/src/screens/GovernanceScreen.tsx index fd5b423..b2f1255 100644 --- a/packages/ui/src/screens/GovernanceScreen.tsx +++ b/packages/ui/src/screens/GovernanceScreen.tsx @@ -284,9 +284,7 @@ export const GovernanceScreen = ({ const handleVotePress = (item: IProposal) => { handleResetError(); const currentVoteValue = getGovernanceVoteValue(userVotes[item.id]); - if (currentVoteValue) { - onOptionChange(currentVoteValue); - } + onOptionChange(currentVoteValue || '1'); setVoteOpen(true); setSelectedItem(item); } From b88b844d762410c84f29cff550c2b7ca31b2cf75 Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:14:42 -0400 Subject: [PATCH 31/45] Update packages/ui/src/screens/GovernanceDetailsScreen.tsx Co-authored-by: Kullat Nunu --- packages/ui/src/screens/GovernanceDetailsScreen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/screens/GovernanceDetailsScreen.tsx b/packages/ui/src/screens/GovernanceDetailsScreen.tsx index 1999a60..9b0bdac 100644 --- a/packages/ui/src/screens/GovernanceDetailsScreen.tsx +++ b/packages/ui/src/screens/GovernanceDetailsScreen.tsx @@ -204,9 +204,7 @@ export const GovernanceDetailsScreen = ({ const handleVotePress = () => { vote.handleResetError(); const currentVoteValue = getGovernanceVoteValue(vote.currentVote); - if (currentVoteValue) { - vote.onOptionChange(currentVoteValue); - } + vote.onOptionChange(currentVoteValue || '1'); vote.setVoteOpen(true); } From 1b84bc9eb98a5002a8553be4f29be0cd5bd46159 Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:14:58 -0400 Subject: [PATCH 32/45] Update apps/web/src/utils/staking-overview-cache.ts Co-authored-by: Kullat Nunu --- apps/web/src/utils/staking-overview-cache.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/utils/staking-overview-cache.ts b/apps/web/src/utils/staking-overview-cache.ts index 88edf2d..418d835 100644 --- a/apps/web/src/utils/staking-overview-cache.ts +++ b/apps/web/src/utils/staking-overview-cache.ts @@ -32,6 +32,7 @@ export interface StakingOverviewCache { } export const STAKING_AUTO_REFRESH_INTERVAL_MS = 5 * 60 * 1000; +export const STAKING_REFRESH_RETRY_DELAY_MS = 30 * 1000; interface CacheStorage { getItem: (key: string) => string | null; From 4b94675d4911bd9eb78ac2e75edcc6d1867b5cc6 Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:15:05 -0400 Subject: [PATCH 33/45] Update apps/web/src/hooks/useStaking.ts Co-authored-by: Kullat Nunu --- apps/web/src/hooks/useStaking.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index b579a13..b1a4c1b 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -15,6 +15,7 @@ import { writeStakingOverviewCache, getStakingRefreshProgress, getStakingAutoRefreshDelay, + STAKING_REFRESH_RETRY_DELAY_MS, type StakingOverviewCache, type StakingParams, type SlashingParams, From f9edb26d5a10a75b80ec0e96b78c029575acd35b Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:27:35 -0400 Subject: [PATCH 34/45] Update apps/web/src/hooks/useStaking.ts Co-authored-by: Kullat Nunu --- apps/web/src/hooks/useStaking.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index b1a4c1b..d901366 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -64,6 +64,7 @@ const useStaking = (address = '', isEvm = false) => { const [refreshProgress, setRefreshProgress] = useState(0); const [lastUpdated, setLastUpdated] = useState(null); const [isCacheReady, setCacheReady] = useState(false); + const [refreshAttempt, setRefreshAttempt] = useState(0); const refreshingRef = useRef(false); const initializedRef = useRef(false); const [rewards, setRewards] = useState([]); From 7ff0611da50a25d4f2a6896a339ea8b7c272121d Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:27:46 -0400 Subject: [PATCH 35/45] Update apps/web/src/hooks/useStaking.ts Co-authored-by: Kullat Nunu --- apps/web/src/hooks/useStaking.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index d901366..a58ed14 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -188,9 +188,11 @@ const useStaking = (address = '', isEvm = false) => { applyOverview(updatedOverview); writeStakingOverviewCache(window.localStorage, CHAIN_ID, updatedOverview); + setRefreshAttempt(0); } catch (refreshError) { setError(refreshError instanceof Error ? refreshError.message : 'Unable to update staking data.'); setHasLoadedOverview(true); + setRefreshAttempt((attempt) => attempt + 1); } finally { setRefreshing(false); refreshingRef.current = false; From 87a6b81ed10fe73518d53a17db09db0008c41da0 Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:27:53 -0400 Subject: [PATCH 36/45] Update apps/web/src/hooks/useStaking.ts Co-authored-by: Kullat Nunu --- apps/web/src/hooks/useStaking.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index a58ed14..88acf7b 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -295,15 +295,18 @@ const useStaking = (address = '', isEvm = false) => { if (cachedOverview) applyOverview(cachedOverview); setCacheReady(true); }, [applyOverview]); - useEffect(() => { if (!isCacheReady) return; const refreshTimer = window.setTimeout(() => { void refreshOverview(); - }, getStakingAutoRefreshDelay(lastUpdated)); + }, Math.max( + getStakingAutoRefreshDelay(lastUpdated), + refreshAttempt > 0 ? STAKING_REFRESH_RETRY_DELAY_MS : 0, + )); return () => window.clearTimeout(refreshTimer); + }, [isCacheReady, lastUpdated, refreshAttempt, refreshOverview]); }, [isCacheReady, lastUpdated, refreshOverview]); useEffect(() => { From f2bfdfa4ff027d07deb0ba2e028f239e3c004b4f Mon Sep 17 00:00:00 2001 From: a-ok123 <54385956+a-ok123@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:28:02 -0400 Subject: [PATCH 37/45] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- apps/web/src/hooks/useStaking.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index 88acf7b..a214e8d 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -15,7 +15,6 @@ import { writeStakingOverviewCache, getStakingRefreshProgress, getStakingAutoRefreshDelay, - STAKING_REFRESH_RETRY_DELAY_MS, type StakingOverviewCache, type StakingParams, type SlashingParams, From 78584ef9003e4e5bf9422456bba92b298b1ca5af Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Mon, 17 Aug 2026 12:13:18 -0400 Subject: [PATCH 38/45] repair staking auto-refresh fix and cover it with tests Applying the review suggestions for the staking auto-refresh retry left the branch unable to compile: the dependency-array suggestion was added without replacing the original line, and the follow-up autofix then dropped the STAKING_REFRESH_RETRY_DELAY_MS import that the new delay calculation needs. Remove the duplicated dependency array and restore the import, then add regression tests for the behaviour the fix was meant to deliver: a failed refresh re-arms the timer instead of dying, retries persist across repeated failures, and a success resets the attempt counter so the long cadence resumes. Verified by reverting each half of the fix in turn - dropping the refreshAttempt dependency fails all three tests, and dropping the retry floor fails the tight-loop assertion. These are the first React-rendering tests in the app, so jsdom is opted into per file via a docblock and the other suites keep the node environment. window.localStorage is stubbed because Node defines a localStorage global that stays undefined without --localstorage-file, which vitest's jsdom environment will not shadow. Co-Authored-By: Claude Opus 5 (1M context) --- apps/web/package.json | 3 + apps/web/src/hooks/useStaking.test.ts | 157 ++++++++++ apps/web/src/hooks/useStaking.ts | 2 +- pnpm-lock.yaml | 406 +++++++++++++++++++++++++- 4 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/hooks/useStaking.test.ts diff --git a/apps/web/package.json b/apps/web/package.json index d0f8a01..d3edaaf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -56,6 +56,8 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.11", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/numeral": "^2.0.5", "@types/react": "^19", @@ -63,6 +65,7 @@ "@types/react-paginate": "^7.1.4", "eslint": "^9", "eslint-config-next": "15.4.6", + "jsdom": "^30.0.1", "typescript": "^5", "vitest": "^3.2.4" }, diff --git a/apps/web/src/hooks/useStaking.test.ts b/apps/web/src/hooks/useStaking.test.ts new file mode 100644 index 0000000..b7908f7 --- /dev/null +++ b/apps/web/src/hooks/useStaking.test.ts @@ -0,0 +1,157 @@ +// @vitest-environment jsdom +import { act, renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + STAKING_AUTO_REFRESH_INTERVAL_MS, + STAKING_REFRESH_RETRY_DELAY_MS, +} from '@/utils/staking-overview-cache'; + +const { get } = vi.hoisted(() => ({ get: vi.fn() })); + +vi.mock('@/utils/api', () => ({ + get, + getExternal: vi.fn(), + post: vi.fn(), + put: vi.fn(), + remove: vi.fn(), + upload: vi.fn(), +})); + +vi.mock('@/redux/hooks', () => ({ + useDispatch: () => vi.fn(), + useSelector: (select: (state: unknown) => unknown) => select({ + app: { + activeView: 'dashboard', + currentPath: '/', + viewTitle: '', + currentTab: 'active', + validatorTab: 'all', + subTab: 'delegations', + }, + }), +})); + +import useStaking from './useStaking'; + +const PUBLIC_REQUESTS_PER_REFRESH = 10; + +const publicOverviewResponse = (path: string) => { + if (path.includes('BOND_STATUS_UNBONDING')) return { data: { validators: [] } }; + if (path.includes('BOND_STATUS_UNBONDED')) return { data: { validators: [] } }; + if (path.includes('/staking/v1beta1/validators')) { + return { data: { validators: [], pagination: { total: '0' } } }; + } + if (path.includes('/staking/v1beta1/params')) { + return { data: { params: { bond_denom: 'ulume', unbonding_time: '1814400s', max_validators: 100 } } }; + } + if (path.includes('/slashing/v1beta1/params')) { + return { + data: { + params: { + signed_blocks_window: '100', + min_signed_per_window: '0.05', + downtime_jail_duration: '600s', + slash_fraction_double_sign: '0.05', + slash_fraction_downtime: '0.01', + }, + }, + }; + } + if (path.includes('signing_infos')) return { data: { info: [] } }; + if (path.includes('/mint/v1beta1/inflation')) return { data: { inflation: '0.1' } }; + if (path.includes('/staking/v1beta1/pool')) return { data: { pool: { bonded_tokens: '1000000' } } }; + if (path.includes('/bank/v1beta1/supply')) { + return { data: { supply: [{ denom: 'ulume', amount: '5000000' }] } }; + } + if (path.includes('/distribution/v1beta1/params')) { + return { data: { params: { community_tax: '0.02' } } }; + } + throw new Error(`unexpected request path: ${path}`); +}; + +const advance = async (ms: number) => { + await act(async () => { + await vi.advanceTimersByTimeAsync(ms); + }); +}; + +// Node exposes a `localStorage` global that stays undefined without +// `--localstorage-file`, and vitest's jsdom environment will not shadow an +// existing globalThis key — so `window.localStorage` needs stubbing here. +const createMemoryStorage = (): Storage => { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { store.set(key, value); }, + removeItem: (key: string) => { store.delete(key); }, + clear: () => { store.clear(); }, + key: (index: number) => Array.from(store.keys())[index] ?? null, + get length() { return store.size; }, + } as Storage; +}; + +describe('useStaking auto-refresh recovery', () => { + beforeEach(() => { + vi.useFakeTimers(); + Object.defineProperty(window, 'localStorage', { + value: createMemoryStorage(), + configurable: true, + writable: true, + }); + get.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('re-arms the auto-refresh timer after a failed refresh', async () => { + get.mockRejectedValue(new Error('LCD unavailable')); + + renderHook(() => useStaking()); + await advance(0); + + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + + // Waits out the retry floor instead of tight-looping against the LCD. + await advance(STAKING_REFRESH_RETRY_DELAY_MS - 1); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + + await advance(1); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH * 2); + }); + + it('keeps retrying while refreshes keep failing', async () => { + get.mockRejectedValue(new Error('LCD unavailable')); + + renderHook(() => useStaking()); + await advance(0); + + for (let attempt = 2; attempt <= 4; attempt += 1) { + await advance(STAKING_REFRESH_RETRY_DELAY_MS); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH * attempt); + } + }); + + it('returns to the long refresh cadence once a refresh succeeds', async () => { + get.mockRejectedValue(new Error('LCD unavailable')); + + renderHook(() => useStaking()); + await advance(0); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + + get.mockReset(); + get.mockImplementation(async (path: string) => publicOverviewResponse(path)); + + await advance(STAKING_REFRESH_RETRY_DELAY_MS); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + + // A successful refresh clears the attempt counter, so the retry floor lifts. + await advance(STAKING_REFRESH_RETRY_DELAY_MS); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + + await advance(STAKING_AUTO_REFRESH_INTERVAL_MS); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH * 2); + }); +}); diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index a214e8d..f190ace 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -15,6 +15,7 @@ import { writeStakingOverviewCache, getStakingRefreshProgress, getStakingAutoRefreshDelay, + STAKING_REFRESH_RETRY_DELAY_MS, type StakingOverviewCache, type StakingParams, type SlashingParams, @@ -306,7 +307,6 @@ const useStaking = (address = '', isEvm = false) => { return () => window.clearTimeout(refreshTimer); }, [isCacheReady, lastUpdated, refreshAttempt, refreshOverview]); - }, [isCacheReady, lastUpdated, refreshOverview]); useEffect(() => { if (canQueryCosmosAccountData({ address, isEvmNetwork: isEvm })) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68caf29..bbf5022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,12 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.11 version: 4.1.11 + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.0.14))(@types/react@19.0.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/node': specifier: ^20 version: 20.19.10 @@ -255,12 +261,15 @@ importers: eslint-config-next: specifier: 15.4.6 version: 15.4.6(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) + jsdom: + specifier: ^30.0.1 + version: 30.0.1(@noble/hashes@2.3.0) typescript: specifier: ^5 version: 5.8.3 vitest: specifier: ^3.2.4 - version: 3.2.7(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + version: 3.2.7(@types/node@20.19.10)(jiti@2.5.1)(jsdom@30.0.1(@noble/hashes@2.3.0))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) packages/core: dependencies: @@ -377,6 +386,14 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} + '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -874,6 +891,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bufbuild/protobuf@2.10.1': resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==} @@ -973,6 +994,42 @@ packages: '@cosmjs/utils@0.39.0': resolution: {integrity: sha512-h7fy7Tbcl9v8ABntp8+kqw2VmUus2HbnRJFyzTkM7byRktLtECHYNMsztwyl1rdHkzc8Nc2xs6K/56d7a+75aw==} + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.0': + resolution: {integrity: sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.8': + resolution: {integrity: sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@egjs/hammerjs@2.0.17': resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} @@ -1396,6 +1453,15 @@ packages: '@ethersproject/web@5.8.0': resolution: {integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@expo/cli@0.24.20': resolution: {integrity: sha512-uF1pOVcd+xizNtVTuZqNGzy7I6IJon5YMmQidsURds1Ww96AFDxrR/NEACqeATNAmY60m8wy1VZZpSg5zLNkpw==} hasBin: true @@ -3964,9 +4030,31 @@ packages: engines: {node: '>= 10'} hasBin: true + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4550,6 +4638,9 @@ packages: resolution: {integrity: sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==} engines: {node: '>= 6.0.0'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -4727,6 +4818,9 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -5142,6 +5236,10 @@ packages: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -5157,6 +5255,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -5279,6 +5381,10 @@ packages: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -5306,6 +5412,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -5406,6 +5515,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-editor@0.4.2: resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} engines: {node: '>=8'} @@ -6091,6 +6204,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} engines: {node: '>=12'} @@ -6297,6 +6414,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-primitive@3.0.1: resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} engines: {node: '>=0.10.0'} @@ -6464,6 +6584,15 @@ packages: jsc-safe-url@0.2.4: resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -6765,6 +6894,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6773,6 +6906,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -6796,6 +6933,9 @@ packages: mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-query-parser@2.0.2: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} @@ -7281,6 +7421,9 @@ packages: resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} engines: {node: '>=10'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -7485,6 +7628,10 @@ packages: pretty-error@4.0.0: resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7625,6 +7772,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -7984,6 +8134,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -8346,6 +8500,9 @@ packages: resolution: {integrity: sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==} engines: {node: '>=0.10'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -8466,6 +8623,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -8484,9 +8648,17 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -8622,6 +8794,10 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -8898,6 +9074,10 @@ packages: w-json@1.3.11: resolution: {integrity: sha512-Xa8vTinB5XBIYZlcN8YyHpE625pBU6k+lvCetTQM+FKxRtLJxAY9zUVZbRqCqkMeEGbQpKvGUzwh4wZKGem+ag==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -8918,6 +9098,10 @@ packages: resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} engines: {node: '>=8'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-sources@1.4.3: resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} @@ -8938,10 +9122,22 @@ packages: whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url-without-unicode@8.0.0-3: resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} engines: {node: '>=10'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -9059,6 +9255,10 @@ packages: resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} engines: {node: '>=10.0.0'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml2js@0.6.0: resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} engines: {node: '>=4.0.0'} @@ -9071,6 +9271,9 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xstream@11.14.0: resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==} @@ -9160,6 +9363,21 @@ snapshots: '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -9762,6 +9980,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@bufbuild/protobuf@2.10.1': {} '@chain-registry/keplr@2.0.42': @@ -9986,6 +10208,30 @@ snapshots: '@cosmjs/utils@0.39.0': {} + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.8(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@egjs/hammerjs@2.0.17': dependencies: '@types/hammerjs': 2.0.46 @@ -10327,6 +10573,10 @@ snapshots: '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 + '@exodus/bytes@1.15.1(@noble/hashes@2.3.0)': + optionalDependencies: + '@noble/hashes': 2.3.0 + '@expo/cli@0.24.20(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@0no-co/graphql.web': 1.2.0 @@ -16894,11 +17144,34 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.7.1 '@tauri-apps/cli-win32-x64-msvc': 2.7.1 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.0.14))(@types/react@19.0.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.2 + '@testing-library/dom': 10.4.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.0.14 + '@types/react-dom': 19.1.7(@types/react@19.0.14) + '@tybys/wasm-util@0.10.0': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.0 @@ -17752,6 +18025,10 @@ snapshots: leven: 2.1.0 mri: 1.1.4 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -18009,6 +18286,10 @@ snapshots: dependencies: open: 8.4.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big-integer@1.6.52: {} big.js@5.2.2: {} @@ -18472,6 +18753,11 @@ snapshots: mdn-data: 2.0.14 source-map: 0.6.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.2.2: {} cssesc@3.0.0: {} @@ -18480,6 +18766,13 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-urls@7.0.0(@noble/hashes@2.3.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.3.0) + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -18566,6 +18859,8 @@ snapshots: dependency-graph@1.0.0: {} + dequal@2.0.3: {} + destr@2.0.5: {} destroy@1.2.0: {} @@ -18584,6 +18879,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -18705,6 +19002,8 @@ snapshots: entities@4.5.0: {} + entities@8.0.0: {} + env-editor@0.4.2: {} error-ex@1.3.2: @@ -19651,6 +19950,12 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-encoding-sniffer@6.0.0(@noble/hashes@2.3.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0) + transitivePeerDependencies: + - '@noble/hashes' + html-minifier-terser@6.1.0: dependencies: camel-case: 4.1.2 @@ -19892,6 +20197,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-potential-custom-element-name@1.0.1: {} + is-primitive@3.0.1: {} is-regex@1.2.1: @@ -20105,6 +20412,32 @@ snapshots: jsc-safe-url@0.2.4: {} + jsdom@30.0.1(@noble/hashes@2.3.0): + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.8(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.3.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.3.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.0(@noble/hashes@2.3.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.0.2: {} jsesc@3.1.0: {} @@ -20343,6 +20676,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -20351,6 +20686,8 @@ snapshots: dependencies: react: 19.1.0 + lz-string@1.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 @@ -20375,6 +20712,8 @@ snapshots: mdn-data@2.0.14: {} + mdn-data@2.27.1: {} + media-query-parser@2.0.2: dependencies: '@babel/runtime': 7.28.2 @@ -21105,6 +21444,10 @@ snapshots: dependencies: pngjs: 3.4.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -21320,6 +21663,12 @@ snapshots: lodash: 4.17.21 renderkid: 3.0.0 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -21579,6 +21928,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-is@19.1.1: {} @@ -22259,6 +22610,10 @@ snapshots: sax@1.4.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.25.0: {} scheduler@0.26.0: {} @@ -22703,6 +23058,8 @@ snapshots: symbol-observable@2.0.3: {} + symbol-tree@3.2.4: {} + tabbable@6.2.0: {} table@6.9.0: @@ -22958,6 +23315,12 @@ snapshots: tinyspy@4.0.4: {} + tldts-core@7.4.10: {} + + tldts@7.4.10: + dependencies: + tldts-core: 7.4.10 + tmpl@1.0.5: {} to-buffer@1.2.1: @@ -22974,8 +23337,16 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.10 + tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -23102,6 +23473,8 @@ snapshots: undici@6.21.3: {} + undici@8.10.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -23290,7 +23663,7 @@ snapshots: terser: 5.43.1 yaml: 2.8.1 - vitest@3.2.7(@types/node@20.19.10)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): + vitest@3.2.7(@types/node@20.19.10)(jiti@2.5.1)(jsdom@30.0.1(@noble/hashes@2.3.0))(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -23317,6 +23690,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.10 + jsdom: 30.0.1(@noble/hashes@2.3.0) transitivePeerDependencies: - jiti - less @@ -23337,6 +23711,10 @@ snapshots: w-json@1.3.11: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -23356,6 +23734,8 @@ snapshots: webidl-conversions@5.0.0: {} + webidl-conversions@8.0.1: {} + webpack-sources@1.4.3: dependencies: source-list-map: 2.0.1 @@ -23397,12 +23777,30 @@ snapshots: whatwg-fetch@3.6.20: {} + whatwg-mimetype@5.0.0: {} + whatwg-url-without-unicode@8.0.0-3: dependencies: buffer: 5.7.1 punycode: 2.3.1 webidl-conversions: 5.0.0 + whatwg-url@16.0.1(@noble/hashes@2.3.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.0(@noble/hashes@2.3.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -23517,6 +23915,8 @@ snapshots: simple-plist: 1.3.1 uuid: 7.0.3 + xml-name-validator@5.0.0: {} + xml2js@0.6.0: dependencies: sax: 1.4.1 @@ -23526,6 +23926,8 @@ snapshots: xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} + xstream@11.14.0: dependencies: globalthis: 1.0.4 From 0661d19fd81df46a1601cafa9ed163b35a89a7ac Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Mon, 17 Aug 2026 12:27:00 -0400 Subject: [PATCH 39/45] handle unavailable staking cache storage --- apps/web/src/hooks/useStaking.test.ts | 18 ++++++++++++++++++ apps/web/src/hooks/useStaking.ts | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useStaking.test.ts b/apps/web/src/hooks/useStaking.test.ts index b7908f7..23bcbb2 100644 --- a/apps/web/src/hooks/useStaking.test.ts +++ b/apps/web/src/hooks/useStaking.test.ts @@ -154,4 +154,22 @@ describe('useStaking auto-refresh recovery', () => { await advance(STAKING_AUTO_REFRESH_INTERVAL_MS); expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH * 2); }); + + it('refreshes normally when browser storage access is blocked', async () => { + Object.defineProperty(window, 'localStorage', { + get: () => { throw new DOMException('Access denied', 'SecurityError'); }, + configurable: true, + }); + get.mockImplementation(async (path: string) => publicOverviewResponse(path)); + + const { result } = renderHook(() => useStaking()); + await advance(0); + + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + expect(result.current.error).toBe(''); + expect(result.current.isLoading).toBe(false); + + await advance(STAKING_REFRESH_RETRY_DELAY_MS); + expect(get).toHaveBeenCalledTimes(PUBLIC_REQUESTS_PER_REFRESH); + }); }); diff --git a/apps/web/src/hooks/useStaking.ts b/apps/web/src/hooks/useStaking.ts index f190ace..18bc319 100644 --- a/apps/web/src/hooks/useStaking.ts +++ b/apps/web/src/hooks/useStaking.ts @@ -49,6 +49,16 @@ interface ValidatorsResponse { pagination?: { total?: string }; } +const getBrowserLocalStorage = (): Storage | null => { + if (typeof window === 'undefined') return null; + + try { + return window.localStorage ?? null; + } catch { + return null; + } +}; + const useStaking = (address = '', isEvm = false) => { const dispatch = useDispatch(); const { currentTab, validatorTab, subTab } = useSelector((state) => state.app); @@ -187,7 +197,8 @@ const useStaking = (address = '', isEvm = false) => { }; applyOverview(updatedOverview); - writeStakingOverviewCache(window.localStorage, CHAIN_ID, updatedOverview); + const storage = getBrowserLocalStorage(); + if (storage) writeStakingOverviewCache(storage, CHAIN_ID, updatedOverview); setRefreshAttempt(0); } catch (refreshError) { setError(refreshError instanceof Error ? refreshError.message : 'Unable to update staking data.'); @@ -291,7 +302,10 @@ const useStaking = (address = '', isEvm = false) => { if (initializedRef.current) return; initializedRef.current = true; - const cachedOverview = readStakingOverviewCache(window.localStorage, CHAIN_ID); + const storage = getBrowserLocalStorage(); + const cachedOverview = storage + ? readStakingOverviewCache(storage, CHAIN_ID) + : null; if (cachedOverview) applyOverview(cachedOverview); setCacheReady(true); }, [applyOverview]); From 5d204ca740a8ad8ed0c91c59e24b59d803430bbc Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Mon, 17 Aug 2026 12:46:34 -0400 Subject: [PATCH 40/45] fix portfolio tooltip units, degenerate heights, and EVM sync races Addresses the three low-severity review findings on this branch. The portfolio pie carries micro-denom totals, so the default tooltip printed raw micro-LUME beside figures that were already formatted. Format the tooltip value through the same helper the surrounding labels use, so the two cannot drift apart. Global search treated any digit string as a block height, routing "0" and "007" to /block/0 and /block/007. Strip leading zeros and reject an all-zero height instead. The EVM wallet sync had two defects. Overlapping accountsChanged and chainChanged events each started a multi-round-trip sync, so the slowest response won and could restore state a newer event had already superseded; a sequence counter now discards stale results, and a cancelled flag stops a sync that resolves after unmount. Separately, any verification failure cleared the connected address, so a transient RPC error made the wallet look disconnected. Distinguish the stages: a missing account or a genuine EvmNetworkMismatchError still clears, while an unverifiable network keeps the address and surfaces the reason. This does not weaken signing safety, because the send path re-runs the same check via ensureNetwork before broadcasting. The provider error state was also rendered nowhere, leaving the header simply looking disconnected with no explanation. Show it as an alert chip that stays visible whether or not an address survived, collapsing to icon-only on narrow screens. Verified by reverting the provider fix: the race test then reports the stale address winning, and the transient-failure test reports a cleared address. The mismatch and missing-account tests pass either way by design, pinning the behaviour the fix had to preserve. Co-Authored-By: Claude Opus 5 (1M context) --- .../app/providers/evm-wallet-provider.test.ts | 160 ++++++++++++++++++ .../src/app/providers/evm-wallet-provider.tsx | 50 +++++- .../src/components/ConnectWallet.module.css | 30 ++++ apps/web/src/components/ConnectWallet.tsx | 13 +- apps/web/src/utils/portfolio.test.ts | 16 ++ apps/web/src/utils/portfolio.ts | 10 ++ apps/web/src/utils/search.test.ts | 10 ++ apps/web/src/utils/search.ts | 5 +- packages/ui/src/screens/HomeScreen.tsx | 18 +- 9 files changed, 303 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/providers/evm-wallet-provider.test.ts diff --git a/apps/web/src/app/providers/evm-wallet-provider.test.ts b/apps/web/src/app/providers/evm-wallet-provider.test.ts new file mode 100644 index 0000000..769e482 --- /dev/null +++ b/apps/web/src/app/providers/evm-wallet-provider.test.ts @@ -0,0 +1,160 @@ +// @vitest-environment jsdom +import { act, renderHook } from '@testing-library/react'; +import { createElement, type ReactNode } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// The default profile is mainnet, which has no EVM endpoints, so the wallet +// sync effect would bail out before any of this behaviour runs. +process.env.NEXT_PUBLIC_NETWORK_PROFILE = 'testnet'; + +const mocks = vi.hoisted(() => ({ + getEvmAccountForChain: vi.fn(), + assertEvmProviderMatchesRpc: vi.fn(), +})); + +vi.mock('@/utils/evm', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getEvmAccountForChain: mocks.getEvmAccountForChain, + assertEvmProviderMatchesRpc: mocks.assertEvmProviderMatchesRpc, + }; +}); + +const { EvmNetworkMismatchError } = await import('@/utils/evm'); +const { EvmWalletProvider, useEvmWallet } = await import('./evm-wallet-provider'); + +const ADDRESS_A = '0x1111111111111111111111111111111111111111'; +const ADDRESS_B = '0x2222222222222222222222222222222222222222'; + +const deferred = () => { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + +const createFakeProvider = () => { + const listeners = new Map void>>(); + return { + isMetaMask: true, + request: vi.fn(async () => undefined), + on: (event: string, handler: () => void) => { + listeners.set(event, [...(listeners.get(event) || []), handler]); + }, + removeListener: (event: string, handler: () => void) => { + listeners.set(event, (listeners.get(event) || []).filter((item) => item !== handler)); + }, + emit: (event: string) => { + (listeners.get(event) || []).forEach((handler) => handler()); + }, + }; +}; + +let fakeProvider: ReturnType; + +const wrapper = ({ children }: { children: ReactNode }) => + createElement(EvmWalletProvider, null, children); + +const renderWallet = () => renderHook(() => useEvmWallet(), { wrapper }); + +const flush = async () => { + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); +}; + +describe('EvmWalletProvider account sync', () => { + beforeEach(() => { + fakeProvider = createFakeProvider(); + (window as unknown as { ethereum: unknown }).ethereum = fakeProvider; + mocks.getEvmAccountForChain.mockReset(); + mocks.assertEvmProviderMatchesRpc.mockReset(); + mocks.assertEvmProviderMatchesRpc.mockResolvedValue(undefined); + }); + + afterEach(() => { + delete (window as unknown as { ethereum?: unknown }).ethereum; + }); + + it('ignores a superseded sync that resolves after a newer one', async () => { + const slowSync = deferred(); + const fastSync = deferred(); + mocks.getEvmAccountForChain + .mockReturnValueOnce(slowSync.promise) + .mockReturnValueOnce(fastSync.promise); + + const { result } = renderWallet(); + await flush(); + + // A wallet event starts a second sync while the first is still in flight. + await act(async () => { + fakeProvider.emit('accountsChanged'); + }); + + // The newer sync resolves first and wins. + fastSync.resolve(ADDRESS_B); + await flush(); + expect(result.current.address).toBe(ADDRESS_B); + + // The older, slower sync must not clobber it on arrival. + slowSync.resolve(ADDRESS_A); + await flush(); + expect(result.current.address).toBe(ADDRESS_B); + }); + + it('keeps the connected address when verification fails transiently', async () => { + mocks.getEvmAccountForChain.mockResolvedValue(ADDRESS_A); + + const { result } = renderWallet(); + await flush(); + expect(result.current.address).toBe(ADDRESS_A); + + mocks.assertEvmProviderMatchesRpc.mockRejectedValue(new Error('socket hang up')); + await act(async () => { + fakeProvider.emit('chainChanged'); + }); + await flush(); + + expect(result.current.address).toBe(ADDRESS_A); + expect(result.current.isConnected).toBe(true); + expect(result.current.error).toBe('socket hang up'); + }); + + it('clears the address when the network genuinely mismatches', async () => { + mocks.getEvmAccountForChain.mockResolvedValue(ADDRESS_A); + + const { result } = renderWallet(); + await flush(); + expect(result.current.address).toBe(ADDRESS_A); + + mocks.assertEvmProviderMatchesRpc.mockRejectedValue( + new EvmNetworkMismatchError('MetaMask is connected to a different Lumera network.'), + ); + await act(async () => { + fakeProvider.emit('chainChanged'); + }); + await flush(); + + expect(result.current.address).toBe(''); + expect(result.current.isConnected).toBe(false); + expect(result.current.error).toBe('MetaMask is connected to a different Lumera network.'); + }); + + it('clears the address when the wallet reports no usable account', async () => { + mocks.getEvmAccountForChain.mockRejectedValue( + new Error('No EVM wallet account is connected.'), + ); + + const { result } = renderWallet(); + await flush(); + + expect(result.current.address).toBe(''); + expect(result.current.error).toBe('No EVM wallet account is connected.'); + expect(mocks.assertEvmProviderMatchesRpc).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/app/providers/evm-wallet-provider.tsx b/apps/web/src/app/providers/evm-wallet-provider.tsx index 53eefd6..74f6e78 100644 --- a/apps/web/src/app/providers/evm-wallet-provider.tsx +++ b/apps/web/src/app/providers/evm-wallet-provider.tsx @@ -12,6 +12,7 @@ import { import { assertEvmProviderMatchesRpc, ensureEvmWalletNetwork, + EvmNetworkMismatchError, getEvmAccountForChain, getEvmConnectionErrorMessage, getMetaMaskProvider, @@ -128,16 +129,54 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { if (!IS_EVM_NETWORK || !provider || !EVM_CHAIN_ID) return; const expectedChainId = EVM_CHAIN_ID; + // `accountsChanged` and `chainChanged` can overlap, and each sync awaits + // several RPC round trips. Without a sequence guard the slowest response + // wins and can restore state the newer event already superseded. + let cancelled = false; + let latestSyncId = 0; + const syncAccounts = async () => { + const syncId = latestSyncId + 1; + latestSyncId = syncId; + const isStale = () => cancelled || syncId !== latestSyncId; + + let activeAddress: string; + try { + activeAddress = await getEvmAccountForChain(provider, expectedChainId); + } catch (accountError) { + // The wallet reports no usable account for this chain, so there is + // genuinely nothing connected. + if (isStale()) return; + setAddress(''); + setError(accountError instanceof Error + ? accountError.message + : 'Unable to read the MetaMask account.'); + return; + } + try { - const activeAddress = await getEvmAccountForChain(provider, expectedChainId); await assertEvmProviderMatchesRpc(provider, { rpcEndpoint: EVM_RPC_ENDPOINT || undefined }); + } catch (verifyError) { + if (isStale()) return; + if (verifyError instanceof EvmNetworkMismatchError) { + setAddress(''); + setError(verifyError.message); + return; + } + // Verification could not complete (a transient RPC failure rather than + // a real mismatch). Keep the connected address so the wallet does not + // appear to disconnect, and surface why it is unverified. Signing paths + // re-run this check via `ensureNetwork`, so nothing is signed unverified. setAddress(activeAddress); - setError(''); - } catch (syncError) { - setAddress(''); - setError(syncError instanceof Error ? syncError.message : 'Unable to verify the MetaMask network.'); + setError(verifyError instanceof Error + ? verifyError.message + : 'Unable to verify the MetaMask network.'); + return; } + + if (isStale()) return; + setAddress(activeAddress); + setError(''); }; const handleAccountsChanged = () => void syncAccounts(); @@ -148,6 +187,7 @@ export function EvmWalletProvider({ children }: { children: React.ReactNode }) { provider.on?.('chainChanged', handleChainChanged); return () => { + cancelled = true; provider.removeListener?.('accountsChanged', handleAccountsChanged); provider.removeListener?.('chainChanged', handleChainChanged); }; diff --git a/apps/web/src/components/ConnectWallet.module.css b/apps/web/src/components/ConnectWallet.module.css index e337ef0..3b028c4 100644 --- a/apps/web/src/components/ConnectWallet.module.css +++ b/apps/web/src/components/ConnectWallet.module.css @@ -161,9 +161,39 @@ .accountControls { display: flex; + align-items: center; gap: 8px; } +.walletAlert { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + max-width: 260px; + margin: 0; + padding: 5px 10px; + border: 1px solid rgba(255, 143, 143, 0.4); + border-radius: 8px; + background: rgba(255, 143, 143, 0.12); + color: #ff8f8f; + font-size: 13px; + line-height: 1.2; +} + +.walletAlertText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Keep the header usable on narrow screens: the icon and its title remain. */ +@media (max-width: 768px) { + .walletAlertText { + display: none; + } +} + .accountMenuRoot { position: relative; } diff --git a/apps/web/src/components/ConnectWallet.tsx b/apps/web/src/components/ConnectWallet.tsx index 3cc0e1b..6002b33 100644 --- a/apps/web/src/components/ConnectWallet.tsx +++ b/apps/web/src/components/ConnectWallet.tsx @@ -9,7 +9,7 @@ import { useChain, useChainWallet, } from '@interchain-kit/react'; -import { ChevronDown, Copy, LogOut, RefreshCw } from 'lucide-react'; +import { ChevronDown, Copy, LogOut, RefreshCw, TriangleAlert } from 'lucide-react'; import { toast } from 'react-toastify'; import { useDispatch, useSelector } from '@/redux/hooks'; @@ -282,9 +282,20 @@ export function ConnectWallet() { { label: 'ETH hex address', value: ethAddress }, ] : [{ label: 'Bech32 address', value: address }]; + // Surface MetaMask sync/verification problems, which otherwise leave the + // header looking simply disconnected with no explanation. + const evmWalletError = IS_EVM_NETWORK && walletName !== KEPLR_WALLET_NAME + ? evmWallet.error + : ''; return (
    + {evmWalletError && ( +

    +

    + )} {!address ?
    - {transactions.map((tx) => ( -
    + {transactions.map((tx) => { + const transactionType = getTransactionDisplayType(tx, { + bech32Address, + ethAddress, + }); + + return ( +
    Block Height:
    -
    - {getTxIcon(getMessages(tx.tx.body.messages))} +
    + {getTxIcon(transactionType)}
    {tx.height}
    @@ -118,10 +142,10 @@ export default function TransactionHistory({
    TX Type:
    - {getMessages(tx.tx.body.messages)} + {transactionType}
    TX Status:
    @@ -134,8 +158,9 @@ export default function TransactionHistory({ {dayjs(tx.timestamp).format('MMMM DD, YYYY')} at {dayjs(tx.timestamp).format('HH:mm:ss')} ()
    -
    - ))} +
    + ); + })} {!transactions?.length && !isLoading ?

    No Transactions

    diff --git a/apps/web/src/utils/transaction-history.test.ts b/apps/web/src/utils/transaction-history.test.ts index 3632018..19f0b09 100644 --- a/apps/web/src/utils/transaction-history.test.ts +++ b/apps/web/src/utils/transaction-history.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { buildTxHistoryPath, getTransactionHistoryAddress, + getTransactionDisplayType, hasEthereumTransactionHash, isTransactionSuccessful, } from './transaction-history'; @@ -67,4 +68,203 @@ describe('transaction history', () => { expect(hasEthereumTransactionHash(transactions, '0x1234')).toBe(false); }); + it('classifies Ethereum sends from the indexed EVM sender', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [{ '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }], + }, + }, + events: [ + { + type: 'transfer', + attributes: [ + { key: 'sender', value: 'lumera1fee' }, + { key: 'recipient', value: 'lumera1account' }, + ], + }, + { + type: 'ethereum_tx', + attributes: [ + { key: 'recipient', value: '0x2222222222222222222222222222222222222222' }, + { key: 'msg_index', value: '0' }, + ], + }, + { + type: 'message', + attributes: [ + { key: 'module', value: 'evm' }, + { key: 'sender', value: '0x1111111111111111111111111111111111111111' }, + { key: 'msg_index', value: '0' }, + ], + }, + ], + }, { + bech32Address: 'lumera1account', + ethAddress: '0x1111111111111111111111111111111111111111', + })).toBe('EthereumTx Send'); + }); + + it('classifies Ethereum receipts case-insensitively', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [{ '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }], + }, + }, + events: [ + { + type: 'ethereum_tx', + attributes: [ + { key: 'recipient', value: '0xAABBccDDeeFF0011223344556677889900AAbbCC' }, + { key: 'msg_index', value: '0' }, + ], + }, + { + type: 'message', + attributes: [ + { key: 'module', value: 'evm' }, + { key: 'sender', value: '0x2222222222222222222222222222222222222222' }, + { key: 'msg_index', value: '0' }, + ], + }, + ], + }, { + ethAddress: '0xaabbccddeeff0011223344556677889900aabbcc', + })).toBe('EthereumTx Recv'); + }); + + it('classifies Ethereum sends from the indexed Cosmos sender fallback', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [{ '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }], + }, + }, + events: [{ + type: 'message', + attributes: [ + { key: 'action', value: '/cosmos.evm.vm.v1.MsgEthereumTx' }, + { key: 'sender', value: 'lumera1account' }, + { key: 'msg_index', value: '0' }, + ], + }], + }, { + bech32Address: 'lumera1account', + })).toBe('EthereumTx Send'); + }); + + it('matches Ethereum directions by message index', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [ + { '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }, + { '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }, + ], + }, + }, + events: [ + { + type: 'ethereum_tx', + attributes: [ + { key: 'recipient', value: '0x2222222222222222222222222222222222222222' }, + { key: 'msg_index', value: '0' }, + ], + }, + { + type: 'message', + attributes: [ + { key: 'module', value: 'evm' }, + { key: 'sender', value: '0x1111111111111111111111111111111111111111' }, + { key: 'msg_index', value: '0' }, + ], + }, + { + type: 'ethereum_tx', + attributes: [ + { key: 'recipient', value: '0x1111111111111111111111111111111111111111' }, + { key: 'msg_index', value: '1' }, + ], + }, + { + type: 'message', + attributes: [ + { key: 'module', value: 'evm' }, + { key: 'sender', value: '0x3333333333333333333333333333333333333333' }, + { key: 'msg_index', value: '1' }, + ], + }, + ], + }, { + ethAddress: '0x1111111111111111111111111111111111111111', + })).toBe('EthereumTx Send, EthereumTx Recv'); + }); + + it('classifies Cosmos bank sends and receipts from message addresses', () => { + const transaction = (fromAddress: string, toAddress: string) => ({ + tx: { + body: { + messages: [{ + '@type': '/cosmos.bank.v1beta1.MsgSend', + from_address: fromAddress, + to_address: toAddress, + }], + }, + }, + }); + + expect(getTransactionDisplayType( + transaction('lumera1account', 'lumera1other'), + { bech32Address: 'lumera1account' }, + )).toBe('Send'); + expect(getTransactionDisplayType( + transaction('lumera1other', 'lumera1account'), + { bech32Address: 'lumera1account' }, + )).toBe('Recv'); + expect(getTransactionDisplayType( + transaction('lumera1account', 'lumera1account'), + { bech32Address: 'lumera1account' }, + )).toBe('Self Transfer'); + }); + + it('classifies Cosmos multisend and IBC transfer directions', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [ + { + '@type': '/cosmos.bank.v1beta1.MsgMultiSend', + inputs: [{ address: 'lumera1other' }], + outputs: [{ address: 'lumera1account' }], + }, + { + '@type': '/ibc.applications.transfer.v1.MsgTransfer', + sender: 'lumera1account', + receiver: 'remote1recipient', + }, + ], + }, + }, + }, { + bech32Address: 'lumera1account', + })).toBe('Recv, Send'); + }); + + it('preserves the original message type when direction data is unavailable', () => { + expect(getTransactionDisplayType({ + tx: { + body: { + messages: [ + { '@type': '/cosmos.evm.vm.v1.MsgEthereumTx' }, + { '@type': '/cosmos.staking.v1beta1.MsgDelegate' }, + ], + }, + }, + events: [], + }, { + ethAddress: '0x1111111111111111111111111111111111111111', + })).toBe('EthereumTx, Delegate'); + }); + }); diff --git a/apps/web/src/utils/transaction-history.ts b/apps/web/src/utils/transaction-history.ts index 406a4b7..4da7abc 100644 --- a/apps/web/src/utils/transaction-history.ts +++ b/apps/web/src/utils/transaction-history.ts @@ -16,6 +16,32 @@ interface IndexedTransactionEvents { }>; } +interface TransactionMessage { + '@type'?: string; + typeUrl?: string; + from_address?: string; + fromAddress?: string; + to_address?: string; + toAddress?: string; + sender?: string; + receiver?: string; + inputs?: Array<{ address?: string }>; + outputs?: Array<{ address?: string }>; +} + +interface DirectionalTransaction extends IndexedTransactionEvents { + tx: { + body: { + messages?: TransactionMessage[]; + }; + }; +} + +interface TransactionAddresses { + bech32Address?: string; + ethAddress?: string; +} + export type TxHistoryDirection = 'sent' | 'received'; interface TxHistoryPathOptions { @@ -57,3 +83,127 @@ export const hasEthereumTransactionHash = ( )) ))); }; + +const normalizeAddress = (address?: string) => address?.trim().toLowerCase() ?? ''; + +const getMessageName = (message: TransactionMessage) => { + const messageType = message['@type'] || message.typeUrl || 'unknown'; + return messageType + .substring(messageType.lastIndexOf('.') + 1) + .replace('Msg', ''); +}; + +const getEventAttribute = ( + event: NonNullable[number], + key: string, +) => event.attributes?.find((attribute) => attribute.key === key)?.value; + +const isEventForMessage = ( + event: NonNullable[number], + messageIndex: number, + messageCount: number, +) => { + const indexedMessage = getEventAttribute(event, 'msg_index'); + return indexedMessage === String(messageIndex) + || (indexedMessage === undefined && messageCount === 1); +}; + +const getEthereumDirection = ( + transaction: DirectionalTransaction, + messageIndex: number, + messageCount: number, + bech32Address: string, + ethAddress: string, +) => { + if (!bech32Address && !ethAddress) return undefined; + + const events = transaction.events ?? []; + const ethereumSenderEvent = events.find((event) => ( + event.type === 'message' + && getEventAttribute(event, 'module') === 'evm' + && isEventForMessage(event, messageIndex, messageCount) + )); + const cosmosSenderEvent = events.find((event) => ( + event.type === 'message' + && getEventAttribute(event, 'action')?.endsWith('.MsgEthereumTx') + && isEventForMessage(event, messageIndex, messageCount) + )); + const recipientEvent = events.find((event) => ( + event.type === 'ethereum_tx' + && getEventAttribute(event, 'recipient') !== undefined + && isEventForMessage(event, messageIndex, messageCount) + )); + const ethereumSender = ethereumSenderEvent + ? normalizeAddress(getEventAttribute(ethereumSenderEvent, 'sender')) + : ''; + const cosmosSender = cosmosSenderEvent + ? normalizeAddress(getEventAttribute(cosmosSenderEvent, 'sender')) + : ''; + const recipient = recipientEvent + ? normalizeAddress(getEventAttribute(recipientEvent, 'recipient')) + : ''; + const isSender = (Boolean(ethAddress) && ethereumSender === ethAddress) + || (Boolean(bech32Address) && cosmosSender === bech32Address); + const isRecipient = Boolean(ethAddress) && recipient === ethAddress; + + if (isSender && isRecipient) return 'EthereumTx Self'; + if (isSender) return 'EthereumTx Send'; + if (isRecipient) return 'EthereumTx Recv'; + return undefined; +}; + +const getCosmosDirection = (message: TransactionMessage, bech32Address: string) => { + if (!bech32Address) return undefined; + + const messageName = getMessageName(message); + let isSender = false; + let isRecipient = false; + + if (messageName === 'Send') { + isSender = normalizeAddress(message.from_address ?? message.fromAddress) === bech32Address; + isRecipient = normalizeAddress(message.to_address ?? message.toAddress) === bech32Address; + } else if (messageName === 'MultiSend') { + isSender = message.inputs?.some(({ address }) => normalizeAddress(address) === bech32Address) ?? false; + isRecipient = message.outputs?.some(({ address }) => normalizeAddress(address) === bech32Address) ?? false; + } else if (messageName === 'Transfer') { + isSender = normalizeAddress(message.sender) === bech32Address; + isRecipient = normalizeAddress(message.receiver) === bech32Address; + } else { + return undefined; + } + + if (isSender && isRecipient) return 'Self Transfer'; + if (isSender) return 'Send'; + if (isRecipient) return 'Recv'; + return undefined; +}; + +const summarizeTypes = (types: string[]) => { + const counts = new Map(); + types.forEach((type) => counts.set(type, (counts.get(type) ?? 0) + 1)); + return [...counts].map(([type, count]) => count > 1 ? `${type}×${count}` : type).join(', '); +}; + +export const getTransactionDisplayType = ( + transaction: DirectionalTransaction, + { bech32Address, ethAddress }: TransactionAddresses, +) => { + const messages = transaction.tx.body.messages ?? []; + const normalizedBech32Address = normalizeAddress(bech32Address); + const normalizedEthAddress = normalizeAddress(ethAddress); + + return summarizeTypes(messages.map((message, messageIndex) => { + const messageName = getMessageName(message); + if (messageName === 'EthereumTx') { + return getEthereumDirection( + transaction, + messageIndex, + messages.length, + normalizedBech32Address, + normalizedEthAddress, + ) ?? messageName; + } + + return getCosmosDirection(message, normalizedBech32Address) ?? messageName; + })); +}; diff --git a/packages/ui/src/screens/AccountScreen.tsx b/packages/ui/src/screens/AccountScreen.tsx index a66d5c8..f520e5f 100644 --- a/packages/ui/src/screens/AccountScreen.tsx +++ b/packages/ui/src/screens/AccountScreen.tsx @@ -232,6 +232,8 @@ export const AccountScreen = ({ totalTransactions={activeTransactions.totalTransactions} isLoading={activeTransactions.isLoading} handlePageClick={activeTransactions.handlePageClick} + bech32Address={bech32Address} + ethAddress={ethAddress} />
    diff --git a/packages/ui/src/screens/WalletScreen.tsx b/packages/ui/src/screens/WalletScreen.tsx index 3c81b1a..a956f3b 100644 --- a/packages/ui/src/screens/WalletScreen.tsx +++ b/packages/ui/src/screens/WalletScreen.tsx @@ -317,6 +317,8 @@ export const WalletScreen = ({ totalTransactions={totalTransactions} isLoading={isLoading} handlePageClick={handlePageClick} + bech32Address={bech32Address || walletAddress} + ethAddress={ethAddress} />
    From 359b483109833009d003fc73ee3accbe4bd0aeed Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Mon, 17 Aug 2026 13:50:49 -0400 Subject: [PATCH 44/45] improve transaction history action icons --- .../web/src/components/TransactionHistory.tsx | 81 +++++++++++++------ .../web/src/utils/transaction-history.test.ts | 8 ++ apps/web/src/utils/transaction-history.ts | 29 +++++++ 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/TransactionHistory.tsx b/apps/web/src/components/TransactionHistory.tsx index bcfb9c2..821e038 100644 --- a/apps/web/src/components/TransactionHistory.tsx +++ b/apps/web/src/components/TransactionHistory.tsx @@ -3,10 +3,18 @@ import { ArrowUpRight, ArrowLeftRight, ArrowDownLeft, - Layers, - ClockPlus, - Unlink, - Star, + BadgeDollarSign, + CircleArrowDown, + CircleArrowUp, + CircleDotDashed, + CircleMinus, + FilePlus2, + Gift, + HandCoins, + Receipt, + Repeat2, + Vote, + Waypoints, } from 'lucide-react'; import { H3 } from 'tamagui'; import ReactPaginate from 'react-paginate'; @@ -17,6 +25,7 @@ import Loading from '@/components/Loading'; import PastTime from '@/components/PastTime'; import { ITransaction } from '@/hooks/useTransaction'; import { + getPrimaryTransactionType, getTransactionDisplayType, isTransactionSuccessful, } from '@/utils/transaction-history'; @@ -33,54 +42,76 @@ interface ITransactionHistory { } const getTxIcon = (type: string) => { - const normalizedType = type.replace(/×\d+$/, ''); - switch(normalizedType) { + switch(getPrimaryTransactionType(type)) { case 'Send': - case 'EthereumTx Send': return ; + case 'EthereumTx Send': + return ; case 'Recv': - case 'EthereumTx Recv': return ; + case 'EthereumTx Recv': + return ; case 'Self Transfer': - case 'EthereumTx Self': return ; + case 'EthereumTx Self': + return ; + case 'Vote': + return ; + case 'SubmitProposal': + return ; + case 'Deposit': + return ; case 'BeginRedelegate': - return ; + return ; case 'Delegate': - return ; + return ; + case 'Undelegate': + return ; + case 'WithdrawDelegatorReward': + return ; + case 'MultiSend': + case 'Transfer': + return ; + case 'EthereumTx': + return ; case 'Failed': return ; - case 'Undelegate': - return ; default: - if (type.indexOf('WithdrawDelegatorReward') !== -1) { - return ; - } return ; } }; const getColor = (type: string) => { - const normalizedType = type.replace(/×\d+$/, ''); - switch(normalizedType) { + switch(getPrimaryTransactionType(type)) { case 'Send': case 'EthereumTx Send': case 'Failed': return 'bg-red-500/20'; - case 'Delegate': - case 'BeginRedelegate': - return 'bg-green-400/20'; case 'Recv': case 'EthereumTx Recv': return 'bg-green-500/20'; case 'Self Transfer': case 'EthereumTx Self': return 'bg-blue-500/20'; + case 'Vote': + return 'bg-violet-500/20'; + case 'SubmitProposal': + return 'bg-purple-500/20'; + case 'Deposit': + return 'bg-sky-500/20'; + case 'Delegate': + return 'bg-indigo-500/20'; + case 'BeginRedelegate': + return 'bg-cyan-500/20'; + case 'Undelegate': + return 'bg-orange-500/20'; + case 'WithdrawDelegatorReward': + return 'recent-activity-icon'; + case 'MultiSend': + case 'Transfer': + return 'bg-blue-500/20'; default: - if (type.indexOf('WithdrawDelegatorReward') !== -1) { - return 'recent-activity-icon'; - } - return 'bg-red-500/20'; + return 'bg-gray-500/20'; } } diff --git a/apps/web/src/utils/transaction-history.test.ts b/apps/web/src/utils/transaction-history.test.ts index 19f0b09..d818a60 100644 --- a/apps/web/src/utils/transaction-history.test.ts +++ b/apps/web/src/utils/transaction-history.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { buildTxHistoryPath, + getPrimaryTransactionType, getTransactionHistoryAddress, getTransactionDisplayType, hasEthereumTransactionHash, @@ -267,4 +268,11 @@ describe('transaction history', () => { })).toBe('EthereumTx, Delegate'); }); + it('selects semantic icon types for repeated and compound transactions', () => { + expect(getPrimaryTransactionType('Vote×2')).toBe('Vote'); + expect(getPrimaryTransactionType('WithdrawDelegatorReward, Undelegate')).toBe('Undelegate'); + expect(getPrimaryTransactionType('WithdrawDelegatorReward, BeginRedelegate')).toBe('BeginRedelegate'); + expect(getPrimaryTransactionType('EthereumTx Recv')).toBe('EthereumTx Recv'); + }); + }); diff --git a/apps/web/src/utils/transaction-history.ts b/apps/web/src/utils/transaction-history.ts index 4da7abc..a16a666 100644 --- a/apps/web/src/utils/transaction-history.ts +++ b/apps/web/src/utils/transaction-history.ts @@ -184,6 +184,35 @@ const summarizeTypes = (types: string[]) => { return [...counts].map(([type, count]) => count > 1 ? `${type}×${count}` : type).join(', '); }; +const TRANSACTION_TYPE_PRIORITY = [ + 'Failed', + 'EthereumTx Send', + 'EthereumTx Recv', + 'EthereumTx Self', + 'Send', + 'Recv', + 'Self Transfer', + 'Vote', + 'SubmitProposal', + 'Deposit', + 'BeginRedelegate', + 'Undelegate', + 'Delegate', + 'WithdrawDelegatorReward', + 'MultiSend', + 'Transfer', + 'EthereumTx', +] as const; + +export const getPrimaryTransactionType = (transactionType: string) => { + const transactionTypes = transactionType + .split(',') + .map((type) => type.trim().replace(/×\d+$/, '')); + return TRANSACTION_TYPE_PRIORITY.find((type) => transactionTypes.includes(type)) + ?? transactionTypes[0] + ?? 'unknown'; +}; + export const getTransactionDisplayType = ( transaction: DirectionalTransaction, { bech32Address, ethAddress }: TransactionAddresses, From e2ea27288abb0c2a2f7d45d3adb4c829b87d5a90 Mon Sep 17 00:00:00 2001 From: Andrey Kobrin Date: Mon, 17 Aug 2026 16:13:28 -0400 Subject: [PATCH 45/45] add design for integrating EVM work into the deployed develop line The hub's deployed revision (39aedbc) sits on develop, which is not an ancestor of main. main has not moved since 2025-12-09 while develop accumulated 264 commits and serves both production hubs. evm-support was branched from the stale main, so it lacks that feature surface. Records the decision to move the smaller tested change onto the live line rather than the reverse, the file-by-file reconciliation plan for the 40 overlapping files, the wallet consolidation to Keplr + MetaMask, and a route-parity gate to stop a wrong HUB_VERSION re-pin from silently dropping routes from the public hub. Co-Authored-By: Claude Opus 5 --- .../2026-08-17-develop-evm-integration.md | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 docs/design/2026-08-17-develop-evm-integration.md diff --git a/docs/design/2026-08-17-develop-evm-integration.md b/docs/design/2026-08-17-develop-evm-integration.md new file mode 100644 index 0000000..de60e5d --- /dev/null +++ b/docs/design/2026-08-17-develop-evm-integration.md @@ -0,0 +1,244 @@ +# Integrating the EVM work into the deployed `develop` line + +- **Date:** 2026-08-17 +- **Status:** Approved design, pending implementation +- **Source branch:** `evm-support` (44 commits) +- **Target branch:** cut from `origin/develop`, merged back via PR +- **First delivery target:** one branch that carries both the deployed feature surface + and the EVM wallet layer, suitable for pinning as `HUB_VERSION` in lumera-deploy + +## Problem + +The hub has two divergent development lines, and the one everybody treats as the +trunk is not the one that is deployed. + +| Branch | Last commit | Commits since fork | Tests | Deployed? | +| --- | --- | --- | --- | --- | +| `origin/main` | 2025-12-09 | — | 0 | no | +| `origin/develop` | 2026-07-06 (`39aedbc`) | 264 | 0 | **yes, both hubs** | +| `evm-support` | 2026-08-17 | 44 | 21 files | no | + +`main` has not moved in eight months. `develop` is the real trunk: it carries 264 +commits and it is what serves `hub.testnet.lumera.io` and `hub.lumera.io` today. +`evm-support` was branched from the stale `main`, so it is missing that entire +feature surface. + +Measured, not assumed: + +```console +$ git merge-base --is-ancestor 39aedbc origin/main # -> not an ancestor +$ git rev-list --left-right --count origin/main...origin/develop +1 264 +$ git merge-base origin/main origin/develop +430a7cd +``` + +The divergence is complementary rather than competing: + +- `develop` holds the feature surface `evm-support` lacks — `/blocks`, `/supernodes`, + `/admin`, `/loyalty/*`, `/referral`, `/wasm`, plus a much richer account page. +- `evm-support` holds the EVM/wallet layer `develop` lacks. A grep for + `EVM_RPC` / `eip1193` / `evmRpc` across `develop` returns nothing. + +Confirmed against production by probing routes that exist only on `develop`: +`/supernodes`, `/admin`, `/referral`, `/wasm`, `/loyalty/wallet/connect` all return +200, while `/transactions` returns 404 — so the deployed build does real +server-side routing and those 200s are genuine pages. + +### The consequence that forces this work + +lumera-deploy pins `HUB_VERSION=39aedbc` and its records describe the situation +backwards, as *"upstream main 3b1b871 (> deployed 39aedbc)"*. If a re-pin points at +`main` or at `evm-support`, runbook 04's cutover would build that image and move +`hub.testnet.lumera.io` onto it, **silently deleting `/admin`, `/loyalty/*`, +`/referral`, `/supernodes`, `/wasm` and `/blocks` from the public hub.** Nothing in +the runbook's gates would catch it: P4.4 and P4.5 both probe `/`, which exists on +every branch. The cutover would look green. + +## Decision + +### Direction and topology + +Move the smaller, tested change onto the live line — not the reverse. + +```bash +git switch -c evm-on-develop origin/develop +git rm -r --cached apps/web/.tamagui/ # hygiene, see below +git commit -m "untrack generated tamagui artifacts" +git merge evm-support # ONE resolution pass +``` + +`develop` is the first parent, so the branch reads as "develop, plus the EVM work", +which is what the deploy pin needs. Both histories stay intact. + +**Not** a cherry-pick replay of the 44 commits. Those include seven separate +"Update `useStaking.ts`" fixups and repeatedly touch the same files, so a replay +means resolving the same conflicts over and over. One merge commit, one resolution. + +### Hygiene first: untrack the generated Tamagui artifacts + +`apps/web/.tamagui/lumerahubui-components.config.cjs` is an 8.9 MB generated build +artifact. It is **already gitignored** at `apps/web/.gitignore:44`, and tracked only +because it predates that rule. It accounts for 999,257 of `develop`'s 1,050,272 +insertions. + +Untracking it first drops the reconciliation from ~1,050,000 lines to ~51,000 and +makes the real conflict surface visible. Regenerate locally via the build. + +### Reconciliation plan + +40 files are touched by both sides, but `develop`'s deltas on the wallet layer are +mostly trivial. Measured against the merge-base: + +| File | `develop` delta | `evm-support` | Resolution | +| --- | --- | --- | --- | +| `contants/network.ts` | +2/-0 | 96 lines (profiles) | take ours, re-apply 2 lines | +| `providers/wallet-provider.tsx` | +5/-1 | 151 lines | take ours, re-apply | +| `hooks/useWalletConnect.ts` | +1/-1 | 99 lines | take ours, re-apply | +| `components/SendModal.tsx` | +13/-7 | 290 lines | take ours, re-apply | +| `hooks/useSend.ts` | **+146/-138** | 233 lines | real reconciliation | +| `components/ConnectWallet.tsx` | 91 -> 165 | 404 lines + CSS module | real reconciliation | +| `hooks/useStaking.ts` | 285 -> 304 | 429 lines | real reconciliation | +| `hooks/useAccount.ts` | 0 -> **382** | 55 lines | see below | +| `screens/AccountScreen.tsx` | +1136/-9 | +235/-11 | see below | + +Resolve cheapest first: `network.ts` -> `wallet-provider` -> `useWalletConnect` -> +`SendModal` -> `useSend` -> `ConnectWallet` -> `useStaking` -> `useAccount` / +`AccountScreen` last. + +#### The `useAccount` crux + +`useAccount.ts` is an add/add conflict: both branches created it independently for +the same job, with opposite philosophies. + +- `develop`: a 382-line hook calling `@/utils/api` through `@interchain-kit/react`, + formatting inline. Feeds a much richer account page (`AccountScreen.tsx`, +1136). +- `evm-support`: 55 lines delegating to extracted, **tested** helpers + (`fetchAccountInfo`, `parseAccountAddress`; covered by `useAccountInfo.test.ts` + and `account.test.ts`). + +**Decision: keep `develop`'s richer page, ported onto the tested data layer.** It is +the only combination that preserves both the UI users currently see and the test +coverage. This is the largest single work item in the merge and should be budgeted +separately from the other seven files. + +### Wallet consolidation: Keplr + MetaMask only + +Three adapters come out. + +- **Leap** — discontinued. +- **Cosmostation** — the entire wallet service shuts down starting **2026-09-01** + (iOS, Android and Chrome extension; only seed-phrase and private-key export + survive). It is registered on both production hubs today. +- **WalletConnect** — not a wallet but a transport (now Reown). Dropped as a product + decision. Cost: Keplr's *mobile* app connects via WalletConnect, so the hub becomes + desktop-extension-only. Reversible later by re-adding it deliberately, with a real + WalletConnect mode in the selection logic. + +This makes the adapter array agree with `wallet-selection.ts` for the first time. +That module already encodes a two-wallet world — `METAMASK_WALLET_NAME`, +`KEPLR_WALLET_NAME`, `ActiveWalletMode = 'none' | 'evm' | 'cosmos'` — and +`getActiveWalletMode`, `getPreferredWalletSelection` and `getAlternativeWalletName` +reason about only those two. The other three were registered but unmodeled. + +Removal footprint: + +| File | Change | +| --- | --- | +| `apps/web/package.json` | drop `@interchain-kit/leap-extension`, `@interchain-kit/cosmostation-extension` | +| `providers/wallet-provider.tsx` | drop both imports; drop the `walletConnect` adapter and its `WALLET_CONNECT_*` imports; array becomes `[keplrWallet]` | +| `types/window.d.ts` | drop `interface Leap` and `window.leap?` | +| `contants/network.ts` | drop the six `WALLET_CONNECT_*` constants (L90-95) | +| `apps/web/.env.example` | drop the six `NEXT_PUBLIC_WALLET_CONNECT_*` vars | +| `components/GetStarted.tsx` *(develop side)* | remove the Leap card (L125-132) and the Cosmostation card (L140-146); keep Keplr (L110-117); **add a MetaMask card** — see below | +| `app/styles.css` *(develop side)* | remove `.get-started .leap-wallet::after` / `.cosmostation-wallet::after` rules at L494-495, L508, L511-512, L724-725 | +| `public/leap.svg`, `public/img/leap.jpg`, `public/cosmostation.svg`, `public/img/cosmostation.jpg` | delete | +| `utils/wallet-selection.test.ts` | the `'leap-extension'` fixture is a stand-in for "some other wallet", proving that disconnecting Keplr leaves other entries intact. Rename the fixture to a neutral `'other-extension'` rather than deleting the cases — the assertion keeps its value | + +WalletConnect needs no `package.json` change; the adapter comes from +`@interchain-kit/core`, which stays. + +#### The onboarding gap the removal exposes + +`GetStarted.tsx` is the "install a wallet" onboarding UI, and it currently ships +three cards — Keplr, Leap, Cosmostation — and **no MetaMask card**, because it +predates the EVM work. Removing the two dead wallets would leave the hub advertising +only Keplr while actually supporting Keplr *and* MetaMask. + +Add a MetaMask card alongside Keplr, following the existing card shape (link with the +`?referrer=` param, heading, description, icon plus URL line) and a +`.get-started .metamask-wallet::after` rule mirroring the ones being deleted. The +icon asset already exists — `evm-support` added `apps/web/public/metamask.png` in +commit `50f9df8` — so no new asset is required. + +Incidental bug worth fixing while in the file: all three existing cards hardcode +`alt='Keplr Wallet'` on their icons (L116, L131, L146). Moot for the two being +removed; fix the Keplr one and set the new MetaMask card's `alt` correctly. + +After this, interchain-kit carries exactly one adapter (Keplr); MetaMask lives +entirely outside it in `evm-wallet-provider.tsx` via EIP-1193 with EIP-6963 +discovery. Whether interchain-kit still earns its weight for a single wallet is a +real question, deliberately out of scope here. + +## Verification + +`develop` has **zero test files**; `evm-support` has 21. So after the merge, 264 +commits' worth of features have no automated coverage, and those 21 files are the +only net. Verification therefore leans on structure: + +1. `pnpm --filter web test` — all 21 must pass. They cover the layer being + reconciled, which is exactly where the risk concentrates. +2. `pnpm --filter web exec tsc --noEmit` +3. `make testnet-build` +4. **Route-parity gate.** Enumerate `page.tsx` under `apps/web/src/app` on + `origin/develop` and on the merged branch. **No route may disappear.** Mechanical, + and it catches the whole class of "the merge silently dropped a feature". +5. Manual smoke on the `develop`-only surface: `/admin`, `/loyalty/wallet/connect`, + `/referral`, `/supernodes`, `/blocks`, `/wasm` — plus Keplr and MetaMask connect + against an EVM-enabled profile. + +## Consequences for lumera-deploy + +- `HUB_VERSION` re-pins to the merge commit — the first pin that is both current and + feature-complete. +- The route-parity check belongs in runbook 04's P4.4/P4.5 gates, so a future wrong + pin fails loudly instead of shipping green. +- The `update-hub.sh` work (in-place PM2 updater for both AWS hubs) sits on top of a + correct pin, and is tracked separately. + +## Resolved decisions + +- **The loyalty / admin / referral / snag surface is kept in full** (operator, + 2026-08-17). All 264 commits' worth of `develop` features carry over. Nothing is + retired as part of this merge, so the route-parity gate below applies to the + complete `develop` route set with no exclusions. +- **Cosmostation removal is folded into this merge**, not split out (operator, + 2026-08-17). The standalone-PR alternative was considered and declined. + + **This puts a hard date on the merge.** Cosmostation's service begins shutting down + **2026-09-01**. Until this merge lands and is deployed, both production hubs + continue to offer a wallet that is being switched off — so the merge, the + `HUB_VERSION` re-pin, and the deploy all have to complete inside that window, or + the fallback is to ship the wallet removal on its own after all. Fifteen days from + this design's date. + +## Open questions + +1. **Keplr as a dual-mode wallet.** Keplr exposes an EIP-1193 provider at + `window.keplr.ethereum` (EIP-1193 + EIP-2255, EIP-6963 discovery) and supports + EVM-compatible Cosmos chains. If that works against Lumera's `cosmos/evm` v0.6.0 + chain, Keplr sidesteps the entire Phase 1 limitation in + `2026-07-31-evm-metamask-cosmos-signing.md` — it would sign Cosmos messages + natively *and* speak EVM, with no dependency on chain-side EIP-712 verification. + Keplr's EVM support covers chains it has registered, so whether a custom chain + works via `suggestChain` with EVM info needs hands-on verification. Worth a spike + **after** the merge; the merge keeps the current two-mode mapping. + +## Out of scope + +- Writing tests for `develop`'s 264 commits. It is the real coverage gap and deserves + its own slice. +- Retiring or replacing interchain-kit now that it carries one adapter. +- Reconciling `main`. It is eight months stale; whether it is fast-forwarded to the + merge result or retired outright is a separate decision. +- Widening Keplr to dual Cosmos/EVM mode (open question 1).