Skip to content

Commit 33f4e10

Browse files
committed
Merge branch 'dev' of github.com-johnnyd-eth:jbx-protocol/juice-interface
2 parents 9e541ac + 2518da8 commit 33f4e10

6 files changed

Lines changed: 131 additions & 52 deletions

File tree

src/components/Footer/Footer.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { GithubFilled, TwitterCircleFilled } from '@ant-design/icons'
22
import { Trans, t } from '@lingui/macro'
3+
import { LinkColProps, LinkColumn } from './LinkColumn'
4+
35
import ExternalLink from 'components/ExternalLink'
4-
import Logo from 'components/Logo'
56
import Discord from 'components/icons/Discord'
7+
import Logo from 'components/Logo'
68
import { TERMS_OF_SERVICE_URL } from 'constants/links'
7-
import { useWallet } from 'hooks/Wallet'
89
import { useFetchDeveloperWallets } from 'hooks/useFetchDeveloperWallets'
10+
import { useWallet } from 'hooks/Wallet'
911
import { isEqualAddress } from 'utils/address'
10-
import { LinkColProps, LinkColumn } from './LinkColumn'
1112

1213
const ImageButtons = [
1314
{
@@ -160,7 +161,7 @@ export function Footer() {
160161
</div>
161162

162163
<div className="mt-32 flex justify-between border-t border-slate-400 pb-16 pt-5">
163-
<span className="text-slate-200">© Juicebox 2024</span>
164+
<span className="text-slate-200">© Juicebox 2025</span>
164165

165166
<div className="flex gap-x-7">
166167
{gitCommit && <AppVersion gitCommit={gitCommit} />}

src/locales/messages.pot

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,9 @@ msgstr ""
518518
msgid "Claiming {tokenSymbol} tokens will convert your {tokenSymbol} balance to ERC-20 tokens and mint them to your wallet."
519519
msgstr ""
520520

521+
msgid "Queue ruleset"
522+
msgstr ""
523+
521524
msgid "Manage project on {0}"
522525
msgstr ""
523526

@@ -4175,6 +4178,9 @@ msgstr ""
41754178
msgid "<0>This project has no ETH available for redemptions</0>. You won't receive any ETH for burning your tokens."
41764179
msgstr ""
41774180

4181+
msgid "You'll be prompted a wallet signature to queue the updated ruleset."
4182+
msgstr ""
4183+
41784184
msgid "Change network"
41794185
msgstr ""
41804186

src/packages/v4/hooks/useV4WalletHasPermission.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
useReadJbPermissionsHasPermissions,
44
} from 'juice-sdk-react'
55

6+
import { useGnosisSafe } from 'hooks/safe/useGnosisSafe'
67
import { useWallet } from 'hooks/Wallet'
78
import { isEqualAddress } from 'utils/address'
9+
import { isSafeSigner } from 'utils/safe'
810
import { zeroAddress } from 'viem'
911
import { V4OperatorPermission } from '../models/v4Permissions'
1012
import useV4ProjectOwnerOf from './useV4ProjectOwnerOf'
@@ -17,6 +19,9 @@ export function useV4WalletHasPermission(
1719
const { projectId } = useJBContractContext()
1820
const { data: projectOwnerAddress } = useV4ProjectOwnerOf()
1921

22+
// If project owner is a Safe, fetch Safe details to determine signer membership
23+
const { data: safeData } = useGnosisSafe(projectOwnerAddress)
24+
2025
const _operator = userAddress ?? zeroAddress
2126
const _account = projectOwnerAddress ?? zeroAddress
2227
const hasOperatorPermission = useReadJbPermissionsHasPermissions({
@@ -30,8 +35,17 @@ export function useV4WalletHasPermission(
3035
],
3136
})
3237

33-
const isOwner = isEqualAddress(userAddress, projectOwnerAddress)
38+
// Treat a Safe signer the same as the project owner if the project owner is a Safe
39+
const isSafeOwnerSigner =
40+
!!safeData && isSafeSigner({ address: userAddress, safe: safeData })
41+
const isOwner =
42+
isEqualAddress(userAddress, projectOwnerAddress) || isSafeOwnerSigner
3443

44+
// Allow access if:
45+
// - wallet is direct owner
46+
// - wallet has explicit operator permission
47+
// - wallet is a signer on the Safe that owns the project (covers multisig ownership case)
48+
// - in development environment
3549
return (
3650
isOwner ||
3751
hasOperatorPermission.data ||

src/packages/v4/views/V4ProjectDashboard/V4ProjectHeader.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@ import { Button, Divider } from 'antd'
22
import { JBChainId, useJBChainId, useSuckers } from 'juice-sdk-react'
33
import { settingsPagePath, v4ProjectRoute } from 'packages/v4/utils/routes'
44

5-
import { Badge } from 'components/Badge'
6-
import { ChainLogo } from 'packages/v4/components/ChainLogo'
75
import { Cog6ToothIcon } from '@heroicons/react/24/outline'
6+
import { Trans } from '@lingui/macro'
7+
import { Badge } from 'components/Badge'
88
import EthereumAddress from 'components/EthereumAddress'
99
import { GnosisSafeBadge } from 'components/Project/ProjectHeader/GnosisSafeBadge'
10-
import Link from 'next/link'
1110
import { ProjectHeaderLogo } from 'components/Project/ProjectHeader/ProjectHeaderLogo'
12-
import { ProjectHeaderPopupMenu } from 'packages/v4/components/ProjectDashboard/components/ProjectHeaderPopupMenu'
13-
import { ProjectHeaderStats } from './ProjectHeaderStats'
1411
import { SocialLinkButton } from 'components/Project/ProjectHeader/SocialLinkButton'
15-
import { SuckerPair } from 'juice-sdk-core'
16-
import { Trans } from '@lingui/macro'
1712
import { TruncatedText } from 'components/TruncatedText'
18-
import { V4OperatorPermission } from 'packages/v4/models/v4Permissions'
13+
import useMobile from 'hooks/useMobile'
14+
import { SuckerPair } from 'juice-sdk-core'
15+
import Link from 'next/link'
16+
import { ChainLogo } from 'packages/v4/components/ChainLogo'
17+
import { ProjectHeaderPopupMenu } from 'packages/v4/components/ProjectDashboard/components/ProjectHeaderPopupMenu'
1918
import V4ProjectHandleLink from 'packages/v4/components/V4ProjectHandleLink'
19+
import { V4OperatorPermission } from 'packages/v4/models/v4Permissions'
2020
import { twMerge } from 'tailwind-merge'
21-
import useMobile from 'hooks/useMobile'
21+
import { ProjectHeaderStats } from './ProjectHeaderStats'
2222
// import { Subtitle } from 'components/Project/ProjectHeader/Subtitle'
2323
import { useSocialLinks } from 'components/Project/ProjectHeader/hooks/useSocialLinks'
24-
import { useV4ProjectHeader } from './hooks/useV4ProjectHeader'
2524
import { useV4WalletHasPermission } from 'packages/v4/hooks/useV4WalletHasPermission'
25+
import { useV4ProjectHeader } from './hooks/useV4ProjectHeader'
2626

2727
export type SocialLink = 'twitter' | 'discord' | 'telegram' | 'website'
2828

@@ -46,6 +46,8 @@ export const V4ProjectHeader = ({ className }: { className?: string }) => {
4646
V4OperatorPermission.QUEUE_RULESETS,
4747
)
4848

49+
const canManageProject = canQueueRuleSets
50+
4951
// convert createdAtSeconds to date string Month DD, YYYY in local time
5052
const createdAt = createdAtSeconds
5153
? new Date(createdAtSeconds * 1000).toLocaleDateString(undefined, {
@@ -78,7 +80,7 @@ export const V4ProjectHeader = ({ className }: { className?: string }) => {
7880
))}
7981
</div>
8082
<ProjectHeaderPopupMenu projectId={projectId} />
81-
{canQueueRuleSets && chainId && (
83+
{canManageProject && chainId && (
8284
<Link
8385
href={settingsPagePath({ projectId, chainId }, undefined)}
8486
legacyBehavior

src/packages/v4/views/V4ProjectSettings/EditCyclePage/ReviewConfirmModal/ReviewConfirmModal.tsx

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
1-
import { EditCycleTxArgs, transformEditCycleFormFieldsToTxArgs } from 'packages/v4/utils/editRuleset'
2-
import { JBChainId, NATIVE_TOKEN } from 'juice-sdk-core'
31
import { Trans, t } from '@lingui/macro'
4-
import { useEffect, useState } from 'react'
2+
import { JBChainId, NATIVE_TOKEN } from 'juice-sdk-core'
53
import { useJBChainId, useJBContractContext, useJBProjectId, useJBRuleset, useSuckers } from 'juice-sdk-react'
4+
import { EditCycleTxArgs, transformEditCycleFormFieldsToTxArgs } from 'packages/v4/utils/editRuleset'
5+
import { useEffect, useState } from 'react'
66

77
import { BigNumber } from '@ethersproject/bignumber'
8+
import { Form } from 'antd'
89
import { Callout } from 'components/Callout/Callout'
9-
import { ChainSelect } from 'packages/v4/components/ChainSelect'
10-
import { CreateCollapse } from 'packages/v4/components/Create/components/CreateCollapse/CreateCollapse'
11-
import { DetailsSectionDiff } from './DetailsSectionDiff'
1210
import ETHAmount from 'components/currency/ETHAmount'
13-
import { Form } from 'antd'
1411
import { JuiceDatePicker } from 'components/inputs/JuiceDatePicker'
1512
import { JuiceTextArea } from 'components/inputs/JuiceTextArea'
16-
import { PayoutsSectionDiff } from './PayoutsSectionDiff'
17-
import QueueSafeEditRulesetTxsModal from 'packages/v4/components/QueueSafeEditRulesetTxsModal'
18-
import type { RelayrPostBundleResponse } from 'juice-sdk-react'
19-
import { SectionCollapseHeader } from './SectionCollapseHeader'
20-
import { TokensSectionDiff } from './TokensSectionDiff'
2113
import TransactionModal from 'components/modals/TransactionModal'
22-
import { TransactionSuccessModal } from '../TransactionSuccessModal'
23-
import { emitErrorNotification } from 'utils/notifications'
14+
import { useGnosisSafe } from 'hooks/safe/useGnosisSafe'
15+
import { useWallet } from 'hooks/Wallet'
16+
import type { RelayrPostBundleResponse } from 'juice-sdk-react'
2417
import moment from 'moment'
18+
import { ChainSelect } from 'packages/v4/components/ChainSelect'
19+
import { CreateCollapse } from 'packages/v4/components/Create/components/CreateCollapse/CreateCollapse'
20+
import QueueSafeEditRulesetTxsModal from 'packages/v4/components/QueueSafeEditRulesetTxsModal'
21+
import { useEditRulesetTx } from 'packages/v4/hooks/useEditRulesetTx'
22+
import useV4ProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf'
23+
import { emitErrorNotification } from 'utils/notifications'
2524
import { useChainId } from 'wagmi'
26-
import { useDetailsSectionValues } from './hooks/useDetailsSectionValues'
2725
import { useEditCycleFormContext } from '../EditCycleFormContext'
28-
import { useGnosisSafe } from 'hooks/safe/useGnosisSafe'
2926
import { useOmnichainEditCycle } from '../hooks/useOmnichainEditCycle'
27+
import { TransactionSuccessModal } from '../TransactionSuccessModal'
28+
import { DetailsSectionDiff } from './DetailsSectionDiff'
29+
import { useDetailsSectionValues } from './hooks/useDetailsSectionValues'
3030
import { usePayoutsSectionValues } from './hooks/usePayoutsSectionValues'
3131
import { useTokensSectionValues } from './hooks/useTokensSectionValues'
32-
import useV4ProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf'
33-
import { useWallet } from 'hooks/Wallet'
32+
import { PayoutsSectionDiff } from './PayoutsSectionDiff'
33+
import { SectionCollapseHeader } from './SectionCollapseHeader'
34+
import { TokensSectionDiff } from './TokensSectionDiff'
3435

3536
export function ReviewConfirmModal({
3637
open,
@@ -65,9 +66,12 @@ export function ReviewConfirmModal({
6566

6667
// Omnichain edit state
6768
const { getEditQuote, sendRelayrTx, relayrBundle } = useOmnichainEditCycle()
69+
// Direct single-chain tx hook
70+
const editRulesetTx = useEditRulesetTx()
6871
const [selectedGasChain, setSelectedGasChain] = useState<JBChainId | undefined>(chainId)
6972
const [txQuote, setTxQuote] = useState<RelayrPostBundleResponse>()
7073
const [txQuoteLoading, setTxQuoteLoading] = useState(false)
74+
const [txPending, setTxPending] = useState(false) // single-chain pending state
7175

7276
const { data: suckers } = useSuckers()
7377

@@ -162,6 +166,45 @@ export function ReviewConfirmModal({
162166
}
163167
}
164168

169+
// Direct single-chain confirm (no relayr)
170+
const handleConfirmSingle = async () => {
171+
if (isOmnichainProject) return // safety guard
172+
if (walletConnectedToWrongChain) {
173+
try {
174+
await changeNetworks(chainId as JBChainId)
175+
} catch (error) {
176+
emitErrorNotification(`Error changing networks: ${error}`)
177+
return
178+
}
179+
}
180+
const formVals = editCycleForm!.getFieldsValue(true)
181+
setConfirmLoading(true)
182+
setTxPending(false)
183+
try {
184+
await editRulesetTx(formVals, {
185+
onTransactionPending: () => {
186+
setTxPending(true)
187+
},
188+
onTransactionConfirmed: () => {
189+
setTxPending(false)
190+
setConfirmLoading(false)
191+
editCycleForm!.resetFields()
192+
setEditCycleSuccessModalOpen(true)
193+
onClose()
194+
},
195+
onTransactionError: (e) => {
196+
setTxPending(false)
197+
setConfirmLoading(false)
198+
emitErrorNotification(e.message)
199+
},
200+
})
201+
} catch (e) {
202+
setTxPending(false)
203+
setConfirmLoading(false)
204+
emitErrorNotification((e as Error).message)
205+
}
206+
}
207+
165208
// Poll and handle completion
166209
useEffect(() => {
167210
if (relayrBundle.isComplete) {
@@ -181,7 +224,9 @@ export function ReviewConfirmModal({
181224
const panelProps = { className: 'text-lg' }
182225

183226
const txSigning = Boolean(relayrBundle.uuid) && !relayrBundle.isComplete
184-
const okText = isProjectOwnerGnosisSafe ? <Trans>Queue on Safe</Trans> : !txQuote ? <Trans>Get edit quote</Trans> : <Trans>Deploy changes</Trans>
227+
const okText = isOmnichainProject
228+
? (isProjectOwnerGnosisSafe ? <Trans>Queue on Safe</Trans> : !txQuote ? <Trans>Get edit quote</Trans> : <Trans>Deploy changes</Trans>)
229+
: <Trans>Queue ruleset</Trans>
185230
const mustStartAtOrAfterField = (
186231
<div className="mt-1">
187232
<Form.Item
@@ -221,13 +266,13 @@ export function ReviewConfirmModal({
221266
<TransactionModal
222267
open={open}
223268
title={<Trans>Review & confirm</Trans>}
224-
onOk={handleConfirmOmni}
269+
onOk={isOmnichainProject ? handleConfirmOmni : handleConfirmSingle}
225270
okText={okText}
226271
okButtonProps={{ disabled: !formHasChanges }}
227272
confirmLoading={confirmLoading || txQuoteLoading || txSigning}
228-
transactionPending={txSigning}
229-
chainIds={projectChains}
230-
relayrResponse={relayrBundle.response}
273+
transactionPending={isOmnichainProject ? txSigning : txPending}
274+
chainIds={isOmnichainProject ? projectChains : undefined}
275+
relayrResponse={isOmnichainProject ? relayrBundle.response : undefined}
231276
cancelButtonProps={{ hidden: true }}
232277
onCancel={onClose}
233278
>
@@ -293,7 +338,16 @@ export function ReviewConfirmModal({
293338
{mustStartAtOrAfterField}
294339
</div>
295340
) : null}
296-
{!txQuote && (
341+
{!isOmnichainProject && (
342+
<div className="mt-10 py-4 text-sm stroke-tertiary border-t rounded-none">
343+
<Callout.Info>
344+
<Trans>
345+
You'll be prompted a wallet signature to queue the updated ruleset.
346+
</Trans>
347+
</Callout.Info>
348+
</div>
349+
)}
350+
{isOmnichainProject && !txQuote && (
297351
<div className="mt-10 py-4 text-sm stroke-tertiary border-t rounded-none">
298352
<Callout.Info>
299353
{isProjectOwnerGnosisSafe && isOmnichainProject ? (
@@ -308,7 +362,7 @@ export function ReviewConfirmModal({
308362
</Callout.Info>
309363
</div>
310364
)}
311-
{txQuote ? (
365+
{isOmnichainProject && txQuote ? (
312366
<div className="mb-4 mt-10">
313367
<div className="flex items-center justify-between">
314368
<span><Trans>Gas quote</Trans>:</span>

src/packages/v4/views/V4ProjectSettings/EditCyclePage/ReviewConfirmModal/hooks/usePayoutsSectionValues.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CurrencyName } from 'constants/currency'
22
import { JBSplit } from 'juice-sdk-core'
33
import { distributionLimitsEqual } from 'packages/v4/utils/distributions'
4+
import { isInfinitePayoutLimit } from 'packages/v4/utils/fundingCycle'
45
import { MAX_PAYOUT_LIMIT } from 'packages/v4/utils/math'
56
import { splitsListsHaveDiff } from 'packages/v4/utils/v4Splits'
67
import { parseWad } from 'utils/format/formatNumber'
@@ -25,26 +26,27 @@ export const usePayoutsSectionValues = () => {
2526
'ETH'
2627
const currencyHasDiff = currentCurrency !== newCurrency
2728

28-
const newDistributionLimitForm: bigint =
29-
parseWad(editCycleForm?.getFieldValue('payoutLimit')).toBigInt()
30-
31-
const newDistributionLimit =
32-
newDistributionLimitForm === undefined
29+
// Raw form field value. For "No limit" the UI leaves the field blank / undefined.
30+
const rawNewDistributionLimit = editCycleForm?.getFieldValue('payoutLimit')
31+
const newDistributionLimit: bigint =
32+
rawNewDistributionLimit === undefined || rawNewDistributionLimit === null || rawNewDistributionLimit === ''
3333
? MAX_PAYOUT_LIMIT
34-
: newDistributionLimitForm
34+
: parseWad(rawNewDistributionLimit).toBigInt()
3535

3636
const currentDistributionLimitNum = initialFormData?.payoutLimit
3737
const currentDistributionLimit =
3838
currentDistributionLimitNum === undefined
3939
? MAX_PAYOUT_LIMIT
4040
: parseWad(currentDistributionLimitNum).toBigInt()
4141

42+
// A distribution limit diff should only show when either:
43+
// 1. The numeric (semantic) payout limit changed (taking into account that any infinite value forms are equal)
44+
// 2. The currency changed
45+
// NOTE: Previously we flagged a diff when both values were infinite ("No limit" -> "No limit").
4246
const distributionLimitHasDiff =
43-
!distributionLimitsEqual(currentDistributionLimit, newDistributionLimit) ||
44-
currencyHasDiff
45-
// TODO: When no limit is set and doesnt change, distributionLimitHasDiff still true
46-
const distributionLimitIsInfinite =
47-
!newDistributionLimit || newDistributionLimit === MAX_PAYOUT_LIMIT
47+
currencyHasDiff ||
48+
!distributionLimitsEqual(currentDistributionLimit, newDistributionLimit)
49+
const distributionLimitIsInfinite = isInfinitePayoutLimit(newDistributionLimit)
4850

4951
const newHoldFees = Boolean(editCycleForm?.getFieldValue('holdFees'))
5052
const currentHoldFees = Boolean(initialFormData?.holdFees)

0 commit comments

Comments
 (0)