diff --git a/.gitignore b/.gitignore index 0c2a3e0..280ff42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ rootstock-wallet.json rootstock-wallet-dev.json -#lock files +#lock files pnpm-lock.yaml bun.lockb # Logs @@ -164,6 +164,7 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +.vscode # yarn v2 diff --git a/README.md b/README.md index e2a94a7..1342983 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,25 @@ `rsk-cli` is a command-line tool for interacting with Rootstock blockchain +## Table of Contents + +- [Installation](#installation) +- [Configuration](#configuration) +- [Features](#features) + 1. [Manage Wallet](#1-manage-wallet) + 2. [Check Balance](#2-check-balance) + 3. [Transfer](#3-transfer-rbtc-and-erc20) + 4. [Check Transaction Status](#4-check-transaction-status) + 5. [Deploy Smart Contract](#5-deploy-smart-contract) + 6. [Verify Smart Contract](#6-verify-smart-contract) + 7. [Interact with Verified Contracts](#7-interact-with-verified-smart-contracts) + 8. [Interact with RSK Bridge](#8-interact-with-rsk-bridge-contract) + 9. [Fetch Wallet History](#9-fetch-wallet-history) + 10. [Batch Transfer](#10-batch-transfer) + 11. [Transaction Simulation](#11-transaction-simulation) + 12. [RNS Operations](#12-rns-operations) +- [Contributing](#contributing) + ## Installation To install the CLI tool globally, use the following command: @@ -232,7 +251,7 @@ Use the `-t` or `--testnet` flag to check the balance on the Rootstock testnet. # Check balance on testnet rsk-cli balance -t -# Check balance using RNS domain on testnet +# Check balance using RNS domain on testnet rsk-cli balance -t --rns testing.rsk ``` @@ -851,27 +870,113 @@ The simulation provides comprehensive information: > **Note**: Simulation uses real blockchain state but does not execute transactions. It provides accurate estimates based on current network conditions. Gas prices may vary, so actual costs might differ slightly from simulation results. -### 12. RNS Resolve +### 12. RNS Operations +The `rns` command provides a unified interface to interact with the RIF Name Service (RNS). You can Register, Transfer, Update, and Resolve domains using specific flags. + +#### 1. Register a Domain + +Secure a `.rsk` domain name through a two-step commitment process. Requires a wallet with RBTC for gas and RIF for the registration fee. + +##### Mainnet + +```bash +rsk-cli rns --register .rsk --wallet +``` + +##### Testnet + +```bash +rsk-cli rns --register .rsk --wallet --testnet +``` + +##### Output example: -The `resolve` command allows you to interact with the RIF Name Service (RNS) on the Rootstock blockchain. You can perform both forward resolution (domain to address) and reverse resolution (address to domain name). +``` +🔍 Checking availability for 'mycoolname.rsk'... +Price: 2.0 tRIF +Step 1/2: Sending commitment... +✅ Commitment sent. +⏳ Waiting for commitment maturity (approx 1 min)... +......... +Step 2/2: Registering domain... +Tx: https://rootstock-testnet.blockscout.com/tx/0x_transaction_hash + +✅ Success! 'mycoolname.rsk' is now registered to 0x123...FFf + +``` + +#### 2. Transfer Ownership + +Transfer the ownership of an existing domain to another address. Note: Recipient must be a raw address `(0x...)`, RNS names are not supported as recipient; use the `resolve` command first if needed. Requires: `--recipient` flag. + +##### Mainnet -#### Forward Resolution (Domain to Address) +```bash +rsk-cli rns --transfer .rsk --recipient --wallet +``` + +##### Testnet + +```bash +rsk-cli rns --transfer .rsk --recipient --wallet --testnet +``` + +##### Output example: + +``` +Preparing to transfer 'mycoolname.rsk' to 0x123...FFf +🔄 Transferring ownership... +Tx: https://rootstock-testnet.blockscout.com/tx/0x_transaction_hash +✅ Success! 'mycoolname.rsk' has been transferred to 0x123...FFf + +``` + +#### 3. Update Resolver + +Change the resolution address of a domain (where the domain "points" to). Requires: `--address` flag. + +##### Mainnet + +```bash +rsk-cli rns --update blessings.rsk --address --wallet +``` + +##### Testnet + +```bash +rsk-cli rns --update blessings.rsk --address --wallet --testnet +``` + +##### Output example: + +``` +Preparing to update records for 'mycoolname.rsk'... +🔄 Setting resolution address to 0x123...FFf +Tx: https://rootstock-testnet.blockscout.com/tx/0x_transaction_hash +✅ Success! 'mycoolname.rsk' now resolves to 0x123...FFf +``` + +#### 4. Resolve Domain + +Perform both forward resolution (domain to address) and reverse resolution (address to domain name). + +**Forward Resolution (Domain to Address):** Convert an RNS domain name to its associated address: ##### Mainnet ```bash -rsk-cli resolve testing.rsk +rsk-cli rns --resolve testing.rsk ``` ##### Testnet ```bash -rsk-cli resolve testing.rsk --testnet +rsk-cli rns --resolve testing.rsk --testnet ``` -Output example: +##### Output example: ``` 🔍 Resolving testing.rsk... @@ -881,20 +986,20 @@ Output example: 🌐 Network: Rootstock Mainnet ``` -#### Reverse Resolution (Address to Domain) +**Reverse Resolution (Address to Domain):** Convert an address back to its RNS domain name: ##### Mainnet ```bash -rsk-cli resolve 0x123456789abcdef0123456789abcdef012345678 --reverse +rsk-cli rns --resolve 0x123456789abcdef0123456789abcdef012345678 --reverse ``` ##### Testnet ```bash -rsk-cli resolve 0x123456789abcdef0123456789abcdef012345678 --reverse --testnet +rsk-cli rns --resolve 0x123456789abcdef0123456789abcdef012345678 --reverse --testnet ``` Output example: @@ -907,11 +1012,12 @@ Output example: 🌐 Network: Rootstock Testnet ``` -> **Note**: +> **Note**: > - The `.rsk` extension is automatically appended if not provided > - Both checksummed and non-checksummed addresses are supported > - The command will show appropriate error messages if the name or address cannot be resolved + ## Contributing We welcome contributions from the community. Please fork the repository and submit pull requests with your changes. Ensure your code adheres to the project's main objective. diff --git a/bin/index.ts b/bin/index.ts index 517c695..21999c6 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -22,6 +22,9 @@ import { simulateCommand, TransactionSimulationOptions } from "../src/commands/s import { parseEther } from "viem"; import { resolveRNSToAddress } from "../src/utils/rnsHelper.js"; import { validateAndFormatAddressRSK } from "../src/utils/index.js"; +import { rnsUpdateCommand } from "../src/commands/rnsUpdate.js"; +import { rnsTransferCommand } from "../src/commands/rnsTransfer.js"; +import { rnsRegisterCommand } from "../src/commands/rnsRegister.js"; interface CommandOptions { testnet?: boolean; @@ -103,7 +106,7 @@ program } holderAddress = resolvedAddress; } - + await balanceCommand({ testnet: options.testnet, walletName: options.wallet!, @@ -333,19 +336,6 @@ program } }); -program - .command("resolve ") - .description("Resolve RNS names to addresses or reverse lookup addresses to names") - .option("-t, --testnet", "Use testnet (currently mainnet only)") - .option("-r, --reverse", "Reverse lookup: address to name") - .action(async (name: string, options: CommandOptions) => { - await resolveCommand({ - name, - testnet: !!options.testnet, - reverse: !!options.reverse - }); - }); - program .command("config") .description("Manage CLI configuration settings") @@ -485,4 +475,88 @@ program } }); +program + .command("rns") + .description("RNS Manager: Register, Transfer, Update, or Resolve domains") + .option("--register ", "Register a new RNS domain") + .option("--transfer ", "Transfer ownership of a domain") + .option("--update ", "Update resolver records for a domain") + .option("--resolve ", "Resolve a name to address (or address to name)") + + .option("-t, --testnet", "Use testnet network") + .option("-w, --wallet ", "Wallet name or private key to use") + .option("--recipient
", "Recipient address (required for --transfer)") + .option("--address
", "New address to set (required for --update)") + .option("-r, --reverse", "Perform reverse lookup (required for --resolve)") + + .action(async (options: any) => { + const actions = [ + options.register ? "register" : null, + options.transfer ? "transfer" : null, + options.update ? "update" : null, + options.resolve ? "resolve" : null, + ].filter(Boolean); + + if (actions.length === 0) { + console.error(chalk.red("❌ Error: You must specify an action.")); + console.log("Try: --register, --transfer, --update, or --resolve"); + process.exit(1); + } + if (actions.length > 1) { + console.error(chalk.red("❌ Error: Please specify only one action at a time.")); + process.exit(1); + } + + const action = actions[0]; + + try { + switch (action) { + case "register": + await rnsRegisterCommand({ + domain: options.register, + wallet: options.wallet, + testnet: !!options.testnet, + }); + break; + + case "transfer": + if (!options.recipient) { + console.error(chalk.red("❌ Error: --recipient
is required for transfer.")); + process.exit(1); + } + await rnsTransferCommand({ + domain: options.transfer, + recipient: options.recipient, + wallet: options.wallet, + testnet: !!options.testnet, + }); + break; + + case "update": + if (!options.address) { + console.error(chalk.red("❌ Error: --address
is required for update.")); + process.exit(1); + } + await rnsUpdateCommand({ + domain: options.update, + address: options.address, + wallet: options.wallet, + testnet: !!options.testnet, + }); + break; + + case "resolve": + await resolveCommand({ + name: options.resolve, + testnet: !!options.testnet, + reverse: !!options.reverse, + }); + break; + } + } catch (error: any) { + console.error(chalk.red(`❌ Operation failed: ${error.message || error}`)); + process.exit(1); + } + }); + program.parse(process.argv); diff --git a/package.json b/package.json index 7bd95da..1f06947 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "@openzeppelin/contracts": "^5.0.2", "@rsksmart/rns-resolver.js": "^1.1.0", + "@rsksmart/rns-sdk": "^1.0.0-beta.9", "@rsksmart/rsk-precompiled-abis": "^6.0.0-ARROWHEAD", "@types/fs-extra": "^11.0.4", "@types/zxcvbn": "^4.4.5", @@ -55,6 +56,7 @@ "cli-table3": "^0.6.5", "commander": "^13.1.0", "figlet": "^1.7.0", + "ethers": "^5.8.0", "fs-extra": "^11.2.0", "inquirer": "^12.1.0", "ora": "^8.0.1", diff --git a/src/commands/rnsRegister.ts b/src/commands/rnsRegister.ts new file mode 100644 index 0000000..4a4a528 --- /dev/null +++ b/src/commands/rnsRegister.ts @@ -0,0 +1,144 @@ +import chalk from "chalk"; +import { BigNumber, ethers } from "ethers"; +import { RNSADDRESSES } from "../constants/rnsAddress.js"; +import { TOKENS, TOKENS_METADATA } from "../constants/tokenAdress.js"; +import { getEthersSigner } from "../utils/ethersWallet.js"; +import { + logInfo, + logError, + logSuccess, + logWarning, + logMessage, +} from "../utils/logger.js"; +import { rootstock, rootstockTestnet } from "viem/chains"; +import rnsSdk from "@rsksmart/rns-sdk"; +import { EXPLORER } from "../constants/explorer.js"; +const { RSKRegistrar } = rnsSdk; + +interface RnsRegisterOptions { + domain: string; + wallet: string; + testnet?: boolean; + isExternal?: boolean; +} + +export async function rnsRegisterCommand(options: RnsRegisterOptions) { + const { domain, wallet, testnet, isExternal = false } = options; + const network = testnet ? "testnet" : "mainnet"; + const rpcUrl = testnet + ? rootstockTestnet.rpcUrls.default.http[0] + : rootstock.rpcUrls.default.http[0]; + + try { + const signer = await getEthersSigner(wallet, rpcUrl); + const registrar = new RSKRegistrar( + ethers.utils.getAddress( + RNSADDRESSES.rskOwnerAddress[network].toLowerCase() + ), + ethers.utils.getAddress( + RNSADDRESSES.fifsAddrRegistrarAddress[network].toLowerCase() + ), + ethers.utils.getAddress(TOKENS["RIF"][network].toLowerCase()), + signer + ); + const label = domain.replace(".rsk", ""); + logInfo(isExternal, `🔍 Checking availability for '${label}.rsk'...`); + + const available = await registrar.available(label); + if (!available) { + logError(isExternal, `❌ Domain '${label}.rsk' is already taken.`); + return; + } + const duration = BigNumber.from(1); + const price = await registrar.price(label, duration as any); + logMessage( + isExternal, + `Price: ${ethers.utils.formatUnits(price, 18)} ${ + TOKENS_METADATA.RIF[network] + }`, + chalk.dim + ); + + const rbtcBalance = await signer.getBalance(); + if (rbtcBalance.eq(0)) { + logError( + isExternal, + `❌ Insufficient ${TOKENS_METADATA.RBTC[network]} balance for gas. Please fund your wallet.` + ); + if (network == "testnet") { + logMessage( + isExternal, + `💡 Get test rBTC here: ${TOKENS_METADATA.RBTC.faucet.link}`, + chalk.yellow + ); + } + return; + } + + const rifAddress = TOKENS["RIF"][network]; + const rifAbi = ["function balanceOf(address) view returns (uint256)"]; + const rifContract = new ethers.Contract(rifAddress, rifAbi, signer); + const userRifBalance = await rifContract.balanceOf(signer.address); + if (userRifBalance.lt(price)) { + logError( + isExternal, + `❌ Insufficient ${TOKENS_METADATA.RIF[network]}. Have: ${ethers.utils.formatUnits( + userRifBalance, + 18 + )} ${TOKENS_METADATA.RIF[network]}, Need: ${ethers.utils.formatUnits(price, 18)} ${ + TOKENS_METADATA.RIF[network] + }` + ); + if (network == "testnet") { + logMessage( + isExternal, + `💡 Get test tRIF here: ${TOKENS_METADATA.RIF.faucet.link}`, + chalk.yellow + ); + } + return; + } + + logMessage(isExternal, "Step 1/2: Sending commitment...", chalk.yellow); + const { makeCommitmentTransaction, secret, canReveal } = + await registrar.commitToRegister(label, signer.address); + + await makeCommitmentTransaction.wait(); + logSuccess(isExternal, "✅ Commitment sent."); + + logMessage( + isExternal, + "⏳ Waiting for commitment maturity (approx 1 min)...", + chalk.cyan + ); + + while (!(await canReveal())) { + await new Promise((r) => setTimeout(r, 5000)); + if (!isExternal) process.stdout.write("."); + } + if (!isExternal) console.log(""); + + logWarning(isExternal, "Step 2/2: Registering domain..."); + + const registerTx = await registrar.register( + label, + signer.address, + secret, + duration, + price + ); + logMessage( + isExternal, + `Tx: ${EXPLORER.BLOCKSCOUT[network]}/tx/${registerTx.hash}`, + chalk.dim + ); + await registerTx.wait(); + + logSuccess( + isExternal, + `✅ Success! '${domain}' is now registered to ${signer.address}` + ); + } catch (error: any) { + logError(isExternal, `Registration Error: ${error.message || error}`); + } +} diff --git a/src/commands/rnsTransfer.ts b/src/commands/rnsTransfer.ts new file mode 100644 index 0000000..df2caff --- /dev/null +++ b/src/commands/rnsTransfer.ts @@ -0,0 +1,110 @@ +import chalk from "chalk"; +import { ethers } from "ethers"; +import { TOKENS_METADATA } from "../constants/tokenAdress.js"; +import { getEthersSigner } from "../utils/ethersWallet.js"; +import { + logInfo, + logError, + logSuccess, + logWarning, + logMessage, +} from "../utils/logger.js"; +import { rootstock, rootstockTestnet } from "viem/chains"; +import rnsSdk from "@rsksmart/rns-sdk"; +import { EXPLORER } from "../constants/explorer.js"; +const { PartnerRegistrar } = rnsSdk; + +interface RnsTransferOptions { + domain: string; + wallet: string; + testnet?: boolean; + recipient: string; + isExternal?: boolean; +} + +export async function rnsTransferCommand(options: RnsTransferOptions) { + const { domain, wallet, testnet, recipient, isExternal = false } = options; + const network = testnet ? "testnet" : "mainnet"; + const rpcUrl = testnet + ? rootstockTestnet.rpcUrls.default.http[0] + : rootstock.rpcUrls.default.http[0]; + + try { + const signer = await getEthersSigner(wallet, rpcUrl); + + const partnerRegistrar = new PartnerRegistrar(signer, network); + + const label = domain.replace(".rsk", ""); + const cleanRecipientAddress = ethers.utils.getAddress( + recipient.toLowerCase() + ); + + const isAvailable = await partnerRegistrar.available(label); + if (isAvailable) { + logError( + isExternal, + `❌ The domain '${domain}' is not registered yet. You can only transfer domains you already own.` + ); + return; + } + + const owner = await partnerRegistrar.ownerOf(label); + if (owner.toLowerCase() !== signer.address.toLowerCase()) { + logError( + isExternal, + `❌ You do not own '${domain}'. Current owner: ${owner}` + ); + return; + } + + if (owner.toLowerCase() === cleanRecipientAddress.toLowerCase()) { + logError( + isExternal, + `❌ You already own '${domain}'. Can't transfer to yourself!` + ); + return; + } + + logInfo( + isExternal, + `Preparing to transfer '${domain}' to ${cleanRecipientAddress}...` + ); + + const rbtcBalance = await signer.getBalance(); + if (rbtcBalance.eq(0)) { + logError( + isExternal, + `❌ Insufficient ${TOKENS_METADATA.RBTC[network]} for gas.` + ); + if (network == "testnet") { + logMessage( + isExternal, + `💡 Get test rBTC here: ${TOKENS_METADATA.RBTC.faucet.link}`, + chalk.yellow + ); + } + return; + } + + logWarning(isExternal, `🔄 Transferring ownership...`); + + const transferTxHash = await partnerRegistrar.transfer( + label, + cleanRecipientAddress + ); + + logMessage( + isExternal, + `Tx: ${EXPLORER.BLOCKSCOUT[network]}/tx/${transferTxHash}`, + chalk.dim + ); + + logSuccess( + isExternal, + `✅ Success! '${domain}' has been transferred to ${cleanRecipientAddress}` + ); + } catch (error: any) { + const errorMessage = error.reason || error.message || error; + logError(isExternal, `Transfer Error: ${errorMessage}`); + } +} diff --git a/src/commands/rnsUpdate.ts b/src/commands/rnsUpdate.ts new file mode 100644 index 0000000..ad6bb30 --- /dev/null +++ b/src/commands/rnsUpdate.ts @@ -0,0 +1,105 @@ +import chalk from "chalk"; +import { ethers } from "ethers"; +import { RNSADDRESSES } from "../constants/rnsAddress.js"; +import { TOKENS_METADATA } from "../constants/tokenAdress.js"; +import { getEthersSigner } from "../utils/ethersWallet.js"; +import { + logInfo, + logError, + logSuccess, + logWarning, + logMessage, +} from "../utils/logger.js"; +import { rootstock, rootstockTestnet } from "viem/chains"; +import rnsSdk from "@rsksmart/rns-sdk"; +import { EXPLORER } from "../constants/explorer.js"; + +const { AddrResolver, PartnerRegistrar } = rnsSdk; + +interface RnsUpdateOptions { + domain: string; + wallet: string; + address: string; + testnet?: boolean; + isExternal?: boolean; +} + +export async function rnsUpdateCommand(options: RnsUpdateOptions) { + const { domain, wallet, address, testnet, isExternal = false } = options; + const network = testnet ? "testnet" : "mainnet"; + const rpcUrl = testnet + ? rootstockTestnet.rpcUrls.default.http[0] + : rootstock.rpcUrls.default.http[0]; + + try { + const signer = await getEthersSigner(wallet, rpcUrl); + const registryAddress = ethers.utils.getAddress( + RNSADDRESSES.rnsRegistryAddress[network] + ); + const partnerRegistrar = new PartnerRegistrar(signer, network); + + const addrResolver = new AddrResolver(registryAddress, signer); + const cleanRecipientAddress = ethers.utils.getAddress( + address.toLowerCase() + ); + + logInfo(isExternal, `Preparing to update records for '${domain}'...`); + + const rbtcBalance = await signer.getBalance(); + if (rbtcBalance.eq(0)) { + logError( + isExternal, + `❌ Insufficient ${TOKENS_METADATA.RBTC[network]} for gas.` + ); + if (network == "testnet") { + logMessage( + isExternal, + `💡 Get test rBTC here: ${TOKENS_METADATA.RBTC.faucet.link}`, + chalk.yellow + ); + } + return; + } + + const label = domain.replace(".rsk", ""); + + const isAvailable = await partnerRegistrar.available(label); + if (isAvailable) { + logError( + isExternal, + `❌ The domain '${domain}' is not registered yet. You can only update domains you already own.` + ); + return; + } + + const owner = await partnerRegistrar.ownerOf(label); + if (owner.toLowerCase() !== signer.address.toLowerCase()) { + logError( + isExternal, + `❌ You do not own '${domain}' and can't update it. Current owner: ${owner}` + ); + return; + } + + logWarning( + isExternal, + `🔄 Setting resolution address to ${cleanRecipientAddress}...` + ); + + const updateTx = await addrResolver.setAddr(domain, cleanRecipientAddress); + logMessage( + isExternal, + `Tx: ${EXPLORER.BLOCKSCOUT[network]}/tx/${updateTx.hash}`, + chalk.dim + ); + await updateTx.wait(); + + logSuccess( + isExternal, + `✅ Success! '${domain}' now resolves to ${cleanRecipientAddress}` + ); + } catch (error: any) { + const errorMessage = error.reason || error.message || error; + logError(isExternal, `Update Error: ${errorMessage}`); + } +} diff --git a/src/constants/explorer.ts b/src/constants/explorer.ts new file mode 100644 index 0000000..4b80ed0 --- /dev/null +++ b/src/constants/explorer.ts @@ -0,0 +1,6 @@ +export const EXPLORER = { + BLOCKSCOUT : { + mainnet : "https://rootstock.blockscout.com", + testnet : "https://rootstock-testnet.blockscout.com" + } +} diff --git a/src/constants/rnsAddress.ts b/src/constants/rnsAddress.ts new file mode 100644 index 0000000..b1ed9b2 --- /dev/null +++ b/src/constants/rnsAddress.ts @@ -0,0 +1,23 @@ +import { Address } from "viem"; + +type Network = "mainnet" | "testnet"; + +type RnsAddressKey = + | "rnsRegistryAddress" + | "rskOwnerAddress" + | "fifsAddrRegistrarAddress"; + +export const RNSADDRESSES: Record> = { + rnsRegistryAddress: { + mainnet: "0xcb868aeabd31e2b66f74e9a55cf064abb31a4ad5", + testnet: "0x7d284aaac6e925aad802a53c0c69efe3764597b8", + }, + rskOwnerAddress: { + mainnet: "0x45d3e4fb311982a06ba52359d44cb4f5980e0ef1", + testnet: "0xca0a477e19bac7e0e172ccfd2e3c28a7200bdb71", + }, + fifsAddrRegistrarAddress: { + mainnet: "0xd9c79ced86ecf49f5e4a973594634c83197c35ab", + testnet: "0x90734bd6bf96250a7b262e2bc34284b0d47c1e8d", + }, +}; diff --git a/src/constants/tokenAdress.ts b/src/constants/tokenAdress.ts index d3d803d..e26cbcf 100644 --- a/src/constants/tokenAdress.ts +++ b/src/constants/tokenAdress.ts @@ -14,3 +14,16 @@ export const TOKENS: Record> = { testnet: "0xd37a3e5874be2dc6c732ad21c008a1e4032a6040", }, }; + +export const TOKENS_METADATA = { + RIF: { + mainnet: "RIF", + testnet: "tRIF", + faucet : {link : "https://faucet.rootstock.io/"} + }, + RBTC: { + mainnet: "rBTC", + testnet: "tRBTC", + faucet : {link : "https://faucet.rifos.org/"}, + }, +}; diff --git a/src/utils/ethersWallet.ts b/src/utils/ethersWallet.ts new file mode 100644 index 0000000..65853c2 --- /dev/null +++ b/src/utils/ethersWallet.ts @@ -0,0 +1,65 @@ +import { existsSync, readFileSync } from "fs"; +import crypto from "crypto"; +import inquirer from "inquirer"; +import { ethers, Wallet } from "ethers"; +import { walletFilePath } from "./constants.js"; + +/** + * Decrypts the local wallet and returns an Ethers.js Wallet instance. + * @param walletName - The name of the wallet to load (optional, defaults to current). + * @param providerUrl - The RPC URL to connect to. + */ +export async function getEthersSigner( + walletName: string | undefined, + providerUrl: string +): Promise { + if (!existsSync(walletFilePath)) { + throw new Error("No wallet file found. Please create one first."); + } + + const walletsData = JSON.parse(readFileSync(walletFilePath, "utf8")); + + const name = walletName || walletsData.currentWallet; + + if (!walletsData.wallets || !walletsData.wallets[name]) { + throw new Error(`Wallet '${name}' not found.`); + } + + const walletData = walletsData.wallets[name]; + + const { password } = await inquirer.prompt([ + { + type: "password", + name: "password", + message: `Enter password for wallet '${name}':`, + mask: "*", + }, + ]); + + try { + const iv = Uint8Array.from(Buffer.from(walletData.iv, "hex")); + const key = crypto.scryptSync(password, iv, 32); + const decipher = crypto.createDecipheriv( + "aes-256-cbc", + Uint8Array.from(key), + iv + ); + + let decrypted = decipher.update( + walletData.encryptedPrivateKey, + "hex", + "utf8" + ); + decrypted += decipher.final("utf8"); + + const provider = new ethers.providers.JsonRpcProvider(providerUrl); + + const privateKey = decrypted.startsWith("0x") + ? decrypted + : `0x${decrypted}`; + + return new Wallet(privateKey, provider); + } catch (error) { + throw new Error("Incorrect password or corrupted wallet file."); + } +}