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 IFrameModal from './IFrameModal'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import { EnvironmentVariablesFormValues } from 'types/types'; import { VITE_LACONICD_CHAIN_ID, VITE_WALLET_IFRAME_URL, } from 'utils/constants'; import AccountsDropdown from './AccountsDropdown'; type ConfigureDeploymentFormValues = { option: string; lrn?: string; numProviders?: number; 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 [accounts, setAccounts] = 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 [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 | 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; if (createFormData.option === 'LRN') { amount = deployer?.minimumPayment!; } else { amount = ( createFormData.numProviders * createFormData.maxPrice ).toString(); } const amountToBePaid = amount.replace(/\D/g, '').toString(); 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], ); 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 (!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) => { 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); } 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, }); } setIsFrameVisible(false); window.removeEventListener('message', handleTxStatus); }; 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: VITE_LACONICD_CHAIN_ID, fromAddress: sender, toAddress: recipient, amount, }, VITE_WALLET_IFRAME_URL, ); setIsFrameVisible(true); }; 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 ? (
) : ( <> {accounts.length > 0 && (
)} )}
); }; export default Configure;