Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler): add generateAci, generateAciBySourceCode #1847

Merged
merged 5 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
thepiwo marked this conversation as resolved.
Show resolved Hide resolved
): 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);
thepiwo marked this conversation as resolved.
Show resolved Hide resolved
} 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 } });
thepiwo marked this conversation as resolved.
Show resolved Hide resolved
} 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