Skip to content

Commit

Permalink
Merge pull request #1847 from aeternity/feature/generate-aci
Browse files Browse the repository at this point in the history
feat(compiler): add `generateAci`, `generateAciBySourceCode`
  • Loading branch information
davidyuk authored Jul 7, 2023
2 parents c8d2d87 + 22cc31b commit b35274b
Show file tree
Hide file tree
Showing 17 changed files with 194 additions and 48 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ services:
- ./docker/accounts_test.json:/home/aeternity/node/data/aecore/.genesis/accounts_test.json

compiler:
image: aeternity/aesophia_http:v7.2.0
image: aeternity/aesophia_http:v7.4.0
hostname: compiler
ports: ["3080:3080"]
26 changes: 26 additions & 0 deletions src/contract/compiler/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ export default abstract class CompilerBase {
aci: Aci;
}>;

/**
* Generate contract's ACI by contract's path
* Available only in Node.js
* @param path - Path to contract source code
* @returns ACI
*/
abstract generateAci(path: string): Promise<Aci>;

/**
* Generate contract's ACI by contract's source code
* @param sourceCode - Contract source code as string
* @param fileSystem - A map of contract filename to the corresponding contract source code to
* include into the main contract
* @example
* ```js
* {
* 'library.aes': 'namespace TestLib =\n function sum(x: int, y: int) : int = x + y'
* }
* ```
* @returns ACI
*/
abstract generateAciBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): Promise<Aci>;

