Skip to content

Commit

Permalink
feat(contract): add ability to compile by path in node.js
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Dec 16, 2022
1 parent 25c63e0 commit 74867b7
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 24 deletions.
14 changes: 10 additions & 4 deletions src/contract/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ class Contract<M extends ContractMethodsBase> {
bytecode,
aci,
address,
sourceCodePath,
sourceCode,
fileSystem,
validateBytecode,
Expand All @@ -495,10 +496,14 @@ class Contract<M extends ContractMethodsBase> {
address?: Encoded.ContractAddress | AensName;
},
): Promise<ContractWithMethods<M>> {
if (aci == null && sourceCode != null && onCompiler != null) {
const res = await onCompiler.compileBySourceCode(sourceCode, fileSystem);
aci = res.aci;
bytecode ??= res.bytecode;
if (aci == null && onCompiler != null) {
let res;
if (sourceCodePath != null) res = await onCompiler.compile(sourceCodePath);
if (sourceCode != null) res = await onCompiler.compileBySourceCode(sourceCode, fileSystem);
if (res != null) {
aci = res.aci;
bytecode ??= res.bytecode;
}
}
if (aci == null) throw new MissingContractDefError();

Expand Down Expand Up @@ -550,6 +555,7 @@ class Contract<M extends ContractMethodsBase> {
bytecode?: Encoded.ContractBytearray;
aci: Aci;
address?: Encoded.ContractAddress;
sourceCodePath?: Parameters<CompilerBase['compile']>[0];
sourceCode?: Parameters<CompilerBase['compileBySourceCode']>[0];
fileSystem?: Parameters<CompilerBase['compileBySourceCode']>[1];
} & Parameters<Contract<M>['$deploy']>[1]) {
Expand Down
20 changes: 20 additions & 0 deletions src/contract/compiler/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export interface Aci extends BaseAci {
* A base class for all compiler implementations
*/
export default abstract class CompilerBase {
/**
* Compile contract by contract's path
* Available only in Node.js
* @param path - Path to contract source code
* @returns ACI and bytecode
*/
abstract compile(path: string): Promise<{
bytecode: Encoded.ContractBytearray;
aci: Aci;
}>;

/**
* Compile contract by contract's source code
* @param sourceCode - Contract source code as string
Expand All @@ -48,6 +59,15 @@ export default abstract class CompilerBase {
aci: Aci;
}>;

/**
* Verify that a contract bytecode is the result of compiling the given source code
* Available only in Node.js
* @param bytecode - Contract bytecode to verify
* @param path - Path to contract source code
* @returns ACI and bytecode
*/
abstract validate(bytecode: Encoded.ContractBytearray, path: string): Promise<boolean>;

/**
* Verify that a contract bytecode is the result of compiling the given source code
* @param bytecode - Contract bytecode to verify
Expand Down
14 changes: 14 additions & 0 deletions src/contract/compiler/Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
import { RestError } from '@azure/core-rest-pipeline';
import { readFile } from 'fs/promises';
import {
Compiler as CompilerApi,
ErrorModel,
Expand All @@ -24,6 +25,7 @@ import { genErrorFormatterPolicy, genVersionCheckPolicy } from '../../utils/auto
import CompilerBase, { Aci } from './Base';
import { Encoded } from '../../utils/encoder';
import { CompilerError } from '../../utils/errors';
import getFileSystem from './getFileSystem';

type GeneralCompilerError = ErrorModel & {
info?: object;
Expand Down Expand Up @@ -95,6 +97,12 @@ export default class CompilerHttp extends CompilerBase {
}
}

async compile(path: string): Promise<{ bytecode: Encoded.ContractBytearray; aci: Aci }> {
const fileSystem = await getFileSystem(path);
const sourceCode = await readFile(path, 'utf8');
return this.compileBySourceCode(sourceCode, fileSystem);
}

async validateBySourceCode(
bytecode: Encoded.ContractBytearray,
sourceCode: string,
Expand All @@ -108,6 +116,12 @@ export default class CompilerHttp extends CompilerBase {
}
}

async validate(bytecode: Encoded.ContractBytearray, path: string): Promise<boolean> {
const fileSystem = await getFileSystem(path);
const sourceCode = await readFile(path, 'utf8');
return this.validateBySourceCode(bytecode, sourceCode, fileSystem);
}

async version(): Promise<string> {
return (await this.api.version()).version;
}
Expand Down
42 changes: 42 additions & 0 deletions src/contract/compiler/getFileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFile } from 'fs/promises';
import { dirname, resolve, basename } from 'path';
import { InternalError } from '../../utils/errors';

const defaultIncludes = [
'List.aes', 'Option.aes', 'String.aes',
'Func.aes', 'Pair.aes', 'Triple.aes',
'BLS12_381.aes', 'Frac.aes', 'Set.aes',
'Bitwise.aes',
];
const includeRegExp = /^include\s*"([\w/.-]+)"/mi;
const includesRegExp = new RegExp(includeRegExp.source, `${includeRegExp.flags}g`);

async function getFileSystemRec(
root: string,
relative: string,
): Promise<Record<string, string>> {
const sourceCode = await readFile(resolve(root, relative), 'utf8');
const filesystem: Record<string, string> = {};
await Promise.all((sourceCode.match(includesRegExp) ?? [])
.map((include) => {
const m = include.match(includeRegExp);
if (m?.length !== 2) throw new InternalError('Unexpected match length');
return m[1];
})
.filter((include) => !defaultIncludes.includes(include))
.map(async (include) => {
const includePath = resolve(root, include);
filesystem[include] = await readFile(includePath, 'utf8');
Object.assign(filesystem, await getFileSystemRec(root, include));
}));
return filesystem;
}

/**
* Reads all files included in the provided contract
* Available only in Node.js
* @param path - a path to the main contract source code
*/
export default async function getFileSystem(path: string): Promise<Record<string, string>> {
return getFileSystemRec(dirname(path), basename(path));
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { default as AccountLedger } from './account/Ledger';
export { default as AccountLedgerFactory } from './account/LedgerFactory';
export { default as CompilerBase } from './contract/compiler/Base';
export { default as CompilerHttp } from './contract/compiler/Http';
export { default as getFileSystem } from './contract/compiler/getFileSystem';
export { default as Channel } from './channel/Contract';

export { default as connectionProxy } from './aepp-wallet-communication/connection-proxy';
Expand Down
71 changes: 51 additions & 20 deletions test/integration/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,42 @@
*/
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { readFile } from 'fs/promises';
import { compilerUrl, ignoreVersion } from '.';
import { CompilerError, CompilerHttp } from '../../src';
import { CompilerError, CompilerHttp, getFileSystem } from '../../src';
import { Encoded } from '../../src/utils/encoder';

const identitySourceCode = `
contract Identity =
entrypoint getArg(x : int) = x
`;

describe('Sophia Compiler', () => {
const compiler = new CompilerHttp(compilerUrl, { ignoreVersion });
let identityBytecode: Encoded.ContractBytearray;
const testSourceCodePath = './test/integration/contracts/Includes.aes';
let testSourceCode: string;
let testFileSystem: Record<string, string>;
const testBytecode = 'cb_+QEGRgOg7BH1sCv+p2IrS0Pn3/i6AfE8lOGUuC71lLPn6mbUm9PAuNm4cv4AWolkAjcCBwcHFBQAAgD+RNZEHwA3ADcAGg6CPwEDP/5Nt4A5AjcCBwcHDAECDAEABAMRAFqJZP6SiyA2ADcBBwcMAwgMAQAEAxFNt4A5/pSgnxIANwF3BwwBAAQDEarAwob+qsDChgI3AXcHPgQAALhgLwYRAFqJZD0uU3VibGlicmFyeS5zdW0RRNZEHxFpbml0EU23gDkxLkxpYnJhcnkuc3VtEZKLIDYRdGVzdBGUoJ8SJWdldExlbmd0aBGqwMKGOS5TdHJpbmcubGVuZ3Rogi8AhTcuMC4xAGzn9fM=';
const testBytecode2 = 'cb_+GhGA6BgYgXqYB9ctBcQ8mJ0+we5OXhb9PpsSQWP2DhPx9obn8C4O57+RNZEHwA3ADcAGg6CPwEDP/6AeCCSADcBd3cBAQCYLwIRRNZEHxFpbml0EYB4IJIZZ2V0QXJngi8AhTcuMC4xAMXqWXc=';

before(async () => {
testSourceCode = await readFile(testSourceCodePath, 'utf8');
testFileSystem = await getFileSystem(testSourceCodePath);
});

it('returns version', async () => {
expect(await compiler.version()).to.be.equal('7.0.1');
});

it('compiles and generates aci', async () => {
const { bytecode, aci } = await compiler.compileBySourceCode(identitySourceCode);
expect(bytecode).to.satisfy((b: string) => b.startsWith('cb_'));
it('compiles and generates aci by path', async () => {
const { bytecode, aci } = await compiler.compile(testSourceCodePath);
expect(bytecode).to.equal(testBytecode);
expect(aci).to.have.property('encodedAci');
expect(aci).to.have.property('externalEncodedAci');
expect(aci).to.have.property('interface');
});

it('compiles and generates aci by source code', async () => {
const { bytecode, aci } = await compiler.compileBySourceCode(testSourceCode, testFileSystem);
expect(bytecode).to.equal(testBytecode);
expect(aci).to.have.property('encodedAci');
expect(aci).to.have.property('externalEncodedAci');
expect(aci).to.have.property('interface');
identityBytecode = bytecode;
});

it('throws clear exception if compile broken contract', async () => {
Expand All @@ -59,16 +71,21 @@ describe('Sophia Compiler', () => {
);
});

it('validates bytecode', async () => {
expect(await compiler.validateBySourceCode(identityBytecode, identitySourceCode))
it('validates bytecode by path', async () => {
expect(await compiler.validate(testBytecode, testSourceCodePath))
.to.be.equal(true);
const { bytecode } = await compiler.compileBySourceCode(
'contract Identity =\n'
+ ' entrypoint getArg(x : string) = x',
);
expect(await compiler.validateBySourceCode(bytecode, identitySourceCode)).to.be.equal(false);
const invalidBytecode = `${bytecode}test` as Encoded.ContractBytearray;
expect(await compiler.validateBySourceCode(invalidBytecode, identitySourceCode))
expect(await compiler.validate(testBytecode2, testSourceCodePath)).to.be.equal(false);
const invalidBytecode = `${testBytecode2}test` as Encoded.ContractBytearray;
expect(await compiler.validate(invalidBytecode, testSourceCodePath))
.to.be.equal(false);
});

it('validates bytecode by source code', async () => {
expect(await compiler.validateBySourceCode(testBytecode, testSourceCode, testFileSystem))
.to.be.equal(true);
expect(await compiler.validateBySourceCode(testBytecode2, testSourceCode)).to.be.equal(false);
const invalidBytecode = `${testBytecode2}test` as Encoded.ContractBytearray;
expect(await compiler.validateBySourceCode(invalidBytecode, testSourceCode))
.to.be.equal(false);
});

Expand All @@ -77,4 +94,18 @@ describe('Sophia Compiler', () => {
await expect(c.compileBySourceCode('test'))
.to.be.rejectedWith('getaddrinfo ENOTFOUND compiler.aepps.comas');
});

describe('getFileSystem', () => {
it('reads file system', async () => {
expect(await getFileSystem('./test/integration/contracts/Includes.aes')).to.be.eql({
'./lib/Library.aes':
'include"lib/Sublibrary.aes"\n\n'
+ 'namespace Library =\n'
+ ' function sum(x: int, y: int): int = Sublibrary.sum(x, y)\n',
'lib/Sublibrary.aes':
'namespace Sublibrary =\n'
+ ' function sum(x: int, y: int): int = x + y\n',
});
});
});
});
7 changes: 7 additions & 0 deletions test/integration/contract-aci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ describe('Contract instance', () => {
testContractAddress = deployInfo.address;
});

it('can be deployed by source code path', async () => {
const contract = await aeSdk.initializeContract<{}>({
sourceCodePath: './test/integration/contracts/Includes.aes',
});
await contract.$deploy([]);
});

it('calls', async () => {
expect((await testContract.intFn(2)).decodedResult).to.be.equal(2n);
});
Expand Down
7 changes: 7 additions & 0 deletions test/integration/contracts/Includes.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include "String.aes"
include "./lib/Library.aes"

contract Includes =
entrypoint test(x: int): int = Library.sum(x, 4)

entrypoint getLength(x: string): int = String.length(x)
4 changes: 4 additions & 0 deletions test/integration/contracts/lib/Library.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include"lib/Sublibrary.aes"

namespace Library =
function sum(x: int, y: int): int = Sublibrary.sum(x, y)
2 changes: 2 additions & 0 deletions test/integration/contracts/lib/Sublibrary.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace Sublibrary =
function sum(x: int, y: int): int = x + y

0 comments on commit 74867b7

Please sign in to comment.