import { useCallback, useState, useEffect, useMemo } 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 { BigNumber } from 'ethers'; 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 ApproveTransactionModal from './ApproveTransactionModal'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import { EnvironmentVariablesFormValues } from 'types/types'; import { VITE_WALLET_IFRAME_URL, } from 'utils/constants'; import CheckBalanceIframe from './CheckBalanceIframe'; import { useAddNetwork } from '../../../hooks/useAddNetwork'; type ConfigureDeploymentFormValues = { option: string; lrn?: string; numProviders?: string; maxPrice?: string; }; type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues; const DEFAULT_MAX_PRICE = '10000'; const TX_APPROVAL_TIMEOUT_MS = 60000; const Configure = () => { 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 [isFrameVisible, setIsFrameVisible] = useState(false); const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false); const [balanceMessage, setBalanceMessage] = useState(); const [isBalanceSufficient, setIsBalanceSufficient] = useState(); 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 { networkData } = useAddNetwork() const methods = useForm({ defaultValues: { option: 'Auction', maxPrice: DEFAULT_MAX_PRICE, lrn: '', numProviders: '1', variables: [], }, }); const selectedOption = methods.watch('option'); const selectedNumProviders = methods.watch('numProviders') ?? '1'; const selectedMaxPrice = methods.watch('maxPrice') ?? DEFAULT_MAX_PRICE; const isTabletView = useMediaQuery('(min-width: 720px)'); // md: const buttonSize = isTabletView ? { size: 'lg' as const } : {}; const amountToBePaid = useMemo(() => { let amount: string; if (selectedOption === 'LRN') { amount = selectedDeployer?.minimumPayment?.replace(/\D/g, '') ?? '0'; } else { if (!selectedNumProviders || !selectedMaxPrice) { return ''; } const bigMaxPrice = BigNumber.from(selectedMaxPrice); amount = bigMaxPrice.mul(selectedNumProviders).toString(); } return amount; }, [ selectedOption, selectedDeployer?.minimumPayment, selectedMaxPrice, selectedNumProviders, ]); 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 senderAddress: string; let txHash: string | null = null; 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; txHash = await cosmosSendTokensHandler(senderAddress, amountToBePaid); if (!txHash) { toast({ id: 'unsuccessful-tx', title: 'Transaction rejected', variant: 'error', onDismiss: dismiss, }); setIsFrameVisible(false); setIsPaymentLoading(false); throw new Error('Transaction rejected'); } // Validate transaction hash const isTxHashValid = await verifyTx( senderAddress, txHash, amountToBePaid, ); setIsPaymentLoading(false); if (isTxHashValid) { toast({ id: 'payment-successful', title: 'Payment successful', variant: 'success', onDismiss: dismiss, }); setIsPaymentDone(true); } else { toast({ id: 'invalid-tx-hash', title: 'Transaction validation failed', variant: 'error', onDismiss: dismiss, }); throw new Error('Transaction validation failed'); } } 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: any) { toast({ id: 'error-deploying-app', title: 'Error deploying app', variant: 'error', onDismiss: dismiss, }); throw new Error(error); } }, [client, createProject, dismiss, toast, amountToBePaid], ); const fetchDeployers = useCallback(async () => { const res = await client.getDeployers(); setDeployers(res.deployers); }, [client]); 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 (!selectedAccount) { throw new Error('Account not selected'); } const senderAddress = selectedAccount; const snowballAddress = await client.getAddress(); let timeoutId; try { setIsPaymentDone(false); setIsPaymentLoading(true); await requestTx(senderAddress, snowballAddress, amount); const txHash = await new Promise((resolve, reject) => { // Call cleanup method only if appropriate event type is recieved const cleanup = () => { setIsFrameVisible(false); window.removeEventListener('message', handleTxStatus); }; const handleTxStatus = async (event: MessageEvent) => { if (event.origin !== VITE_WALLET_IFRAME_URL) return; if (event.data.type === 'TRANSACTION_RESPONSE') { const txResponse = event.data.data; resolve(txResponse); cleanup(); } else if (event.data.type === 'ERROR') { console.error('Error from wallet:', event.data.message); reject(new Error('Transaction failed')); toast({ id: 'error-transaction', title: 'Error during transaction', variant: 'error', onDismiss: dismiss, }); cleanup(); } }; window.addEventListener('message', handleTxStatus); // Set a timeout, consider unsuccessful after 1 min timeoutId = setTimeout(() => { reject(new Error('Transaction timeout')); window.removeEventListener('message', handleTxStatus); toast({ id: 'transaction-timeout', title: 'The transaction request timed out. Please try again', variant: 'error', onDismiss: dismiss, }); setIsFrameVisible(false); setIsPaymentLoading(false); }, TX_APPROVAL_TIMEOUT_MS); }); return txHash; } catch (error) { console.error('Error in transaction:', error); throw new Error('Error in transaction'); } finally { clearTimeout(timeoutId); } }, [client, dismiss, toast], ); const requestTx = async ( sender: string, recipient: string, amount: string, ) => { const iframe = document.getElementById('walletIframe') as HTMLIFrameElement; if (!iframe.contentWindow) { console.error('Iframe not found or not loaded'); throw new Error('Iframe not found or not loaded'); } iframe.contentWindow.postMessage( { type: 'REQUEST_TX', chainId: networkData?.chainId, fromAddress: sender, toAddress: recipient, amount, }, VITE_WALLET_IFRAME_URL, ); setIsFrameVisible(true); }; useEffect(() => { fetchDeployers(); }, []); useEffect(() => { if (isBalanceSufficient) { setBalanceMessage(undefined); } }, [isBalanceSufficient]); 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)} min={1} /> )} />
Maximum Price (alnt) ( )} />
)} Environment Variables
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
) : (
{isAccountsDataReceived && isBalanceSufficient !== undefined ? ( !selectedAccount || !isBalanceSufficient ? (

{balanceMessage !== undefined ? (

{balanceMessage}

) : !selectedAccount ? ( 'No accounts found. Create a wallet.' ) : ( 'Insufficient funds.' )}

) : null ) : null}
)}
); }; export default Configure;