From 1e962c5fcf492112fe7bd3877e75ba9f42b73685 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 13:36:13 -0600 Subject: [PATCH 01/23] refactor(activity): remove network toggle and separate event title - Remove testnet/mainnet toggle from protocol activity feed - Move event title (action label) to its own line above amount --- .../ProtocolActivityElement.tsx | 18 ++++---- .../ProtocolActivity/ProtocolActivityList.tsx | 44 +------------------ 2 files changed, 11 insertions(+), 51 deletions(-) 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..dd84385f00 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' @@ -16,26 +15,12 @@ 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' - 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: {}, @@ -65,31 +50,6 @@ export function ProtocolActivityList() {

Protocol Activity

- {/* Network Toggle */} -
- - -
From 0760dbf23942e733ec7b8aad268fc01062d0132b Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 13:36:54 -0600 Subject: [PATCH 02/23] chore: add CLAUDE.md to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From cbd41e03a4f41d10ac4edb50e0cb6176df954f30 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 13:56:03 -0600 Subject: [PATCH 03/23] fix: USD-based project issuance display and token calculation - Display correct currency symbol (ETH/USD) in Rulesets tab issuance rates - Convert ETH payments to USD value for token calculation on USD-based projects --- .../hooks/useProjectPaymentTokens.ts | 33 +++++++++++-------- .../useV4V5FormatConfigurationTokenSection.ts | 21 +++++++++--- 2 files changed, 36 insertions(+), 18 deletions(-) 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/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts index b8929f9500..58c49fc09d 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,7 +55,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ const totalIssuanceRateDatum: ConfigurationPanelDatum = useMemo(() => { const current = currentTotalIssuanceRateFormatted !== undefined - ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined if (upcomingRuleset === null || upcomingRulesetLoading) { @@ -54,13 +63,14 @@ export const useV4V5FormatConfigurationTokenSection = ({ } 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,7 +91,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ const currentPayerIssuanceRateFormatted = formattedNum(currentPayerIssuanceRate) const current = currentPayerIssuanceRate !== undefined - ? `${currentPayerIssuanceRateFormatted} ${tokenSymbol}/ETH` + ? `${currentPayerIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined if ( @@ -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, From ef909f1d117dac35c4e48e40451f3a5a4fb4f7f8 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 14:14:49 -0600 Subject: [PATCH 04/23] feat: add Safe support for ERC-20 token deployment - Add QueueSafeDeployErc20TxsModal for Safe-owned projects - Detect Safe ownership and route to Safe modal instead of Relayr - Queue deployERC20For transactions to Safe for each chain --- .../CreateErc20TokenSettingsPage.tsx | 34 +++++++- .../QueueSafeDeployErc20TxsModal.tsx | 85 +++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/packages/v4v5/views/V4V5ProjectSettings/components/QueueSafeDeployErc20TxsModal.tsx 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/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} + }} + /> + ) +} From 81028a062d84159dcaf2238ac93a51c81a6f2057 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 14:15:37 -0600 Subject: [PATCH 05/23] chore: bump para version to 2.0.0 --- package.json | 6 +- yarn.lock | 659 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 584 insertions(+), 81 deletions(-) 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/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" From c1b6f6694146044e1722d715fee5d868c9a38031 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 14:25:39 -0600 Subject: [PATCH 06/23] fix: show loading state instead of empty state while project loads - Add isLoading to ProjectMetadataContext - Pass loading state from metadata providers (v1, v2v3, v4v5) - Show loading spinner in V4V5ProjectDashboard while metadata loads --- src/contexts/ProjectMetadataContext.ts | 2 ++ .../v1/contexts/V1ProjectMetadataProvider.tsx | 3 +++ .../v2v3/contexts/V2V3ProjectMetadataProvider.tsx | 3 +++ .../v4v5/contexts/V4V5ProjectMetadataProvider.tsx | 2 ++ .../V4V5ProjectDashboard/V4V5ProjectDashboard.tsx | 11 +++++++++++ 5 files changed, 21 insertions(+) diff --git a/src/contexts/ProjectMetadataContext.ts b/src/contexts/ProjectMetadataContext.ts index cfa8461ef3..c84e741ef6 100644 --- a/src/contexts/ProjectMetadataContext.ts +++ b/src/contexts/ProjectMetadataContext.ts @@ -7,6 +7,7 @@ interface ProjectMetadataContextType { isArchived: boolean | undefined projectId: number | undefined pv: PV | undefined + isLoading: boolean refetchProjectMetadata: VoidFunction } @@ -16,6 +17,7 @@ export const ProjectMetadataContext = createContext( isArchived: undefined, projectId: undefined, pv: undefined, + isLoading: true, refetchProjectMetadata: () => console.error( 'ProjectMetadataContext.refetchProjectMetadata called but no provider set', 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/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/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 ( <> From 13c5771cc58c2e3493f802a9b97f162e671f3d27 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 14:33:12 -0600 Subject: [PATCH 07/23] fix: prevent undefined seoProps serialization in getStaticProps --- src/pages/v4/[jbUrn]/index.tsx | 4 ++-- src/pages/v5/[jbUrn]/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 }), } } From 4b756a19f2cd15429b4086c6a8811ec0c6261e81 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 16:31:04 -0600 Subject: [PATCH 08/23] feat: show NFT quick actions for Safe signers - Use useV4V5WalletHasPermission for NFT tab and button visibility - Remove dev environment bypass from permission hook - Show NFT tab when user has permission to add NFTs (owner/Safe signer/operator) - Show Add NFT button only for users with actual permission --- .../v4v5/hooks/useV4V5WalletHasPermission.ts | 7 +------ .../V4V5NftRewardsPanel/V4V5NftRewardsPanel.tsx | 17 +++++------------ .../V4V5ProjectTabs/V4V5ProjectTabs.tsx | 8 +++++++- 3 files changed, 13 insertions(+), 19 deletions(-) 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/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) From 59572a6ec5067977d8fa483685a397fded4eb3bb Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 16:44:23 -0600 Subject: [PATCH 09/23] fix: filter out burnEvent from activity feeds Add burnEvent to IGNORED_EVENTS to prevent duplicate entries when users cash out. Only manualBurnEvent (true manual burns) will now appear in activity feeds. --- .../ProtocolActivity/ProtocolActivityList.tsx | 10 +++++++++- .../V4V5ActivityPanel/V4V5ActivityList.tsx | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/ProtocolActivity/ProtocolActivityList.tsx b/src/components/ProtocolActivity/ProtocolActivityList.tsx index dd84385f00..c69268cd4d 100644 --- a/src/components/ProtocolActivity/ProtocolActivityList.tsx +++ b/src/components/ProtocolActivity/ProtocolActivityList.tsx @@ -14,6 +14,14 @@ import { translateEventDataToProtocolPresenter } from './utils/translateEventDat const PAGE_SIZE = 20 const POLL_INTERVAL = 30000 // 30 seconds +const IGNORED_EVENTS = ['mintNftEvent', 'burnEvent'] +const baseEventFilter = IGNORED_EVENTS.reduce( + (acc, curr) => ({ + ...acc, + [curr]: null, + }), + {}, +) export function ProtocolActivityList() { const [endCursor, setEndCursor] = useState(null) @@ -23,7 +31,7 @@ export function ProtocolActivityList() { client: mainnetBendystrawClient, pollInterval: POLL_INTERVAL, // Poll every 30 seconds variables: { - where: {}, + where: baseEventFilter, orderBy: 'timestamp', orderDirection: 'desc', after: endCursor, diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx index a678126a49..18b37b33c9 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx @@ -25,7 +25,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) => ({ From 81ff49929b4edbceb00ae4c331b96cf34fb3a87b Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 17:11:26 -0600 Subject: [PATCH 10/23] feat: link $NANA fee to Bananapus project in activity feed Display fee as "$NANA fee" with link to Bananapus project (ID 1) in Send Payouts activity. Links to correct version (v4/v5) and chain based on the event's project context. --- .../graphql/queries/activityEvents.graphql | 1 + .../V4V5ActivityPanel/V4V5ActivityList.tsx | 28 +++++++++++++++---- .../utils/transformEventsData.ts | 28 +++++++++++-------- .../components/ProjectActivityList.tsx | 4 +-- 4 files changed, 42 insertions(+), 19 deletions(-) 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/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList.tsx index 18b37b33c9..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' @@ -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/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 ( From 823c404ce3f3c3fcc368b23b28a783f3439de497 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 17:13:59 -0600 Subject: [PATCH 11/23] fix: rename token badge from 'Juicebox native' to 'Token credits' --- .../V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx index b4907656f0..467735fef2 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx @@ -246,7 +246,7 @@ const ProjectTokenBadge = () => { const { projectHasErc20Token } = useV4V5TokensPanel() return ( - {projectHasErc20Token ? 'ERC-20' : t`Juicebox native`} + {projectHasErc20Token ? 'ERC-20' : t`Token credits`} ) } From e1fac6d4198a2e5b9ada4d5d80d5a58cca9b79c9 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 17:25:37 -0600 Subject: [PATCH 12/23] feat: show 'None' in tokens section when no tokens exist Display 'None' when project has zero total supply and zero ruleset weight, indicating no tokens exist and none will be issued. --- .../V4V5TokensPanel/V4V5TokensPanel.tsx | 13 ++++++++++++- .../V4V5TokensPanel/hooks/useV4V5TokensPanel.tsx | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5TokensPanel/V4V5TokensPanel.tsx index 467735fef2..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 ( <>
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, } } From 57718bf78a7c819889f5874986f3238126c6a627 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sat, 6 Dec 2025 17:38:12 -0600 Subject: [PATCH 13/23] fix: disable animations on analytics charts --- src/components/VolumeChart/components/TimelineChart.tsx | 2 +- .../TokenDistributionChart/TokenPieChart.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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} /> )} {pieChartData.map((entry, index) => { let fill: string From 9c985290603fb500304ddff161ec62a97f26efbb Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 08:05:41 -0600 Subject: [PATCH 14/23] fix: improve omnichain gas estimation with dynamic per-chain estimates - Add estimateOmnichainGas utility with 20% buffer and silent fallback - Replace hardcoded gas * chainCount with dynamic estimation per chain - Consolidate fallback gas values in OMNICHAIN_GAS_FALLBACKS constant - Add omnichain support to ArchiveProjectSettingsPage --- .../hooks/useDeployOmnichainProject.ts | 210 ++++++---- .../v4v5/hooks/useDeployOmnichainErc20.ts | 60 +-- .../hooks/useOmnichainEditProjectDetailsTx.ts | 45 +- .../useTransferOmnichainProjectOwnership.ts | 59 ++- .../v4v5/utils/estimateOmnichainGas.ts | 85 ++++ .../ArchiveProjectSettingsPage.tsx | 395 +++++++++++++----- .../hooks/useOmnichainEditCycle.ts | 65 +-- 7 files changed, 635 insertions(+), 284 deletions(-) create mode 100644 src/packages/v4v5/utils/estimateOmnichainGas.ts 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/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/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/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/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/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) } From feb4da3ad9f05eea0a190ef2635000ab4bd3a3a0 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 08:51:49 -0600 Subject: [PATCH 15/23] fix: handle V4/V5 bigint price comparison in NFT tier sorting V5 was not explicitly handled in buildJB721TierParams, causing it to fall through to V3.2 params. The sort then failed when accessing undefined contributionFloor property. Added V5 to use V4 params and implemented runtime type detection for bigint vs BigNumber comparison. --- src/utils/nftRewards.ts | 60 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 41 deletions(-) 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 }) } From 5b0b65c96878d41aba0d42576457809446b663c4 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 09:21:01 -0600 Subject: [PATCH 16/23] fix: display balance for project's chain in pay card Use selectedChainBalance instead of wallet.balance to show the user's balance on the project's deployment chain rather than the wallet's default chain. --- .../ProjectDashboard/V4V5PayRedeemCard/PayConfiguration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', From 1267ce917a215d9a0d309b96e099bd7e0f06af87 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 09:39:59 -0600 Subject: [PATCH 17/23] fix: pass chainId to cash out quote calculation hooks Fixes incorrect ETH received calculation on non-context chains (e.g., Optimism) by ensuring totalSupply and nativeTokenSurplus are fetched from the user's selected chain rather than the context chain. --- src/packages/v4v5/hooks/useETHReceivedFromTokens.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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, From e547daa3f7c69527f80e172b9cd9c63134730f69 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 09:55:50 -0600 Subject: [PATCH 18/23] fix: update cash out modal to show pendulum loading animation - Replace "Redeeming tokens" text with "Cashing out tokens" - Use orange pendulum loading image instead of generic spinner --- .../V4V5PayRedeemCard/RedeemConfiguration.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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. From e72c3bc414287d396758295efbe6cb91c17a517a Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 10:05:18 -0600 Subject: [PATCH 19/23] fix: change unlocked rulesets badge text to beginner friendly --- .../Create/components/CreateBadge/RecommendedBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ) From 4d2b7f09dbb61f30b4e5b2e47e344f3d212ee784 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 10:24:45 -0600 Subject: [PATCH 20/23] fix: add new localization strings for project features and confirmations --- src/locales/messages.pot | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/locales/messages.pot b/src/locales/messages.pot index 0ad4340fbb..91d252f7e3 100644 --- a/src/locales/messages.pot +++ b/src/locales/messages.pot @@ -1226,6 +1226,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 "" @@ -1682,6 +1685,9 @@ msgstr "" msgid "Reserved recipients:" msgstr "" +msgid "Beginner Friendly" +msgstr "" + msgid "Transfer ownership" msgstr "" @@ -1748,6 +1754,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 +2099,9 @@ msgstr "" msgid "allocation" msgstr "" +msgid "Token credits" +msgstr "" + msgid "This project's rules may pose risks for contributors:" msgstr "" @@ -2123,6 +2135,9 @@ msgstr "" msgid "Edit NFTs" msgstr "" +msgid "Confirm archive" +msgstr "" + msgid "{tokenSymbol} ERC-20 address" msgstr "" @@ -2612,6 +2627,9 @@ msgstr "" msgid "reserved token recipient" msgstr "" +msgid "Cashing out tokens" +msgstr "" + msgid "Controller configuration" msgstr "" @@ -4445,6 +4463,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 "" From 8e3ffdbd0e7d7d960c1bb5821167c894ded7d7de Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 10:45:33 -0600 Subject: [PATCH 21/23] fix: resolve upcoming ruleset loading states for start time, payer issuance rate, and cash out tax rate - Change strict equality (=== null) to loose equality (== null) to handle both null and undefined values from useJBUpcomingRuleset - Rewrite startTimeDatum to use pairToDatum pattern with proper loading state checks --- ...useV4V5FormatConfigurationCycleSection.tsx | 24 +++++++++---------- .../useV4V5FormatConfigurationTokenSection.ts | 16 ++++++------- 2 files changed, 19 insertions(+), 21 deletions(-) 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 58c49fc09d..e2cd5c2a86 100644 --- a/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts +++ b/src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatConfigurationTokenSection.ts @@ -58,7 +58,7 @@ export const useV4V5FormatConfigurationTokenSection = ({ ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/${currencySymbol}` : undefined - if (upcomingRuleset === null || upcomingRulesetLoading) { + if (upcomingRuleset == null || upcomingRulesetLoading) { return pairToDatum(t`Total issuance rate`, current, null) } @@ -95,8 +95,8 @@ export const useV4V5FormatConfigurationTokenSection = ({ : undefined if ( - upcomingRuleset === null || - upcomingRulesetMetadata === null || + upcomingRuleset == null || + upcomingRulesetMetadata == null || upcomingRulesetLoading ) { return pairToDatum(t`Payer issuance rate`, current, null) @@ -129,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) } @@ -146,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 @@ -165,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) } @@ -182,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, @@ -213,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, From c9e0d2eb4f4f3418c7baeaa328aadac479ea8d55 Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 19:20:34 -0600 Subject: [PATCH 22/23] feat: add ruleset history navigation with cycle selector Replace tabs with unified cycle navigator for browsing current, upcoming, and historical rulesets. Add navigation arrows and dropdown for direct cycle selection with status badges and date ranges. --- .../v4v5/hooks/useJBAllRulesetsCrossChain.ts | 35 +- .../v4v5/hooks/useJBRulesetHistory.ts | 305 ++++++++++++++++++ src/packages/v4v5/utils/rulesetDiff.ts | 22 +- .../V4V5CycleConfigurationDisplayCard.tsx | 72 +++++ .../V4V5CycleConfigurationPanel.tsx | 15 + .../V4V5CycleSubPanel.tsx | 224 +++++++++++++ .../V4V5CyclesPayoutsPanel.tsx | 119 ++++--- .../components/CycleNavigator.tsx | 201 ++++++++++++ .../components/RulesetDiffSection.tsx | 247 ++++++++++++++ .../CyclesPanelSelectedCycleContext.tsx | 97 ++++++ .../hooks/useV4V5CycleConfigurationPanel.ts | 22 ++ .../hooks/useV4V5FormatCycleSection.ts | 50 +++ .../hooks/useV4V5FormatExtensionSection.ts | 42 +++ .../hooks/useV4V5FormatOtherRulesSection.ts | 53 +++ .../hooks/useV4V5FormatTokenSection.ts | 114 +++++++ 15 files changed, 1551 insertions(+), 67 deletions(-) create mode 100644 src/packages/v4v5/hooks/useJBRulesetHistory.ts create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationDisplayCard.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleConfigurationPanel.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/V4V5CycleSubPanel.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/CycleNavigator.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/components/RulesetDiffSection.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/contexts/CyclesPanelSelectedCycleContext.tsx create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5CycleConfigurationPanel.ts create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatCycleSection.ts create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatExtensionSection.ts create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatOtherRulesSection.ts create mode 100644 src/packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5CyclesPayoutsPanel/hooks/useV4V5FormatTokenSection.ts 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/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/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/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, + ]) +} From a751d0bf6c3385b2cd8973f0e7e3e5e69476004e Mon Sep 17 00:00:00 2001 From: Jordy McNab Date: Sun, 7 Dec 2025 19:24:20 -0600 Subject: [PATCH 23/23] Update messages.pot --- src/locales/messages.pot | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/locales/messages.pot b/src/locales/messages.pot index 91d252f7e3..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 "" @@ -1268,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 "" @@ -1334,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 "" @@ -1370,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 "" @@ -2249,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 "" @@ -2531,6 +2549,9 @@ msgstr "" msgid "Not a valid ETH address" msgstr "" +msgid "Previous cycle" +msgstr "" + msgid "{0} after JBX fee" msgstr "" @@ -2888,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 "" @@ -2909,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 "" @@ -3593,6 +3623,9 @@ msgstr "" msgid "Previous value" msgstr "" +msgid "changes" +msgstr "" + msgid "No chains available for multi-chain deployment" msgstr "" @@ -3713,6 +3746,9 @@ msgstr "" msgid "Reserve 1 NFT of every" msgstr "" +msgid "Select cycle" +msgstr "" + msgid "NFT Projects" msgstr "" @@ -3995,6 +4031,9 @@ msgstr "" msgid "Redeem {tokensLabel} for ETH" msgstr "" +msgid "Past ruleset cycle" +msgstr "" + msgid "Made a mistake?" msgstr "" @@ -4577,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 ""