Skip to content

Commit

Permalink
Add Cairo compilation tests (#361)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Nordelo <eric.nordelo39@gmail.com>
  • Loading branch information
ericglau and ericnordelo authored Jun 20, 2024
1 parent 5cc1451 commit 5ddbf45
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 19 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/cairo-compilation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Compile Cairo contracts

on:
pull_request:
branches:
- master
push:
branches:
- master

jobs:
generate_and_compile:
name: Compile Cairo contracts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Generate contracts
run: yarn workspace @openzeppelin/wizard-cairo update_scarb_project
- name: Extract scarb version
run: |
SCARB_VERSION=$(grep 'scarb-version = ' packages/core-cairo/test_project/Scarb.toml | sed 's/scarb-version = "\(.*\)"/\1/')
echo "SCARB_VERSION=$SCARB_VERSION" >> $GITHUB_ENV
- uses: software-mansion/setup-scarb@v1
with:
scarb-version: ${{ env.SCARB_VERSION }}
- name: Compile contracts
working-directory: ./packages/core-cairo/test_project
run: scarb build
1 change: 1 addition & 0 deletions packages/core-cairo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
"scripts": {
"prepare": "tsc",
"update_scarb_project": "ts-node src/scripts/update-scarb-project.ts",
"prepublish": "rimraf dist *.tsbuildinfo",
"test": "ava",
"test:watch": "ava --watch",
Expand Down
23 changes: 23 additions & 0 deletions packages/core-cairo/src/generate/erc1155.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ERC1155Options } from '../erc1155';
import { accessOptions } from '../set-access-control';
import { infoOptions } from '../set-info';
import { upgradeableOptions } from '../set-upgradeable';
import { generateAlternatives } from './alternatives';

const booleans = [true, false];

const blueprint = {
name: ['MyToken'],
baseUri: ['https://example.com/'],
burnable: booleans,
pausable: booleans,
mintable: booleans,
updatableUri: booleans,
access: accessOptions,
upgradeable: upgradeableOptions,
info: infoOptions,
};

export function* generateERC1155Options(): Generator<Required<ERC1155Options>> {
yield* generateAlternatives(blueprint);
}
54 changes: 39 additions & 15 deletions packages/core-cairo/src/generate/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,41 @@ import crypto from 'crypto';

import { generateERC20Options } from './erc20';
import { generateERC721Options } from './erc721';
import { generateERC1155Options } from './erc1155';
import { generateCustomOptions } from './custom';
import { buildGeneric, GenericOptions } from '../build-generic';
import { buildGeneric, GenericOptions, KindedOptions } from '../build-generic';
import { printContract } from '../print';
import { OptionsError } from '../error';
import { findCover } from '../utils/find-cover';
import type { Contract } from '../contract';

type Subset = 'all' | 'minimal-cover';

export function* generateOptions(): Generator<GenericOptions> {
for (const kindOpts of generateERC20Options()) {
yield { kind: 'ERC20', ...kindOpts };
type Kind = keyof KindedOptions;

export function* generateOptions(kind?: Kind): Generator<GenericOptions> {
if (!kind || kind === 'ERC20') {
for (const kindOpts of generateERC20Options()) {
yield { kind: 'ERC20', ...kindOpts };
}
}

for (const kindOpts of generateERC721Options()) {
yield { kind: 'ERC721', ...kindOpts };
if (!kind || kind === 'ERC721') {
for (const kindOpts of generateERC721Options()) {
yield { kind: 'ERC721', ...kindOpts };
}
}

for (const kindOpts of generateCustomOptions()) {
yield { kind: 'Custom', ...kindOpts };
if (!kind || kind === 'ERC1155') {
for (const kindOpts of generateERC1155Options()) {
yield { kind: 'ERC1155', ...kindOpts };
}
}

if (!kind || kind === 'Custom') {
for (const kindOpts of generateCustomOptions()) {
yield { kind: 'Custom', ...kindOpts };
}
}
}

Expand All @@ -37,10 +52,10 @@ interface GeneratedSource extends GeneratedContract {
source: string;
}

function generateContractSubset(subset: Subset): GeneratedContract[] {
function generateContractSubset(subset: Subset, kind?: Kind): GeneratedContract[] {
const contracts = [];

for (const options of generateOptions()) {
for (const options of generateOptions(kind)) {
const id = crypto
.createHash('sha1')
.update(JSON.stringify(options))
Expand Down Expand Up @@ -70,17 +85,26 @@ function generateContractSubset(subset: Subset): GeneratedContract[] {
}
}

export function* generateSources(subset: Subset): Generator<GeneratedSource> {
for (const c of generateContractSubset(subset)) {
export function* generateSources(subset: Subset, uniqueName?: boolean, kind?: Kind): Generator<GeneratedSource> {
let counter = 1;
for (const c of generateContractSubset(subset, kind)) {
if (uniqueName) {
c.contract.name = `Contract${counter++}`;
}
const source = printContract(c.contract);
yield { ...c, source };
}
}

export async function writeGeneratedSources(dir: string, subset: Subset): Promise<void> {
export async function writeGeneratedSources(dir: string, subset: Subset, uniqueName?: boolean, kind?: Kind): Promise<string[]> {
await fs.mkdir(dir, { recursive: true });
let contractNames = [];

for (const { id, source } of generateSources(subset)) {
await fs.writeFile(path.format({ dir, name: id, ext: '.cairo' }), source);
for (const { id, contract, source } of generateSources(subset, uniqueName, kind)) {
const name = uniqueName ? contract.name : id;
await fs.writeFile(path.format({ dir, name, ext: '.cairo' }), source);
contractNames.push(name);
}

return contractNames;
}
41 changes: 41 additions & 0 deletions packages/core-cairo/src/scripts/update-scarb-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { promises as fs } from 'fs';
import path from 'path';

import { writeGeneratedSources } from '../generate/sources';
import { contractsVersionTag, edition, cairoVersion, scarbVersion } from '../utils/version';

export async function updateScarbProject() {
const generatedSourcesPath = path.join('test_project', 'src');
await fs.rm(generatedSourcesPath, { force: true, recursive: true });

// Generate the contracts source code
const contractNames = await writeGeneratedSources(generatedSourcesPath, 'all', true);

// Generate lib.cairo file
writeLibCairo(contractNames);

// Update Scarb.toml
updateScarbToml();
}

async function writeLibCairo(contractNames: string[]) {
const libCairoPath = path.join('test_project/src', 'lib.cairo');
const libCairo = contractNames.map(name => `pub mod ${name};\n`).join('');
await fs.writeFile(libCairoPath, libCairo);
}

async function updateScarbToml() {
const scarbTomlPath = path.join('test_project', 'Scarb.toml');

let currentContent = await fs.readFile(scarbTomlPath, 'utf8');
let updatedContent = currentContent
.replace(/edition = "\w+"/, `edition = "${edition}"`)
.replace(/cairo-version = "\d+\.\d+\.\d+"/, `cairo-version = "${cairoVersion}"`)
.replace(/scarb-version = "\d+\.\d+\.\d+"/, `scarb-version = "${scarbVersion}"`)
.replace(/starknet = "\d+\.\d+\.\d+"/, `starknet = "${cairoVersion}"`)
.replace(/tag = "v\d+\.\d+\.\d+"/, `tag = "${contractsVersionTag}"`);

await fs.writeFile(scarbTomlPath, updatedContent, 'utf8');
}

updateScarbProject();
44 changes: 40 additions & 4 deletions packages/core-cairo/src/test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
import test from 'ava';
import { promises as fs } from 'fs';
import os from 'os';
import _test, { TestFn, ExecutionContext } from 'ava';
import path from 'path';

import { generateSources } from './generate/sources';
import type { GenericOptions } from './build-generic';
import { custom, erc20, erc721 } from './api';
import { generateSources, writeGeneratedSources } from './generate/sources';
import type { GenericOptions, KindedOptions } from './build-generic';
import { custom, erc20, erc721, erc1155 } from './api';


interface Context {
generatedSourcesPath: string
}

const test = _test as TestFn<Context>;

test.serial('erc20 result generated', async t => {
await testGenerate(t, 'ERC20');
});

test.serial('erc721 result generated', async t => {
await testGenerate(t, 'ERC721');
});

test.serial('erc1155 result generated', async t => {
await testGenerate(t, 'ERC1155');
});

test.serial('custom result generated', async t => {
await testGenerate(t, 'Custom');
});

async function testGenerate(t: ExecutionContext<Context>, kind: keyof KindedOptions) {
const generatedSourcesPath = path.join(os.tmpdir(), 'oz-wizard-cairo');
await fs.rm(generatedSourcesPath, { force: true, recursive: true });
await writeGeneratedSources(generatedSourcesPath, 'all', true, kind);

t.pass();
}

function isAccessControlRequired(opts: GenericOptions) {
switch(opts.kind) {
case 'ERC20':
return erc20.isAccessControlRequired(opts);
case 'ERC721':
return erc721.isAccessControlRequired(opts);
case 'ERC1155':
return erc1155.isAccessControlRequired(opts);
case 'Custom':
return custom.isAccessControlRequired(opts);
default:
Expand Down
7 changes: 7 additions & 0 deletions packages/core-cairo/src/utils/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
export const contractsVersion = '0.13.0';
export const contractsVersionTag = `v${contractsVersion}`;

/**
* Cairo compiler versions.
*/
export const edition = '2023_01';
export const cairoVersion = '2.6.4';
export const scarbVersion = '2.6.5';

/**
* Semantic version string representing of the minimum compatible version of Contracts to display in output.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/core-cairo/test_project/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src
target
14 changes: 14 additions & 0 deletions packages/core-cairo/test_project/Scarb.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "openzeppelin"
version = "0.13.0"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.13.0#978b4e75209da355667d8954d2450e32bd71fe49"

[[package]]
name = "test_project"
version = "0.1.0"
dependencies = [
"openzeppelin",
]
17 changes: 17 additions & 0 deletions packages/core-cairo/test_project/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "test_project"
version = "0.1.0"
edition = "2023_01"
cairo-version = "2.6.4"
scarb-version = "2.6.5"

[dependencies]
starknet = "2.6.4"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.13.0" }

[lib]

[[target.starknet-contract]]
allowed-libfuncs-list.name = "experimental"
sierra = true
casm = false

0 comments on commit 5ddbf45

Please sign in to comment.