Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d086221
feat: add encode CLI command
oddaf Jun 4, 2025
0978077
feat: encode
oddaf Jun 6, 2025
ba76403
chore: update table header
oddaf Jun 6, 2025
28f8341
chore: update cli readme
oddaf Jun 6, 2025
02eff26
chore: formatting
oddaf Jun 6, 2025
66a00b7
fix: fix evm version to avoid conflicts in fork tests
oddaf Jun 6, 2025
1db9c2d
refactor: list command (no longer default)
oddaf Jun 10, 2025
dac05a8
Apply suggestions from code review
oddaf Jun 10, 2025
a12ec20
Merge branch 'feat/cli-encode' of https://github.com/makerdao/protego…
oddaf Jun 10, 2025
808db10
refactor: use createJson on encode command
oddaf Jun 10, 2025
4742b37
refactor: move no records to display msg to cli functions
oddaf Jun 10, 2025
e8865a4
refactor: project structure
oddaf Jun 10, 2025
da52695
chore: readme
oddaf Jun 10, 2025
1d69622
chore: formatting
oddaf Jun 10, 2025
35913b8
refactor: global options
oddaf Jun 10, 2025
1621108
chore: formatting
oddaf Jun 10, 2025
5ac801e
refactor: remove npm scripts
oddaf Jun 17, 2025
515e393
chore: update readme
oddaf Jun 17, 2025
12ab27d
fix: restore test script
oddaf Jun 17, 2025
074b5ac
refactor: add guy to encode command prompt
oddaf Jun 17, 2025
dfbb0d9
feat: readable date/time for ETA
oddaf Jun 17, 2025
54beb18
refactor: format hex on encode command
oddaf Jun 20, 2025
b899de3
chore: update readme
oddaf Jun 20, 2025
55149b7
fix: guy in cli encode command
oddaf Jun 23, 2025
c1c881b
refactor: display timestamp on table along with readable date
oddaf Jun 24, 2025
60704ca
refactor: format hex strings on list command table (cli)
oddaf Jun 24, 2025
1f83dcf
refactor: use short year
oddaf Jun 24, 2025
f995105
refactor: improve date formatting
oddaf Jun 24, 2025
be7d555
chore: Improve cli readme
oddaf Jul 15, 2025
888a6ab
refactor: JSON loading without experimental features
oddaf Jul 15, 2025
7de943b
refactor: Encode command description
oddaf Jul 15, 2025
6cfc040
Apply suggestions from code review
oddaf Jul 21, 2025
12d6d63
refactor: move loadJson right after createJson
oddaf Jul 21, 2025
85d404a
Update cli/package.json
oddaf Jul 23, 2025
e4f4c49
update docs for sky-ecosystem npm org
oddaf Jul 23, 2025
4818fe3
chore: bump npm version (minor)
oddaf Jul 23, 2025
511685c
Update cli/list.js
oddaf Jul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 175 additions & 51 deletions cli/README.md

Large diffs are not rendered by default.

189 changes: 27 additions & 162 deletions cli/cli.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
#!/usr/bin/env node

import { Command, Option } from "commander";
import chalk from "chalk";
import yoctoSpinner from "yocto-spinner";
import figlet from "figlet";
import { ethers } from "ethers";
import { table } from "table";
import { fetchPausePlans } from "./fetchPausePlans.js";
import defaults from "./defaults.js";
import p from "./package.json" with { type: "json" };
import { list } from "./list.js";
import { encode } from "./encode.js";
import { loadJson } from "./utils.js";

const p = loadJson(import.meta.url, "./package.json");

const program = new Command();

Expand All @@ -29,168 +28,34 @@ program
.argParser(Number.parseInt)
.default(defaults.FROM_BLOCK),
)
.addOption(
new Option("-s, --status <status>", "Filter by status")
.choices(["PENDING", "DROPPED", "EXECUTED", "ALL"])
.default(defaults.STATUS),
)
.addOption(
new Option(
"--pause-address <address>",
"MCD_PAUSE contract address",
).default(defaults.MCD_PAUSE_ADDRESS),
)
.addOption(
new Option("-f, --format <format>", "Output format")
.choices(["TABLE", "JSON"])
.default(defaults.FORMAT),
)
.showHelpAfterError()
.action(run);

program.parse();

/**
* Runs the CLI
* @param {string} options.rpcUrl Ethereum Node RPC URL
* @param {number} options.fromBlock Display spells from a given block
* @param {"ALL"|"PENDING"|"DROPPED"|"EXECUTED"} options.status Filter by status
* @param {string} options.pauseAddress MCD_PAUSE contract address
* @returns {Promise<void>}
*/
async function run({ rpcUrl, fromBlock, status, pauseAddress, format }) {
if (rpcUrl === defaults.RPC_URL) {
console.warn(
chalk.yellow(
`🟡 WARNING: Falling back to a public provider: ${rpcUrl}. For a better experience, set a custom RPC URL with the --rpc-url <rpc-url> option or the ETH_RPC_URL env variable.`,
),
);
}

const spinner = ttyOnlySpinner().start("Fetching pause plans...");

try {
const pause = new ethers.Contract(
pauseAddress,
defaults.MCD_PAUSE_ABI,
ethers.getDefaultProvider(rpcUrl),
);

const plans = await fetchPausePlans(pause, { fromBlock, status });
spinner.success("Done!");

if (format === "TABLE") {
console.log(createTable(plans));
} else {
console.log(createJson(plans));
}

process.exit(0);
} catch (error) {
spinner.error("Failed!");
console.error(chalk.red("An error occurred:", error));
process.exit(1);
}
}

