├── test ├── fixture-projects │ ├── basic_ingot │ │ ├── src │ │ │ ├── ding │ │ │ │ ├── dang.fe │ │ │ │ └── dong.fe │ │ │ ├── bar.fe │ │ │ ├── bar │ │ │ │ ├── baz.fe │ │ │ │ └── mee.fe │ │ │ ├── bing.fe │ │ │ └── main.fe │ │ └── hardhat.config.js │ ├── .gitignore │ ├── none-fe-config │ │ └── hardhat.config.js │ ├── empty-fe-config │ │ └── hardhat.config.js │ ├── fe-config-with-version │ │ └── hardhat.config.js │ ├── bountiful │ │ ├── hardhat.config.js │ │ └── contracts │ │ │ ├── BountyRegistry.fe │ │ │ ├── Game.fe │ │ │ └── Game_i8.fe │ └── sunstone_compiler │ │ ├── hardhat.config.js │ │ └── contracts │ │ ├── foo.fe │ │ ├── greeter.fe │ │ └── main.fe ├── helpers.ts └── project.test.ts ├── .mocharc.json ├── .editorconfig ├── .travis.yml ├── src ├── type-extensions.ts ├── task-names.ts ├── constants.ts ├── util.ts ├── types.ts ├── index.ts ├── downloader.ts └── compile.ts ├── tsconfig.json ├── tslint.json ├── LICENSE ├── .gitignore ├── package.json └── README.md /test/fixture-projects/basic_ingot/src/ding/dang.fe: -------------------------------------------------------------------------------- 1 | type Dang = Array 2 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/bar.fe: -------------------------------------------------------------------------------- 1 | 2 | pub fn file_items_work() -> bool: 3 | return true 4 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/bar/baz.fe: -------------------------------------------------------------------------------- 1 | pub struct Baz: 2 | pub my_bool: bool 3 | pub my_u256: u256 4 | -------------------------------------------------------------------------------- /test/fixture-projects/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | #Hardhat files 5 | cache 6 | artifacts 7 | 8 | fe_output 9 | -------------------------------------------------------------------------------- /test/fixture-projects/none-fe-config/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | 3 | module.exports = { 4 | }; 5 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register/files", 3 | "ignore": ["test/fixture-projects/**/*"], 4 | "timeout": 6000 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture-projects/empty-fe-config/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | 3 | module.exports = { 4 | fe: { 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixture-projects/fe-config-with-version/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | 3 | module.exports = { 4 | fe: { 5 | version: "1.0.0", 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/bar/mee.fe: -------------------------------------------------------------------------------- 1 | pub struct Mee: 2 | pub fn kawum() -> u256: 3 | return 1 4 | 5 | pub fn rums(self) -> u256: 6 | return 1 7 | -------------------------------------------------------------------------------- /test/fixture-projects/bountiful/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | const constants = require("../../../src/constants"); 3 | 4 | module.exports = { 5 | fe: { 6 | version: constants.QUARTZ_VERSION 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixture-projects/sunstone_compiler/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | const constants = require("../../../src/constants"); 3 | 4 | module.exports = { 5 | fe: { 6 | version: constants.DEFAULT_FE_VERSION, 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/ding/dong.fe: -------------------------------------------------------------------------------- 1 | use ingot::bing::Bing 2 | 3 | pub struct Dyng: 4 | pub my_address: address 5 | pub my_u256: u256 6 | pub my_i8: i8 7 | 8 | fn get_bing() -> Bing: 9 | return Bing(my_address: address(0)) 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "11" 7 | 8 | install: 9 | - npm ci 10 | 11 | script: 12 | - npm run test 13 | - npm run lint 14 | 15 | cache: npm 16 | 17 | branches: 18 | only: 19 | - master 20 | -------------------------------------------------------------------------------- /test/fixture-projects/sunstone_compiler/contracts/foo.fe: -------------------------------------------------------------------------------- 1 | pub struct Foo { 2 | my_address: address 3 | } 4 | 5 | pub struct Bar { 6 | pub my_u256: u256 7 | } 8 | 9 | pub contract FooBar { 10 | pub fn add(x: u256, y: u256) -> u256 { 11 | return x + y 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/type-extensions.ts: -------------------------------------------------------------------------------- 1 | import "hardhat/types/config"; 2 | 3 | import { FeBuild } from "./types"; 4 | 5 | declare module "hardhat/types/config" { 6 | interface HardhatUserConfig { 7 | fe?: Partial; 8 | } 9 | 10 | interface HardhatConfig { 11 | fe: FeBuild; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("../../../src/index"); 2 | 3 | const constants = require("../../../src/constants"); 4 | 5 | module.exports = { 6 | paths: { 7 | sources: "./src", 8 | }, 9 | fe: { 10 | version: constants.QUARTZ_VERSION, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/bing.fe: -------------------------------------------------------------------------------- 1 | use std::error::Error 2 | 3 | pub struct Bing: 4 | pub my_address: address 5 | 6 | pub fn get_42_backend() -> u256: 7 | return std::evm::add(21, 21) 8 | 9 | pub contract BingContract: 10 | pub fn add(_ x: u256, _ y: u256) -> u256: 11 | return x + y 12 | -------------------------------------------------------------------------------- /src/task-names.ts: -------------------------------------------------------------------------------- 1 | export const TASK_COMPILE_FE = "compile:fe"; 2 | export const TASK_COMPILE_FE_RUN_BINARY = "compile:fe:binary:run"; 3 | export const TASK_COMPILE_FE_GET_BUILD = "compile:fe:binary:get-build"; 4 | export const TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_START = 5 | "compile:fe:log:download-compiler-start"; 6 | export const TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_END = 7 | "compile:fe:log:download-compiler-end"; -------------------------------------------------------------------------------- /test/fixture-projects/sunstone_compiler/contracts/greeter.fe: -------------------------------------------------------------------------------- 1 | use std::context::Context 2 | 3 | contract GuestBook { 4 | messages: Map> 5 | 6 | pub fn sign(self, ctx: Context, book_msg: String<100>) { 7 | self.messages[ctx.msg_sender()] = book_msg 8 | } 9 | 10 | pub fn get_msg(self, addr: address) -> String<100> { 11 | return self.messages[addr].to_mem() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "strict": true, 10 | "rootDirs": ["./src", "./test"], 11 | "esModuleInterop": true 12 | }, 13 | "exclude": ["dist", "node_modules"], 14 | "include": [ 15 | "./test", 16 | "./src" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/fixture-projects/sunstone_compiler/contracts/main.fe: -------------------------------------------------------------------------------- 1 | use foo::{Foo, Bar, FooBar} 2 | use std::context::Context 3 | 4 | contract FooBarBing { 5 | pub fn get_bar() -> Bar { 6 | return Bar( 7 | my_u256: 24 8 | ) 9 | } 10 | 11 | pub fn create_foobar_contract(ctx: Context) -> u256 { 12 | let foo_bar: FooBar = FooBar.create(ctx, 0) 13 | return foo_bar.add(x: 40, y: 50) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/fe/releases/tag/v0.17.0-alpha 2 | export const QUARTZ_VERSION = "0.17.0-alpha" 3 | // https://github.com/ethereum/fe/releases/tag/v0.19.1-alpha 4 | export const SUNSTONE_VERSION = "0.19.1-alpha" 5 | export const DEFAULT_FE_VERSION = SUNSTONE_VERSION; 6 | export const ARTIFACT_FORMAT_VERSION = "hh-fe-artifact-1"; 7 | export const DEBUG_NAMESPACE = "hardhat:plugin:fe"; 8 | 9 | export const GITHUB_RELEASES_URL = 10 | "https://api.github.com/repos/ethereum/fe/releases?per_page=100"; 11 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import type { Debugger } from "debug"; 2 | import { NomicLabsHardhatPluginError } from "hardhat/plugins"; 3 | import { DEBUG_NAMESPACE } from "./constants"; 4 | 5 | export class FePluginError extends NomicLabsHardhatPluginError { 6 | constructor(message: string, parent?: Error, shouldBeReported?: boolean) { 7 | super("hardhat-fe", message, parent, shouldBeReported); 8 | } 9 | } 10 | 11 | export function getLogger(suffix: string): Debugger { 12 | const debug = require("debug"); 13 | return debug(`${DEBUG_NAMESPACE}:${suffix}`); 14 | } 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface FeBuild { 2 | version: string; 3 | compilerPath: string; 4 | } 5 | 6 | export enum CompilerPlatform { 7 | LINUX = "amd64", 8 | MACOS = "mac", 9 | } 10 | 11 | export interface CompilerReleaseAsset { 12 | name: string; 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | browser_download_url: string; 15 | } 16 | 17 | export interface CompilerRelease { 18 | assets: CompilerReleaseAsset[]; 19 | // eslint-disable-next-line @typescript-eslint/naming-convention 20 | tag_name: string; 21 | } 22 | 23 | export type CompilersList = CompilerRelease[]; -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-plugin-prettier", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "prettier": true, 9 | "object-literal-sort-keys": false, 10 | "no-submodule-imports": false, 11 | "interface-name": false, 12 | "max-classes-per-file": false, 13 | "no-empty": false, 14 | "no-console": false, 15 | "only-arrow-functions": false, 16 | "variable-name": [ 17 | true, 18 | "check-format", 19 | "allow-leading-underscore", 20 | "allow-pascal-case" 21 | ], 22 | "ordered-imports": [ 23 | true, 24 | { 25 | "grouped-imports": true, 26 | "import-sources-order": "case-insensitive" 27 | } 28 | ], 29 | "no-floating-promises": true, 30 | "prefer-conditional-expression": false, 31 | "no-implicit-dependencies": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/fixture-projects/basic_ingot/src/main.fe: -------------------------------------------------------------------------------- 1 | use bar::baz::Baz 2 | use bar::file_items_work 3 | use bing::Bing as Bong 4 | use bing::get_42_backend 5 | use ding::{dang::Dang as Dung, dong} 6 | use bing::BingContract 7 | use std::context::Context 8 | 9 | contract Foo: 10 | pub fn get_my_baz() -> Baz: 11 | assert file_items_work() 12 | return Baz(my_bool: true, my_u256: 26) 13 | 14 | pub fn get_my_bing() -> Bong: 15 | return Bong(my_address: address(42)) 16 | 17 | pub fn get_42() -> u256: 18 | return get_42_backend() 19 | 20 | pub fn get_26() -> u256: 21 | return std::evm::add(13, 13) 22 | 23 | pub fn call_on_path(): 24 | assert bar::mee::Mee::kawum() == 1 25 | assert bar::mee::Mee().rums() == 1 26 | 27 | pub fn get_my_dyng() -> dong::Dyng: 28 | return dong::Dyng( 29 | my_address: address(8), 30 | my_u256: 42, 31 | my_i8: -1 32 | ) 33 | 34 | pub fn create_bing_contract(ctx: Context) -> u256: 35 | let bing_contract: BingContract = BingContract.create(ctx, 0) 36 | return bing_contract.add(40, 50) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nomic Labs LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # next.js build output 60 | .next 61 | 62 | # environment variables 63 | .env 64 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { assert } from "chai"; 3 | import * as fsExtra from "fs-extra"; 4 | import { resetHardhatContext } from "hardhat/plugins-testing"; 5 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 6 | 7 | declare module "mocha" { 8 | interface Context { 9 | hre: HardhatRuntimeEnvironment; 10 | } 11 | } 12 | 13 | export function assertFileExists(pathToFile: string) { 14 | assert.isTrue( 15 | fsExtra.existsSync(pathToFile), 16 | `Expected ${pathToFile} to exist` 17 | ); 18 | } 19 | 20 | export function useFixtureProject(projectName: string) { 21 | let projectPath: string; 22 | let prevWorkingDir: string; 23 | 24 | before(() => { 25 | projectPath = getFixtureProjectPath(projectName); 26 | }); 27 | 28 | before(() => { 29 | prevWorkingDir = process.cwd(); 30 | process.chdir(projectPath); 31 | }); 32 | 33 | after(() => { 34 | process.chdir(prevWorkingDir); 35 | }); 36 | } 37 | 38 | function getFixtureProjectPath(projectName: string): string { 39 | const projectPath = path.join(__dirname, "fixture-projects", projectName); 40 | 41 | if (!fsExtra.pathExistsSync(projectPath)) { 42 | throw new Error(`Fixture project ${projectName} doesn't exist`); 43 | } 44 | 45 | return fsExtra.realpathSync(projectPath); 46 | } 47 | 48 | export function useEnvironment(configPath?: string) { 49 | beforeEach("Loading hardhat environment", function () { 50 | if (configPath !== undefined) { 51 | process.env.HARDHAT_CONFIG = configPath; 52 | } 53 | 54 | this.hre = require("hardhat"); 55 | }); 56 | 57 | afterEach("Resetting hardhat context", function () { 58 | delete process.env.HARDHAT_CONFIG; 59 | resetHardhatContext(); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@developerdao/hardhat-fe", 3 | "version": "0.5.0", 4 | "description": "Hardhat plugin to develop smart contracts with fe", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/Developer-DAO/hardhat-fe.git" 8 | }, 9 | "author": "Developer DAO", 10 | "license": "MIT", 11 | "main": "dist/src/index.js", 12 | "types": "dist/src/index.d.ts", 13 | "keywords": [ 14 | "ethereum", 15 | "smart-contracts", 16 | "hardhat", 17 | "hardhat-plugin", 18 | "fe" 19 | ], 20 | "scripts": { 21 | "lint:fix": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' && tslint --fix --config tslint.json --project tsconfig.json", 22 | "lint": "tslint --config tslint.json --project tsconfig.json", 23 | "test": "mocha --exit --recursive 'test/**/*.test.ts'", 24 | "build": "tsc", 25 | "watch": "tsc -w", 26 | "prepublishOnly": "npm run build" 27 | }, 28 | "files": [ 29 | "dist/src/", 30 | "src/", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "devDependencies": { 35 | "@types/chai": "^4.1.7", 36 | "@types/debug": "^4.1.7", 37 | "@types/fs-extra": "^5.0.4", 38 | "@types/glob": "^7.2.0", 39 | "@types/mocha": "^5.2.6", 40 | "@types/node": "^8.10.38", 41 | "@types/semver": "^7.3.9", 42 | "chai": "^4.2.0", 43 | "hardhat": "^2.9.3", 44 | "mocha": "^7.1.2", 45 | "prettier": "2.0.5", 46 | "ts-node": "^8.1.0", 47 | "tslint": "^5.16.0", 48 | "tslint-config-prettier": "^1.18.0", 49 | "tslint-plugin-prettier": "^2.0.1", 50 | "typescript": "^4.0.3" 51 | }, 52 | "peerDependencies": { 53 | "hardhat": "^2.0.0" 54 | }, 55 | "dependencies": { 56 | "glob": "^8.0.1", 57 | "semver": "^7.3.7" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/Developer-DAO/hardhat-fe/issues" 61 | }, 62 | "homepage": "https://github.com/Developer-DAO/hardhat-fe#readme", 63 | "directories": { 64 | "test": "test" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

