diff --git a/README.md b/README.md index 2b31b25..3e1bfc2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,77 @@ # wasm-deploy -Usage for the example project: +Usage: -1. Pull the Solidity source files from git: `npx ts-node src/index.ts pull nabla` -2. Deploy: `npx ts-node src/index.ts deploy nabla --network foucoco` +1. Install the `wasm-deploy` tool globally: `npm install -g wasm-deploy` + +2. Initiate a project by moving to the desired folder, and running: `wasm-deploy init ` + This will create a local typescript project, and add `wasm-deploy` dependency to it. + +3. Edit `config.json` in the root project folder. + +4. Write deploy/tests scripts in corresponding project folders. + +5. Deploy or Test: + - Deploy: `wasm-deploy deploy --network ` + - Test: `wasm-deploy test --network ` Alternatively you can use the parameter `local` instead of `foucoco`. This expects that there is a local chain running on port `9944` – this is particularly useful to run together with the [foucoco-standalone](https://github.com/pendulum-chain/foucoco-standalone) node. +### Running with typescript +Alternatively, clone this repo and run the following commands: + +```npx ts-node src/cli.ts ...``` + # Required ``` brew install binaryen ``` +# Solang Compiler Config +If contracts are not pre-compiled, solang is used to compile contracts and obtain the meta-data. + +Run `wasm-deploy set-solang ` to point to the local binary of the compiler. + +# Project Config +## Contracts +Inside the config.json created in your project root folder, add the conctract information that will be used in deployments or testing. + +Contracts can either be pulled from a remote repository (defined elsewhere in config.json), or added to the project precompiled. + +Example pulling contract from remote repository: +``` +"contracts": { + "MyContract": { + "repository": "MyContracts", + "path": "path/to/my_contract.sol" + }, + ... +``` + + +Example using a pre-compiled version of the contract, including the metadata: +``` +"contracts": { + "MyPcContract": { + "path": "/local/path/to/myPcContract.contract", + "isPrecompiled": true + }, + ... +``` + +Example using local contract: +``` +"contracts": { + "MyContract": { + "path": "/local/path/to/myContract.sol", + }, + ... +``` +Keep in mind that if the contract has dependencies, they must also be locally available. + +# Example Project + +## ERC20 +Create a sample project with an ERC20 contract and tests: + +`wasm-deploy init sample -e erc20` diff --git a/package.json b/package.json index 548d49b..102a018 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,19 @@ "name": "wasm-deploy", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "./dist/index.js", "scripts": { - "start": "ts-node src/index.ts" + "start": "ts-node index.ts", + "build": "npx tsc", + "postbuild": "node dist/projectTemplates/copySampleFiles.js", + "prepublishOnly": "npm run build" }, "keywords": [], "author": "", "license": "ISC", + "bin": { + "wasm-deploy": "./dist/cli.js" + }, "devDependencies": { "@tsconfig/node16": "^1.0.4", "@types/blake2b": "^2.1.0", @@ -37,4 +43,4 @@ "workspaces": [ "packages/api-solang" ] -} +} \ No newline at end of file diff --git a/src/actions/compileContract.ts b/src/actions/compileContract.ts index 8cda032..d6756a9 100644 --- a/src/actions/compileContract.ts +++ b/src/actions/compileContract.ts @@ -6,6 +6,8 @@ import { runCommand } from "../utils/childProcess"; import { ContractSourcecodeId } from "../types"; import { ContractDeploymentState } from "../processScripts"; import { Project } from "../project"; +import { getSolangPath } from "../utils/config"; + export async function compileContract( contractId: ContractSourcecodeId, @@ -60,7 +62,7 @@ async function actuallyCompileContract( updateContractStatus("compiling"); const solangResult = await runCommand([ - "../clones/solang/target/release/solang", + getSolangPath(), "compile", "--no-strength-reduce", // temporary fix for https://github.com/hyperledger/solang/issues/1507 "--target", diff --git a/src/actions/compileScripts.ts b/src/actions/compileScripts.ts new file mode 100644 index 0000000..61b3179 --- /dev/null +++ b/src/actions/compileScripts.ts @@ -0,0 +1,44 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const folders = ['deploy', 'test', '.']; +export const distDir = 'dist'; + +export function compileInPlace(baseProjectDir: string): void { + + execSync(`npm run build`, { cwd: process.cwd(), stdio: 'inherit' }); + + //Move the compiled files to the project dir + for (const dir of folders) { + + moveCompiledFiles(path.join(distDir, dir), path.join(`./${baseProjectDir}`, dir)); + + + } + + fs.rmdirSync(distDir, { recursive: true }); +} + + +function moveCompiledFiles(fromDir: string, toDir: string) { + if (fs.existsSync(fromDir)) { + const files = fs.readdirSync(fromDir); + + for (const file of files) { + + if (path.extname(file) === '.js') { + const from = path.join(fromDir, file); + const to = path.join(toDir, file); + fs.renameSync(from, to); + } + } + } + +} + + + + + + diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..f43073e --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,11 @@ +#! /usr/bin/env node + +const cryptoWaitReady = require("@polkadot/util-crypto"); +const parseCommandLine = require("./commandLine"); + +async function main() { + await cryptoWaitReady.cryptoWaitReady(); + parseCommandLine.parseCommandLine(); +} + +main().catch((error) => console.log(error)); diff --git a/src/commandLine.ts b/src/commandLine.ts index 93f2945..2a70721 100644 --- a/src/commandLine.ts +++ b/src/commandLine.ts @@ -2,6 +2,8 @@ import { Command } from "commander"; import { deploy } from "./commands/deploy"; import { pull } from "./commands/pull"; import { runTestSuits } from "./commands/test"; +import { initializeProject } from "./commands/init"; +import { setSolangPath } from "./utils/config"; export function parseCommandLine() { const program = new Command(); @@ -37,5 +39,22 @@ export function parseCommandLine() { await runTestSuits({ projectFolder: str as string, network: options.network }); }); + program + .command("init") + .description("Create the boilerplate for a new project to be used with the cli") + .argument("", "project name") + .option('-e, --example ') + .action(async (str, options: Record) => { + await initializeProject({ projectName: str as string, example: options.example }); + }); + + program + .command("set-solang") + .description("Set the path to the solang compiler") + .argument("", "solang path") + .action(async (str) => { + setSolangPath(str); + }); + program.parse(); } diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 0958594..4fa4464 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -2,7 +2,8 @@ import { Address, ArgumentType, ContractSourcecodeId, DeployedContractId, NamedA import { connectToChain } from "../api/api"; import { createAnimatedTextContext } from "../utils/terminal"; import { processScripts } from "../processScripts"; -import { initializeProject } from "../project"; +import { initializeProject, isTypescript } from "../project"; +import { compileInPlace } from "../actions/compileScripts"; export interface DeployOptions { projectFolder: string; @@ -55,6 +56,12 @@ export interface DeployScript { } export async function deploy(options: DeployOptions) { + + //compile deploy scripts if we are NOT in typescript + if (!isTypescript()) { + compileInPlace(options.projectFolder); + } + const project = await initializeProject(options.projectFolder); const networkName = options.network; diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..d2be9c5 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,101 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; +import { distDir } from "../actions/compileScripts"; +import { generateTsConfig } from '../projectTemplates/tsconfigTemplate'; +import { generatePackageJson } from '../projectTemplates/packageJsonTemplate'; +import { generateConfigJson } from '../projectTemplates/configJson'; + +export interface InitOptions { + projectName: string; + example: string; +} + +export async function initializeProject({ projectName, example }: InitOptions): Promise { + + // Create and write tsconfig.json + let tsConfigPath = path.join(process.cwd(), 'tsconfig.json'); + if (!fs.existsSync(tsConfigPath)) { + const tsConfigJson = generateTsConfig(distDir, projectName); + fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfigJson, null, 2)); + } else { + throw Error("tsconfig.json conflict, please ensure there is no project in directory") + } + + // Create and write package.json + let packageConfigPath = path.join(process.cwd(), 'package.json'); + if (!fs.existsSync(packageConfigPath)) { + const packageJson = generatePackageJson(); + fs.writeFileSync(packageConfigPath, JSON.stringify(packageJson, null, 2)); + } else { + throw Error("package.json conflict, please ensure there is no project in directory") + } + + // Create project directory + const projectPath = path.join(process.cwd(), projectName); + fs.mkdirSync(projectPath, { recursive: true }); + + // Create subdirectories + fs.mkdirSync(path.join(projectPath, 'deploy')); + fs.mkdirSync(path.join(projectPath, 'test')); + + //populate example files + let contractObject = {}; + if (example) { + fs.mkdirSync(path.join(projectPath, 'contracts')); + contractObject = await populateExample(projectName, example); + } + + fs.mkdirSync(path.join(process.cwd(), `./${projectName}/target`)); + + // Create and write config.json + const configJson = generateConfigJson(contractObject); + fs.writeFileSync(path.join(projectPath, 'config.json'), JSON.stringify(configJson, null, 2)); + + + + + // Execute npm install in the project directory + execSync('npm install', { cwd: process.cwd(), stdio: 'inherit' }); + execSync('npm install --save-dev @types/node', { cwd: process.cwd(), stdio: 'inherit' }); + +} + +async function populateExample(projectName: string, exampleName: string): Promise { + + let rootDir = path.join(__dirname, '..'); + + + switch (exampleName) { + case "erc20": + //copy required contracts + const contracts = ['ERC20.sol', 'SafeMath.sol']; + for (const contract of contracts) { + let sourcePath = path.join(rootDir, `projectTemplates/rawFiles/contractSamples/${contract}`); + let targetPath = path.join(process.cwd(), `./${projectName}/contracts/${contract}`); + fs.copyFileSync(sourcePath, targetPath); + } + + //copy deploy sample script + let sourcePath = path.join(rootDir, 'projectTemplates/rawFiles/deploySamples/01_sample_deploy_erc20.ts'); + let targetPath = path.join(process.cwd(), `./${projectName}/deploy/01_sample_deploy_erc20.ts`); + fs.copyFileSync(sourcePath, targetPath); + + //copy test files + sourcePath = path.join(rootDir, 'projectTemplates/rawFiles/testSamples/sampleERC20Test.ts'); + targetPath = path.join(process.cwd(), `./${projectName}/test/sampleERC20Test.ts`); + fs.copyFileSync(sourcePath, targetPath); + + //contracts object that need to be declared in config.json + const contractsObj = { + "ERC20": { + "path": "./contracts/ERC20.sol" + } + }; + + return contractsObj + + } + + throw Error("example does not exists"); +} \ No newline at end of file diff --git a/src/commands/test.ts b/src/commands/test.ts index 28c9b9b..e6c74d6 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -2,13 +2,14 @@ import { readFile } from "node:fs/promises"; import { Address, ArgumentType, ContractSourcecodeId } from "../types"; import { ChainApi, connectToChain } from "../api/api"; -import { Project, initializeProject } from "../project"; +import { Project, initializeProject, isTypescript } from "../project"; import { StyledText, createAnimatedTextContext } from "../utils/terminal"; import { compileContract } from "../actions/compileContract"; import { toUnit } from "../utils/rationals"; import { SigningSubmitter, Submitter, getSubmitterAddress } from "../api/submitter"; import { PanicCode } from "@pendulum-chain/api-solang"; import { Codec } from "@polkadot/types-codec/types"; +import { compileInPlace } from "../actions/compileScripts"; export interface RunTestSuitesOptions { projectFolder: string; @@ -16,6 +17,12 @@ export interface RunTestSuitesOptions { } export async function runTestSuits(options: RunTestSuitesOptions) { + + //compile tests scripts if we are NOT in typescript + if (!isTypescript()) { + compileInPlace(options.projectFolder); + } + const project = await initializeProject(options.projectFolder); const networkName = options.network; @@ -370,7 +377,22 @@ async function processTestScripts( } addStaticText([[{ text: "Process test suite " }, { text: testSuiteName, color: "cyan" }]], false); - let testSuiteInstance = await testSuite.default(environmentForTestSuite); + + let testSuiteInstance: any; + try { + testSuiteInstance = await testSuite.default(environmentForTestSuite); + } catch (error) { + if (error instanceof TypeError && /is not a function/.test(error.message)) { + + let attemptedContractInstance = error.message.split(" ")[0].replace("new", "").trim(); + + throw Error(`Contract ${attemptedContractInstance} does not exist in project nabla, please add it in the config.json file`) + } else { + throw error; + } + + } + const tests = Object.keys(testSuiteInstance).filter((key) => key.startsWith("test") && key !== "setUp"); for (const test of tests) { diff --git a/src/index.ts b/src/index.ts index 0652125..6dc4287 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,3 @@ export { TestContract, TestSuiteEnvironment } from "./commands/test"; export * from "./testing/stdLib"; export { DeploymentsExtension, Network, TxOptions } from "./commands/deploy"; -async function main() { - await cryptoWaitReady(); - parseCommandLine(); -} - -main().catch((error) => console.log(error)); diff --git a/src/processScripts.ts b/src/processScripts.ts index 65c3d87..1fc955c 100644 --- a/src/processScripts.ts +++ b/src/processScripts.ts @@ -120,19 +120,19 @@ function renderMethodExecutionStatus( { text: functionName, color: "green" }, ...(transactionFee !== undefined || (gasRequired !== undefined && SHOW_ESTIMATED_GAS) ? [ - { text: ` [` }, - ...(transactionFee !== undefined ? [{ text: ` fee: ${chainApi.getAmountString(transactionFee)}` }] : []), - ...(gasRequired !== undefined && SHOW_ESTIMATED_GAS - ? [ - { - text: ` gasTime: ${String(gasRequired.refTime.toHuman())} gasProof: ${String( - gasRequired.proofSize.toHuman() - )}`, - }, - ] - : []), - { text: ` ]` }, - ] + { text: ` [` }, + ...(transactionFee !== undefined ? [{ text: ` fee: ${chainApi.getAmountString(transactionFee)}` }] : []), + ...(gasRequired !== undefined && SHOW_ESTIMATED_GAS + ? [ + { + text: ` gasTime: ${String(gasRequired.refTime.toHuman())} gasProof: ${String( + gasRequired.proofSize.toHuman() + )}`, + }, + ] + : []), + { text: ` ]` }, + ] : []), { text: ` ${failure ?? state}`, diff --git a/src/project.ts b/src/project.ts index 57dad78..b4fe840 100644 --- a/src/project.ts +++ b/src/project.ts @@ -27,6 +27,23 @@ export type RepositoryInitialization = "npm" | "yarn"; export type Project = ReturnType extends Promise ? T : never; +import * as path from "path"; + +export function isTypescript(): boolean { + const extension = path.extname(__filename); + if (extension === ".ts") { + return true; + } + + const lastArg = process.execArgv[process.execArgv.length - 1]; + if (lastArg && path.parse(lastArg).name.indexOf("ts-node") > 0) { + return true; + } + return false; + +} + + export async function initializeProject(relativeProjectPath: string, configFileName: string = "config.json") { console.log(`Load project in folder "${relativeProjectPath}"`); @@ -52,6 +69,8 @@ export async function initializeProject(relativeProjectPath: string, configFileN const getGitCloneFolder = (contractId: ContractSourcecodeId): string => { const contractSource = getContractConfiguration(contractId); + + //if repository undefined, aasume path in config starts from project root if (contractSource.repository === undefined) { return projectFolder; } @@ -207,11 +226,13 @@ export async function initializeProject(relativeProjectPath: string, configFileN }, async readDeploymentScripts(): Promise<[ScriptName, DeployScript][]> { + + const fileExtension = isTypescript() ? ".ts" : ".js"; const entries = await readdir(deployScriptsPath, { recursive: true, withFileTypes: true }); const fileNames = entries .filter((entry) => entry.isFile()) .map((entry) => entry.name) - .filter((fileName) => fileName !== configFileName); + .filter((fileName) => fileName.endsWith(fileExtension) && fileName !== configFileName); const scripts: [ScriptName, DeployScript][] = await Promise.all( fileNames.map(async (file) => { @@ -227,8 +248,9 @@ export async function initializeProject(relativeProjectPath: string, configFileN }, async readTests(): Promise<{ testSuitConfig: TestSuiteConfig; testSuites: [ScriptName, TestSuite][] }> { + const fileExtension = isTypescript() ? ".ts" : ".js"; const entries = await readdir(testsPath, { recursive: true, withFileTypes: true }); - const fileNames = entries.filter((entry) => entry.isFile()).map((entry) => entry.name); + const fileNames = entries.filter((entry) => entry.name.endsWith(fileExtension) && entry.isFile()).map((entry) => entry.name); const testSuites: [string, TestSuite][] = await Promise.all( fileNames.map(async (file) => { diff --git a/src/projectTemplates/configJson.ts b/src/projectTemplates/configJson.ts new file mode 100644 index 0000000..29492f7 --- /dev/null +++ b/src/projectTemplates/configJson.ts @@ -0,0 +1,43 @@ +export const generateConfigJson = (contracts: object): object => ({ + contracts, + "repositories": { + }, + "networks": { + "foucoco": { + "namedAccounts": { + "deployer": "6iFKMtX29zYoHRgDkPTXKuRsHRbJ3Gnaxyc4dRrZTANHvZi3" + }, + "rpcUrl": "wss://rpc-foucoco.pendulumchain.tech:443" + }, + "local": { + "namedAccounts": { + "deployer": { + "address": "6mfqoTMHrMeVMyKwjqomUjVomPMJ4AjdCm1VReFtk7Be8wqr", + "suri": "//Alice" + }, + "bob": { + "address": "6k6gXPB9idebCxqSJuqpjPaqfYLQbdLHhvsANH8Dg8GQN3tT", + "suri": "//Bob" + }, + "root": { + "address": "6hc7e55FaBEbQAHB7hFFU39CPvcrsW7QhM3Qv15S9cWjkK6t", + "suri": "//AltoParaíso" + } + }, + "rpcUrl": "ws://127.0.0.1:9944" + } + }, + "tests": { + "tester": "deployer", + "root": "root" + }, + "buildFolder": "./target", + "limits": { + "gas": { + "refTime": "100000000000", + "proofSize": "10000000" + }, + "storageDeposit": "10000000000000" + } +} +); \ No newline at end of file diff --git a/src/projectTemplates/copySampleFiles.ts b/src/projectTemplates/copySampleFiles.ts new file mode 100644 index 0000000..8942cbd --- /dev/null +++ b/src/projectTemplates/copySampleFiles.ts @@ -0,0 +1,31 @@ +import * as fs from 'fs'; +import * as path from 'path'; + + +function recursiveCopy(srcDir: string, destDir: string): void { + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + + for (const item of fs.readdirSync(srcDir)) { + const srcItem = path.join(srcDir, item); + const destItem = path.join(destDir, item); + + const stat = fs.statSync(srcItem); + + if (stat.isDirectory()) { + + recursiveCopy(srcItem, destItem); + } else { + + fs.copyFileSync(srcItem, destItem); + } + } +} + +const sourceDir = path.resolve(__dirname, '../../src/projectTemplates/rawFiles'); +const targetDir = path.resolve(__dirname, '../../dist/projectTemplates/rawFiles'); + +recursiveCopy(sourceDir, targetDir); \ No newline at end of file diff --git a/src/projectTemplates/packageJsonTemplate.ts b/src/projectTemplates/packageJsonTemplate.ts new file mode 100644 index 0000000..9839940 --- /dev/null +++ b/src/projectTemplates/packageJsonTemplate.ts @@ -0,0 +1,18 @@ +export const generatePackageJson = (): object => ({ + "name": "wasm-deploy-package-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "commonjs", + "dependencies": { + "wasm-deploy": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.6.3" + }, + "scripts": { + "build": "npx tsc" + }, + "author": "", + "license": "ISC" +}); \ No newline at end of file diff --git a/src/projectTemplates/rawFiles/contractSamples/ERC20.sol b/src/projectTemplates/rawFiles/contractSamples/ERC20.sol new file mode 100644 index 0000000..ef4b944 --- /dev/null +++ b/src/projectTemplates/rawFiles/contractSamples/ERC20.sol @@ -0,0 +1,120 @@ +pragma solidity ^0.6.0; + +// SPDX-License-Identifier: MIT + +import "./SafeMath.sol"; + +/** + @title Bare-bones Token implementation + @notice Based on the ERC-20 token standard as defined at + https://eips.ethereum.org/EIPS/eip-20 + */ +contract ERC20 { + + using SafeMath for uint256; + + string public symbol; + string public name; + uint256 public decimals; + uint256 public totalSupply; + + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowed; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor( + string memory _name, + string memory _symbol, + uint256 _decimals, + uint256 _totalSupply + ) + public + { + name = _name; + symbol = _symbol; + decimals = _decimals; + totalSupply = _totalSupply; + balances[msg.sender] = _totalSupply; + emit Transfer(address(0), msg.sender, _totalSupply); + } + + /** + @notice Getter to check the current balance of an address + @param _owner Address to query the balance of + @return Token balance + */ + function balanceOf(address _owner) public view returns (uint256) { + return balances[_owner]; + } + + /** + @notice Getter to check the amount of tokens that an owner allowed to a spender + @param _owner The address which owns the funds + @param _spender The address which will spend the funds + @return The amount of tokens still available for the spender + */ + function allowance( + address _owner, + address _spender + ) + public + view + returns (uint256) + { + return allowed[_owner][_spender]; + } + + /** + @param _spender The address which will spend the funds. + @param _value The amount of tokens to be spent. + @return Success boolean + */ + function approve(address _spender, uint256 _value) public returns (bool) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /** shared logic for transfer and transferFrom */ + function _transfer(address _from, address _to, uint256 _value) internal { + require(balances[_from] >= _value, "Insufficient balance"); + balances[_from] = balances[_from].sub(_value); + balances[_to] = balances[_to].add(_value); + emit Transfer(_from, _to, _value); + } + + /** + @notice Transfer tokens to a specified address + @param _to The address to transfer to + @param _value The amount to be transferred + @return Success boolean + */ + function transfer(address _to, uint256 _value) public returns (bool) { + _transfer(msg.sender, _to, _value); + return true; + } + + /** + @notice Transfer tokens from one address to another + @param _from The address which you want to send tokens from + @param _to The address which you want to transfer to + @param _value The amount of tokens to be transferred + @return Success boolean + */ + function transferFrom( + address _from, + address _to, + uint256 _value + ) + public + returns (bool) + { + require(allowed[_from][msg.sender] >= _value, "Insufficient allowance"); + allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); + _transfer(_from, _to, _value); + return true; + } + +} \ No newline at end of file diff --git a/src/projectTemplates/rawFiles/contractSamples/SafeMath.sol b/src/projectTemplates/rawFiles/contractSamples/SafeMath.sol new file mode 100644 index 0000000..1b48c6b --- /dev/null +++ b/src/projectTemplates/rawFiles/contractSamples/SafeMath.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.6.0; + +library SafeMath { + + function add(uint a, uint b) internal pure returns (uint c) { + c = a + b; + require(c >= a); + return c; + } + + function sub(uint a, uint b) internal pure returns (uint c) { + require(b <= a); + c = a - b; + return c; + } + + function mul(uint a, uint b) internal pure returns (uint c) { + c = a * b; + require(a == 0 || c / a == b); + return c; + } + + function div(uint a, uint b) internal pure returns (uint c) { + require(b > 0); + c = a / b; + return c; + } + +} \ No newline at end of file diff --git a/src/projectTemplates/rawFiles/deploySamples/01_sample_deploy_erc20.ts b/src/projectTemplates/rawFiles/deploySamples/01_sample_deploy_erc20.ts new file mode 100644 index 0000000..bd7f591 --- /dev/null +++ b/src/projectTemplates/rawFiles/deploySamples/01_sample_deploy_erc20.ts @@ -0,0 +1,22 @@ +import { WasmDeployEnvironment } from "wasm-deploy"; + +async function DeployCurves({ getNamedAccounts, deployments }: WasmDeployEnvironment) { + const { deployer } = await getNamedAccounts(); + + await deployments.deploy("erc20-0.1", { + from: deployer, + contract: "ERC20", + args: ["MyToken", "MT", 12, 100_000_000n], + log: true, + }); + +} + +DeployCurves.tags = ["token"]; + +DeployCurves.skip = async function skip({ deployments }: WasmDeployEnvironment): Promise { + const alreadyDeployed = Boolean(await deployments.getOrNull("erc20-0.1")); + return alreadyDeployed; +}; + +export default DeployCurves; diff --git a/src/projectTemplates/rawFiles/testSamples/sampleERC20Test.ts b/src/projectTemplates/rawFiles/testSamples/sampleERC20Test.ts new file mode 100644 index 0000000..07cf7ae --- /dev/null +++ b/src/projectTemplates/rawFiles/testSamples/sampleERC20Test.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { TestSuiteEnvironment } from "wasm-deploy"; +import { assertEq } from "wasm-deploy"; + +const CHARLIE = "6k9LbZKC3dYDqaF6qhS9j438Vg1nawD98i8VuHRKxXSvf1rp"; +const BOB = "6k6gXPB9idebCxqSJuqpjPaqfYLQbdLHhvsANH8Dg8GQN3tT"; + +export default async function (environment: TestSuiteEnvironment) { + const { + constructors: { newERC20 }, + vm, + tester + } = environment; + + const erc20 = await newERC20("MyToken", "MT", 12, 100_000_000n); + + return { + async testApprove() { + vm.expectEmit(erc20, "Approval", [tester, CHARLIE, 100n]); + await erc20.approve(CHARLIE, 100); + + }, + + async testTransfer() { + await erc20.transfer(CHARLIE, 100); + let balanceOfRec = await erc20.balanceOf(CHARLIE); + + assertEq(100n, balanceOfRec, "transferred amount does not match") + }, + + async testTransferInsBalance() { + + vm.startPrank(BOB); + vm.expectRevert("Insufficient balance"); + await erc20.transfer(CHARLIE, 100); + vm.stopPrank(); + }, + + async testAllowance() { + await erc20.approve(BOB, 100); + + vm.startPrank(BOB); + await erc20.transferFrom(tester, CHARLIE, 80); + vm.stopPrank(); + + let balanceOfRec = await erc20.balanceOf(CHARLIE); + assertEq(80n, balanceOfRec, "transferred amount does not match") + }, + + async testInsuficientAllowanceForTransfer() { + await erc20.approve(BOB, 100); + + vm.startPrank(BOB); + vm.expectRevert("Insufficient allowance"); + await erc20.transferFrom(tester, CHARLIE, 120); + vm.stopPrank(); + + }, + + }; +} diff --git a/src/projectTemplates/tsconfigTemplate.ts b/src/projectTemplates/tsconfigTemplate.ts new file mode 100644 index 0000000..aed10b3 --- /dev/null +++ b/src/projectTemplates/tsconfigTemplate.ts @@ -0,0 +1,20 @@ +export const generateTsConfig = (distDir: string, projectName: string): object => ({ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "rootDir": `${projectName}`, + "outDir": distDir, + "declaration": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "types": [ + "node" + ] + }, + "include": [ + `${projectName}/**/*` + ] +}); \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..842ce4c --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const configPath = path.join(__dirname, 'config.json'); + +const readConfig = (): any => { + if (fs.existsSync(configPath)) { + const rawData = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(rawData); + } + return {}; +}; + +const writeConfig = (data: any): void => { + const jsonData = JSON.stringify(data, null, 2); + fs.writeFileSync(configPath, jsonData); +}; + +export function getSolangPath(): string { + + const currentConfig = readConfig(); + if (currentConfig.solangPath) { + return currentConfig.solangPath; + } else { + return "solang"; + } + +}; + +export function setSolangPath(path: string): void { + + let currentConfig = readConfig(); + currentConfig.solangPath = path; + writeConfig(currentConfig); + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b88e660..6c71c77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,25 @@ { "extends": "@tsconfig/node16/tsconfig.json", - "compilerOptions": { - "outDir": "build", + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", "declaration": true, - "typeRoots": ["./node_modules/@types"], - "types": ["node"] + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "types": [ + "node" + ] }, - - "include": ["src/**/*", "nabla/**/*"] -} + "include": [ + "src/**/*", + //"nabla/**/*" + ], + "exclude": [ + "src/projectTemplates/rawFiles/**/*" + ] +} \ No newline at end of file