/**
* Creates a spinner that only shows if stdout is a TTY
* @param {...any} args
* @returns {import("yocto-spinner").Spinner}
*/
function ttyOnlySpinner(...args) {
// Only show a spinner if stdout is a TTY
if (process.stdout.isTTY) {
return yoctoSpinner(...args);
}

// If not a TTY, return a dummy spinner with empty chainable methods
return {
start() {
return this;
},
success() {
return this;
},
error() {
return this;
},
};
}

/**
* Converts a list of pause plans to a formatted table
* @param {import("./fetchPausePlans").PausePlan[]} plans
* @returns {string}
*/
function createTable(plans) {
const data = [...plans]
.sort((a, b) => {
const etaA = BigInt(a.eta);
const etaB = BigInt(b.eta);
return etaB > etaA ? 1 : etaB < etaA ? -1 : 0;
})
.map((event) => [
colorize(event.status, event.guy),
colorize(event.status, event.hash),
colorize(event.status, event.usr),
colorize(event.status, event.tag),
colorize(event.status, event.fax),
colorize(event.status, event.eta),
colorize(event.status),
]);

if (data.length === 0) {
return "No records to display.";
}

data.unshift(
["SPELL", "HASH", "USR", "TAG", "FAX", "ETA", "STATUS"].map((item) =>
chalk.bold.cyan(item),
),
.addCommand(
new Command("list")
.description("List spells by status")
.addOption(
new Option("-s, --status <status>", "Filter by status")
.choices(["PENDING", "DROPPED", "EXECUTED", "ALL"])
.default(defaults.STATUS),
)
.addOption(
new Option("-f, --format <format>", "Output format")
.choices(["TABLE", "JSON"])
.default(defaults.FORMAT),
)
.action(list),
)
.addCommand(
new Command("encode")
.description(
"Encode calldata to drop spells (i.e: input for Etherscan/Tenderly UIs)",
)
.action(encode),
);

return table(data, {
columns: {
0: { width: 21, wrapWord: true },
1: { width: 33, wrapWord: true },
2: { width: 21, wrapWord: true },
3: { width: 33, wrapWord: true },
4: { width: 10, wrapWord: true },
5: { width: 10, wrapWord: true },
6: { width: 10, wrapWord: true },
},
});
}

/**
* Colorizes a status string
* @param {string} status The status to define the color
* @param {string} [text=status] The text to colorize
* @returns {string}
*/
function colorize(status, text = status) {
switch (status) {
case "PENDING":
return chalk.yellow(String(text));
case "DROPPED":
return chalk.red(String(text));
case "EXECUTED":
return chalk.green(String(text));
default:
return status;
}
}

/**
* Converts a list of pause plans to a JSON string
* @param {import("./fetchPausePlans").PausePlan[]} plans
* @returns {string}
*/
function createJson(plans) {
return JSON.stringify(
plans,
(_, v) => (typeof v === "bigint" ? v.toString() : v),
2,
);
}
program.parse();
4 changes: 3 additions & 1 deletion cli/defaults.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pauseABI from "./pause-abi.json" with { type: "json" };
import { loadJson } from "./utils.js";

const pauseABI = loadJson(import.meta.url, "./pause-abi.json");

export default {
MCD_PAUSE_ADDRESS: "0xbE286431454714F511008713973d3B053A2d38f3",
Expand Down
69 changes: 69 additions & 0 deletions cli/encode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import chalk from "chalk";
import { ethers } from "ethers";
import prompts from "prompts";
import { fetchPausePlans } from "./fetchPausePlans.js";
import defaults from "./defaults.js";
import { ttyOnlySpinner, createJson, formatDate, formatHex } from "./utils.js";

/**
* Runs the CLI Encode command
* @param {object} localOptions Command options (currently unused)
* @param {import("commander").Command} command Commander command object
* @returns {Promise<void>}
*/
export async function encode(localOptions, command) {
const { rpcUrl, fromBlock, pauseAddress } = command.optsWithGlobals();

if (rpcUrl === defaults.RPC_URL) {
console.warn(
chalk.yellow(
`🟡 WARNING: Falling back to a public provider: ${rpcUrl}. For a better experience, set a custom RPC URL with the --rpc-url <rpc-url> option or the ETH_RPC_URL env variable.`,
),
);
}

const spinner = ttyOnlySpinner().start("Fetching pending pause plans...");

try {
const pause = new ethers.Contract(
pauseAddress,
defaults.MCD_PAUSE_ABI,
ethers.getDefaultProvider(rpcUrl),
);

const plans = await fetchPausePlans(pause, {
fromBlock,
status: "PENDING",
});
spinner.success("Done!");

if (plans.length === 0) {
console.log(chalk.yellow("No pending spells found to encode."));
process.exit(1);
}

const response = await prompts([
{
type: "multiselect",
name: "plans",
message:
"Select spells to be encoded for `drop(Plan[] calldata _plans)`",
choices: plans.map((plan) => ({
title: `guy: ${formatHex(plan.guy)} | hash: ${formatHex(plan.hash)} | usr: ${formatHex(plan.usr)} | eta: ${plan.eta} (${formatDate(plan.eta)} UTC)`,
value: plan.hash,
})),
},
]);

const selectedPlans = plans
.filter((plan) => response.plans.includes(plan.hash))
.map((plan) => [plan.usr, plan.tag, plan.fax, plan.eta.toString()]);

console.log("\nEncoded plans:");
console.log(chalk.green(createJson(selectedPlans)));
} catch (error) {
spinner.error("Failed!");
console.error(chalk.red("An error occurred:", error));
process.exit(1);
}
}
Loading