import { useCallback, useState, useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { FormProvider, FieldValues } from 'react-hook-form'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useMediaQuery } from 'usehooks-ts'; import { AddEnvironmentVariableInput, AuctionParams, Deployer, } from 'gql-client'; import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material'; import { ArrowRightCircleFilledIcon, LoadingIcon, } from 'components/shared/CustomIcon'; import { Heading } from '../../shared/Heading'; import { Button } from '../../shared/Button'; import { Input } from 'components/shared/Input'; import { useToast } from 'components/shared/Toast'; import { useGQLClient } from '../../../context/GQLClientContext'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import { EnvironmentVariablesFormValues } from 'types/types'; import ConnectWallet from './ConnectWallet'; import { useWalletConnectClient } from 'context/WalletConnectContext'; type ConfigureDeploymentFormValues = { option: string; lrn?: string; numProviders?: number; maxPrice?: string; }; type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues; const DEFAULT_MAX_PRICE = '10000'; const Configure = () => { const { signClient, session, accounts } = useWalletConnectClient(); const [isLoading, setIsLoading] = useState(false); const [deployers, setDeployers] = useState([]); const [selectedAccount, setSelectedAccount] = useState(); const [selectedDeployer, setSelectedDeployer] = useState(); const [isPaymentLoading, setIsPaymentLoading] = useState(false); const [isPaymentDone, setIsPaymentDone] = useState(false); const [searchParams] = useSearchParams(); const templateId = searchParams.get('templateId'); const queryParams = new URLSearchParams(location.search); const owner = queryParams.get('owner'); const name = queryParams.get('name'); const defaultBranch = queryParams.get('defaultBranch'); const fullName = queryParams.get('fullName'); const orgSlug = queryParams.get('orgSlug'); const templateOwner = queryParams.get('templateOwner'); const templateRepo = queryParams.get('templateRepo'); const isPrivate = queryParams.get('isPrivate') === 'true'; const navigate = useNavigate(); const { toast, dismiss } = useToast(); const client = useGQLClient(); const methods = useForm({ defaultValues: { option: 'Auction', maxPrice: DEFAULT_MAX_PRICE, lrn: '', numProviders: 1, variables: [], }, }); const selectedOption = methods.watch('option'); const isTabletView = useMediaQuery('(min-width: 720px)'); // md: const buttonSize = isTabletView ? { size: 'lg' as const } : {}; const createProject = async ( data: FieldValues, envVariables: AddEnvironmentVariableInput[], senderAddress: string, txHash: string, ): Promise => { setIsLoading(true); let projectId: string | null = null; try { let lrn: string | undefined; let auctionParams: AuctionParams | undefined; if (data.option === 'LRN') { lrn = data.lrn; } else if (data.option === 'Auction') { auctionParams = { numProviders: Number(data.numProviders!), maxPrice: data.maxPrice!.toString(), }; } if (templateId) { const projectData: any = { templateOwner, templateRepo, owner, name, isPrivate, paymentAddress: senderAddress, txHash, }; const { addProjectFromTemplate } = await client.addProjectFromTemplate( orgSlug!, projectData, lrn, auctionParams, envVariables, ); projectId = addProjectFromTemplate.id; } else { const { addProject } = await client.addProject( orgSlug!, { name: `${owner}-${name}`, prodBranch: defaultBranch!, repository: fullName!, template: 'webapp', paymentAddress: senderAddress, txHash, }, lrn, auctionParams, envVariables, ); projectId = addProject.id; } } catch (error) { console.error('Error creating project:', error); toast({ id: 'error-creating-project', title: 'Error creating project', variant: 'error', onDismiss: dismiss, }); } finally { setIsLoading(false); } if (projectId) { return projectId; } else { throw new Error('Project creation failed'); } }; const verifyTx = async ( senderAddress: string, txHash: string, amount: string, ): Promise => { const isValid = await client.verifyTx( txHash, `${amount.toString()}alnt`, senderAddress, ); return isValid; }; const handleFormSubmit = useCallback( async (createFormData: FieldValues) => { try { const deployerLrn = createFormData.lrn; const deployer = deployers.find( (deployer) => deployer.deployerLrn === deployerLrn, ); let amount: string; let senderAddress: string; let txHash: string; if (createFormData.option === 'LRN' && !deployer?.minimumPayment) { toast({ id: 'no-payment-required', title: 'No payment required. Deploying app...', variant: 'info', onDismiss: dismiss, }); txHash = ''; senderAddress = ''; } else { if (!selectedAccount) return; senderAddress = selectedAccount.split(':')[2]; if (createFormData.option === 'LRN') { amount = deployer?.minimumPayment!; } else { amount = ( createFormData.numProviders * createFormData.maxPrice ).toString(); } const amountToBePaid = amount.replace(/\D/g, '').toString(); const txHashResponse = await cosmosSendTokensHandler( selectedAccount, amountToBePaid, ); if (!txHashResponse) { console.error('Tx not successful'); return; } txHash = txHashResponse; const isTxHashValid = await verifyTx( senderAddress, txHash, amountToBePaid.toString(), ); if (isTxHashValid === false) { console.error('Invalid Tx hash', txHash); return; } } const environmentVariables = createFormData.variables.map( (variable: any) => { return { key: variable.key, value: variable.value, environments: Object.entries(createFormData.environment) .filter(([, value]) => value === true) .map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)), }; }, ); const projectId = await createProject( createFormData, environmentVariables, senderAddress, txHash, ); await client.getEnvironmentVariables(projectId); if (templateId) { createFormData.option === 'Auction' ? navigate( `/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, ) : navigate( `/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`, ); } else { createFormData.option === 'Auction' ? navigate( `/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, ) : navigate( `/${orgSlug}/projects/create/deploy?projectId=${projectId}`, ); } } catch (error) { console.error(error); toast({ id: 'error-deploying-app', title: 'Error deploying app', variant: 'error', onDismiss: dismiss, }); } }, [client, createProject, dismiss, toast], ); const fetchDeployers = useCallback(async () => { const res = await client.getDeployers(); setDeployers(res.deployers); }, [client]); const onAccountChange = useCallback((account: string) => { setSelectedAccount(account); }, []); const onDeployerChange = useCallback( (selectedLrn: string) => { const deployer = deployers.find((d) => d.deployerLrn === selectedLrn); setSelectedDeployer(deployer); }, [deployers], ); const cosmosSendTokensHandler = useCallback( async (selectedAccount: string, amount: string) => { if (!signClient || !session || !selectedAccount) { return; } const chainId = selectedAccount.split(':')[1]; const senderAddress = selectedAccount.split(':')[2]; const snowballAddress = await client.getAddress(); try { setIsPaymentDone(false); setIsPaymentLoading(true); toast({ id: 'sending-payment-request', title: 'Check your wallet and approve payment request', variant: 'loading', onDismiss: dismiss, }); const result: { signature: string } = await signClient.request({ topic: session.topic, chainId: `cosmos:${chainId}`, request: { method: 'cosmos_sendTokens', params: [ { from: senderAddress, to: snowballAddress, value: amount, }, ], }, }); if (!result) { throw new Error('Error completing transaction'); } toast({ id: 'payment-successful', title: 'Payment successful', variant: 'success', onDismiss: dismiss, }); setIsPaymentDone(true); return result.signature; } catch (error: any) { console.error('Error sending tokens', error); toast({ id: 'error-sending-tokens', title: 'Error sending tokens', variant: 'error', onDismiss: dismiss, }); setIsPaymentDone(false); } finally { setIsPaymentLoading(false); } }, [session, signClient, toast], ); useEffect(() => { fetchDeployers(); }, []); return (
Configure deployment The app can be deployed by setting the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
( )} />
{selectedOption === 'LRN' && (
The app will be deployed by the configured deployer ( Select deployer LRN {fieldState.error && ( {fieldState.error.message} )} )} />
)} {selectedOption === 'Auction' && ( <>
Set the number of deployers and maximum price for each deployment Number of Deployers ( onChange(e)} /> )} />
Maximum Price (alnt) ( )} />
)} Environment Variables
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
) : ( <> Connect to your wallet {accounts && accounts?.length > 0 && (
)} )}
); }; export default Configure;