Skip to content

Commit

Permalink
feat(cli): support config:forger:bip39 command (#418)
Browse files Browse the repository at this point in the history
* support `config:forger:bip39` command

* style: resolve style guide violations

---------

Co-authored-by: oXtxNt9U <[email protected]>
  • Loading branch information
oXtxNt9U and oXtxNt9U authored Feb 2, 2024
1 parent 67dfac0 commit d22af15
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 98 deletions.
77 changes: 77 additions & 0 deletions packages/core/source/commands/config-forger-bip38.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Console, describe } from "@mainsail/test-framework";
import { ensureDirSync, writeJSONSync } from "fs-extra";
import prompts from "prompts";
import { dirSync, setGracefulCleanup } from "tmp";

import { Command } from "./config-forger-bip38";
import { Command as BIP39Command } from "./config-forger-bip39";

describe<{
cli: Console;
}>("ConfigForgerBIP38Command", ({ beforeEach, afterAll, it, assert }) => {
const bip39 = "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire";
const bip39Flags = "venue below waste gather spin cruise title still boost mother flash tuna";
const bip39Prompt = "craft imitate step mixture patch forest volcano business charge around girl confirm";

beforeEach((context) => {
process.env.CORE_PATH_CONFIG = dirSync().name;

ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainsail/`);
writeJSONSync(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`, {});

context.cli = new Console();
});

afterAll(() => setGracefulCleanup());

it("should configure from flags", async ({ cli }) => {
await cli.withFlags({ bip39: bip39Flags, password: "password" }).execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [] });
});

it("should configure from a prompt if it receives a valid bip39 and confirmation", async ({ cli }) => {
prompts.inject([bip39Prompt, "password", "password"]);

await cli.execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [] });
});

it("should fail to configure from a prompt if it receives an invalid bip39", async ({ cli }) => {
await cli.withFlags({ bip39 }).execute(BIP39Command);

prompts.inject(["random-string", "password", "password"]);

await assert.rejects(() => cli.execute(Command), "Failed to verify the given passphrase as BIP39 compliant.");

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [bip39] });
});

it("should configure from a prompt if it receives an invalid bip39 and skipValidation flag is set", async ({
cli,
}) => {
await cli.withFlags({ bip39 }).execute(BIP39Command);

prompts.inject(["random-string", "password", "password"]);

await cli.withFlags({ skipValidation: true }).execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), {
secrets: [],
});
});

it("should fail to configure from a prompt if it doesn't receive a bip39", async ({ cli }) => {
prompts.inject([null, true]);

await assert.rejects(() => cli.execute(Command), "Failed to verify the given passphrase as BIP39 compliant.");
});

it("should fail to configure from a prompt if it doesn't receive a valid bip39", async ({ cli }) => {
await assert.rejects(
() => cli.withFlags({ bip39: "random-string" }).execute(Command),
"Failed to verify the given passphrase as BIP39 compliant.",
);
});
});
104 changes: 104 additions & 0 deletions packages/core/source/commands/config-forger-bip38.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Commands, Contracts } from "@mainsail/cli";
import { injectable } from "@mainsail/container";
import { validateMnemonic } from "bip39";
import { writeJSONSync } from "fs-extra";
import Joi from "joi";

@injectable()
export class Command extends Commands.Command {
public signature = "config:forger:bip38";

public description = "Configure the forging validator (BIP38).";

public isHidden = true;

public configure(): void {
this.definition
.setFlag("token", "The name of the token.", Joi.string())
.setFlag("network", "The name of the network.", Joi.string())
.setFlag("bip39", "A validator plain text passphrase. Referred to as BIP39.", Joi.string())
.setFlag("password", "A custom password that encrypts the BIP39. Referred to as BIP38.", Joi.string())
.setFlag("skipValidation", "Skip BIP39 mnemonic validation", Joi.boolean().default(false));
}

public async execute(): Promise<void> {
if (this.hasFlag("bip39") && this.hasFlag("password")) {
return this.performConfiguration(this.getFlags());
}

const response = await this.components.prompt([
{
message: "Please enter your validator plain text passphrase. Referred to as BIP39.",
name: "bip39",
type: "password",
validate: (value) =>
!validateMnemonic(value) && !this.getFlag("skipValidation")
? `Failed to verify the given passphrase as BIP39 compliant.`
: true,
},
{
message: "Please enter your custom password that encrypts the BIP39. Referred to as BIP38.",
name: "password",
type: "password",
validate: (value) => (typeof value !== "string" ? "The BIP38 password has to be a string." : true),
},
]);

await this.components.prompt([
{
message: "Confirm custom password that encrypts the BIP39. Referred to as BIP38.",
name: "passwordConfirmation",
type: "password",
validate: (value) =>
value !== response.password ? "Confirm password does not match BIP38 password." : true,
},
]);

if (!response.bip39) {
throw new Error("Failed to verify the given passphrase as BIP39 compliant.");
}

if (!response.password) {
throw new Error("The BIP38 password has to be a string.");
}

return this.performConfiguration({ ...this.getFlags(), ...response });
}

private async performConfiguration(flags: Contracts.AnyObject): Promise<void> {
//let decodedWIF;

await this.components.taskList([
{
task: () => {
if (!flags.bip39 || (!validateMnemonic(flags.bip39) && !flags.skipValidation)) {
throw new Error(`Failed to verify the given passphrase as BIP39 compliant.`);
}
},
title: "Validating passphrase is BIP39 compliant.",
},
{
task: () => {
// decodedWIF = wif.decode(Identities.WIF.fromPassphrase(flags.bip39));
},
title: "Loading private key.",
},
{
task: () => {
const validatorsConfig = this.app.getCorePath("config", "validators.json");

const validators: Record<string, string | string[]> = require(validatorsConfig);
// validators.bip38 = bip38.encrypt(
// decodedWIF.privateKey,
// decodedWIF.compressed,
// flags.password,
// );
validators.secrets = [];

writeJSONSync(validatorsConfig, validators);
},
title: "Writing BIP39 passphrase to configuration.",
},
]);
}
}
88 changes: 88 additions & 0 deletions packages/core/source/commands/config-forger-bip39.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Console, describe } from "@mainsail/test-framework";
import { ensureDirSync, writeJSONSync } from "fs-extra";
import prompts from "prompts";
import { dirSync, setGracefulCleanup } from "tmp";

