From 708684a98353e47aab0dd252b6bc4c5d6bc141c7 Mon Sep 17 00:00:00 2001 From: Sebastien La Duca Date: Mon, 29 Apr 2024 10:44:34 -0400 Subject: [PATCH] add untested CLI (#682) * add untested CLI * add missing argument --- packages/ejector/package.json | 1 + packages/ejector/src/cli.ts | 78 +++++++++++++++++++ packages/ejector/src/deployment/index.ts | 57 ++++++++++---- .../src/setup/downloadCircuitArtifacts.ts | 17 ++-- packages/ejector/src/setup/index.ts | 9 ++- packages/ejector/src/withdraw.ts | 4 +- yarn.lock | 8 ++ 7 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 packages/ejector/src/cli.ts diff --git a/packages/ejector/package.json b/packages/ejector/package.json index 65018d129..3b764e71d 100644 --- a/packages/ejector/package.json +++ b/packages/ejector/package.json @@ -40,6 +40,7 @@ "@nocturne-xyz/op-request-plugins": "workspace:^", "@nocturne-xyz/subgraph-sync-adapters": "workspace:^", "async-mutex": "^0.5.0", + "commander": "^12.0.0", "dotenv": "^16.4.5", "ethers": "^5.7.2", "got": "^12", diff --git a/packages/ejector/src/cli.ts b/packages/ejector/src/cli.ts new file mode 100644 index 000000000..0800a1a28 --- /dev/null +++ b/packages/ejector/src/cli.ts @@ -0,0 +1,78 @@ +#! /usr/bin/env node + +import { program, Command } from "commander"; +import { setup } from "./setup"; +import { WithdrawalClient } from "./withdraw"; +import * as dotenv from "dotenv"; +import { setupEjectorDeployment } from "./deployment"; + +export default async function main(): Promise { + dotenv.config(); + + program + .name("nocturne-ejector") + .description("CLI for withdrawing from Nocturne post-shutdown") + .addCommand(withdraw); + await program.parseAsync(process.argv); +} + +const withdraw = new Command("withdraw") + .summary("withdraw from Nocturne post-shutdown") + .description( + "must supply .env file with RPC_URL, SPEND_PRIVATE_KEY, and WITHDRAWAL_EOA_PRIVATE_KEY" + ) + .option( + "--update-tree", + "if this option is set, the CLI will also run a subtree-updater and update the tree. 99.99% of the time you won't need this - it's only needed if, for some reason, not all of the nodes for an asset are spent at once. See the README for more information.", + false + ) + .option( + "--config-path", + "path to the nocturne deployment config file. 99.99% of the time you wont need this - it's only needed if you want to interact with a secondary deployment of the nocturne contracts. See the README for more information." + ) + .action(async (options) => { + const { updateTree, configPath } = options; + + // download any artifacts necessary for withdrawal, including circuits + await setup({ skipSubtreeUpdateCircuit: !updateTree }); + + // setup local deployment of necessary offchain infra + // by default, this is just the subgraph. + // if updateTree is set, this will also run the subtree-updater and the insertion writer + const localDeployment = await setupEjectorDeployment({ + updateTree, + networkNameOrConfigPath: configPath, + }); + const client = new WithdrawalClient(configPath); + + // sync all note balances into the withdrawal client + await client.sync(); + + // withdraw all notes of every asset owned by the nocturne acount with the provided spend private key + await client.withdrawEverything(); + + // if updateTree is set, fill a batch with zeros and wait for a subtree update to take place + if (updateTree) { + await localDeployment.fillSubtreeBatch(); + } + + // teardown local deployment after we're done + await localDeployment.teardown(); + }); + +process.on("SIGINT", () => { + process.exit(1); +}); + +// ! HACK +// ! in principle, there should be no hanging promises once `main()` resolves or rejects. +// ! as a backstop, we manually call `process.exit()` to ensure the process actually exits +// ! even if there's a bug somewhere that results in a hanging promise +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.log(`exited with error: ${e}`); + process.exit(1); + }); diff --git a/packages/ejector/src/deployment/index.ts b/packages/ejector/src/deployment/index.ts index f7d3bee59..e36bb863d 100644 --- a/packages/ejector/src/deployment/index.ts +++ b/packages/ejector/src/deployment/index.ts @@ -8,7 +8,7 @@ import { import * as ethers from "ethers"; import { startSubtreeUpdater, SubtreeUpdaterConfig } from "./subtreeUpdater"; import { InsertionWriterConfig, startInsertionWriter } from "./insertionWriter"; -import { sleep, SUBGRAPH_URL } from "../utils"; +import { sleep, SUBGRAPH_URL, TeardownFn } from "../utils"; import { getEnvVars } from "../env"; import { startSubgraph } from "./subgraph"; @@ -45,9 +45,17 @@ const INSERTION_WRITER_CONFIG: InsertionWriterConfig = { subgraphUrl: SUBGRAPH_URL, }; +export type SetupOptions = { + networkNameOrConfigPath: string; + updateTree: boolean; +}; + export async function setupEjectorDeployment( - networkName = "mainnet" + options: Partial = {} ): Promise { + const networkNameOrConfigPath = options.networkNameOrConfigPath ?? "mainnet"; + const updateTree = options.updateTree ?? false; + const { RPC_URL, SPEND_PRIVATE_KEY, WITHDRAWAL_EOA_PRIVATE_KEY } = getEnvVars(); @@ -59,7 +67,7 @@ export async function setupEjectorDeployment( const eoa = new ethers.Wallet(SPEND_PRIVATE_KEY, provider); // get contract instances - const contractConfig = loadNocturneConfig(networkName); + const contractConfig = loadNocturneConfig(networkNameOrConfigPath); const tellerAddress = contractConfig.contracts.handlerProxy.proxy; const handlerAddress = contractConfig.contracts.tellerProxy.proxy; const [teller, handler] = await Promise.all([ @@ -67,23 +75,35 @@ export async function setupEjectorDeployment( Handler__factory.connect(handlerAddress, eoa), ]); + const teardownFns: TeardownFn[] = []; + // deploy subgraph const teardownSubgraph = await startSubgraph(); + teardownFns.push(teardownSubgraph); - // deploy subtree updater - const teardownSubtreeUpdater = await startSubtreeUpdater( - SUBTREE_UPDATER_CONFIG(RPC_URL, handlerAddress, WITHDRAWAL_EOA_PRIVATE_KEY) - ); + // if updateTree is true, deploy subtree updater + insertion writer using the withdrawal EOA as the tx signer + if (updateTree) { + // deploy subtree updater + const teardownSubtreeUpdater = await startSubtreeUpdater( + SUBTREE_UPDATER_CONFIG( + RPC_URL, + handlerAddress, + WITHDRAWAL_EOA_PRIVATE_KEY + ) + ); + teardownFns.push(teardownSubtreeUpdater); - // deploy insertion writer - const teardownInsertionWriter = await startInsertionWriter( - INSERTION_WRITER_CONFIG - ); + // deploy insertion writer + const teardownInsertionWriter = await startInsertionWriter( + INSERTION_WRITER_CONFIG + ); + teardownFns.push(teardownInsertionWriter); + } const teardown = async () => { - await teardownInsertionWriter(); - await teardownSubtreeUpdater(); - await teardownSubgraph(); + for (const fn of teardownFns) { + await fn(); + } }; const fillSubtreeBatch = async () => { @@ -100,6 +120,7 @@ export async function setupEjectorDeployment( } // wait for subgraph / subtree updater + console.log("waiting for tree update. this may take a few minutes..."); // TODO - listen for subtree update event on teller contract instead await sleep(300_000); }; @@ -109,7 +130,13 @@ export async function setupEjectorDeployment( handler, config: contractConfig, eoa, - fillSubtreeBatch, + fillSubtreeBatch: updateTree + ? fillSubtreeBatch + : async () => { + console.log( + "skipping fillSubtreeBatch - the `update-tree` flag is set to false" + ); + }, teardown, }; } diff --git a/packages/ejector/src/setup/downloadCircuitArtifacts.ts b/packages/ejector/src/setup/downloadCircuitArtifacts.ts index 327991d7a..034694d91 100644 --- a/packages/ejector/src/setup/downloadCircuitArtifacts.ts +++ b/packages/ejector/src/setup/downloadCircuitArtifacts.ts @@ -18,13 +18,18 @@ export const CIRCUIT_ARTIFACTS = { }, }; -export async function downloadCircuitArtifacts(): Promise { - await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.wasm), - await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.zkey), - await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.vkey), +export async function downloadCircuitArtifacts( + skipSubtreeUpdateCircuit: boolean +): Promise { + await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.wasm); + await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.zkey); + await downloadFile(CIRCUIT_ARTIFACTS.joinSplit.vkey); + + if (!skipSubtreeUpdateCircuit) { await downloadFile(CIRCUIT_ARTIFACTS.subtreeupdate.wasm), - await downloadFile(CIRCUIT_ARTIFACTS.subtreeupdate.vkey), - await downloadFile(CIRCUIT_ARTIFACTS.subtreeupdate.zkey); + await downloadFile(CIRCUIT_ARTIFACTS.subtreeupdate.vkey), + await downloadFile(CIRCUIT_ARTIFACTS.subtreeupdate.zkey); + } } async function alreadyDownloadedFile(path: string): Promise { diff --git a/packages/ejector/src/setup/index.ts b/packages/ejector/src/setup/index.ts index 0ef321fff..bd8239516 100644 --- a/packages/ejector/src/setup/index.ts +++ b/packages/ejector/src/setup/index.ts @@ -1,5 +1,10 @@ import { downloadCircuitArtifacts } from "./downloadCircuitArtifacts"; -export async function setup(): Promise { - await Promise.all([downloadCircuitArtifacts()]); +export type SetupArgs = { + skipSubtreeUpdateCircuit: boolean; +}; +export async function setup( + args: SetupArgs = { skipSubtreeUpdateCircuit: true } +): Promise { + await Promise.all([downloadCircuitArtifacts(args.skipSubtreeUpdateCircuit)]); } diff --git a/packages/ejector/src/withdraw.ts b/packages/ejector/src/withdraw.ts index 494417915..6c521854d 100644 --- a/packages/ejector/src/withdraw.ts +++ b/packages/ejector/src/withdraw.ts @@ -43,7 +43,7 @@ export class WithdrawalClient { joinSplitProver: Thunk; - constructor(networkName = "mainnet") { + constructor(networkNameOrConfigPath = "mainnet") { const { RPC_URL, SPEND_PRIVATE_KEY } = getEnvVars(); this.db = new NocturneDB(new InMemoryKVStore()); this.syncAdapter = new SubgraphSDKSyncAdapter(SUBGRAPH_URL); @@ -61,7 +61,7 @@ export class WithdrawalClient { new MockOpTracker() ); - this.config = loadNocturneConfig(networkName); + this.config = loadNocturneConfig(networkNameOrConfigPath); this.teller = Teller__factory.connect( this.config.contracts.tellerProxy.proxy, diff --git a/yarn.lock b/yarn.lock index 9a29224b3..cb2ac541a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3046,6 +3046,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.20.0 "@typescript-eslint/parser": ^5.20.0 async-mutex: ^0.5.0 + commander: ^12.0.0 dotenv: ^16.4.5 eslint: ^7.32.0 eslint-config-prettier: ^8.8.0 @@ -6742,6 +6743,13 @@ circomlibjs@nocturne-xyz/circomlibjs: languageName: node linkType: hard +"commander@npm:^12.0.0": + version: 12.0.0 + resolution: "commander@npm:12.0.0" + checksum: bce9e243dc008baba6b8d923f95b251ad115e6e7551a15838d7568abebcca0fc832da1800cf37caf37852f35ce4b7fb794ba7a4824b88c5adb1395f9268642df + languageName: node + linkType: hard + "commander@npm:^2.19.0": version: 2.20.3 resolution: "commander@npm:2.20.3"