/**
* Verify that a contract bytecode is the result of compiling the given source code
* Available only in Node.js
Expand Down
26 changes: 24 additions & 2 deletions src/contract/compiler/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class CompilerCli extends CompilerBase {
this.#path = compilerPath;
if (ignoreVersion !== true) {
this.#ensureCompatibleVersion = this.version().then((version) => {
const versions = [version, '7.0.1', '8.0.0'] as const;
const versions = [version, '7.2.1', '8.0.0'] as const;
if (!semverSatisfies(...versions)) throw new UnsupportedVersionError('compiler', ...versions);
});
}
Expand Down Expand Up @@ -79,7 +79,7 @@ export default class CompilerCli extends CompilerBase {
]);
return {
bytecode: bytecode.trimEnd() as Encoded.ContractBytearray,
aci: aci as Aci,
aci,
};
} catch (error) {
ensureError(error);
Expand All @@ -99,6 +99,28 @@ export default class CompilerCli extends CompilerBase {
}
}

async generateAci(path: string): Promise<Aci> {
await this.#ensureCompatibleVersion;
try {
return JSON.parse(await this.#run('--no_code', '--create_json_aci', path));
} catch (error) {
ensureError(error);
throw new CompilerError(error.message);
}
}

async generateAciBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): Promise<Aci> {
const tmp = await CompilerCli.#saveContractToTmpDir(sourceCode, fileSystem);
try {
return await this.generateAci(tmp);
} finally {
await rm(dirname(tmp), { recursive: true });
}
}

async validate(bytecode: Encoded.ContractBytearray, path: string): Promise<boolean> {
await this.#ensureCompatibleVersion;
try {
Expand Down
23 changes: 21 additions & 2 deletions src/contract/compiler/Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class CompilerHttp extends CompilerBase {
const versionPromise = this.api.apiVersion()
.then(({ apiVersion }) => apiVersion, (error) => error);
this.api.pipeline.addPolicy(
genVersionCheckPolicy('compiler', '/api-version', versionPromise, '7.1.1', '8.0.0'),
genVersionCheckPolicy('compiler', '/api-version', versionPromise, '7.3.0', '8.0.0'),
);
}
}
Expand All @@ -70,7 +70,7 @@ export default class CompilerHttp extends CompilerBase {
return res as { bytecode: Encoded.ContractBytearray; aci: Aci };
} catch (error) {
if (error instanceof RestError && error.statusCode === 400) {
throw new CompilerError(error.message.replace(/^aci error:/, 'compile error:'));
throw new CompilerError(error.message);
}
throw error;
}
Expand All @@ -81,6 +81,25 @@ export default class CompilerHttp extends CompilerBase {
throw new NotImplementedError('File system access, use CompilerHttpNode instead');
}

async generateAciBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): Promise<Aci> {
try {
return await this.api.generateACI({ code: sourceCode, options: { fileSystem } });
} catch (error) {
if (error instanceof RestError && error.statusCode === 400) {
throw new CompilerError(error.message);
}
throw error;
}
}

// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
async generateAci(path: string): Promise<Aci> {
throw new NotImplementedError('File system access, use CompilerHttpNode instead');
}

async validateBySourceCode(
bytecode: Encoded.ContractBytearray,
sourceCode: string,
Expand Down
6 changes: 6 additions & 0 deletions src/contract/compiler/HttpNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export default class CompilerHttpNode extends HttpBrowser {
return this.compileBySourceCode(sourceCode, fileSystem);
}

override async generateAci(path: string): Promise<Aci> {
const fileSystem = await getFileSystem(path);
const sourceCode = await readFile(path, 'utf8');
return this.generateAciBySourceCode(sourceCode, fileSystem);
}

override async validate(bytecode: Encoded.ContractBytearray, path: string): Promise<boolean> {
const fileSystem = await getFileSystem(path);
const sourceCode = await readFile(path, 'utf8');
Expand Down
6 changes: 6 additions & 0 deletions test/integration/Middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ describe('Middleware API', () => {
const expectedRes: typeof res = {
data: [{
blockHash: 'mh_LAo6Cg6d8LGDpxJ3se2aGJZbCubDZyC6GonHK58MKiW4a4LWb',
// @ts-expect-error https://github.com/aeternity/ae_mdw/issues/1454
block_time: 1684995426848,
height: 779178,
payload: {
block_hash: 'mh_LAo6Cg6d8LGDpxJ3se2aGJZbCubDZyC6GonHK58MKiW4a4LWb',
Expand Down Expand Up @@ -73,6 +75,8 @@ describe('Middleware API', () => {
type: 'GAMetaTxEvent',
}, {
blockHash: 'mh_2R1PVwTNP3Jha7oRby9Me3SRBP4R9he6RMH6eCCJGyVBHAzy5f',
// @ts-expect-error https://github.com/aeternity/ae_mdw/issues/1454
block_time: 1684995366595,
height: 779178,
payload: {
block_hash: 'mh_2R1PVwTNP3Jha7oRby9Me3SRBP4R9he6RMH6eCCJGyVBHAzy5f',
Expand Down Expand Up @@ -111,6 +115,8 @@ describe('Middleware API', () => {
type: 'GAAttachTxEvent',
}, {
blockHash: 'mh_25snWYwTkU1xjPCcH592XVNzL894qSpF4yqnt8tABKGEVm6nSz',
// @ts-expect-error https://github.com/aeternity/ae_mdw/issues/1454
block_time: 1684995336526,
height: 779178,
payload: {
block_hash: 'mh_25snWYwTkU1xjPCcH592XVNzL894qSpF4yqnt8tABKGEVm6nSz',
Expand Down
82 changes: 63 additions & 19 deletions test/integration/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { describe, it } from 'mocha';
import { readFile } from 'fs/promises';
import { compilerUrl, ignoreVersion } from '.';
import inclAci from './contracts/Includes.json';
import {
CompilerBase, CompilerHttpNode, CompilerCli, CompilerError, getFileSystem, Encoded,
} from '../../src';
Expand All @@ -10,35 +11,69 @@ function testCompiler(compiler: CompilerBase): void {
const inclSourceCodePath = './test/integration/contracts/Includes.aes';
let inclSourceCode: string;
let inclFileSystem: Record<string, string>;
const inclBytecode = 'cb_+QEGRgOg7BH1sCv+p2IrS0Pn3/i6AfE8lOGUuC71lLPn6mbUm9PAuNm4cv4AWolkAjcCBwcHFBQAAgD+RNZEHwA3ADcAGg6CPwEDP/5Nt4A5AjcCBwcHDAECDAEABAMRAFqJZP6SiyA2ADcBBwcMAwgMAQAEAxFNt4A5/pSgnxIANwF3BwwBAAQDEarAwob+qsDChgI3AXcHPgQAALhgLwYRAFqJZD0uU3VibGlicmFyeS5zdW0RRNZEHxFpbml0EU23gDkxLkxpYnJhcnkuc3VtEZKLIDYRdGVzdBGUoJ8SJWdldExlbmd0aBGqwMKGOS5TdHJpbmcubGVuZ3Rogi8AhTcuMS4wAGHgFTw=';
// TODO: use Includes.aes after fixing https://github.com/aeternity/aesophia_cli/issues/74
const incSourceCodePath = './test/integration/contracts/Increment.aes';
let incSourceCode: string;
const incBytecode = 'cb_+G1GA6Cln3BxyOo1iNITGseMS58ZfBbRNB0x8Ix7Bh54qZlSOcC4QKD+Er1R0wA3AQcHFDQAAgD+RNZEHwA3ADcAGg6CPwEDP5svAhESvVHTJWluY3JlbWVudBFE1kQfEWluaXSCLwCFNy4wLjEAfImpuQ==';
const inclBytecode = 'cb_+QEGRgOg7BH1sCv+p2IrS0Pn3/i6AfE8lOGUuC71lLPn6mbUm9PAuNm4cv4AWolkAjcCBwcHFBQAAgD+RNZEHwA3ADcAGg6CPwEDP/5Nt4A5AjcCBwcHDAECDAEABAMRAFqJZP6SiyA2ADcBBwcMAwgMAQAEAxFNt4A5/pSgnxIANwF3BwwBAAQDEarAwob+qsDChgI3AXcHPgQAALhgLwYRAFqJZD0uU3VibGlicmFyeS5zdW0RRNZEHxFpbml0EU23gDkxLkxpYnJhcnkuc3VtEZKLIDYRdGVzdBGUoJ8SJWdldExlbmd0aBGqwMKGOS5TdHJpbmcubGVuZ3Rogi8AhTcuMi4xAFw7b7s=';
const testBytecode = 'cb_+GhGA6BgYgXqYB9ctBcQ8mJ0+we5OXhb9PpsSQWP2DhPx9obn8C4O57+RNZEHwA3ADcAGg6CPwEDP/6AeCCSADcBd3cBAQCYLwIRRNZEHxFpbml0EYB4IJIZZ2V0QXJngi8AhTcuMC4xAMXqWXc=';

const interfaceSourceCodePath = './test/integration/contracts/Interface.aes';
let interfaceSourceCode: string;
let interfaceFileSystem: Record<string, string>;
const interfaceAci = [
{ namespace: { name: 'ListInternal', typedefs: [] } },
{ namespace: { name: 'List', typedefs: [] } },
{ namespace: { name: 'String', typedefs: [] } },
{
contract: {
functions: [{
arguments: [{ name: '_1', type: 'int' }],
name: 'decrement',
payable: false,
returns: 'int',
stateful: false,
}],
kind: 'contract_child',
name: 'Decrement',
payable: false,
typedefs: [],
},
},
{
contract: {
functions: [{
arguments: [{ name: '_1', type: 'int' }],
name: 'increment',
payable: false,
returns: 'int',
stateful: false,
}],
kind: 'contract_main',
name: 'Increment',
payable: false,
typedefs: [],
},
},
];

before(async () => {
inclSourceCode = await readFile(inclSourceCodePath, 'utf8');
inclFileSystem = await getFileSystem(inclSourceCodePath);
incSourceCode = await readFile(incSourceCodePath, 'utf8');
interfaceSourceCode = await readFile(interfaceSourceCodePath, 'utf8');
interfaceFileSystem = await getFileSystem(interfaceSourceCodePath);
});

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

it('compiles and generates aci by path', async () => {
const { bytecode, aci } = await compiler.compile(inclSourceCodePath);
expect(bytecode).to.equal(inclBytecode);
expect(aci).to.have.length(6);
expect(aci[aci.length - 1]).to.have.property('contract');
expect(aci).to.eql(inclAci);
});

it('compiles and generates aci by source code', async () => {
const { bytecode, aci } = await compiler.compileBySourceCode(inclSourceCode, inclFileSystem);
expect(bytecode).to.equal(inclBytecode);
expect(aci).to.have.length(6);
expect(aci[aci.length - 1]).to.have.property('contract');
expect(aci).to.eql(inclAci);
});

it('throws clear exception if compile broken contract', async () => {
Expand All @@ -60,21 +95,30 @@ function testCompiler(compiler: CompilerBase): void {
);
});

it('generates aci by path', async () => {
const aci = await compiler.generateAci(interfaceSourceCodePath);
expect(aci).to.eql(interfaceAci);
});

it('generates aci by source code', async () => {
const aci = await compiler.generateAciBySourceCode(interfaceSourceCode, interfaceFileSystem);
expect(aci).to.eql(interfaceAci);
});

it('validates bytecode by path', async () => {
expect(await compiler.validate(incBytecode, incSourceCodePath))
.to.be.equal(true);
expect(await compiler.validate(testBytecode, incSourceCodePath)).to.be.equal(false);
expect(await compiler.validate(inclBytecode, inclSourceCodePath)).to.be.equal(true);
expect(await compiler.validate(testBytecode, inclSourceCodePath)).to.be.equal(false);
const invalidBytecode = `${testBytecode}test` as Encoded.ContractBytearray;
expect(await compiler.validate(invalidBytecode, incSourceCodePath))
.to.be.equal(false);
expect(await compiler.validate(invalidBytecode, inclSourceCodePath)).to.be.equal(false);
});

it('validates bytecode by source code', async () => {
expect(await compiler.validateBySourceCode(incBytecode, incSourceCode))
expect(await compiler.validateBySourceCode(inclBytecode, inclSourceCode, inclFileSystem))
.to.be.equal(true);
expect(await compiler.validateBySourceCode(testBytecode, incSourceCode)).to.be.equal(false);
expect(await compiler.validateBySourceCode(testBytecode, inclSourceCode, inclFileSystem))
.to.be.equal(false);
const invalidBytecode = `${testBytecode}test` as Encoded.ContractBytearray;
expect(await compiler.validateBySourceCode(invalidBytecode, incSourceCode))
expect(await compiler.validateBySourceCode(invalidBytecode, inclSourceCode, inclFileSystem))
.to.be.equal(false);
});
}
Expand Down
20 changes: 3 additions & 17 deletions test/integration/contract-aci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../utils';
import { Aci } from '../../src/contract/compiler/Base';
import { ContractCallObject } from '../../src/contract/Contract';
import includesAci from './contracts/Includes.json';

const identityContractSourceCode = `
contract Identity =
Expand Down Expand Up @@ -207,23 +208,8 @@ describe('Contract instance', () => {

it('compiles contract by sourceCodePath', async () => {
const ctr = await aeSdk.initializeContract({
aci: [{
contract: {
functions: [{
arguments: [{ name: 'x', type: 'int' }],
name: 'increment',
payable: false,
returns: 'int',
stateful: false,
},
],
kind: 'contract_main',
name: 'Increment',
payable: false,
typedefs: [],
},
}],
sourceCodePath: './test/integration/contracts/Increment.aes',
aci: includesAci,
sourceCodePath: './test/integration/contracts/Includes.aes',
});
expect(ctr.$options.bytecode).to.equal(undefined);
expect(await ctr.$compile()).to.satisfy((b: string) => b.startsWith('cb_'));
Expand Down
31 changes: 31 additions & 0 deletions test/integration/contracts/Includes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[{
"namespace": { "name": "ListInternal", "typedefs": [] }
}, {
"namespace": { "name": "List", "typedefs": [] }
}, {
"namespace": { "name": "String", "typedefs": [] }
}, {
"namespace": { "name": "Sublibrary", "typedefs": [] }
}, {
"namespace": { "name": "Library", "typedefs": [] }
}, {
"contract": {
"functions": [{
"arguments": [{ "name": "x", "type": "int" }],
"name": "test",
"payable": false,
"returns": "int",
"stateful": false
}, {
"arguments": [{ "name": "x", "type": "string" }],
"name": "getLength",
"payable": false,
"returns": "int",
"stateful": false
}],
"kind": "contract_main",
"name": "Includes",
"payable": false,
"typedefs": []
}
}]
2 changes: 0 additions & 2 deletions test/integration/contracts/Increment.aes

This file was deleted.

5 changes: 5 additions & 0 deletions test/integration/contracts/Interface.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include "String.aes"
include "./lib/DecrementInterface.aes"

main contract Increment =
entrypoint increment: (int) => int
2 changes: 2 additions & 0 deletions test/integration/contracts/lib/DecrementInterface.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
contract Decrement =
entrypoint decrement: (int) => int
2 changes: 1 addition & 1 deletion test/integration/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('Transaction', () => {
}),
], [
'contract create',
'tx_+LAqAaEBhAyXS5cWR3ZFS6EZ2E7cTWBYqN7JK27cV4qy0wtMQgABuGr4aEYDoKEijZbj/w2AeiWwAbldusME5pm3ZgPuomnZ3TbUbYgrwLg7nv5E1kQfADcANwAaDoI/AQM//oB4IJIANwEHBwEBAJgvAhFE1kQfEWluaXQRgHggkhlnZXRBcmeCLwCFNy4xLjAAgwcAA4ZHcyzkwAAAAACDTEtAhDuaygCHKxFE1kQfP0tdwp4=',
'tx_+LAqAaEBhAyXS5cWR3ZFS6EZ2E7cTWBYqN7JK27cV4qy0wtMQgABuGr4aEYDoKEijZbj/w2AeiWwAbldusME5pm3ZgPuomnZ3TbUbYgrwLg7nv5E1kQfADcANwAaDoI/AQM//oB4IJIANwEHBwEBAJgvAhFE1kQfEWluaXQRgHggkhlnZXRBcmeCLwCFNy4yLjEAgwcAA4ZHcyzkwAAAAACDTEtAhDuaygCHKxFE1kQfP7cARy4=',
async () => aeSdk.buildTx({
tag: Tag.ContractCreateTx,
nonce,
Expand Down
2 changes: 1 addition & 1 deletion tooling/autorest/compiler-prepare.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'fs';

const swaggerUrl = 'https://raw.githubusercontent.com/aeternity/aesophia_http/v7.1.1/config/swagger.yaml';
const swaggerUrl = 'https://raw.githubusercontent.com/aeternity/aesophia_http/v7.4.0/config/swagger.yaml';

const response = await fetch(swaggerUrl);
console.assert(response.status === 200, 'Invalid response code', response.status);
Expand Down
Loading

0 comments on commit b35274b

Please sign in to comment.