diff --git a/.gitignore b/.gitignore index 68ecb34701..24b4ba6352 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ src/generated .vercel .env*.local + +CLAUDE.md diff --git a/package.json b/package.json index 611cedf872..d9f7381575 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,9 @@ }, "dependencies": { "@apollo/client": "^3.10.8", - "@getpara/ethers-v5-integration": "2.0.0-alpha.64", - "@getpara/evm-wallet-connectors": "2.0.0-alpha.64", - "@getpara/react-sdk-lite": "2.0.0-alpha.64", + "@getpara/ethers-v5-integration": "2.0.0", + "@getpara/evm-wallet-connectors": "2.0.0", + "@getpara/react-sdk-lite": "2.0.0", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.4", "@jbx-protocol/contracts-v1": "2.0.0", diff --git a/src/components/ProtocolActivity/ProtocolActivityElement.tsx b/src/components/ProtocolActivity/ProtocolActivityElement.tsx index 54b208a085..7f760f0e78 100644 --- a/src/components/ProtocolActivity/ProtocolActivityElement.tsx +++ b/src/components/ProtocolActivity/ProtocolActivityElement.tsx @@ -62,17 +62,17 @@ export function ProtocolActivityElement({ /> - {/* Row 2: Action Label + Amount */} -
- - {header} - -
- {subject} -
+ {/* Row 2: Action Label */} +
+ {header} +
+ + {/* Row 3: Amount */} +
+ {subject}
- {/* Row 3: Timestamp + From Address */} + {/* Row 4: Timestamp + From Address */}
{formatHistoricalDate(event.timestamp * 1000)} · diff --git a/src/components/ProtocolActivity/ProtocolActivityList.tsx b/src/components/ProtocolActivity/ProtocolActivityList.tsx index be97bb3009..c69268cd4d 100644 --- a/src/components/ProtocolActivity/ProtocolActivityList.tsx +++ b/src/components/ProtocolActivity/ProtocolActivityList.tsx @@ -1,13 +1,12 @@ import { Button } from 'antd' import Loading from 'components/Loading' import { useActivityEventsQuery } from 'generated/v4v5/graphql' -import { testnetBendystrawClient, mainnetBendystrawClient } from 'lib/apollo/bendystrawClient' +import { mainnetBendystrawClient } from 'lib/apollo/bendystrawClient' import { AnyEvent, transformEventData, } from 'packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData' import React, { useState } from 'react' -import { twMerge } from 'tailwind-merge' import Link from 'next/link' import { v4v5ProjectRoute } from 'packages/v4v5/utils/routes' import { ProtocolActivityElement } from './ProtocolActivityElement' @@ -15,30 +14,24 @@ import { translateEventDataToProtocolPresenter } from './utils/translateEventDat const PAGE_SIZE = 20 const POLL_INTERVAL = 30000 // 30 seconds - -type NetworkType = 'testnet' | 'mainnet' - -// Always default to mainnet first -const defaultNetwork: NetworkType = 'mainnet' +const IGNORED_EVENTS = ['mintNftEvent', 'burnEvent'] +const baseEventFilter = IGNORED_EVENTS.reduce( + (acc, curr) => ({ + ...acc, + [curr]: null, + }), + {}, +) export function ProtocolActivityList() { - const [network, setNetwork] = useState(defaultNetwork) const [endCursor, setEndCursor] = useState(null) - // Select client based on network toggle - const client = network === 'testnet' ? testnetBendystrawClient : mainnetBendystrawClient - - // Reset cursor when network changes - React.useEffect(() => { - setEndCursor(null) - }, [network]) - // Query protocol activity (no chain filter) const { data: activityEvents, loading, error } = useActivityEventsQuery({ - client, + client: mainnetBendystrawClient, pollInterval: POLL_INTERVAL, // Poll every 30 seconds variables: { - where: {}, + where: baseEventFilter, orderBy: 'timestamp', orderDirection: 'desc', after: endCursor, @@ -65,31 +58,6 @@ export function ProtocolActivityList() {

Protocol Activity

- {/* Network Toggle */} -
- - -
diff --git a/src/components/VolumeChart/components/TimelineChart.tsx b/src/components/VolumeChart/components/TimelineChart.tsx index 9b5bcefb31..919e917e09 100644 --- a/src/components/VolumeChart/components/TimelineChart.tsx +++ b/src/components/VolumeChart/components/TimelineChart.tsx @@ -256,7 +256,7 @@ export default function TimelineChart({ type="monotone" dataKey={view} activeDot={{ r: 6, fill: colors.juice[400], stroke: undefined }} - animationDuration={750} + isAnimationActive={false} /> )} ( isArchived: undefined, projectId: undefined, pv: undefined, + isLoading: true, refetchProjectMetadata: () => console.error( 'ProjectMetadataContext.refetchProjectMetadata called but no provider set', diff --git a/src/locales/messages.pot b/src/locales/messages.pot index 0ad4340fbb..db3edb3323 100644 --- a/src/locales/messages.pot +++ b/src/locales/messages.pot @@ -167,6 +167,9 @@ msgstr "" msgid "An address" msgstr "" +msgid "change" +msgstr "" + msgid "Activating this option enables running the user's wallet address against OFAC’s Specially Designated Nationals (SDN) list" msgstr "" @@ -1091,6 +1094,9 @@ msgstr "" msgid "Edit project and cycle rules" msgstr "" +msgid "Next cycle" +msgstr "" + msgid "Your project was successfully created!" msgstr "" @@ -1226,6 +1232,9 @@ msgstr "" msgid "Choose an ENS name to use as the project's handle. Subdomains are allowed and will be included in the handle. Handles won't include the \".eth\" extension." msgstr "" +msgid "Queue Safe ERC-20 Deploy Transactions" +msgstr "" + msgid "Total:" msgstr "" @@ -1265,6 +1274,9 @@ msgstr "" msgid "Project tokens will be minted to the address that made the payment." msgstr "" +msgid "You are viewing a past ruleset. This cycle has ended." +msgstr "" + msgid "Migrate legacy tokens" msgstr "" @@ -1331,6 +1343,9 @@ msgstr "" msgid "Cash outs are disabled when all of the project's ETH is being used for payouts (when payouts are unlimited)." msgstr "" +msgid "Started {0}" +msgstr "" + msgid "Fee from <0><1/>" msgstr "" @@ -1367,6 +1382,9 @@ msgstr "" msgid "A fixed amount of ETH can be paid out from your project each ruleset. You can send specific ETH amounts (or ETH amounts based on USD values) to one or more recipients. Any remaining ETH will stay in your project for token cash outs or use in future rulesets." msgstr "" +msgid "Past" +msgstr "" + msgid "Not able to process fees" msgstr "" @@ -1682,6 +1700,9 @@ msgstr "" msgid "Reserved recipients:" msgstr "" +msgid "Beginner Friendly" +msgstr "" + msgid "Transfer ownership" msgstr "" @@ -1748,6 +1769,9 @@ msgstr "" msgid "This cycle's payouts" msgstr "" +msgid "Since your project owner is a Gnosis Safe and this is an omnichain project, you need to queue separate ERC-20 deploy transactions on each chain." +msgstr "" + msgid "<0>This website interacts with the Ethereum blockchain — to use it, you'll need to have a wallet and some ETH (ETH is the main currency on Ethereum). You can get a free wallet from <1>MetaMask.io, and buy ETH from within the wallet by using a credit card." msgstr "" @@ -2090,6 +2114,9 @@ msgstr "" msgid "allocation" msgstr "" +msgid "Token credits" +msgstr "" + msgid "This project's rules may pose risks for contributors:" msgstr "" @@ -2123,6 +2150,9 @@ msgstr "" msgid "Edit NFTs" msgstr "" +msgid "Confirm archive" +msgstr "" + msgid "{tokenSymbol} ERC-20 address" msgstr "" @@ -2234,6 +2264,9 @@ msgstr "" msgid "Enable reserved tokens" msgstr "" +msgid "This project's rules will be locked in place for {rulesetLength}." +msgstr "" + msgid "Case study" msgstr "" @@ -2516,6 +2549,9 @@ msgstr "" msgid "Not a valid ETH address" msgstr "" +msgid "Previous cycle" +msgstr "" + msgid "{0} after JBX fee" msgstr "" @@ -2612,6 +2648,9 @@ msgstr "" msgid "reserved token recipient" msgstr "" +msgid "Cashing out tokens" +msgstr "" + msgid "Controller configuration" msgstr "" @@ -2870,9 +2909,15 @@ msgstr "" msgid "Redeem tokens" msgstr "" +msgid "Load more cycles..." +msgstr "" + msgid "Add an on-chain note about this cycle." msgstr "" +msgid "Changes from Cycle #{previousCycleNumber}" +msgstr "" + msgid "Archive project" msgstr "" @@ -2891,6 +2936,9 @@ msgstr "" msgid "All of this project's ETH will be paid out. Token holders will receive <0>no ETH when redeeming their tokens." msgstr "" +msgid "Loading..." +msgstr "" + msgid "Approve" msgstr "" @@ -3575,6 +3623,9 @@ msgstr "" msgid "Previous value" msgstr "" +msgid "changes" +msgstr "" + msgid "No chains available for multi-chain deployment" msgstr "" @@ -3695,6 +3746,9 @@ msgstr "" msgid "Reserve 1 NFT of every" msgstr "" +msgid "Select cycle" +msgstr "" + msgid "NFT Projects" msgstr "" @@ -3977,6 +4031,9 @@ msgstr "" msgid "Redeem {tokensLabel} for ETH" msgstr "" +msgid "Past ruleset cycle" +msgstr "" + msgid "Made a mistake?" msgstr "" @@ -4445,6 +4502,9 @@ msgstr "" msgid "Simple token rules that will work for most projects. You can edit these rules in future cycles." msgstr "" +msgid "Confirm unarchive" +msgstr "" + msgid "Start" msgstr "" @@ -4556,6 +4616,9 @@ msgstr "" msgid "ETH" msgstr "" +msgid "Ruleset cycle" +msgstr "" + msgid "The amount of reserved tokens currently available to be distributed to the recipients below." msgstr "" diff --git a/src/packages/v1/contexts/V1ProjectMetadataProvider.tsx b/src/packages/v1/contexts/V1ProjectMetadataProvider.tsx index 27cf12804e..b4845de2eb 100644 --- a/src/packages/v1/contexts/V1ProjectMetadataProvider.tsx +++ b/src/packages/v1/contexts/V1ProjectMetadataProvider.tsx @@ -21,6 +21,8 @@ export function V1ProjectMetadataProvider({ metadata?.archived) ?? false + const isLoading = !metadata + return ( {children} diff --git a/src/packages/v2v3/contexts/V2V3ProjectMetadataProvider.tsx b/src/packages/v2v3/contexts/V2V3ProjectMetadataProvider.tsx index 90bd29a3b5..5d4034b096 100644 --- a/src/packages/v2v3/contexts/V2V3ProjectMetadataProvider.tsx +++ b/src/packages/v2v3/contexts/V2V3ProjectMetadataProvider.tsx @@ -30,6 +30,8 @@ export default function V2V3ProjectMetadataProvider({ projectMetadata?.archived) ?? false + const isLoading = !projectMetadata + return ( diff --git a/src/packages/v4v5/components/Create/components/CreateBadge/RecommendedBadge.tsx b/src/packages/v4v5/components/Create/components/CreateBadge/RecommendedBadge.tsx index e638a0e834..d5c671204d 100644 --- a/src/packages/v4v5/components/Create/components/CreateBadge/RecommendedBadge.tsx +++ b/src/packages/v4v5/components/Create/components/CreateBadge/RecommendedBadge.tsx @@ -7,7 +7,7 @@ export const RecommendedBadge = ({ tooltip }: { tooltip?: ReactNode }) => { return ( - Recommended + Beginner Friendly ) diff --git a/src/packages/v4v5/components/Create/hooks/DeployProject/hooks/useDeployOmnichainProject.ts b/src/packages/v4v5/components/Create/hooks/DeployProject/hooks/useDeployOmnichainProject.ts index 1e1107b1b9..f7489bba0e 100644 --- a/src/packages/v4v5/components/Create/hooks/DeployProject/hooks/useDeployOmnichainProject.ts +++ b/src/packages/v4v5/components/Create/hooks/DeployProject/hooks/useDeployOmnichainProject.ts @@ -1,6 +1,5 @@ import { createSalt, - JB721HookContracts, jb721TiersHookProjectDeployerAbi, JBChainId, jbContractAddress, @@ -18,6 +17,7 @@ import { import { ContractFunctionArgs, encodeFunctionData } from 'viem' import { useWallet } from 'hooks/Wallet' +import { estimateContractGasWithFallback, OMNICHAIN_GAS_FALLBACKS } from 'packages/v4v5/utils/estimateOmnichainGas' export function useDeployOmnichainProject() { const { userAddress } = useWallet() @@ -40,54 +40,68 @@ export function useDeployOmnichainProject() { } const salt = createSalt() - const relayrTransactions = chainIds.map(chainId => { - const suckerDeploymentConfiguration = parseSuckerDeployerConfig( - chainId, - chainIds, - ) - - const chainDeployData = deployData[chainId] - if (!chainDeployData) { - throw new Error('No deploy data for chain: ' + chainId) - } - - const args = [ - chainDeployData[0], - chainDeployData[1], - chainDeployData[2], - chainDeployData[3], - chainDeployData[4], - { - deployerConfigurations: - suckerDeploymentConfiguration.deployerConfigurations, - salt, - }, - ] as const - - // Always use v5 JBController - const controllerAddress = jbContractAddress['5'][JBCoreContracts.JBController][chainId] - const encodedData = encodeFunctionData({ - abi: jbOmnichainDeployer4_1Abi, // ABI of the contract - functionName: 'launchProjectFor', - args: [...args, controllerAddress as `0x${string}`], + const relayrTransactions = await Promise.all( + chainIds.map(async chainId => { + const suckerDeploymentConfiguration = parseSuckerDeployerConfig( + chainId, + chainIds, + ) + + const chainDeployData = deployData[chainId] + if (!chainDeployData) { + throw new Error('No deploy data for chain: ' + chainId) + } + + const baseArgs = [ + chainDeployData[0], + chainDeployData[1], + chainDeployData[2], + chainDeployData[3], + chainDeployData[4], + { + deployerConfigurations: + suckerDeploymentConfiguration.deployerConfigurations, + salt, + }, + ] as const + + // Always use v5 JBController + const controllerAddress = jbContractAddress['5'][JBCoreContracts.JBController][chainId] + const args = [...baseArgs, controllerAddress as `0x${string}`] as const + + // Always use v5 JBOmnichainDeployer + const omnichainDeployerAddress = jbContractAddress['5'][ + JBOmnichainDeployerContracts.JBOmnichainDeployer + ][chainId] as `0x${string}` + + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: omnichainDeployerAddress, + abi: jbOmnichainDeployer4_1Abi, + functionName: 'launchProjectFor', + args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.LAUNCH_PROJECT, + }) + + const encodedData = encodeFunctionData({ + abi: jbOmnichainDeployer4_1Abi, + functionName: 'launchProjectFor', + args, + }) + + return { + data: { + from: userAddress, + to: omnichainDeployerAddress, + value: 0n, + gas, + data: encodedData, + }, + chainId, + } }) - - - // Always use v5 JBOmnichainDeployer - const omnichainDeployerAddress = jbContractAddress['5'][ - JBOmnichainDeployerContracts.JBOmnichainDeployer - ][chainId] - return { - data: { - from: userAddress, - to: omnichainDeployerAddress as `0x${string}`, - value: 0n, - gas: 1_000_000n * BigInt(chainIds.length), - data: encodedData, - }, - chainId, - } - }) + ) return await getRelayrTxQuote(relayrTransactions) } @@ -108,52 +122,64 @@ export function useDeployOmnichainProject() { const salt = createSalt() - const relayrTransactions = chainIds.map(chainId => { - const suckerDeploymentConfiguration = parseSuckerDeployerConfig( - chainId, - chainIds, - ) - - const chainDeployData = deployData[chainId] - if (!chainDeployData) { - throw new Error('No deploy data for chain: ' + chainId) - } - - const args = [ - chainDeployData[0], - chainDeployData[1], - chainDeployData[2], - salt, - { - deployerConfigurations: - suckerDeploymentConfiguration.deployerConfigurations, + const relayrTransactions = await Promise.all( + chainIds.map(async chainId => { + const suckerDeploymentConfiguration = parseSuckerDeployerConfig( + chainId, + chainIds, + ) + + const chainDeployData = deployData[chainId] + if (!chainDeployData) { + throw new Error('No deploy data for chain: ' + chainId) + } + + const args = [ + chainDeployData[0], + chainDeployData[1], + chainDeployData[2], salt, - }, - jbContractAddress['5'][JBCoreContracts.JBController][chainId] as `0x${string}`, // all chains use the same controller - ] as const - - const encodedData = encodeFunctionData({ - abi: jbOmnichainDeployer4_1Abi, // ABI of the contract - functionName: 'launch721ProjectFor', - args, + { + deployerConfigurations: + suckerDeploymentConfiguration.deployerConfigurations, + salt, + }, + jbContractAddress['5'][JBCoreContracts.JBController][chainId] as `0x${string}`, + ] as const + + // Always use v5 JBOmnichainDeployer + const omnichainDeployerAddress = jbContractAddress['5'][ + JBOmnichainDeployerContracts.JBOmnichainDeployer + ][chainId] as `0x${string}` + + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: omnichainDeployerAddress, + abi: jbOmnichainDeployer4_1Abi, + functionName: 'launch721ProjectFor', + args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.LAUNCH_NFT_PROJECT, + }) + + const encodedData = encodeFunctionData({ + abi: jbOmnichainDeployer4_1Abi, + functionName: 'launch721ProjectFor', + args, + }) + + return { + data: { + from: userAddress, + to: omnichainDeployerAddress, + value: 0n, + gas, + data: encodedData, + }, + chainId, + } }) - - - // Always use v5 JBOmnichainDeployer - const omnichainDeployerAddress = jbContractAddress['5'][ - JBOmnichainDeployerContracts.JBOmnichainDeployer - ][chainId] - return { - data: { - from: userAddress, - to: omnichainDeployerAddress as `0x${string}`, - value: 0n, - gas: 3_000_000n * BigInt(chainIds.length), // Bigger mutliple for NFTS. TODO ba5sed might have a better suggestion here. - data: encodedData, - }, - chainId, - } - }) + ) return await getRelayrTxQuote(relayrTransactions) } diff --git a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayConfiguration.tsx b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayConfiguration.tsx index c20335f988..dde241af59 100644 --- a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayConfiguration.tsx +++ b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayConfiguration.tsx @@ -128,7 +128,7 @@ export const PayConfiguration: React.FC = ({ , ticker: 'ETH', type: 'eth', diff --git a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts index d06253d517..1e1498b7fb 100644 --- a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts +++ b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts @@ -1,4 +1,4 @@ -import { NATIVE_TOKEN_DECIMALS, getTokenAToBQuote } from 'juice-sdk-core' +import { ETH_CURRENCY_ID, NATIVE_TOKEN_DECIMALS, getTokenAToBQuote } from 'juice-sdk-core' import { fromWad, parseWad } from 'utils/format/formatNumber' import { useJBRulesetContext, @@ -7,7 +7,6 @@ import { } from 'juice-sdk-react' import { FixedInt } from 'fpnum' -import { V4V5_CURRENCY_USD } from 'packages/v4v5/utils/currency' import { formatUnits } from 'viem' import { tokenSymbolText } from 'utils/tokenSymbolText' import { useCurrencyConverter } from 'hooks/useCurrencyConverter' @@ -25,13 +24,16 @@ export const useProjectPaymentTokens = (): { const { token } = useJBTokenContext() const { data: nftCreditsData } = useV4V5UserNftCredits() const converter = useCurrencyConverter() - + + const baseCurrency = rulesetMetadata.data?.baseCurrency + const isUsdBasedProject = baseCurrency !== undefined && baseCurrency !== ETH_CURRENCY_ID + // Calculate effective payment amount after NFT credits (same logic as usePayAmounts) - const effectivePayAmountWei: number = (() => { + const effectivePayAmountEth: number = (() => { if (!payAmount) return 0 const payAmountWei = parseWad(payAmount.amount) - + if (!nftCreditsData || !chosenNftRewards.length) { return parseFloat(fromWad(payAmountWei)) } @@ -49,8 +51,8 @@ export const useProjectPaymentTokens = (): { if (cartNftValueWei.eq(0)) return parseFloat(fromWad(payAmountWei)) // Credits can only be applied up to the value of NFTs in cart - const maxApplicableCredits = cartNftValueWei.lt(nftCreditsData) - ? cartNftValueWei + const maxApplicableCredits = cartNftValueWei.lt(nftCreditsData) + ? cartNftValueWei : nftCreditsData // And only up to the total pay amount @@ -62,14 +64,19 @@ export const useProjectPaymentTokens = (): { return parseFloat(fromWad(totalAfterCredits)) })() - if (payAmount?.currency === V4V5_CURRENCY_USD) { - // convert to wei first - // TODO support usd payments - } + // For USD-based projects, convert ETH payment to USD value for token calculation + // The project's weight is denominated in the base currency (USD), so we need + // to express the payment in that currency to calculate correct token issuance + const effectivePayAmountInBaseCurrency: number = (() => { + if (!isUsdBasedProject || !converter.usdPerEth) { + return effectivePayAmountEth + } + return effectivePayAmountEth * converter.usdPerEth + })() - const amountBQuote = ruleset.data && rulesetMetadata.data && effectivePayAmountWei > 0 + const amountBQuote = ruleset.data && rulesetMetadata.data && effectivePayAmountInBaseCurrency > 0 ? getTokenAToBQuote( - FixedInt.parse(effectivePayAmountWei.toString(), tokenA.decimals), + FixedInt.parse(effectivePayAmountInBaseCurrency.toString(), tokenA.decimals), { weight: ruleset.data.weight, reservedPercent: rulesetMetadata.data.reservedPercent, diff --git a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/RedeemConfiguration.tsx b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/RedeemConfiguration.tsx index 028735bffe..07f8557b94 100644 --- a/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/RedeemConfiguration.tsx +++ b/src/packages/v4v5/components/ProjectDashboard/V4V5PayRedeemCard/RedeemConfiguration.tsx @@ -14,7 +14,7 @@ import { useWriteContract } from 'wagmi' import { CheckCircleIcon } from '@heroicons/react/24/outline' import { EthereumLogo } from './EthereumLogo' -import Loading from 'components/Loading' +import Image from 'next/legacy/image' import { PayRedeemInput } from './PayRedeemInput' import { ProjectHeaderLogo } from 'components/Project/ProjectHeader/ProjectHeaderLogo' import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' @@ -232,9 +232,18 @@ const RedeemModal: React.FC = ({
{redeeming ? ( <> - -

- Redeeming tokens + {t`Juicebox +

+ Cashing out tokens

Your transaction is processing. diff --git a/src/packages/v4v5/components/modals/V4V5TokenHoldersModal/TokenDistributionChart/TokenPieChart.tsx b/src/packages/v4v5/components/modals/V4V5TokenHoldersModal/TokenDistributionChart/TokenPieChart.tsx index ab0a3b5d75..a4714663b1 100644 --- a/src/packages/v4v5/components/modals/V4V5TokenHoldersModal/TokenDistributionChart/TokenPieChart.tsx +++ b/src/packages/v4v5/components/modals/V4V5TokenHoldersModal/TokenDistributionChart/TokenPieChart.tsx @@ -106,6 +106,7 @@ export default function TokenPieChart({ innerRadius={size / 2 - 60} stroke={stroke} minAngle={1.5} + isAnimationActive={false} > {pieChartData.map((entry, index) => { let fill: string diff --git a/src/packages/v4v5/contexts/V4V5ProjectMetadataProvider.tsx b/src/packages/v4v5/contexts/V4V5ProjectMetadataProvider.tsx index 67f1f41e7d..5ec7e5f491 100644 --- a/src/packages/v4v5/contexts/V4V5ProjectMetadataProvider.tsx +++ b/src/packages/v4v5/contexts/V4V5ProjectMetadataProvider.tsx @@ -15,6 +15,7 @@ export default function V4V5ProjectMetadataProvider({ const { metadata } = useJBProjectMetadataContext() const projectMetadata = metadata?.data ?? undefined + const isLoading = metadata?.isLoading ?? true const isArchived = false @@ -25,6 +26,7 @@ export default function V4V5ProjectMetadataProvider({ isArchived, projectId: _projectId, pv: PV_V4, + isLoading, refetchProjectMetadata: () => null, // TODO }} > diff --git a/src/packages/v4v5/graphql/queries/activityEvents.graphql b/src/packages/v4v5/graphql/queries/activityEvents.graphql index 451c364d96..cb67a3a31d 100644 --- a/src/packages/v4v5/graphql/queries/activityEvents.graphql +++ b/src/packages/v4v5/graphql/queries/activityEvents.graphql @@ -27,6 +27,7 @@ query ActivityEvents( token currency decimals + version } payEvent { id diff --git a/src/packages/v4v5/hooks/useDeployOmnichainErc20.ts b/src/packages/v4v5/hooks/useDeployOmnichainErc20.ts index 1b81c61657..eef2fc08a1 100644 --- a/src/packages/v4v5/hooks/useDeployOmnichainErc20.ts +++ b/src/packages/v4v5/hooks/useDeployOmnichainErc20.ts @@ -5,6 +5,7 @@ import { ContractFunctionArgs, encodeFunctionData } from 'viem' import { useWallet } from 'hooks/Wallet' import { Address } from 'viem' import { useV4V5Version } from '../contexts/V4V5VersionProvider' +import { estimateContractGasWithFallback, OMNICHAIN_GAS_FALLBACKS } from '../utils/estimateOmnichainGas' export function useDeployOmnichainErc20() { const { userAddress } = useWallet() @@ -23,41 +24,46 @@ export function useDeployOmnichainErc20() { }, chainIds: JBChainId[], ) { - if (!userAddress) return + if (!userAddress || !projectControllerAddress) return - const relayrTransactions = chainIds.map(chainId => { - const args = deployData[chainId] - let encoded - if (!args) throw new Error('No deploy data for chain ' + chainId) - - if (version === 4 && projectControllerAddress === jbContractAddress['4'][JBCoreContracts.JBController4_1][chainId]) { - // Use v4.1 controller ABI - encoded = encodeFunctionData({ - abi: jbController4_1Abi, + const relayrTransactions = await Promise.all( + chainIds.map(async chainId => { + const args = deployData[chainId] + if (!args) throw new Error('No deploy data for chain ' + chainId) + + const useV41Abi = version === 4 && projectControllerAddress === jbContractAddress['4'][JBCoreContracts.JBController4_1][chainId] + const abi = useV41Abi ? jbController4_1Abi : jbControllerAbi + + const to = projectControllerAddress as Address + + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: to, + abi, functionName: 'deployERC20For', args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.DEPLOY_ERC20, }) - } else { - // Use v4 controller ABI - encoded = encodeFunctionData({ - abi: jbControllerAbi, + + const encoded = encodeFunctionData({ + abi, functionName: 'deployERC20For', args, }) - } - const to = projectControllerAddress as Address - return { - data: { - from: userAddress, - to, - value: 0n, - gas: 300_000n * BigInt(chainIds.length), - data: encoded, - }, - chainId, - } - }) + return { + data: { + from: userAddress, + to, + value: 0n, + gas, + data: encoded, + }, + chainId, + } + }) + ) return getRelayrTxQuote(relayrTransactions) } diff --git a/src/packages/v4v5/hooks/useETHReceivedFromTokens.ts b/src/packages/v4v5/hooks/useETHReceivedFromTokens.ts index e62415e21e..4e9f9bf3fc 100644 --- a/src/packages/v4v5/hooks/useETHReceivedFromTokens.ts +++ b/src/packages/v4v5/hooks/useETHReceivedFromTokens.ts @@ -16,11 +16,12 @@ export function useETHReceivedFromTokens( const { projectId, contracts, contractAddress } = useJBContractContext() const { data: totalSupply } = useReadContract({ abi: jbTokensAbi, - address: contractAddress(JBCoreContracts.JBTokens), + address: contractAddress(JBCoreContracts.JBTokens, chainId), functionName: 'totalSupplyOf', args: [projectId], + chainId, }) - const { data: nativeTokenSurplus } = useNativeTokenSurplus() + const { data: nativeTokenSurplus } = useNativeTokenSurplus({ chainId }) const { rulesetMetadata } = useJBRulesetByChain(chainId) const { data: tokensReserved } = useReadContract({ abi: jbControllerAbi, diff --git a/src/packages/v4v5/hooks/useJBAllRulesetsCrossChain.ts b/src/packages/v4v5/hooks/useJBAllRulesetsCrossChain.ts index f4765818f8..5c6cc65ed5 100644 --- a/src/packages/v4v5/hooks/useJBAllRulesetsCrossChain.ts +++ b/src/packages/v4v5/hooks/useJBAllRulesetsCrossChain.ts @@ -1,16 +1,24 @@ -import { CashOutTaxRate, ReservedPercent, RulesetWeight, WeightCutPercent, jbControllerAbi, jbContractAddress, JBCoreContracts } from "juice-sdk-core" +import { CashOutTaxRate, ReservedPercent, RulesetWeight, WeightCutPercent, jbControllerAbi, jbContractAddress, JBCoreContracts, JBRulesetData, JBRulesetMetadata } from "juice-sdk-core" import { useReadContract } from "wagmi" import { JBChainId } from "juice-sdk-react" import { useV4V5Version } from '../contexts/V4V5VersionProvider' +export type RulesetWithMetadata = { + ruleset: JBRulesetData + metadata: JBRulesetMetadata +} + export function useJBAllRulesetsCrossChain({ projectId, - rulesetNumber, - chainId + startingId, + chainId, + size = 10n, }: { projectId: bigint - rulesetNumber: bigint + /** The ruleset ID to start fetching from (going backwards). Use ruleset.id, not cycleNumber. */ + startingId: bigint chainId: JBChainId + size?: bigint }) { const { version } = useV4V5Version() // For v4, use JBController4_1. For v5, use standard JBController @@ -18,26 +26,26 @@ export function useJBAllRulesetsCrossChain({ ? jbContractAddress['4'][JBCoreContracts.JBController4_1][chainId] : jbContractAddress['5'][JBCoreContracts.JBController][chainId] - const { data, isLoading } = useReadContract({ + const { data, isLoading, refetch } = useReadContract({ abi: jbControllerAbi, address: controllerAddress, functionName: 'allRulesetsOf', args: [ projectId, - rulesetNumber, - 10n, // size (The maximum number of rulesets to return). Arbritrarily set + startingId, + size, ], chainId }) - if (!data) return { data: undefined, isLoading } + if (!data) return { data: undefined, isLoading, refetch } return { - data: data?.map((obj) => ({ + data: data?.map((obj): RulesetWithMetadata => ({ ruleset: { - ...obj.ruleset, - weight: new RulesetWeight(obj.ruleset.weight), - weightCutPercent: new WeightCutPercent(obj.ruleset.weightCutPercent), + ...obj.ruleset, + weight: new RulesetWeight(obj.ruleset.weight), + weightCutPercent: new WeightCutPercent(obj.ruleset.weightCutPercent), }, metadata: { ...obj.metadata, @@ -45,6 +53,7 @@ export function useJBAllRulesetsCrossChain({ reservedPercent: new ReservedPercent(obj.metadata.reservedPercent) } })), - isLoading + isLoading, + refetch, } } diff --git a/src/packages/v4v5/hooks/useJBRulesetHistory.ts b/src/packages/v4v5/hooks/useJBRulesetHistory.ts new file mode 100644 index 0000000000..92ebd406f8 --- /dev/null +++ b/src/packages/v4v5/hooks/useJBRulesetHistory.ts @@ -0,0 +1,305 @@ +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { useJBProjectId, useJBRuleset } from 'juice-sdk-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useJBAllRulesetsCrossChain, RulesetWithMetadata } from './useJBAllRulesetsCrossChain' +import { useJBUpcomingRuleset } from './useJBUpcomingRuleset' +import { useCyclesPanelSelectedChain } from '../views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/contexts/CyclesPanelSelectedChainContext' + +export type CycleStatus = 'current' | 'upcoming' | 'past' + +export type CycleOption = { + cycleNumber: number + status: CycleStatus + start: number + duration: number +} + +export type RulesetDiff = { + field: string + current: unknown + previous: unknown +} + +const INITIAL_LOAD_SIZE = 20n +const LOAD_MORE_SIZE = 10n + +export function useJBRulesetHistory() { + const { selectedChainId } = useCyclesPanelSelectedChain() + const { projectId } = useJBProjectId(selectedChainId) + + const [selectedCycleNumber, setSelectedCycleNumber] = useState(null) + const [loadedRulesets, setLoadedRulesets] = useState([]) + const [hasMoreRulesets, setHasMoreRulesets] = useState(true) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const [historicalDataLoaded, setHistoricalDataLoaded] = useState(false) + + // Get current ruleset + const { + ruleset: currentRuleset, + rulesetMetadata: currentRulesetMetadata, + isLoading: currentRulesetLoading, + } = useJBRuleset({ + chainId: selectedChainId, + projectId, + }) + + // Get upcoming ruleset + const { + ruleset: upcomingRuleset, + rulesetMetadata: upcomingRulesetMetadata, + isLoading: upcomingRulesetLoading, + } = useJBUpcomingRuleset(selectedChainId) + + // Use the ruleset ID (not cycleNumber) for fetching historical rulesets + // The allRulesetsOf contract method takes startingId which is the ruleset.id + const currentRulesetId = currentRuleset?.id ?? 0 + + // Fetch historical rulesets starting from current ruleset ID (going backwards) + const { data: historicalRulesets, isLoading: historicalRulesetsLoading } = + useJBAllRulesetsCrossChain({ + projectId: BigInt(projectId ?? 0), + startingId: BigInt(currentRulesetId || 1), + chainId: selectedChainId!, + size: INITIAL_LOAD_SIZE, + }) + + // Initialize loaded rulesets when historical data is fetched (only once) + useEffect(() => { + if (historicalRulesets && historicalRulesets.length > 0 && !historicalDataLoaded) { + setLoadedRulesets(historicalRulesets) + setHistoricalDataLoaded(true) + // If we got fewer rulesets than requested, there are no more to load + setHasMoreRulesets(historicalRulesets.length >= Number(INITIAL_LOAD_SIZE)) + } + }, [historicalRulesets, historicalDataLoaded]) + + // Set default selected cycle to current when data loads + useEffect(() => { + if (currentRuleset && selectedCycleNumber === null) { + setSelectedCycleNumber(currentRuleset.cycleNumber) + } + }, [currentRuleset, selectedCycleNumber]) + + // Determine if upcoming ruleset is different from current + const hasUpcoming = useMemo(() => { + if (!upcomingRuleset || !currentRuleset) return false + return upcomingRuleset.cycleNumber > currentRuleset.cycleNumber + }, [upcomingRuleset, currentRuleset]) + + // Build the list of available cycles for the dropdown + const cycleOptions = useMemo((): CycleOption[] => { + const options: CycleOption[] = [] + + // Add upcoming if available + if (hasUpcoming && upcomingRuleset) { + options.push({ + cycleNumber: upcomingRuleset.cycleNumber, + status: 'upcoming', + start: upcomingRuleset.start, + duration: upcomingRuleset.duration, + }) + } + + // Add current + if (currentRuleset) { + options.push({ + cycleNumber: currentRuleset.cycleNumber, + status: 'current', + start: currentRuleset.start, + duration: currentRuleset.duration, + }) + } + + // Add historical from loaded rulesets (skip current as it's already added) + loadedRulesets.forEach(({ ruleset }) => { + if (ruleset.cycleNumber < (currentRuleset?.cycleNumber ?? 0)) { + options.push({ + cycleNumber: ruleset.cycleNumber, + status: 'past', + start: ruleset.start, + duration: ruleset.duration, + }) + } + }) + + // Sort by cycle number descending (newest first) + return options.sort((a, b) => b.cycleNumber - a.cycleNumber) + }, [hasUpcoming, upcomingRuleset, currentRuleset, loadedRulesets]) + + // Get the currently selected ruleset data + const selectedRuleset = useMemo((): { + ruleset: JBRulesetData | undefined + metadata: JBRulesetMetadata | undefined + status: CycleStatus + } => { + if (!selectedCycleNumber) { + return { ruleset: undefined, metadata: undefined, status: 'current' } + } + + // Check if it's upcoming + if (hasUpcoming && upcomingRuleset?.cycleNumber === selectedCycleNumber) { + return { + ruleset: upcomingRuleset, + metadata: upcomingRulesetMetadata, + status: 'upcoming', + } + } + + // Check if it's current + if (currentRuleset?.cycleNumber === selectedCycleNumber) { + return { + ruleset: currentRuleset, + metadata: currentRulesetMetadata, + status: 'current', + } + } + + // Find in historical + const historical = loadedRulesets.find( + r => r.ruleset.cycleNumber === selectedCycleNumber, + ) + if (historical) { + return { + ruleset: historical.ruleset, + metadata: historical.metadata, + status: 'past', + } + } + + return { ruleset: undefined, metadata: undefined, status: 'past' } + }, [ + selectedCycleNumber, + hasUpcoming, + upcomingRuleset, + upcomingRulesetMetadata, + currentRuleset, + currentRulesetMetadata, + loadedRulesets, + ]) + + // Get the previous ruleset for diff comparison + const previousRuleset = useMemo((): RulesetWithMetadata | undefined => { + if (!selectedCycleNumber) return undefined + + // Find the ruleset with cycle number - 1 + const prevCycleNumber = selectedCycleNumber - 1 + if (prevCycleNumber < 1) return undefined + + // Check if it's in loaded rulesets + return loadedRulesets.find(r => r.ruleset.cycleNumber === prevCycleNumber) + }, [selectedCycleNumber, loadedRulesets]) + + // Load more historical rulesets + const loadMoreRulesets = useCallback(async () => { + if (!hasMoreRulesets || isLoadingMore || loadedRulesets.length === 0) return + + setIsLoadingMore(true) + + // Find the oldest loaded ruleset + const oldestCycle = Math.min(...loadedRulesets.map(r => r.ruleset.cycleNumber)) + + if (oldestCycle <= 1) { + setHasMoreRulesets(false) + setIsLoadingMore(false) + return + } + + // Note: In a real implementation, you'd need to use a separate query here + // For now, we indicate that more can be loaded but the actual loading + // would require adjusting the hook to support dynamic fetching + setIsLoadingMore(false) + }, [hasMoreRulesets, isLoadingMore, loadedRulesets]) + + // Navigation functions + const goToNextCycle = useCallback(() => { + if (!selectedCycleNumber) return + + const currentIndex = cycleOptions.findIndex( + opt => opt.cycleNumber === selectedCycleNumber, + ) + if (currentIndex > 0) { + setSelectedCycleNumber(cycleOptions[currentIndex - 1].cycleNumber) + } + }, [selectedCycleNumber, cycleOptions]) + + const goToPreviousCycle = useCallback(() => { + if (!selectedCycleNumber) return + + const currentIndex = cycleOptions.findIndex( + opt => opt.cycleNumber === selectedCycleNumber, + ) + if (currentIndex < cycleOptions.length - 1) { + setSelectedCycleNumber(cycleOptions[currentIndex + 1].cycleNumber) + } + }, [selectedCycleNumber, cycleOptions]) + + const jumpToCycle = useCallback((cycleNumber: number) => { + setSelectedCycleNumber(cycleNumber) + }, []) + + const goToCurrent = useCallback(() => { + if (currentRuleset) { + setSelectedCycleNumber(currentRuleset.cycleNumber) + } + }, [currentRuleset]) + + // Determine if navigation is possible + const canGoNext = useMemo(() => { + if (!selectedCycleNumber) return false + const currentIndex = cycleOptions.findIndex( + opt => opt.cycleNumber === selectedCycleNumber, + ) + return currentIndex > 0 + }, [selectedCycleNumber, cycleOptions]) + + const canGoPrevious = useMemo(() => { + if (!selectedCycleNumber) return false + const currentIndex = cycleOptions.findIndex( + opt => opt.cycleNumber === selectedCycleNumber, + ) + return currentIndex < cycleOptions.length - 1 + }, [selectedCycleNumber, cycleOptions]) + + const isLoading = + currentRulesetLoading || upcomingRulesetLoading || historicalRulesetsLoading + + const totalCycles = currentRuleset?.cycleNumber ?? 0 + + return { + // Selected ruleset data + selectedRuleset: selectedRuleset.ruleset, + selectedRulesetMetadata: selectedRuleset.metadata, + selectedCycleNumber, + selectedCycleStatus: selectedRuleset.status, + + // Previous ruleset for diff + previousRuleset: previousRuleset?.ruleset, + previousRulesetMetadata: previousRuleset?.metadata, + + // Current ruleset reference + currentRuleset, + currentRulesetMetadata, + currentCycleNumber: currentRuleset?.cycleNumber, + + // Upcoming ruleset + upcomingRuleset: hasUpcoming ? upcomingRuleset : undefined, + upcomingRulesetMetadata: hasUpcoming ? upcomingRulesetMetadata : undefined, + hasUpcoming, + + // Navigation + cycleOptions, + canGoNext, + canGoPrevious, + goToNextCycle, + goToPreviousCycle, + jumpToCycle, + goToCurrent, + totalCycles, + + // Loading states + isLoading, + isLoadingMore, + hasMoreRulesets, + loadMoreRulesets, + } +} diff --git a/src/packages/v4v5/hooks/useOmnichainEditProjectDetailsTx.ts b/src/packages/v4v5/hooks/useOmnichainEditProjectDetailsTx.ts index ba249a7b86..e56e26fbf1 100644 --- a/src/packages/v4v5/hooks/useOmnichainEditProjectDetailsTx.ts +++ b/src/packages/v4v5/hooks/useOmnichainEditProjectDetailsTx.ts @@ -1,9 +1,10 @@ -import { JBChainId, createSalt, jbControllerAbi } from 'juice-sdk-core' +import { JBChainId, jbControllerAbi } from 'juice-sdk-core' import { useGetRelayrTxBundle, useGetRelayrTxQuote, useJBContractContext, useSendRelayrTx } from 'juice-sdk-react' import { useWallet } from 'hooks/Wallet' import { useSuckers } from 'juice-sdk-react' import { encodeFunctionData } from 'viem' +import { estimateContractGasWithFallback, OMNICHAIN_GAS_FALLBACKS } from '../utils/estimateOmnichainGas' export function useOmnichainEditProjectDetailsTx() { const { userAddress } = useWallet() @@ -18,21 +19,35 @@ export function useOmnichainEditProjectDetailsTx() { ) { if (!userAddress || !controllerAddress) return if (!suckers || suckers.length === 0) throw new Error('No project chains available') - const salt = createSalt() - const txs = suckers.map(sucker => { - const chainId = sucker.peerChainId as JBChainId - const projectId = BigInt(sucker.projectId) - const encoded = encodeFunctionData({ - abi: jbControllerAbi, - functionName: 'setUriOf', - args: [projectId, cid], + + const txs = await Promise.all( + suckers.map(async sucker => { + const chainId = sucker.peerChainId as JBChainId + const projectId = BigInt(sucker.projectId) + const args = [projectId, cid] as const + + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: controllerAddress as `0x${string}`, + abi: jbControllerAbi, + functionName: 'setUriOf', + args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.SET_URI, + }) + + const encoded = encodeFunctionData({ + abi: jbControllerAbi, + functionName: 'setUriOf', + args, + }) + const to = controllerAddress as `0x${string}` + return { + data: { from: userAddress, to, value: 0n, gas, data: encoded }, + chainId, + } }) - const to = controllerAddress as `0x${string}` - return { - data: { from: userAddress, to, value: 0n, gas: 200_000n * BigInt(suckers.length), data: encoded }, - chainId, - } - }) + ) return getRelayrTxQuote(txs) } diff --git a/src/packages/v4v5/hooks/useTransferOmnichainProjectOwnership.ts b/src/packages/v4v5/hooks/useTransferOmnichainProjectOwnership.ts index 5801f1e022..88b7a20467 100644 --- a/src/packages/v4v5/hooks/useTransferOmnichainProjectOwnership.ts +++ b/src/packages/v4v5/hooks/useTransferOmnichainProjectOwnership.ts @@ -4,6 +4,7 @@ import { Address, encodeFunctionData } from 'viem' import { useWallet } from 'hooks/Wallet' import { useV4V5Version } from '../contexts/V4V5VersionProvider' +import { estimateContractGasWithFallback, OMNICHAIN_GAS_FALLBACKS } from '../utils/estimateOmnichainGas' export function useTransferOmnichainProjectOwnership() { const { userAddress } = useWallet() @@ -23,33 +24,47 @@ export function useTransferOmnichainProjectOwnership() { } }, chainIds: JBChainId[], - ) { + ) { if (!userAddress) { return } - const relayrTransactions = chainIds.map(chainId => { - const args = transferData[chainId] - if (!args) throw new Error('No transfer data for chain ' + chainId) - - const encoded = encodeFunctionData({ - abi: jbProjectsAbi, - functionName: 'safeTransferFrom', - args: [args.from, args.to, args.tokenId], + const relayrTransactions = await Promise.all( + chainIds.map(async chainId => { + const transferArgs = transferData[chainId] + if (!transferArgs) throw new Error('No transfer data for chain ' + chainId) + + const args = [transferArgs.from, transferArgs.to, transferArgs.tokenId] as const + const to = jbContractAddress[versionString][JBCoreContracts.JBProjects][chainId] as Address + + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: to, + abi: jbProjectsAbi, + functionName: 'safeTransferFrom', + args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.TRANSFER_OWNERSHIP, + }) + + const encoded = encodeFunctionData({ + abi: jbProjectsAbi, + functionName: 'safeTransferFrom', + args, + }) + + return { + data: { + from: userAddress, + to, + value: 0n, + gas, + data: encoded, + }, + chainId, + } }) - - const to = jbContractAddress[versionString][JBCoreContracts.JBProjects][chainId] as Address - return { - data: { - from: userAddress, - to, - value: 0n, - gas: 300_000n * BigInt(chainIds.length), - data: encoded, - }, - chainId, - } - }) + ) const result = await getRelayrTxQuote(relayrTransactions) return result diff --git a/src/packages/v4v5/hooks/useV4V5WalletHasPermission.ts b/src/packages/v4v5/hooks/useV4V5WalletHasPermission.ts index 4a8b469628..07c64823ee 100644 --- a/src/packages/v4v5/hooks/useV4V5WalletHasPermission.ts +++ b/src/packages/v4v5/hooks/useV4V5WalletHasPermission.ts @@ -57,10 +57,5 @@ export function useV4V5WalletHasPermission( // - wallet is direct owner // - wallet has explicit operator permission // - wallet is a signer on the Safe that owns the project (covers multisig ownership case) - // - in development environment - return ( - isOwner || - hasOperatorPermission.data || - process.env.NODE_ENV === 'development' - ) + return isOwner || !!hasOperatorPermission.data } diff --git a/src/packages/v4v5/utils/estimateOmnichainGas.ts b/src/packages/v4v5/utils/estimateOmnichainGas.ts new file mode 100644 index 0000000000..540a582749 --- /dev/null +++ b/src/packages/v4v5/utils/estimateOmnichainGas.ts @@ -0,0 +1,85 @@ +import { Abi, Address, Client, encodeFunctionData, PublicClient, Transport } from 'viem' +import { estimateGas } from 'viem/actions' +import { JBChainId } from 'juice-sdk-core' +import { wagmiConfig } from 'contexts/Para/Providers' +import { Chain } from 'viem/chains' + +const GAS_BUFFER_PERCENT = 120n // 20% buffer + +/** + * Fallback gas values per operation type. + * These are based on the original hardcoded values in the codebase, + * used when dynamic estimation fails. + */ +export const OMNICHAIN_GAS_FALLBACKS = { + SET_URI: 200_000n, + QUEUE_RULESETS: 200_000n, + DEPLOY_ERC20: 300_000n, + TRANSFER_OWNERSHIP: 300_000n, + LAUNCH_PROJECT: 1_000_000n, + LAUNCH_NFT_PROJECT: 3_000_000n, +} as const + +export type EstimateGasParams = { + chainId: JBChainId + contractAddress: Address + abi: Abi + functionName: string + args: readonly unknown[] + userAddress: Address + fallbackGas: bigint +} + +/** + * Estimates gas for a contract call on a specific chain. + * Applies a 20% buffer to the estimate. + * Falls back to the provided fallbackGas value if estimation fails. + */ +export async function estimateContractGasWithFallback({ + chainId, + contractAddress, + abi, + functionName, + args, + userAddress, + fallbackGas, +}: EstimateGasParams): Promise { + try { + const client = wagmiConfig.getClient({ chainId }) as Client + + const data = encodeFunctionData({ + abi, + functionName, + args, + }) + + const gasEstimate = await estimateGas(client as PublicClient, { + to: contractAddress, + data, + account: userAddress as Address, + }) + + // Apply 20% buffer + return (gasEstimate * GAS_BUFFER_PERCENT) / 100n + } catch { + // Silent fallback to default gas value + return fallbackGas + } +} + +/** + * Estimates gas for multiple chains in parallel. + * Each chain estimation is independent and uses its own fallback. + */ +export async function estimateGasForChains( + chainEstimates: EstimateGasParams[], +): Promise> { + const results = await Promise.all( + chainEstimates.map(async (params) => { + const gas = await estimateContractGasWithFallback(params) + return { chainId: params.chainId, gas } + }), + ) + + return new Map(results.map(({ chainId, gas }) => [chainId, gas])) +} diff --git a/src/packages/v4v5/utils/rulesetDiff.ts b/src/packages/v4v5/utils/rulesetDiff.ts index 3e3e7becf8..dd099962e6 100644 --- a/src/packages/v4v5/utils/rulesetDiff.ts +++ b/src/packages/v4v5/utils/rulesetDiff.ts @@ -4,18 +4,22 @@ export function getDiffedAttrBetweenRulesets(rulesetsByChain: Record) { + const rulesets = Object.values(rulesetsByChain) + + if (rulesets.length === 0) return {} + // Get a list of all attributes we're keeping (diffed attributes) - const diffedRulesetAttrs = Object.keys(Object.values(rulesetsByChain)[0].ruleset).filter((rulesetAttr) => ( + const diffedRulesetAttrs = Object.keys(rulesets[0].ruleset).filter((rulesetAttr) => ( // @ts-ignore - rulesets.slice(1).some(({ ruleset }) => Boolean(ruleset[rulesetAttr] !== rulesets[0][rulesetAttr])) + rulesets.slice(1).some(({ ruleset }) => Boolean(ruleset[rulesetAttr] !== rulesets[0].ruleset[rulesetAttr])) )) - const diffedMetadataAttrs = Object.keys(Object.values(rulesetsByChain)[0].metadata).filter((metadataAttr) => ( - // @ts-ignore - rulesets.slice(1).some(({ ruleset }) => Boolean(ruleset[metadataAttr] !== rulesets[0][metadataAttr])) - )) + const diffedMetadataAttrs = Object.keys(rulesets[0].metadata).filter((metadataAttr) => ( + // @ts-ignore + rulesets.slice(1).some(({ metadata }) => Boolean(metadata[metadataAttr] !== rulesets[0].metadata[metadataAttr])) + )) - if (!diffedRulesetAttrs.length && !diffedMetadataAttrs) return [] + if (!diffedRulesetAttrs.length && !diffedMetadataAttrs.length) return {} const rulesetsWithDiffedAttrsOnly = {} as Record> @@ -24,7 +28,7 @@ export function getDiffedAttrBetweenRulesets(rulesetsByChain: Record { // @ts-ignore rulesetsWithDiffedAttrsOnly[chainId][rulesetAttr] = ruleset[rulesetAttr] @@ -32,7 +36,7 @@ export function getDiffedAttrBetweenRulesets(rulesetsByChain: Record { // @ts-ignore - rulesetsWithDiffedAttrsOnly[chainId][metadataAttr] = metadata[rulesetAttr] + rulesetsWithDiffedAttrsOnly[chainId][metadataAttr] = metadata[metadataAttr] }) }) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectDashboard.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectDashboard.tsx index 9e19d61fa8..371b0679c3 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectDashboard.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectDashboard.tsx @@ -1,5 +1,7 @@ import { Footer } from 'components/Footer/Footer' +import Loading from 'components/Loading' import { CoverPhoto } from 'components/Project/ProjectHeader/CoverPhoto' +import { useProjectMetadataContext } from 'contexts/ProjectMetadataContext' import { SuccessPayView } from 'packages/v4v5/components/ProjectDashboard/components/SuccessPayView/SuccessPayView' import { useProjectDispatch } from 'packages/v4v5/components/ProjectDashboard/redux/hooks' import { payRedeemActions } from 'packages/v4v5/components/ProjectDashboard/redux/payRedeemSlice' @@ -14,6 +16,7 @@ import { ProjectActivityList } from './components/ProjectActivityList' export default function V4V5ProjectDashboard() { const { projectPayReceipt } = useProjectPageQueries() + const { isLoading } = useProjectMetadataContext() if (projectPayReceipt !== undefined) { return ( @@ -23,6 +26,14 @@ export default function V4V5ProjectDashboard() { ) } + if (isLoading) { + return ( +
+ +
+ ) + } + return ( <> diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx index a678126a49..5c09b31b97 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx @@ -11,6 +11,8 @@ import { Button } from 'antd' import { JuiceListbox } from 'components/inputs/JuiceListbox' import Loading from 'components/Loading' import RichNote from 'components/RichNote/RichNote' +import Link from 'next/link' +import { v4v5ProjectRoute } from 'packages/v4v5/utils/routes' import { ETH_TOKEN_ADDRESS } from 'constants/juiceboxTokens' import { NETWORKS } from 'constants/networks' import { useActivityEventsQuery, useProjectQuery } from 'generated/v4v5/graphql' @@ -25,7 +27,7 @@ import { ActivityEvent } from './activityEventElems/ActivityElement' const PAGE_SIZE = 10 -const IGNORED_EVENTS = ['mintNftEvent'] +const IGNORED_EVENTS = ['mintNftEvent', 'burnEvent'] const baseEventFilter = IGNORED_EVENTS.reduce( (acc, curr) => ({ @@ -85,9 +87,9 @@ export function V4V5ActivityList() { return activityEvents.activityEvents.items .map(transformEventData) .filter((event): event is AnyEvent => !!event) - .map(e => translateEventDataToPresenter(e, tokenSymbol)) + .map(e => translateEventDataToPresenter(e, tokenSymbol, version)) }, - [activityEvents?.activityEvents.items, tokenSymbol], + [activityEvents?.activityEvents.items, tokenSymbol, version], ) return ( @@ -161,7 +163,10 @@ function getCurrencySymbol(currency?: string | null): string { export function translateEventDataToPresenter( event: AnyEvent, tokenSymbol: string | undefined, + version?: 4 | 5, ) { + // Use passed version or fall back to event's projectVersion + const effectiveVersion = version ?? event.projectVersion // Use projectToken (the actual token address) for currency symbol lookup const currencySymbol = getCurrencySymbol(event.projectToken) // Use project decimals (e.g., 6 for USDC, 18 for ETH) @@ -240,9 +245,22 @@ export function translateEventDataToPresenter( ), extra: ( - +
+ + Paid out: Ξ{formatActivityAmount(event.amountPaidOut.value)},{' '} + {effectiveVersion ? ( + + $NANA fee + + ) : ( + '$NANA fee' + )} + : Ξ{formatActivityAmount(event.fee.value)} + +
), } case 'distributeReservedTokensEvent': diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData.ts index c0325c570a..e58e27ae78 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData.ts +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData.ts @@ -27,6 +27,7 @@ export interface Event { projectToken?: string | null projectCurrency?: string | null projectDecimals?: number | null + projectVersion?: 4 | 5 | null timestamp: number txHash: string from: string @@ -168,6 +169,7 @@ function extractBaseEventData( projectToken?: string | null, projectCurrency?: string | null, projectDecimals?: number | null, + projectVersion?: 4 | 5 | null, ): AnyEvent { return { // Make type null and set it later @@ -181,6 +183,7 @@ function extractBaseEventData( projectToken, projectCurrency, projectDecimals, + projectVersion, timestamp: event.timestamp, txHash: event.txHash, from: event.from, @@ -200,6 +203,7 @@ export function transformEventData( // Currency appears to be an ID/enum, not the token address const projectCurrency = data.project?.currency ? String(data.project.currency) : null const projectDecimals = data.project?.decimals ? Number(data.project.decimals) : null + const projectVersion = data.project?.version === 4 || data.project?.version === 5 ? data.project.version : null // Check for aggregated events first // TODO: Aggregated event handling - temporarily disabled @@ -212,7 +216,7 @@ export function transformEventData( // Handle individual events if (data.payEvent) { return { - ...extractBaseEventData(data.payEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.payEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'payEvent', amount: new Ether(BigInt(data.payEvent.amount)), @@ -227,7 +231,7 @@ export function transformEventData( } if (data.addToBalanceEvent) { return { - ...extractBaseEventData(data.addToBalanceEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.addToBalanceEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'addToBalanceEvent', amount: new Ether(BigInt(data.addToBalanceEvent.amount)), @@ -236,7 +240,7 @@ export function transformEventData( } if (data.manualMintTokensEvent) { return { - ...extractBaseEventData(data.manualMintTokensEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.manualMintTokensEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'manualMintTokensEvent', amount: new Ether(BigInt(data.manualMintTokensEvent.tokenCount)), @@ -246,7 +250,7 @@ export function transformEventData( } if (data.cashOutTokensEvent) { return { - ...extractBaseEventData(data.cashOutTokensEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.cashOutTokensEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'cashOutEvent', metadata: data.cashOutTokensEvent.metadata, @@ -258,7 +262,7 @@ export function transformEventData( } if (data.deployErc20Event) { return { - ...extractBaseEventData(data.deployErc20Event, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.deployErc20Event, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'deployedERC20Event', symbol: data.deployErc20Event.symbol, @@ -267,14 +271,14 @@ export function transformEventData( } if (data.projectCreateEvent) { return { - ...extractBaseEventData(data.projectCreateEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.projectCreateEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'projectCreateEvent', } } if (data.sendPayoutsEvent) { return { - ...extractBaseEventData(data.sendPayoutsEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.sendPayoutsEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'distributePayoutsEvent', amount: new Ether(BigInt(data.sendPayoutsEvent.amount)), @@ -286,7 +290,7 @@ export function transformEventData( } if (data.sendReservedTokensToSplitsEvent) { return { - ...extractBaseEventData(data.sendReservedTokensToSplitsEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.sendReservedTokensToSplitsEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'distributeReservedTokensEvent', rulesetCycleNumber: @@ -296,7 +300,7 @@ export function transformEventData( } if (data.sendReservedTokensToSplitEvent) { return { - ...extractBaseEventData(data.sendReservedTokensToSplitEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.sendReservedTokensToSplitEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'distributeToReservedTokenSplitEvent', tokenCount: data.sendReservedTokensToSplitEvent.tokenCount, @@ -310,7 +314,7 @@ export function transformEventData( } if (data.sendPayoutToSplitEvent) { return { - ...extractBaseEventData(data.sendPayoutToSplitEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.sendPayoutToSplitEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'distributeToPayoutSplitEvent', amount: new Ether(BigInt(data.sendPayoutToSplitEvent.amount)), @@ -323,7 +327,7 @@ export function transformEventData( } if (data.useAllowanceEvent) { return { - ...extractBaseEventData(data.useAllowanceEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.useAllowanceEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'useAllowanceEvent', rulesetId: BigInt(data.useAllowanceEvent.rulesetId), @@ -341,7 +345,7 @@ export function transformEventData( } if (data.manualBurnEvent) { return { - ...extractBaseEventData(data.manualBurnEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals), + ...extractBaseEventData(data.manualBurnEvent, projectName, projectHandle, projectLogoUri, projectToken, projectCurrency, projectDecimals, projectVersion), chainId: data.chainId, type: 'manualBurnEvent', holder: data.manualBurnEvent.from, diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationDisplayCard.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationDisplayCard.tsx new file mode 100644 index 0000000000..b0f446e71c --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationDisplayCard.tsx @@ -0,0 +1,72 @@ +import { Disclosure, Transition } from '@headlessui/react' +import { ChevronDownIcon } from '@heroicons/react/24/outline' +import { t, Trans } from '@lingui/macro' +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { Fragment, useMemo } from 'react' +import { twMerge } from 'tailwind-merge' +import { CycleStatus } from 'packages/v4v5/hooks/useJBRulesetHistory' +import { V4V5CycleConfigurationPanel } from './V4V5CycleConfigurationPanel' + +export const V4V5CycleConfigurationDisplayCard = ({ + ruleset, + rulesetMetadata, + cycleStatus, +}: { + ruleset: JBRulesetData | undefined + rulesetMetadata: JBRulesetMetadata | undefined + cycleStatus: CycleStatus +}) => { + const title = useMemo(() => { + switch (cycleStatus) { + case 'current': + return t`Current ruleset cycle` + case 'upcoming': + return t`Upcoming ruleset cycle` + case 'past': + return t`Past ruleset cycle` + default: + return t`Ruleset cycle` + } + }, [cycleStatus]) + + return ( + + {({ open }) => ( +
+ +
+ {title} +
+ Rules +
+
+ +
+ + + + + + +
+ )} +
+ ) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationPanel.tsx new file mode 100644 index 0000000000..d61a880ccb --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationPanel.tsx @@ -0,0 +1,15 @@ +import { ConfigurationPanel } from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { useV4V5CycleConfigurationPanel } from './hooks/useV4V5CycleConfigurationPanel' + +type V4V5CycleConfigurationPanelProps = { + ruleset: JBRulesetData | undefined + rulesetMetadata: JBRulesetMetadata | undefined +} + +export const V4V5CycleConfigurationPanel: React.FC< + V4V5CycleConfigurationPanelProps +> = ({ ruleset, rulesetMetadata }) => { + const props = useV4V5CycleConfigurationPanel(ruleset, rulesetMetadata) + return +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleSubPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleSubPanel.tsx new file mode 100644 index 0000000000..17cd3de0af --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleSubPanel.tsx @@ -0,0 +1,224 @@ +import { Trans, t } from '@lingui/macro' + +import { InformationCircleIcon } from '@heroicons/react/24/outline' +import { CountdownCallout } from 'components/Project/ProjectTabs/CyclesPayoutsTab/CountdownCallout' +import { currentCycleRemainingLengthTooltip } from 'components/Project/ProjectTabs/CyclesPayoutsTab/CyclesPanelTooltips' +import { UpcomingCycleChangesCallout } from 'components/Project/ProjectTabs/CyclesPayoutsTab/UpcomingCycleChangesCallout' +import { TitleDescriptionDisplayCard } from 'components/Project/ProjectTabs/TitleDescriptionDisplayCard' +import { RulesetCountdownProvider } from 'packages/v4v5/contexts/RulesetCountdownProvider' +import { twMerge } from 'tailwind-merge' +import { timeSecondsToDateString } from 'utils/timeSecondsToDateString' +import { useRulesetCountdown } from '../../hooks/useRulesetCountdown' +import { useCyclesPanelSelectedCycle } from './contexts/CyclesPanelSelectedCycleContext' +import { useV4V5UpcomingRulesetHasChanges } from './hooks/useV4V5UpcomingRulesetHasChanges' +import { V4V5CycleConfigurationDisplayCard } from './V4V5CycleConfigurationDisplayCard' +import { V4V5PayoutsSubPanel } from './V4V5PayoutsSubPanel' +import { RulesetDiffSection } from './components/RulesetDiffSection' + +function CountdownClock({ rulesetUnlocked }: { rulesetUnlocked: boolean }) { + const { timeRemainingText } = useRulesetCountdown() + + const remainingTime = rulesetUnlocked ? '-' : timeRemainingText + + if (!remainingTime) { + return + } + return <>{remainingTime} +} + +export const V4V5CycleSubPanel = () => { + const { + selectedRuleset, + selectedRulesetMetadata, + selectedCycleNumber, + selectedCycleStatus, + previousRuleset, + previousRulesetMetadata, + currentRuleset, + isLoading, + } = useCyclesPanelSelectedCycle() + + const { hasChanges, loading: hasChangesLoading } = useV4V5UpcomingRulesetHasChanges() + + const isCurrentCycle = selectedCycleStatus === 'current' + const isUpcomingCycle = selectedCycleStatus === 'upcoming' + const isPastCycle = selectedCycleStatus === 'past' + + const rulesetUnlocked = selectedRuleset?.duration === 0 + const currentRulesetUnlocked = currentRuleset?.duration === 0 + + const rulesetLengthTooltip = isCurrentCycle + ? currentCycleRemainingLengthTooltip + : undefined + + const rulesetLength = selectedRuleset?.duration + ? timeSecondsToDateString(selectedRuleset.duration, 'short') + : undefined + + const status = rulesetUnlocked ? t`Unlocked` : t`Locked` + + const rulesetStatusTooltip = rulesetUnlocked ? ( + The project's rules are unlocked and can change at any time. + ) : ( + + This project's rules will be locked in place for {rulesetLength}. + + ) + + // Show "no upcoming ruleset" message only for upcoming tab when conditions met + const hasNoUpcomingRuleset = + isUpcomingCycle && + currentRulesetUnlocked && + selectedCycleNumber !== 0 && + !hasChanges + + if (isLoading) { + return ( +
+
+ } + /> + } + /> + } + /> +
+
+ ) + } + + if (hasNoUpcomingRuleset) { + return ( +
+
+ + + This project has no upcoming ruleset. Its rules can change at any + time. + +
+
+ ) + } + + const upcomingRulesetChangesCalloutText = hasChanges + ? t`This ruleset has upcoming changes` + : t`This ruleset has no upcoming changes` + + return ( + <> +
+
+ {/* Scheduled launch callout */} + {isUpcomingCycle && selectedCycleNumber === 1 && ( + + )} + + {/* Upcoming changes callout */} + {isUpcomingCycle && selectedCycleNumber !== 1 && ( + + )} + + {/* Past cycle info banner */} + {isPastCycle && ( +
+ + + You are viewing a past ruleset. This cycle has ended. + +
+ )} + +
+ } + /> + } + tooltip={rulesetStatusTooltip} + /> + + {isCurrentCycle ? ( + + + + } + tooltip={rulesetLengthTooltip} + /> + ) : ( + } + tooltip={rulesetLengthTooltip} + /> + )} +
+ + {/* Configuration display card */} + + + {/* Diff section showing changes from previous cycle */} + {previousRuleset && selectedCycleNumber && selectedCycleNumber > 1 && ( + + )} +
+ + {/* Payouts section - only show for current cycle */} + {isCurrentCycle && ( + + )} +
+ + ) +} + +const Skeleton = ({ className }: { className?: string }) => ( +
+) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CyclesPayoutsPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CyclesPayoutsPanel.tsx index edae0ef524..b073bf8081 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CyclesPayoutsPanel.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CyclesPayoutsPanel.tsx @@ -1,52 +1,81 @@ -import { Tab } from '@headlessui/react' -import { t } from '@lingui/macro' -import { CyclesTab } from 'components/Project/ProjectTabs/CyclesPayoutsTab/CyclesTab' -import { forwardRef, useMemo } from 'react' -import { useV4V5CurrentUpcomingSubPanel } from '../../hooks/useV4V5CurrentUpcomingSubPanel' -import { V4V5CurrentUpcomingSubPanel } from './V4V5CurrentUpcomingSubPanel' - -type V4V5CyclesSubPanel = { - id: 'current' | 'upcoming' | 'history' - name: string -} +import { Trans } from '@lingui/macro' +import { useSuckers } from 'juice-sdk-react' +import { ChainSelect } from 'packages/v4v5/components/ChainSelect' +import { forwardRef } from 'react' +import { CycleNavigator } from './components/CycleNavigator' +import { useCyclesPanelSelectedChain } from './contexts/CyclesPanelSelectedChainContext' +import { CyclesPanelSelectedCycleProvider, useCyclesPanelSelectedCycle } from './contexts/CyclesPanelSelectedCycleContext' +import { V4V5CycleSubPanel } from './V4V5CycleSubPanel' -export const V4V5CyclesPayoutsPanel = forwardRef((props, ref) => { - const upcomingInfo = useV4V5CurrentUpcomingSubPanel('upcoming') - const tabs: V4V5CyclesSubPanel[] = useMemo(() => { - // If upcoming ruleset is cycle #1 (scheduled launch), hide "Current" - if (!upcomingInfo.loading && upcomingInfo.rulesetNumber === 1) { - return [{ id: 'upcoming', name: t`Upcoming` }] - } - return [ - { id: 'current', name: t`Current` }, - { id: 'upcoming', name: t`Upcoming` }, - // { id: 'history', name: t`History` }, - ] - }, [upcomingInfo.loading, upcomingInfo.rulesetNumber]) +const V4V5CyclesPayoutsPanelContent = forwardRef((props, ref) => { + const { + cycleOptions, + selectedCycleNumber, + selectedCycleStatus, + canGoNext, + canGoPrevious, + totalCycles, + hasMoreRulesets, + isLoadingMore, + goToNextCycle, + goToPreviousCycle, + jumpToCycle, + loadMoreRulesets, + isLoading, + } = useCyclesPanelSelectedCycle() + + const { selectedChainId, setSelectedChainId } = useCyclesPanelSelectedChain() + const { data: suckers } = useSuckers() return ( - -
-

Rulesets

- {/* ProjectChainSelect is in V4CurrentUpcomingSubPanel */} - - {tabs.map(tab => ( - - ))} - +
+ {/* Row 1: Title + Chain selector */} +
+
+

+ Rulesets +

+ {selectedChainId && suckers && suckers.length > 1 && ( + setSelectedChainId(chainId)} + chainIds={suckers.map(s => s.peerChainId)} + /> + )} +
- - {tabs.map(tab => ( - - {tab.id === 'history' ? ( - <> // - ) : ( - - )} - - ))} - - + + {/* Row 2: Cycle Navigator */} + {!isLoading && cycleOptions.length > 0 && ( + + )} + + {/* Cycle content */} + +
+ ) +}) + +V4V5CyclesPayoutsPanelContent.displayName = 'V4V5CyclesPayoutsPanelContent' + +export const V4V5CyclesPayoutsPanel = forwardRef((props, ref) => { + return ( + + + ) }) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/CycleNavigator.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/CycleNavigator.tsx new file mode 100644 index 0000000000..1bfdee4761 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/CycleNavigator.tsx @@ -0,0 +1,201 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline' +import { Trans, t } from '@lingui/macro' +import { JuiceListbox } from 'components/inputs/JuiceListbox' +import { useMemo } from 'react' +import { twMerge } from 'tailwind-merge' +import { CycleOption, CycleStatus } from 'packages/v4v5/hooks/useJBRulesetHistory' + +type CycleNavigatorProps = { + cycleOptions: CycleOption[] + selectedCycleNumber: number | null + selectedCycleStatus: CycleStatus + canGoNext: boolean + canGoPrevious: boolean + totalCycles: number + hasMoreRulesets: boolean + isLoadingMore: boolean + onGoNext: () => void + onGoPrevious: () => void + onJumpToCycle: (cycleNumber: number) => void + onLoadMore: () => void +} + +function CycleStatusBadge({ status }: { status: CycleStatus }) { + const badgeStyles = { + current: 'bg-bluebs-100 text-bluebs-700 dark:bg-bluebs-900 dark:text-bluebs-300', + upcoming: 'bg-melon-100 text-melon-700 dark:bg-melon-900 dark:text-melon-300', + past: 'bg-smoke-200 text-smoke-700 dark:bg-slate-600 dark:text-slate-300', + } + + const statusText = { + current: t`Current`, + upcoming: t`Upcoming`, + past: t`Past`, + } + + return ( + + {statusText[status]} + + ) +} + +function formatShortDate(timestamp: number): string { + const date = new Date(timestamp * 1000) + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }) +} + +function formatCycleDateRange(start: number, duration: number): string { + const startDate = formatShortDate(start) + if (duration === 0) { + return startDate + } + const endDate = formatShortDate(start + duration) + return `${startDate} - ${endDate}` +} + +export function CycleNavigator({ + cycleOptions, + selectedCycleNumber, + selectedCycleStatus, + canGoNext, + canGoPrevious, + totalCycles, + hasMoreRulesets, + isLoadingMore, + onGoNext, + onGoPrevious, + onJumpToCycle, + onLoadMore, +}: CycleNavigatorProps) { + const selectedOption = useMemo(() => { + const option = cycleOptions.find(opt => opt.cycleNumber === selectedCycleNumber) + if (!option) return null + return option + }, [cycleOptions, selectedCycleNumber]) + + const dropdownOptions = useMemo(() => { + const options = cycleOptions.map(option => ({ + label: ( + + + Cycle #{option.cycleNumber} + + + + ), + value: option.cycleNumber, + })) + + // Add "Load more" option if available + if (hasMoreRulesets) { + options.push({ + label: ( + + {isLoadingMore ? Loading... : Load more cycles...} + + ), + value: -1, // Special value for load more + }) + } + + return options + }, [cycleOptions, hasMoreRulesets, isLoadingMore]) + + const currentValue = useMemo(() => { + if (!selectedOption) return { label: t`Select cycle`, value: undefined } + return { + label: ( + + Cycle #{selectedOption.cycleNumber} + + + ), + value: selectedOption.cycleNumber, + } + }, [selectedOption]) + + const handleDropdownChange = (option: { value: number | undefined }) => { + if (option.value === -1) { + onLoadMore() + return + } + if (option.value !== undefined) { + onJumpToCycle(option.value) + } + } + + return ( +
+
+ {/* Previous button */} + + + {/* Cycle selector dropdown */} + + + {/* Next button */} + +
+ + {/* Date range display */} + {selectedOption && ( +
+ {selectedOption.duration === 0 ? ( + + Started {formatShortDate(selectedOption.start)} + + ) : ( + {formatCycleDateRange(selectedOption.start, selectedOption.duration)} + )} + {totalCycles > 0 && ( + + ({selectedCycleNumber} of {totalCycles}) + + )} +
+ )} +
+ ) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/RulesetDiffSection.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/RulesetDiffSection.tsx new file mode 100644 index 0000000000..5214c8f449 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/RulesetDiffSection.tsx @@ -0,0 +1,247 @@ +import { Disclosure, Transition } from '@headlessui/react' +import { ChevronDownIcon } from '@heroicons/react/24/outline' +import { Trans, t } from '@lingui/macro' +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { Fragment, useMemo } from 'react' +import { twMerge } from 'tailwind-merge' +import { formattedNum } from 'utils/format/formatNumber' +import { timeSecondsToDateString } from 'utils/timeSecondsToDateString' + +type RulesetDiffSectionProps = { + currentRuleset: JBRulesetData | undefined + currentMetadata: JBRulesetMetadata | undefined + previousRuleset: JBRulesetData | undefined + previousMetadata: JBRulesetMetadata | undefined + previousCycleNumber: number +} + +type DiffItem = { + label: string + currentValue: string + previousValue: string +} + +function formatDuration(duration: number | undefined): string { + if (duration === undefined) return '-' + if (duration === 0) return t`Not set` + return timeSecondsToDateString(duration, 'short', 'lower') +} + +function formatPercent(value: number | undefined): string { + if (value === undefined) return '-' + return `${formattedNum(value * 100)}%` +} + +function formatWeight(weight: number | undefined, symbol: string = 'tokens/ETH'): string { + if (weight === undefined) return '-' + return `${formattedNum(weight)} ${symbol}` +} + +function formatBoolean(value: boolean | undefined): string { + if (value === undefined) return '-' + return value ? t`Enabled` : t`Disabled` +} + +function computeDiffs( + current: JBRulesetData | undefined, + currentMeta: JBRulesetMetadata | undefined, + previous: JBRulesetData | undefined, + previousMeta: JBRulesetMetadata | undefined, +): DiffItem[] { + const diffs: DiffItem[] = [] + + if (!current || !previous) return diffs + + // Duration + if (current.duration !== previous.duration) { + diffs.push({ + label: t`Duration`, + currentValue: formatDuration(current.duration), + previousValue: formatDuration(previous.duration), + }) + } + + // Weight (token issuance) + const currentWeight = current.weight.toFloat() + const previousWeight = previous.weight.toFloat() + if (currentWeight !== previousWeight) { + diffs.push({ + label: t`Total issuance rate`, + currentValue: formatWeight(currentWeight), + previousValue: formatWeight(previousWeight), + }) + } + + // Weight cut percent + const currentWeightCut = current.weightCutPercent.toFloat() + const previousWeightCut = previous.weightCutPercent.toFloat() + if (currentWeightCut !== previousWeightCut) { + diffs.push({ + label: t`Issuance cut percent`, + currentValue: formatPercent(currentWeightCut), + previousValue: formatPercent(previousWeightCut), + }) + } + + // Metadata comparisons + if (currentMeta && previousMeta) { + // Reserved percent + const currentReserved = currentMeta.reservedPercent.toFloat() + const previousReserved = previousMeta.reservedPercent.toFloat() + if (currentReserved !== previousReserved) { + diffs.push({ + label: t`Reserved rate`, + currentValue: formatPercent(currentReserved), + previousValue: formatPercent(previousReserved), + }) + } + + // Cash out tax rate + const currentCashOut = currentMeta.cashOutTaxRate.toFloat() + const previousCashOut = previousMeta.cashOutTaxRate.toFloat() + if (currentCashOut !== previousCashOut) { + diffs.push({ + label: t`Cash out tax rate`, + currentValue: formatPercent(currentCashOut), + previousValue: formatPercent(previousCashOut), + }) + } + + // Pause pay + if (currentMeta.pausePay !== previousMeta.pausePay) { + diffs.push({ + label: t`Payments`, + currentValue: formatBoolean(!currentMeta.pausePay), + previousValue: formatBoolean(!previousMeta.pausePay), + }) + } + + // Allow owner minting + if (currentMeta.allowOwnerMinting !== previousMeta.allowOwnerMinting) { + diffs.push({ + label: t`Owner token minting`, + currentValue: formatBoolean(currentMeta.allowOwnerMinting), + previousValue: formatBoolean(previousMeta.allowOwnerMinting), + }) + } + + // Token transfers + if (currentMeta.pauseCreditTransfers !== previousMeta.pauseCreditTransfers) { + diffs.push({ + label: t`Token transfers`, + currentValue: formatBoolean(!currentMeta.pauseCreditTransfers), + previousValue: formatBoolean(!previousMeta.pauseCreditTransfers), + }) + } + + // Terminal migration + if (currentMeta.allowTerminalMigration !== previousMeta.allowTerminalMigration) { + diffs.push({ + label: t`Terminal migration`, + currentValue: formatBoolean(currentMeta.allowTerminalMigration), + previousValue: formatBoolean(previousMeta.allowTerminalMigration), + }) + } + + // Controller migration + if (currentMeta.allowSetController !== previousMeta.allowSetController) { + diffs.push({ + label: t`Controller migration`, + currentValue: formatBoolean(currentMeta.allowSetController), + previousValue: formatBoolean(previousMeta.allowSetController), + }) + } + + // Hold fees + if (currentMeta.holdFees !== previousMeta.holdFees) { + diffs.push({ + label: t`Hold fees`, + currentValue: formatBoolean(currentMeta.holdFees), + previousValue: formatBoolean(previousMeta.holdFees), + }) + } + } + + return diffs +} + +function DiffRow({ item }: { item: DiffItem }) { + return ( +
+ {item.label} +
+ + {item.previousValue} + + + + {item.currentValue} + +
+
+ ) +} + +export function RulesetDiffSection({ + currentRuleset, + currentMetadata, + previousRuleset, + previousMetadata, + previousCycleNumber, +}: RulesetDiffSectionProps) { + const diffs = useMemo( + () => computeDiffs(currentRuleset, currentMetadata, previousRuleset, previousMetadata), + [currentRuleset, currentMetadata, previousRuleset, previousMetadata], + ) + + if (diffs.length === 0) { + return null + } + + return ( + + {({ open }) => ( + <> + + + + Changes from Cycle #{previousCycleNumber} + + + ({diffs.length} {diffs.length === 1 ? t`change` : t`changes`}) + + + + + + + +
+ {diffs.map((diff, index) => ( + + ))} +
+
+
+ + )} +
+ ) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/contexts/CyclesPanelSelectedCycleContext.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/contexts/CyclesPanelSelectedCycleContext.tsx new file mode 100644 index 0000000000..0a9a27c3b5 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/contexts/CyclesPanelSelectedCycleContext.tsx @@ -0,0 +1,97 @@ +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { createContext, useContext } from 'react' +import { + CycleOption, + CycleStatus, + useJBRulesetHistory, +} from 'packages/v4v5/hooks/useJBRulesetHistory' + +type CyclesPanelSelectedCycleContextType = { + // Selected cycle data + selectedRuleset: JBRulesetData | undefined + selectedRulesetMetadata: JBRulesetMetadata | undefined + selectedCycleNumber: number | null + selectedCycleStatus: CycleStatus + + // Previous cycle for diff + previousRuleset: JBRulesetData | undefined + previousRulesetMetadata: JBRulesetMetadata | undefined + + // Current cycle reference + currentRuleset: JBRulesetData | undefined + currentRulesetMetadata: JBRulesetMetadata | undefined + currentCycleNumber: number | undefined + + // Upcoming cycle + upcomingRuleset: JBRulesetData | undefined + upcomingRulesetMetadata: JBRulesetMetadata | undefined + hasUpcoming: boolean + + // Navigation + cycleOptions: CycleOption[] + canGoNext: boolean + canGoPrevious: boolean + goToNextCycle: () => void + goToPreviousCycle: () => void + jumpToCycle: (cycleNumber: number) => void + goToCurrent: () => void + totalCycles: number + + // Loading states + isLoading: boolean + isLoadingMore: boolean + hasMoreRulesets: boolean + loadMoreRulesets: () => void +} + +const defaultContext: CyclesPanelSelectedCycleContextType = { + selectedRuleset: undefined, + selectedRulesetMetadata: undefined, + selectedCycleNumber: null, + selectedCycleStatus: 'current', + previousRuleset: undefined, + previousRulesetMetadata: undefined, + currentRuleset: undefined, + currentRulesetMetadata: undefined, + currentCycleNumber: undefined, + upcomingRuleset: undefined, + upcomingRulesetMetadata: undefined, + hasUpcoming: false, + cycleOptions: [], + canGoNext: false, + canGoPrevious: false, + goToNextCycle: () => {}, + goToPreviousCycle: () => {}, + jumpToCycle: () => {}, + goToCurrent: () => {}, + totalCycles: 0, + isLoading: false, + isLoadingMore: false, + hasMoreRulesets: false, + loadMoreRulesets: () => {}, +} + +export const CyclesPanelSelectedCycleContext = + createContext(defaultContext) + +export const CyclesPanelSelectedCycleProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const rulesetHistory = useJBRulesetHistory() + + return ( + + {children} + + ) +} + +export const useCyclesPanelSelectedCycle = () => { + const context = useContext(CyclesPanelSelectedCycleContext) + if (!context) { + throw new Error( + 'useCyclesPanelSelectedCycle must be used within a CyclesPanelSelectedCycleProvider', + ) + } + return context +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5CycleConfigurationPanel.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5CycleConfigurationPanel.ts new file mode 100644 index 0000000000..1b356b7f5c --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5CycleConfigurationPanel.ts @@ -0,0 +1,22 @@ +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { useV4V5FormatCycleSection } from './useV4V5FormatCycleSection' +import { useV4V5FormatTokenSection } from './useV4V5FormatTokenSection' +import { useV4V5FormatOtherRulesSection } from './useV4V5FormatOtherRulesSection' +import { useV4V5FormatExtensionSection } from './useV4V5FormatExtensionSection' + +export const useV4V5CycleConfigurationPanel = ( + ruleset: JBRulesetData | undefined, + rulesetMetadata: JBRulesetMetadata | undefined, +) => { + const cycle = useV4V5FormatCycleSection(ruleset) + const token = useV4V5FormatTokenSection(ruleset, rulesetMetadata) + const otherRules = useV4V5FormatOtherRulesSection(rulesetMetadata) + const extension = useV4V5FormatExtensionSection(rulesetMetadata) + + return { + cycle, + token, + otherRules, + extension, + } +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationCycleSection.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationCycleSection.tsx index 3e0d2fc416..c25cf225dc 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationCycleSection.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationCycleSection.tsx @@ -45,7 +45,7 @@ export const useV4V5FormatConfigurationCycleSection = ({ const durationDatum: ConfigurationPanelDatum = useMemo(() => { const currentDuration = formatDuration(ruleset?.duration) - if (upcomingRuleset === null || upcomingRulesetLoading) { + if (upcomingRuleset == null || upcomingRulesetLoading) { return pairToDatum(t`Duration`, currentDuration, null) } const upcomingDuration = formatDuration( @@ -61,17 +61,15 @@ export const useV4V5FormatConfigurationCycleSection = ({ const upcomingRulesetStart = upcomingRuleset?.start ?? derivedUpcomingRulesetStart const startTimeDatum: ConfigurationPanelDatum = useMemo(() => { - const formattedTime = Boolean(upcomingRuleset) - ? formatTime(upcomingRulesetStart) - : formatTime(ruleset?.start) - - const formatTimeDatum: ConfigurationPanelDatum = { - name: t`Start time`, - new: formattedTime, - easyCopy: true, + const current = formatTime(ruleset?.start) + + if (upcomingRuleset == null || upcomingRulesetLoading) { + return pairToDatum(t`Start time`, current, null, undefined, true) } - return formatTimeDatum - }, [ruleset?.start, upcomingRuleset, upcomingRulesetStart]) + + const upcoming = formatTime(upcomingRulesetStart) + return pairToDatum(t`Start time`, current, upcoming, undefined, true) + }, [ruleset?.start, upcomingRuleset, upcomingRulesetStart, upcomingRulesetLoading]) const formatPayoutAmount = ( amount: bigint | undefined, @@ -92,7 +90,7 @@ export const useV4V5FormatConfigurationCycleSection = ({ const currentPayout = formatPayoutAmount(amount, currency) if ( - upcomingPayoutLimitAmountCurrency === null || + upcomingPayoutLimitAmountCurrency == null || upcomingPayoutLimitLoading ) { return pairToDatum(t`Payouts`, currentPayout, null) @@ -121,7 +119,7 @@ export const useV4V5FormatConfigurationCycleSection = ({ ? getApprovalStrategyByAddress(ruleset.approvalHook, version, chainId) : undefined const current = currentApprovalStrategy?.name - if (upcomingRuleset === null || upcomingPayoutLimitLoading) { + if (upcomingRuleset == null || upcomingPayoutLimitLoading) { return pairToDatum(t`Rule change deadline`, current, null) } diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts index b8929f9500..e2cd5c2a86 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts @@ -2,7 +2,7 @@ import { ConfigurationPanelDatum, ConfigurationPanelTableData, } from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' -import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { ETH_CURRENCY_ID, JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' import { flagPairToDatum } from 'components/Project/ProjectTabs/utils/flagPairToDatum' import { formattedNum } from 'utils/format/formatNumber' @@ -11,6 +11,13 @@ import { t } from '@lingui/macro' import { tokenSymbolText } from 'utils/tokenSymbolText' import { useMemo } from 'react' +const getCurrencySymbol = (baseCurrency: number | undefined): string => { + if (baseCurrency === undefined || baseCurrency === ETH_CURRENCY_ID) { + return 'ETH' + } + return 'USD' +} + export const useV4V5FormatConfigurationTokenSection = ({ ruleset, rulesetMetadata, @@ -32,6 +39,8 @@ export const useV4V5FormatConfigurationTokenSection = ({ plural: true, }) + const currencySymbol = getCurrencySymbol(rulesetMetadata?.baseCurrency) + const weightCutPercentFloat = ruleset?.weightCutPercent.toFloat() const currentTotalIssuanceRate = ruleset?.weight.toFloat() const currentTotalIssuanceRateFormatted = formattedNum(currentTotalIssuanceRate) @@ -46,21 +55,22 @@ export const useV4V5FormatConfigurationTokenSection = ({ const totalIssuanceRateDatum: ConfigurationPanelDatum = useMemo(() => { const current = currentTotalIssuanceRateFormatted !== undefined - ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined - if (upcomingRuleset === null || upcomingRulesetLoading) { + if (upcomingRuleset == null || upcomingRulesetLoading) { return pairToDatum(t`Total issuance rate`, current, null) } const queued = queuedTotalIssuanceRate !== undefined - ? `${queuedTotalIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${queuedTotalIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined return pairToDatum(t`Total issuance rate`, current, queued) }, [ upcomingRuleset, tokenSymbol, + currencySymbol, queuedTotalIssuanceRate, upcomingRulesetLoading, currentTotalIssuanceRateFormatted, @@ -81,12 +91,12 @@ export const useV4V5FormatConfigurationTokenSection = ({ const currentPayerIssuanceRateFormatted = formattedNum(currentPayerIssuanceRate) const current = currentPayerIssuanceRate !== undefined - ? `${currentPayerIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${currentPayerIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined if ( - upcomingRuleset === null || - upcomingRulesetMetadata === null || + upcomingRuleset == null || + upcomingRulesetMetadata == null || upcomingRulesetLoading ) { return pairToDatum(t`Payer issuance rate`, current, null) @@ -99,12 +109,13 @@ export const useV4V5FormatConfigurationTokenSection = ({ : undefined const queuedPayerIssuanceRateFormatted = formattedNum(queuedPayerIssuanceRate) const queued = queuedPayerIssuanceRate !== undefined - ? `${queuedPayerIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${queuedPayerIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined return pairToDatum(t`Payer issuance rate`, current, queued) }, [ tokenSymbol, + currencySymbol, upcomingRuleset, queuedReservedPercentFloat, upcomingRulesetMetadata, @@ -118,7 +129,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ const current = rulesetMetadata?.reservedPercent ? `${rulesetMetadata.reservedPercent.formatPercentage()}%` : undefined - if (upcomingRulesetMetadata === null || upcomingRulesetLoading) { + if (upcomingRulesetMetadata == null || upcomingRulesetLoading) { return pairToDatum(t`Reserved rate`, current, null) } @@ -135,7 +146,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ ? `${ruleset.weightCutPercent.formatPercentage()}%` : undefined - if (upcomingRuleset === null || upcomingRulesetLoading) { + if (upcomingRuleset == null || upcomingRulesetLoading) { return pairToDatum(t`Issuance cut percent`, current, null) } const queued = upcomingRuleset @@ -154,7 +165,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ ? `${currentRedemptionRate}%` : undefined - if (upcomingRulesetMetadata === null || upcomingRulesetLoading) { + if (upcomingRulesetMetadata == null || upcomingRulesetLoading) { return pairToDatum(t`Cash out tax rate`, current, null) } @@ -171,7 +182,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ rulesetMetadata?.allowOwnerMinting !== undefined ? rulesetMetadata?.allowOwnerMinting : undefined - if (upcomingRulesetMetadata === null || upcomingRulesetLoading) { + if (upcomingRulesetMetadata == null || upcomingRulesetLoading) { return flagPairToDatum( t`Owner token minting`, currentOwnerTokenMinting, @@ -202,7 +213,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ rulesetMetadata?.pauseCreditTransfers !== undefined ? !rulesetMetadata?.pauseCreditTransfers : undefined - if (upcomingRulesetMetadata === null || upcomingRulesetLoading) { + if (upcomingRulesetMetadata == null || upcomingRulesetLoading) { return flagPairToDatum( t`Token transfers`, !!currentTokenTransfersDatum, diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatCycleSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatCycleSection.ts new file mode 100644 index 0000000000..6f3d7bd0b4 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatCycleSection.ts @@ -0,0 +1,50 @@ +import { t } from '@lingui/macro' +import { ConfigurationPanelTableData } from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' +import { pairToDatum } from 'components/Project/ProjectTabs/utils/pairToDatum' +import { JBRulesetData } from 'juice-sdk-core' +import { useJBChainId } from 'juice-sdk-react' +import { useV4V5Version } from 'packages/v4v5/contexts/V4V5VersionProvider' +import { getApprovalStrategyByAddress } from 'packages/v4v5/utils/approvalHooks' +import { useMemo } from 'react' +import { formatTime } from 'utils/format/formatTime' +import { timeSecondsToDateString } from 'utils/timeSecondsToDateString' + +export const useV4V5FormatCycleSection = ( + ruleset: JBRulesetData | undefined, +): ConfigurationPanelTableData => { + const { version } = useV4V5Version() + const chainId = useJBChainId() + + const formatDuration = (duration: number | undefined) => { + if (duration === undefined) return undefined + if (duration === 0) return t`Not set` + return timeSecondsToDateString(Number(duration), 'short', 'lower') + } + + const durationDatum = useMemo(() => { + const duration = formatDuration(ruleset?.duration) + return pairToDatum(t`Duration`, duration, null) + }, [ruleset?.duration]) + + const startTimeDatum = useMemo(() => { + const startTime = formatTime(ruleset?.start) + return pairToDatum(t`Start time`, startTime, null, undefined, true) + }, [ruleset?.start]) + + const editDeadlineDatum = useMemo(() => { + const approvalStrategy = + ruleset?.approvalHook && version && chainId + ? getApprovalStrategyByAddress(ruleset.approvalHook, version, chainId) + : undefined + const approvalStrategyName = approvalStrategy?.name + return pairToDatum(t`Rule change deadline`, approvalStrategyName, null) + }, [ruleset?.approvalHook, version, chainId]) + + return useMemo(() => { + return { + duration: durationDatum, + startTime: startTimeDatum, + editDeadline: editDeadlineDatum, + } + }, [durationDatum, startTimeDatum, editDeadlineDatum]) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatExtensionSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatExtensionSection.ts new file mode 100644 index 0000000000..eea50bd47b --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatExtensionSection.ts @@ -0,0 +1,42 @@ +import { t } from '@lingui/macro' +import { ConfigurationPanelTableData } from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' +import { flagPairToDatum } from 'components/Project/ProjectTabs/utils/flagPairToDatum' +import { pairToDatum } from 'components/Project/ProjectTabs/utils/pairToDatum' +import { JBRulesetMetadata } from 'juice-sdk-core' +import { useMemo } from 'react' +import { isZeroAddress } from 'utils/address' +import { etherscanLink } from 'utils/etherscan' + +export const useV4V5FormatExtensionSection = ( + rulesetMetadata: JBRulesetMetadata | undefined, +): ConfigurationPanelTableData | null => { + const contractDatum = useMemo(() => { + const contract = rulesetMetadata?.dataHook + const link = contract ? etherscanLink('address', contract) : undefined + return pairToDatum(t`Contract`, contract, null, link, true) + }, [rulesetMetadata?.dataHook]) + + const useForPaymentsDatum = useMemo(() => { + const value = rulesetMetadata?.useDataHookForPay + return flagPairToDatum(t`Use for payments`, value, null) + }, [rulesetMetadata?.useDataHookForPay]) + + const useForRedemptionsDatum = useMemo(() => { + const value = rulesetMetadata?.useDataHookForCashOut + return flagPairToDatum(t`Use for redemptions`, value, null) + }, [rulesetMetadata?.useDataHookForCashOut]) + + const formatted = useMemo(() => { + return { + contract: contractDatum, + useForPayments: useForPaymentsDatum, + useForRedemptions: useForRedemptionsDatum, + } + }, [contractDatum, useForPaymentsDatum, useForRedemptionsDatum]) + + if (isZeroAddress(rulesetMetadata?.dataHook)) { + return null + } + + return formatted +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatOtherRulesSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatOtherRulesSection.ts new file mode 100644 index 0000000000..10533f2c73 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatOtherRulesSection.ts @@ -0,0 +1,53 @@ +import { t } from '@lingui/macro' +import { ConfigurationPanelTableData } from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' +import { flagPairToDatum } from 'components/Project/ProjectTabs/utils/flagPairToDatum' +import { JBRulesetMetadata } from 'juice-sdk-core' +import { useMemo } from 'react' + +export const useV4V5FormatOtherRulesSection = ( + rulesetMetadata: JBRulesetMetadata | undefined, +): ConfigurationPanelTableData => { + const paymentsToThisProjectDatum = useMemo(() => { + const value = + rulesetMetadata?.pausePay !== undefined + ? !rulesetMetadata.pausePay + : undefined + return flagPairToDatum(t`Payments to this project`, value, null) + }, [rulesetMetadata?.pausePay]) + + const holdFeesDatum = useMemo(() => { + const value = rulesetMetadata?.holdFees + return flagPairToDatum(t`Hold fees`, value, null) + }, [rulesetMetadata?.holdFees]) + + const setPaymentTerminalsDatum = useMemo(() => { + const value = rulesetMetadata?.allowSetTerminals + return flagPairToDatum(t`Set payment terminals`, value, null) + }, [rulesetMetadata?.allowSetTerminals]) + + const setControllerDatum = useMemo(() => { + const value = rulesetMetadata?.allowSetController + return flagPairToDatum(t`Set controller`, value, null) + }, [rulesetMetadata?.allowSetController]) + + const migratePaymentTerminalDatum = useMemo(() => { + const value = rulesetMetadata?.allowTerminalMigration + return flagPairToDatum(t`Migrate payment terminal`, value, null) + }, [rulesetMetadata?.allowTerminalMigration]) + + return useMemo(() => { + return { + paymentsToThisProject: paymentsToThisProjectDatum, + holdFees: holdFeesDatum, + setPaymentTerminals: setPaymentTerminalsDatum, + setController: setControllerDatum, + migratePaymentTerminal: migratePaymentTerminalDatum, + } + }, [ + holdFeesDatum, + migratePaymentTerminalDatum, + paymentsToThisProjectDatum, + setControllerDatum, + setPaymentTerminalsDatum, + ]) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatTokenSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatTokenSection.ts new file mode 100644 index 0000000000..34b3530f23 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatTokenSection.ts @@ -0,0 +1,114 @@ +import { t } from '@lingui/macro' +import { + ConfigurationPanelTableData, +} from 'components/Project/ProjectTabs/CyclesPayoutsTab/ConfigurationPanel' +import { flagPairToDatum } from 'components/Project/ProjectTabs/utils/flagPairToDatum' +import { pairToDatum } from 'components/Project/ProjectTabs/utils/pairToDatum' +import { ETH_CURRENCY_ID, JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { useJBTokenContext } from 'juice-sdk-react' +import { useMemo } from 'react' +import { formattedNum } from 'utils/format/formatNumber' +import { tokenSymbolText } from 'utils/tokenSymbolText' + +const getCurrencySymbol = (baseCurrency: number | undefined): string => { + if (baseCurrency === undefined || baseCurrency === ETH_CURRENCY_ID) { + return 'ETH' + } + return 'USD' +} + +export const useV4V5FormatTokenSection = ( + ruleset: JBRulesetData | undefined, + rulesetMetadata: JBRulesetMetadata | undefined, +): ConfigurationPanelTableData => { + const { token } = useJBTokenContext() + const tokenSymbolRaw = token?.data?.symbol + + const tokenSymbol = tokenSymbolText({ + tokenSymbol: tokenSymbolRaw, + capitalize: false, + plural: true, + }) + + const currencySymbol = getCurrencySymbol(rulesetMetadata?.baseCurrency) + + const totalIssuanceRate = ruleset?.weight.toFloat() + const totalIssuanceRateFormatted = formattedNum(totalIssuanceRate) + const reservedPercentFloat = rulesetMetadata?.reservedPercent.toFloat() + + const totalIssuanceRateDatum = useMemo(() => { + const value = + totalIssuanceRateFormatted !== undefined + ? `${totalIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` + : undefined + return pairToDatum(t`Total issuance rate`, value, null) + }, [totalIssuanceRateFormatted, tokenSymbol, currencySymbol]) + + const payerIssuanceRateDatum = useMemo(() => { + const payerIssuanceRate = + typeof totalIssuanceRate !== 'undefined' && + typeof reservedPercentFloat !== 'undefined' + ? totalIssuanceRate - totalIssuanceRate * reservedPercentFloat + : undefined + const payerIssuanceRateFormatted = formattedNum(payerIssuanceRate) + const value = + payerIssuanceRate !== undefined + ? `${payerIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` + : undefined + return pairToDatum(t`Payer issuance rate`, value, null) + }, [totalIssuanceRate, reservedPercentFloat, tokenSymbol, currencySymbol]) + + const reservedPercentDatum = useMemo(() => { + const value = rulesetMetadata?.reservedPercent + ? `${rulesetMetadata.reservedPercent.formatPercentage()}%` + : undefined + return pairToDatum(t`Reserved rate`, value, null) + }, [rulesetMetadata?.reservedPercent]) + + const weightCutPercentDatum = useMemo(() => { + const value = ruleset + ? `${ruleset.weightCutPercent.formatPercentage()}%` + : undefined + return pairToDatum(t`Issuance cut percent`, value, null) + }, [ruleset]) + + const cashOutTaxRateDatum = useMemo(() => { + const value = rulesetMetadata?.cashOutTaxRate + ? `${rulesetMetadata.cashOutTaxRate.formatPercentage()}%` + : undefined + return pairToDatum(t`Cash out tax rate`, value, null) + }, [rulesetMetadata?.cashOutTaxRate]) + + const ownerTokenMintingDatum = useMemo(() => { + const value = rulesetMetadata?.allowOwnerMinting + return flagPairToDatum(t`Owner token minting`, value, null) + }, [rulesetMetadata?.allowOwnerMinting]) + + const tokenTransfersDatum = useMemo(() => { + const value = + rulesetMetadata?.pauseCreditTransfers !== undefined + ? !rulesetMetadata.pauseCreditTransfers + : undefined + return flagPairToDatum(t`Token transfers`, value, null) + }, [rulesetMetadata?.pauseCreditTransfers]) + + return useMemo(() => { + return { + totalIssuanceRate: totalIssuanceRateDatum, + payerIssuanceRate: payerIssuanceRateDatum, + reservedPercent: reservedPercentDatum, + weightCutPercentDatum: weightCutPercentDatum, + cashOutTaxRate: cashOutTaxRateDatum, + ownerTokenMintingRate: ownerTokenMintingDatum, + tokenTransfers: tokenTransfersDatum, + } + }, [ + totalIssuanceRateDatum, + payerIssuanceRateDatum, + reservedPercentDatum, + weightCutPercentDatum, + cashOutTaxRateDatum, + ownerTokenMintingDatum, + tokenTransfersDatum, + ]) +} diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5NftRewardsPanel/V4V5NftRewardsPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5NftRewardsPanel/V4V5NftRewardsPanel.tsx index 859d7141f3..b311093da6 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5NftRewardsPanel/V4V5NftRewardsPanel.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5NftRewardsPanel/V4V5NftRewardsPanel.tsx @@ -4,9 +4,9 @@ import { NftReward, NftRewardSkeleton } from './NftReward/NftReward' import { Button } from 'antd' import { EmptyScreen } from 'components/Project/ProjectTabs/EmptyScreen' -import { useWallet } from 'hooks/Wallet/useWallet' -import useV4V5ProjectOwnerOf from 'packages/v4v5/hooks/useV4V5ProjectOwnerOf' -import { useV4V5ProjectHeader } from 'packages/v4v5/views/V4V5ProjectDashboard/hooks/useV4V5ProjectHeader' +import { useWallet } from 'hooks/Wallet' +import { useV4V5WalletHasPermission } from 'packages/v4v5/hooks/useV4V5WalletHasPermission' +import { V4V5OperatorPermission } from 'packages/v4v5/models/v4Permissions' import { useSettingsPagePath } from 'packages/v4v5/views/V4V5ProjectSettings/hooks/useSettingsPagePath' import { useRouter } from 'next/router' import { RedeemNftsSection } from './RedeemNftsSection/RedeemNftsSection' @@ -22,16 +22,9 @@ export const V4V5NftRewardsPanel = forwardRef((props, ref) => { const router = useRouter() const { userAddress } = useWallet() - const { data: projectOwnerAddress } = useV4V5ProjectOwnerOf() - const { isRevnet, operatorAddress } = useV4V5ProjectHeader() const nftSettingsPath = useSettingsPagePath('nfts') - - // Check if user can add NFTs (project owner or revnet operator) - const canAddNfts = - userAddress && - (userAddress.toLowerCase() === projectOwnerAddress?.toLowerCase() || - (isRevnet && - userAddress.toLowerCase() === operatorAddress?.toLowerCase())) + const hasPermission = useV4V5WalletHasPermission(V4V5OperatorPermission.ADJUST_721_TIERS) + const canAddNfts = !!userAddress && hasPermission return (
diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ProjectTabs.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ProjectTabs.tsx index 64a85898d9..5582e937ff 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ProjectTabs.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ProjectTabs.tsx @@ -4,7 +4,10 @@ import { Tab } from '@headlessui/react' import { t } from '@lingui/macro' import { ProjectTab } from 'components/Project/ProjectTabs/ProjectTab' import { useOnScreen } from 'hooks/useOnScreen' +import { useWallet } from 'hooks/Wallet' import { useV4V5NftRewards } from 'packages/v4v5/contexts/V4V5NftRewards/V4V5NftRewardsProvider' +import { useV4V5WalletHasPermission } from 'packages/v4v5/hooks/useV4V5WalletHasPermission' +import { V4V5OperatorPermission } from 'packages/v4v5/models/v4Permissions' import { twMerge } from 'tailwind-merge' import { useProjectPageQueries } from '../hooks/useProjectPageQueries' import V4V5AboutPanel from './V4V5AboutPanel' @@ -22,6 +25,7 @@ type ProjectTabConfig = { export const V4V5ProjectTabs = ({ className }: { className?: string }) => { const { projectPageTab, setProjectPageTab } = useProjectPageQueries() + const { userAddress } = useWallet() const { nftRewards: { rewardTiers }, } = useV4V5NftRewards() @@ -32,8 +36,10 @@ export const V4V5ProjectTabs = ({ className }: { className?: string }) => { }, [rewardTiers], ) + const hasPermission = useV4V5WalletHasPermission(V4V5OperatorPermission.ADJUST_721_TIERS) + const canAddNfts = !!userAddress && hasPermission - const showNftRewards = hasNftRewards + const showNftRewards = hasNftRewards || canAddNfts const containerRef = useRef(null) const panelRef = useRef(null) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx index b4907656f0..551e281e1b 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx @@ -29,7 +29,7 @@ import { V4V5ReservedTokensSubPanel } from './V4V5ReservedTokensSubPanel' import { V4V5TokenRedemptionCallout } from './V4V5TokenRedemptionCallout' export const V4V5TokensPanel = forwardRef((props, ref) => { - const { userTokenBalanceLoading, projectToken, totalTokenSupplyElement } = + const { userTokenBalanceLoading, projectToken, totalTokenSupplyElement, hasNoTokens } = useV4V5TokensPanel() const projectHasErc20Token = useProjectHasErc20Token() const { data: suckersBalance } = useSuckersUserTokenBalance() @@ -95,6 +95,17 @@ export const V4V5TokensPanel = forwardRef((props, ref) => { ) }, [totalBalance, suckersBalance, projectToken]) + if (hasNoTokens) { + return ( +
+
+

Tokens

+
+ None +
+ ) + } + return ( <>
@@ -246,7 +257,7 @@ const ProjectTokenBadge = () => { const { projectHasErc20Token } = useV4V5TokensPanel() return ( - {projectHasErc20Token ? 'ERC-20' : t`Juicebox native`} + {projectHasErc20Token ? 'ERC-20' : t`Token credits`} ) } diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/hooks/useV4V5TokensPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/hooks/useV4V5TokensPanel.tsx index bea5440be8..00a4de883d 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/hooks/useV4V5TokensPanel.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/hooks/useV4V5TokensPanel.tsx @@ -2,7 +2,7 @@ import { JBChainId, JBProjectToken } from 'juice-sdk-core' import { Tooltip } from 'antd' import { NETWORKS } from 'constants/networks' -import { useJBTokenContext } from 'juice-sdk-react' +import { useJBRulesetContext, useJBTokenContext } from 'juice-sdk-react' import { ChainLogo } from 'packages/v4v5/components/ChainLogo' import { useV4V5UserTotalTokensBalance } from 'packages/v4v5/contexts/V4V5UserTotalTokensBalanceProvider' import { useProjectHasErc20Token } from 'packages/v4v5/hooks/useProjectHasErc20Token' @@ -15,6 +15,7 @@ import { tokenSymbolText } from 'utils/tokenSymbolText' export const useV4V5TokensPanel = () => { const { token } = useJBTokenContext() const tokenAddress = token?.data?.address + const { ruleset } = useJBRulesetContext() const { data: _totalTokenSupply } = useV4V5TotalTokenSupply() @@ -87,6 +88,10 @@ export const useV4V5TokensPanel = () => { const canCreateErc20Token = !projectHasErc20Token && hasDeployErc20Permission + // Check if project has no tokens: total supply is 0 AND ruleset weight is 0 + const rulesetWeight = ruleset.data?.weight?.value ?? 0n + const hasNoTokens = aggregatedTotalSupply === 0n && rulesetWeight === 0n + return { userTokenBalance, userTokenBalanceLoading, @@ -99,5 +104,6 @@ export const useV4V5TokensPanel = () => { totalTokenSupplyElement, projectHasErc20Token, canCreateErc20Token, + hasNoTokens, } } diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/components/ProjectActivityList.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/components/ProjectActivityList.tsx index d0967f9eb2..37aa3e605e 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/components/ProjectActivityList.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/components/ProjectActivityList.tsx @@ -53,8 +53,8 @@ export function ProjectActivityList() { activityEvents?.activityEvents.items .map(transformEventData) .filter((event): event is AnyEvent => !!event) - .map(e => translateEventDataToPresenter(e, undefined)) ?? [], - [activityEvents?.activityEvents.items], + .map(e => translateEventDataToPresenter(e, undefined, version)) ?? [], + [activityEvents?.activityEvents.items, version], ) return ( diff --git a/src/packages/v4v5/views/V4V5ProjectSettings/ArchiveProjectSettingsPage.tsx b/src/packages/v4v5/views/V4V5ProjectSettings/ArchiveProjectSettingsPage.tsx index 931d220030..b7b280e267 100644 --- a/src/packages/v4v5/views/V4V5ProjectSettings/ArchiveProjectSettingsPage.tsx +++ b/src/packages/v4v5/views/V4V5ProjectSettings/ArchiveProjectSettingsPage.tsx @@ -1,24 +1,135 @@ import { Trans } from '@lingui/macro' import { Button, Statistic } from 'antd' import { Callout } from 'components/Callout/Callout' -import { useJBProjectMetadataContext } from 'juice-sdk-react' +import { RelayrPostBundleResponse, useJBProjectMetadataContext, useSuckers } from 'juice-sdk-react' import { uploadProjectMetadata } from 'lib/api/ipfs' import { useEditProjectDetailsTx } from 'packages/v4v5/hooks/useEditProjectDetailsTx' -import { useCallback, useState } from 'react' +import { useOmnichainEditProjectDetailsTx } from 'packages/v4v5/hooks/useOmnichainEditProjectDetailsTx' +import { useCallback, useEffect, useMemo, useState } from 'react' import { emitErrorNotification, emitInfoNotification } from 'utils/notifications' +import { BigNumber } from '@ethersproject/bignumber' +import ETHAmount from 'components/currency/ETHAmount' +import TransactionModal from 'components/modals/TransactionModal' +import { JBChainId } from 'juice-sdk-core' +import { ChainSelect } from 'packages/v4v5/components/ChainSelect' export function ArchiveProjectSettingsPage() { const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [pendingArchived, setPendingArchived] = useState(null) + const [cid, setCid] = useState<`0x${string}` | null>(null) + + // Omnichain state + const { data: suckers } = useSuckers() + const projectChains = useMemo(() => suckers?.map(s => s.peerChainId) || [], [suckers]) + const isOmnichainProject = projectChains.length > 1 + const [selectedGasChain, setSelectedGasChain] = useState(projectChains[0]) + const [txQuote, setTxQuote] = useState() + const [txQuoteLoading, setTxQuoteLoading] = useState(false) + const [confirmLoading, setConfirmLoading] = useState(false) const editV4ProjectDetailsTx = useEditProjectDetailsTx() + const { getEditQuote, sendRelayrTx, relayrBundle } = useOmnichainEditProjectDetailsTx() const { metadata } = useJBProjectMetadataContext() const projectMetadata = metadata.data + // Set default gas chain when suckers load + useEffect(() => { + if (projectChains.length > 0 && !selectedGasChain) { + setSelectedGasChain(projectChains[0]) + } + }, [projectChains, selectedGasChain]) + + const handleGetQuote = async () => { + if (!cid) return + setTxQuoteLoading(true) + try { + const quote = await getEditQuote(cid) + setTxQuote(quote) + } catch (e) { + emitErrorNotification((e as Error).message) + } finally { + setTxQuoteLoading(false) + } + } + + const handleSingleChainArchive = useCallback(async () => { + if (!cid) return + + setConfirmLoading(true) + try { + await editV4ProjectDetailsTx(cid, { + onTransactionPending: () => null, + onTransactionConfirmed: () => { + setConfirmLoading(false) + setModalOpen(false) + emitInfoNotification( + pendingArchived ? 'Project archived' : 'Project unarchived', + { + description: pendingArchived + ? 'Your project has been archived.' + : 'Your project has been unarchived.', + }, + ) + }, + onTransactionError: (error) => { + console.error(error) + setConfirmLoading(false) + emitErrorNotification(`Error updating project: ${error.message}`) + }, + }) + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error('Failed to update project', error) + emitErrorNotification(`Failed to update project: ${errorMessage}`) + setConfirmLoading(false) + } + }, [cid, editV4ProjectDetailsTx, pendingArchived]) + + const handleConfirm = async () => { + if (!isOmnichainProject) { + return handleSingleChainArchive() + } + + if (!txQuote) return handleGetQuote() + + setConfirmLoading(true) + try { + const payment = txQuote.payment_info.find((p) => Number(p.chain) === selectedGasChain) + if (!payment) throw new Error('No payment info for selected chain') + await sendRelayrTx(payment) + relayrBundle.startPolling(txQuote.bundle_uuid) + } catch (e) { + emitErrorNotification((e as Error).message) + setConfirmLoading(false) + } + } + + // Poll for omnichain completion + useEffect(() => { + if (relayrBundle.isComplete) { + setConfirmLoading(false) + setModalOpen(false) + emitInfoNotification( + pendingArchived ? 'Project archived' : 'Project unarchived', + { + description: pendingArchived + ? 'Your project has been archived across all chains.' + : 'Your project has been unarchived across all chains.', + }, + ) + } else if (relayrBundle.error) { + emitErrorNotification(relayrBundle.error as string) + setConfirmLoading(false) + } + }, [relayrBundle.isComplete, relayrBundle.error, pendingArchived]) + const setArchived = useCallback(async (archived: boolean) => { if (!projectMetadata) return setLoading(true) + setPendingArchived(archived) const uploadedMetadata = await uploadProjectMetadata({ ...projectMetadata, @@ -30,138 +141,220 @@ export function ArchiveProjectSettingsPage() { return } - editV4ProjectDetailsTx( - uploadedMetadata.Hash as `0x${string}`, { - onTransactionPending: () => null, - onTransactionConfirmed: () => { - setLoading(false) - emitInfoNotification('Project archived', { - description: 'Your project has been archived.', - }) - - // v4Todo: part of v2, not sure if necessary - // if (projectId) { - // await revalidateProject({ - // pv: PV_V4, - // projectId: String(projectId), - // }) - // } - }, - onTransactionError: (error: unknown) => { - console.error(error) - setLoading(false) - emitErrorNotification(`Error launching ruleset: ${error}`) - }, - } - ) - }, [ - editV4ProjectDetailsTx, - projectMetadata, - ]) + const hash = uploadedMetadata.Hash as `0x${string}` + setCid(hash) + setLoading(false) + setTxQuote(undefined) + setModalOpen(true) + }, [projectMetadata]) + + const txSigning = Boolean(relayrBundle.uuid) && !relayrBundle.isComplete if (projectMetadata?.archived) { return ( + <> +
+ Project state} + valueRender={() => Archived} + /> + +
+

+ Unarchiving your project has the following effects: +

+ +
    +
  • + Your project will appear as 'active'. +
  • +
  • + + Your project can receive payments through the juicebox.money + app. + +
  • +
+
+ +

+ + Allow a few days for your project to appear in the "active" projects + list on the Projects page. + +

+
+ +
+
+ Confirm unarchive} + onOk={handleConfirm} + okText={ + !isOmnichainProject ? ( + Unarchive project + ) : !txQuote ? ( + Get quote + ) : ( + Unarchive project + ) + } + confirmLoading={confirmLoading || txQuoteLoading || txSigning} + transactionPending={txSigning} + chainIds={isOmnichainProject ? projectChains : undefined} + relayrResponse={relayrBundle.response} + cancelButtonProps={{ hidden: true }} + onCancel={() => setModalOpen(false)} + > + {!isOmnichainProject ? ( +
+ + + You'll be prompted a wallet signature to submit the transaction. + + +
+ ) : !txQuote ? ( +
+ + + You'll be prompted a wallet signature for each of this project's chains before submitting the final transaction. + + +
+ ) : ( + <> +
+ Gas quote: + Number(p.chain) === selectedGasChain)?.amount || '0')} /> +
+
+ Pay gas on: + +
+ + )} +
+ + ) + } + + return ( + <>
Project state} - valueRender={() => Archived} + valueRender={() => Active} />

- Unarchiving your project has the following effects: + Archiving your project has the following effects:

  • - Your project will appear as 'active'. + Your project will appear as 'archived'.
  • - Your project can receive payments through the juicebox.money + Your project can't receive payments through the juicebox.money app.
  • +
  • + + Unless payments to this project are paused in your cycle's + rules, your project can still receive payments directly through + the Juicebox protocol contracts. + +
-

- - Allow a few days for your project to appear in the "active" projects - list on the Projects page. - -

+
+

+ + Allow a few days for your project to appear in the "archived" + projects list on the Projects page. + +

+ + + You can unarchive your project at any time. + +
+
- ) - } - - return ( -
- Project state} - valueRender={() => Active} - /> - -
-

- Archiving your project has the following effects: -

- -
    -
  • - Your project will appear as 'archived'. -
  • -
  • - - Your project can't receive payments through the juicebox.money - app. - -
  • -
  • - - Unless payments to this project are paused in your cycle's - rules, your project can still receive payments directly through - the Juicebox protocol contracts. - -
  • -
-
- -
-

- - Allow a few days for your project to appear in the "archived" - projects list on the Projects page. - -

- - - You can unarchive your project at any time. - -
- -
- -
-
+ ) : !txQuote ? ( + Get quote + ) : ( + Archive project + ) + } + confirmLoading={confirmLoading || txQuoteLoading || txSigning} + transactionPending={txSigning} + chainIds={isOmnichainProject ? projectChains : undefined} + relayrResponse={relayrBundle.response} + cancelButtonProps={{ hidden: true }} + onCancel={() => setModalOpen(false)} + > + {!isOmnichainProject ? ( +
+ + + You'll be prompted a wallet signature to submit the transaction. + + +
+ ) : !txQuote ? ( +
+ + + You'll be prompted a wallet signature for each of this project's chains before submitting the final transaction. + + +
+ ) : ( + <> +
+ Gas quote: + Number(p.chain) === selectedGasChain)?.amount || '0')} /> +
+
+ Pay gas on: + +
+ + )} + + ) } diff --git a/src/packages/v4v5/views/V4V5ProjectSettings/CreateErc20TokenSettingsPage.tsx b/src/packages/v4v5/views/V4V5ProjectSettings/CreateErc20TokenSettingsPage.tsx index b6873af731..c3e876b97b 100644 --- a/src/packages/v4v5/views/V4V5ProjectSettings/CreateErc20TokenSettingsPage.tsx +++ b/src/packages/v4v5/views/V4V5ProjectSettings/CreateErc20TokenSettingsPage.tsx @@ -22,6 +22,9 @@ import { useProjectHasErc20Token } from 'packages/v4v5/hooks/useProjectHasErc20T import { useV4V5IssueErc20TokenTx } from 'packages/v4v5/hooks/useV4V5IssueErc20TokenTx' import { useV4V5Version } from 'packages/v4v5/contexts/V4V5VersionProvider' import { useV4V5WalletHasPermission } from 'packages/v4v5/hooks/useV4V5WalletHasPermission' +import { useGnosisSafe } from 'hooks/safe/useGnosisSafe' +import useV4V5ProjectOwnerOf from 'packages/v4v5/hooks/useV4V5ProjectOwnerOf' +import QueueSafeDeployErc20TxsModal from './components/QueueSafeDeployErc20TxsModal' export function CreateErc20TokenSettingsPage() { const [form] = Form.useForm() @@ -29,6 +32,7 @@ export function CreateErc20TokenSettingsPage() { const [transactionModalOpen, setTransactionModalOpen] = useState(false) const [successModalOpen, setSuccessModalOpen] = useState(false) + const [safeModalOpen, setSafeModalOpen] = useState(false) const router = useRouter() const chainId = useJBChainId() @@ -52,6 +56,11 @@ export function CreateErc20TokenSettingsPage() { const { projectId: _singleChainProjectId } = useJBContractContext() // still used for single-chain flow const { data: suckers } = useSuckers() + // Safe detection + const { data: projectOwnerAddress } = useV4V5ProjectOwnerOf() + const { data: gnosisSafeData } = useGnosisSafe(projectOwnerAddress, Number(chainId)) + const isProjectOwnerGnosisSafe = Boolean(gnosisSafeData) + const chainIds = useMemo( () => suckers?.map(s => s.peerChainId) ?? [], [suckers] @@ -108,6 +117,19 @@ export function CreateErc20TokenSettingsPage() { } async function onLaunchMulti() { + // Validate form first + try { + await form.validateFields() + } catch { + return + } + + // Check if project owner is Safe - route to Safe modal + if (isProjectOwnerGnosisSafe) { + setSafeModalOpen(true) + return + } + if (!txQuoteResponse) { // get quote first await getMultiChainQuote() @@ -266,7 +288,7 @@ export function CreateErc20TokenSettingsPage() {
): null}
@@ -321,6 +343,16 @@ export function CreateErc20TokenSettingsPage() {
+ + setSafeModalOpen(false)} + onSuccess={() => { + setSafeModalOpen(false) + setSuccessModalOpen(true) + }} + form={form} + /> ) } diff --git a/src/packages/v4v5/views/V4V5ProjectSettings/EditCyclePage/hooks/useOmnichainEditCycle.ts b/src/packages/v4v5/views/V4V5ProjectSettings/EditCyclePage/hooks/useOmnichainEditCycle.ts index 1f202f057e..833ffedea0 100644 --- a/src/packages/v4v5/views/V4V5ProjectSettings/EditCyclePage/hooks/useOmnichainEditCycle.ts +++ b/src/packages/v4v5/views/V4V5ProjectSettings/EditCyclePage/hooks/useOmnichainEditCycle.ts @@ -1,10 +1,11 @@ -import { JBChainId, createSalt, jbContractAddress, JBOmnichainDeployerContracts, JBCoreContracts, jbOmnichainDeployer4_1Abi } from 'juice-sdk-core' +import { JBChainId, jbContractAddress, JBOmnichainDeployerContracts, JBCoreContracts, jbOmnichainDeployer4_1Abi } from 'juice-sdk-core' import { useGetRelayrTxBundle, useGetRelayrTxQuote, useJBContractContext, useSendRelayrTx } from 'juice-sdk-react' import { useWallet } from 'hooks/Wallet' import { EditCycleTxArgs } from 'packages/v4v5/utils/editRuleset' import { encodeFunctionData } from 'viem' import { useV4V5Version } from 'packages/v4v5/contexts/V4V5VersionProvider' +import { estimateContractGasWithFallback, OMNICHAIN_GAS_FALLBACKS } from 'packages/v4v5/utils/estimateOmnichainGas' export function useOmnichainEditCycle() { const { userAddress } = useWallet() @@ -24,34 +25,44 @@ export function useOmnichainEditCycle() { chainIds: JBChainId[], ) { if (!userAddress || !projectControllerAddress) return - const salt = createSalt() - const versionString = version.toString() as '4' | '5' - const txs = chainIds.map(chainId => { - const baseArgs = editData[chainId] - if (!baseArgs) throw new Error(`No edit data for chain ${chainId}`) - const args = [...baseArgs, projectControllerAddress] as const - // ensure same salt in args if needed by transformEditCycleFormFieldsToTxArgs - console.info('Edit cycle tx args', args) - const encoded = encodeFunctionData({ abi: jbOmnichainDeployer4_1Abi, functionName: 'queueRulesetsOf', args}) - - // V5 projects use V5's omnichain deployer, V4 projects use V4's deployers - let to: `0x${string}` - if (version === 5) { - // V5 only has JBOmnichainDeployer - to = jbContractAddress['5']['JBOmnichainDeployer'][chainId] as `0x${string}` - } else { - // V4 has two deployers: use 4_1 for controller 4.1, regular for older controllers - to = jbContractAddress['4'][JBOmnichainDeployerContracts.JBOmnichainDeployer4_1][chainId] as `0x${string}` - if (projectControllerAddress === jbContractAddress['4'][JBCoreContracts.JBController][chainId]) { - to = jbContractAddress['4'][JBOmnichainDeployerContracts.JBOmnichainDeployer][chainId] as `0x${string}` + + const txs = await Promise.all( + chainIds.map(async chainId => { + const baseArgs = editData[chainId] + if (!baseArgs) throw new Error(`No edit data for chain ${chainId}`) + const args = [...baseArgs, projectControllerAddress] as const + + // V5 projects use V5's omnichain deployer, V4 projects use V4's deployers + let to: `0x${string}` + if (version === 5) { + // V5 only has JBOmnichainDeployer + to = jbContractAddress['5']['JBOmnichainDeployer'][chainId] as `0x${string}` + } else { + // V4 has two deployers: use 4_1 for controller 4.1, regular for older controllers + to = jbContractAddress['4'][JBOmnichainDeployerContracts.JBOmnichainDeployer4_1][chainId] as `0x${string}` + if (projectControllerAddress === jbContractAddress['4'][JBCoreContracts.JBController][chainId]) { + to = jbContractAddress['4'][JBOmnichainDeployerContracts.JBOmnichainDeployer][chainId] as `0x${string}` + } } - } - return { - data: { from: userAddress, to, value: 0n, gas: 200_000n * BigInt(chainIds.length), data: encoded }, - chainId, - } - }) + const gas = await estimateContractGasWithFallback({ + chainId, + contractAddress: to, + abi: jbOmnichainDeployer4_1Abi, + functionName: 'queueRulesetsOf', + args, + userAddress, + fallbackGas: OMNICHAIN_GAS_FALLBACKS.QUEUE_RULESETS, + }) + + const encoded = encodeFunctionData({ abi: jbOmnichainDeployer4_1Abi, functionName: 'queueRulesetsOf', args }) + + return { + data: { from: userAddress, to, value: 0n, gas, data: encoded }, + chainId, + } + }) + ) return getRelayrTxQuote(txs) } diff --git a/src/packages/v4v5/views/V4V5ProjectSettings/components/QueueSafeDeployErc20TxsModal.tsx b/src/packages/v4v5/views/V4V5ProjectSettings/components/QueueSafeDeployErc20TxsModal.tsx new file mode 100644 index 0000000000..4c5f469766 --- /dev/null +++ b/src/packages/v4v5/views/V4V5ProjectSettings/components/QueueSafeDeployErc20TxsModal.tsx @@ -0,0 +1,85 @@ +import { Trans, t } from '@lingui/macro' +import { FormInstance } from 'antd' +import { JBChainId, createSalt, jbControllerAbi } from 'juice-sdk-core' +import { useJBContractContext, useSuckers } from 'juice-sdk-react' +import { SafeProposeTransactionResponse, useProposeSafeTransaction } from 'packages/v4v5/hooks/useProposeSafeTransaction' + +import { IssueErc20TokenTxArgs } from 'components/buttons/IssueErc20TokenButton' +import QueueSafeTxsModal from 'packages/v4v5/components/QueueSafeTxsModal' +import useV4V5ProjectOwnerOf from 'packages/v4v5/hooks/useV4V5ProjectOwnerOf' +import { getChainName } from 'packages/v4v5/utils/networks' +import { useCallback, useMemo } from 'react' +import { emitInfoNotification } from 'utils/notifications' +import { encodeFunctionData } from 'viem' + +export interface QueueSafeDeployErc20TxsModalProps { + open: boolean + onCancel: VoidFunction + onSuccess?: VoidFunction + form: FormInstance +} + +export default function QueueSafeDeployErc20TxsModal({ + open, + onCancel, + onSuccess, + form, +}: QueueSafeDeployErc20TxsModalProps) { + const { data: safeAddress } = useV4V5ProjectOwnerOf() + const { proposeTransaction } = useProposeSafeTransaction({ safeAddress: safeAddress || '' }) + const { data: suckers } = useSuckers() + const { contracts } = useJBContractContext() + + const salt = useMemo(() => createSalt(), []) + + const handleExecuteDeployErc20OnChain = useCallback(async (chainId: JBChainId, signerAddress?: string) => { + const formValues = form.getFieldsValue() + if (!formValues.name || !formValues.symbol) throw new Error('Token name and symbol are required') + if (!safeAddress) throw new Error('Safe address is required') + if (!suckers) throw new Error('No project chains available') + + const sucker = suckers.find(s => s.peerChainId === chainId) + if (!sucker) throw new Error(`No project found for chain ${chainId}`) + + const projectId = BigInt(sucker.projectId) + + const data = encodeFunctionData({ + abi: jbControllerAbi, + functionName: 'deployERC20For', + args: [projectId, formValues.name, formValues.symbol, salt], + }) + + const result: SafeProposeTransactionResponse = await proposeTransaction({ + to: contracts.controller.data ?? '', + data, + value: '0', + chainId, + signerAddressOverride: signerAddress, + }) + + emitInfoNotification( + t`Safe transaction queued on ${getChainName(chainId)}` + ) + return result + }, [form, safeAddress, suckers, proposeTransaction, contracts.controller.data, salt]) + + return ( + Queue Safe ERC-20 Deploy Transactions} + description={ + + Since your project owner is a Gnosis Safe and this is an omnichain project, + you need to queue separate ERC-20 deploy transactions on each chain. + + } + onExecuteChain={handleExecuteDeployErc20OnChain} + safeAddress={safeAddress || ''} + buttonTextOverride={{ + execute: (chainName: string) => Queue on {chainName} + }} + /> + ) +} diff --git a/src/pages/v4/[jbUrn]/index.tsx b/src/pages/v4/[jbUrn]/index.tsx index ec32f4b643..1581782f71 100644 --- a/src/pages/v4/[jbUrn]/index.tsx +++ b/src/pages/v4/[jbUrn]/index.tsx @@ -81,9 +81,9 @@ export const getStaticProps: GetStaticProps< props.props.seoProps = { title: metadata?.name, url: `${SiteBaseUrl}v4/${urn}`, - description, - image: projectImage, twitterCard: 'summary_large_image' as const, + ...(description && { description }), + ...(projectImage && { image: projectImage }), ...(metadata?.twitter && { twitterCreator: metadata.twitter }), } } diff --git a/src/pages/v5/[jbUrn]/index.tsx b/src/pages/v5/[jbUrn]/index.tsx index b44c618cdf..b5f3ea52d8 100644 --- a/src/pages/v5/[jbUrn]/index.tsx +++ b/src/pages/v5/[jbUrn]/index.tsx @@ -81,9 +81,9 @@ export const getStaticProps: GetStaticProps< props.props.seoProps = { title: metadata?.name, url: `${SiteBaseUrl}v5/${urn}`, - description, - image: projectImage, twitterCard: 'summary_large_image' as const, + ...(description && { description }), + ...(projectImage && { image: projectImage }), ...(metadata?.twitter && { twitterCreator: metadata.twitter }), } } diff --git a/src/utils/nftRewards.ts b/src/utils/nftRewards.ts index 47766d2ada..40a1db090c 100644 --- a/src/utils/nftRewards.ts +++ b/src/utils/nftRewards.ts @@ -380,7 +380,10 @@ export function buildJB721TierParams({ if (version === JB721DelegateVersion.JB721DELEGATE_V3_1) { return nftRewardTierToJB721TierParamsV3_1(rewardTier, cid) } - if (version === JB721DelegateVersion.JB721DELEGATE_V4) { + if ( + version === JB721DelegateVersion.JB721DELEGATE_V4 || + version === JB721DelegateVersion.JB721DELEGATE_V5 + ) { return nftRewardTierToJB721TierParamsV4(rewardTier, cid) } @@ -391,49 +394,24 @@ export function buildJB721TierParams({ .slice() // clone object .sort((a, b) => { // Tiers MUST BE in ascending order when sent to contract. - - // bit bongy, sorry! - if ( - version === JB721DelegateVersion.JB721DELEGATE_V3_2 || - version === JB721DelegateVersion.JB721DELEGATE_V3_3 || - version === JB721DelegateVersion.JB721DELEGATE_V3_4 || - version === JB721DelegateVersion.JB721DELEGATE_V4 - ) { - if ( - (a as JB_721_TIER_PARAMS_V3_2).price.gt( - (b as JB_721_TIER_PARAMS_V3_2).price, - ) - ) { - return 1 - } - - if ( - (a as JB_721_TIER_PARAMS_V3_2).price.lt( - (b as JB_721_TIER_PARAMS_V3_2).price, - ) - ) { - return -1 - } - + // Determine price property: V3.2+ uses 'price', V3.1 uses 'contributionFloor' + const priceA = + 'price' in a ? a.price : (a as JB_721_TIER_PARAMS_V3_1).contributionFloor + const priceB = + 'price' in b ? b.price : (b as JB_721_TIER_PARAMS_V3_1).contributionFloor + + // Runtime type check: bigint (V4/V5) vs BigNumber (V3.x) + if (typeof priceA === 'bigint' && typeof priceB === 'bigint') { + if (priceA > priceB) return 1 + if (priceA < priceB) return -1 return 0 } - if ( - (a as JB_721_TIER_PARAMS_V3_1).contributionFloor.gt( - (b as JB_721_TIER_PARAMS_V3_1).contributionFloor, - ) - ) { - return 1 - } - - if ( - (a as JB_721_TIER_PARAMS_V3_1).contributionFloor.lt( - (b as JB_721_TIER_PARAMS_V3_1).contributionFloor, - ) - ) { - return -1 - } - + // BigNumber comparison (V3.x) + const bnPriceA = priceA as BigNumber + const bnPriceB = priceB as BigNumber + if (bnPriceA.gt(bnPriceB)) return 1 + if (bnPriceA.lt(bnPriceB)) return -1 return 0 }) } diff --git a/yarn.lock b/yarn.lock index 7e1a8bfba8..93bb94ceda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2275,10 +2275,10 @@ dependencies: tslib "^2.1.0" -"@getpara/core-components@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.0.0-alpha.64.tgz#631c37d8cf73ca35dc4ebc1329b56088ea7dcf3e" - integrity sha512-7MqAKFeRx+QzLv1MoC8x/xrVTNg2eBte/N7DKUB8uPlQeslJODuS1rLbdF448xDmxCZ25M2vD7YjTCO9labnSA== +"@getpara/core-components@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.0.0.tgz#cd328e292dc9c2fc132dbf7784e53c71e2c663b1" + integrity sha512-XPjL4AaVKhUcNWKmHuUqKJPPjGFsUgAVvWPI1LlLwnei63Uvj7Mk5sc/PR3ohwcHTSJ9WPPHTMWlop4Xu0gOgw== dependencies: "@stencil/core" "^4.7.0" color-blend "^4.0.0" @@ -2287,67 +2287,68 @@ inputmask "5.0.9" qrcode-with-logos "1.1.1" -"@getpara/core-sdk@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.0.0-alpha.64.tgz#81d3008f7d926afec482d584e026cdfd76045b9c" - integrity sha512-QnIdJhtUX+hwkUyW1//qb2od8mdw6CqdwtPThWsNmBCxjW7BL9tSveB++82fm9CELcIMkp0OcNTT+b6JCGKd0w== +"@getpara/core-sdk@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.0.0.tgz#5eeb9168736b52a29fecc74e729cee4ebe512e3c" + integrity sha512-oMrShuiC5vQs21MW+4BsoqiR1ACgkRi/EYCq2Ns2aa4TZDphV4H0mFYYOVQVHuWy2i/Iql/zsxNedqEc4bIocg== dependencies: "@celo/utils" "^8.0.2" "@cosmjs/encoding" "^0.32.4" "@ethereumjs/util" "^9.1.0" - "@getpara/user-management-client" "2.0.0-alpha.64" + "@getpara/user-management-client" "2.0.0" "@noble/hashes" "^1.5.0" base64url "^3.0.1" libphonenumber-js "^1.11.7" node-forge "^1.3.1" uuid "^11.1.0" -"@getpara/ethers-v5-integration@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/ethers-v5-integration/-/ethers-v5-integration-2.0.0-alpha.64.tgz#9faa96eaa8640a089bae21d8b98cb9eba45ab222" - integrity sha512-XCVbt/gWN0aSIu79uPh7pNRriv8O5ni9tis7hq2lPlgcgWef2woYh9VpiYEnRk09NThWZecNehnnTO1/FTr9HQ== +"@getpara/ethers-v5-integration@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/ethers-v5-integration/-/ethers-v5-integration-2.0.0.tgz#b2749f442251c70dd943a178d086f04ce76299fb" + integrity sha512-Q5/KkFoktTV+Netx56UXfVAZMp0P2FZd6nkzJ2HeNc0vCNTHZoujwmDVtmJlGhVQq4FEtgYlTodFgVKh9yDJxw== dependencies: - "@getpara/core-sdk" "2.0.0-alpha.64" + "@getpara/core-sdk" "2.0.0" -"@getpara/evm-wallet-connectors@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/evm-wallet-connectors/-/evm-wallet-connectors-2.0.0-alpha.64.tgz#c09188d5174bbe9856e269341a4fa1b07dcd9e25" - integrity sha512-gCJHw+CR57XYMZv47ru6ZTCYq26ry5zSisdDXm5KuDZRmG1pOAYMof5bD7SwynAP9vt2LKQ4WSpDRjzcW/WUZg== +"@getpara/evm-wallet-connectors@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/evm-wallet-connectors/-/evm-wallet-connectors-2.0.0.tgz#d37cd293df996066d9fa68368ae8377e912bb56d" + integrity sha512-mvAeP2qPeonkZjtqa74COLMxWDXtl4+jVD7JSPEMv/H/7TgQr2+VGJ5+k0JV6aOMkR9vyuAO4Cbm6a7tH+nSlQ== dependencies: "@coinbase/wallet-sdk" "4.3.0" - "@getpara/wagmi-v2-connector" "2.0.0-alpha.64" - "@getpara/web-sdk" "2.0.0-alpha.64" + "@getpara/wagmi-v2-connector" "2.0.0" + "@getpara/web-sdk" "2.0.0" + "@walletconnect/ethereum-provider" "2.23.0" zustand "^4.5.2" zustand-sync-tabs "^0.2.2" -"@getpara/react-common@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.0.0-alpha.64.tgz#5e5363316f2a2abce931cbf23edbe05902650a9c" - integrity sha512-hOs2VQvWsLSv4O30NakimYsi/4nT8fyK3DqpxjwVQT1PZ+Es2WqPQ+wlnMzzPaoHdbjhG7BJePnYJxpMu2z9Ew== +"@getpara/react-common@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.0.0.tgz#7e13ca19b333c7e4f85e82d367b9e9b36825d66f" + integrity sha512-CrkkjIKQPAzBZwXvuK0j8bkXkFlxAIJKBGL4oWC1mmKCTt3pzBsUR91ya9L9gF8eKOkPGH42kfMTkQZTt8G9Pw== dependencies: - "@getpara/react-components" "2.0.0-alpha.64" - "@getpara/web-sdk" "2.0.0-alpha.64" + "@getpara/react-components" "2.0.0" + "@getpara/web-sdk" "2.0.0" "@moonpay/moonpay-react" "^1.8.3" "@ramp-network/ramp-instant-sdk" "^4.0.5" libphonenumber-js "^1.11.7" styled-components "^6.1.8" ua-parser-js "^2.0.2" -"@getpara/react-components@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.0.0-alpha.64.tgz#e54ac2686b25408b24af257d0a951c989459048e" - integrity sha512-cfUa5x/4FaTTZ6IGoUnHp+YxvILqnhKK74RP5l8ScqejNRNVE5ZrYl1Qy8dwY3zkKWFGvH1Cd7xIUCXVTqCogA== +"@getpara/react-components@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.0.0.tgz#3fb95464475ddae2002c935f17246c3c49a96b3e" + integrity sha512-V3gGFFcSG1nd9ZoN9rtsaKx2SlyePPvzL+wkLvIPHoe0dKrK5EwLdCxsBzpZvJSCSyE9fZ6DFMviWsO9P+Klkw== dependencies: - "@getpara/core-components" "2.0.0-alpha.64" + "@getpara/core-components" "2.0.0" -"@getpara/react-sdk-lite@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.0.0-alpha.64.tgz#6896ac0fd9fa62f630a346e7b9ef4b1d5af5e45f" - integrity sha512-bZpdPzuWjwuYbYKDWQhdk4Rpp3735+tqEtBhEXMOfFflDGWJF7dRXQh3eVqgzEmBpeX9F5ZaEH8uqSn61lrQiA== +"@getpara/react-sdk-lite@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.0.0.tgz#63df154139ad61af575e75ef1ce3826663ccc8b2" + integrity sha512-kT32KBrdBh0g/xuRBByaj0KnAitb0HVcKDm5Yn4ibec0u6Nn38jQHf0ZGwSsTKUJtz680Jn0lF1uTbGODE5iWg== dependencies: - "@getpara/react-common" "2.0.0-alpha.64" - "@getpara/react-components" "2.0.0-alpha.64" - "@getpara/web-sdk" "2.0.0-alpha.64" + "@getpara/react-common" "2.0.0" + "@getpara/react-components" "2.0.0" + "@getpara/web-sdk" "2.0.0" date-fns "^3.6.0" framer-motion "^11.3.31" libphonenumber-js "^1.11.7" @@ -2355,42 +2356,42 @@ zustand "^4.5.2" zustand-sync-tabs "^0.2.2" -"@getpara/shared@1.3.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@getpara/shared/-/shared-1.3.0.tgz#ea6c1936c2354bde9a677c6245bf45b7e676a5f4" - integrity sha512-AmxY6XRJrBdQh37HRb10QwPwCbwuQPX7WrZOppTCl5IteK4/O/EwAhsugneYm0dU6dKGnpxj8qL5m1aa6Bp6TA== +"@getpara/shared@1.8.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@getpara/shared/-/shared-1.8.0.tgz#edaf9c58402c97b49fcc0938051735ed32fbf79a" + integrity sha512-zDZRtTfmrNvuve/99axHpgEl0HfwmESS37BfWk2yCCw397N6wThSKEFKJN/mpJCMVKCPcZjRrKtxq1iTjhmAEA== -"@getpara/user-management-client@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.0.0-alpha.64.tgz#03b5b955bd617a758469564a3349bdcf6c5cd5ec" - integrity sha512-hw9AYdnXQP4C1A6mO7oOKJot88x6soEGnNNdzd0wv302tN6XtUGaGGOot/HJ0K4s0bB+NC3hVk8/GVN9/16D4A== +"@getpara/user-management-client@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.0.0.tgz#8cce77b2c761f9ba3815e5cc4ab2e18d411ebc40" + integrity sha512-0FNORN8jIewPgg21OuNUqYZHIr5Ea9JKe0M6WUz0FGdKLx27UeHMOoFUe3TkKhBjLGEbkQiXwoSSkUg4el775A== dependencies: - "@getpara/shared" "1.3.0" + "@getpara/shared" "1.8.0" axios "^1.8.4" libphonenumber-js "^1.11.7" -"@getpara/viem-v2-integration@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/viem-v2-integration/-/viem-v2-integration-2.0.0-alpha.64.tgz#d2beffdcfb7410dba52f888fa9e2fe048b0bae04" - integrity sha512-n9w76d1OeiPaIbqsekHcuExX55HKsfpxuhLknQDA7C045QAslcv58/+6elsPCwTwWiMq1sVVf0/BsI61OPWxUA== +"@getpara/viem-v2-integration@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/viem-v2-integration/-/viem-v2-integration-2.0.0.tgz#fa8081dbd220fdc7a6591894214169df84a6f3be" + integrity sha512-G6F4nl+joSj0uNRVbHQjthXDNVi7vZRHPjNotYAM+h5Ek9wZI5hg3XZjMc0wbKSoqXx1Nn/jg8FfO2T9Xfqesw== dependencies: - "@getpara/core-sdk" "2.0.0-alpha.64" + "@getpara/core-sdk" "2.0.0" -"@getpara/wagmi-v2-connector@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/wagmi-v2-connector/-/wagmi-v2-connector-2.0.0-alpha.64.tgz#12b99a63c53ccada594a843cad5b449fca3d76ce" - integrity sha512-PFcNNMOkPiFQKRrJREGBVCrXCwjMolR6gJe4d2rMlptzv6+lxGlTJdWC0OvWXcLZZ1Fzw1rz6OrLSYj8GDjKgA== +"@getpara/wagmi-v2-connector@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/wagmi-v2-connector/-/wagmi-v2-connector-2.0.0.tgz#8707d70d16d51cddab12717ef677e22d3d6828df" + integrity sha512-q6WBlRbXkB5BZfaV/yOgaVX1M3xPYXQFHlt9oK8mqTUizWgdU0NiIWXNIuen9wFXZ23ji2bl5A+GNBQX8MDbnw== dependencies: - "@getpara/viem-v2-integration" "2.0.0-alpha.64" - "@getpara/web-sdk" "2.0.0-alpha.64" + "@getpara/viem-v2-integration" "2.0.0" + "@getpara/web-sdk" "2.0.0" -"@getpara/web-sdk@2.0.0-alpha.64": - version "2.0.0-alpha.64" - resolved "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.0.0-alpha.64.tgz#6b871018e6561b5e626cc18c0c10a747c8e121a0" - integrity sha512-gm7i91MhWYBTTVnXnb9V4vs/2gbI7N3B5D/puGxVN9VnGyTtT5jbVR66IPDuiUSzQSvWV8AJajCUXX1lDRCXLQ== +"@getpara/web-sdk@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.0.0.tgz#e67569c1e244e5561bbdbc8178547832bb784518" + integrity sha512-GE8yj9QOpNBxdzdmMBhMHhdkKOp0gtirYOffgibM2rqJCO87sSTqY84a2X+fSvAYRaiMNXk1sBVUcGfAIjz+7g== dependencies: - "@getpara/core-sdk" "2.0.0-alpha.64" - "@getpara/user-management-client" "2.0.0-alpha.64" + "@getpara/core-sdk" "2.0.0" + "@getpara/user-management-client" "2.0.0" base64url "^3.0.1" buffer "6.0.3" cbor-web "^9.0.2" @@ -3672,6 +3673,11 @@ resolved "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz#a28799c463177d1a0b0e5cefdc173da5ac859eb4" integrity sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ== +"@lit/react@1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz#b3e229173b7b57d550909bf95d8f3da1a9510557" + integrity sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw== + "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03" @@ -3971,6 +3977,11 @@ "@motionone/dom" "^10.16.4" tslib "^2.3.1" +"@msgpack/msgpack@3.1.2": + version "3.1.2" + resolved "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@next/bundle-analyzer@^14.2.0": version "14.2.0" resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-14.2.0.tgz#f22fa4c7e4c73f76eee13921cca1973c72e726c4" @@ -4050,7 +4061,7 @@ resolved "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz#3812b72c057a28b44ff0ad4aff5ca846e5b9cdc9" integrity sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA== -"@noble/ciphers@^1.3.0": +"@noble/ciphers@1.3.0", "@noble/ciphers@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== @@ -4090,6 +4101,13 @@ dependencies: "@noble/hashes" "1.7.1" +"@noble/curves@1.9.1": + version "1.9.1" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz#9654a0bc6c13420ae252ddcf975eaf0f58f0a35c" + integrity sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA== + dependencies: + "@noble/hashes" "1.8.0" + "@noble/curves@1.9.2", "@noble/curves@^1.9.1", "@noble/curves@~1.9.0": version "1.9.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" @@ -4097,6 +4115,13 @@ dependencies: "@noble/hashes" "1.8.0" +"@noble/curves@1.9.7": + version "1.9.7" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + "@noble/curves@^1.6.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" @@ -4358,6 +4383,13 @@ tslib "^2.5.0" webcrypto-core "^1.7.7" +"@phosphor-icons/webcomponents@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz#79dcdcc49ae17ea309080eebc463710221ced890" + integrity sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q== + dependencies: + lit "^3" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -4432,6 +4464,15 @@ dayjs "1.11.13" viem ">=2.29.0" +"@reown/appkit-common@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.11.tgz#90e350d024c82d711b42319eb7d6768568287e0d" + integrity sha512-rRcxrah6uouqEo/VbniVH11Y3H27BsP+Psv2+Usic+3Rt4kiSImIyeDG1YBV0gZNmME9N3sXHK8Bt7iqkxdOWw== + dependencies: + big.js "6.2.2" + dayjs "1.11.13" + viem ">=2.37.9" + "@reown/appkit-controllers@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.8.tgz#0e4c24afaacca2251745c8844463589dda6d9e66" @@ -4443,6 +4484,17 @@ valtio "1.13.2" viem ">=2.29.0" +"@reown/appkit-controllers@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.11.tgz#2e818f770f396b5df799e8818379cfc5fb803d0a" + integrity sha512-V3hPB6NE7kM+7pS8n4ygZWbMh/XoHYxdPWxH3qtDlvYlH9Rgc3yvU8+IFWdB79Ta0lyJIbGqVlQeH5sQe4QOsw== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-wallet" "1.8.11" + "@walletconnect/universal-provider" "2.22.4" + valtio "2.1.7" + viem ">=2.37.9" + "@reown/appkit-pay@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz#c1ff423635869578f6ad12e6c08180c0532bf8ab" @@ -4455,6 +4507,18 @@ lit "3.3.0" valtio "1.13.2" +"@reown/appkit-pay@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.11.tgz#797875488114ab33b55f02cad39ae7221944e709" + integrity sha512-68IB3sKfxlCwLz44jvWWpULnmyIGHwnItojrr/PRXUof3z9t/nV8G7FiRY4ZDIo75EkabcIguhtYNSQ7R3vZbA== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-controllers" "1.8.11" + "@reown/appkit-ui" "1.8.11" + "@reown/appkit-utils" "1.8.11" + lit "3.3.0" + valtio "2.1.7" + "@reown/appkit-polyfills@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.8.tgz#a0d362df8479cc66b7c6aa89e696f30783a3d21b" @@ -4462,6 +4526,13 @@ dependencies: buffer "6.0.3" +"@reown/appkit-polyfills@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.11.tgz#a88fe85db23aa57cecf4377da37c82a92ce285c3" + integrity sha512-pP9k5dvtWil88Zv3UgGurtbUmTx47z/5eriClGf8JI0VjBu/IExbAHg2gIZNEFdvkFD5/fIqIg8zno46SKbCKQ== + dependencies: + buffer "6.0.3" + "@reown/appkit-scaffold-ui@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.8.tgz#36b5eb71b2e4d6525fa9a696af5c4ae83ae17f63" @@ -4474,6 +4545,18 @@ "@reown/appkit-wallet" "1.7.8" lit "3.3.0" +"@reown/appkit-scaffold-ui@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.11.tgz#2db340be494ea671d110f671c38de69f7bfbd94a" + integrity sha512-oCEhNdUh2d59UHp/rLwfzv5odWg0pw000fg4Z53orjnKzcZfBpBDM00I9hu2pijnaYitJVqwsqCQ1F1q6hju/Q== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-controllers" "1.8.11" + "@reown/appkit-ui" "1.8.11" + "@reown/appkit-utils" "1.8.11" + "@reown/appkit-wallet" "1.8.11" + lit "3.3.0" + "@reown/appkit-ui@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.8.tgz#014b30a7378cfc685aa1d5a543d59ac5a9dd8ed2" @@ -4485,6 +4568,18 @@ lit "3.3.0" qrcode "1.5.3" +"@reown/appkit-ui@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.11.tgz#bc37c160892fca91a0d7af718abdc0d6f92dc4ea" + integrity sha512-kBWZCiGaB/M2exIiDglsaTWYsWR0L89WXi7IFA6RgW0B0piYv5eiS3l/KPNj9/zxK8UnKsGsMefxl/UK/QqMng== + dependencies: + "@phosphor-icons/webcomponents" "2.1.5" + "@reown/appkit-common" "1.8.11" + "@reown/appkit-controllers" "1.8.11" + "@reown/appkit-wallet" "1.8.11" + lit "3.3.0" + qrcode "1.5.3" + "@reown/appkit-utils@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz#86a35184976a9ba8a935ba44ca68567eea10fba0" @@ -4499,6 +4594,21 @@ valtio "1.13.2" viem ">=2.29.0" +"@reown/appkit-utils@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.11.tgz#8f98c45b1094c3003b7b1be185a1148125bd0136" + integrity sha512-6Wplz7LNe2HoxHmGGYT6FIp8saa0DzVP/427jlNuWpkL65/azPyYjA8tKptxbFWViQ4VYBsqLoQxivXC/xNMBg== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-controllers" "1.8.11" + "@reown/appkit-polyfills" "1.8.11" + "@reown/appkit-wallet" "1.8.11" + "@wallet-standard/wallet" "1.1.0" + "@walletconnect/logger" "^3.0.0" + "@walletconnect/universal-provider" "2.22.4" + valtio "2.1.7" + viem ">=2.37.9" + "@reown/appkit-wallet@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz#291b8c225fd3c2585d1f3e65c689a791d5ce3e5d" @@ -4509,6 +4619,16 @@ "@walletconnect/logger" "2.1.2" zod "3.22.4" +"@reown/appkit-wallet@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.11.tgz#8d52a16ccca560ff4326af77fc3391b394114442" + integrity sha512-tCzrieMuOD4tDcNQjMe83T38tJUfRdv4LG+cYGyGK1RpPGaszbq06TQVbCPkRrGiTELwkrKwXjSg1XAuHktS2w== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-polyfills" "1.8.11" + "@walletconnect/logger" "^3.0.0" + zod "3.22.4" + "@reown/appkit@1.7.8": version "1.7.8" resolved "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz#6174bca032a4a2bf4fcfc78969e09210dff85214" @@ -4528,6 +4648,27 @@ valtio "1.13.2" viem ">=2.29.0" +"@reown/appkit@1.8.11": + version "1.8.11" + resolved "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.11.tgz#67bfd0f6fa095ad9c3838be1e75dc4423b6393c5" + integrity sha512-G96zt1P3ASrivV7HRrFQuzkCEzSmy3ca7cSQxaLeIS+P2VVhjE5YAyINArGVbooY0heHCPvQuTNM+YgK4oBmfg== + dependencies: + "@reown/appkit-common" "1.8.11" + "@reown/appkit-controllers" "1.8.11" + "@reown/appkit-pay" "1.8.11" + "@reown/appkit-polyfills" "1.8.11" + "@reown/appkit-scaffold-ui" "1.8.11" + "@reown/appkit-ui" "1.8.11" + "@reown/appkit-utils" "1.8.11" + "@reown/appkit-wallet" "1.8.11" + "@walletconnect/universal-provider" "2.22.4" + bs58 "6.0.0" + semver "7.7.2" + valtio "2.1.7" + viem ">=2.37.9" + optionalDependencies: + "@lit/react" "1.0.8" + "@repeaterjs/repeater@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.4.tgz#a04d63f4d1bf5540a41b01a921c9a7fddc3bd1ca" @@ -4677,6 +4818,11 @@ resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.1.tgz#254521c188b49e8b2d4cc048b475fb2b38737fec" integrity sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA== +"@scure/base@1.2.6", "@scure/base@~1.2.2", "@scure/base@~1.2.4", "@scure/base@~1.2.5": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== + "@scure/base@^1.1.3": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" @@ -4687,11 +4833,6 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== -"@scure/base@~1.2.2", "@scure/base@~1.2.4", "@scure/base@~1.2.5": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" - integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== - "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -6098,6 +6239,18 @@ mipd "0.0.7" zustand "5.0.0" +"@wallet-standard/base@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz#214093c0597a1e724ee6dbacd84191dfec62bb33" + integrity sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ== + +"@wallet-standard/wallet@1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz#a1e46a3f1b2d06a0206058562169b1f0e9652d0f" + integrity sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg== + dependencies: + "@wallet-standard/base" "^1.1.0" + "@walletconnect/core@2.17.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.17.0.tgz#bf490e85a4702eff0f7cf81ba0d3c1016dffff33" @@ -6166,6 +6319,52 @@ events "3.3.0" uint8arrays "3.1.0" +"@walletconnect/core@2.22.4": + version "2.22.4" + resolved "https://registry.npmjs.org/@walletconnect/core/-/core-2.22.4.tgz#8a4b71d245fd7625e17a0e83f8e00830c201d256" + integrity sha512-ZQnyDDpqDPAk5lyLV19BRccQ3wwK3LmAwibuIv3X+44aT/dOs2kQGu9pla3iW2LgZ5qRMYvgvvfr5g3WlDGceQ== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.16" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.22.4" + "@walletconnect/utils" "2.22.4" + "@walletconnect/window-getters" "1.0.1" + es-toolkit "1.39.3" + events "3.3.0" + uint8arrays "3.1.1" + +"@walletconnect/core@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.0.tgz#a329de65d3e52401a234484d8351b16f3101f7aa" + integrity sha512-W++xuXf+AsMPrBWn1It8GheIbCTp1ynTQP+aoFB86eUwyCtSiK7UQsn/+vJZdwElrn+Ptp2A0RqQx2onTMVHjQ== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.16" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.23.0" + "@walletconnect/utils" "2.23.0" + "@walletconnect/window-getters" "1.0.1" + es-toolkit "1.39.3" + events "3.3.0" + uint8arrays "3.1.1" + "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -6190,6 +6389,24 @@ "@walletconnect/utils" "2.21.1" events "3.3.0" +"@walletconnect/ethereum-provider@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/ethereum-provider/-/ethereum-provider-2.23.0.tgz#d24cba95b942564999754e7fc944a38da06c0e47" + integrity sha512-jDuFarWWTbET2UhwUBBDfr1ZcPnrKBmqQtIc5EG6+ftzD+GcCz+cEJH7YJ5O77IdT8Uds9ETuIngvRokyWRSUw== + dependencies: + "@reown/appkit" "1.8.11" + "@walletconnect/jsonrpc-http-connection" "1.0.8" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/sign-client" "2.23.0" + "@walletconnect/types" "2.23.0" + "@walletconnect/universal-provider" "2.23.0" + "@walletconnect/utils" "2.23.0" + events "3.3.0" + "@walletconnect/ethereum-provider@^2.13.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.17.0.tgz#d74feaaed6180a6799e96760d7ee867ff3a083d2" @@ -6296,6 +6513,14 @@ "@walletconnect/safe-json" "^1.0.2" pino "7.11.0" +"@walletconnect/logger@3.0.0", "@walletconnect/logger@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz#7bea325db870a047ea98a2f23cc3037a3142bf86" + integrity sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg== + dependencies: + "@walletconnect/safe-json" "^1.0.2" + pino "10.0.0" + "@walletconnect/modal-core@2.7.0": version "2.7.0" resolved "https://registry.yarnpkg.com/@walletconnect/modal-core/-/modal-core-2.7.0.tgz#73c13c3b7b0abf9ccdbac9b242254a86327ce0a4" @@ -6403,6 +6628,36 @@ "@walletconnect/utils" "2.21.1" events "3.3.0" +"@walletconnect/sign-client@2.22.4": + version "2.22.4" + resolved "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.22.4.tgz#170412738e7a6c273cc0b3da6c3b226464e9551c" + integrity sha512-la+sol0KL33Fyx5DRlupHREIv8wA6W33bRfuLAfLm8pINRTT06j9rz0IHIqJihiALebFxVZNYzJnF65PhV0q3g== + dependencies: + "@walletconnect/core" "2.22.4" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "3.0.0" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.22.4" + "@walletconnect/utils" "2.22.4" + events "3.3.0" + +"@walletconnect/sign-client@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.0.tgz#0039af472635dc0b85e78d3e288dd47b0111464c" + integrity sha512-Nzf5x/LnQgC0Yjk0NmkT8kdrIMcScpALiFm9gP0n3CulL+dkf3HumqWzdoTmQSqGPxwHu/TNhGOaRKZLGQXSqw== + dependencies: + "@walletconnect/core" "2.23.0" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "3.0.0" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.23.0" + "@walletconnect/utils" "2.23.0" + events "3.3.0" + "@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" @@ -6446,6 +6701,30 @@ "@walletconnect/logger" "2.1.2" events "3.3.0" +"@walletconnect/types@2.22.4": + version "2.22.4" + resolved "https://registry.npmjs.org/@walletconnect/types/-/types-2.22.4.tgz#42b50d81c30c8c4d58b60fe9177034afcb0243de" + integrity sha512-KJdiS9ezXzx1uASanldYaaenDwb42VOQ6Rj86H7FRwfYddhNnYnyEaDjDKOdToGRGcpt5Uzom6qYUOnrWEbp5g== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + events "3.3.0" + +"@walletconnect/types@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.0.tgz#4e49bdcc0e7f77f3aae9f0a8189ad7322687fcde" + integrity sha512-9ZEOJyx/kNVCRncDHh3Qr9eH7Ih1dXBFB4k1J8iEudkv3t4GhYpXhqIt2kNdQWluPb1BBB4wEuckAT96yKuA8g== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + events "3.3.0" + "@walletconnect/universal-provider@2.17.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.17.0.tgz#c9d4bbd9b8f0e41b500b2488ccbc207dc5f7a170" @@ -6497,6 +6776,42 @@ es-toolkit "1.33.0" events "3.3.0" +"@walletconnect/universal-provider@2.22.4": + version "2.22.4" + resolved "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.22.4.tgz#5b51ce1f06d48e586722e88a8acc33fc1d9af5d4" + integrity sha512-TF2RNX13qxa0rrBAhVDs5+C2G8CHX7L0PH5hF2uyQHdGyxZ3pFbXf8rxmeW1yKlB76FSbW80XXNrUes6eK/xHg== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/jsonrpc-http-connection" "1.0.8" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/sign-client" "2.22.4" + "@walletconnect/types" "2.22.4" + "@walletconnect/utils" "2.22.4" + es-toolkit "1.39.3" + events "3.3.0" + +"@walletconnect/universal-provider@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.23.0.tgz#fe2c533436a38fec6478872ef067ac2baf2c30d4" + integrity sha512-3ZEqAsbtCbk+CV0ZLpy7Qzc04KXEnrW4zCboZ+gkkC0ey4H62x9h23kBOIrU9qew6orjA7D5gg0ikRC2Up1lbw== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/jsonrpc-http-connection" "1.0.8" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/sign-client" "2.23.0" + "@walletconnect/types" "2.23.0" + "@walletconnect/utils" "2.23.0" + es-toolkit "1.39.3" + events "3.3.0" + "@walletconnect/utils@2.17.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.17.0.tgz#02b3af0b80d0c1a994d692d829d066271b04d071" @@ -6565,6 +6880,58 @@ uint8arrays "3.1.0" viem "2.23.2" +"@walletconnect/utils@2.22.4": + version "2.22.4" + resolved "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.22.4.tgz#4d6ac7782e0c68e27f4e170ff017369630973747" + integrity sha512-coAPrNiTiD+snpiXQyXakMVeYcddqVqII7aLU39TeILdPoXeNPc2MAja+MF7cKNM/PA3tespljvvxck/oTm4+Q== + dependencies: + "@msgpack/msgpack" "3.1.2" + "@noble/ciphers" "1.3.0" + "@noble/curves" "1.9.7" + "@noble/hashes" "1.8.0" + "@scure/base" "1.2.6" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.22.4" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + blakejs "1.2.1" + bs58 "6.0.0" + detect-browser "5.3.0" + ox "0.9.3" + uint8arrays "3.1.1" + +"@walletconnect/utils@2.23.0": + version "2.23.0" + resolved "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.0.tgz#58f05d9fd5ada294b9404e32babd762633097ba8" + integrity sha512-bVyv4Hl+/wVGueZ6rEO0eYgDy5deSBA4JjpJHAMOdaNoYs05NTE1HymV2lfPQQHuqc7suYexo9jwuW7i3JLuAA== + dependencies: + "@msgpack/msgpack" "3.1.2" + "@noble/ciphers" "1.3.0" + "@noble/curves" "1.9.7" + "@noble/hashes" "1.8.0" + "@scure/base" "1.2.6" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "3.0.0" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.23.0" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + blakejs "1.2.1" + bs58 "6.0.0" + detect-browser "5.3.0" + ox "0.9.3" + uint8arrays "3.1.1" + "@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" @@ -6765,6 +7132,16 @@ abitype@1.0.8, abitype@^1.0.2, abitype@^1.0.6, abitype@^1.0.8: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba" integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== +abitype@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz#510c5b3f92901877977af5e864841f443bf55406" + integrity sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A== + +abitype@^1.0.9: + version "1.2.1" + resolved "https://registry.npmjs.org/abitype/-/abitype-1.2.1.tgz#392df8d88388089908a7c38bcaf0ef7cb5039e8d" + integrity sha512-AhkAWBE5QqzSuaPi6B9w5scl5739iBknQdFFAbY/CybASOBVWtVmPavUYW1OrDRX/iZWB/Je80xhJMZz2G4G1Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -7601,9 +7978,9 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -blakejs@^1.1.0: +blakejs@1.2.1, blakejs@^1.1.0: version "1.2.1" - resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + resolved "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== bluebird@^3.5.0, bluebird@^3.5.2: @@ -9942,6 +10319,11 @@ es-toolkit@1.33.0: resolved "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz#bcc9d92ef2e1ed4618c00dd30dfda9faddf4a0b7" integrity sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg== +es-toolkit@1.39.3: + version "1.39.3" + resolved "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.3.tgz#934b2cab9578c496dcbc0305cae687258cb14aee" + integrity sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww== + es-toolkit@^1.39.3: version "1.40.0" resolved "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz#1132e3d8a990298edd08d7b830a12c4eebbe82bb" @@ -14031,6 +14413,15 @@ lit@3.3.0: lit-element "^4.2.0" lit-html "^3.3.0" +lit@^3: + version "3.3.1" + resolved "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz#9dc79be626bc9a3b824de98b107dd662cabdeda6" + integrity sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA== + dependencies: + "@lit/reactive-element" "^2.1.0" + lit-element "^4.2.0" + lit-html "^3.3.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -16000,6 +16391,34 @@ ox@0.8.1: abitype "^1.0.8" eventemitter3 "5.0.1" +ox@0.9.3: + version "0.9.3" + resolved "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz#92cc1008dcd913e919364fd4175c860b3eeb18db" + integrity sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg== + dependencies: + "@adraffy/ens-normalize" "^1.11.0" + "@noble/ciphers" "^1.3.0" + "@noble/curves" "1.9.1" + "@noble/hashes" "^1.8.0" + "@scure/bip32" "^1.7.0" + "@scure/bip39" "^1.6.0" + abitype "^1.0.9" + eventemitter3 "5.0.1" + +ox@0.9.6: + version "0.9.6" + resolved "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz#5cf02523b6db364c10ee7f293ff1e664e0e1eab7" + integrity sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg== + dependencies: + "@adraffy/ens-normalize" "^1.11.0" + "@noble/ciphers" "^1.3.0" + "@noble/curves" "1.9.1" + "@noble/hashes" "^1.8.0" + "@scure/bip32" "^1.7.0" + "@scure/bip39" "^1.6.0" + abitype "^1.0.9" + eventemitter3 "5.0.1" + p-cancelable@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" @@ -16412,6 +16831,13 @@ pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0: readable-stream "^4.0.0" split2 "^4.0.0" +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + pino-abstract-transport@v0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" @@ -16450,6 +16876,28 @@ pino-std-serializers@^6.0.0: resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.1.tgz#369f4ae2a19eb6d769ddf2c88a2164b76879a284" integrity sha512-wHuWB+CvSVb2XqXM0W/WOYUkVSPbiJb9S5fNB7TBhd8s892Xq910bRxwHtC4l71hgztObTjXL6ZheZXFjhDrDQ== +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz#3d1a8abc7a700142edebf02a7b291834da199fbe" + integrity sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA== + dependencies: + atomic-sleep "^1.0.0" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + slow-redact "^0.3.0" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pino@7.11.0: version "7.11.0" resolved "https://registry.yarnpkg.com/pino/-/pino-7.11.0.tgz#0f0ea5c4683dc91388081d44bff10c83125066f6" @@ -16750,6 +17198,11 @@ process-warning@^2.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626" integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg== +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -16817,6 +17270,11 @@ proxy-compare@2.6.0: resolved "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz#5e8c8b5c3af7e7f17e839bf6cf1435bcc4d315b0" integrity sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw== +proxy-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz#3262cff3a25a6dedeaa299f6cf2369d6f7588a94" + integrity sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q== + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -18292,6 +18750,11 @@ semver@7.3.7: dependencies: lru-cache "^6.0.0" +semver@7.7.2, semver@^7.5.3, semver@^7.6.2, semver@^7.7.1: + version "7.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -18319,11 +18782,6 @@ semver@^7.3.8, semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -semver@^7.5.3, semver@^7.6.2, semver@^7.7.1: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -18578,6 +19036,11 @@ slick@^1.12.2: resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== +slow-redact@^0.3.0: + version "0.3.2" + resolved "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz#d06e25195aa5c492d32631c53d9ae86043b8b0e2" + integrity sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw== + snake-case@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" @@ -18636,6 +19099,13 @@ sonic-boom@^3.0.0, sonic-boom@^3.1.0: dependencies: atomic-sleep "^1.0.0" +sonic-boom@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" + integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + dependencies: + atomic-sleep "^1.0.0" + "source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -19348,6 +19818,13 @@ thread-stream@^2.0.0: dependencies: real-require "^0.2.0" +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -19844,7 +20321,7 @@ uint8arrays@3.1.0: dependencies: multiformats "^9.4.2" -uint8arrays@^3.0.0: +uint8arrays@3.1.1, uint8arrays@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== @@ -20253,6 +20730,13 @@ valtio@1.13.2: proxy-compare "2.6.0" use-sync-external-store "1.2.0" +valtio@2.1.7: + version "2.1.7" + resolved "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz#203396dd7be778d00dca03fd629ded1254178eeb" + integrity sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg== + dependencies: + proxy-compare "^3.0.1" + value-or-promise@^1.0.11, value-or-promise@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c" @@ -20365,6 +20849,20 @@ viem@>=2.29.0, viem@^2.31.6: ox "0.8.1" ws "8.18.2" +viem@>=2.37.9: + version "2.41.2" + resolved "https://registry.npmjs.org/viem/-/viem-2.41.2.tgz#117eab182b6b5501e47462269bb63f8b365a802e" + integrity sha512-LYliajglBe1FU6+EH9mSWozp+gRA/QcHfxeD9Odf83AdH5fwUS7DroH4gHvlv6Sshqi1uXrYFA2B/EOczxd15g== + dependencies: + "@noble/curves" "1.9.1" + "@noble/hashes" "1.8.0" + "@scure/bip32" "1.7.0" + "@scure/bip39" "1.6.0" + abitype "1.1.0" + isows "1.0.7" + ox "0.9.6" + ws "8.18.3" + viem@^1.0.0, viem@^1.5.3: version "1.21.4" resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d" @@ -21027,6 +21525,11 @@ ws@8.18.2: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== +ws@8.18.3: + version "8.18.3" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"