Skip to content

Commit 5ddbf45

Browse files
Add Cairo compilation tests (#361)
Co-authored-by: Eric Nordelo <[email protected]>
1 parent 5cc1451 commit 5ddbf45

File tree

10 files changed

+218
-19
lines changed

10 files changed

+218
-19
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Compile Cairo contracts
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
push:
8+
branches:
9+
- master
10+
11+
jobs:
12+
generate_and_compile:
13+
name: Compile Cairo contracts
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 18.x
20+
cache: 'yarn'
21+
- name: Install dependencies
22+
run: yarn install
23+
- name: Generate contracts
24+
run: yarn workspace @openzeppelin/wizard-cairo update_scarb_project
25+
- name: Extract scarb version
26+
run: |
27+
SCARB_VERSION=$(grep 'scarb-version = ' packages/core-cairo/test_project/Scarb.toml | sed 's/scarb-version = "\(.*\)"/\1/')
28+
echo "SCARB_VERSION=$SCARB_VERSION" >> $GITHUB_ENV
29+
- uses: software-mansion/setup-scarb@v1
30+
with:
31+
scarb-version: ${{ env.SCARB_VERSION }}
32+
- name: Compile contracts
33+
working-directory: ./packages/core-cairo/test_project
34+
run: scarb build

packages/core-cairo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"scripts": {
1515
"prepare": "tsc",
16+
"update_scarb_project": "ts-node src/scripts/update-scarb-project.ts",
1617
"prepublish": "rimraf dist *.tsbuildinfo",
1718
"test": "ava",
1819
"test:watch": "ava --watch",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ERC1155Options } from '../erc1155';
2+
import { accessOptions } from '../set-access-control';
3+
import { infoOptions } from '../set-info';
4+
import { upgradeableOptions } from '../set-upgradeable';
5+
import { generateAlternatives } from './alternatives';
6+
7+
const booleans = [true, false];
8+
9+
const blueprint = {
10+
name: ['MyToken'],
11+
baseUri: ['https://example.com/'],
12+
burnable: booleans,
13+
pausable: booleans,
14+
mintable: booleans,
15+
updatableUri: booleans,
16+
access: accessOptions,
17+
upgradeable: upgradeableOptions,
18+
info: infoOptions,
19+
};
20+
21+
export function* generateERC1155Options(): Generator<Required<ERC1155Options>> {
22+
yield* generateAlternatives(blueprint);
23+
}

packages/core-cairo/src/generate/sources.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,41 @@ import crypto from 'crypto';
44

55
import { generateERC20Options } from './erc20';
66
import { generateERC721Options } from './erc721';
7+
import { generateERC1155Options } from './erc1155';
78
import { generateCustomOptions } from './custom';
8-
import { buildGeneric, GenericOptions } from '../build-generic';
9+
import { buildGeneric, GenericOptions, KindedOptions } from '../build-generic';
910
import { printContract } from '../print';
1011
import { OptionsError } from '../error';
1112
import { findCover } from '../utils/find-cover';
1213
import type { Contract } from '../contract';
1314

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

16-
export function* generateOptions(): Generator<GenericOptions> {
17-
for (const kindOpts of generateERC20Options()) {
18-
yield { kind: 'ERC20', ...kindOpts };
17+
type Kind = keyof KindedOptions;
18+
19+
export function* generateOptions(kind?: Kind): Generator<GenericOptions> {
20+
if (!kind || kind === 'ERC20') {
21+
for (const kindOpts of generateERC20Options()) {
22+
yield { kind: 'ERC20', ...kindOpts };
23+
}
1924
}
2025

21-
for (const kindOpts of generateERC721Options()) {
22-
yield { kind: 'ERC721', ...kindOpts };
26+
if (!kind || kind === 'ERC721') {
27+
for (const kindOpts of generateERC721Options()) {
28+
yield { kind: 'ERC721', ...kindOpts };
29+
}
2330
}
2431

25-
for (const kindOpts of generateCustomOptions()) {
26-
yield { kind: 'Custom', ...kindOpts };
32+
if (!kind || kind === 'ERC1155') {
33+
for (const kindOpts of generateERC1155Options()) {
34+
yield { kind: 'ERC1155', ...kindOpts };
35+
}
36+
}
37+
38+
if (!kind || kind === 'Custom') {
39+
for (const kindOpts of generateCustomOptions()) {
40+
yield { kind: 'Custom', ...kindOpts };
41+
}
2742
}
2843
}
2944

@@ -37,10 +52,10 @@ interface GeneratedSource extends GeneratedContract {
3752
source: string;
3853
}
3954

40-
function generateContractSubset(subset: Subset): GeneratedContract[] {
55+
function generateContractSubset(subset: Subset, kind?: Kind): GeneratedContract[] {
4156
const contracts = [];
4257

43-
for (const options of generateOptions()) {
58+
for (const options of generateOptions(kind)) {
4459
const id = crypto
4560
.createHash('sha1')
4661
.update(JSON.stringify(options))
@@ -70,17 +85,26 @@ function generateContractSubset(subset: Subset): GeneratedContract[] {
7085
}
7186
}
7287

73-
export function* generateSources(subset: Subset): Generator<GeneratedSource> {
74-
for (const c of generateContractSubset(subset)) {
88+
export function* generateSources(subset: Subset, uniqueName?: boolean, kind?: Kind): Generator<GeneratedSource> {
89+
let counter = 1;
90+
for (const c of generateContractSubset(subset, kind)) {
91+
if (uniqueName) {
92+
c.contract.name = `Contract${counter++}`;
93+
}
7594
const source = printContract(c.contract);
7695
yield { ...c, source };
7796
}
7897
}
7998

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

83-
for (const { id, source } of generateSources(subset)) {
84-
await fs.writeFile(path.format({ dir, name: id, ext: '.cairo' }), source);
103+
for (const { id, contract, source } of generateSources(subset, uniqueName, kind)) {
104+
const name = uniqueName ? contract.name : id;
105+
await fs.writeFile(path.format({ dir, name, ext: '.cairo' }), source);
106+
contractNames.push(name);
85107
}
108+
109+
return contractNames;
86110
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { promises as fs } from 'fs';
2+
import path from 'path';
3+
4+
import { writeGeneratedSources } from '../generate/sources';
5+
import { contractsVersionTag, edition, cairoVersion, scarbVersion } from '../utils/version';
6+
7+
export async function updateScarbProject() {
8+
const generatedSourcesPath = path.join('test_project', 'src');
9+
await fs.rm(generatedSourcesPath, { force: true, recursive: true });
10+
11+
// Generate the contracts source code
12+
const contractNames = await writeGeneratedSources(generatedSourcesPath, 'all', true);
13+
14+
// Generate lib.cairo file
15+
writeLibCairo(contractNames);
16+
17+
// Update Scarb.toml
18+
updateScarbToml();
19+
}
20+
21+
async function writeLibCairo(contractNames: string[]) {
22+
const libCairoPath = path.join('test_project/src', 'lib.cairo');
23+
const libCairo = contractNames.map(name => `pub mod ${name};\n`).join('');
24+
await fs.writeFile(libCairoPath, libCairo);
25+
}
26+
27+
async function updateScarbToml() {
28+
const scarbTomlPath = path.join('test_project', 'Scarb.toml');
29+
30+
let currentContent = await fs.readFile(scarbTomlPath, 'utf8');
31+
let updatedContent = currentContent
32+
.replace(/edition = "\w+"/, `edition = "${edition}"`)
33+
.replace(/cairo-version = "\d+\.\d+\.\d+"/, `cairo-version = "${cairoVersion}"`)
34+
.replace(/scarb-version = "\d+\.\d+\.\d+"/, `scarb-version = "${scarbVersion}"`)
35+
.replace(/starknet = "\d+\.\d+\.\d+"/, `starknet = "${cairoVersion}"`)
36+
.replace(/tag = "v\d+\.\d+\.\d+"/, `tag = "${contractsVersionTag}"`);
37+
38+
await fs.writeFile(scarbTomlPath, updatedContent, 'utf8');
39+
}
40+
41+
updateScarbProject();

packages/core-cairo/src/test.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
1-
import test from 'ava';
1+
import { promises as fs } from 'fs';
2+
import os from 'os';
3+
import _test, { TestFn, ExecutionContext } from 'ava';
4+
import path from 'path';
25

3-
import { generateSources } from './generate/sources';
4-
import type { GenericOptions } from './build-generic';
5-
import { custom, erc20, erc721 } from './api';
6+
import { generateSources, writeGeneratedSources } from './generate/sources';
7+
import type { GenericOptions, KindedOptions } from './build-generic';
8+
import { custom, erc20, erc721, erc1155 } from './api';
9+
10+
11+
interface Context {
12+
generatedSourcesPath: string
13+
}
14+
15+
const test = _test as TestFn<Context>;
16+
17+
test.serial('erc20 result generated', async t => {
18+
await testGenerate(t, 'ERC20');
19+
});
20+
21+
test.serial('erc721 result generated', async t => {
22+
await testGenerate(t, 'ERC721');
23+
});
24+
25+
test.serial('erc1155 result generated', async t => {
26+
await testGenerate(t, 'ERC1155');
27+
});
28+
29+
test.serial('custom result generated', async t => {
30+
await testGenerate(t, 'Custom');
31+
});
32+
33+
async function testGenerate(t: ExecutionContext<Context>, kind: keyof KindedOptions) {
34+
const generatedSourcesPath = path.join(os.tmpdir(), 'oz-wizard-cairo');
35+
await fs.rm(generatedSourcesPath, { force: true, recursive: true });
36+
await writeGeneratedSources(generatedSourcesPath, 'all', true, kind);
37+
38+
t.pass();
39+
}
640

741
function isAccessControlRequired(opts: GenericOptions) {
842
switch(opts.kind) {
943
case 'ERC20':
1044
return erc20.isAccessControlRequired(opts);
1145
case 'ERC721':
1246
return erc721.isAccessControlRequired(opts);
47+
case 'ERC1155':
48+
return erc1155.isAccessControlRequired(opts);
1349
case 'Custom':
1450
return custom.isAccessControlRequired(opts);
1551
default:

packages/core-cairo/src/utils/version.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
export const contractsVersion = '0.13.0';
55
export const contractsVersionTag = `v${contractsVersion}`;
66

7+
/**
8+
* Cairo compiler versions.
9+
*/
10+
export const edition = '2023_01';
11+
export const cairoVersion = '2.6.4';
12+
export const scarbVersion = '2.6.5';
13+
714
/**
815
* Semantic version string representing of the minimum compatible version of Contracts to display in output.
916
*/
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
src
2+
target
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Code generated by scarb DO NOT EDIT.
2+
version = 1
3+
4+
[[package]]
5+
name = "openzeppelin"
6+
version = "0.13.0"
7+
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.13.0#978b4e75209da355667d8954d2450e32bd71fe49"
8+
9+
[[package]]
10+
name = "test_project"
11+
version = "0.1.0"
12+
dependencies = [
13+
"openzeppelin",
14+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "test_project"
3+
version = "0.1.0"
4+
edition = "2023_01"
5+
cairo-version = "2.6.4"
6+
scarb-version = "2.6.5"
7+
8+
[dependencies]
9+
starknet = "2.6.4"
10+
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.13.0" }
11+
12+
[lib]
13+
14+
[[target.starknet-contract]]
15+
allowed-libfuncs-list.name = "experimental"
16+
sierra = true
17+
casm = false

0 commit comments

Comments
 (0)