From 4872c115aa1292131f34c5f876f646f234688718 Mon Sep 17 00:00:00 2001 From: tonibcn Date: Wed, 9 Jul 2025 12:17:24 +0200 Subject: [PATCH 1/4] feat: add contract write feature to contract command --- src/commands/contract.ts | 118 +++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/src/commands/contract.ts b/src/commands/contract.ts index 003c03c..8d2da2d 100644 --- a/src/commands/contract.ts +++ b/src/commands/contract.ts @@ -66,35 +66,54 @@ 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) { @@ -112,28 +131,53 @@ export async function ReadContract( ); } - 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, - }); - - 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, + }); + 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 From 8af025ff556ce45516da8ad05da9372915e99cb5 Mon Sep 17 00:00:00 2001 From: tonibcn Date: Wed, 9 Jul 2025 12:52:44 +0200 Subject: [PATCH 2/4] feat: add robust argument handling and user input validation - Support for boolean, string, array, and payable argument types - Improved error messages for invalid or empty input values --- src/commands/contract.ts | 68 +++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/src/commands/contract.ts b/src/commands/contract.ts index 8d2da2d..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; @@ -116,19 +117,57 @@ export async function ReadContract( ).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; + } + + 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) } if (selectedFunction.type === "read") { @@ -169,6 +208,7 @@ export async function ReadContract( args, account: walletClient.account, chain: provider.chain, + ...(value !== undefined ? { value } : {}), }); spinner.succeed(chalk.green(`βœ… Transaction sent! Hash: ${txHash}`)); const explorerUrl = testnet @@ -191,4 +231,4 @@ export async function ReadContract( } catch (error) { spinner.fail("❌ Error during contract interaction."); } -} +} \ No newline at end of file From 61bd623fbbb3a294d274bc8ad1b56e6bbdbdcbc5 Mon Sep 17 00:00:00 2001 From: tonibcn Date: Wed, 9 Jul 2025 13:47:50 +0200 Subject: [PATCH 3/4] docs: add focused README-contract-write.md for new contract write feature - Documents new write support, robust argument handling, improved UX, and error feedback in the contract command. - Includes usage and concrete contract examples. --- README-contract-write.md | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 README-contract-write.md diff --git a/README-contract-write.md b/README-contract-write.md new file mode 100644 index 0000000..78400b2 --- /dev/null +++ b/README-contract-write.md @@ -0,0 +1,52 @@ +## ✨ New **write** feature in the `contract` Command + +- You can now select and call both read and **write functions** from the contract menu. +- Prompts support booleans (`true`, `false`, `1`, `0`, `yes`, `no`), non-empty strings, comma-separated arrays, and payable RBTC values (auto-converted to wei). +- Invalid input (e.g., empty string) triggers a clear, user-friendly error message and prevents the contract call. +- Enhanced UX: intuitive function selectors, clear prompts, and automatic explorer links to check your transactions and contract addresses. + + +## πŸ“ 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 + ``` +- **Payable function:** + ```sh + rsk-cli contract -a 0xeAe6CF2f7Ed752e7765856272Ad521410db34210 -t + ``` +- **Array parameters:** + ```sh + rsk-cli contract -a 0x6156Cd50B74da35dA1857860EEa88591Cb584be9 -t + ``` + +--- + +## πŸ›‘οΈ Error Handling Example + +- If you enter an empty string for a required argument: + ``` + ❌ String argument cannot be empty. + ``` + +--- + +## πŸ”— Explorer Links + +- After a write transaction, you’ll see: + - A link to the transaction details + - A link to the contract address + +--- + + From 30e675409a66a19cf43fac4bebf4a0f74d6c9ab5 Mon Sep 17 00:00:00 2001 From: tonibcn Date: Thu, 24 Jul 2025 14:10:36 +0200 Subject: [PATCH 4/4] docs: improve contract-write feature summary in README-contract-write --- README-contract-write.md | 95 +++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/README-contract-write.md b/README-contract-write.md index 78400b2..ac834a5 100644 --- a/README-contract-write.md +++ b/README-contract-write.md @@ -1,9 +1,13 @@ -## ✨ New **write** feature in the `contract` Command +## ✨ New **write** Feature in the `contract` Command -- You can now select and call both read and **write functions** from the contract menu. -- Prompts support booleans (`true`, `false`, `1`, `0`, `yes`, `no`), non-empty strings, comma-separated arrays, and payable RBTC values (auto-converted to wei). -- Invalid input (e.g., empty string) triggers a clear, user-friendly error message and prevents the contract call. -- Enhanced UX: intuitive function selectors, clear prompts, and automatic explorer links to check your transactions and contract addresses. +- **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 @@ -21,32 +25,99 @@ rsk-cli contract -a -t ```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"] + ``` ---- - -## πŸ”— Explorer Links - -- After a write transaction, you’ll see: - - A link to the transaction details - - A link to the contract address +> **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. --- +