From 820e84f1eed82e6a0ce51f4541d748a8997c6511 Mon Sep 17 00:00:00 2001 From: lbqds Date: Thu, 5 Dec 2024 11:05:41 +0800 Subject: [PATCH] Generate ralph interfaces based on contract artifacts --- packages/cli/cli_internal.ts | 14 +++ packages/cli/src/gen-interfaces.ts | 165 +++++++++++++++++++++++++++++ packages/cli/src/project.ts | 4 +- 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/gen-interfaces.ts diff --git a/packages/cli/cli_internal.ts b/packages/cli/cli_internal.ts index 24605aafa..4cf99ed43 100644 --- a/packages/cli/cli_internal.ts +++ b/packages/cli/cli_internal.ts @@ -33,6 +33,7 @@ import { loadConfig } from './src' import { Project } from './src/project' +import { genInterfaces } from './src/gen-interfaces' function getConfig(options: any): Configuration { const configFile = options.config ? (options.config as string) : getConfigFile() @@ -212,4 +213,17 @@ program } }) +program + .command('gen-interfaces') + .description('generate interfaces based on contract artifacts') + .requiredOption('-a, --artifactDir ', 'the contract artifacts root dir') + .requiredOption('-o, --outputDir ', 'the dir where the generated interfaces will be saved') + .action(async (options) => { + try { + await genInterfaces(options.artifactDir, options.outputDir) + } catch (error) { + program.error(`✘ Failed to generate interfaces, error: `, error) + } + }) + program.parseAsync(process.argv) diff --git a/packages/cli/src/gen-interfaces.ts b/packages/cli/src/gen-interfaces.ts new file mode 100644 index 000000000..30058ffa4 --- /dev/null +++ b/packages/cli/src/gen-interfaces.ts @@ -0,0 +1,165 @@ +/* +Copyright 2018 - 2022 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import path from 'path' +import { Contract, decodeArrayType, PrimitiveTypes, Struct } from '@alephium/web3' +import { Project } from './project' +import { promises as fsPromises } from 'fs' + +export async function genInterfaces(artifactDir: string, outDir: string) { + const structs = await Project.loadStructs(artifactDir) + const contracts = await loadContracts(artifactDir, structs) + const structNames = structs.map((s) => s.name) + const contractNames = contracts.map((c) => c.name) + const interfaceDefs = contracts.map((c) => genInterface(c, structNames, contractNames)) + + const outPath = path.resolve(outDir) + await fsPromises.rm(outPath, { recursive: true, force: true }) + await fsPromises.mkdir(outPath, { recursive: true }) + for (const i of interfaceDefs) { + const filePath = path.join(outPath, `${i.name}.ral`) + await saveToFile(filePath, i.def) + } + if (structs.length > 0) { + const structDefs = genStructs(structs, structNames, contractNames) + await saveToFile(path.join(outPath, 'structs.ral'), structDefs) + } +} + +async function saveToFile(filePath: string, content: string) { + await fsPromises.writeFile(filePath, content, 'utf-8') +} + +function genInterface(contract: Contract, structNames: string[], contractNames: string[]) { + const interfaceName = `I${contract.name}` + const functions: string[] = [] + let publicFuncIndex = 0 + contract.functions.forEach((funcSig, index) => { + const method = contract.decodedContract.methods[`${index}`] + if (!method.isPublic) return + const usingAnnotations: string[] = [] + if (publicFuncIndex !== index) usingAnnotations.push(`methodIndex = ${index}`) + if (method.useContractAssets) usingAnnotations.push('assetsInContract = true') + if (method.usePreapprovedAssets) usingAnnotations.push('preapprovedAssets = true') + if (method.usePayToContractOnly) usingAnnotations.push('payToContractOnly = true') + const annotation = usingAnnotations.length === 0 ? '' : `@using(${usingAnnotations.join(', ')})` + + const params = funcSig.paramNames.map((paramName, index) => { + const type = getType(funcSig.paramTypes[`${index}`], structNames, contractNames) + const isMutable = funcSig.paramIsMutable[`${index}`] + return isMutable ? `mut ${paramName}: ${type}` : `${paramName}: ${type}` + }) + const rets = funcSig.returnTypes.map((type) => getType(type, structNames, contractNames)) + const result = ` + ${annotation} + pub fn ${funcSig.name}(${params.join(', ')}) -> (${rets.join(', ')}) + ` + functions.push(result.trim()) + publicFuncIndex += 1 + }) + const interfaceDef = format( + `@using(methodSelector = false) + Interface ${interfaceName} { + ${functions.join('\n\n')} + }`, + 3 + ) + return { name: interfaceName, def: interfaceDef } +} + +function getType(typeName: string, structNames: string[], contractNames: string[]): string { + if (PrimitiveTypes.includes(typeName)) return typeName + if (typeName.startsWith('[')) { + const [baseType, size] = decodeArrayType(typeName) + return `[${getType(baseType, structNames, contractNames)}; ${size}]` + } + if (structNames.includes(typeName)) return typeName + if (contractNames.includes(typeName)) return `I${typeName}` + // We currently do not generate artifacts for interface types, so when a function + // param/ret is of an interface type, we use `ByteVec` as the param/ret type + return 'ByteVec' +} + +function genStructs(structs: Struct[], structNames: string[], contractNames: string[]) { + const structDefs = structs.map((s) => { + const fields = s.fieldNames.map((fieldName, index) => { + const fieldType = getType(s.fieldTypes[`${index}`], structNames, contractNames) + const isMutable = s.isMutable[`${index}`] + return isMutable ? `mut ${fieldName}: ${fieldType}` : `${fieldName}: ${fieldType}` + }) + return format( + `struct ${s.name} { + ${fields.join(',\n')} + }`, + 2 + ) + }) + return structDefs.join('\n\n') +} + +function format(str: string, lineToIndentFrom: number): string { + const padding = ' ' // 2 spaces + const lines = str.trim().split('\n') + return lines + .map((line, index) => { + const newLine = line.trim() + if (index < lineToIndentFrom - 1 || index === lines.length - 1) { + return newLine + } else if (newLine.length === 0) { + return line + } else { + return padding + newLine + } + }) + .join('\n') +} + +async function loadContracts(artifactDir: string, structs: Struct[]) { + const contracts: Contract[] = [] + const load = async function (dirPath: string): Promise { + const dirents = await fsPromises.readdir(dirPath, { withFileTypes: true }) + for (const dirent of dirents) { + if (dirent.isFile()) { + const artifactPath = path.join(dirPath, dirent.name) + const contract = await getContractFromArtifact(artifactPath, structs) + if (contract !== undefined) contracts.push(contract) + } else { + const newPath = path.join(dirPath, dirent.name) + await load(newPath) + } + } + } + await load(artifactDir) + return contracts +} + +async function getContractFromArtifact(filePath: string, structs: Struct[]): Promise { + if (!filePath.endsWith('.ral.json')) return undefined + if (filePath.endsWith(Project.structArtifactFileName) || filePath.endsWith(Project.constantArtifactFileName)) { + return undefined + } + const content = await fsPromises.readFile(filePath) + const artifact = JSON.parse(content.toString()) + if ('bytecodeTemplate' in artifact) return undefined + try { + return Contract.fromJson(artifact, '', '', structs) + } catch (error) { + console.error(`Failed to load contract from artifact ${filePath}: `, error) + return undefined + } +} diff --git a/packages/cli/src/project.ts b/packages/cli/src/project.ts index 3897c9701..62c1bc36b 100644 --- a/packages/cli/src/project.ts +++ b/packages/cli/src/project.ts @@ -433,7 +433,7 @@ export class Project { return script.artifact } - private static async loadStructs(artifactsRootDir: string): Promise { + static async loadStructs(artifactsRootDir: string): Promise { const filePath = path.join(artifactsRootDir, Project.structArtifactFileName) if (!fs.existsSync(filePath)) return [] const content = await fsPromises.readFile(filePath) @@ -471,7 +471,7 @@ export class Project { if (this.enums.length !== 0) { object['enums'] = this.enums } - const filePath = path.join(this.artifactsRootDir, 'constants.ral.json') + const filePath = path.join(this.artifactsRootDir, Project.constantArtifactFileName) return fsPromises.writeFile(filePath, JSON.stringify(object, null, 2)) }