import { Command } from "./config-forger-bip39";

describe<{
cli: Console;
}>("ConfigForgerBIP39Command", ({ beforeEach, afterAll, it, assert }) => {
const bip39 = "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire";
const bip39Flags = "venue below waste gather spin cruise title still boost mother flash tuna";
const bip39Prompt = "craft imitate step mixture patch forest volcano business charge around girl confirm";

beforeEach((context) => {
process.env.CORE_PATH_CONFIG = dirSync().name;

ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainsail/`);
writeJSONSync(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`, {});

context.cli = new Console();
});

afterAll(() => setGracefulCleanup());

it("should configure from flags", async ({ cli }) => {
await cli.withFlags({ bip39: bip39Flags }).execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [bip39Flags] });
});

it("should configure from a prompt if it receives a valid bip39 and confirmation", async ({ cli }) => {
prompts.inject([bip39Prompt, true]);

await cli.execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [bip39Prompt] });
});

it("should fail to configure from a prompt if it receives a valid bip39 and but no confirmation", async ({
cli,
}) => {
await cli.withFlags({ bip39 }).execute(Command);

prompts.inject([bip39Prompt, false]);

await cli.execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [bip39] });
});

it("should fail to configure from a prompt if it receives an invalid bip39", async ({ cli }) => {
await cli.withFlags({ bip39 }).execute(Command);

prompts.inject(["random-string", true]);

await assert.rejects(() => cli.execute(Command), "Failed to verify the given passphrase as BIP39 compliant.");

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), { secrets: [bip39] });
});

it("should configure from a prompt if it receives an invalid bip39 and skipValidation flag is set", async ({
cli,
}) => {
await cli.withFlags({ bip39 }).execute(Command);

prompts.inject(["random-string", true]);

await cli.withFlags({ skipValidation: true }).execute(Command);

assert.equal(require(`${process.env.CORE_PATH_CONFIG}/mainsail/validators.json`), {
secrets: ["random-string"],
});
});

it("should fail to configure from a prompt if it doesn't receive a bip39", async ({ cli }) => {
prompts.inject([null, true]);

await assert.rejects(() => cli.execute(Command), "Failed to verify the given passphrase as BIP39 compliant.");
});

it("should fail to configure from a prompt if it doesn't receive a valid bip39", async ({ cli }) => {
await assert.rejects(
() => cli.withFlags({ bip39: "random-string" }).execute(Command),
"Failed to verify the given passphrase as BIP39 compliant.",
);
});
});
71 changes: 71 additions & 0 deletions packages/core/source/commands/config-forger-bip39.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Commands, Contracts } from "@mainsail/cli";
import { injectable } from "@mainsail/container";
import { validateMnemonic } from "bip39";
import { writeJSONSync } from "fs-extra";
import Joi from "joi";

@injectable()
export class Command extends Commands.Command {
public signature = "config:forger:bip39";

public description = "Configure the forging validator (BIP39).";

public configure(): void {
this.definition
.setFlag("token", "The name of the token.", Joi.string())
.setFlag("network", "The name of the network.", Joi.string())
.setFlag("bip39", "A validator plain text passphrase. Referred to as BIP39.", Joi.string())
.setFlag("skipValidation", "Skip BIP39 mnemonic validation", Joi.boolean().default(false));
}

public async execute(): Promise<void> {
if (this.hasFlag("bip39")) {
return this.performConfiguration(this.getFlags());
}

const response = await this.components.prompt([
{
message: "Please enter your validator plain text passphrase. Referred to as BIP39.",
name: "bip39",
type: "password",
validate: (value) =>
!validateMnemonic(value) && !this.getFlag("skipValidation")
? `Failed to verify the given passphrase as BIP39 compliant.`
: true,
},
{
message: "Can you confirm?",
name: "confirm",
type: "confirm",
},
]);

if (response.confirm) {
return this.performConfiguration({ ...this.getFlags(), ...response });
}
}

private async performConfiguration(flags: Contracts.AnyObject): Promise<void> {
await this.components.taskList([
{
task: () => {
if (!flags.bip39 || (!validateMnemonic(flags.bip39) && !flags.skipValidation)) {
throw new Error(`Failed to verify the given passphrase as BIP39 compliant.`);
}
},
title: "Validating passphrase is BIP39 compliant.",
},
{
task: () => {
const validatorsConfig = this.app.getCorePath("config", "validators.json");

const validators: Record<string, string | string[]> = require(validatorsConfig);
validators.secrets = [flags.bip39];

writeJSONSync(validatorsConfig, validators);
},
title: "Writing BIP39 passphrase to configuration.",
},
]);
}
}
Loading

0 comments on commit d22af15

Please sign in to comment.