diff --git a/README-contract-write.md b/README-contract-write.md new file mode 100644 index 0000000..ac834a5 --- /dev/null +++ b/README-contract-write.md @@ -0,0 +1,123 @@ +## ✨ New **write** Feature in the `contract` Command + +- **Call both read and write functions** directly from the contract menu, with intuitive function selection. +- **User-friendly prompts** for all argument types: + - Booleans (`true`, `false`, `1`, `0`, `yes`, `no`) + - Non-empty strings (empty input is rejected with a clear error) + - Comma-separated arrays (empty values are ignored) + - Payable RBTC values (auto-converted to wei) +- **Clear, actionable error messages** for invalid inputβ€”prevents contract calls until all arguments are valid. +- **Automatic explorer links** after each transaction, so you can easily check your transaction and contract address. + + +## πŸ“ Example Usage + +```sh +rsk-cli contract -a -t +``` +- Select a function (read or write) from the menu +- Enter arguments as prompted +- For payable functions, enter the amount in RBTC (e.g., `0.01`) + +## πŸ“š Example Contracts + +- **Various parameter types:** + ```sh + rsk-cli contract -a 0x429907ECe0c4E51417DAFA5EBcb2A2c1c2fbFF37 -t + ``` + Output example: + + ``` + βœ” Select a contract function to call or modify: setStoredData + ✍️ Write Functions + setAllData + setStoredAddress + setStoredBool + setStoredData + setStoredString + πŸ”Ž Read Functions + + πŸ“œ You selected: setStoredData + + βœ” Enter the value for argument _value (uint256): 7 + βœ” Enter your password to decrypt the wallet: ******** + βœ” βœ… Transaction sent! Hash: 0x1e90e7f238cf3d096e7a20a333c74f8495a563359385015d8ea79de7bca16464 + πŸ”— View transaction on Explorer: https://explorer.testnet.rootstock.io/tx/0x1e90e7f238cf3d096e7a20a333c74f8495a563359385015d8ea79de7bca16464 + πŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x429907ece0c4e51417dafa5ebcb2a2c1c2fbff37 + ``` + + +- **Payable function:** + ```sh + rsk-cli contract -a 0xeAe6CF2f7Ed752e7765856272Ad521410db34210 -t + ``` + Output example: + + ``` + ? Select a contract function to call or modify: + πŸ”Ž Read Functions + getBalance + ✍️ Write Functions + ❯ deposit + + πŸ“œ You selected: deposit + + βœ” Enter the value to send (in RBTC, e.g. 0.01): 0.0000823 + βœ” Enter your password to decrypt the wallet: ******** + βœ” βœ… Transaction sent! Hash: 0xdc1741250cb3e50527593846dd9d40e0c24db2b274853708563041ea2b04b97d + πŸ”— View transaction on Explorer: https://explorer.testnet.rootstock.io/tx/0xdc1741250cb3e50527593846dd9d40e0c24db2b274853708563041ea2b04b97d + πŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0xeae6cf2f7ed752e7765856272ad521410db34210 + ``` + +- **Array parameters:** + ```sh + rsk-cli contract -a 0x6156Cd50B74da35dA1857860EEa88591Cb584be9 -t + ``` + + Output example: + + ``` + ? Select a contract function to call or modify: + πŸ”Ž Read Functions + ❯ authorized + getAuthorized + ✍️ Write Functions + addAuthorized + + πŸ“œ You selected: addAuthorized + βœ” Enter the value for argument users (address[]): 0xF889Ad94a99fE80f1EEF42689ad7f274368B24DD + βœ” Enter your password to decrypt the wallet: ******** + βœ” βœ… Transaction sent! Hash: 0xe2fde34624e2143ef72fd3a95aefcdcff2df7ce426aca7cd4825b233d9a06e3f + πŸ”— View transaction on Explorer: https://explorer.testnet.rootstock.io/tx/0xe2fde34624e2143ef72fd3a95aefcdcff2df7ce426aca7cd4825b233d9a06e3f + πŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x6156cd50b74da35da1857860eea88591cb584be9 + + ``` + +--- + +## πŸ›‘οΈ Error Handling Example + +- This update provides more user-friendly input validation and clearer error messages than ethers.js typically offers. For example, it validates booleans, ensures required strings are not empty, and parses arrays from comma-separated input. Instead of generic or technical errors, users receive actionable feedback at the prompt, making the experience smoother and less error-prone. + +**Examples:** + +- If you enter an empty string for a required argument: + ``` + ❌ String argument cannot be empty. + ``` +- If you enter an invalid boolean value: + ``` + ❌ Invalid boolean value. Please enter true or false. + ``` +- If you enter a comma-separated list for an array argument, empty values are ignored: + ``` + (input: "a, ,b,,c") β†’ ["a", "b", "c"] + ``` + +> **Note:** +> Unlike ethers.js, which often throws generic or technical errors, this update provides clear, context-aware messages for supported types and prevents contract calls until all required inputs are valid. This results in a much smoother and safer user experience for the supported argument types. + +--- + + + diff --git a/src/commands/contract.ts b/src/commands/contract.ts index 003c03c..ef32161 100644 --- a/src/commands/contract.ts +++ b/src/commands/contract.ts @@ -2,6 +2,7 @@ import chalk from "chalk"; import ora from "ora"; import inquirer from "inquirer"; import ViemProvider from "../utils/viemProvider.js"; +import { parseEther } from "viem/utils"; type InquirerAnswers = { selectedFunction?: string; @@ -66,74 +67,157 @@ export async function ReadContract( item.type === "function" && (item.stateMutability === "view" || item.stateMutability === "pure") ); + const writeFunctions = abi.filter( + (item: any) => + item.type === "function" && + (item.stateMutability === "nonpayable" || item.stateMutability === "payable") + ); - if (readFunctions.length === 0) { + if (readFunctions.length === 0 && writeFunctions.length === 0) { spinner.stop(); - console.log(chalk.yellow("⚠️ No read functions found in the contract.")); + console.log(chalk.yellow("⚠️ No read or write functions found in the contract.")); return; } spinner.stop(); - const questions: any = [ + const choices = []; + if (readFunctions.length > 0) { + choices.push(new inquirer.Separator("πŸ”Ž Read Functions")); + choices.push(...readFunctions.map((item: any) => ({ + name: item.name, + value: { type: "read", name: item.name } + }))); + } + if (writeFunctions.length > 0) { + choices.push(new inquirer.Separator("✍️ Write Functions")); + choices.push(...writeFunctions.map((item: any) => ({ + name: item.name, + value: { type: "write", name: item.name } + }))); + } + + const { selectedFunction } = await inquirer.prompt([ { type: "list", name: "selectedFunction", - message: "Select a read function to call:", - choices: [...readFunctions.map((item: any) => item.name)], - }, - ]; - - const { selectedFunction } = await inquirer.prompt( - questions - ); + message: "Select a contract function to call or modify:", + choices + } + ]); console.log( - chalk.green(`πŸ“œ You selected: ${chalk.cyan(selectedFunction)}\n`) + chalk.green(`πŸ“œ You selected: ${chalk.cyan(selectedFunction.name)}\n`) ); - const selectedAbiFunction = readFunctions.find( - (item: any) => item.name === selectedFunction - ); + const selectedAbiFunction = + (selectedFunction.type === "read" + ? readFunctions + : writeFunctions + ).find((item: any) => item.name === selectedFunction.name); let args: any[] = []; - if (selectedAbiFunction.inputs && selectedAbiFunction.inputs.length > 0) { - const argQuestions = selectedAbiFunction.inputs.map((input: any) => ({ - type: "input", - name: input.name, - message: `Enter the value for argument ${chalk.yellow( - input.name - )} (${chalk.yellow(input.type)}):`, - })); - - const answers = await inquirer.prompt(argQuestions); - args = selectedAbiFunction.inputs.map( - (input: any) => answers[input.name] - ); + try { + if (selectedAbiFunction.inputs && selectedAbiFunction.inputs.length > 0) { + const argQuestions = selectedAbiFunction.inputs.map((input: any) => ({ + type: "input", + name: input.name, + message: `Enter the value for argument ${chalk.yellow( + input.name + )} (${chalk.yellow(input.type)}):`, + })); + + const answers = await inquirer.prompt(argQuestions); + args = selectedAbiFunction.inputs.map((input: any) => { + let val = answers[input.name]; + if (input.type === "bool") { + if (typeof val === "string") { + if (val.toLowerCase() === "true" || val === "1" || val.toLowerCase() === "yes") return true; + if (val.toLowerCase() === "false" || val === "0" || val.toLowerCase() === "no") return false; + } else if (typeof val === "boolean") { + return val; + } + throw new Error("Invalid boolean value. Please enter true or false."); + } + if (input.type === "string") { + val = val.trim(); + if (val.length === 0) { + throw new Error("String argument cannot be empty."); + } + return val; + } + if (input.type.endsWith("[]")) { + return val.split(",").map((v: string) => v.trim()).filter((v: string) => v.length > 0); + } + return val; + }); + } + } catch (err: any) { + console.log(chalk.red(`❌ ${err.message}`)); + return; } - spinner.start("⏳ Calling read function..."); - - const provider = new ViemProvider(testnet); - const publicClient = await provider.getPublicClient(); - - try { - const data = await publicClient.readContract({ - address, - abi, - functionName: selectedFunction, - args, - }); + let value; + if (selectedAbiFunction.stateMutability === "payable") { + const { valueInput } = await inquirer.prompt([ + { + type: "input", + name: "valueInput", + message: "Enter the value to send (in RBTC, e.g. 0.01):", + }, + ]); + + value = parseEther(valueInput); // Converts RBTC string to wei (BigInt) + } - spinner.stop(); - console.log( - chalk.green(`βœ… Function ${selectedFunction} called successfully!`) - ); - spinner.succeed(chalk.white(`πŸ”§ Result:`) + " " + chalk.green(data)); - } catch (error) { - spinner.fail( - `❌ Error while calling function ${chalk.cyan(selectedFunction)}.` - ); + if (selectedFunction.type === "read") { + spinner.start("⏳ Calling read function..."); + const provider = new ViemProvider(testnet); + const publicClient = await provider.getPublicClient(); + try { + const data = await publicClient.readContract({ + address, + abi, + functionName: selectedFunction.name, + args, + }); + spinner.stop(); + console.log( + chalk.green(`βœ… Function ${selectedFunction.name} called successfully!`) + ); + spinner.succeed(chalk.white(`πŸ”§ Result:`) + " " + chalk.green(data)); + } catch (error) { + spinner.fail( + `❌ Error while calling function ${chalk.cyan(selectedFunction.name)}.` + ); + } + } else { + const provider = new ViemProvider(testnet); + const walletClient = await provider.getWalletClient(); + + if (!walletClient.account) { + throw new Error("No account found in wallet client. Please check your wallet setup."); + } + + try { + spinner.start("⏳ Sending transaction (write function)..."); + const txHash = await walletClient.writeContract({ + address, + abi, + functionName: selectedFunction.name, + args, + account: walletClient.account, + chain: provider.chain, + ...(value !== undefined ? { value } : {}), + }); + spinner.succeed(chalk.green(`βœ… Transaction sent! Hash: ${txHash}`)); + const explorerUrl = testnet + ? `https://explorer.testnet.rootstock.io/tx/${txHash}` + : `https://explorer.rootstock.io/tx/${txHash}`; + console.log(chalk.white(`πŸ”— View transaction on Explorer:`), chalk.dim(explorerUrl)); + } catch (error: any) { + spinner.fail(`❌ Error while sending transaction: ${error}`); + } } const explorerUrl = testnet @@ -147,4 +231,4 @@ export async function ReadContract( } catch (error) { spinner.fail("❌ Error during contract interaction."); } -} +} \ No newline at end of file