Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions README-contract-write.md
Original file line number Diff line number Diff line change
@@ -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 <contract-address> -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.

---



184 changes: 134 additions & 50 deletions src/commands/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<InquirerAnswers>(
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
Expand All @@ -147,4 +231,4 @@ export async function ReadContract(
} catch (error) {
spinner.fail("❌ Error during contract interaction.");
}
}
}