UNPKG

@solarity/hardhat-migrate

Version:
205 lines (182 loc) 6.05 kB
import { exec } from "child_process"; import { promisify } from "util"; import { Transaction } from "ethers"; import { MigrateError } from "../../utils/index.js"; const execAsync = promisify(exec); /** * Options for Cast wallet commands */ export interface CastWalletOptions { /** The sender account (e.g. address) */ from?: string; /** Open an interactive prompt to enter your private key */ interactive?: boolean; /** Use a provided raw private key */ privateKey?: string; /** Use a mnemonic phrase (or path to one) */ mnemonic?: string; /** Use a BIP39 passphrase for the mnemonic */ mnemonicPassphrase?: string; /** The derivation path for the mnemonic */ mnemonicDerivationPath?: string; /** The mnemonic index (default 0) */ mnemonicIndex?: number; /** Use a keystore file or directory */ keystore?: string; /** The account name (when using the default keystore directory) */ account?: string; /** Keystore password */ password?: string; /** File path to the keystore password */ passwordFile?: string; /** Use a Ledger hardware wallet */ ledger?: boolean; /** Use a Trezor hardware wallet */ trezor?: boolean; /** Use AWS KMS */ aws?: boolean; /** Log color: "auto", "always", or "never" */ color?: "auto" | "always" | "never"; /** Format log messages as JSON */ json?: boolean; /** Suppress log messages */ quiet?: boolean; /** Increase verbosity (repeat flag `-v` as needed) */ verbosity?: number; /** Number of threads to use */ threads?: number; } /** * Options specific to the sign command */ export interface CastSignOptions extends CastWalletOptions { /** Treat the message as JSON typed data */ data?: boolean; /** When using typed data, treat the message as a file containing JSON */ fromFile?: boolean; /** Do not hash the input; sign the provided 32-byte hash directly */ noHash?: boolean; } export async function getCastVersion(): Promise<string> { try { const { stdout } = await execAsync("cast -V"); return stdout.trim(); } catch (error: any) { throw new MigrateError(`Error executing cast -V: ${error.message}. Is cast installed?`); } } export async function getSignedTxViaCast(tx: Transaction, castOpts: CastSignOptions = {}): Promise<string> { tx.signature = await signMessage(tx.unsignedHash, { noHash: true, ...castOpts, }); return tx.serialized; } function normalizeNameForEnv(name: string) { return `PASSWORD_${name.replace(/[-_]/g, "_").toUpperCase()}`; } /** * Get the Ethereum address associated with the Cast wallet configuration * * @param options Cast wallet options * @returns The Ethereum address */ export async function getCastWalletAddress(options: CastWalletOptions = {}): Promise<string> { const args = ["wallet", "address"]; appendWalletOptions(args, options); const command = `cast ${args.join(" ")}`; try { const { stdout, stderr } = await execAsync(command); if (stderr && stderr.trim()) { console.error("cast stderr:", stderr); } return stdout.trim(); } catch (error: any) { throw new MigrateError(`Error getting address from Cast wallet: ${error.message}`); } } /** * Sign a message (or data/transaction hash) using Foundry's cast wallet sign command. * * @param message The message (or hash) to sign. * @param options Additional options mapping to cast's flags. * @returns The signature string from cast. */ export async function signMessage(message: string, options: CastSignOptions = {}): Promise<string> { const args = ["wallet", "sign"]; // Process signing-specific flags if (options.data) args.push("--data"); if (options.fromFile) args.push("--from-file"); if (options.noHash) args.push("--no-hash"); // Add wallet options appendWalletOptions(args, options); // Add the message to sign args.push(message); const command = `cast ${args.join(" ")}`; try { const { stdout, stderr } = await execAsync(command); if (stderr && stderr.trim()) { console.error("cast stderr:", stderr); } return stdout.trim(); } catch (error: any) { throw new MigrateError(`Error executing cast sign command: ${error.message}`); } } /** * Helper function to append common wallet options to a command args array */ function appendWalletOptions(args: string[], options: CastWalletOptions): void { if (options.account) { const possiblePassword = process.env[normalizeNameForEnv(options.account)]; if (possiblePassword) { options.password = possiblePassword; delete options.passwordFile; } } if (options.from) { args.push("--from", options.from); } if (options.interactive) args.push("--interactive"); if (options.privateKey) { args.push("--private-key", options.privateKey); } if (options.mnemonic) { args.push("--mnemonic", options.mnemonic); } if (options.mnemonicPassphrase) { args.push("--mnemonic-passphrase", options.mnemonicPassphrase); } if (options.mnemonicDerivationPath) { args.push("--mnemonic-derivation-path", options.mnemonicDerivationPath); } if (typeof options.mnemonicIndex === "number") { args.push("--mnemonic-index", options.mnemonicIndex.toString()); } if (options.keystore) { args.push("--keystore", options.keystore); } if (options.account) { args.push("--account", options.account); } if (options.password) { args.push("--password", `'${options.password}'`); } if (options.passwordFile) { args.push("--password-file", options.passwordFile); } if (options.ledger) args.push("--ledger"); if (options.trezor) args.push("--trezor"); if (options.aws) args.push("--aws"); if (options.color) { args.push("--color", options.color); } if (options.json) args.push("--json"); if (options.quiet) args.push("--quiet"); if (options.verbosity && options.verbosity > 0) { args.push("-" + "v".repeat(options.verbosity)); } if (typeof options.threads === "number") { args.push("--threads", options.threads.toString()); } }