From 12ad685850496ce2a59d1f9f8011f1bef77761e3 Mon Sep 17 00:00:00 2001 From: Malik Zulqurnain Date: Mon, 16 Sep 2024 18:42:32 +0500 Subject: [PATCH 1/4] Added integration for launching token on tokenized community step 1 --- .../helpers/ContractHelpers/Launchpad.ts | 18 +++ .../scripts/state/api/launchPad/index.ts | 3 + .../state/api/launchPad/launchToken.ts | 35 +++++ .../NewCommunityAdminModal.tsx | 21 ++- .../views/pages/LaunchToken/LaunchToken.tsx | 23 ++- .../CommunityInformationStep.tsx | 2 + .../TokenInformationForm.tsx | 140 ++++++++++++++++-- .../TokenInformationForm/types.ts | 6 +- .../TokenInformationStep.tsx | 17 ++- .../scripts/views/pages/LaunchToken/types.ts | 6 + .../pages/LaunchToken/useCreateCommunity.ts | 10 +- 11 files changed, 258 insertions(+), 23 deletions(-) create mode 100644 packages/commonwealth/client/scripts/state/api/launchPad/index.ts create mode 100644 packages/commonwealth/client/scripts/state/api/launchPad/launchToken.ts create mode 100644 packages/commonwealth/client/scripts/views/pages/LaunchToken/types.ts diff --git a/packages/commonwealth/client/scripts/helpers/ContractHelpers/Launchpad.ts b/packages/commonwealth/client/scripts/helpers/ContractHelpers/Launchpad.ts index d55cf7054c7..6016bbf5e64 100644 --- a/packages/commonwealth/client/scripts/helpers/ContractHelpers/Launchpad.ts +++ b/packages/commonwealth/client/scripts/helpers/ContractHelpers/Launchpad.ts @@ -1,6 +1,7 @@ import { buyToken, getPrice, + launchToken, sellToken, transferLiquidity, } from '../../../../../../libs/shared/src/commonProtocol'; @@ -16,6 +17,23 @@ class LaunchpadBondingCurve extends ContractBase { this.tokenAddress = tokenAddress; } + async launchToken(name: string, symbol: string, walletAddress: string) { + if (!this.initialized || !this.walletEnabled) { + await this.initialize(true); + } + + const txReceipt = await launchToken( + this.contract, + name, + symbol, + [], // TODO 9207: where do shares come from? + [], // TODO 9207: where do holders come from? + 0, // TODO 9207: where does totalSupply come from? + walletAddress, + ); + return txReceipt; + } + async buyToken(amountEth: number, walletAddress: string) { if (!this.initialized || !this.walletEnabled) { await this.initialize(true); diff --git a/packages/commonwealth/client/scripts/state/api/launchPad/index.ts b/packages/commonwealth/client/scripts/state/api/launchPad/index.ts new file mode 100644 index 00000000000..0a80c7cf292 --- /dev/null +++ b/packages/commonwealth/client/scripts/state/api/launchPad/index.ts @@ -0,0 +1,3 @@ +import useLaunchTokenMutation from './launchToken'; + +export { useLaunchTokenMutation }; diff --git a/packages/commonwealth/client/scripts/state/api/launchPad/launchToken.ts b/packages/commonwealth/client/scripts/state/api/launchPad/launchToken.ts new file mode 100644 index 00000000000..75710d1b5c2 --- /dev/null +++ b/packages/commonwealth/client/scripts/state/api/launchPad/launchToken.ts @@ -0,0 +1,35 @@ +import { commonProtocol } from '@hicommonwealth/shared'; +import { useMutation } from '@tanstack/react-query'; +import LaunchpadBondingCurve from 'helpers/ContractHelpers/Launchpad'; + +interface LaunchTokenProps { + chainRpc: string; + ethChainId: number; + name: string; + symbol: string; + walletAddress: string; +} + +const launchToken = async ({ + ethChainId, + chainRpc, + name, + symbol, + walletAddress, +}: LaunchTokenProps) => { + const launchPad = new LaunchpadBondingCurve( + '', + commonProtocol.factoryContracts[ethChainId].factory, + chainRpc, + ); + + return await launchPad.launchToken(name, symbol, walletAddress); +}; + +const useLaunchTokenMutation = () => { + return useMutation({ + mutationFn: launchToken, + }); +}; + +export default useLaunchTokenMutation; diff --git a/packages/commonwealth/client/scripts/views/modals/NewCommunityAdminModal/NewCommunityAdminModal.tsx b/packages/commonwealth/client/scripts/views/modals/NewCommunityAdminModal/NewCommunityAdminModal.tsx index 1e0099e6a3d..278def51d11 100644 --- a/packages/commonwealth/client/scripts/views/modals/NewCommunityAdminModal/NewCommunityAdminModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/NewCommunityAdminModal/NewCommunityAdminModal.tsx @@ -26,6 +26,7 @@ interface NewCommunityAdminModalProps { handleClickConnectNewWallet: () => void; handleClickContinue: (selectedAddress: string) => void; selectedCommunity: SelectedCommunity; + isTokenizedCommunity?: boolean; } const NewCommunityAdminModal = ({ @@ -33,6 +34,7 @@ const NewCommunityAdminModal = ({ handleClickConnectNewWallet, handleClickContinue, selectedCommunity, + isTokenizedCommunity, }: NewCommunityAdminModalProps) => { const user = useUserStore(); const availableAddressesOnSelectedChain = user.addresses?.filter( @@ -68,14 +70,23 @@ const NewCommunityAdminModal = ({ return (
- + {walletsAvailable - ? 'Community admins are associated with a wallet. ' + - 'Which wallet would you like to serve as the admin of the new community?' - : 'In order to launch a community within an ecosystem you must have a compatible wallet connected. ' + - 'How would you like to create your community?'} + ? `${isTokenizedCommunity ? 'Token creators and' : ''} Community admins are associated with a wallet. ` + + `Which wallet would you like to serve as the ${ + isTokenizedCommunity ? 'author/' : '' + }admin of the new community?` + : `In order to launch a ${ + isTokenizedCommunity ? 'token and' : '' + } community within ${ + isTokenizedCommunity ? 'the BASE' : 'an' + } ecosystem you must have a compatible wallet connected. ` + + `How would you like to create your ${isTokenizedCommunity ? 'token and' : ''} community?`} {walletsAvailable && ( { const navigate = useCommonNavigate(); - const { createTokenCommunityStep, onChangeStep } = useCreateCommunity(); + const { + createTokenCommunityStep, + onChangeStep, + selectedAddress, + setSelectedAddress, + createdTokenInfo, + setCreatedTokenInfo, + } = useCreateCommunity(); const { isAddedToHomeScreen } = useAppStatus(); @@ -33,7 +40,18 @@ const LaunchToken = () => { return ( navigate('/')} // redirect to home - handleContinue={() => onChangeStep(true)} + handleContinue={(tokenInfo) => { + setCreatedTokenInfo({ + name: tokenInfo.tokenName, + symbol: tokenInfo.tokenTicker, + description: tokenInfo.tokenDescription, + imageURL: tokenInfo.tokenImageURL, + }); + + onChangeStep(true); + }} + onAddressSelected={(address) => setSelectedAddress(address)} + selectedAddress={selectedAddress} /> ); case CreateTokenCommunityStep.CommunityInformation: @@ -41,6 +59,7 @@ const LaunchToken = () => { onChangeStep(false)} handleContinue={() => onChangeStep(true)} + tokenInfo={createdTokenInfo} /> ); case CreateTokenCommunityStep.SignatureLaunch: diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/CommunityInformationStep/CommunityInformationStep.tsx b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/CommunityInformationStep/CommunityInformationStep.tsx index b8f19d1f5e0..37e8d474882 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/CommunityInformationStep/CommunityInformationStep.tsx +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/CommunityInformationStep/CommunityInformationStep.tsx @@ -10,11 +10,13 @@ import CommunityInformationForm from 'views/components/CommunityInformationForm/ import { CWText } from 'views/components/component_kit/cw_text'; import CWBanner from 'views/components/component_kit/new_designs/CWBanner'; import { openConfirmation } from 'views/modals/confirmation_modal'; +import { TokenInfo } from '../../types'; import './CommunityInformationStep.scss'; interface CommunityInformationStepProps { handleGoBack: () => void; handleContinue: () => void; + tokenInfo?: TokenInfo; } const CommunityInformationStep = ({ diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx index 719b5927d58..a3d894af84e 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx @@ -1,11 +1,17 @@ +import { ChainBase, commonProtocol } from '@hicommonwealth/shared'; +import { notifyError } from 'controllers/app/notifications'; import useAppStatus from 'hooks/useAppStatus'; import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; -import React, { useState } from 'react'; +import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { BaseMixpanelPayload, MixpanelCommunityCreationEvent, MixpanelLoginPayload, } from 'shared/analytics/types'; +import { useLaunchTokenMutation } from 'state/api/launchPad'; +import { fetchCachedNodes } from 'state/api/nodes'; +import useUserStore from 'state/ui/user'; import { CWCoverImageUploader, ImageBehavior, @@ -13,9 +19,17 @@ import { import { CWLabel } from 'views/components/component_kit/cw_label'; import { CWTextArea } from 'views/components/component_kit/cw_text_area'; import { CWButton } from 'views/components/component_kit/new_designs/CWButton'; -import CWCommunitySelector from 'views/components/component_kit/new_designs/CWCommunitySelector'; -import { CWForm } from 'views/components/component_kit/new_designs/CWForm'; +import CWCommunitySelector, { + CommunityType, +} from 'views/components/component_kit/new_designs/CWCommunitySelector'; +import { + CWForm, + CWFormRef, +} from 'views/components/component_kit/new_designs/CWForm'; +import { CWModal } from 'views/components/component_kit/new_designs/CWModal'; import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextInput'; +import { AuthModal } from 'views/modals/AuthModal'; +import NewCommunityAdminModal from 'views/modals/NewCommunityAdminModal'; import { openConfirmation } from 'views/modals/confirmation_modal'; import { communityTypeOptions } from 'views/pages/CreateCommunity/steps/CommunityTypeStep/helpers'; import './TokenInformationForm.scss'; @@ -25,25 +39,89 @@ import { tokenInformationFormValidationSchema } from './validation'; const TokenInformationForm = ({ onSubmit, onCancel, + onAddressSelected, + selectedAddress, }: TokenInformationFormProps) => { + const user = useUserStore(); + const [baseOption] = communityTypeOptions; + + const shouldSubmitOnAddressSelection = useRef(false); + const formMethodsRef = useRef(); + const [ + isNewTokenCommunityAdminModalOpen, + setIsNewTokenCommunityAdminModalOpen, + ] = useState(false); + const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); const [isProcessingProfileImage, setIsProcessingProfileImage] = useState(false); const { isAddedToHomeScreen } = useAppStatus(); - const [baseOption] = communityTypeOptions; - const { trackAnalytics } = useBrowserAnalyticsTrack< MixpanelLoginPayload | BaseMixpanelPayload >({ onAction: true, }); - const handleSubmit = (values: FormSubmitValues) => { - // TODO 8705: call token launch endpoint - console.log('values => ', values); - onSubmit(); - }; + const { mutateAsync: launchToken } = useLaunchTokenMutation(); + + const openAddressSelectionModal = useCallback(() => { + if (selectedAddress) { + setIsAuthModalOpen(false); + setIsNewTokenCommunityAdminModalOpen(false); + } else { + setIsAuthModalOpen(!user.isLoggedIn); + setIsNewTokenCommunityAdminModalOpen(user.isLoggedIn); + } + }, [selectedAddress, user.isLoggedIn]); + + useRunOnceOnCondition({ + callback: openAddressSelectionModal, + shouldRun: true, + }); + + const handleSubmit = useCallback( + async (values: FormSubmitValues) => { + // get address from user + if (!selectedAddress) { + openAddressSelectionModal(); + shouldSubmitOnAddressSelection.current = true; + return; + } + + // get base chain node info + const nodes = fetchCachedNodes(); + const baseNode = nodes?.find( + (n) => n.ethChainId === commonProtocol.ValidChains.Base, + ); + if (!baseNode || !baseNode.ethChainId) { + notifyError('Could not find base chain node'); + return; + } + + // call endpoint + const payload = { + chainRpc: baseNode.url, + ethChainId: baseNode.ethChainId, + name: values.tokenName.trim(), + symbol: values.tokenTicker.trim(), + walletAddress: selectedAddress.address, + // TODO 9207: where to store values.tokenDescription and values.tokenImageURL + }; + await launchToken(payload); + + onSubmit(values); + }, + [openAddressSelectionModal, selectedAddress, onSubmit, launchToken], + ); + + useEffect(() => { + if (shouldSubmitOnAddressSelection.current) { + formMethodsRef.current && + formMethodsRef.current.handleSubmit(handleSubmit)(); + shouldSubmitOnAddressSelection.current = false; + } + }, [selectedAddress, handleSubmit]); const handleCancel = () => { openConfirmation({ @@ -73,8 +151,24 @@ const TokenInformationForm = ({ }); }; + const handleClickConnectNewWallet = () => { + setIsAuthModalOpen(true); + setIsNewTokenCommunityAdminModalOpen(false); + }; + + const handleSelectedAddress = (address: string) => { + const pickedAddressInfo = user.addresses.find( + ({ addressId }) => String(addressId) === address, + ); + pickedAddressInfo && onAddressSelected(pickedAddressInfo); + + setIsNewTokenCommunityAdminModalOpen(false); + }; + return ( + ref={formMethodsRef} validationSchema={tokenInformationFormValidationSchema} onSubmit={handleSubmit} className="TokenInformationForm" @@ -147,6 +241,32 @@ const TokenInformationForm = ({ disabled={isProcessingProfileImage} /> + + setIsNewTokenCommunityAdminModalOpen(false)} + selectedCommunity={{ + // token launch is only support for `Base` + chainBase: ChainBase.Ethereum, + type: CommunityType.Base, + }} + handleClickConnectNewWallet={handleClickConnectNewWallet} + handleClickContinue={handleSelectedAddress} + isTokenizedCommunity + /> + } + onClose={() => setIsNewTokenCommunityAdminModalOpen(false)} + open={isNewTokenCommunityAdminModalOpen} + /> + setIsAuthModalOpen(false)} + onSuccess={() => setIsNewTokenCommunityAdminModalOpen(true)} + showWalletsFor={ChainBase.Ethereum} + /> ); }; diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/types.ts b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/types.ts index ec00d841d58..af4a9fa174e 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/types.ts +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/types.ts @@ -1,3 +1,5 @@ +import AddressInfo from 'models/AddressInfo'; + export type FormSubmitValues = { tokenChain: string; tokenName: string; @@ -7,6 +9,8 @@ export type FormSubmitValues = { }; export type TokenInformationFormProps = { - onSubmit: () => void; + onSubmit: (values: FormSubmitValues) => void; onCancel: () => void; + selectedAddress?: AddressInfo; + onAddressSelected: (address: AddressInfo) => void; }; diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationStep.tsx b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationStep.tsx index cdb59beeb14..c6fa0767e67 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationStep.tsx +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationStep.tsx @@ -1,18 +1,22 @@ +import AddressInfo from 'models/AddressInfo'; import React from 'react'; - import { CWText } from 'views/components/component_kit/cw_text'; import TokenInformationForm from './TokenInformationForm/TokenInformationForm'; - +import { FormSubmitValues } from './TokenInformationForm/types'; import './TokenInformationStep.scss'; interface TokenInformationStepProps { handleGoBack: () => void; - handleContinue: () => void; + handleContinue: (values: FormSubmitValues) => void; + selectedAddress?: AddressInfo; + onAddressSelected: (address: AddressInfo) => void; } const TokenInformationStep = ({ handleGoBack, handleContinue, + selectedAddress, + onAddressSelected, }: TokenInformationStepProps) => { return (
@@ -23,7 +27,12 @@ const TokenInformationStep = ({ - +
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/types.ts b/packages/commonwealth/client/scripts/views/pages/LaunchToken/types.ts new file mode 100644 index 00000000000..131a0aad9eb --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/types.ts @@ -0,0 +1,6 @@ +export type TokenInfo = { + name: string; + symbol: string; + description?: string; + imageURL?: string; +}; diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts b/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts index bcc37715fcb..2013b6b598c 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts @@ -1,11 +1,15 @@ +import AddressInfo from 'client/scripts/models/AddressInfo'; import { useState } from 'react'; +import { TokenInfo } from './types'; import { CreateTokenCommunityStep, handleChangeStep } from './utils'; const useCreateCommunity = () => { const [createTokenCommunityStep, setCreateTokenCommunityStep] = useState( - CreateTokenCommunityStep.CommunityInformation, + CreateTokenCommunityStep.TokenInformation, ); + const [selectedAddress, setSelectedAddress] = useState(); + const [createdTokenInfo, setCreatedTokenInfo] = useState(); const onChangeStep = (forward: boolean) => { handleChangeStep( @@ -18,6 +22,10 @@ const useCreateCommunity = () => { return { createTokenCommunityStep, onChangeStep, + selectedAddress, + setSelectedAddress, + createdTokenInfo, + setCreatedTokenInfo, }; }; From 855ee5b5a6033740bae02da60433e7f568b50209 Mon Sep 17 00:00:00 2001 From: Malik Zulqurnain Date: Mon, 16 Sep 2024 18:55:52 +0500 Subject: [PATCH 2/4] Fix lint --- .../TokenInformationForm/TokenInformationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx index a3d894af84e..7613811cf73 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx @@ -108,7 +108,7 @@ const TokenInformationForm = ({ walletAddress: selectedAddress.address, // TODO 9207: where to store values.tokenDescription and values.tokenImageURL }; - await launchToken(payload); + await launchToken(payload).catch(console.error); onSubmit(values); }, From 9deb8fd95fc29c037eb3de5da667516635b46ef0 Mon Sep 17 00:00:00 2001 From: Malik Zulqurnain Date: Mon, 16 Sep 2024 19:47:36 +0500 Subject: [PATCH 3/4] Fix lint --- .../TokenInformationForm/TokenInformationForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx index 7613811cf73..20b600a448d 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/steps/TokenInformationStep/TokenInformationForm/TokenInformationForm.tsx @@ -118,7 +118,9 @@ const TokenInformationForm = ({ useEffect(() => { if (shouldSubmitOnAddressSelection.current) { formMethodsRef.current && - formMethodsRef.current.handleSubmit(handleSubmit)(); + formMethodsRef.current + .handleSubmit(handleSubmit)() + .catch(console.error); shouldSubmitOnAddressSelection.current = false; } }, [selectedAddress, handleSubmit]); From 3658ecb38cee649aedcbd36abd53b464d7ae1822 Mon Sep 17 00:00:00 2001 From: Malik Zulqurnain Date: Mon, 16 Sep 2024 21:48:15 +0500 Subject: [PATCH 4/4] Updated import path --- .../scripts/views/pages/LaunchToken/useCreateCommunity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts b/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts index 2013b6b598c..21fd2909e08 100644 --- a/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts +++ b/packages/commonwealth/client/scripts/views/pages/LaunchToken/useCreateCommunity.ts @@ -1,4 +1,4 @@ -import AddressInfo from 'client/scripts/models/AddressInfo'; +import AddressInfo from 'models/AddressInfo'; import { useState } from 'react'; import { TokenInfo } from './types'; import { CreateTokenCommunityStep, handleChangeStep } from './utils';