diff --git a/src/index.ts b/src/index.ts index 561e7275..609498cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ import { DevnetUtils } from "./devnet-utils"; import { ExternalServer } from "./external-server"; import { ArgentAccount, OpenZeppelinAccount } from "./account"; import { AmarnaDocker } from "./external-server/docker-amarna"; +import { StarknetLegacyWrapper } from "./starknet-js-wrapper"; exitHook(() => { ExternalServer.cleanAll(); @@ -150,11 +151,8 @@ extendConfig((config: HardhatConfig) => { // set network as specified in userConfig extendConfig((config: HardhatConfig, userConfig: Readonly) => { - if (userConfig.starknet && userConfig.starknet.network) { - config.starknet.network = userConfig.starknet.network; - } else { - config.starknet.network = DEFAULT_STARKNET_NETWORK; - } + config.starknet.network = userConfig.starknet?.network ?? DEFAULT_STARKNET_NETWORK; + const networkConfig = getNetwork( config.starknet.network, config.networks, @@ -174,6 +172,8 @@ function setVenvWrapper(hre: HardhatRuntimeEnvironment, venvPath: string) { // add venv wrapper or docker wrapper of starknet extendEnvironment((hre) => { + hre.starknetJs = new StarknetLegacyWrapper(hre.config.starknet.networkConfig); + const venvPath = hre.config.starknet.venv; if (venvPath) { setVenvWrapper(hre, venvPath); diff --git a/src/starknet-js-wrapper.ts b/src/starknet-js-wrapper.ts new file mode 100644 index 00000000..0b18a25f --- /dev/null +++ b/src/starknet-js-wrapper.ts @@ -0,0 +1,91 @@ +import { ProcessResult } from "@nomiclabs/hardhat-docker"; +import { promises as fsp } from "fs"; +import { NetworkConfig } from "hardhat/types/config"; +import { BigNumberish, BlockIdentifier, json, provider as providerUtil, SequencerProvider } from "starknet"; + +export class StarknetJsWrapper { + public provider: SequencerProvider; + + constructor(networkConfig: NetworkConfig) { + this.provider = new SequencerProvider({ + baseUrl: networkConfig.url + }); + } +} + +/** + * StarknetLegacyWrapper is meant to facilitate the discontinuation of the Starknet CLI usage within StarknetWrapper + */ +export class StarknetLegacyWrapper extends StarknetJsWrapper { + private async readContract (contractPath: string) { + return json.parse((await fsp.readFile(contractPath)).toString("ascii")); + } + + private stringifyResponse (r: unknown) { + return typeof r !== "string" + ?`${json.stringify(r, undefined, "\n").replace(/\n+/g, "\n")}\n` + : r; + } + + private generateProcessResult (statusCode: number, stdout: string, stderr: string): ProcessResult { + return { + statusCode, + stdout, + stderr + } as unknown as ProcessResult; + } + + private async wrapProcessResult(p: Promise): Promise { + return p + .then((a) => this.generateProcessResult(0, this.stringifyResponse(a), "")) + .catch((e) => this.generateProcessResult(1, "", this.stringifyResponse(e))); + } + + public async declare( + contractPath: string, + senderAddress: string, + signature: string[], + nonce: string, + maxFee: string + ): Promise { + const contractJson = await this.readContract(contractPath); + const contract = providerUtil.parseContract(contractJson); + + return this.wrapProcessResult(this.provider.declareContract({ + contract, + senderAddress, + signature + }, { + nonce, + maxFee + }).then(({ class_hash, transaction_hash }) => + "DeprecatedDeclare transaction was sent.\n" + + `Contract class hash: ${class_hash}\n` + + `Transaction hash: ${transaction_hash}\n` + )); + } + + public async getTxStatus(txHash: BigNumberish): Promise { + return this.wrapProcessResult(this.provider.getTransactionStatus(txHash)); + } + + public async getTransactionTrace(txHash: BigNumberish): Promise { + return this.wrapProcessResult(this.provider.getTransactionTrace(txHash)); + } + + public async getTransactionReceipt(txHash: BigNumberish): Promise { + return this.wrapProcessResult(this.provider.getTransactionReceipt(txHash)); + } + + public async getTransaction(txHash: BigNumberish): Promise { + return this.wrapProcessResult(this.provider.getTransaction(txHash)); + } + + public async getBlock(blockIdentifier?: BlockIdentifier): Promise { + return this.wrapProcessResult(this.provider.getBlock(blockIdentifier)); + } + + public async getNonce(address: string, blockIdentifier?: BlockIdentifier): Promise { + return this.wrapProcessResult(this.provider.getNonceForAddress(address, blockIdentifier).then(BigInt)); + } +} diff --git a/src/starknet-wrappers.ts b/src/starknet-wrappers.ts index 4077604a..8d92269b 100644 --- a/src/starknet-wrappers.ts +++ b/src/starknet-wrappers.ts @@ -98,6 +98,7 @@ export abstract class StarknetWrapper { // it's dangerous because in getters (e.g. get gatewayUrl) we rely on it being initialized } + // _TODO: check if this is necessary and should be applied to starknet.js protected get gatewayUrl(): string { const url = this.hre.starknet.networkConfig.url; if (this.externalServer.isDockerDesktop) { @@ -127,6 +128,7 @@ export abstract class StarknetWrapper { public async execute( command: + // _TODO: remove | "starknet" | "starknet-compile-deprecated" | "get_class_hash" @@ -214,9 +216,13 @@ export abstract class StarknetWrapper { } public async declare(options: DeclareWrapperOptions): Promise { - const preparedOptions = this.prepareDeclareOptions(options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.declare( + options.contract, + options.sender, + options.signature, + options.nonce, + options.maxFee + ); } protected prepareCairoToSierraOptions(options: CairoToSierraOptions): string[] { @@ -336,27 +342,19 @@ export abstract class StarknetWrapper { } public async getTxStatus(options: TxHashQueryWrapperOptions): Promise { - const preparedOptions = this.prepareTxQueryOptions("tx_status", options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.getTxStatus(options.hash); } public async getTransactionTrace(options: TxHashQueryWrapperOptions): Promise { - const preparedOptions = this.prepareTxQueryOptions("get_transaction_trace", options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.getTransactionTrace(options.hash); } public async getTransactionReceipt(options: TxHashQueryWrapperOptions): Promise { - const preparedOptions = this.prepareTxQueryOptions("get_transaction_receipt", options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.getTransactionReceipt(options.hash); } public async getTransaction(options: TxHashQueryWrapperOptions): Promise { - const preparedOptions = this.prepareTxQueryOptions("get_transaction", options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return await this.hre.starknetJs.getTransaction(options.hash); } protected prepareBlockQueryOptions(options: BlockQueryWrapperOptions): string[] { @@ -382,9 +380,7 @@ export abstract class StarknetWrapper { } public async getBlock(options: BlockQueryWrapperOptions): Promise { - const preparedOptions = this.prepareBlockQueryOptions(options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.getBlock(options.hash ?? options.number); } protected prepareNonceQueryOptions(options: NonceQueryWrapperOptions): string[] { @@ -408,9 +404,7 @@ export abstract class StarknetWrapper { } public async getNonce(options: NonceQueryWrapperOptions): Promise { - const preparedOptions = this.prepareNonceQueryOptions(options); - const executed = await this.execute("starknet", preparedOptions); - return executed; + return this.hre.starknetJs.getNonce(options.address, options.blockHash ?? options.blockNumber); } public async getClassHash(artifactPath: string): Promise { diff --git a/src/type-extensions.ts b/src/type-extensions.ts index 9a727efb..f17778dd 100644 --- a/src/type-extensions.ts +++ b/src/type-extensions.ts @@ -8,6 +8,7 @@ import { Account } from "./account"; import { Transaction, TransactionReceipt, Block, TransactionTrace } from "./starknet-types"; import { StarknetChainId } from "./constants"; import { AmarnaDocker } from "./external-server/docker-amarna"; +import { StarknetLegacyWrapper } from "./starknet-js-wrapper"; declare module "hardhat/types/config" { export interface ProjectPathsUserConfig { @@ -87,6 +88,7 @@ declare module "hardhat/types/runtime" { starknetWrapper: StarknetWrapper; amarnaDocker: AmarnaDocker; starknet: StarknetTypes.Starknet; + starknetJs: StarknetLegacyWrapper } type StarknetContract = StarknetContractType; diff --git a/src/types/index.ts b/src/types/index.ts index 561e0b87..42f1da43 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { selector } from "starknet"; +import { SequencerProvider, selector } from "starknet"; import { adaptInputUtil, adaptOutputUtil } from "../adapt"; import { @@ -525,6 +525,10 @@ export class StarknetContract { return; } + get provider(): SequencerProvider { + return this.hre.starknetJs.provider; + } + /** * Set a custom abi and abi path to the contract * @param implementation the contract factory of the implementation to be set @@ -591,6 +595,19 @@ export class StarknetContract { args?: StringMap, options: InvokeOptions = {} ): Promise { + + // _TODO: revisit + // const { transaction_hash: txHash } = await this.provider.invokeFunction({ + // contractAddress: this.address, + // entrypoint: functionName, + // calldata: args, + // signature: options.signature.map(String) + // }, { + // nonce: options.nonce ?? await this.provider.getNonceForAddress(this.address), + // maxFee: options.maxFee, + // version: InteractChoice.INVOKE.transactionVersion + // }); + const executed = await this.interact(InteractChoice.INVOKE, functionName, args, options); const txHash = extractTxHash(executed.stdout.toString()); @@ -638,17 +655,25 @@ export class StarknetContract { args?: StringMap, options: CallOptions = {} ): Promise { - const adaptedOptions = defaultToPendingBlock(options); - const executed = await this.interact( - InteractChoice.CALL, - functionName, - args, - adaptedOptions - ); - if (options.rawOutput) { - return { response: executed.stdout.toString().split(" ") }; + try { + const adaptedOptions = defaultToPendingBlock(options); + + const { result } = await this.provider.callContract({ + contractAddress: this.address, + entrypoint: functionName, + calldata: args + }, adaptedOptions.blockNumber); + // align to legacy stdout output + const response = result.join(" "); + + if (options.rawOutput) { + return { response }; + } + return this.adaptOutput(functionName, response); + + } catch (error) { + throw new StarknetPluginError("Contract call failure", error as Error); } - return this.adaptOutput(functionName, executed.stdout.toString()); } /** @@ -690,13 +715,23 @@ export class StarknetContract { options: EstimateFeeOptions = {} ): Promise { const adaptedOptions = defaultToPendingBlock(options); - const executed = await this.interact( - InteractChoice.ESTIMATE_FEE, - functionName, - args, - adaptedOptions - ); - return parseFeeEstimation(executed.stdout.toString()); + + const result = await this.provider.getInvokeEstimateFee({ + contractAddress: this.address, + calldata: args, + signature: adaptedOptions.signature.map(String) + }, { + nonce: adaptedOptions.nonce ?? await this.provider.getNonceForAddress(this.address), + maxFee: adaptedOptions.maxFee, + version: InteractChoice.ESTIMATE_FEE.transactionVersion + }, options.blockNumber); + + return { + amount: result.overall_fee, + unit: "wei", + gas_price: result.gas_price, + gas_usage: result.gas_consumed + }; } /**