Skip to content

Commit e32e659

Browse files
committed
Generate ralph interfaces based on contract artifacts
1 parent 9b650ab commit e32e659

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

packages/cli/cli_internal.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
loadConfig
3434
} from './src'
3535
import { Project } from './src/project'
36+
import { genInterfaces } from './src/gen-interfaces'
3637

3738
function getConfig(options: any): Configuration {
3839
const configFile = options.config ? (options.config as string) : getConfigFile()
@@ -212,4 +213,17 @@ program
212213
}
213214
})
214215

216+
program
217+
.command('gen-interfaces')
218+
.description('generate interfaces based on contract artifacts')
219+
.requiredOption('-a, --artifactDir <artifact-dir>', 'the contract artifacts root dir')
220+
.requiredOption('-o, --outputDir <output-dir>', 'the dir where the generated interfaces will be saved')
221+
.action(async (options) => {
222+
try {
223+
await genInterfaces(options.artifactDir, options.outputDir)
224+
} catch (error) {
225+
program.error(`✘ Failed to generate interfaces, error: `, error)
226+
}
227+
})
228+
215229
program.parseAsync(process.argv)

packages/cli/src/gen-interfaces.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright 2018 - 2022 The Alephium Authors
3+
This file is part of the alephium project.
4+
5+
The library is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU Lesser General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
The library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public License
16+
along with the library. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import path from 'path'
20+
import { Contract, decodeArrayType, PrimitiveTypes, Struct } from '@alephium/web3'
21+
import { Project } from './project'
22+
import { promises as fsPromises } from 'fs'
23+
24+
export async function genInterfaces(artifactDir: string, outDir: string) {
25+
const structs = await Project.loadStructs(artifactDir)
26+
const contracts = await loadContracts(artifactDir, structs)
27+
const structNames = structs.map((s) => s.name)
28+
const contractNames = contracts.map((c) => c.name)
29+
const interfaceDefs = contracts.map((c) => genInterface(c, structNames, contractNames))
30+
31+
const outPath = path.resolve(outDir)
32+
await fsPromises.rm(outPath, { recursive: true, force: true })
33+
await fsPromises.mkdir(outPath, { recursive: true })
34+
for (const i of interfaceDefs) {
35+
const filePath = path.join(outPath, `${i.name}.ral`)
36+
await saveToFile(filePath, i.def)
37+
}
38+
if (structs.length > 0) {
39+
const structDefs = genStructs(structs, structNames, contractNames)
40+
await saveToFile(path.join(outPath, 'structs.ral'), structDefs)
41+
}
42+
}
43+
44+
async function saveToFile(filePath: string, content: string) {
45+
await fsPromises.writeFile(filePath, content, 'utf-8')
46+
}
47+
48+
function genInterface(contract: Contract, structNames: string[], contractNames: string[]) {
49+
const interfaceName = `I${contract.name}`
50+
const functions: string[] = []
51+
let publicFuncIndex = 0
52+
contract.functions.forEach((funcSig, index) => {
53+
const method = contract.decodedContract.methods[`${index}`]
54+
if (!method.isPublic) return
55+
const usingAnnotations: string[] = []
56+
if (publicFuncIndex !== index) usingAnnotations.push(`methodIndex = ${index}`)
57+
if (method.useContractAssets) usingAnnotations.push('assetsInContract = true')
58+
if (method.usePreapprovedAssets) usingAnnotations.push('preapprovedAssets = true')
59+
if (method.usePayToContractOnly) usingAnnotations.push('payToContractOnly = true')
60+
const annotation = usingAnnotations.length === 0 ? '' : `@using(${usingAnnotations.join(', ')})`
61+
62+
const params = funcSig.paramNames.map((paramName, index) => {
63+
const type = getType(funcSig.paramTypes[`${index}`], structNames, contractNames)
64+
const isMutable = funcSig.paramIsMutable[`${index}`]
65+
return isMutable ? `mut ${paramName}: ${type}` : `${paramName}: ${type}`
66+
})
67+
const rets = funcSig.returnTypes.map((type) => getType(type, structNames, contractNames))
68+
const result = `
69+
${annotation}
70+
pub fn ${funcSig.name}(${params.join(', ')}) -> (${rets.join(', ')})
71+
`
72+
functions.push(result.trim())
73+
publicFuncIndex += 1
74+
})
75+
const interfaceDef = format(`
76+
Interface ${interfaceName} {
77+
${functions.join('\n\n')}
78+
}`)
79+
return { name: interfaceName, def: interfaceDef }
80+
}
81+
82+
function getType(typeName: string, structNames: string[], contractNames: string[]): string {
83+
if (PrimitiveTypes.includes(typeName)) return typeName
84+
if (typeName.startsWith('[')) {
85+
const [baseType, size] = decodeArrayType(typeName)
86+
return `[${getType(baseType, structNames, contractNames)}; ${size}]`
87+
}
88+
if (structNames.includes(typeName)) return typeName
89+
if (contractNames.includes(typeName)) return `I${typeName}`
90+
// We currently do not generate artifacts for interface types, so when a function
91+
// param/ret is of an interface type, we use `ByteVec` as the param/ret type
92+
return 'ByteVec'
93+
}
94+
95+
function genStructs(structs: Struct[], structNames: string[], contractNames: string[]) {
96+
const structDefs = structs.map((s) => {
97+
const fields = s.fieldNames.map((fieldName, index) => {
98+
const fieldType = getType(s.fieldTypes[`${index}`], structNames, contractNames)
99+
const isMutable = s.isMutable[`${index}`]
100+
return isMutable ? `mut ${fieldName}: ${fieldType}` : `${fieldName}: ${fieldType}`
101+
})
102+
return format(`
103+
struct ${s.name} {
104+
${fields.join(',\n')}
105+
}`)
106+
})
107+
return structDefs.join('\n\n')
108+
}
109+
110+
function format(str: string): string {
111+
const padding = ' ' // 2 spaces
112+
const lines = str.trim().split('\n')
113+
return lines
114+
.map((line, index) => {
115+
const newLine = line.trim()
116+
if (index === 0 || index === lines.length - 1) {
117+
return newLine
118+
} else if (newLine.length === 0) {
119+
return line
120+
} else {
121+
return padding + newLine
122+
}
123+
})
124+
.join('\n')
125+
}
126+
127+
async function loadContracts(artifactDir: string, structs: Struct[]) {
128+
const contracts: Contract[] = []
129+
const load = async function (dirPath: string): Promise<void> {
130+
const dirents = await fsPromises.readdir(dirPath, { withFileTypes: true })
131+
for (const dirent of dirents) {
132+
if (dirent.isFile()) {
133+
const artifactPath = path.join(dirPath, dirent.name)
134+
const contract = await getContractFromArtifact(artifactPath, structs)
135+
if (contract !== undefined) contracts.push(contract)
136+
} else {
137+
const newPath = path.join(dirPath, dirent.name)
138+
await load(newPath)
139+
}
140+
}
141+
}
142+
await load(artifactDir)
143+
return contracts
144+
}
145+
146+
async function getContractFromArtifact(filePath: string, structs: Struct[]): Promise<Contract | undefined> {
147+
if (!filePath.endsWith('.ral.json')) return undefined
148+
if (filePath.endsWith(Project.structArtifactFileName) || filePath.endsWith(Project.constantArtifactFileName)) {
149+
return undefined
150+
}
151+
const content = await fsPromises.readFile(filePath)
152+
const artifact = JSON.parse(content.toString())
153+
if ('bytecodeTemplate' in artifact) return undefined
154+
try {
155+
return Contract.fromJson(artifact, '', '', structs)
156+
} catch (error) {
157+
console.error(`Failed to load contract from artifact ${filePath}: `, error)
158+
return undefined
159+
}
160+
}

packages/cli/src/project.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ export class Project {
433433
return script.artifact
434434
}
435435

436-
private static async loadStructs(artifactsRootDir: string): Promise<Struct[]> {
436+
static async loadStructs(artifactsRootDir: string): Promise<Struct[]> {
437437
const filePath = path.join(artifactsRootDir, Project.structArtifactFileName)
438438
if (!fs.existsSync(filePath)) return []
439439
const content = await fsPromises.readFile(filePath)
@@ -471,7 +471,7 @@ export class Project {
471471
if (this.enums.length !== 0) {
472472
object['enums'] = this.enums
473473
}
474-
const filePath = path.join(this.artifactsRootDir, 'constants.ral.json')
474+
const filePath = path.join(this.artifactsRootDir, Project.constantArtifactFileName)
475475
return fsPromises.writeFile(filePath, JSON.stringify(object, null, 2))
476476
}
477477

0 commit comments

Comments
 (0)