@neo-one/server-plugin-compiler
Version:
NEO•ONE Server compiler plugin.
267 lines (237 loc) • 8.04 kB
text/typescript
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)],
];
}
}