diff --git a/src/commands/key/delete.ts b/src/commands/key/delete.ts index 5ae013a..bae711f 100644 --- a/src/commands/key/delete.ts +++ b/src/commands/key/delete.ts @@ -13,8 +13,8 @@ export default class KeyDelete extends Command { public async run(): Promise { const { args } = await this.parse(KeyDelete); if (!args.ADDR) { - const addresses = await listWallets(); - if (!addresses.length) { + const wallets = await listWallets(); + if (!wallets.length) { throw Error("Error: No private keys found."); } @@ -22,7 +22,10 @@ export default class KeyDelete extends Command { name: "address", message: "Pick the account to delete:", type: "list", - choices: addresses, + choices: wallets.map((w) => ({ + value: w.address, + name: w.description ? `${w.address} - ${w.description}` : w.address, + })), }); args.ADDR = res.address; diff --git a/src/commands/key/edit.ts b/src/commands/key/edit.ts new file mode 100644 index 0000000..717fb70 --- /dev/null +++ b/src/commands/key/edit.ts @@ -0,0 +1,52 @@ +import { Command } from "@oclif/core"; +import { Arg } from "@oclif/core/lib/interfaces"; +import inquirer from "inquirer"; +import { listWallets, loadWallet, readWallet, updateWallet } from "../../keystore"; + +export default class KeyEdit extends Command { + static description = "Changes the optional key description."; + static examples = ["<%= config.bin %> <%= command.id %>"]; + static usage = "<%= command.id %>"; + static args: Arg[] = [ + { name: "ADDR", description: "The address of the key to describe." }, + { name: "DESC", description: "The new description for the key." }, + ]; + + public async run(): Promise { + const { args } = await this.parse(KeyEdit); + if (!!args.ADDR !== !!args.DESC) { + this.error("ADDR and DESC must be specified together"); + } + + if (!args.ADDR) { + const wallets = await listWallets(); + if (!wallets.length) { + throw Error("Error: No private keys found."); + } + + const res1 = await inquirer.prompt({ + name: "address", + message: "Pick the account to describe:", + type: "list", + choices: wallets.map((w) => ({ + value: w.address, + name: w.description ? `${w.address} - ${w.description}` : w.address, + })), + }); + + const json = await readWallet(res1.address); + const res2 = await inquirer.prompt({ + name: "description", + message: "Enter the key description:", + default: json.description, + }); + + args.ADDR = res1.address; + args.DESC = res2.description; + } + + const address = args.ADDR; + await updateWallet(address, args.DESC); + console.log(`Account ${address} updated`); + } +} diff --git a/src/commands/key/import.ts b/src/commands/key/import.ts index f06ee39..0743211 100644 --- a/src/commands/key/import.ts +++ b/src/commands/key/import.ts @@ -11,10 +11,11 @@ export default class KeyImport extends Command { await this.parse(KeyImport); const responses = await inquirer.prompt([ { name: "privateKey", message: "Paste the private key to import:", type: "password", mask: "*" }, - { name: "password", message: "Enter a key encryption password:", type: "password", mask: "*" }, + { name: "password", message: "Create a key encryption password:", type: "password", mask: "*" }, + { name: "description", message: "Enter an optional key description:" }, ]); - const address = await saveWallet(responses.privateKey, responses.password); + const address = await saveWallet(responses.privateKey, responses.password, responses.description); await loadWallet(address, responses.password); console.log(`Account ${address} imported`); } diff --git a/src/commands/key/list.ts b/src/commands/key/list.ts index e0ad09c..f42866a 100644 --- a/src/commands/key/list.ts +++ b/src/commands/key/list.ts @@ -8,9 +8,10 @@ export default class KeyList extends Command { public async run(): Promise { await this.parse(KeyList); - const keys = await listWallets(); - for (let i = 0; i < keys.length; ++i) { - console.log(keys[i]); + const wallets = await listWallets(); + for (let i = 0; i < wallets.length; ++i) { + const w = wallets[i]; + console.log(w.description ? `${w.address} - ${w.description}` : w.address); } } } diff --git a/src/helpers.ts b/src/helpers.ts index cb69ee2..621f3a7 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -98,8 +98,8 @@ export async function getSigner( wallet = wallet.connect(provider); } else { if (!address) { - const addresses = await listWallets(); - if (!addresses.length) { + const wallets = await listWallets(); + if (!wallets.length) { throw Error("Error: No private keys found. Use key import command."); } @@ -107,13 +107,16 @@ export async function getSigner( name: "address", message: "Pick the wallet to sign the transaction:", type: "list", - choices: addresses, + choices: wallets.map((w) => ({ + value: w.address, + name: w.description ? `${w.address} - ${w.description}` : w.address, + })), }); address = res.address as string; } else { - const addresses = await listWallets(); - if (!addresses.includes(address)) { + const wallets = await listWallets(); + if (!wallets.find((w) => w.address === address)) { throw Error(`No saved key for address ${address}`); } } diff --git a/src/keystore.ts b/src/keystore.ts index 5afb777..49edb74 100644 --- a/src/keystore.ts +++ b/src/keystore.ts @@ -7,49 +7,65 @@ import keytar from "keytar"; import { normalizeHash } from "./helpers"; const homedir = os.homedir(); -const keyStoreFolderPath = path.join(homedir, ".armada"); +const keyStoreFolderPath = path.join(homedir, ".armada/keystore"); -export async function saveWallet(privateKey: string, password: string): Promise { +export async function saveWallet(privateKey: string, password: string, description: string): Promise { privateKey = normalizeHash(privateKey); const wallet = new Wallet(privateKey); const address = wallet.address; - const json = await encryptKeystore({ address, privateKey }, password); - const filename = `keystore_${address}.json`; - + const text = await encryptKeystore({ address, privateKey }, password); + const json = { ...JSON.parse(text), description }; + const filename = `${address}.json`; if (!fs.existsSync(keyStoreFolderPath)) { fs.mkdirSync(keyStoreFolderPath); } - fs.writeFileSync(path.join(keyStoreFolderPath, filename), json); + fs.writeFileSync(path.join(keyStoreFolderPath, filename), JSON.stringify(json, null, " ")); keytar.setPassword("armada-cli", address, password); return address; } export async function loadWallet(address: string, password: string): Promise { - const filename = `keystore_${address}.json`; - const json = fs.readFileSync(path.join(keyStoreFolderPath, filename), "utf8"); - const wallet = await Wallet.fromEncryptedJson(json, password); + const filename = `${address}.json`; + const text = fs.readFileSync(path.join(keyStoreFolderPath, filename), "utf8"); + const wallet = await Wallet.fromEncryptedJson(text, password); return wallet; } +export async function readWallet(address: string): Promise<{ description: string }> { + const filename = `${address}.json`; + const text = fs.readFileSync(path.join(keyStoreFolderPath, filename), "utf8"); + return JSON.parse(text); +} + export async function deleteWallet(address: string): Promise { - const filename = `keystore_${address}.json`; + const filename = `${address}.json`; fs.unlinkSync(path.join(keyStoreFolderPath, filename)); } -export async function listWallets(): Promise { +export async function updateWallet(address: string, description: string): Promise { + const filename = `${address}.json`; + const pathname = path.join(keyStoreFolderPath, filename); + const text = fs.readFileSync(pathname, "utf8"); + const json = { ...JSON.parse(text), description }; + fs.writeFileSync(pathname, JSON.stringify(json, null, " ")); +} + +export async function listWallets(): Promise<{ address: string; description: string }[]> { if (!fs.existsSync(keyStoreFolderPath)) { return []; } - const wallets: string[] = []; + const wallets: { address: string; description: string }[] = []; const files = fs.readdirSync(keyStoreFolderPath); for (const filename of files) { - if (!filename.match(/keystore_.*\.json/)) { + if (!filename.match(/.*\.json/)) { continue; } - wallets.push(filename.split("_")[1].split(".")[0]); + const text = fs.readFileSync(path.join(keyStoreFolderPath, filename), "utf8"); + const json = JSON.parse(text); + wallets.push({ address: filename.split(".")[0], description: json.description }); } return wallets;