Skip to content

Commit

Permalink
Merge pull request #480 from alephium/gen-interface
Browse files Browse the repository at this point in the history
Generate ralph interfaces based on contract artifacts
  • Loading branch information
polarker authored Dec 13, 2024
2 parents 5204934 + c4efad2 commit f408322
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 2 deletions.
14 changes: 14 additions & 0 deletions packages/cli/cli_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Configuration, DEFAULT_CONFIGURATION_VALUES } from './src/types'
import { createProject, genRalph } from './scripts/create-project'
import { checkFullNodeVersion, codegen, getConfigFile, getSdkFullNodeVersion, isNetworkLive, loadConfig } from './src'
import { Project } from './src/project'
import { genInterfaces } from './src/gen-interfaces'

function getConfig(options: any): Configuration {

Check warning on line 30 in packages/cli/cli_internal.ts

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected any. Specify a different type
const configFile = options.config ? (options.config as string) : getConfigFile()
Expand Down Expand Up @@ -211,4 +212,17 @@ program
}
})

program
.command('gen-interfaces')
.description('generate interfaces based on contract artifacts')
.requiredOption('-a, --artifactDir <artifact-dir>', 'the contract artifacts root dir')
.requiredOption('-o, --outputDir <output-dir>', '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)
167 changes: 167 additions & 0 deletions packages/cli/src/gen-interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
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 <http://www.gnu.org/licenses/>.
*/

import path from 'path'
import { Contract, decodeArrayType, PrimitiveTypes, Struct } from '@alephium/web3'
import { Project } from './project'
import { promises as fsPromises } from 'fs'

const header = '// Autogenerated file. Do not edit manually.\n\n'

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, header + 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<void> {
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<Contract | undefined> {
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
}
}
4 changes: 2 additions & 2 deletions packages/cli/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export class Project {
return script.artifact
}

private static async loadStructs(artifactsRootDir: string): Promise<Struct[]> {
static async loadStructs(artifactsRootDir: string): Promise<Struct[]> {
const filePath = path.join(artifactsRootDir, Project.structArtifactFileName)
if (!fs.existsSync(filePath)) return []
const content = await fsPromises.readFile(filePath)
Expand Down Expand Up @@ -474,7 +474,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))
}

Expand Down

0 comments on commit f408322

Please sign in to comment.