diff --git a/package.json b/package.json index a3b93e6e65..c9b9f8708e 100644 --- a/package.json +++ b/package.json @@ -107,8 +107,8 @@ "graphql": "^16.8.1", "he": "^1.2.0", "jsonwebtoken": "^9.0.0", - "juice-sdk-core": "^11.0.0-alpha", - "juice-sdk-react": "^11.0.0-alpha", + "juice-sdk-core": "^11.5.0-alpha", + "juice-sdk-react": "^11.6.0-alpha", "juicebox-metadata-helper": "0.1.7", "less": "4.1.2", "lodash": "^4.17.21", diff --git a/src/packages/v4/components/Create/Create.tsx b/src/packages/v4/components/Create/Create.tsx index e4bd960031..12d0fe009e 100644 --- a/src/packages/v4/components/Create/Create.tsx +++ b/src/packages/v4/components/Create/Create.tsx @@ -10,7 +10,9 @@ import Loading from 'components/Loading' import { readNetwork } from 'constants/networks' import { NetworkName } from 'models/networkName' import { useRouter } from 'next/router' +import { CreateBadge } from './components/CreateBadge' import { FundingCyclesPage } from './components/pages/FundingCycles/FundingCyclesPage' +import { NftRewardsPage } from './components/pages/NftRewards/NftRewardsPage' import { PayoutsPage } from './components/pages/PayoutsPage/PayoutsPage' import { ProjectDetailsPage } from './components/pages/ProjectDetails/ProjectDetailsPage' import { ProjectTokenPage } from './components/pages/ProjectToken/ProjectTokenPage' @@ -86,7 +88,7 @@ export function Create() { > - {/* @@ -99,7 +101,7 @@ export function Create() { } > - */} + Edit Deadline} diff --git a/src/packages/v4/components/Create/components/Wizard/hooks/useSteps.ts b/src/packages/v4/components/Create/components/Wizard/hooks/useSteps.ts index 2c58a52f47..89862b1bdc 100644 --- a/src/packages/v4/components/Create/components/Wizard/hooks/useSteps.ts +++ b/src/packages/v4/components/Create/components/Wizard/hooks/useSteps.ts @@ -1,6 +1,7 @@ +import { useCallback, useContext, useMemo } from 'react' + import { t } from '@lingui/macro' import { CreatePage } from 'models/createPage' -import { useCallback, useContext, useMemo } from 'react' import { useAppSelector } from 'redux/hooks/useAppSelector' import { useEditingCreateFurthestPageReached } from 'redux/hooks/useEditingCreateFurthestPageReached' import { WizardContext } from '../contexts/WizardContext' @@ -11,7 +12,7 @@ const stepNames = (): Record => { fundingCycles: t`Rulesets`, payouts: t`Payouts`, projectToken: t`Token`, - // nftRewards: t`NFTs`, + nftRewards: t`NFTs`, reconfigurationRules: t`Deadline`, reviewDeploy: t`Deploy`, } diff --git a/src/packages/v4/components/Create/components/pages/ReviewDeploy/ReviewDeployPage.tsx b/src/packages/v4/components/Create/components/pages/ReviewDeploy/ReviewDeployPage.tsx index 17d5d5554e..7302f3591b 100644 --- a/src/packages/v4/components/Create/components/pages/ReviewDeploy/ReviewDeployPage.tsx +++ b/src/packages/v4/components/Create/components/pages/ReviewDeploy/ReviewDeployPage.tsx @@ -24,6 +24,7 @@ import { WizardContext } from '../../Wizard/contexts/WizardContext' import { FundingConfigurationReview } from './components/FundingConfigurationReview/FundingConfigurationReview' import { ProjectDetailsReview } from './components/ProjectDetailsReview/ProjectDetailsReview' import { ProjectTokenReview } from './components/ProjectTokenReview/ProjectTokenReview' +import { RewardsReview } from './components/RewardsReview/RewardsReview' import { RulesReview } from './components/RulesReview/RulesReview' enum ReviewDeployKey { @@ -164,7 +165,7 @@ export const ReviewDeployPage = () => { > - {/* { } > - */} + { + const rewardTier = sortedRewardTiers[index] + + return nftRewardTierToJB721TierConfig(rewardTier, cid) + }) +} + +function toV4Flags(v2v3Flags: JBTiered721Flags): JB721TiersHookFlags { + return { + noNewTiersWithOwnerMinting: v2v3Flags.lockManualMintingChanges, + noNewTiersWithReserves: v2v3Flags.lockReservedTokenChanges, + noNewTiersWithVotes: v2v3Flags.lockVotingUnitChanges, + preventOverspending: v2v3Flags.preventOverspending, + } +} /** * Hook that returns a function that deploys a project with NFT rewards. @@ -33,22 +100,12 @@ export const useDeployNftProject = () => { const fundingCycleData = useEditingV2V3FundingCycleDataSelector() const fundAccessConstraints = useEditingV2V3FundAccessConstraintsSelector() - const collectionName = useMemo( - () => - nftRewards.collectionMetadata.name - ? nftRewards.collectionMetadata.name - : projectMetadata.name, - [nftRewards.collectionMetadata.name, projectMetadata.name], - ) - const collectionSymbol = useMemo( - () => nftRewards.collectionMetadata.symbol ?? '', - [nftRewards.collectionMetadata.symbol], - ) - const nftFlags = useMemo( - () => nftRewards.flags ?? DEFAULT_NFT_FLAGS, - [nftRewards.flags], - ) - const governanceType = nftRewards.governanceType + const collectionName = nftRewards.collectionMetadata.name + ? nftRewards.collectionMetadata.name + : projectMetadata.name + const collectionSymbol = nftRewards.collectionMetadata.symbol ?? '' + const nftFlags = nftRewards.flags ?? DEFAULT_NFT_FLAGS + // const governanceType = nftRewards.governanceType const currency = nftRewards.pricing.currency /** @@ -63,15 +120,14 @@ export const useDeployNftProject = () => { rewardTierCids, nftCollectionMetadataUri, - onDone, - onConfirmed, - onCancelled, - onError, + onTransactionPending, + onTransactionConfirmed, + onTransactionError, }: { metadataCid: string rewardTierCids: string[] nftCollectionMetadataUri: string - } & TransactionCallbacks) => { + } & LaunchTxOpts) => { if (!collectionName) throw new Error('No collection name or project name') if (!(rewardTierCids.length && nftRewards.rewardTiers)) throw new Error('No NFTs') @@ -80,8 +136,8 @@ export const useDeployNftProject = () => { const tiers = buildJB721TierParams({ cids: rewardTierCids, rewardTiers: nftRewards.rewardTiers, - version: DEFAULT_JB_721_DELEGATE_VERSION, }) + const flags = toV4Flags(nftFlags) return await launchProjectWithNftsTx( { @@ -90,9 +146,8 @@ export const useDeployNftProject = () => { collectionName, collectionSymbol, currency, - governanceType, tiers, - flags: nftFlags, + flags, }, projectData: { owner: inputProjectOwner?.length ? inputProjectOwner : undefined, @@ -108,10 +163,9 @@ export const useDeployNftProject = () => { }, }, { - onDone, - onConfirmed, - onCancelled, - onError, + onTransactionPending, + onTransactionConfirmed, + onTransactionError, }, ) }, @@ -123,7 +177,6 @@ export const useDeployNftProject = () => { reservedTokensGroupedSplits, launchProjectWithNftsTx, collectionSymbol, - governanceType, inputProjectOwner, fundingCycleData, mustStartAtOrAfter, diff --git a/src/packages/v4/components/Create/hooks/DeployProject/useDeployProject.ts b/src/packages/v4/components/Create/hooks/DeployProject/useDeployProject.ts index a0de85a48a..843ea6aeb2 100644 --- a/src/packages/v4/components/Create/hooks/DeployProject/useDeployProject.ts +++ b/src/packages/v4/components/Create/hooks/DeployProject/useDeployProject.ts @@ -1,15 +1,19 @@ -import { uploadProjectMetadata } from 'lib/api/ipfs' -import { LaunchTxOpts } from 'packages/v4/hooks/useLaunchProjectTx' import { useCallback, useState } from 'react' -import { useAppDispatch } from 'redux/hooks/useAppDispatch' import { useAppSelector, useEditingV2V3FundAccessConstraintsSelector, useEditingV2V3FundingCycleDataSelector, useEditingV2V3FundingCycleMetadataSelector, } from 'redux/hooks/useAppSelector' + +import { uploadProjectMetadata } from 'lib/api/ipfs' +import { LaunchTxOpts } from 'packages/v4/hooks/useLaunchProjectTx' +import { useAppDispatch } from 'redux/hooks/useAppDispatch' import { editingV2ProjectActions } from 'redux/slices/editingV2Project' import { emitErrorNotification } from 'utils/notifications' +import { useDeployNftProject } from './hooks/NFT/useDeployNftProject' +import { useIsNftProject } from './hooks/NFT/useIsNftProject' +import { useUploadNftRewards } from './hooks/NFT/useUploadNftRewards' import { useDeployStandardProject } from './hooks/useDeployStandardProject' const JUICEBOX_DOMAIN = 'juicebox' @@ -22,10 +26,9 @@ export const useDeployProject = () => { const [isDeploying, setIsDeploying] = useState(false) const [transactionPending, setTransactionPending] = useState() - // const isNftProject = useIsNftProject() - // const uploadNftRewards = useUploadNftRewards() - // const deployNftProject = useDeployNftProject() - + const isNftProject = useIsNftProject() + const uploadNftRewards = useUploadNftRewards() + const deployNftProject = useDeployNftProject() const deployStandardProject = useDeployStandardProject() const { @@ -89,15 +92,15 @@ export const useDeployProject = () => { setIsDeploying(false) throw new Error('Error deploying project.') } - // let nftCids: Awaited> | undefined - // try { - // if (isNftProject) { - // nftCids = await uploadNftRewards() - // } - // } catch (error) { - // handleDeployFailure(error) - // return - // } + let nftCids: Awaited> | undefined + try { + if (isNftProject) { + nftCids = await uploadNftRewards() + } + } catch (error) { + handleDeployFailure(error) + return + } let softTargetAmount: string | undefined let softTargetCurrency: string | undefined @@ -120,42 +123,42 @@ export const useDeployProject = () => { } try { - // let tx - // if (isNftProject) { - // tx = await deployNftProject({ - // metadataCid: projectMetadataCid, - // rewardTierCids: nftCids!.rewardTiers, - // nftCollectionMetadataUri: nftCids!.nfCollectionMetadata, - // ...operationCallbacks(onProjectDeployed), - // }) - // } else { - const tx = await deployStandardProject({ - metadataCid: projectMetadataCid, - ...operationCallbacks(onProjectDeployed), - }) - // } - // if (!tx) { - setIsDeploying(false) - setTransactionPending(false) + let tx + if (isNftProject) { + tx = await deployNftProject({ + metadataCid: projectMetadataCid, + rewardTierCids: nftCids!.rewardTiers, + nftCollectionMetadataUri: nftCids!.nfCollectionMetadata, + ...operationCallbacks(onProjectDeployed), + }) + } else { + tx = await deployStandardProject({ + metadataCid: projectMetadataCid, + ...operationCallbacks(onProjectDeployed), + }) + } + if (!tx) { + setIsDeploying(false) + setTransactionPending(false) return - // } + } } catch (error) { handleDeployFailure(error) return } }, [ - // deployNftProject, + deployNftProject, deployStandardProject, fundAccessConstraints, fundingCycleData, fundingCycleMetadata, handleDeployFailure, - // isNftProject, + isNftProject, operationCallbacks, postPayModal, projectMetadata, - // uploadNftRewards, + uploadNftRewards, ], ) return { diff --git a/src/packages/v4/contexts/V4ProjectMetadataProvider.tsx b/src/packages/v4/contexts/V4ProjectMetadataProvider.tsx index 9841567941..d92dc5bbfd 100644 --- a/src/packages/v4/contexts/V4ProjectMetadataProvider.tsx +++ b/src/packages/v4/contexts/V4ProjectMetadataProvider.tsx @@ -1,6 +1,7 @@ +import { useJBProjectMetadataContext } from 'juice-sdk-react' + import { PV_V4 } from 'constants/pv' import { ProjectMetadataContext } from 'contexts/ProjectMetadataContext' -import { useJBProjectMetadataContext } from 'juice-sdk-react' import { PropsWithChildren } from 'react' export default function V4ProjectMetadataProvider({ diff --git a/src/packages/v4/hooks/useCurrentRouteChainId.ts b/src/packages/v4/hooks/useCurrentRouteChainId.ts index 58db1b737a..d5766f5bf1 100644 --- a/src/packages/v4/hooks/useCurrentRouteChainId.ts +++ b/src/packages/v4/hooks/useCurrentRouteChainId.ts @@ -5,7 +5,7 @@ export function useCurrentRouteChainId() { const router = useRouter() const { chainName } = router.query if (!chainName) { - return null + return undefined } return chainNameMap[chainName as string] diff --git a/src/packages/v4/hooks/useLaunchProjectTx.ts b/src/packages/v4/hooks/useLaunchProjectTx.ts index a337c808aa..cf1d758f64 100644 --- a/src/packages/v4/hooks/useLaunchProjectTx.ts +++ b/src/packages/v4/hooks/useLaunchProjectTx.ts @@ -1,3 +1,10 @@ +import { useCallback, useContext } from 'react' +import { Address, WaitForTransactionReceiptReturnType } from 'viem' +import { + LaunchV2V3ProjectArgs, + transformV2V3CreateArgsToV4, +} from '../utils/launchProjectTransformers' + import { waitForTransactionReceipt } from '@wagmi/core' import { JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN } from 'constants/metadataDomain' import { DEFAULT_MEMO } from 'constants/transactionDefaults' @@ -6,14 +13,8 @@ import { useWallet } from 'hooks/Wallet' import { NATIVE_TOKEN } from 'juice-sdk-core' import { useWriteJbControllerLaunchProjectFor } from 'juice-sdk-react' import { LaunchV2V3ProjectData } from 'packages/v2v3/hooks/transactor/useLaunchProjectTx' -import { useCallback, useContext } from 'react' import { DEFAULT_MUST_START_AT_OR_AFTER } from 'redux/slices/editingV2Project' -import { Address, WaitForTransactionReceiptReturnType } from 'viem' import { sepolia } from 'viem/chains' -import { - LaunchV2V3ProjectArgs, - transformV2V3CreateArgsToV4, -} from '../utils/launchProject' import { wagmiConfig } from '../wagmiConfig' import { useCurrentRouteChainId } from './useCurrentRouteChainId' @@ -31,7 +32,7 @@ export interface LaunchTxOpts { * Return the project ID created from a `launchProjectFor` transaction. * @param txReceipt receipt of `launchProjectFor` transaction */ -const getProjectIdFromLaunchReceipt = ( +export const getProjectIdFromLaunchReceipt = ( txReceipt: WaitForTransactionReceiptReturnType, ): number => { const projectIdHex: string | undefined = @@ -46,14 +47,14 @@ const getProjectIdFromLaunchReceipt = ( * The contract addresses to use for deployment * @todo not ideal to hardcode these addresses */ -const SUPPORTED_JB_MULTITERMINAL_ADDRESS = { +export const SUPPORTED_JB_MULTITERMINAL_ADDRESS = { '84532': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, '421614': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, '11155111': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, '11155420': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, } -const SUPPORTED_JB_CONTROLLER_ADDRESS = { +export const SUPPORTED_JB_CONTROLLER_ADDRESS = { '84532': '0x219A5cE6d1c512D5b050ad2E3d380b8746BE0Cb8' as Address, '421614': '0x219A5cE6d1c512D5b050ad2E3d380b8746BE0Cb8' as Address, '11155111': '0x219A5cE6d1c512D5b050ad2E3d380b8746BE0Cb8' as Address, diff --git a/src/packages/v4/hooks/useLaunchProjectWithNftsTx.ts b/src/packages/v4/hooks/useLaunchProjectWithNftsTx.ts new file mode 100644 index 0000000000..ae2f584691 --- /dev/null +++ b/src/packages/v4/hooks/useLaunchProjectWithNftsTx.ts @@ -0,0 +1,234 @@ +import { waitForTransactionReceipt } from '@wagmi/core' +import { JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN } from 'constants/metadataDomain' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { useWallet } from 'hooks/Wallet' +import { DEFAULT_MEMO, NATIVE_TOKEN, NATIVE_TOKEN_DECIMALS } from 'juice-sdk-core' +import { + jbPricesAddress, + useJBContractContext, + useReadJb721TiersHookStoreTiersOf, + useWriteJb721TiersHookProjectDeployerLaunchProjectFor, +} from 'juice-sdk-react' +import { isValidMustStartAtOrAfter } from 'packages/v2v3/utils/fundingCycle' +import { + JBDeploy721TiersHookConfig, + LaunchProjectWithNftsTxArgs, +} from 'packages/v4/models/nfts' +import { useContext } from 'react' +import { DEFAULT_MUST_START_AT_OR_AFTER } from 'redux/slices/editingV2Project' +import { ipfsUri } from 'utils/ipfs' +import { + Address, + toBytes, + toHex, + WaitForTransactionReceiptReturnType, + zeroAddress, +} from 'viem' +import { sepolia } from 'viem/chains' +import { + LaunchV2V3ProjectArgs, + transformV2V3CreateArgsToV4, +} from '../utils/launchProjectTransformers' +import { wagmiConfig } from '../wagmiConfig' +import { useCurrentRouteChainId } from './useCurrentRouteChainId' +import { + LaunchTxOpts, + SUPPORTED_JB_CONTROLLER_ADDRESS, + SUPPORTED_JB_MULTITERMINAL_ADDRESS +} from './useLaunchProjectTx' + +function createSalt() { + const base: string = '0x' + Math.random().toString(16).slice(2) // idk lol + const salt = toHex(toBytes(base, { size: 32 })) + + return salt +} + +/** + * Return the project ID created from a `launchProjectFor` transaction. + * @param txReceipt receipt of `launchProjectFor` transaction + */ +export const getProjectIdFromNftLaunchReceipt = ( + txReceipt: WaitForTransactionReceiptReturnType, +): number => { + const projectIdHex: string | undefined = + txReceipt?.logs[0]?.topics?.[1] + if (!projectIdHex) return 0 + + const projectId = parseInt(projectIdHex, 16) + return projectId +} + +/** + * The contract addresses to use for deployment + * @todo not ideal to hardcode these addresses + */ +export const SUPPORTED_JB_721_TIER_STORE = { + '84532': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, + '421614': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, + '11155111': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, + '11155420': '0x4DeF0AA5B9CA095d11705284221b2878731ab4EF' as Address, +} + +/** + * + * TODO still wip + */ +export function useLaunchProjectWithNftsTx() { + const { contracts } = useJBContractContext() + const { addTransaction } = useContext(TxHistoryContext) + + const { userAddress } = useWallet() + const chainId = useCurrentRouteChainId() ?? sepolia.id // default to sepolia + const defaultJBController = chainId + ? SUPPORTED_JB_CONTROLLER_ADDRESS[chainId] + : undefined + const defaultJBETHPaymentTerminal = chainId + ? SUPPORTED_JB_MULTITERMINAL_ADDRESS[chainId] + : undefined + const JBTiered721DelegateStoreAddress = chainId + ? SUPPORTED_JB_721_TIER_STORE[chainId] + : undefined + + const { writeContractAsync: writeLaunchProject } = + useWriteJb721TiersHookProjectDeployerLaunchProjectFor() + + return async ( + { + tiered721DelegateData: { + collectionUri, + collectionName, + collectionSymbol, + currency, + tiers, + flags, + }, + projectData: { + projectMetadataCID, + fundingCycleData, + fundingCycleMetadata, + fundAccessConstraints, + groupedSplits = [], + mustStartAtOrAfter = DEFAULT_MUST_START_AT_OR_AFTER, + owner, + }, + }: LaunchProjectWithNftsTxArgs, + { + onTransactionPending: onTransactionPendingCallback, + onTransactionConfirmed: onTransactionConfirmedCallback, + onTransactionError: onTransactionErrorCallback, + }: LaunchTxOpts, + ) => { + if ( + !userAddress || + !contracts || + !defaultJBController || + !defaultJBETHPaymentTerminal || + !JBTiered721DelegateStoreAddress || + !isValidMustStartAtOrAfter(mustStartAtOrAfter, fundingCycleData.duration) + ) { + const missingParam = !userAddress + ? 'userAddress' + : !contracts + ? 'contracts' + : !defaultJBController + ? 'defaultJBController' + : !JBTiered721DelegateStoreAddress + ? 'JBTiered721DelegateProjectDeployer' + : null + + onTransactionErrorCallback?.( + new DOMException( + `Transaction failed, missing argument "${ + missingParam ?? '' + }".`, + ), + ) + + return Promise.resolve(false) + } + const _owner = (owner?.length ? owner : userAddress) as Address + + const deployTiered721HookData: JBDeploy721TiersHookConfig = { + name: collectionName, + symbol: collectionSymbol, + baseUri: ipfsUri(''), + tokenUriResolver: zeroAddress, + contractUri: ipfsUri(collectionUri), + tiersConfig: { + currency, + decimals: NATIVE_TOKEN_DECIMALS, + prices: jbPricesAddress[chainId], + tiers, + }, + reserveBeneficiary: zeroAddress, + flags, + } + + const v2v3LaunchProjectArgs = [ + _owner, + [projectMetadataCID, JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN], + fundingCycleData, + fundingCycleMetadata, + mustStartAtOrAfter, + groupedSplits, + fundAccessConstraints, + [defaultJBETHPaymentTerminal], // _terminals, just supporting single for now + // Eventually should be something like: + // getTerminalsFromFundAccessConstraints( + // fundAccessConstraints, + // contracts.primaryNativeTerminal.data, + // ), + DEFAULT_MEMO, + ] as LaunchV2V3ProjectArgs + const launchProjectData = transformV2V3CreateArgsToV4({ + v2v3Args: v2v3LaunchProjectArgs, + primaryNativeTerminal: defaultJBETHPaymentTerminal, + currencyTokenAddress: NATIVE_TOKEN, + }) + + const args = [ + _owner, + deployTiered721HookData, //_deployTiered721HookData + { + projectUri: launchProjectData[1], + rulesetConfigurations: launchProjectData[2], + terminalConfigurations: launchProjectData[3], + memo: launchProjectData[4], + }, // _launchProjectData, + defaultJBController, + // createSalt(), + ] as const + + try { + // SIMULATE TX: TODO update for nfts + // const encodedData = encodeFunctionData({ + // abi: jbControllerAbi, // ABI of the contract + // functionName: 'launchProjectFor', + // args, + // }) + + const hash = await writeLaunchProject({ + chainId, + args, + }) + + type x = typeof useReadJb721TiersHookStoreTiersOf + + onTransactionPendingCallback(hash) + addTransaction?.('Launch Project', { hash }) + const transactionReceipt: WaitForTransactionReceiptReturnType = + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + + const newProjectId = getProjectIdFromNftLaunchReceipt(transactionReceipt) + + onTransactionConfirmedCallback(hash, newProjectId) + } catch (e) { + onTransactionErrorCallback( + (e as Error) ?? new Error('Transaction failed'), + ) + } + } +} diff --git a/src/packages/v4/models/fundAccessLimits.ts b/src/packages/v4/models/fundAccessLimits.ts new file mode 100644 index 0000000000..d5e1f0b919 --- /dev/null +++ b/src/packages/v4/models/fundAccessLimits.ts @@ -0,0 +1,12 @@ +export type FundAccessLimitGroup = { + terminal: `0x${string}`; + token: `0x${string}`; + payoutLimits: { + amount: bigint; + currency: number; + }[]; + surplusAllowances: { + amount: bigint; + currency: number; + }[]; +} diff --git a/src/packages/v4/models/nfts.ts b/src/packages/v4/models/nfts.ts new file mode 100644 index 0000000000..f0fcdd7ec1 --- /dev/null +++ b/src/packages/v4/models/nfts.ts @@ -0,0 +1,98 @@ +import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' +import { jb721TiersHookStoreAbi } from 'juice-sdk-react' +import { LaunchV2V3ProjectData } from 'packages/v2v3/hooks/transactor/useLaunchProjectTx' +import { Address, ContractFunctionReturnType } from 'viem' +import { LaunchV4ProjectGroupedSplit } from '../utils/launchProjectTransformers' +import { FundAccessLimitGroup } from './fundAccessLimits' +import { LaunchProjectJBTerminal } from './terminals' +import { V4CurrencyOption } from './v4CurrencyOption' + +/** + * @see https://github.com/Bananapus/nana-721-hook/blob/main/src/structs/JB721TierConfig.sol + */ +export type JB721TierConfig = Omit< + ContractFunctionReturnType< + typeof jb721TiersHookStoreAbi, + 'view', + 'tiersOf' + >[0], + 'id' | 'votingUnits' +> & { + useReserveBeneficiaryAsDefault: boolean + useVotingUnits: boolean + votingUnits: number +} + +type JB721InitTiersConfig = { + tiers: JB721TierConfig[] + currency: number + decimals: number + prices: Address // JBPrices address +} + +export type JB721TiersHookFlags = { + noNewTiersWithReserves: boolean + noNewTiersWithVotes: boolean + noNewTiersWithOwnerMinting: boolean + preventOverspending: boolean +} + +/** + * string name; + string symbol; + string baseUri; + IJB721TokenUriResolver tokenUriResolver; + string contractUri; + JB721InitTiersConfig tiersConfig; + address reserveBeneficiary; + JB721TiersHookFlags flags; + */ +export type JBDeploy721TiersHookConfig = { + name: string + symbol: string + baseUri: string + tokenUriResolver: Address //IJB721TokenUriResolver; + contractUri: string + tiersConfig: JB721InitTiersConfig + reserveBeneficiary: Address + flags: JB721TiersHookFlags +} + +export type JBPayDataHookRulesetConfig = JBRulesetData & { + metadata: JBPayDataHookRulesetMetadata + memo?: string + fundAccessLimitGroups: FundAccessLimitGroup[] + mustStartAtOrAfter?: string // epoch seconds. anything less than "now" will start immediately. + terminals: string[] + duration: bigint + weight: bigint + decayPercent: bigint + approvalHook: Address + splitGroups: LaunchV4ProjectGroupedSplit[] +} + +interface DeployTiered721DelegateData { + collectionUri: string + collectionName: string + collectionSymbol: string + currency: V4CurrencyOption + tiers: JB721TierConfig[] + flags: JB721TiersHookFlags +} + +export interface LaunchProjectWithNftsTxArgs { + tiered721DelegateData: DeployTiered721DelegateData + projectData: LaunchV2V3ProjectData +} + +export type JB721DelegateLaunchProjectData = { + rulesetConfigurations: JBPayDataHookRulesetConfig[] + terminalConfigurations: LaunchProjectJBTerminal[] + projectMetadataUri: string + memo?: string +} + +export type JBPayDataHookRulesetMetadata = Omit< + JBRulesetMetadata, + 'useDataSourceForPay' | 'dataSource' +> diff --git a/src/packages/v4/models/splits.ts b/src/packages/v4/models/splits.ts new file mode 100644 index 0000000000..7e5fb0e075 --- /dev/null +++ b/src/packages/v4/models/splits.ts @@ -0,0 +1,6 @@ +import { JBSplit } from "juice-sdk-core" + +export interface GroupedSplits { + groupId: G + splits: JBSplit[] +} diff --git a/src/packages/v4/models/terminals.ts b/src/packages/v4/models/terminals.ts new file mode 100644 index 0000000000..c5f474928f --- /dev/null +++ b/src/packages/v4/models/terminals.ts @@ -0,0 +1,10 @@ +import { Address } from 'viem'; + +export type LaunchProjectJBTerminal = { + terminal: Address; + accountingContextsToAccept: { + token: `0x${string}`; + decimals: 18; + currency: number; + }[]; +} diff --git a/src/packages/v4/utils/launchProject.ts b/src/packages/v4/utils/launchProject.ts deleted file mode 100644 index 21292b1834..0000000000 --- a/src/packages/v4/utils/launchProject.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { NATIVE_TOKEN, NATIVE_TOKEN_DECIMALS, SplitGroup } from 'juice-sdk-core' -import round from 'lodash/round' -import { V2FundingCycleMetadata } from 'packages/v2/models/fundingCycle' -import { - V2V3FundAccessConstraint, - V2V3FundingCycleData, -} from 'packages/v2v3/models/fundingCycle' -import { GroupedSplits } from 'packages/v2v3/models/splits' -import { V3FundingCycleMetadata } from 'packages/v3/models/fundingCycle' -import { Address } from 'viem' - -export type LaunchV2V3ProjectArgs = [ - string, // _owner - [string, number], // _projectMetadata [projectMetadataCID, JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN] - V2V3FundingCycleData, // _data - V2FundingCycleMetadata | V3FundingCycleMetadata, // _metadata - string, // _mustStartAtOrAfter - GroupedSplits[], // _groupedSplits - V2V3FundAccessConstraint[], // _fundAccessConstraints - string[], // _terminals - string, // _memo -] - -export function transformV2V3CreateArgsToV4({ - v2v3Args, - primaryNativeTerminal, - currencyTokenAddress, -}: { - v2v3Args: LaunchV2V3ProjectArgs - primaryNativeTerminal: Address - currencyTokenAddress: Address -}) { - const [ - _owner, - _projectMetadata, - _data, - _metadata, - _mustStartAtOrAfter, - _groupedSplits, - _fundAccessConstraints, - _terminals, - _memo, - ] = v2v3Args - - const mustStartAtOrAfterNum = parseInt(_mustStartAtOrAfter) - const now = round(new Date().getTime() / 1000) - - const ruleset = { - mustStartAtOrAfter: mustStartAtOrAfterNum > now ? mustStartAtOrAfterNum : now, - duration: _data.duration.toNumber(), - weight: _data.weight.toBigInt(), - decayPercent: _data.discountRate.toNumber(), - - approvalHook: _data.ballot as Address, - - metadata: { - reservedPercent: _metadata.reservedRate.toNumber(), - redemptionRate: _metadata.redemptionRate.toNumber(), - baseCurrency: 1, // Not present in v2v3, passing 1 by default - pausePay: _metadata.pausePay, - pauseRedeem: _metadata.pauseRedeem, - pauseCreditTransfers: Boolean(_metadata.global.pauseTransfers), - allowOwnerMinting: _metadata.allowMinting, - allowSetCustomToken: false, // Assuming false by default - allowTerminalMigration: _metadata.allowTerminalMigration, - allowSetTerminals: _metadata.global.allowSetTerminals, - allowSetController: _metadata.global.allowSetController, - allowAddAccountingContext: false, // Not present in v2v3, passing false by default - allowAddPriceFeed: false, // Not present in v2v3, passing false by default - ownerMustSendPayouts: false, // Not present in v2v3, passing false by default - holdFees: _metadata.holdFees, - useTotalSurplusForRedemptions: _metadata.useTotalOverflowForRedemptions, - useDataHookForPay: _metadata.useDataSourceForPay, - useDataHookForRedeem: _metadata.useDataSourceForRedeem, - dataHook: _metadata.dataSource as Address, - metadata: 0, - allowCrosschainSuckerExtension: false, - }, - - splitGroups: _groupedSplits.map(group => ({ - groupId: - group.group === SplitGroup.ETHPayout - ? BigInt(NATIVE_TOKEN) - : 1n, // TODO dont hardcode reserved token group as 1n - splits: group.splits.map(split => ({ - preferAddToBalance: Boolean(split.preferClaimed), - percent: split.percent, - projectId: BigInt(parseInt(split.projectId ?? '0x00', 16)), - beneficiary: split.beneficiary as Address, - lockedUntil: split.lockedUntil ?? 0, - hook: split.allocator as Address, - })), - })), - - fundAccessLimitGroups: _fundAccessConstraints.map(constraint => ({ - terminal: primaryNativeTerminal, - token: currencyTokenAddress, - payoutLimits: [ - { - amount: constraint.distributionLimit.toBigInt(), - currency: Number(BigInt(NATIVE_TOKEN)), // TODO support USD somehow - }, - ], - surplusAllowances: [ - { - amount: constraint.overflowAllowance.toBigInt(), - currency: Number(BigInt(NATIVE_TOKEN)), - }, - ], - })), - } - - const rulesetConfigurations = [ruleset] - - const terminalConfigurations = _terminals.map(terminal => ({ - terminal: terminal as Address, - accountingContextsToAccept: [ - { - token: currencyTokenAddress, - decimals: NATIVE_TOKEN_DECIMALS, - currency: Number(BigInt(currencyTokenAddress)), - }, - ], - })) - - return [ - _owner as Address, - _projectMetadata[0], - rulesetConfigurations, - terminalConfigurations, - _memo, - ] as const -} diff --git a/src/packages/v4/utils/launchProjectTransformers.ts b/src/packages/v4/utils/launchProjectTransformers.ts new file mode 100644 index 0000000000..d5cfb39edb --- /dev/null +++ b/src/packages/v4/utils/launchProjectTransformers.ts @@ -0,0 +1,195 @@ +import { + JBSplit, + NATIVE_TOKEN, + NATIVE_TOKEN_DECIMALS, + SplitGroup, +} from 'juice-sdk-core' +import round from 'lodash/round' +import { V2FundingCycleMetadata } from 'packages/v2/models/fundingCycle' +import { + V2V3FundAccessConstraint, + V2V3FundingCycleData, +} from 'packages/v2v3/models/fundingCycle' +import { GroupedSplits as V2V3GroupedSplits } from 'packages/v2v3/models/splits' +import { V3FundingCycleMetadata } from 'packages/v3/models/fundingCycle' +import { Address } from 'viem' +import { FundAccessLimitGroup } from '../models/fundAccessLimits' +import { GroupedSplits as V4GroupedSplits } from '../models/splits' +import { LaunchProjectJBTerminal } from '../models/terminals' + +export type LaunchV2V3ProjectArgs = [ + string, // _owner + [string, number], // _projectMetadata [projectMetadataCID, JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN] + V2V3FundingCycleData, // _fundingCycleData + V2FundingCycleMetadata | V3FundingCycleMetadata, // _fundingCycleMetadata + string, // _mustStartAtOrAfter + V2V3GroupedSplits[], // _groupedSplits + V2V3FundAccessConstraint[], // _fundAccessConstraints + string[], // _terminals + string, // _memo +] + +export function transformV2V3CreateArgsToV4({ + v2v3Args, + primaryNativeTerminal, + currencyTokenAddress, +}: { + v2v3Args: LaunchV2V3ProjectArgs + primaryNativeTerminal: Address + currencyTokenAddress: Address +}) { + const [ + _owner, + _projectMetadata, + _fundingCycleData, + _fundingCycleMetadata, + _mustStartAtOrAfter, + _groupedSplits, + _fundAccessConstraints, + _terminals, + _memo, + ] = v2v3Args + + const mustStartAtOrAfterNum = parseInt(_mustStartAtOrAfter) + const now = round(new Date().getTime() / 1000) + + const ruleset = { + mustStartAtOrAfter: + mustStartAtOrAfterNum > now ? mustStartAtOrAfterNum : now, + duration: _fundingCycleData.duration.toNumber(), + weight: _fundingCycleData.weight.toBigInt(), + decayPercent: _fundingCycleData.discountRate.toNumber(), + + approvalHook: _fundingCycleData.ballot as Address, + + metadata: transformFCMetadataToRulesetMetadata({ + fundingCycleMetadata: _fundingCycleMetadata, + }), + + splitGroups: transformV2V3SplitsToV4({ v2v3Splits: _groupedSplits }), + + fundAccessLimitGroups: transformV2V3FundAccessConstraintsToV4({ + v2V3FundAccessConstraints: _fundAccessConstraints, + primaryNativeTerminal, + currencyTokenAddress, + }), + } + + const rulesetConfigurations = [ruleset] + + const terminalConfigurations = generateV4LaunchTerminalConfigurationsArg({ + terminals: _terminals, + currencyTokenAddress, + }) + + return [ + _owner as Address, + _projectMetadata[0], + rulesetConfigurations, + terminalConfigurations, + _memo, + ] as const +} + +export function transformFCMetadataToRulesetMetadata({ + fundingCycleMetadata, +}: { + fundingCycleMetadata: V2FundingCycleMetadata | V3FundingCycleMetadata +}) { + return { + reservedPercent: fundingCycleMetadata.reservedRate.toNumber(), + redemptionRate: fundingCycleMetadata.redemptionRate.toNumber(), + baseCurrency: 1, // Not present in v2v3, passing 1 by default + pausePay: fundingCycleMetadata.pausePay, + pauseRedeem: fundingCycleMetadata.pauseRedeem, + pauseCreditTransfers: Boolean(fundingCycleMetadata.global.pauseTransfers), + allowOwnerMinting: fundingCycleMetadata.allowMinting, + allowSetCustomToken: false, // Assuming false by default + allowTerminalMigration: fundingCycleMetadata.allowTerminalMigration, + allowSetTerminals: fundingCycleMetadata.global.allowSetTerminals, + allowSetController: fundingCycleMetadata.global.allowSetController, + allowAddAccountingContext: false, // Not present in v2v3, passing false by default + allowAddPriceFeed: false, // Not present in v2v3, passing false by default + ownerMustSendPayouts: false, // Not present in v2v3, passing false by default + holdFees: fundingCycleMetadata.holdFees, + useTotalSurplusForRedemptions: + fundingCycleMetadata.useTotalOverflowForRedemptions, + useDataHookForPay: fundingCycleMetadata.useDataSourceForPay, + useDataHookForRedeem: fundingCycleMetadata.useDataSourceForRedeem, + dataHook: fundingCycleMetadata.dataSource as Address, + metadata: 0, + allowCrosschainSuckerExtension: false, + } +} + +type LaunchProjectJBSplit = Omit & { percent: number } + +export type LaunchV4ProjectGroupedSplit = Omit< + V4GroupedSplits, + 'splits' | 'groupId' +> & { splits: LaunchProjectJBSplit[], groupId: bigint } + +export function transformV2V3SplitsToV4({ + v2v3Splits, +}: { + v2v3Splits: V2V3GroupedSplits[] +}): LaunchV4ProjectGroupedSplit[] { + return v2v3Splits.map(group => ({ + groupId: + group.group === SplitGroup.ETHPayout ? BigInt(NATIVE_TOKEN) : 1n, // TODO dont hardcode reserved token group as 1n + splits: group.splits.map(split => ({ + preferAddToBalance: Boolean(split.preferClaimed), + percent: split.percent, + projectId: BigInt(parseInt(split.projectId ?? '0x00', 16)), + beneficiary: split.beneficiary as Address, + lockedUntil: split.lockedUntil ?? 0, + hook: split.allocator as Address, + })), + })) +} + +export function transformV2V3FundAccessConstraintsToV4({ + v2V3FundAccessConstraints, + primaryNativeTerminal, + currencyTokenAddress, +}: { + v2V3FundAccessConstraints: V2V3FundAccessConstraint[] + primaryNativeTerminal: Address + currencyTokenAddress: Address +}): FundAccessLimitGroup[] { + return v2V3FundAccessConstraints.map(constraint => ({ + terminal: primaryNativeTerminal, + token: currencyTokenAddress, + payoutLimits: [ + { + amount: constraint.distributionLimit.toBigInt(), + currency: Number(BigInt(NATIVE_TOKEN)), // TODO support USD somehow + }, + ], + surplusAllowances: [ + { + amount: constraint.overflowAllowance.toBigInt(), + currency: Number(BigInt(NATIVE_TOKEN)), + }, + ], + })) +} + +function generateV4LaunchTerminalConfigurationsArg({ + terminals, + currencyTokenAddress, +}: { + terminals: string[] + currencyTokenAddress: Address +}): LaunchProjectJBTerminal[] { + return terminals.map(terminal => ({ + terminal: terminal as Address, + accountingContextsToAccept: [ + { + token: currencyTokenAddress, + decimals: NATIVE_TOKEN_DECIMALS, + currency: Number(BigInt(currencyTokenAddress)), + }, + ], + })) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx index f88a7c5575..cc99f1436c 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx @@ -1,8 +1,9 @@ +import { Fragment, useEffect, useMemo, useRef, useState } from 'react' + import { Tab } from '@headlessui/react' import { t } from '@lingui/macro' import { ProjectTab } from 'components/Project/ProjectTabs/ProjectTab' import { useOnScreen } from 'hooks/useOnScreen' -import { Fragment, useEffect, useMemo, useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' import { useProjectPageQueries } from '../hooks/useProjectPageQueries' import V4AboutPanel from './V4AboutPanel' @@ -47,6 +48,12 @@ export const V4ProjectTabs = ({ className }: { className?: string }) => { () => [ { id: 'activity', name: t`Activity`, panel: }, { id: 'about', name: t`About`, panel: }, + // { + // id: 'nft_rewards', + // name: t`NFTs`, + // panel: , + // hideTab: !showNftRewards, + // }, { id: 'cycle_payouts', name: t`Cycles & Payouts`, diff --git a/src/redux/slices/editingV2Project/defaultState.ts b/src/redux/slices/editingV2Project/defaultState.ts index 3467dcf5ac..dacd6c6e16 100644 --- a/src/redux/slices/editingV2Project/defaultState.ts +++ b/src/redux/slices/editingV2Project/defaultState.ts @@ -22,6 +22,7 @@ import { serializeV2V3FundingCycleData, serializeV2V3FundingCycleMetadata, } from 'packages/v2v3/utils/serializers' +import { JB721TiersHookFlags } from 'packages/v4/models/nfts' import { projectDescriptionTemplate } from 'templates/create/projectDescriptionTemplate' import { CreateState, ProjectState } from './types' @@ -92,6 +93,14 @@ export const DEFAULT_NFT_FLAGS: JBTiered721Flags = { preventOverspending: false, } + +export const DEFAULT_NFT_FLAGS_V4: JB721TiersHookFlags = { + noNewTiersWithReserves: false, + noNewTiersWithVotes: false, + noNewTiersWithOwnerMinting: false, + preventOverspending: false, +} + const DEFAULT_PROJECT_METADATA_STATE: ProjectMetadata = { name: '', infoUri: '', diff --git a/src/utils/ipfs.ts b/src/utils/ipfs.ts index c8ea023760..f13a560715 100644 --- a/src/utils/ipfs.ts +++ b/src/utils/ipfs.ts @@ -62,8 +62,8 @@ export function pinataToGatewayUrl(url: string) { * * Hex-encoded CIDs are used to store some CIDs on-chain because they are more gas-efficient. */ -export function encodeIpfsUri(cid: string) { - return '0x' + Buffer.from(base58.decode(cid).slice(2)).toString('hex') +export function encodeIpfsUri(cid: string): `0x${string}` { + return `0x${Buffer.from(base58.decode(cid).slice(2)).toString('hex')}` } /** diff --git a/yarn.lock b/yarn.lock index 9aae589e59..44e92fbd19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12837,18 +12837,18 @@ jsx-ast-utils@^3.3.5: object.assign "^4.1.4" object.values "^1.1.6" -juice-sdk-core@^11.0.0-alpha: - version "11.0.0-alpha" - resolved "https://registry.yarnpkg.com/juice-sdk-core/-/juice-sdk-core-11.0.0-alpha.tgz#dd0228158de3c3a2799ea6ba3b1f22c8c0635d60" - integrity sha512-wqKAb9f88579CiTZP6MNb08TOStQ4OqgLQYh7cwmONcpon09+A9tyHPCxhgKjGHGevzF80Wv2Z0dPjgujMRI2Q== +juice-sdk-core@^11.5.0-alpha: + version "11.5.0-alpha" + resolved "https://registry.yarnpkg.com/juice-sdk-core/-/juice-sdk-core-11.5.0-alpha.tgz#40ae9e4a44a86dc74777842bb18b30ae36373344" + integrity sha512-0T/v1OHsG99tO/Zo5pY1tbJzKjh0KB4bEMLAnJSRNOeb6hAKiS77xgcth22uYRRpZc4OLZWFjszR+FC2dAKQUg== dependencies: bs58 "^5.0.0" fpnum "^1.0.0" -juice-sdk-react@^11.0.0-alpha: - version "11.0.0-alpha" - resolved "https://registry.yarnpkg.com/juice-sdk-react/-/juice-sdk-react-11.0.0-alpha.tgz#15b93be75d80e9e83a0f45b4cc8c81d3f1828e49" - integrity sha512-G0zCPMCozfAK0Y9mxGMtMOYW8Bm3ml5bEEr/pKFbrrxIs+sEUnK4f5CZWiaA6fZlN8QnUbXbIRteFjv+RB2Yzg== +juice-sdk-react@^11.6.0-alpha: + version "11.6.0-alpha" + resolved "https://registry.yarnpkg.com/juice-sdk-react/-/juice-sdk-react-11.6.0-alpha.tgz#5ddc6a42f2586ed3b694bde5b8ad76b9ad788b9c" + integrity sha512-9ji5COnA0aaqXQOnqbqpohaOXPE7tTNz9J9FlA2d4jG9wlXjYDJrpCAB42g1p1Ojvel6zsLLzuY7aT25UrYFjg== juice@^10.0.0: version "10.0.0"