diff --git a/README.md b/README.md index b0ab8c1b..43f2dc57 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ Read more about the network used in tests in the [Testing network](#testing-netw Inside the tests, use the following *modus operandi* (comparable to the [official Python tutorial](https://www.cairo-lang.org/docs/hello_starknet/unit_tests.html)): ```javascript const { expect } = require("chai"); -const { getStarknetContract } = require("hardhat"); +const { starknet } = require("hardhat"); describe("Starknet", function () { this.timeout(300_000); // 5 min - it("Should work", async function () { - const contract = await getStarknetContract("MyContract"); // assumes there is a file MyContract.cairo - await contract.deploy(); + it("should work for a fresh deployment", async function () { + const contractFactory = await starknet.getContractFactory("MyContract"); // assumes there is a file MyContract.cairo + const contract = await contractFactory.deploy(); console.log("Deployed at", contract.address); await contract.invoke("increase_balance", [10]); // invoke method by name and pass arguments in an array await contract.invoke("increase_balance", [20]); @@ -49,6 +49,12 @@ describe("Starknet", function () { const balance = parseInt(balanceStr); expect(balance).to.equal(30); }); + + it("should work for a previously deployed contract", async function () { + const contractFactory = await starknet.getContractFactory("MyContract"); // assumes there is a file MyContract.cairo + const contract = contractFactory.getContractAt("0x123..."); // you might wanna put an actual address here + await contract.invoke(...); + }); }); ``` @@ -77,8 +83,8 @@ A list of available versions can be found [here](https://hub.docker.com/r/shardl module.exports = { ... cairo: { - // Defaults to "latest" - version: "0.4.1" + // Defaults to the latest version + version: "0.4.2" } ... }; diff --git a/package.json b/package.json index 4734b328..c262d55d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shardlabs/starknet-hardhat-plugin", - "version": "0.1.5", + "version": "0.2.0", "description": "Plugin for using Starknet tools within Hardhat projects", "main": "dist/index.js", "files": [ diff --git a/scripts/test.sh b/scripts/test.sh index 01b0cc5b..112c43a6 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,19 +6,23 @@ git clone git@github.com:Shard-Labs/starknet-hardhat-example.git cd starknet-hardhat-example npm install +CONFIG_FILE_NAME="hardhat.config.ts" + TOTAL=0 SUCCESS=0 for TEST_CASE in ../test/*; do - CONFIG_FILE="$TEST_CASE/hardhat.config.js" - if [ ! -f "$CONFIG_FILE" ]; then - echo "Skipping; no config file provided" + TOTAL=$((TOTAL + 1)) + TEST_NAME=$(basename $TEST_CASE) + echo "Test $TOTAL) $TEST_NAME" + + CONFIG_FILE_PATH="$TEST_CASE/$CONFIG_FILE_NAME" + if [ ! -f "$CONFIG_FILE_PATH" ]; then + echo "No config file provided!" continue fi - /bin/cp "$CONFIG_FILE" hardhat.config.js - TEST_NAME=$(basename $TEST_CASE) - TOTAL=$((TOTAL + 1)) - echo "Test $TOTAL) $TEST_NAME" + #replace the dummy config (config_file_name) with the one of this test (config_file_path) + /bin/cp "$CONFIG_FILE_PATH" "$CONFIG_FILE_NAME" INIT_SCRIPT="$TEST_CASE/init.sh" if [ -f "$INIT_SCRIPT" ]; then diff --git a/src/constants.ts b/src/constants.ts index 52154466..816e7746 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,7 @@ export const ABI_SUFFIX = "_abi.json"; export const DEFAULT_STARKNET_SOURCES_PATH = "contracts"; export const DEFAULT_STARKNET_ARTIFACTS_PATH = "starknet-artifacts"; export const DOCKER_REPOSITORY = "shardlabs/cairo-cli"; -export const DEFAULT_DOCKER_IMAGE_TAG = "latest"; +export const DEFAULT_DOCKER_IMAGE_TAG = "0.4.2"; export const DEFAULT_STARKNET_NETWORK = "alpha"; export const ALPHA_URL = "https://alpha2.starknet.io:443" +export const CHECK_STATUS_TIMEOUT = 1000; // ms diff --git a/src/index.ts b/src/index.ts index 505bffb5..de205879 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ import * as path from "path"; import * as fs from "fs"; import { task, extendEnvironment, extendConfig } from "hardhat/config"; -import { HardhatPluginError } from "hardhat/plugins"; +import { HardhatPluginError, lazyObject } from "hardhat/plugins"; import { ProcessResult } from "@nomiclabs/hardhat-docker"; import "./type-extensions"; -import { DockerWrapper, StarknetContract } from "./types"; +import { DockerWrapper, StarknetContractFactory } from "./types"; import { PLUGIN_NAME, ABI_SUFFIX, DEFAULT_STARKNET_SOURCES_PATH, DEFAULT_STARKNET_ARTIFACTS_PATH, DEFAULT_DOCKER_IMAGE_TAG, DOCKER_REPOSITORY, DEFAULT_STARKNET_NETWORK, ALPHA_URL } from "./constants"; import { HardhatConfig, HardhatUserConfig, HttpNetworkConfig } from "hardhat/types"; import { adaptLog } from "./utils"; @@ -266,44 +266,52 @@ task("starknet-deploy", "Deploys Starknet contracts which have been compiled.") }); extendEnvironment(hre => { - hre.getStarknetContract = async contractName => { - let metadataPath: string; - await traverseFiles( - hre.config.paths.starknetArtifacts, - file => path.basename(file) === `${contractName}.json`, - async file => { - metadataPath = file; - return 0; + hre.starknet = lazyObject(() => ({ + getContractFactory: async contractName => { + let metadataPath: string; + await traverseFiles( + hre.config.paths.starknetArtifacts, + file => path.basename(file) === `${contractName}.json`, + async file => { + metadataPath = file; + return 0; + } + ); + if (!metadataPath) { + throw new HardhatPluginError(PLUGIN_NAME, `Could not find metadata for ${contractName}`); } - ); - if (!metadataPath) { - throw new HardhatPluginError(PLUGIN_NAME, `Could not find metadata for ${contractName}`); - } - let abiPath: string; - await traverseFiles( - hre.config.paths.starknetArtifacts, - file => path.basename(file) === `${contractName}${ABI_SUFFIX}`, - async file => { - abiPath = file; - return 0; + let abiPath: string; + await traverseFiles( + hre.config.paths.starknetArtifacts, + file => path.basename(file) === `${contractName}${ABI_SUFFIX}`, + async file => { + abiPath = file; + return 0; + } + ); + if (!abiPath) { + throw new HardhatPluginError(PLUGIN_NAME, `Could not find ABI for ${contractName}`); } - ); - if (!abiPath) { - throw new HardhatPluginError(PLUGIN_NAME, `Could not find ABI for ${contractName}`); - } - const testNetworkName = hre.config.mocha.starknetNetwork || DEFAULT_STARKNET_NETWORK; - const testNetwork: HttpNetworkConfig = hre.config.networks[testNetworkName]; - if (!testNetwork) { - const msg = `Network ${testNetworkName} is specified under "mocha.starknetNetwork", but not defined in "networks".`; - throw new HardhatPluginError(PLUGIN_NAME, msg); - } + const testNetworkName = hre.config.mocha.starknetNetwork || DEFAULT_STARKNET_NETWORK; + const testNetwork: HttpNetworkConfig = hre.config.networks[testNetworkName]; + if (!testNetwork) { + const msg = `Network ${testNetworkName} is specified under "mocha.starknetNetwork", but not defined in "networks".`; + throw new HardhatPluginError(PLUGIN_NAME, msg); + } - if (!testNetwork.url) { - throw new HardhatPluginError(PLUGIN_NAME, `Cannot use network ${testNetworkName}. No "url" specified.`); - } + if (!testNetwork.url) { + throw new HardhatPluginError(PLUGIN_NAME, `Cannot use network ${testNetworkName}. No "url" specified.`); + } - return new StarknetContract(hre.dockerWrapper, metadataPath, abiPath, testNetwork.url); - } + return new StarknetContractFactory({ + dockerWrapper: hre.dockerWrapper, + metadataPath, + abiPath, + gatewayUrl: testNetwork.url, + feederGatewayUrl: testNetwork.url + }); + } + })); }); diff --git a/src/type-extensions.ts b/src/type-extensions.ts index 93d372c5..790cf497 100644 --- a/src/type-extensions.ts +++ b/src/type-extensions.ts @@ -1,4 +1,6 @@ -import { DockerWrapper, StarknetContract } from "./types"; +import "hardhat/types/config"; +import "hardhat/types/runtime"; +import { DockerWrapper, StarknetContract, StarknetContractFactory } from "./types"; type CairoConfig = { version: string; @@ -28,11 +30,24 @@ declare module "hardhat/types/config" { } } +type StarknetContractType = StarknetContract; +type StarknetContractFactoryType = StarknetContractFactory; + declare module "hardhat/types/runtime" { - export interface HardhatRuntimeEnvironment { + interface HardhatRuntimeEnvironment { dockerWrapper: DockerWrapper; - getStarknetContract: (name: string) => Promise; + /** + * Fetches a compiled contract by name. E.g. if the contract is defined in MyContract.cairo, + * the provided string should be `MyContract`. + * @param name the case-sensitive contract name + */ + starknet: { + getContractFactory: (name: string) => Promise; + } } + + type StarknetContract = StarknetContractType; + type StarknetContractFactory = StarknetContractFactoryType; } declare module "mocha" { diff --git a/src/types.ts b/src/types.ts index 9a2d1fe9..7e3253ea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import { HardhatDocker, Image } from "@nomiclabs/hardhat-docker"; import { HardhatPluginError } from "hardhat/plugins"; -import { PLUGIN_NAME } from "./constants"; +import { PLUGIN_NAME, CHECK_STATUS_TIMEOUT } from "./constants"; import { adaptLog } from "./utils"; export class DockerWrapper { @@ -23,21 +23,37 @@ export class DockerWrapper { } } -export class StarknetContract { +export type StarknetContractFactoryConfig = StarknetContractConfig & { + metadataPath: string; +}; + +export interface StarknetContractConfig { + dockerWrapper: DockerWrapper; + abiPath: string; + gatewayUrl: string; + feederGatewayUrl: string; +} + +export class StarknetContractFactory { private dockerWrapper: DockerWrapper; - private metadataPath: string; private abiPath: string; - private address: string; + private metadataPath: string; private gatewayUrl: string; - - constructor(dockerWrapper: DockerWrapper, metadataPath: string, abiPath: string, gatewayUrl: string) { - this.dockerWrapper = dockerWrapper; - this.metadataPath = metadataPath; - this.abiPath = abiPath; - this.gatewayUrl = gatewayUrl; + private feederGatewayUrl: string; + + constructor(config: StarknetContractFactoryConfig) { + this.dockerWrapper = config.dockerWrapper; + this.abiPath = config.abiPath; + this.gatewayUrl = config.gatewayUrl; + this.feederGatewayUrl = config.feederGatewayUrl; + this.metadataPath = config.metadataPath; } - async deploy() { + /** + * Deploy a contract instance to a new address. + * @returns the newly created instance + */ + async deploy(): Promise { const docker = await this.dockerWrapper.getDocker(); const executed = await docker.runContainer( this.dockerWrapper.image, @@ -59,10 +75,51 @@ export class StarknetContract { } const matched = executed.stdout.toString().match(/^Contract address: (.*)$/m); - this.address = matched[1]; - if (!this.address) { + const address = matched[1]; + if (!address) { throw new HardhatPluginError(PLUGIN_NAME, "Could not extract the address from the deployment response."); } + + const contract = new StarknetContract({ + abiPath: this.abiPath, + dockerWrapper: this.dockerWrapper, + feederGatewayUrl: this.feederGatewayUrl, + gatewayUrl: this.gatewayUrl + }); + contract.address = address; + return contract; + } + + /** + * Returns a contract instance with set address. + * No address validity checks are performed. + * @param address the address of a previously deployed contract + * @returns a contract instance + */ + getContractAt(address: string) { + const contract = new StarknetContract({ + abiPath: this.abiPath, + dockerWrapper: this.dockerWrapper, + feederGatewayUrl: this.feederGatewayUrl, + gatewayUrl: this.gatewayUrl + }); + contract.address = address; + return contract; + } +} + +export class StarknetContract { + private dockerWrapper: DockerWrapper; + private abiPath: string; + public address: string; + private gatewayUrl: string; + private feederGatewayUrl: string; + + constructor(config: StarknetContractConfig) { + this.dockerWrapper = config.dockerWrapper; + this.abiPath = config.abiPath; + this.gatewayUrl = config.gatewayUrl; + this.feederGatewayUrl = config.feederGatewayUrl; } private async invokeOrCall(kind: "invoke" | "call", functionName: string, functionArgs: string[] = []) { @@ -77,7 +134,7 @@ export class StarknetContract { "--abi", this.abiPath, "--function", functionName, "--gateway_url", this.gatewayUrl, - "--feeder_gateway_url", this.gatewayUrl + "--feeder_gateway_url", this.feederGatewayUrl ]; if (functionArgs.length) { @@ -104,7 +161,7 @@ export class StarknetContract { return executed; } - async checkStatus(txID: string) { + private async checkStatus(txID: string) { const docker = await this.dockerWrapper.getDocker(); const executed = await docker.runContainer( this.dockerWrapper.image, @@ -112,7 +169,7 @@ export class StarknetContract { "starknet", "tx_status", "--id", txID, "--gateway_url", this.gatewayUrl, - "--feeder_gateway_url", this.gatewayUrl + "--feeder_gateway_url", this.feederGatewayUrl ] ); @@ -129,17 +186,23 @@ export class StarknetContract { } } - async iterativelyCheckStatus(txID: string, resolve: () => void) { - const TIMEOUT = 1000; // ms + private async iterativelyCheckStatus(txID: string, resolve: () => void) { + const timeout = CHECK_STATUS_TIMEOUT; // ms const status = await this.checkStatus(txID); if (["PENDING", "ACCEPTED_ONCHAIN"].includes(status)) { resolve(); } else { - setTimeout(this.iterativelyCheckStatus.bind(this), TIMEOUT, txID, resolve); + setTimeout(this.iterativelyCheckStatus.bind(this), timeout, txID, resolve); } } - async invoke(functionName: string, functionArgs: string[]): Promise { + /** + * Invoke the function by name and optionally provide arguments in an array. + * @param functionName + * @param functionArgs + * @returns a Promise that resolves when the status of the transaction is at least `PENDING` + */ + async invoke(functionName: string, functionArgs: any[] = []): Promise { const executed = await this.invokeOrCall("invoke", functionName, functionArgs); const matched = executed.stdout.toString().match(/^Transaction ID: (.*)$/m); @@ -150,7 +213,13 @@ export class StarknetContract { }); } - async call(functionName: string, functionArgs: string[]): Promise { + /** + * Call the function by name and optionally provide arguments in an array. + * @param functionName + * @param functionArgs + * @returns a Promise that resolves when the status of the transaction is at least `PENDING` + */ + async call(functionName: string, functionArgs: any[] = []): Promise { const executed = await this.invokeOrCall("call", functionName, functionArgs); return executed.stdout.toString(); } diff --git a/test/plain/hardhat.config.js b/test/plain/hardhat.config.js deleted file mode 100644 index 93b078a3..00000000 --- a/test/plain/hardhat.config.js +++ /dev/null @@ -1,4 +0,0 @@ -require("../dist/index.js"); - -module.exports = { -}; diff --git a/test/plain/hardhat.config.ts b/test/plain/hardhat.config.ts new file mode 100644 index 00000000..b5db9aa9 --- /dev/null +++ b/test/plain/hardhat.config.ts @@ -0,0 +1,4 @@ +import "../dist/index.js"; + +module.exports = { +}; diff --git a/test/with-artifacts-path/hardhat.config.js b/test/with-artifacts-path/hardhat.config.ts similarity index 76% rename from test/with-artifacts-path/hardhat.config.js rename to test/with-artifacts-path/hardhat.config.ts index 0f9b042d..2e670215 100644 --- a/test/with-artifacts-path/hardhat.config.js +++ b/test/with-artifacts-path/hardhat.config.ts @@ -1,4 +1,4 @@ -require("../dist/index.js"); +import "../dist/index.js"; module.exports = { paths: { diff --git a/test/with-cairo-version/hardhat.config.js b/test/with-cairo-version/hardhat.config.ts similarity index 69% rename from test/with-cairo-version/hardhat.config.js rename to test/with-cairo-version/hardhat.config.ts index fccbdf6c..2524fee7 100644 --- a/test/with-cairo-version/hardhat.config.js +++ b/test/with-cairo-version/hardhat.config.ts @@ -1,4 +1,4 @@ -require("../dist/index.js"); +import "../dist/index.js"; module.exports = { cairo: { diff --git a/test/with-cli-paths/hardhat.config.js b/test/with-cli-paths/hardhat.config.js deleted file mode 100644 index 93b078a3..00000000 --- a/test/with-cli-paths/hardhat.config.js +++ /dev/null @@ -1,4 +0,0 @@ -require("../dist/index.js"); - -module.exports = { -}; diff --git a/test/with-cli-paths/hardhat.config.ts b/test/with-cli-paths/hardhat.config.ts new file mode 100644 index 00000000..b5db9aa9 --- /dev/null +++ b/test/with-cli-paths/hardhat.config.ts @@ -0,0 +1,4 @@ +import "../dist/index.js"; + +module.exports = { +}; diff --git a/test/with-mocha-starknetNetwork/hardhat.config.js b/test/with-mocha-starknetNetwork/hardhat.config.ts similarity index 87% rename from test/with-mocha-starknetNetwork/hardhat.config.js rename to test/with-mocha-starknetNetwork/hardhat.config.ts index dea61b73..5c1877e7 100644 --- a/test/with-mocha-starknetNetwork/hardhat.config.js +++ b/test/with-mocha-starknetNetwork/hardhat.config.ts @@ -1,4 +1,4 @@ -require("../dist/index.js"); +import "../dist/index.js"; module.exports = { networks: { diff --git a/test/with-sources-path/hardhat.config.js b/test/with-sources-path/hardhat.config.ts similarity index 75% rename from test/with-sources-path/hardhat.config.js rename to test/with-sources-path/hardhat.config.ts index c6c481f7..553e45f9 100644 --- a/test/with-sources-path/hardhat.config.js +++ b/test/with-sources-path/hardhat.config.ts @@ -1,4 +1,4 @@ -require("../dist/index.js"); +import "../dist/index.js"; module.exports = { paths: { diff --git a/tsconfig.json b/tsconfig.json index 91b04abf..23b69394 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "allowJs": true, - "target": "es6", - "sourceMap": true, - "esModuleInterop": true, - "strict": true, - "outDir": "dist", - "resolveJsonModule": true, - "strictNullChecks": false, - "declaration": true, - "module": "commonjs" - }, - "include": ["./src"] - } + "compilerOptions": { + "allowJs": true, + "target": "es6", + "sourceMap": true, + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "resolveJsonModule": true, + "strictNullChecks": false, + "declaration": true, + "module": "commonjs" + }, + "include": ["./src"] +}