From 2348a99af9b838bf79d11269924d8f4d41129492 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Thu, 30 Nov 2023 08:50:52 +0530 Subject: [PATCH] Add option to deploy specific contract --- src/components/workspace/ABIUi/ABIUi.tsx | 2 +- .../workspace/BuildProject/BuildProject.tsx | 204 +++++++++++++++--- src/components/workspace/Editor/Editor.tsx | 4 +- src/hooks/contract.hooks.ts | 48 +++-- src/hooks/project.hooks.ts | 194 +++++++---------- src/hooks/workspace.hooks.ts | 46 ++++ src/interfaces/workspace.interface.ts | 6 +- 7 files changed, 342 insertions(+), 162 deletions(-) diff --git a/src/components/workspace/ABIUi/ABIUi.tsx b/src/components/workspace/ABIUi/ABIUi.tsx index 061455d..c0adfcf 100644 --- a/src/components/workspace/ABIUi/ABIUi.tsx +++ b/src/components/workspace/ABIUi/ABIUi.tsx @@ -150,7 +150,7 @@ const ABIUi: FC = ({ htmlType="submit" loading={isLoading} > - {abi.name} + {abi.name || '-- fallback method --'} diff --git a/src/components/workspace/BuildProject/BuildProject.tsx b/src/components/workspace/BuildProject/BuildProject.tsx index 25a6c04..0134313 100644 --- a/src/components/workspace/BuildProject/BuildProject.tsx +++ b/src/components/workspace/BuildProject/BuildProject.tsx @@ -2,10 +2,14 @@ import TonAuth from '@/components/auth/TonAuth/TonAuth'; import { useContractAction } from '@/hooks/contract.hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; import { useWorkspaceActions } from '@/hooks/workspace.hooks'; -import { NetworkEnvironment } from '@/interfaces/workspace.interface'; +import { + ABIField, + InitParams, + NetworkEnvironment, +} from '@/interfaces/workspace.interface'; import { Analytics } from '@/utility/analytics'; import { buildTs } from '@/utility/typescriptHelper'; -import { getContractLINK } from '@/utility/utils'; +import { getContractLINK, getFileExtension } from '@/utility/utils'; import { Network } from '@orbs-network/ton-access'; import { Blockchain } from '@ton-community/sandbox'; import { CHAIN, useTonConnectUI } from '@tonconnect/ui-react'; @@ -30,6 +34,12 @@ import { } from '../abiInputs'; import { globalWorkspace } from '../globalWorkspace'; +const blankABI = { + getters: [], + setters: [], + initParams: [], +}; + const fields = (type: String) => { if ( type.includes('int') || @@ -81,6 +91,15 @@ const BuildProject: FC = ({ dataCell: Cell | null; } | null>(null); const cellBuilderRef = useRef(null); + const [contractABI, setContractABI] = useState<{ + getters: ABIField[]; + setters: ABIField[]; + initParams: InitParams[]; + }>(blankABI); + const [selectedContract, setSelectedContract] = useState( + undefined + ); + const [tonConnector] = useTonConnectUI(); const chain = tonConnector.wallet?.account.chain; @@ -102,21 +121,65 @@ const BuildProject: FC = ({ const activeProject = project(projectId); + const contractsToDeploy = () => { + return projectFiles(projectId) + .filter((f) => { + const _fileExtension = getFileExtension(f?.name || ''); + return ( + f.path?.startsWith('dist') && + ['abi'].includes(_fileExtension as string) + ); + }) + .map((f) => { + return { + id: f.id, + name: f.name + .replace('.abi', '') + .replace('tact_', '') + .replace('func_', ''), + path: f.path, + }; + }); + }; + const deployView = () => { - if (!activeProject?.contractBOC) { - return; - } + const _contractsToDeploy = contractsToDeploy(); - if (activeProject?.language != 'tact' && environment === 'SANDBOX') { + if (_contractsToDeploy.length === 0) { return; } return ( <> -
- {activeProject?.initParams && ( + { + if (Object.hasOwn(changedValues, 'contract')) { + setSelectedContract(changedValues.contract); + } + }} + > + + + + {contractABI?.initParams && (
- {activeProject?.initParams?.map((item, index) => { + {contractABI?.initParams?.map((item, index) => { if (item.name === 'queryId') return ; const Field = fields(item.type); @@ -138,7 +201,7 @@ const BuildProject: FC = ({ type="primary" htmlType="submit" // loading={isLoading == 'deploy'} - disabled={!activeProject?.contractBOC} + disabled={selectedContract === undefined} className="w-100 item-center-align ant-btn-primary-gradient" > Deploy @@ -148,26 +211,26 @@ const BuildProject: FC = ({ ); }; - const initDeploy = async (formValues = {}) => { + const initDeploy = async (formValues: any) => { const _temp: any = { ...formValues }; + let initParams = ''; if (_temp.queryId) { delete _temp.queryId; } - const initParamsData = activeProject?.initParams; + const initParamsData = contractABI?.initParams; let parametrsType: any = {}; if (initParamsData) { parametrsType = initParamsData.reduce( - (acc: any, curr) => ((acc[curr.name] = curr.type), acc), + (acc: any, curr: any) => ((acc[curr.name] = curr.type), acc), {} ); } for (const [key, value] of Object.entries(_temp)) { const type = parametrsType[key]; - console.log(key, value); if ( - type.includes('int') || + type?.includes('int') || type == 'Int' || type == 'bigint | number' || type == 'number | bigint' @@ -190,7 +253,7 @@ const BuildProject: FC = ({ } // } } - initParams = initParams.slice(0, -1); + initParams = initParams?.slice(0, -1); try { if (!tonConnector.connected && environment !== 'SANDBOX') { @@ -214,6 +277,11 @@ const BuildProject: FC = ({ const deploy = async () => { createLog(`Deploying contract ...`, 'info'); + const contractBOCPath = selectedContract?.replace('.abi', '.code.boc'); + const contractBOC = await getFileByPath(contractBOCPath, projectId); + if (!contractBOC?.content) { + throw 'Contract BOC is missing. Rebuild the contract.'; + } try { if (sandboxBlockchain && environment === 'SANDBOX') { const blockchain = await Blockchain.create(); @@ -232,10 +300,11 @@ const BuildProject: FC = ({ contract, logs, } = await deployContract( - activeProject?.contractBOC as string, + contractBOC.content, buildOutput?.dataCell as any, environment.toLowerCase() as Network, - activeProject!! + activeProject!!, + contractABI.initParams ); Analytics.track('Deploy project', { @@ -281,19 +350,23 @@ const BuildProject: FC = ({ }; const createStateInitCell = async (initParams = '') => { + if (!selectedContract) { + throw 'Please select contract'; + } + const contractScriptPath = selectedContract?.replace('.abi', '.ts'); if (!cellBuilderRef.current?.contentWindow) return; + const contractScript = await getFileByPath(contractScriptPath, projectId); + if (activeProject?.language === 'tact' && !contractScript?.content) { + throw 'Contract script is missing. Rebuild the contract.'; + } try { let jsOutout = [{ code: '' }]; if (activeProject?.language == 'tact') { - const contractScript = activeProject?.contractScript?.toString(); - if (!contractScript || typeof contractScript !== 'string') { - throw 'Build project built first'; - } jsOutout = await buildTs( { - 'tact.ts': activeProject?.contractScript?.toString(), + 'tact.ts': contractScript?.content, }, 'tact.ts' ); @@ -320,7 +393,11 @@ const BuildProject: FC = ({ .replace(/}\s+from\s.+/, '} = window.TonCore;') .replace(/^\s*export\s+\{[^}]*\};\s*/m, ''); - let contractName = activeProject?.contractName; + const contractName = selectedContract + .replace('dist/', '') + .replace('.abi', '') + .replace('tact_', '') + .replace('func_', ''); if (activeProject?.language == 'tact') { const _code = `async function main() { @@ -373,6 +450,79 @@ const BuildProject: FC = ({ return isValid; }; + const updateABI = async () => { + if (!selectedContract) { + setContractABI(blankABI); + return; + } + const contractABIFile = await getFileByPath(selectedContract, projectId); + + if (!contractABIFile?.content) { + createLog('Contract ABI is missing. Rebuild the contract.', 'error'); + return; + } + const contractABI = JSON.parse(contractABIFile?.content || '{}'); + if (activeProject?.language === 'tact') { + contractABI.getters = contractABI?.getters?.map((item: any) => { + return { + name: item.name, + parameters: item.arguments.map((parameter: any) => { + return { + name: parameter.name, + type: parameter.type, + format: parameter.format, + optional: parameter.optional, + }; + }), + }; + }); + let setters: any = []; + contractABI?.receivers?.forEach((item: any) => { + if (item.message.type === 'Deploy') { + return; + } + if (item.message.kind) { + if (item.message.kind !== 'typed') { + setters.push({ + name: item.message.text, + parameters: [], + kind: item.message.kind, + }); + return; + } + const singleItem = contractABI.types.find( + (type: any) => type.name === item.message.type + ); + const singleField = { + name: singleItem.name, + parameters: singleItem.fields.map((parameter: any) => { + return { + name: parameter.name, + type: parameter.type.type, + format: parameter.type.format, + optional: parameter.type.optional, + kind: item.message.kind, + }; + }), + }; + setters.push(singleField); + } + }); + + contractABI.setters = setters; + } + + setContractABI({ + getters: contractABI.getters || [], + setters: contractABI.setters || [], + initParams: contractABI.initParams || [], + }); + }; + + useEffect(() => { + updateABI(); + }, [selectedContract]); + useEffect(() => { const handler = ( event: MessageEvent<{ @@ -458,7 +608,7 @@ const BuildProject: FC = ({ icon="Build" label={ environment === 'SANDBOX' && activeProject?.language !== 'tact' - ? 'Build and Deploy' + ? 'Build' : 'Build' } description="- Select a contract file to build and deploy" @@ -468,7 +618,7 @@ const BuildProject: FC = ({ environment == 'SANDBOX' && activeProject?.language !== 'tact' ) { - initDeploy(); + // initDeploy(); } }} /> @@ -493,7 +643,7 @@ const BuildProject: FC = ({ = ({ file, projectId, className = '' }) => { }); // If file is changed e.g. in case of build process then force update in editor - EventEmitter.on('FORCE_UPDATE_FILE', async (fileId: string) => { - if (fileId !== latestFile.current.id) return; + EventEmitter.on('FORCE_UPDATE_FILE', async (filePath: string) => { + if (filePath !== latestFile.current.path) return; await fetchFileContent(true); }); return () => { diff --git a/src/hooks/contract.hooks.ts b/src/hooks/contract.hooks.ts index 5445f5d..214bb14 100644 --- a/src/hooks/contract.hooks.ts +++ b/src/hooks/contract.hooks.ts @@ -1,6 +1,7 @@ import { globalWorkspace } from '@/components/workspace/globalWorkspace'; import { ContractLanguage, + InitParams, NetworkEnvironment, ParameterType, Project, @@ -46,7 +47,8 @@ export function useContractAction() { codeBOC: string, dataCell: string, network: Network | Partial, - project: Project + project: Project, + initParams: InitParams[] ): Promise<{ address: string; contract?: SandboxContract; @@ -58,7 +60,7 @@ export function useContractAction() { let sender: Sender | null = null; // Amount to send to contract. Gas fee - const value = toNano('0.02'); + const value = toNano('0.05'); let stateInit: StateInit = {}; if (project.language === 'tact') { const _contractInit = (window as any).contractInit; @@ -77,8 +79,8 @@ export function useContractAction() { const _contractInit = (window as any).contractInit; let _userContract: any = null; - if (project?.initParams && project?.initParams?.length > 0) { - const hasQueryId = project?.initParams?.findIndex( + if (initParams && initParams?.length > 0) { + const hasQueryId = initParams?.findIndex( (item) => item.name == 'queryId' ); const queryId = BigInt(0); @@ -103,7 +105,11 @@ export function useContractAction() { const client = new TonClient({ endpoint }); _userContract = client.open(_contractInit); client; - } else if (network.toUpperCase() === 'SANDBOX' && sandboxBlockchain) { + } else if ( + network.toUpperCase() === 'SANDBOX' && + sandboxBlockchain && + project.language === 'tact' + ) { _userContract = sandboxBlockchain.openContract(_contractInit); sender = sandboxWallet!!.getSender(); } @@ -144,7 +150,6 @@ export function useContractAction() { } if (network.toUpperCase() === 'SANDBOX' && sandboxBlockchain) { - // else { const _userContract = UserContract.createForDeploy( stateInit.code as Cell, stateInit.data as Cell @@ -158,7 +163,6 @@ export function useContractAction() { address: _userContract.address.toString(), contract: userContract, }; - // } } const _contractAddress = contractAddress(0, stateInit); @@ -259,7 +263,7 @@ export function useContractAction() { $$type: methodName, }; if (kind === 'text') { - messageParams = methodName; + messageParams = methodName || ''; } stack?.forEach((item: any) => { messageParams = { @@ -268,6 +272,13 @@ export function useContractAction() { }; }); + if (kind === 'empty') { + messageParams = null; + } + if (kind === 'text' && Object.keys(messageParams).length === 0) { + messageParams = ''; + } + const response = await (contract as any).send( sender, { value: toNano('0.1') }, @@ -293,11 +304,10 @@ export function useContractAction() { contract: SandboxContract | null = null, language: ContractLanguage, kind?: string, - stack?: TupleItem[], + stack?: TupleItem[] | any, network?: Network | Partial ): Promise<{ message: string; logs?: string[] } | undefined | any> { - console.log(stack, 'stack'); - const parsedStack = stack?.map((item) => { + const parsedStack = stack?.map((item: any) => { switch (item.type as ParameterType) { case 'int': return { @@ -311,6 +321,11 @@ export function useContractAction() { .storeAddress(Address.parse((item as any).value)) .endCell(), }; + case 'bool': + return { + type: item.type, + value: item.value === 'true', + }; default: return { type: item.type, @@ -324,9 +339,16 @@ export function useContractAction() { let responseValues = []; if (language === 'tact') { // convert getter function name as per script function name. Ex. counter will become getCounter - const params = parsedStack?.map((item) => item.value); + const params = parsedStack?.map((item: any) => { + switch (item.type) { + case 'int': + return item.value as any; + default: + return item.value; + } + }); const _method = ('get' + capitalizeFirstLetter(methodName)) as any; - const response = await (contract as any)[_method](params); + const response = await (contract as any)[_method](...(params as any)); responseValues.push({ method: methodName, value: convertToText(response), diff --git a/src/hooks/project.hooks.ts b/src/hooks/project.hooks.ts index 45307e6..a2b1e6e 100644 --- a/src/hooks/project.hooks.ts +++ b/src/hooks/project.hooks.ts @@ -17,12 +17,8 @@ import { } from '@tact-lang/compiler'; import stdLibFiles from '@tact-lang/compiler/dist/imports/stdlib'; import { precompile } from '@tact-lang/compiler/dist/pipeline/precompile'; -import { - getContracts, - getType, -} from '@tact-lang/compiler/dist/types/resolveDescriptors'; +import { getType } from '@tact-lang/compiler/dist/types/resolveDescriptors'; -import EventEmitter from '@/utility/eventEmitter'; import { CompilerContext } from '@tact-lang/compiler/dist/context'; import { CompileResult, @@ -42,7 +38,7 @@ export function useProjectActions() { getFileByPath, addFilesToDatabase, updateProjectById, - createNewItem, + createFiles, updateFileContent, deleteItem, projectFiles, @@ -156,13 +152,26 @@ export function useProjectActions() { } const abi = await generateABI(fileList); - const data: Partial = { - abi: { getters: abi as any, setters: [] }, - contractBOC: (buildResult as SuccessResult).codeBoc, - }; - updateProjectById(data, projectId); - return data; + const contractName = file.path?.replace('.fc', ''); + createFiles( + [ + { + path: `dist/func_${contractName}.abi`, + content: JSON.stringify({ + name: contractName, + getters: abi, + setters: [], + }), + }, + { + path: `dist/func_${contractName}.code.boc`, + content: (buildResult as SuccessResult).codeBoc, + }, + ], + 'dist', + projectId + ); } async function compileTactProgram( @@ -238,115 +247,35 @@ export function useProjectActions() { } }); - const getters = (output.abi as any)?.getters?.map((item: any) => { - return { - name: item.name, - parameters: item.arguments.map((parameter: any) => { - return { - name: parameter.name, - type: parameter.type, - format: parameter.format, - optional: parameter.optional, - }; - }), - }; - }); + let buildFiles: Pick[] = []; + fs.overwrites.forEach((value, key) => { + const filePath = key.slice(1); - let setters: any = []; - (output.abi as any)?.receivers?.forEach((item: any) => { - if (item.message.type === 'Deploy') { - return; - } - if (item.message.kind) { - if (item.message.kind !== 'typed') { - setters.push({ - name: item.message.text, - parameters: [], - kind: item.message.kind, - }); - return; - } - const singleItem = (output.abi as any).types.find( - (type: any) => type.name === item.message.type - ); - const singleField = { - name: singleItem.name, - parameters: singleItem.fields.map((parameter: any) => { - return { - name: parameter.name, - type: parameter.type.type, - format: parameter.type.format, - optional: parameter.type.optional, - kind: item.message.kind, - }; - }), + let fileContent = value.toString(); + if (key.includes('.abi')) { + const contractName = key + .replace('/dist/', '') + .replace('.abi', '') + .replace('tact_', ''); + fileContent = JSON.parse(fileContent); + const parsedFileContent = { + ...(fileContent as Object), + initParams: getInitParams(ctx, contractName, fileContent), }; - setters.push(singleField); + fileContent = JSON.stringify(parsedFileContent); } - }); - - const _contract = getContracts(ctx); - const contactType = getType(ctx, _contract[0]); - - const initParams = contactType.init?.args?.map((item: any) => { - return { - name: item.name, - type: item.type.name, - optional: item.type.optional, - }; - }); - - const deployFields = (output.abi as any).types.find( - (item: any) => item.name === 'Deploy' - )?.fields; - - if (deployFields && deployFields.length > 0) { - deployFields.forEach((item: any) => { - initParams?.push({ - name: item.name, - type: item.type.type, - optional: item.type.optional, - }); + if (key.includes('.boc')) { + fileContent = Buffer.from(value).toString('base64'); + } + buildFiles.push({ + path: filePath, + content: fileContent, }); - } - - const data: Partial = { - abi: { getters, setters }, - contractBOC: output.boc, - initParams, - contractScript: output.contractScript.value, - contractName: _contract[0], - }; - - updateProjectById(data, projectId); - - let scriptPath = output.contractScript.name; - if (scriptPath.startsWith('/')) { - scriptPath = scriptPath.substring(1); - } - - const scriptFile = await getFileByPath(scriptPath, projectId); - - let fileToReRender = scriptFile; - - if (!scriptFile?.id) { - let distDirectory = await getFileByPath('dist', projectId); - fileToReRender = await createNewItem( - distDirectory?.id!!, - scriptPath, - 'file', - projectId, - output.contractScript.value.toString() - ); - } else { - updateFileContent( - scriptFile.id, - output.contractScript.value.toString(), - projectId - ); - } + // TODO: Do this after the build files are updated. + // EventEmitter.emit('FORCE_UPDATE_FILE', filePath); + }); - EventEmitter.emit('FORCE_UPDATE_FILE', fileToReRender?.id); + createFiles(buildFiles, 'dist', projectId); return fs.overwrites; } @@ -464,3 +393,36 @@ const importUserFile = async ( filesWithId: [...filesWithId, ...commonFiles.filesWithId], }; }; + +const getInitParams = ( + ctx: CompilerContext, + contractName: string, + abi: any +) => { + const contactType = getType(ctx, contractName); + let initParams: { name: string; type: string; optional: boolean }[] = []; + + initParams = + contactType.init?.args?.map((item: any) => { + return { + name: item.name, + type: item.type.name, + optional: item.type.optional, + }; + }) ?? []; + + const deployFields = abi.types.find( + (item: any) => item.name === 'Deploy' + )?.fields; + + if (deployFields && deployFields.length > 0) { + deployFields.forEach((item: any) => { + initParams?.push({ + name: item.name, + type: item.type.type, + optional: item.type.optional, + }); + }); + } + return initParams; +}; diff --git a/src/hooks/workspace.hooks.ts b/src/hooks/workspace.hooks.ts index 623189d..1baa1e6 100644 --- a/src/hooks/workspace.hooks.ts +++ b/src/hooks/workspace.hooks.ts @@ -28,6 +28,7 @@ function useWorkspaceActions() { deleteItem, moveFile, createNewItem, + createFiles, openedFiles, activeFile, getFileById, @@ -387,6 +388,51 @@ function useWorkspaceActions() { return newItem; } + async function createFiles( + files: Pick[], + directoryPath: string, + projectId: string + ) { + let _projectFiles = cloneDeep(projectFiles(projectId)); + // check if file name contains directory. Then create a directory first and then create a file + let directoryItem = await getFileByPath(directoryPath, projectId); + if (!directoryItem) { + directoryItem = _createItem('directory', directoryPath || '', '', ''); + _projectFiles.push(directoryItem); + } + + await Promise.all( + files.map(async (file) => { + const fileName = file.path!!.split('/').pop(); + let currentFile = _projectFiles.find((item) => item.name === fileName); + let isNewFile = false; + if (!currentFile) { + currentFile = _createItem( + 'file', + fileName!!, + directoryItem?.id || '', + directoryPath || '' + ); + isNewFile = true; + } + if (isNewFile) { + await fileSystem.files.add({ + id: currentFile.id, + content: file.content || '', + }); + } else { + await fileSystem.files.update(currentFile.id, { + content: file.content || '', + }); + } + if (isNewFile) { + _projectFiles.push(currentFile); + } + }) + ); + updateProjectFiles(_projectFiles, projectId); + } + function isFileExists( name: string, projectId: string, diff --git a/src/interfaces/workspace.interface.ts b/src/interfaces/workspace.interface.ts index 9149610..d6816b6 100644 --- a/src/interfaces/workspace.interface.ts +++ b/src/interfaces/workspace.interface.ts @@ -21,7 +21,7 @@ interface ProjectFiles { [id: string]: Tree[]; } -interface initParams { +export interface InitParams { name: string; type: string; optional: boolean; @@ -37,7 +37,7 @@ export interface Project { contractBOC?: string; abi?: ABI; contractScript?: Buffer; - initParams?: initParams[]; + initParams?: InitParams[]; contractName?: string; isPublic?: boolean; createdAt?: Date; @@ -73,4 +73,4 @@ export interface ABI { // parameters: ABIParameter[]; } -export type ParameterType = 'address' | 'cell' | 'slice' | 'int'; +export type ParameterType = 'address' | 'cell' | 'slice' | 'int' | 'bool';