From 88d613216623d0b1f0c3e7c477bdf4023fc0699e Mon Sep 17 00:00:00 2001 From: Edgars Date: Sat, 7 Feb 2026 23:16:09 +0000 Subject: [PATCH 1/2] fix: bump genlayer-js to 0.18.10 with phase4 testnet addresses --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90588522..2095961d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "dotenv": "^17.0.0", "ethers": "^6.13.4", "fs-extra": "^11.3.0", - "genlayer-js": "^0.18.9", + "genlayer-js": "^0.18.10", "inquirer": "^12.0.0", "keytar": "^7.9.0", "node-fetch": "^3.0.0", @@ -5581,9 +5581,9 @@ } }, "node_modules/genlayer-js": { - "version": "0.18.9", - "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.18.9.tgz", - "integrity": "sha512-KMKLCKU4v7FCQhJBwC3Nx+Y6ezN7UC4ATCJozKoHHhsC9rk9fcac7TIinonPfwQa8ghXMYBLCcqkhNy7BltREA==", + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.18.10.tgz", + "integrity": "sha512-/femmDnoGMm9fTotobkqTndOE//UcJWY1QYLQFueE/rE25shL77vCjKWtg/hj0U8xxg26Mksfjcr/crO22i5kA==", "license": "MIT", "dependencies": { "eslint-plugin-import": "^2.30.0", diff --git a/package.json b/package.json index 0841cd60..686d3b90 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "dotenv": "^17.0.0", "ethers": "^6.13.4", "fs-extra": "^11.3.0", - "genlayer-js": "^0.18.9", + "genlayer-js": "^0.18.10", "inquirer": "^12.0.0", "keytar": "^7.9.0", "node-fetch": "^3.0.0", From 54ac104a40912c885c7c008c027b1f4f7f7ddf6e Mon Sep 17 00:00:00 2001 From: Edgars Date: Sun, 8 Feb 2026 17:55:09 +0000 Subject: [PATCH 2/2] feat: add --password flag for non-interactive CLI usage Add --password option to account create, send, unlock, and all staking write commands (validator-join, validator-deposit, validator-exit, validator-claim, validator-prime, prime-all, set-operator, set-identity, delegator-join, delegator-exit, delegator-claim). When provided, skips interactive password prompts enabling scripted/CI usage. --- src/commands/account/create.ts | 3 ++- src/commands/account/index.ts | 7 +++++-- src/commands/account/send.ts | 12 +++++++++--- src/commands/account/unlock.ts | 11 ++++++++--- src/commands/staking/StakingAction.ts | 18 +++++++++++++++--- src/commands/staking/index.ts | 11 +++++++++++ src/lib/actions/BaseAction.ts | 16 ++++++++++------ tests/actions/create.test.ts | 2 +- 8 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/commands/account/create.ts b/src/commands/account/create.ts index eb33d2b3..40376638 100644 --- a/src/commands/account/create.ts +++ b/src/commands/account/create.ts @@ -4,6 +4,7 @@ export interface CreateAccountOptions { name: string; overwrite: boolean; setActive?: boolean; + password?: string; } export class CreateAccountAction extends BaseAction { @@ -14,7 +15,7 @@ export class CreateAccountAction extends BaseAction { async execute(options: CreateAccountOptions): Promise { try { this.startSpinner(`Creating account '${options.name}'...`); - await this.createKeypairByName(options.name, options.overwrite); + await this.createKeypairByName(options.name, options.overwrite, options.password); if (options.setActive !== false) { this.setActiveAccount(options.name); diff --git a/src/commands/account/index.ts b/src/commands/account/index.ts index 3ccd2479..3fa23f15 100644 --- a/src/commands/account/index.ts +++ b/src/commands/account/index.ts @@ -42,6 +42,7 @@ export function initializeAccountCommands(program: Command) { .command("create") .description("Create a new account with encrypted keystore") .requiredOption("--name ", "Name for the account") + .option("--password ", "Password for the keystore (skips interactive prompt)") .option("--overwrite", "Overwrite existing account", false) .option("--no-set-active", "Do not set as active account") .action(async (options: CreateAccountOptions) => { @@ -99,15 +100,17 @@ export function initializeAccountCommands(program: Command) { .option("--rpc ", "RPC URL for the network") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--account ", "Account to send from") - .action(async (to: string, amount: string, options: {rpc?: string; network?: string; account?: string}) => { + .option("--password ", "Password to unlock account (skips interactive prompt)") + .action(async (to: string, amount: string, options: {rpc?: string; network?: string; account?: string; password?: string}) => { const sendAction = new SendAction(); - await sendAction.execute({to, amount, rpc: options.rpc, network: options.network, account: options.account}); + await sendAction.execute({to, amount, rpc: options.rpc, network: options.network, account: options.account, password: options.password}); }); accountCommand .command("unlock") .description("Unlock account by caching private key in OS keychain") .option("--account ", "Account to unlock") + .option("--password ", "Password to unlock account (skips interactive prompt)") .action(async (options: UnlockAccountOptions) => { const unlockAction = new UnlockAccountAction(); await unlockAction.execute(options); diff --git a/src/commands/account/send.ts b/src/commands/account/send.ts index 7f96e774..66188bc8 100644 --- a/src/commands/account/send.ts +++ b/src/commands/account/send.ts @@ -11,6 +11,7 @@ export interface SendOptions { rpc?: string; network?: string; account?: string; + password?: string; } export class SendAction extends BaseAction { @@ -75,9 +76,14 @@ export class SendAction extends BaseAction { if (cachedKey) { privateKey = cachedKey; } else { - this.stopSpinner(); - const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`); - this.startSpinner("Preparing transfer..."); + let password: string; + if (options.password) { + password = options.password; + } else { + this.stopSpinner(); + password = await this.promptPassword(`Enter password to unlock account '${accountName}':`); + this.startSpinner("Preparing transfer..."); + } const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password); privateKey = wallet.privateKey; } diff --git a/src/commands/account/unlock.ts b/src/commands/account/unlock.ts index 87b10c18..260b864f 100644 --- a/src/commands/account/unlock.ts +++ b/src/commands/account/unlock.ts @@ -4,6 +4,7 @@ import {ethers} from "ethers"; export interface UnlockAccountOptions { account?: string; + password?: string; } export class UnlockAccountAction extends BaseAction { @@ -36,10 +37,14 @@ export class UnlockAccountAction extends BaseAction { return; } - this.stopSpinner(); - try { - const password = await this.promptPassword(`Enter password to unlock '${accountName}':`); + let password: string; + if (options?.password) { + password = options.password; + } else { + this.stopSpinner(); + password = await this.promptPassword(`Enter password to unlock '${accountName}':`); + } const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password); await this.keychainManager.storePrivateKey(accountName, wallet.privateKey); diff --git a/src/commands/staking/StakingAction.ts b/src/commands/staking/StakingAction.ts index e92be2e0..1d678d6d 100644 --- a/src/commands/staking/StakingAction.ts +++ b/src/commands/staking/StakingAction.ts @@ -25,10 +25,12 @@ export interface StakingConfig { stakingAddress?: string; network?: string; account?: string; + password?: string; } export class StakingAction extends BaseAction { private _stakingClient: GenLayerClient | null = null; + private _passwordOverride: string | undefined; constructor() { super(); @@ -53,6 +55,9 @@ export class StakingAction extends BaseAction { if (config.account) { this.accountOverride = config.account; } + if (config.password) { + this._passwordOverride = config.password; + } const network = this.getNetwork(config); @@ -140,9 +145,13 @@ export class StakingAction extends BaseAction { } } - // Stop spinner before prompting for password - this.stopSpinner(); - const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`); + let password: string; + if (this._passwordOverride) { + password = this._passwordOverride; + } else { + this.stopSpinner(); + password = await this.promptPassword(`Enter password to unlock account '${accountName}':`); + } this.startSpinner("Unlocking account..."); const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password); @@ -180,6 +189,9 @@ export class StakingAction extends BaseAction { if (config.account) { this.accountOverride = config.account; } + if (config.password) { + this._passwordOverride = config.password; + } const network = this.getNetwork(config); const rpcUrl = config.rpc || network.rpcUrls.default.http[0]; diff --git a/src/commands/staking/index.ts b/src/commands/staking/index.ts index f3650c5f..41a1d535 100644 --- a/src/commands/staking/index.ts +++ b/src/commands/staking/index.ts @@ -37,6 +37,7 @@ export function initializeStakingCommands(program: Command) { .requiredOption("--amount ", "Amount to stake (in wei or with 'eth'/'gen' suffix, e.g., '42000gen')") .option("--operator
", "Operator address (defaults to signer)") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") @@ -51,6 +52,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .requiredOption("--amount ", "Amount to deposit (in wei or with 'eth'/'gen' suffix)") .option("--account ", "Account to use (must be validator owner)") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .action(async (validatorArg: string | undefined, options: ValidatorDepositOptions) => { @@ -69,6 +71,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .requiredOption("--shares ", "Number of shares to withdraw") .option("--account ", "Account to use (must be validator owner)") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .action(async (validatorArg: string | undefined, options: ValidatorExitOptions) => { @@ -86,6 +89,7 @@ export function initializeStakingCommands(program: Command) { .description("Claim validator withdrawals after unbonding period") .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .action(async (validatorArg: string | undefined, options: ValidatorClaimOptions) => { @@ -103,6 +107,7 @@ export function initializeStakingCommands(program: Command) { .description("Prime a validator to prepare their stake record for the next epoch") .option("--validator
", "Validator address to prime (deprecated, use positional arg)") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") @@ -120,6 +125,7 @@ export function initializeStakingCommands(program: Command) { .command("prime-all") .description("Prime all validators that need priming") .option("--account ", "Account to use (pays gas)") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") @@ -134,6 +140,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator wallet address (deprecated, use positional arg)") .option("--operator
", "New operator address (deprecated, use positional arg)") .option("--account ", "Account to use (must be validator owner)") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .action(async (validatorArg: string | undefined, operatorArg: string | undefined, options: SetOperatorOptions) => { @@ -161,6 +168,7 @@ export function initializeStakingCommands(program: Command) { .option("--github ", "GitHub handle") .option("--extra-cid ", "Extra data as IPFS CID or hex bytes (0x...)") .option("--account ", "Account to use (must be validator operator)") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .action(async (validatorArg: string | undefined, options: SetIdentityOptions) => { @@ -180,6 +188,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator address to delegate to (deprecated, use positional arg)") .requiredOption("--amount ", "Amount to stake (in wei or with 'eth'/'gen' suffix)") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") @@ -199,6 +208,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator address to exit from (deprecated, use positional arg)") .requiredOption("--shares ", "Number of shares to withdraw") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") @@ -218,6 +228,7 @@ export function initializeStakingCommands(program: Command) { .option("--validator
", "Validator address (deprecated, use positional arg)") .option("--delegator
", "Delegator address (defaults to signer)") .option("--account ", "Account to use") + .option("--password ", "Password to unlock account (skips interactive prompt)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index c4161034..fd7d72ce 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -171,7 +171,7 @@ export class BaseAction extends ConfigFileManager { return keystoreData.address as Address; } - protected async createKeypairByName(accountName: string, overwrite: boolean): Promise { + protected async createKeypairByName(accountName: string, overwrite: boolean, passwordInput?: string): Promise { const keystorePath = this.getKeystorePath(accountName); this.stopSpinner(); @@ -181,11 +181,15 @@ export class BaseAction extends ConfigFileManager { const wallet = ethers.Wallet.createRandom(); - const password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):"); - const confirmPassword = await this.promptPassword("Confirm password:"); - - if (password !== confirmPassword) { - this.failSpinner("Passwords do not match"); + let password: string; + if (passwordInput) { + password = passwordInput; + } else { + password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):"); + const confirmPassword = await this.promptPassword("Confirm password:"); + if (password !== confirmPassword) { + this.failSpinner("Passwords do not match"); + } } if (password.length < BaseAction.MIN_PASSWORD_LENGTH) { diff --git a/tests/actions/create.test.ts b/tests/actions/create.test.ts index 3c9af8c5..71342860 100644 --- a/tests/actions/create.test.ts +++ b/tests/actions/create.test.ts @@ -38,7 +38,7 @@ describe("CreateAccountAction", () => { await createAction.execute(options); expect(createAction["startSpinner"]).toHaveBeenCalledWith("Creating account 'main'..."); - expect(createAction["createKeypairByName"]).toHaveBeenCalledWith("main", false); + expect(createAction["createKeypairByName"]).toHaveBeenCalledWith("main", false, undefined); expect(createAction["setActiveAccount"]).toHaveBeenCalledWith("main"); expect(createAction["succeedSpinner"]).toHaveBeenCalledWith( `Account 'main' created at: ${mockKeystorePath}`,