UNPKG

@neo-one/server-plugin-compiler

Version:

NEO•ONE Server compiler plugin.

267 lines (237 loc) 8.04 kB
import { ABI } from '@neo-one/client'; import { Binary, DescribeTable, TaskList } from '@neo-one/server-plugin'; import * as fs from 'fs-extra'; import * as path from 'path'; import { Observable, of as _of, ReplaySubject, Subject } from 'rxjs'; import { shareReplay, switchMap } from 'rxjs/operators'; import { compileSmartContract } from './compileSmartContract'; import { Contract, ContractResourceOptions, ContractResourceType } from './ContractResourceType'; import { ABIRequiredError } from './errors'; const ABI_PATH = 'abi.json'; const AVM_PATH = 'sc.avm'; const CONFIG_PATH = 'sc.json'; export interface ContractResourceAdapterInitOptions { readonly name: string; readonly dataPath: string; readonly binary: Binary; readonly resourceType: ContractResourceType; } export interface ContractResourceAdapterStaticOptions extends ContractResourceAdapterInitOptions { readonly avmPath: string; readonly abiPath: string; readonly configPath: string; } export interface ContractResourceAdapterOptions extends ContractResourceAdapterStaticOptions { readonly script: string; readonly abi: ABI; readonly hasStorage: boolean; readonly hasDynamicInvoke: boolean; readonly payable: boolean; } export class ContractResourceAdapter { public static async init(options: ContractResourceAdapterInitOptions): Promise<ContractResourceAdapter> { const staticOptions = this.getStaticOptions(options); const [abi, script, { hasStorage, hasDynamicInvoke, payable }] = await Promise.all([ fs.readJSON(staticOptions.abiPath), fs.readFile(staticOptions.avmPath, 'hex'), fs.readJSON(staticOptions.configPath), ]); return new this({ name: staticOptions.name, binary: staticOptions.binary, dataPath: staticOptions.dataPath, resourceType: staticOptions.resourceType, abiPath: staticOptions.abiPath, avmPath: staticOptions.avmPath, configPath: staticOptions.configPath, abi, script, hasStorage, hasDynamicInvoke, payable, }); } public static create(adapterOptions: ContractResourceAdapterInitOptions, options: ContractResourceOptions): TaskList { const staticOptions = this.getStaticOptions(adapterOptions); return new TaskList({ tasks: [ { title: 'Create data directory', task: async () => { await fs.ensureDir(staticOptions.dataPath); }, }, { title: 'Compile smart contract', task: async (ctx) => { if (options.scPath === undefined) { throw new Error('Something went wrong, smart contract path was null.'); } try { let { abi, hasStorage, hasDynamicInvoke, payable } = await compileSmartContract({ scPath: options.scPath, avmPath: staticOptions.avmPath, binary: staticOptions.binary, }); if (abi === undefined) { abi = options.abi; } if (abi === undefined) { throw new ABIRequiredError(); } if (hasStorage === undefined) { hasStorage = options.hasStorage; } if (hasDynamicInvoke === undefined) { hasDynamicInvoke = options.hasDynamicInvoke; } if (payable === undefined) { ({ payable } = options); } const script = await fs.readFile(staticOptions.avmPath, 'hex'); ctx.abi = abi; ctx.hasDynamicInvoke = hasDynamicInvoke; ctx.hasStorage = hasStorage; ctx.payable = payable; ctx.script = script; } catch (error) { await fs.remove(staticOptions.dataPath); throw error; } }, }, { title: 'Save ABI and configuration', task: async (ctx) => { const { abi, hasDynamicInvoke, hasStorage, payable, script } = ctx; await Promise.all([ fs.writeJSON(staticOptions.abiPath, abi), fs.writeJSON(staticOptions.configPath, { hasDynamicInvoke, hasStorage, payable, }), ]); ctx.resourceAdapter = new this({ name: staticOptions.name, binary: staticOptions.binary, dataPath: staticOptions.dataPath, resourceType: staticOptions.resourceType, abiPath: staticOptions.abiPath, avmPath: staticOptions.avmPath, configPath: staticOptions.configPath, script, abi, hasStorage, hasDynamicInvoke, payable, }); ctx.dependencies = []; }, }, ], }); } private static getStaticOptions(options: ContractResourceAdapterInitOptions): ContractResourceAdapterStaticOptions { return { name: options.name, binary: options.binary, dataPath: options.dataPath, resourceType: options.resourceType, abiPath: path.resolve(options.dataPath, ABI_PATH), avmPath: path.resolve(options.dataPath, AVM_PATH), configPath: path.resolve(options.dataPath, CONFIG_PATH), }; } public readonly resource$: Observable<Contract>; private readonly name: string; private readonly dataPath: string; private readonly resourceType: ContractResourceType; private readonly avmPath: string; private readonly abiPath: string; private readonly configPath: string; private readonly script: string; private readonly abi: ABI; private readonly hasStorage: boolean; private readonly hasDynamicInvoke: boolean; private readonly payable: boolean; private readonly update$: Subject<void>; public constructor({ name, dataPath, resourceType, avmPath, abiPath, configPath, script, abi, hasStorage, hasDynamicInvoke, payable, }: ContractResourceAdapterOptions) { this.name = name; this.dataPath = dataPath; this.resourceType = resourceType; this.avmPath = avmPath; this.abiPath = abiPath; this.configPath = configPath; this.script = script; this.abi = abi; this.hasStorage = hasStorage; this.hasDynamicInvoke = hasDynamicInvoke; this.payable = payable; this.update$ = new ReplaySubject(1); this.resource$ = this.update$.pipe( switchMap(() => _of<Contract>({ plugin: this.resourceType.plugin.name, resourceType: this.resourceType.name, name: this.name, baseName: this.name, state: 'stopped', avmPath: this.avmPath, script: this.script, abi: this.abi, hasStorage: this.hasStorage, hasDynamicInvoke: this.hasDynamicInvoke, payable: this.payable, }), ), shareReplay(1), ); this.update$.next(); } public async destroy(): Promise<void> { // do nothing } public delete(_options: ContractResourceOptions): TaskList { return new TaskList({ tasks: [ { title: 'Clean local files', task: async () => { await fs.remove(this.dataPath); }, }, ], }); } public start(_options: ContractResourceOptions): TaskList { throw new Error('Cannot be started'); } public stop(_options: ContractResourceOptions): TaskList { throw new Error('Cannot be stopped'); } public getDebug(): DescribeTable { return [ ['Data Path', this.dataPath], ['AVM Path', this.avmPath], ['ABI Path', this.abiPath], ['Config Path', this.configPath], ['Storage', this.hasStorage ? 'Yes' : 'No'], ['Dynamic Invoke', this.hasDynamicInvoke ? 'Yes' : 'No'], ['Payable', this.payable ? 'Yes' : 'No'], ['ABI', JSON.stringify(this.abi, undefined, 2)], ]; } }