hardhat-fe

3 |

4 | Hardhat plugin to develop smart contracts with Fe 5 |

6 |

7 | 8 | [![npm version](https://badge.fury.io/js/@developerdao%2Fhardhat-fe.svg)](https://badge.fury.io/js/@developerdao%2Fhardhat-fe) 9 | [![hardhat](https://hardhat.org/buidler-plugin-badge.svg?1)](https://hardhat.org) 10 | 11 | Related [DevDAO RFC](https://forum.developerdao.com/t/rfc-hardhat-fe-hardhat-plugin-to-develop-smart-contracts-with-fe/2000) 12 | 13 | ## What 14 | 15 | This plugin adds support for Fe to Hardhat. Once installed, Fe contracts can be compiled by running the `compile` task. 16 | 17 | This plugin plans to generate the same artifact format as the built-in Solidity compiler, so that it can be used in conjunction with all other plugins. 18 | 19 | The Fe compiler is run using the [official binary releases](https://github.com/ethereum/fe/releases). 20 | 21 | ## Installation 22 | 23 | First, you need to install the plugin by running 24 | 25 | ```bash 26 | npm install --save-dev @developerdao/hardhat-fe 27 | ``` 28 | 29 | And add the following statement to your `hardhat.config.js`: 30 | 31 | ```js 32 | require("@developerdao/hardhat-fe"); 33 | ``` 34 | 35 | Or, if you are using TypeScript, add this to your `hardhat.config.ts`: 36 | 37 | ```js 38 | import "@developerdao/hardhat-fe"; 39 | ``` 40 | 41 | ## Required plugins 42 | 43 | No plugins dependencies. 44 | 45 | ## Tasks 46 | 47 | This plugin creates no additional tasks. 48 | 49 | ## Environment extensions 50 | 51 | This plugin does not extend the Hardhat Runtime Environment. 52 | 53 | ## Configuration 54 | 55 | This plugin adds an optional `fe` entry to Hardhat's config, which lets you specify the Fe version to use. 56 | 57 | This is an example of how to set it: 58 | 59 | ```js 60 | module.exports = { 61 | fe: { 62 | version: "0.19.1-alpha", 63 | }, 64 | }; 65 | ``` 66 | 67 | Or you can use a customized compiler by specifying the path to the binary instead of downloading from official releases: 68 | 69 | ```js 70 | module.exports = { 71 | fe: { 72 | compilerPath: "absolute_path_to_fe_compiler_binary", 73 | }, 74 | }; 75 | ``` 76 | 77 | Otherwise, if you leave the `fe` config entry blank, the plugin will use the default version of Fe compiler which currently is `0.19.1-alpha` 78 | 79 | ## Usage 80 | 81 | There are no additional steps you need to take for this plugin to work. 82 | 83 | ## Known issues 84 | 85 | since Fe is still under heavy development 86 | 87 | * the abi file generated by Fe compiler does not contain `deployedBytecode` attribute 88 | * the abi file generated by Fe compiler does not contain `stateMutability` attribute, [related issue](https://github.com/ethereum/fe/issues/557) 89 | 90 | ## Development 91 | 92 | ``` 93 | npm install // install dependencies 94 | npm run build // build before publish 95 | npm run test // run tests 96 | ``` 97 | -------------------------------------------------------------------------------- /test/fixture-projects/bountiful/contracts/BountyRegistry.fe: -------------------------------------------------------------------------------- 1 | use std::context::Context 2 | 3 | # Poor man's interface 4 | contract ISolvable: 5 | 6 | pub fn is_solved(self) -> bool: 7 | revert 8 | 9 | # Revert errors 10 | struct AlreadyLocked: 11 | pass 12 | 13 | struct InvalidClaim: 14 | pass 15 | 16 | struct OnlyAdmin: 17 | pass 18 | 19 | struct InvalidDeposit: 20 | pass 21 | 22 | struct MissingLock: 23 | pass 24 | 25 | # Structure to represent a lock with a lease time 26 | struct ClaimLock: 27 | pub claimer: address 28 | pub valid_until_block: u256 29 | 30 | const LOCK_PERIOD_IN_BLOCKS: u256 = 1000 31 | const ONE_ETH_IN_WEI: u256 = 1000000000000000000 32 | 33 | contract BountyRegistry: 34 | 35 | lock: ClaimLock 36 | open_challenges: Map 37 | admin: address 38 | 39 | pub fn __init__(self, admin: address): 40 | self.admin = admin 41 | 42 | pub fn lock(self, ctx: Context): 43 | if self.is_locked(ctx): 44 | revert AlreadyLocked() 45 | elif ctx.msg_value() < ONE_ETH_IN_WEI: 46 | revert InvalidDeposit() 47 | else: 48 | self.lock = ClaimLock(claimer:ctx.msg_sender(), valid_until_block: ctx.block_number() + LOCK_PERIOD_IN_BLOCKS) 49 | 50 | pub fn validate_owns_lock(self, ctx: Context, owner: address): 51 | if not self.is_locked(ctx) or self.lock.claimer != owner: 52 | revert MissingLock() 53 | 54 | pub fn is_locked(self, ctx: Context) -> bool: 55 | return self.lock.valid_until_block >= ctx.block_number() 56 | 57 | pub fn register_challenge(self, ctx: Context, challenge: address): 58 | self.validate_is_admin(ctx) 59 | self.open_challenges[challenge] = true 60 | 61 | pub fn remove_challenge(self, ctx: Context, challenge: address): 62 | self.validate_is_admin(ctx) 63 | 64 | if self.is_locked(ctx): 65 | revert AlreadyLocked() 66 | else: 67 | self.open_challenges[challenge] = false 68 | 69 | pub fn is_open_challenge(self, challenge: address) -> bool: 70 | return self.open_challenges[challenge] 71 | 72 | pub fn claim(self, ctx: Context, challenge: address): 73 | self.validate_owns_lock(ctx, owner: ctx.msg_sender()) 74 | 75 | if not self.open_challenges[challenge]: 76 | revert InvalidClaim() 77 | else: 78 | let puzzle: ISolvable = ISolvable(ctx, challenge) 79 | if puzzle.is_solved(): 80 | ctx.send_value(to: ctx.msg_sender(), wei: ctx.self_balance()) 81 | else: 82 | revert InvalidClaim() 83 | 84 | pub fn withdraw(self, ctx: Context): 85 | # Admin should be allowed to withdraw funds if there's no lock in progress 86 | # This would be used to migrate to a new bounty registry 87 | self.validate_is_admin(ctx) 88 | if self.is_locked(ctx): 89 | revert AlreadyLocked() 90 | else: 91 | ctx.send_value(to: ctx.msg_sender(), wei: ctx.self_balance()) 92 | 93 | fn validate_is_admin(self, ctx: Context): 94 | if ctx.msg_sender() != self.admin: 95 | revert OnlyAdmin() 96 | -------------------------------------------------------------------------------- /test/project.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as assert from "assert"; 3 | import * as fsExtra from "fs-extra"; 4 | import { DEFAULT_FE_VERSION, QUARTZ_VERSION } from "../src/constants"; 5 | import { useFixtureProject, useEnvironment, assertFileExists } from "./helpers"; 6 | import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; 7 | 8 | describe("Default project tests", () => { 9 | beforeEach(function () { 10 | fsExtra.removeSync("artifacts"); 11 | fsExtra.removeSync("cache"); 12 | }); 13 | 14 | describe("user config without fe config", function () { 15 | useFixtureProject("none-fe-config"); 16 | useEnvironment(); 17 | 18 | it("use default fe version", function () { 19 | assert.equal(this.hre.config.fe.version, DEFAULT_FE_VERSION); 20 | }); 21 | }); 22 | describe("fe config without version", function () { 23 | useFixtureProject("empty-fe-config"); 24 | useEnvironment(); 25 | 26 | it("use default fe version", function () { 27 | assert.equal(this.hre.config.fe.version, DEFAULT_FE_VERSION); 28 | }); 29 | }); 30 | describe("fe config with version", function () { 31 | useFixtureProject("fe-config-with-version"); 32 | useEnvironment(); 33 | 34 | it("use specified fe version", function () { 35 | assert.equal(this.hre.config.fe.version, "1.0.0"); 36 | }); 37 | }); 38 | describe("compile ingot module", function () { 39 | // https://github.com/ethereum/fe/tree/master/crates/test-files/fixtures/ingots/basic_ingot 40 | useFixtureProject("basic_ingot"); 41 | useEnvironment(); 42 | 43 | it("The specified version must be used", function () { 44 | assert.equal(this.hre.config.fe.version, QUARTZ_VERSION); 45 | }); 46 | it("should compile and emit artifacts", async function () { 47 | await this.hre.run(TASK_COMPILE); 48 | 49 | assertFileExists(path.join("artifacts", "src", "main.fe", "Foo.json")); 50 | }); 51 | }); 52 | describe("compile separated modules", function () { 53 | // bunch of files need to be compiled 54 | this.timeout(15000); 55 | // https://github.com/cburgdorf/bountiful 56 | useFixtureProject("bountiful"); 57 | useEnvironment(); 58 | 59 | it("The specified version must be used", function () { 60 | assert.equal(this.hre.config.fe.version, QUARTZ_VERSION); 61 | }); 62 | it("should compile and emit artifacts", async function () { 63 | await this.hre.run(TASK_COMPILE); 64 | 65 | assertFileExists(path.join("artifacts", "contracts", "BountyRegistry.fe", "BountyRegistry.json")); 66 | assertFileExists(path.join("artifacts", "contracts", "BountyRegistry.fe", "ISolvable.json")); 67 | assertFileExists(path.join("artifacts", "contracts", "Game_i8.fe", "Game.json")); 68 | assertFileExists(path.join("artifacts", "contracts", "Game_i8.fe", "ILockValidator.json")); 69 | assertFileExists(path.join("artifacts", "contracts", "Game.fe", "Game.json")); 70 | assertFileExists(path.join("artifacts", "contracts", "Game.fe", "ILockValidator.json")); 71 | }); 72 | }); 73 | describe("compile with 0.19.1-alpha 'Sunstone' compiler", function () { 74 | this.timeout(15000); 75 | useFixtureProject("sunstone_compiler"); 76 | useEnvironment(); 77 | 78 | it("The specified version must be used", function () { 79 | assert.equal(this.hre.config.fe.version, DEFAULT_FE_VERSION); 80 | }); 81 | it("should compile and emit artifacts", async function () { 82 | await this.hre.run(TASK_COMPILE); 83 | 84 | assertFileExists(path.join("artifacts", "contracts", "main.fe", "FooBar.json")); 85 | assertFileExists(path.join("artifacts", "contracts", "main.fe", "FooBarBing.json")); 86 | assertFileExists(path.join("artifacts", "contracts", "main.fe", "GuestBook.json")); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/fixture-projects/bountiful/contracts/Game.fe: -------------------------------------------------------------------------------- 1 | use std::context::Context 2 | 3 | # Poor man's interface 4 | contract ILockValidator: 5 | 6 | pub fn validate_owns_lock(self, owner: address): 7 | revert 8 | 9 | struct InvalidIndex: 10 | pass 11 | 12 | struct NotMovable: 13 | pass 14 | 15 | const EMPTY_FIELD_MARKER: u256 = 0 16 | # We don't have dynamically sized arrays. Since the number of moves 17 | # varies depending on the position of the empty field, we need a marker 18 | # for invalid moves. 19 | 20 | # We could turn this into a slightly cleaner value of -1 if we used signed integers 21 | # That's blocked on https://github.com/ethereum/fe/issues/564 22 | const INVALID_FIELD_MARKER: u256 = 666 23 | 24 | contract Game: 25 | 26 | lock_validator: ILockValidator 27 | board: Array 28 | moves: Map> 29 | 30 | pub fn __init__(self, ctx: Context, lock_validator: address, board: Array): 31 | self.lock_validator = ILockValidator(ctx, lock_validator) 32 | self.board = board 33 | # Map each empty slot to a list of possible moves 34 | # It would be nice to have fixed-size maps that work with `const` (baked into code) 35 | self.moves[0] = [1, 4, INVALID_FIELD_MARKER, INVALID_FIELD_MARKER] 36 | self.moves[1] = [0, 2, 5, INVALID_FIELD_MARKER] 37 | self.moves[2] = [1, 3, 6, INVALID_FIELD_MARKER] 38 | self.moves[3] = [2, 7, INVALID_FIELD_MARKER, INVALID_FIELD_MARKER] 39 | self.moves[4] = [0, 5, 8, INVALID_FIELD_MARKER] 40 | self.moves[5] = [1, 4, 6, 9] 41 | self.moves[6] = [2, 5, 7, 10] 42 | self.moves[7] = [3, 6, 11, INVALID_FIELD_MARKER] 43 | self.moves[8] = [4, 9, 12, INVALID_FIELD_MARKER] 44 | self.moves[9] = [5, 8, 10, 13] 45 | self.moves[10] = [6, 9, 11, 14] 46 | self.moves[11] = [7, 10, 15, INVALID_FIELD_MARKER] 47 | self.moves[12] = [8, 13, INVALID_FIELD_MARKER, INVALID_FIELD_MARKER] 48 | self.moves[13] = [9, 12, 14, INVALID_FIELD_MARKER] 49 | self.moves[14] = [10, 13, 15, INVALID_FIELD_MARKER] 50 | self.moves[15] = [11, 14, INVALID_FIELD_MARKER, INVALID_FIELD_MARKER] 51 | 52 | pub fn get_board(self) -> Array: 53 | return self.board.to_mem() 54 | 55 | pub fn is_solved(self) -> bool: 56 | let index: u256 = 0 57 | let current_board: Array = self.board.to_mem() 58 | # Workaround for: https://github.com/ethereum/fe/issues/528 59 | for _field in current_board: 60 | if current_board[index] != get_winning_state()[index]: 61 | return false 62 | index += 1 63 | 64 | return true 65 | 66 | pub fn move_field(self, ctx: Context, index: u256): 67 | self.lock_validator.validate_owns_lock(owner: ctx.msg_sender()) 68 | 69 | if not self.is_valid_index(index): 70 | revert InvalidIndex() 71 | else: 72 | let movable_fields: Array = self.moves[self.get_index_of_empty_field()].to_mem() 73 | if not is_in(num: index, values: movable_fields): 74 | revert NotMovable() 75 | else: 76 | let empty_field_index: u256 = self.get_index_of_empty_field() 77 | let field_value: u256 = self.board[index] 78 | self.board[index] = EMPTY_FIELD_MARKER 79 | self.board[empty_field_index] = field_value 80 | 81 | fn get_index_of_empty_field(self) -> u256: 82 | let index: u256 = 0 83 | let current_board: Array = self.board.to_mem() 84 | for field in current_board: 85 | if field == 0: 86 | break 87 | else: 88 | index += 1 89 | 90 | return index 91 | 92 | fn get_winning_state() -> Array: 93 | # TODO: Make this a constant when complex constants are supported 94 | return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] 95 | 96 | fn is_valid_index(self, index: u256) -> bool: 97 | return index >= 0 and index <= 15 98 | 99 | fn is_in(num: u256, values: Array) -> bool: 100 | for val in values: 101 | if val == num: 102 | return true 103 | return false 104 | -------------------------------------------------------------------------------- /test/fixture-projects/bountiful/contracts/Game_i8.fe: -------------------------------------------------------------------------------- 1 | use std::context::Context 2 | 3 | # Poor man's interface 4 | contract ILockValidator: 5 | 6 | pub fn validate_owns_lock(self, owner: address): 7 | revert 8 | 9 | struct InvalidIndex: 10 | pass 11 | 12 | struct NotMovable: 13 | pass 14 | 15 | const EMPTY_FIELD_MARKER: u8 = 0 16 | # We don't have dynamically sized arrays. Since the number of moves 17 | # varies depending on the position of the empty field, we need a marker 18 | # for invalid moves. 19 | # TODO: We don't yet support type constructors in consts, hence the function 20 | fn invalid_field_marker() -> i8: 21 | return i8(-1) 22 | 23 | contract Game: 24 | 25 | lock_validator: ILockValidator 26 | board: Array 27 | moves: Map> 28 | 29 | pub fn __init__(self, ctx: Context, lock_validator: address, board: Array): 30 | self.lock_validator = ILockValidator(ctx, lock_validator) 31 | self.board = board 32 | # Map each empty slot to a list of possible moves 33 | # It would be nice to have fixed-size maps that work with `const` (baked into code) 34 | self.moves[u8(0)] = [i8(1), i8(4), invalid_field_marker(), invalid_field_marker()] 35 | self.moves[u8(1)] = [i8(0), i8(2), i8(5), invalid_field_marker()] 36 | self.moves[u8(2)] = [i8(1), i8(3), i8(6), invalid_field_marker()] 37 | self.moves[u8(3)] = [i8(2), i8(7), invalid_field_marker(), invalid_field_marker()] 38 | self.moves[u8(4)] = [i8(0), i8(5), i8(8), invalid_field_marker()] 39 | self.moves[u8(5)] = [i8(1), i8(4), i8(6), i8(9)] 40 | self.moves[u8(6)] = [i8(2), i8(5), i8(7), i8(10)] 41 | self.moves[u8(7)] = [i8(3), i8(6), i8(11), invalid_field_marker()] 42 | self.moves[u8(8)] = [i8(4), i8(9), i8(12), invalid_field_marker()] 43 | self.moves[u8(9)] = [i8(5), i8(8), i8(10), i8(13)] 44 | self.moves[u8(10)] = [i8(6), i8(9), i8(11), i8(14)] 45 | self.moves[u8(11)] = [i8(7), i8(10), i8(15), invalid_field_marker()] 46 | self.moves[u8(12)] = [i8(8), i8(13), invalid_field_marker(), invalid_field_marker()] 47 | self.moves[u8(13)] = [i8(9), i8(12), i8(14), invalid_field_marker()] 48 | self.moves[u8(14)] = [i8(10), i8(13), i8(15), invalid_field_marker()] 49 | self.moves[u8(15)] = [i8(11), i8(14), invalid_field_marker(), invalid_field_marker()] 50 | 51 | pub fn get_board(self) -> Array: 52 | return self.board.to_mem() 53 | 54 | pub fn is_solved(self) -> bool: 55 | let index: u256 = 0 56 | let current_board: Array = self.board.to_mem() 57 | # Workaround for: https://github.com/ethereum/fe/issues/528 58 | for _field in current_board: 59 | if current_board[index] != get_winning_state()[index]: 60 | return false 61 | index += 1 62 | 63 | return true 64 | 65 | pub fn move_field(self, ctx: Context, index: u8): 66 | self.lock_validator.validate_owns_lock(owner: ctx.msg_sender()) 67 | 68 | if not self.is_valid_index(index): 69 | revert InvalidIndex() 70 | else: 71 | let movable_fields: Array = self.moves[self.get_index_of_empty_field()].to_mem() 72 | if not is_in(num:index, values: movable_fields): 73 | revert NotMovable() 74 | else: 75 | let empty_field_index: u8 = self.get_index_of_empty_field() 76 | let field_value: u8 = self.board[u256(index)] 77 | self.board[u256(index)] = EMPTY_FIELD_MARKER 78 | self.board[u256(empty_field_index)] = field_value 79 | 80 | fn get_index_of_empty_field(self) -> u8: 81 | let index: u8 = 0 82 | let current_board: Array = self.board.to_mem() 83 | for field in current_board: 84 | if field == 0: 85 | break 86 | else: 87 | index += 1 88 | 89 | return index 90 | 91 | fn get_winning_state() -> Array: 92 | # TODO: Make this a constant when complex constants are supported 93 | return [u8(1), u8(2), u8(3), u8(4), u8(5), u8(6), u8(7), u8(8), u8(9), u8(10), u8(11), u8(12), u8(13), u8(14), u8(15), 0] 94 | 95 | fn is_valid_index(self, index: u8) -> bool: 96 | return index >= 0 and index <= 15 97 | 98 | fn is_in(num: u8, values: Array) -> bool: 99 | for val in values: 100 | if val == i8(num): 101 | return true 102 | return false 103 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { extendConfig, subtask, types } from "hardhat/config"; 2 | import { getCompilersDir } from "hardhat/internal/util/global-dir"; 3 | import { TASK_COMPILE_GET_COMPILATION_TASKS } from "hardhat/builtin-tasks/task-names"; 4 | import { 5 | TASK_COMPILE_FE, 6 | TASK_COMPILE_FE_GET_BUILD, 7 | TASK_COMPILE_FE_RUN_BINARY, 8 | TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_START, 9 | TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_END, 10 | } from "./task-names"; 11 | import { compile } from "./compile"; 12 | import { CompilerDownloader } from "./downloader"; 13 | import { DEFAULT_FE_VERSION } from "./constants"; 14 | 15 | import "./type-extensions"; 16 | import { FeBuild } from "./types"; 17 | import { FePluginError } from "./util"; 18 | import { Artifacts, HardhatConfig } from "hardhat/types"; 19 | 20 | extendConfig((config, userConfig) => { 21 | const defaultConfig = { version: DEFAULT_FE_VERSION }; 22 | config.fe = { ...defaultConfig, ...config.fe }; 23 | }); 24 | 25 | subtask( 26 | TASK_COMPILE_GET_COMPILATION_TASKS, 27 | async (_, __, runSuper): Promise => { 28 | const otherTasks = await runSuper(); 29 | return [...otherTasks, TASK_COMPILE_FE]; 30 | } 31 | ); 32 | 33 | subtask(TASK_COMPILE_FE) 34 | .addParam("quiet", undefined, undefined, types.boolean) 35 | .setAction( 36 | async ({ quiet }: { quiet: boolean }, { artifacts, config, run }) => { 37 | const feVersion = config.fe.version; 38 | const compilerPath = config.fe.compilerPath; 39 | 40 | var fePath; 41 | if (compilerPath) { 42 | fePath = compilerPath; 43 | console.log("using local fe executable:", fePath); 44 | } else { 45 | const feBuild: FeBuild = await run(TASK_COMPILE_FE_GET_BUILD, { 46 | quiet, 47 | feVersion, 48 | }); 49 | fePath = feBuild.compilerPath; 50 | console.log("using downloaded fe executable:", fePath); 51 | } 52 | 53 | await run( 54 | TASK_COMPILE_FE_RUN_BINARY, 55 | { 56 | fePath: fePath, 57 | feVersion: feVersion, 58 | artifacts: artifacts, 59 | config: config 60 | } 61 | ); 62 | } 63 | ); 64 | 65 | subtask(TASK_COMPILE_FE_RUN_BINARY) 66 | .addParam("fePath", undefined, undefined, types.string) 67 | .addParam("feVersion", undefined, undefined, types.string) 68 | .setAction( 69 | async ({ 70 | fePath, 71 | feVersion, 72 | artifacts, 73 | config 74 | }: { 75 | fePath: string; 76 | feVersion: string; 77 | artifacts: Artifacts; 78 | config: HardhatConfig; 79 | }) => { 80 | await compile(fePath, feVersion, config.paths, artifacts); 81 | } 82 | ); 83 | 84 | subtask(TASK_COMPILE_FE_GET_BUILD) 85 | .addParam("quiet", undefined, undefined, types.boolean) 86 | .addParam("feVersion", undefined, undefined, types.string) 87 | .setAction(async ( 88 | { quiet, feVersion }: { quiet: boolean; feVersion: string }, 89 | { run } 90 | ): Promise => { 91 | const compilersCache = await getCompilersDir(); 92 | const downloader = new CompilerDownloader(compilersCache); 93 | 94 | await downloader.initCompilersList(); 95 | 96 | const isDownloaded = downloader.isCompilerDownloaded(feVersion); 97 | 98 | await run(TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_START, { 99 | feVersion, 100 | isDownloaded, 101 | quiet, 102 | }); 103 | 104 | const compilerPath = await downloader.getOrDownloadCompiler(feVersion); 105 | 106 | if (compilerPath === undefined) { 107 | throw new FePluginError("Can't download fe compiler"); 108 | } 109 | 110 | await run(TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_END, { 111 | feVersion, 112 | isDownloaded, 113 | quiet, 114 | }); 115 | 116 | return { compilerPath, version: feVersion }; 117 | } 118 | ); 119 | 120 | subtask(TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_START) 121 | .addParam("quiet", undefined, undefined, types.boolean) 122 | .addParam("isDownloaded", undefined, undefined, types.boolean) 123 | .addParam("feVersion", undefined, undefined, types.string) 124 | .setAction( 125 | async ({ 126 | quiet, 127 | isDownloaded, 128 | feVersion, 129 | }: { 130 | quiet: boolean; 131 | isDownloaded: boolean; 132 | feVersion: string; 133 | }) => { 134 | if (isDownloaded || quiet) return; 135 | 136 | console.log(`Downloading compiler ${feVersion}`); 137 | } 138 | ); 139 | 140 | subtask(TASK_COMPILE_FE_LOG_DOWNLOAD_COMPILER_END) 141 | .addParam("quiet", undefined, undefined, types.boolean) 142 | .addParam("isDownloaded", undefined, undefined, types.boolean) 143 | .addParam("feVersion", undefined, undefined, types.string) 144 | .setAction( 145 | async ({}: { 146 | quiet: boolean; 147 | isDownloaded: boolean; 148 | feVersion: string; 149 | }) => {} 150 | ); 151 | -------------------------------------------------------------------------------- /src/downloader.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import path from "path"; 3 | import fsExtra from "fs-extra"; 4 | import semver from "semver"; 5 | 6 | import { 7 | CompilerReleaseAsset, 8 | CompilersList, 9 | CompilerPlatform, 10 | CompilerRelease, 11 | } from "./types"; 12 | import { GITHUB_RELEASES_URL } from "./constants"; 13 | import { FePluginError, getLogger } from "./util"; 14 | 15 | const log = getLogger("downloader"); 16 | 17 | async function downloadFile( 18 | url: string, 19 | destinationFile: string 20 | ): Promise { 21 | const { download } = await import("hardhat/internal/util/download"); 22 | log(`Downloading from ${url} to ${destinationFile}`); 23 | await download(url, destinationFile); 24 | } 25 | 26 | type DownloadFunction = (url: string, destinationFile: string) => Promise; 27 | 28 | interface CompilerDownloaderOptions { 29 | download?: DownloadFunction; 30 | } 31 | 32 | export class CompilerDownloader { 33 | private readonly _download: DownloadFunction; 34 | private readonly _platform: CompilerPlatform; 35 | public compilersList: CompilersList = []; 36 | 37 | constructor( 38 | private readonly _compilersDir: string, 39 | options: CompilerDownloaderOptions = {} 40 | ) { 41 | this._download = options.download ?? downloadFile; 42 | this._platform = this._getCurrentPlatform(); 43 | } 44 | 45 | public get compilersListExists(): boolean { 46 | return this._fileExists(this.compilersListPath); 47 | } 48 | 49 | public get downloadsDir(): string { 50 | return path.join(this._compilersDir, "fe", this._platform); 51 | } 52 | 53 | public get compilersListPath(): string { 54 | return path.join(this.downloadsDir, "list.json"); 55 | } 56 | 57 | public isCompilerDownloaded(version: string): boolean { 58 | return this._fileExists(this._getDownloadedFilePath(version)); 59 | } 60 | 61 | public async initCompilersList( 62 | { forceDownload } = { forceDownload: true } 63 | ): Promise { 64 | if (forceDownload || !this.compilersListExists) { 65 | await this._downloadCompilersList(); 66 | } 67 | 68 | this.compilersList = this._getCompilersListFromDisk(); 69 | } 70 | 71 | public async getCompilerAsset( 72 | version: string 73 | ): Promise { 74 | let versionRelease = this._findVersionRelease(version); 75 | 76 | if (versionRelease === undefined) { 77 | await this.initCompilersList({ forceDownload: true }); 78 | versionRelease = this._findVersionRelease(version); 79 | 80 | if (versionRelease === undefined) { 81 | throw new FePluginError(`Unsupported fe version: ${version}`); 82 | } 83 | } 84 | 85 | const compilerAsset = versionRelease.assets.find((asset) => 86 | asset.name.includes(this._platform) 87 | ); 88 | 89 | if (compilerAsset === undefined) { 90 | throw new FePluginError( 91 | `Fe version ${version} is not supported on platform ${this._platform}` 92 | ); 93 | } 94 | 95 | return compilerAsset; 96 | } 97 | 98 | public async getOrDownloadCompiler( 99 | version: string 100 | ): Promise { 101 | try { 102 | const downloadedFilePath = this._getDownloadedFilePath(version); 103 | log("downloaded file path:", downloadedFilePath); 104 | 105 | if (!this._fileExists(downloadedFilePath)) { 106 | const compilerAsset = await this.getCompilerAsset(version); 107 | log("compiler asset:", compilerAsset); 108 | 109 | log(`Downloading compiler version ${version} platform ${this._platform}`); 110 | await this._downloadCompiler(compilerAsset, downloadedFilePath); 111 | } 112 | 113 | if (this._isUnix) { 114 | fsExtra.chmodSync(downloadedFilePath, 0o755); 115 | } 116 | 117 | return downloadedFilePath; 118 | } catch (e: unknown) { 119 | if (FePluginError.isNomicLabsHardhatPluginError(e)) { 120 | throw e; 121 | } else { 122 | throw new FePluginError( 123 | "An unexpected error occurred", 124 | e as Error, 125 | true 126 | ); 127 | } 128 | } 129 | } 130 | 131 | private _findVersionRelease(version: string): CompilerRelease | undefined { 132 | return this.compilersList.find((release) => 133 | semver.eq(release.tag_name, version) 134 | ); 135 | } 136 | 137 | private async _downloadCompilersList(): Promise { 138 | try { 139 | await this._download(GITHUB_RELEASES_URL, this.compilersListPath); 140 | } catch { 141 | throw new FePluginError( 142 | "Failed to download compiler list", 143 | undefined, 144 | true 145 | ); 146 | } 147 | } 148 | 149 | private _getCompilersListFromDisk(): CompilersList { 150 | return fsExtra.readJSONSync(this.compilersListPath); 151 | } 152 | 153 | private get _isUnix(): boolean { 154 | return ( 155 | this._platform === CompilerPlatform.MACOS || 156 | this._platform === CompilerPlatform.LINUX 157 | ); 158 | } 159 | 160 | private async _downloadCompiler( 161 | compilerAsset: CompilerReleaseAsset, 162 | downloadedFilePath: string 163 | ): Promise { 164 | try { 165 | await this._download( 166 | compilerAsset.browser_download_url, 167 | downloadedFilePath 168 | ); 169 | } catch (e: unknown) { 170 | throw new FePluginError("Compiler download failed", e as Error); 171 | } 172 | } 173 | 174 | private _getDownloadedFilePath(version: string): string { 175 | return path.join(this.downloadsDir, version); 176 | } 177 | 178 | private _fileExists(filepath: string): boolean { 179 | return fsExtra.pathExistsSync(filepath); 180 | } 181 | 182 | private _getCurrentPlatform(): CompilerPlatform { 183 | switch (os.platform()) { 184 | case "linux": 185 | return CompilerPlatform.LINUX; 186 | case "darwin": 187 | return CompilerPlatform.MACOS; 188 | default: 189 | throw new FePluginError( 190 | `Fe not supported on platform ${os.platform()}` 191 | ); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/compile.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as fs from "fs"; 3 | import { getLogger } from "./util"; 4 | import { ARTIFACT_FORMAT_VERSION } from "./constants"; 5 | import { localPathToSourceName } from "hardhat/utils/source-names"; 6 | import { Artifact, Artifacts, ProjectPathsConfig } from "hardhat/types"; 7 | import { Artifacts as ArtifactsImpl } from "hardhat/internal/artifacts"; 8 | 9 | const log = getLogger("compile"); 10 | 11 | function getFeTempOutputFolder() { 12 | return process.cwd() + "/fe_output"; 13 | } 14 | 15 | function isLegacyVersion(feVersion: string) { 16 | let [major, minor] = feVersion.split("-")[0].split("."); 17 | // versions earlier before v0.18.0-alpha 18 | return major == "0" && (parseInt(minor) <= 18); 19 | } 20 | 21 | function compileFileWithFeBinary(fePath: string, feVersion: string, fileName: string) { 22 | const fe_options = "--overwrite --emit=abi,bytecode"; 23 | const outputFolder = getFeTempOutputFolder(); 24 | 25 | if (fileName.endsWith(".git")) fileName = fileName.slice(0, -4); 26 | // if (!fileName.endsWith(".fe")) return; 27 | 28 | var compileCommand; 29 | if (isLegacyVersion(feVersion)) { 30 | compileCommand = `${fePath} ${fileName} ${fe_options} --output-dir ${outputFolder}`; 31 | } else { 32 | compileCommand = `${fePath} build ${fileName} ${fe_options} --output-dir ${outputFolder}`; 33 | } 34 | 35 | try { 36 | log(compileCommand); 37 | require("child_process").execSync(compileCommand); 38 | } catch (e) { 39 | console.log("[Compiler Exception] " + e); 40 | } 41 | } 42 | 43 | function getCompileResultFromBinaryBuild() { 44 | var compilerResult: { [k: string]: any } = {}; 45 | compilerResult.contracts = {}; 46 | const FE_OUTPUT = fs.readdirSync("fe_output"); 47 | for (const fileName of FE_OUTPUT) { 48 | compilerResult.contracts[fileName] = {}; 49 | compilerResult.contracts[fileName].bytecode = fs.readFileSync( 50 | `fe_output/${fileName}/${fileName}.bin`, 51 | "utf8" 52 | ); 53 | compilerResult.contracts[fileName].abi = fs.readFileSync( 54 | `fe_output/${fileName}/${fileName}_abi.json`, 55 | "utf8" 56 | ); 57 | compilerResult.contracts[fileName].abi = JSON.parse(compilerResult.contracts[fileName].abi); 58 | } 59 | return compilerResult; 60 | } 61 | 62 | function cleanup() { 63 | const outputFolder = getFeTempOutputFolder(); 64 | const cleanUpCommand = `rm -rf ${outputFolder}`; 65 | require("child_process").execSync(cleanUpCommand); 66 | } 67 | 68 | function isIngotProject(files: string[]): [boolean, string] { 69 | for (const file of files) { 70 | if (file.endsWith("main.fe")) { 71 | return [true, file.replace("/main.fe", "")]; 72 | } 73 | } 74 | return [false, ""]; 75 | } 76 | 77 | export async function compile( 78 | fePath: string, 79 | feVersion: string, 80 | paths: ProjectPathsConfig, 81 | artifacts: Artifacts 82 | ) { 83 | const files = await getFeSources(paths); 84 | log(files); 85 | 86 | const [isIngot, ingotModule] = isIngotProject(files); 87 | 88 | if (isIngot) { 89 | const sourceName = await localPathToSourceName(paths.root, `${ingotModule}/main.fe`); 90 | console.log(`Compiling module ${ingotModule} with Fe binary ${fePath}`); 91 | compileFileWithFeBinary(fePath, feVersion, ingotModule); 92 | 93 | const compilerResult = getCompileResultFromBinaryBuild(); 94 | log("compilerResult:", compilerResult); 95 | 96 | let contractNames = []; 97 | for (const key of Object.keys(compilerResult.contracts)) { 98 | const artifact = getArtifactFromFeOutput( 99 | sourceName, 100 | key, 101 | compilerResult.contracts[key] 102 | ); 103 | log("artifact:", artifact); 104 | // https://github.com/NomicFoundation/hardhat/blob/master/packages/hardhat-ethers/src/internal/helpers.ts#L20 105 | await artifacts.saveArtifactAndDebugFile(artifact); 106 | contractNames.push(artifact.contractName); 107 | } 108 | const artifactsImpl = artifacts as ArtifactsImpl; 109 | artifactsImpl.addValidArtifacts([{ sourceName: sourceName, artifacts: contractNames }]); 110 | 111 | cleanup(); 112 | } else { 113 | for (const file of files) { 114 | const sourceName = await localPathToSourceName(paths.root, file); 115 | console.log(`Compiling ${file} with Fe binary ${fePath}`); 116 | compileFileWithFeBinary(fePath, feVersion, file); 117 | 118 | const compilerResult = getCompileResultFromBinaryBuild(); 119 | log("compilerResult:", compilerResult); 120 | 121 | let contractNames = []; 122 | for (const key of Object.keys(compilerResult.contracts)) { 123 | const artifact = getArtifactFromFeOutput( 124 | sourceName, 125 | key, 126 | compilerResult.contracts[key] 127 | ); 128 | log("artifact:", artifact); 129 | // https://github.com/NomicFoundation/hardhat/blob/master/packages/hardhat-ethers/src/internal/helpers.ts#L20 130 | await artifacts.saveArtifactAndDebugFile(artifact); 131 | contractNames.push(artifact.contractName); 132 | } 133 | const artifactsImpl = artifacts as ArtifactsImpl; 134 | artifactsImpl.addValidArtifacts([{ sourceName: sourceName, artifacts: contractNames }]); 135 | 136 | cleanup(); 137 | } 138 | } 139 | } 140 | 141 | async function getFeSources(paths: ProjectPathsConfig) { 142 | const glob = await import("glob"); 143 | const feFiles = glob.sync(path.join(paths.sources, "**", "*.fe")); 144 | 145 | return feFiles; 146 | } 147 | 148 | function getArtifactFromFeOutput( 149 | sourceName: string, 150 | contractName: string, 151 | output: any 152 | ): Artifact { 153 | 154 | return { 155 | _format: ARTIFACT_FORMAT_VERSION, 156 | contractName, 157 | sourceName, 158 | abi: output.abi, 159 | bytecode: add0xPrefixIfNecessary(output.bytecode), 160 | deployedBytecode: "", //add0xPrefixIfNecessary(output.bytecode_runtime), 161 | linkReferences: {}, 162 | deployedLinkReferences: {}, 163 | }; 164 | } 165 | 166 | function add0xPrefixIfNecessary(hex: string): string { 167 | log("...hex..."); 168 | log(hex); 169 | hex = hex.toLowerCase(); 170 | 171 | if (hex.slice(0, 2) === "0x") { 172 | return hex; 173 | } 174 | 175 | return `0x${hex}`; 176 | } 177 | --------------------------------------------------------------------------------