Skip to content

Commit

Permalink
feat: Generate TypeScript class fo contract
Browse files Browse the repository at this point in the history
  • Loading branch information
Kok Kek committed May 31, 2023
1 parent a0fc9ba commit 77ba257
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/cli/actions/compile/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { globSync } from 'glob'

export const ABI_JSON: string = '.abi.json'
export const CONTENT_TS: string = 'Content.ts'
const extensions = ['.tvc', ABI_JSON, CONTENT_TS]
export const TS: string = '.ts'
const extensions = ['.tvc', ABI_JSON, CONTENT_TS, TS]

/**
* Return a set of contract assembly artifacts
Expand Down
51 changes: 51 additions & 0 deletions src/cli/actions/compile/generator/generateType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
type Item = {
components?: Item[]
name: string
type: string
}

export function generateType (items: Item[], input: boolean, level: number = 1): string {
const types = items.reduce((result, value) => {
return `${result}${' '.repeat(level)}${value.name}: ${generateParameterType(value.type, input, level, value.components)}\n`
}, '\n')
return `{${types}${' '.repeat(level - 1)}}`
}

function generateParameterType (type: string, input: boolean, level: number, components?: Item[]): string {
const isArray = type.substring(type.length - 2) === '[]'
const text = generateParameterSingleType(type, input, level, components)
return isArray ? `Array<${text}>` : text
}

function generateParameterSingleType (type: string, input: boolean, level: number, components?: Item[]): string {
if (type.indexOf('bool') === 0)
return input ? 'boolean' : 'string'

if (
type.indexOf('string') === 0 ||
type.indexOf('bytes') === 0 ||
type.indexOf('cell') === 0 ||
type.indexOf('address') === 0
)
return 'string'

if (
type.indexOf('uint') === 0 ||
type.indexOf('int') === 0 ||
type.indexOf('varuint') === 0 ||
type.indexOf('varint') === 0 ||
type.indexOf('byte') === 0
)
return input ? 'string | number | bigint' : 'string'

if (type.indexOf('tuple') === 0)
return generateType(components ?? [], input, ++level)

if (type.indexOf('map') === 0) {
type.indexOf(',')
const value = type.substring(type.indexOf(',') + 1, type.length - 1)
return `{[key: string | number]: ${generateParameterSingleType(value, input, level, components)}}`
}

return 'any'
}
177 changes: 177 additions & 0 deletions src/cli/actions/compile/generator/generateTypeScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import path from 'path'
import fs from 'fs-extra'
import { type VendeeConfig } from '../../../config/types'
import { ABI_JSON, TS } from '../artifacts'
import { type AbiContract } from '@eversdk/core'
import { generateType } from './generateType'

export function generateTypeScript (config: VendeeConfig, relativeDirectory: string, contract: string): void {
const name = path.parse(path.basename(contract)).name
const directory = path.resolve(process.cwd(), config.paths.build, relativeDirectory)
const abiFile = path.resolve(directory, `${name}${ABI_JSON}`)
const file = path.resolve(directory, `${name}${TS}`)
const abi: AbiContract = JSON.parse(fs.readFileSync(abiFile, { encoding: 'utf8' }))
const imports = getImports(name)
const types = getTypes(abi)
const mainClass = getContractClass(name, abi)
const callClass = getCallClass(name, abi)
const runClass = getRunClass(name, abi)
const payloadClass = getPayloadClass(name, abi)
const code = `${imports}
${types}
${mainClass}
${callClass}
${runClass}
${payloadClass}
`
fs.writeFileSync(file, code)
}

function getImports (name: string): string {
return `import { type CompiledContractConfig, Contract, type ContractOptions, type ResultOfCall, ZERO } from 'vendee'
import { type KeyPair, type ResultOfProcessMessage } from '@eversdk/core'
import ${name}Content from './${name}Content'`
}

function getTypes (abi: AbiContract): string {
return (abi.functions != null)
? abi.functions?.reduce((result, value): string => {
if (value.inputs.length > 0)
result += `type ${value.name}In = ${generateType(value.inputs, true)}\n`
if (value.outputs.length > 0)
result += `type ${value.name}Out = ${generateType(value.outputs, false)}\n`
return result
}, '')
: ''
}

function getContractClass (name: string, abi: AbiContract): string {
const deploy = constructorHasInputs(abi)
? `async deploy (
value: string | number | bigint,
input: constructorIn,
useGiver: boolean = true,
timeout: number = 60000
): Promise<ResultOfProcessMessage> {
return await this._deploy(value, input, useGiver, timeout)
}`
: `async deploy (
value: string | number | bigint,
useGiver: boolean = true,
timeout: number = 60000
): Promise<ResultOfProcessMessage> {
return await this._deploy(value, {}, useGiver, timeout)
}`
return `export class ${name} extends Contract {
private readonly _call: ${name}Calls
private readonly _run: ${name}Runs
private readonly _payload: ${name}Payload
constructor (config: CompiledContractConfig, options: ContractOptions = {}) {
if (config.address === undefined)
super({
abi: ${name}Content.abi,
initialData: config.initialData ?? {},
keys: config.keys ?? ZERO.keys,
tvc: ${name}Content.tvc
}, options)
else
super({
address: config.address,
abi: ${name}Content.abi
}, options)
this._call = new ${name}Calls(this)
this._run = new ${name}Runs(this)
this._payload = new ${name}Payload(this)
}
${deploy}
get call (): ${name}Calls {
return this._call
}
get run (): ${name}Runs {
return this._run
}
get payload (): ${name}Payload {
return this._payload
}
}`
}

function constructorHasInputs (abi: AbiContract): boolean {
if (abi.functions === undefined)
return false

for (let i = 0; abi.functions.length > 0; ++i)
if (abi.functions[i].name === 'constructor')
return abi.functions[i].inputs.length > 0
return false
}

function getCallClass (name: string, abi: AbiContract): string {
return `class ${name}Calls {
constructor (private readonly contract: Contract) {}
${getCalls(abi)}}`
}

function getCalls (abi: AbiContract): string {
return (abi.functions != null)
? abi.functions?.reduce((result, value): string => {
if (value.name === 'constructor')
return result

const inputIn = value.inputs.length > 0 ? `input: ${value.name}In, ` : ''
const inputParam = value.inputs.length > 0 ? 'input' : '{}'
const promiseType = value.outputs.length > 0 ? `ResultOfCall & { out: ${value.name}Out }` : 'ResultOfCall'
return result + ` async ${value.name} (${inputIn}keys?: KeyPair): Promise<${promiseType}> {
return await this.contract.callMethod('${value.name}', ${inputParam}, keys)
}
`
}, '')
: ''
}

function getRunClass (name: string, abi: AbiContract): string {
return `class ${name}Runs {
constructor (private readonly contract: Contract) {}
${getRuns(abi)}}`
}

function getRuns (abi: AbiContract): string {
return (abi.functions != null)
? abi.functions?.reduce((result, value): string => {
if (value.name === 'constructor')
return result

if (value.outputs.length === 0)
return result

const inputIn = value.inputs.length > 0 ? `input: ${value.name}In` : ''
const inputParam = value.inputs.length > 0 ? ', input' : ''
return result + ` async ${value.name} (${inputIn}): Promise<${value.name}Out> {
return (await this.contract.runMethod('${value.name}'${inputParam})).value
}
`
}, '')
: ''
}

function getPayloadClass (name: string, abi: AbiContract): string {
return `class ${name}Payload {
constructor (private readonly contract: Contract) {}
${getPayloads(abi)}}`
}

function getPayloads (abi: AbiContract): string {
return (abi.functions != null)
? abi.functions?.reduce((result, value): string => {
if (value.name === 'constructor')
return result

const inputIn = value.inputs.length > 0 ? `input: ${value.name}In` : ''
const inputParam = value.inputs.length > 0 ? ', input' : ''
return result + ` async ${value.name} (${inputIn}): Promise<string> {
return await this.contract.createPayload('${value.name}'${inputParam})
}
`
}, '')
: ''
}
2 changes: 2 additions & 0 deletions src/cli/actions/compile/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type VendeeConfig } from '../../config/types'
import { consoleTerminal, runCommand, type Terminal } from 'everdev'
import { green, grey } from 'colors'
import { ABI_JSON, CONTENT_TS } from './artifacts'
import { generateTypeScript } from './generator/generateTypeScript'

const onlyErrorConsoleTerminal: Terminal = new class implements Terminal {
log (..._0: unknown[]): void {}
Expand All @@ -28,6 +29,7 @@ export async function make (config: VendeeConfig, contracts: string[]): Promise<

await compile(config, directory, contract)
await wrap(config, directory, contract)
generateTypeScript(config, directory, contract)
}
}

Expand Down

0 comments on commit 77ba257

Please sign in to comment.