hts/apps/staffai/components/deploy-contract-button.tsx

223 lines
7.7 KiB
TypeScript

'use client'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { useDeployWithWallet } from '@/lib/functions/deploy-contract/wallet-deploy'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { IconExternalLink, IconSpinner } from './ui/icons'
import { useGlobalStore } from '@/app/state/global-store'
import { useNetwork } from 'wagmi'
export function DeployContractButton({ sourceCode }: { sourceCode: string }) {
const { deploy: deployWithWallet } = useDeployWithWallet()
const [constructorArgs, setConstructorArgs] = useState<string[]>([])
const [explorerUrl, setExplorerUrl] = useState<string>('')
const [ipfsUrl, setIpfsUrl] = useState<string>('')
const [isErrorDeploying, setIsErrorDeploying] = useState<boolean>(false)
const { isDeploying, setIsDeploying, isGenerating } = useGlobalStore()
const { chain } = useNetwork()
const [unsupportedNetwork, setUnsupportedNetwork] = useState<boolean>(false)
// check if the network is supported
useEffect(() => {
if (chain?.unsupported) {
setUnsupportedNetwork(true)
} else {
setUnsupportedNetwork(false)
}
}, [chain])
// function to get the contract name from the source code
const getContractName = () => {
const contractNameRegex = /contract\s+(\w+)\s*(?:is|{)/;
const contractNameMatch = contractNameRegex.exec(sourceCode)
return contractNameMatch ? contractNameMatch[1] : ''
}
// function to get the constructor arguments from the source code
const getConstructorArgs = () => {
// regex match to get the constructor arguments from the source code.
// i.e. constructor(uint256 _initialSupply, string memory _name, string memory _symbol) should get uint256 _initialSupply, string memory _name, string memory _symbol
const constructorArgsRegex = /constructor\(([^)]+)\)/
const constructorArgsMatch = constructorArgsRegex.exec(sourceCode)
if (!constructorArgsMatch) return []
const constructorArgs = constructorArgsMatch[1]
// split the constructor arguments into an array
const constructorArgsArray = constructorArgs.split(',')
// trim the whitespace from each argument
const trimmedConstructorArgsArray = constructorArgsArray.map(arg =>
arg.trim()
)
// return the array of constructor arguments
return trimmedConstructorArgsArray
}
function generateInputFields() {
const generatedConstructorArgs = getConstructorArgs()
const inputFields = generatedConstructorArgs.map((arg, index) => {
return (
<div key={index} className="flex flex-col gap-2">
<Label className="text-sm font-medium">{arg}</Label>
<Input
type="text"
value={constructorArgs[index] || ''}
onChange={e => {
const newConstructorArgs = [...constructorArgs]
newConstructorArgs[index] = e.target.value
setConstructorArgs(newConstructorArgs)
}}
className="rounded-md border border-gray-300 p-2"
/>
</div>
)
})
return inputFields
}
// Handler for deploy with wallet
const handleDeployWithWallet = async () => {
setIsDeploying(true)
setIsErrorDeploying(false)
try {
const { explorerUrl, ipfsUrl } = await deployWithWallet({
contractName: getContractName(),
sourceCode,
constructorArgs: constructorArgs
})
explorerUrl && setExplorerUrl(explorerUrl)
setIpfsUrl(ipfsUrl)
setIsDeploying(false)
} catch (e) {
console.log(e)
setIsErrorDeploying(true)
setIsDeploying(false)
}
}
return (
<div className="ml-4 flex w-full justify-end">
<Dialog
onOpenChange={isOpen => {
if (!isOpen && !isDeploying) {
setExplorerUrl('')
setIpfsUrl('')
setConstructorArgs([])
setIsErrorDeploying(false)
}
}}
>
<DialogTrigger asChild>
<Button
className="mr-2 text-primary-foreground"
variant="default"
disabled={unsupportedNetwork || isGenerating}
size="sm"
>
<p className="hidden sm:flex">Deploy Contract</p>
<p className="flex sm:hidden">Deploy</p>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Manually Deploy Contract</DialogTitle>
<DialogDescription>
Deploy the contract using your wallet. Must be connected to a
supported testnet.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 py-4">
<div className="flex flex-col gap-2">
<div className="flex">
<p className="text-sm font-medium">Agent Deploy</p>
<Badge variant="default" className="ml-2 rounded">
RECOMMENDED
</Badge>
</div>
<p className="text-sm text-gray-500">
{`This method does not require wallet connection. Just use the keyword "deploy" in the chat.`}
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex">
<p className="text-sm font-medium">Deploy with Wallet</p>
<Badge variant="destructive" className="ml-2 rounded">
ADVANCED
</Badge>
</div>
<p className="text-sm text-gray-500">
Sign and deploy the contract using your own wallet. Be cautious
of risks and network fees.
</p>
</div>
{getConstructorArgs().length > 0 && (
<div className="flex max-h-48 flex-col gap-4 overflow-y-auto rounded border-2 p-4">
<DialogTitle className="text-md">
Constructor Arguments
</DialogTitle>
{generateInputFields()}
</div>
)}
</div>
<div className="flex flex-col items-center gap-4 py-4">
{isErrorDeploying && (
<p className="text-sm text-destructive">
Error deploying contract.
</p>
)}
{isDeploying && (
<IconSpinner className="size-8 animate-spin text-gray-500" />
)}
{explorerUrl && (
<Link
href={explorerUrl}
target="_blank"
className="text-sm text-green-500"
>
<div className="flex items-center">
View on Explorer
<IconExternalLink className="ml-1" />
</div>
</Link>
)}
{ipfsUrl && (
<Link
href={ipfsUrl}
target="_blank"
className="text-sm text-blue-500"
>
<div className="flex items-center">
View on IPFS
<IconExternalLink className="ml-1" />
</div>
</Link>
)}
</div>
<DialogFooter>
<Button
className="mb-4 p-6 sm:p-4"
disabled={isDeploying}
onClick={handleDeployWithWallet}
variant="secondary"
>
Deploy with Wallet
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}