From 7f3fc95956c92ebec4119d40fff40ff261f420e8 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 May 2022 22:43:32 +0700 Subject: [PATCH] Generalize eth package manager --- src/commands/build.ts | 13 +- src/commands/githubActions/build/index.ts | 2 + src/commands/increase.ts | 46 ----- src/commands/list.ts | 62 +++++++ src/commands/next.ts | 56 ------ src/commands/publish.ts | 59 +++--- src/commands/version.ts | 63 +++++++ src/dappnodesdk.ts | 8 +- src/index.ts | 10 +- src/{utils => providers/pm/apm}/Apm.ts | 174 ++++++++++++------ .../pm/apm}/ApmRegistryAbi.json | 0 .../pm/apm}/RepoAbi.json | 0 src/providers/pm/index.ts | 10 + src/providers/pm/interface.ts | 28 +++ src/tasks/createGithubRelease.ts | 65 +++---- src/tasks/generatePublishTx.ts | 112 +++-------- src/types.ts | 5 +- src/utils/checkSemverType.ts | 13 -- src/utils/getLinks.ts | 12 +- src/utils/outputTxData.ts | 76 -------- src/utils/verifyEthConnection.ts | 8 +- src/utils/versions/getCurrentLocalVersion.ts | 9 - src/utils/versions/getNextVersionFromApm.ts | 37 ---- src/utils/versions/increaseFromApmVersion.ts | 34 ---- .../versions/increaseFromLocalVersion.ts | 38 ---- 25 files changed, 378 insertions(+), 562 deletions(-) delete mode 100644 src/commands/increase.ts create mode 100644 src/commands/list.ts delete mode 100644 src/commands/next.ts create mode 100644 src/commands/version.ts rename src/{utils => providers/pm/apm}/Apm.ts (50%) rename src/{contracts => providers/pm/apm}/ApmRegistryAbi.json (100%) rename src/{contracts => providers/pm/apm}/RepoAbi.json (100%) create mode 100644 src/providers/pm/index.ts create mode 100644 src/providers/pm/interface.ts delete mode 100644 src/utils/checkSemverType.ts delete mode 100644 src/utils/outputTxData.ts delete mode 100644 src/utils/versions/getCurrentLocalVersion.ts delete mode 100644 src/utils/versions/getNextVersionFromApm.ts delete mode 100644 src/utils/versions/increaseFromApmVersion.ts delete mode 100644 src/utils/versions/increaseFromLocalVersion.ts diff --git a/src/commands/build.ts b/src/commands/build.ts index 2dbfd8d4..00ddc2d5 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -1,11 +1,9 @@ -import path from "path"; import chalk from "chalk"; import { CommandModule } from "yargs"; import Listr from "listr"; // Tasks import { buildAndUpload } from "../tasks/buildAndUpload"; // Utils -import { getCurrentLocalVersion } from "../utils/versions/getCurrentLocalVersion"; import { getInstallDnpLink } from "../utils/getLinks"; import { CliGlobalOptions } from "../types"; import { UploadTo } from "../releaseUploader"; @@ -14,6 +12,7 @@ import { defaultComposeFileName, defaultDir } from "../params"; interface CliCommandOptions extends CliGlobalOptions { provider: string; upload_to: UploadTo; + build_dir: string; timeout?: string; skip_save?: boolean; skip_upload?: boolean; @@ -37,6 +36,11 @@ export const build: CommandModule = { choices: ["ipfs", "swarm"] as UploadTo[], default: "ipfs" as UploadTo }, + build_dir: { + description: "Target directory to write build files", + default: "./build", + normalize: true + }, timeout: { alias: "t", description: `Overrides default build timeout: "15h", "20min 15s", "5000". Specs npmjs.com/package/timestring`, @@ -74,6 +78,7 @@ export async function buildHandler({ provider, timeout, upload_to, + build_dir, skip_save, skip_upload, require_git_data, @@ -91,13 +96,11 @@ export async function buildHandler({ const skipSave = skip_save; const skipUpload = skip_save || skip_upload; const composeFileName = compose_file_name; - const nextVersion = getCurrentLocalVersion({ dir }); - const buildDir = path.join(dir, `build_${nextVersion}`); const buildTasks = new Listr( buildAndUpload({ dir, - buildDir, + buildDir: build_dir, contentProvider, uploadTo, userTimeout, diff --git a/src/commands/githubActions/build/index.ts b/src/commands/githubActions/build/index.ts index 1a967de9..f77efa0e 100644 --- a/src/commands/githubActions/build/index.ts +++ b/src/commands/githubActions/build/index.ts @@ -74,6 +74,7 @@ export async function gaBuildHandler({ await buildHandler({ provider: "dappnode", upload_to: "ipfs", + build_dir: "build", skip_save: true, verbose: true }); @@ -99,6 +100,7 @@ export async function buildAndComment({ const { releaseMultiHash } = await buildHandler({ provider: "pinata", upload_to: "ipfs", + build_dir: "build", require_git_data: true, delete_old_pins: true, verbose: true diff --git a/src/commands/increase.ts b/src/commands/increase.ts deleted file mode 100644 index 80b50155..00000000 --- a/src/commands/increase.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommandModule } from "yargs"; -import { increaseFromLocalVersion } from "../utils/versions/increaseFromLocalVersion"; -import { CliGlobalOptions, ReleaseType } from "../types"; -import { defaultComposeFileName, defaultDir } from "../params"; - -export const command = "increase [type]"; - -export const describe = "Increases the version defined in the manifest"; - -interface CliCommandOptions extends CliGlobalOptions { - type: string; -} - -export const increase: CommandModule = { - command: "increase [type]", - describe: "Increases the version defined in the manifest", - - builder: yargs => - yargs.positional("type", { - description: "Semver update type: [ major | minor | patch ]", - choices: ["major", "minor", "patch"], - type: "string", - demandOption: true - }), - - handler: async (args): Promise => { - const nextVersion = await increaseHandler(args); - // Output result: "0.1.8" - console.log(nextVersion); - } -}; - -/** - * Common handler for CLI and programatic usage - */ -export async function increaseHandler({ - type, - dir = defaultDir, - compose_file_name = defaultComposeFileName -}: CliCommandOptions): Promise { - return await increaseFromLocalVersion({ - type: type as ReleaseType, - dir, - compose_file_name - }); -} diff --git a/src/commands/list.ts b/src/commands/list.ts new file mode 100644 index 00000000..812d6883 --- /dev/null +++ b/src/commands/list.ts @@ -0,0 +1,62 @@ +import { CommandModule } from "yargs"; +import { CliGlobalOptions } from "../types"; +import { defaultDir } from "../params"; +import { readManifest } from "../utils/manifest"; +import { getPM } from "../providers/pm"; + +interface CliCommandOptions extends CliGlobalOptions { + provider: string; + tag?: string; +} + +export const list: CommandModule = { + command: "list [tag]", + describe: "List package version tags", + + builder: yargs => + yargs + .positional("tag", { + description: "tag to print version of", + type: "string" + }) + .option("provider", { + description: `Specify an eth provider: "dappnode" (default), "infura", "localhost:8545"`, + default: "dappnode", + type: "string" + }), + + handler: async (args): Promise => { + const tagsWithVersions = await listHandler(args); + + if (args.tag) { + // Output result: "0.1.8" + const version = tagsWithVersions[args.tag]; + if (!version) { + throw Error(`Tag ${args.tag} not found`); + } else { + console.log(version); + } + } else { + for (const [tag, version] of Object.entries(tagsWithVersions)) { + // Output result: "latest: 0.1.8" + console.log(`${tag}: ${version}`); + } + } + } +}; + +/** + * Common handler for CLI and programatic usage + */ +export async function listHandler({ + provider, + dir = defaultDir +}: CliCommandOptions): Promise> { + const { manifest } = readManifest({ dir }); + + const pm = getPM(provider); + + return { + latest: await pm.getLatestVersion(manifest.name) + }; +} diff --git a/src/commands/next.ts b/src/commands/next.ts deleted file mode 100644 index d35956d6..00000000 --- a/src/commands/next.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CommandModule } from "yargs"; -import { getNextVersionFromApm } from "../utils/versions/getNextVersionFromApm"; -import { verifyEthConnection } from "../utils/verifyEthConnection"; -import { CliGlobalOptions, ReleaseType } from "../types"; -import { defaultDir } from "../params"; - -interface CliCommandOptions extends CliGlobalOptions { - type: string; - provider: string; -} - -export const next: CommandModule = { - command: "next [type]", - describe: "Compute the next release version from local", - - builder: yargs => - yargs - .positional("type", { - description: "Semver update type: [ major | minor | patch ]", - choices: ["major", "minor", "patch"], - type: "string" - }) - .option("provider", { - alias: "p", - description: `Specify an ipfs provider: "dappnode" (default), "infura", "localhost:5002"`, - default: "dappnode", - type: "string" - }) - .require("type"), - - handler: async (args): Promise => { - const nextVersion = await nextHandler(args); - // Output result: "0.1.8" - console.log(nextVersion); - } -}; - -/** - * Common handler for CLI and programatic usage - */ -export async function nextHandler({ - type, - provider, - dir = defaultDir -}: CliCommandOptions): Promise { - const ethProvider = provider; - - await verifyEthConnection(ethProvider); - - // Execute command - return await getNextVersionFromApm({ - type: type as ReleaseType, - ethProvider, - dir - }); -} diff --git a/src/commands/publish.ts b/src/commands/publish.ts index b37922d9..bbbe927a 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -1,4 +1,3 @@ -import path from "path"; import Listr from "listr"; import chalk from "chalk"; import { CommandModule } from "yargs"; @@ -7,14 +6,13 @@ import { buildAndUpload } from "../tasks/buildAndUpload"; import { generatePublishTx } from "../tasks/generatePublishTx"; import { createGithubRelease } from "../tasks/createGithubRelease"; // Utils -import { getCurrentLocalVersion } from "../utils/versions/getCurrentLocalVersion"; -import { increaseFromApmVersion } from "../utils/versions/increaseFromApmVersion"; import { verifyEthConnection } from "../utils/verifyEthConnection"; -import { getInstallDnpLink, getPublishTxLink } from "../utils/getLinks"; +import { getInstallDnpLink } from "../utils/getLinks"; import { defaultComposeFileName, defaultDir, YargsError } from "../params"; import { CliGlobalOptions, ReleaseType, releaseTypes, TxData } from "../types"; import { printObject } from "../utils/print"; import { UploadTo } from "../releaseUploader"; +import { readManifest } from "../utils/manifest"; const typesList = releaseTypes.join(" | "); @@ -88,9 +86,12 @@ export const publish: CommandModule = { }), handler: async args => { - const { txData, nextVersion, releaseMultiHash } = await publishHanlder( - args - ); + const { + version, + releaseMultiHash, + txData, + txPublishLink + } = await publishHanlder(args); if (!args.silent) { const txDataToPrint = { @@ -101,7 +102,7 @@ export const publish: CommandModule = { }; console.log(` - ${chalk.green(`DNP (DAppNode Package) published (version ${nextVersion})`)} + ${chalk.green(`DNP (DAppNode Package) published (version ${version})`)} Release hash : ${releaseMultiHash} ${getInstallDnpLink(releaseMultiHash)} @@ -113,7 +114,7 @@ export const publish: CommandModule = { ${"You can also execute this transaction with Metamask by following this pre-filled link"} - ${chalk.cyan(getPublishTxLink(txData))} + ${chalk.cyan(txPublishLink)} `); } } @@ -140,8 +141,9 @@ export async function publishHanlder({ silent, verbose }: CliCommandOptions): Promise<{ + version: string; txData: TxData; - nextVersion: string; + txPublishLink: string; releaseMultiHash: string; }> { // Parse optionsalias: "release", @@ -159,6 +161,10 @@ export async function publishHanlder({ const tag = process.env.TRAVIS_TAG || process.env.GITHUB_REF; const typeFromEnv = process.env.RELEASE_TYPE; + // Target version to build + const { manifest } = readManifest({ dir }); + const version = manifest.version; + /** * Specific set of options used for internal DAppNode releases. * Caution: options may change without notice. @@ -199,30 +205,7 @@ export async function publishHanlder({ const publishTasks = new Listr( [ - // 1. Fetch current version from APM - { - title: "Fetch current version from APM", - task: async (ctx, task) => { - let nextVersion; - try { - nextVersion = await increaseFromApmVersion({ - type: type as ReleaseType, - ethProvider, - dir, - composeFileName - }); - } catch (e) { - if (e.message.includes("NOREPO")) - nextVersion = getCurrentLocalVersion({ dir }); - else throw e; - } - ctx.nextVersion = nextVersion; - ctx.buildDir = path.join(dir, `build_${nextVersion}`); - task.title = task.title + ` (next version: ${nextVersion})`; - } - }, - - // 2. Build and upload + // Build and upload { title: "Build and upload", task: ctx => @@ -241,7 +224,7 @@ export async function publishHanlder({ ) }, - // 3. Generate transaction + // Generate transaction { title: "Generate transaction", task: ctx => @@ -256,7 +239,7 @@ export async function publishHanlder({ }) }, - // 4. Create github release + // Create github release // [ONLY] add the Release task if requested { title: "Release on github", @@ -276,6 +259,6 @@ export async function publishHanlder({ ); const tasksFinalCtx = await publishTasks.run(); - const { txData, nextVersion, releaseMultiHash } = tasksFinalCtx; - return { txData, nextVersion, releaseMultiHash }; + const { releaseMultiHash, txData, txPublishLink } = tasksFinalCtx; + return { version, releaseMultiHash, txData, txPublishLink }; } diff --git a/src/commands/version.ts b/src/commands/version.ts new file mode 100644 index 00000000..03c456c9 --- /dev/null +++ b/src/commands/version.ts @@ -0,0 +1,63 @@ +import { CommandModule } from "yargs"; +import semver from "semver"; +import { CliGlobalOptions } from "../types"; +import { defaultComposeFileName, defaultDir } from "../params"; +import { readManifest, writeManifest } from "../utils/manifest"; +import { + readCompose, + updateComposeImageTags, + writeCompose +} from "../utils/compose"; + +interface CliCommandOptions extends CliGlobalOptions { + type: string; +} + +export const version: CommandModule = { + command: "version [type]", + describe: "Bump a package version", + + builder: yargs => + yargs.positional("type", { + description: "Semver update type: [ major | minor | patch ]", + choices: ["major", "minor", "patch"], + type: "string", + demandOption: true + }), + + handler: async (args): Promise => { + const nextVersion = await versionHandler(args); + // Output result: "0.1.8" + console.log(nextVersion); + } +}; + +/** + * Common handler for CLI and programatic usage + */ +export async function versionHandler({ + type, + dir = defaultDir, + compose_file_name: composeFileName = defaultComposeFileName +}: CliCommandOptions): Promise { + // Load manifest + const { manifest, format } = readManifest({ dir }); + + // If `type` is a valid `semver.ReleaseType` the version will be bumped, else return null + const nextVersion = + semver.inc(manifest.version, type as semver.ReleaseType) || type; + if (!semver.valid(nextVersion)) { + throw Error(`Invalid semver bump or version: ${type}`); + } + + // Mofidy and write the manifest and docker-compose + manifest.version = nextVersion; + writeManifest(manifest, format, { dir }); + + const { name, version } = manifest; + const compose = readCompose({ dir, composeFileName }); + const newCompose = updateComposeImageTags(compose, { name, version }); + writeCompose(newCompose, { dir, composeFileName }); + + return nextVersion; +} diff --git a/src/dappnodesdk.ts b/src/dappnodesdk.ts index bca9e06f..76e10432 100755 --- a/src/dappnodesdk.ts +++ b/src/dappnodesdk.ts @@ -7,10 +7,10 @@ import dotenv from "dotenv"; import { build } from "./commands/build"; import { fromGithub } from "./commands/from_github"; -import { increase } from "./commands/increase"; import { init } from "./commands/init"; -import { next } from "./commands/next"; +import { list } from "./commands/list"; import { publish } from "./commands/publish"; +import { version } from "./commands/version"; import { githubActions } from "./commands/githubActions"; // "source-map-support" MUST be imported for stack traces to work properly after Typescript transpile - @@ -53,10 +53,10 @@ const dappnodesdk = yargs }) .command(build) .command(fromGithub) - .command(increase) .command(init) - .command(next) + .command(list) .command(publish) + .command(version) .command(githubActions); dappnodesdk.alias("h", "help"); diff --git a/src/index.ts b/src/index.ts index cd117834..219c216d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,15 @@ import { buildHandler } from "./commands/build"; import { fromGithubHandler } from "./commands/from_github"; -import { increaseHandler } from "./commands/increase"; import { initHandler } from "./commands/init"; -import { nextHandler } from "./commands/next"; +import { listHandler } from "./commands/list"; import { publishHanlder } from "./commands/publish"; +import { versionHandler } from "./commands/version"; export const dappnodesdk = { build: buildHandler, fromGithub: fromGithubHandler, - increase: increaseHandler, init: initHandler, - next: nextHandler, - publish: publishHanlder + list: listHandler, + publish: publishHanlder, + version: versionHandler }; diff --git a/src/utils/Apm.ts b/src/providers/pm/apm/Apm.ts similarity index 50% rename from src/utils/Apm.ts rename to src/providers/pm/apm/Apm.ts index e80582fa..636519ba 100644 --- a/src/utils/Apm.ts +++ b/src/providers/pm/apm/Apm.ts @@ -1,8 +1,10 @@ import { ethers } from "ethers"; -import { arrayToSemver } from "../utils/arrayToSemver"; -import repoAbi from "../contracts/RepoAbi.json"; -import registryAbi from "../contracts/ApmRegistryAbi.json"; -import { semverToArray } from "./semverToArray"; +import { arrayToSemver } from "../../../utils/arrayToSemver"; +import repoAbi from "./RepoAbi.json"; +import registryAbi from "./ApmRegistryAbi.json"; +import { semverToArray } from "../../../utils/semverToArray"; +import { IPM, TxInputs, TxSummary } from "../interface"; +import { YargsError } from "../../../params"; function getEthereumProviderUrl(provider = "dappnode"): string { if (provider === "dappnode") { @@ -18,6 +20,8 @@ function getEthereumProviderUrl(provider = "dappnode"): string { } } +const zeroAddress = "0x0000000000000000000000000000000000000000"; + /** * @param provider user selected provider. Possible values: * - null @@ -27,7 +31,7 @@ function getEthereumProviderUrl(provider = "dappnode"): string { * - "ws://localhost:8546" * @return apm instance */ -export class Apm { +export class Apm implements IPM { provider: ethers.providers.JsonRpcProvider; constructor(providerId: string) { @@ -39,6 +43,74 @@ export class Apm { this.provider = new ethers.providers.JsonRpcProvider(providerUrl); } + isListening(): Promise { + return this.provider.send("net_listening", []); + } + + async populatePublishTransaction({ + dnpName, + version, + releaseMultiHash, + developerAddress + }: TxInputs): Promise { + // TODO: Ensure APM format + const contentURI = + "0x" + Buffer.from(releaseMultiHash, "utf8").toString("hex"); + + const repository = await this.getRepoContract(dnpName); + if (repository) { + const repo = new ethers.utils.Interface(repoAbi); + // newVersion( + // uint16[3] _newSemanticVersion, + // address _contractAddress, + // bytes _contentURI + // ) + const txData = repo.encodeFunctionData("newVersion", [ + semverToArray(version), // uint16[3] _newSemanticVersion + zeroAddress, // address _contractAddress + contentURI // bytes _contentURI + ]); + + return { + to: repository.address, + value: 0, + data: txData, + gasLimit: 300000 + }; + } + + // If repository does not exist, deploy new one + else { + const registry = await this.getRegistryContract(dnpName); + if (!registry) { + throw Error(`There must exist a registry for DNP name ${dnpName}`); + } + + // newRepoWithVersion( + // string _name, + // address _dev, + // uint16[3] _initialSemanticVersion, + // address _contractAddress, + // bytes _contentURI + // ) + const registryInt = new ethers.utils.Interface(registryAbi); + const txData = registryInt.encodeFunctionData("newRepoWithVersion", [ + getShortName(dnpName), // string _name + ensureValidDeveloperAddress(developerAddress), // address _dev + semverToArray(version), // uint16[3] _initialSemanticVersion + zeroAddress, // address _contractAddress + contentURI // bytes _contentURI + ]); + + return { + to: registry.address, + value: 0, + data: txData, + gasLimit: 300000 + }; + } + } + // Ens throws if a node is not found // // ens.resolver('admin.dnp.dappnode.eth').addr() @@ -52,7 +124,7 @@ export class Apm { return await this.provider.resolveName(ensDomain); } catch (e) { // This error is particular for ethjs - if (e.message.includes("ENS name not defined")) return null; + if ((e as Error).message.includes("ENS name not defined")) return null; else throw e; } } @@ -85,7 +157,9 @@ export class Apm { return arrayToSemver(res.semanticVersion); } catch (e) { // Rename error for user comprehension - e.message = `Error getting latest version of ${ensName}: ${e.message}`; + (e as Error).message = `Error getting latest version of ${ensName}: ${ + (e as Error).message + }`; throw e; } } @@ -97,7 +171,9 @@ export class Apm { * @param ensName: "admin.dnp.dappnode.eth" * @return contract instance of the Repo "admin.dnp.dappnode.eth" */ - async getRepoContract(ensName: string): Promise { + private async getRepoContract( + ensName: string + ): Promise { const repoAddress = await this.resolve(ensName); if (!repoAddress) return null; return new ethers.Contract(repoAddress, repoAbi, this.provider); @@ -112,7 +188,9 @@ export class Apm { * @param ensName: "admin.dnp.dappnode.eth" * @return contract instance of the Registry "dnp.dappnode.eth" */ - async getRegistryContract(ensName: string): Promise { + private async getRegistryContract( + ensName: string + ): Promise { const repoId = ensName.split(".").slice(1).join("."); const registryAddress = await this.resolve(repoId); if (!registryAddress) return null; @@ -120,58 +198,32 @@ export class Apm { } } -/** - * newVersion( - * uint16[3] _newSemanticVersion, - * address _contractAddress, - * bytes _contentURI - * ) - */ -export function encodeNewVersionCall({ - version, - contractAddress, - contentURI -}: { - version: string; - contractAddress: string; - contentURI: string; -}): string { - const repo = new ethers.utils.Interface(repoAbi); - return repo.encodeFunctionData("newVersion", [ - semverToArray(version), // uint16[3] _newSemanticVersion - contractAddress, // address _contractAddress - contentURI // bytes _contentURI - ]); +/** Short name is the last part of an ENS name */ +function getShortName(dnpName: string): string { + return dnpName.split(".")[0]; } -/** - * newRepoWithVersion( - * string _name, - * address _dev, - * uint16[3] _initialSemanticVersion, - * address _contractAddress, - * bytes _contentURI - * ) - */ -export function encodeNewRepoWithVersionCall({ - name, - developerAddress, - version, - contractAddress, - contentURI -}: { - name: string; - developerAddress: string; - version: string; - contractAddress: string; - contentURI: string; -}): string { - const registry = new ethers.utils.Interface(registryAbi); - return registry.encodeFunctionData("newRepoWithVersion", [ - name, // string _name - developerAddress, // address _dev - semverToArray(version), // uint16[3] _initialSemanticVersion - contractAddress, // address _contractAddress - contentURI // bytes _contentURI - ]); +function ensureValidDeveloperAddress(address: string | undefined): string { + if ( + !address || + !ethers.utils.isAddress(address) || + // check if is zero address + parseInt(address) === 0 + ) { + throw new YargsError( + `A new Aragon Package Manager Repo must be created. +You must specify the developer address that will control it + +with ENV: + +DEVELOPER_ADDRESS=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B dappnodesdk publish [type] + +with command option: + +dappnodesdk publish [type] --developer_address 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B +` + ); + } + + return address; } diff --git a/src/contracts/ApmRegistryAbi.json b/src/providers/pm/apm/ApmRegistryAbi.json similarity index 100% rename from src/contracts/ApmRegistryAbi.json rename to src/providers/pm/apm/ApmRegistryAbi.json diff --git a/src/contracts/RepoAbi.json b/src/providers/pm/apm/RepoAbi.json similarity index 100% rename from src/contracts/RepoAbi.json rename to src/providers/pm/apm/RepoAbi.json diff --git a/src/providers/pm/index.ts b/src/providers/pm/index.ts new file mode 100644 index 00000000..d7202655 --- /dev/null +++ b/src/providers/pm/index.ts @@ -0,0 +1,10 @@ +import { Apm } from "./apm/Apm"; +import { IPM } from "./interface"; + +export function getPM(provider: string): IPM { + // TODO: Generalize with both APM and DPM + // TODO: Find a way to switch between both: + // - Pre-declared in the manifest? + // - Check on chain in multiple providers? + return new Apm(provider); +} diff --git a/src/providers/pm/interface.ts b/src/providers/pm/interface.ts new file mode 100644 index 00000000..3140fd8e --- /dev/null +++ b/src/providers/pm/interface.ts @@ -0,0 +1,28 @@ +type SemverStr = string; + +export type TxInputs = { + dnpName: string; + version: string; + releaseMultiHash: string; + developerAddress?: string; +}; + +export type TxSummary = { + to: string; + value: number; + data: string; + gasLimit: number; +}; + +export interface IPM { + getLatestVersion(dnpName: string): Promise; + // isRepoDeployed(dnpName: string): Promise; + + /** + * Tests if the connected JSON RPC is listening and available. + * Uses the `net_listening` method. + */ + isListening(): Promise; + + populatePublishTransaction(inputs: TxInputs): Promise; +} diff --git a/src/tasks/createGithubRelease.ts b/src/tasks/createGithubRelease.ts index cffd311f..2236f3f2 100644 --- a/src/tasks/createGithubRelease.ts +++ b/src/tasks/createGithubRelease.ts @@ -1,15 +1,11 @@ import fs from "fs"; import path from "path"; import Listr from "listr"; -import { getPublishTxLink, getInstallDnpLink } from "../utils/getLinks"; +import { getInstallDnpLink } from "../utils/getLinks"; import { getGitHead } from "../utils/git"; import { compactManifestIfCore } from "../utils/compactManifest"; import { contentHashFile, defaultDir } from "../params"; -import { - TxData, - CliGlobalOptions, - ListrContextBuildAndPublish -} from "../types"; +import { CliGlobalOptions, ListrContextBuildAndPublish } from "../types"; import { Github } from "../providers/github/Github"; import { composeDeleteBuildProperties } from "../utils/compose"; @@ -108,37 +104,9 @@ export function createGithubRelease({ // https://github.com/dappnode/DAppNode_Installer/issues/161 composeDeleteBuildProperties({ dir: buildDir, composeFileName }); - task.output = `Creating release for tag ${tag}...`; - await github.createReleaseAndUploadAssets(tag, { - body: getReleaseBody(txData), - // Tag as pre-release until it is actually published in APM mainnet - prerelease: true, - assetsDir: buildDir, - // Used to ignore duplicated legacy .tar.xz image - ignorePattern: /\.tar\.xz$/ - }); - - // Clean content hash file so the directory uploaded to IPFS is the same - // as the local build_* dir. User can then `ipfs add -r` and get the same hash - fs.unlinkSync(contentHashPath); - } - } - ], - { renderer: verbose ? "verbose" : silent ? "silent" : "default" } - ); -} - -// Utils - -/** - * Write the release body - * #### TODO: Extend this to automatically write the body - */ -function getReleaseBody(txData: TxData) { - const link = getPublishTxLink(txData); - const changelog = ""; - const installLink = getInstallDnpLink(txData.releaseMultiHash); - return ` + const changelog = ""; + const installLink = getInstallDnpLink(releaseMultiHash); + const releaseBody = ` ##### Changelog ${changelog} @@ -147,7 +115,7 @@ ${changelog} ##### For package mantainer -Authorized developer account may execute this transaction [from a pre-filled link](${link})[.](${installLink}) +Authorized developer account may execute this transaction [from a pre-filled link](${ctx.txPublishLink})[.](${installLink})
Release details

@@ -160,11 +128,30 @@ Gas limit: ${txData.gasLimit} \`\`\` \`\`\` -${txData.releaseMultiHash} +${ctx.releaseMultiHash} \`\`\`

`.trim(); + + task.output = `Creating release for tag ${tag}...`; + await github.createReleaseAndUploadAssets(tag, { + body: releaseBody, + // Tag as pre-release until it is actually published in APM mainnet + prerelease: true, + assetsDir: buildDir, + // Used to ignore duplicated legacy .tar.xz image + ignorePattern: /\.tar\.xz$/ + }); + + // Clean content hash file so the directory uploaded to IPFS is the same + // as the local build_* dir. User can then `ipfs add -r` and get the same hash + fs.unlinkSync(contentHashPath); + } + } + ], + { renderer: verbose ? "verbose" : silent ? "silent" : "default" } + ); } diff --git a/src/tasks/generatePublishTx.ts b/src/tasks/generatePublishTx.ts index d7512fa4..f5254077 100644 --- a/src/tasks/generatePublishTx.ts +++ b/src/tasks/generatePublishTx.ts @@ -1,17 +1,10 @@ import Listr from "listr"; -import { ethers } from "ethers"; -import { - Apm, - encodeNewVersionCall, - encodeNewRepoWithVersionCall -} from "../utils/Apm"; import { readManifest } from "../utils/manifest"; import { getPublishTxLink } from "../utils/getLinks"; import { addReleaseTx } from "../utils/releaseRecord"; -import { defaultDir, YargsError } from "../params"; +import { defaultDir } from "../params"; import { CliGlobalOptions, ListrContextBuildAndPublish } from "../types"; - -const isZeroAddress = (address: string): boolean => parseInt(address) === 0; +import { getPM } from "../providers/pm"; /** * Generates the transaction data necessary to publish the package. @@ -36,95 +29,36 @@ export function generatePublishTx({ developerAddress?: string; ethProvider: string; } & CliGlobalOptions): Listr { - // Init APM instance - const apm = new Apm(ethProvider); - - // Load manifest ##### Verify manifest object - const { manifest } = readManifest({ dir }); - - // Compute tx data - const contentURI = - "0x" + Buffer.from(releaseMultiHash, "utf8").toString("hex"); - const contractAddress = "0x0000000000000000000000000000000000000000"; - const currentVersion = manifest.version; - const ensName = manifest.name; - const shortName = manifest.name.split(".")[0]; - return new Listr( [ { title: "Generate transaction", task: async ctx => { - const repository = await apm.getRepoContract(ensName); - if (repository) { - ctx.txData = { - to: repository.address, - value: 0, - data: encodeNewVersionCall({ - version: currentVersion, - contractAddress, - contentURI - }), - gasLimit: 300000, - ensName, - currentVersion, - releaseMultiHash - }; - } else { - const registry = await apm.getRegistryContract(ensName); - if (!registry) - throw Error( - `There must exist a registry for DNP name ${ensName}` - ); - - // If repo does not exist, create a new repo and push version - // A developer address must be provided by the option -a or --developer_address. - if ( - !developerAddress || - !ethers.utils.isAddress(developerAddress) || - isZeroAddress(developerAddress) - ) { - throw new YargsError( - `A new Aragon Package Manager Repo for ${ensName} must be created. -You must specify the developer address that will control it - -with ENV: + // Load manifest ##### Verify manifest object + const { manifest } = readManifest({ dir }); + const dnpName = manifest.name; + const version = manifest.version; - DEVELOPER_ADDRESS=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B dappnodesdk publish [type] - -with command option: + const pm = getPM(ethProvider); + const txSummary = await pm.populatePublishTransaction({ + dnpName: manifest.name, + version: manifest.version, + releaseMultiHash, + developerAddress + }); - dappnodesdk publish [type] --developer_address 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B -` - ); - } + const txPublishLink = getPublishTxLink({ + dnpName, + version, + releaseMultiHash, + developerAddress + }); - ctx.txData = { - to: registry.address, - value: 0, - data: encodeNewRepoWithVersionCall({ - name: shortName, - developerAddress, - version: currentVersion, - contractAddress, - contentURI - }), - gasLimit: 1100000, - ensName, - currentVersion, - releaseMultiHash, - developerAddress - }; - } + // Write Tx data in a file for future reference + addReleaseTx({ dir, version, link: txPublishLink }); - /** - * Write Tx data in a file for future reference - */ - addReleaseTx({ - dir, - version: manifest.version, - link: getPublishTxLink(ctx.txData) - }); + ctx.txData = txSummary; + ctx.txPublishLink = txPublishLink; } } ], diff --git a/src/types.ts b/src/types.ts index 026e46be..d23e9963 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export interface ListrContextBuildAndPublish { // create Github release nextVersion: string; txData: TxData; + txPublishLink: string; } // Interal types @@ -53,10 +54,6 @@ export interface TxData { value: number; data: string; gasLimit: number; - ensName: string; - currentVersion: string; - releaseMultiHash: string; - developerAddress?: string; } export interface TxDataShortKeys { diff --git a/src/utils/checkSemverType.ts b/src/utils/checkSemverType.ts deleted file mode 100644 index e72e58c5..00000000 --- a/src/utils/checkSemverType.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Valid increase types for the Aragon Package Manager (APM) -// https://hack.aragon.org/docs/apm-ref.html -const validIncreaseTypes = ["major", "minor", "patch"]; - -export function checkSemverType(type: string): void { - if (!validIncreaseTypes.includes(type)) { - throw Error( - `Semver increase type "${type}" is not valid. Must be one of: ${validIncreaseTypes.join( - ", " - )}` - ); - } -} diff --git a/src/utils/getLinks.ts b/src/utils/getLinks.ts index 617d9189..0fb9ce35 100644 --- a/src/utils/getLinks.ts +++ b/src/utils/getLinks.ts @@ -1,7 +1,6 @@ import querystring from "querystring"; import { URL } from "url"; import { publishTxAppUrl } from "../params"; -import { TxData } from "../types"; const adminUiBaseUrl = "http://my.dappnode/#"; @@ -9,11 +8,16 @@ const adminUiBaseUrl = "http://my.dappnode/#"; * Get link to publish a TX from a txData object * @param txData */ -export function getPublishTxLink(txData: TxData): string { +export function getPublishTxLink(txData: { + dnpName: string; + version: string; + releaseMultiHash: string; + developerAddress?: string; +}): string { // txData => Admin UI link const txDataShortKeys: { [key: string]: string } = { - r: txData.ensName, - v: txData.currentVersion, + r: txData.dnpName, + v: txData.version, h: txData.releaseMultiHash }; // Only add developerAddress if necessary to not pollute the link diff --git a/src/utils/outputTxData.ts b/src/utils/outputTxData.ts deleted file mode 100644 index 8ca6eae0..00000000 --- a/src/utils/outputTxData.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs"; -import chalk from "chalk"; -import { getPublishTxLink } from "./getLinks"; -import { TxData } from "../types"; -import { printObject } from "./print"; - -export function outputTxData({ - txData, - toConsole, - toFile -}: { - txData: TxData; - toConsole: string; - toFile: string; -}): void { - const adminUiLink = getPublishTxLink(txData); - - const txDataToPrint = { - To: txData.to, - Value: txData.value, - Data: txData.data, - "Gas limit": txData.gasLimit - }; - - const txDataString = printObject( - txDataToPrint, - (key, value) => `${key}: ${value}` - ); - - // If requested output txDataToPrint to file - if (toFile) { - fs.writeFileSync( - toFile, - ` -${txDataString} - -You can execute this transaction with Metamask by following this pre-filled link - -${adminUiLink} - -` - ); - } - - const txDataStringColored = printObject( - txDataToPrint, - (key, value) => ` ${chalk.green(key)}: ${value}` - ); - - // If requested output txDataToPrint to console - if (toConsole) { - console.log(` -${chalk.green("Transaction successfully generated.")} -You must execute this transaction in mainnet to publish a new version of this DNP -To be able to update this repository you must be the authorized dev. - -${chalk.gray("###########################")} TX data ${chalk.gray( - "#############################################" - )} - -${txDataStringColored} - -${chalk.gray( - "#################################################################################" -)} - - You can execute this transaction with Metamask by following this pre-filled link - - ${adminUiLink} - -${chalk.gray( - "#################################################################################" -)} -`); - } -} diff --git a/src/utils/verifyEthConnection.ts b/src/utils/verifyEthConnection.ts index f98a85fe..0e1ff4df 100644 --- a/src/utils/verifyEthConnection.ts +++ b/src/utils/verifyEthConnection.ts @@ -1,5 +1,5 @@ -import { Apm } from "./Apm"; import { CliError } from "../params"; +import { getPM } from "../providers/pm"; /** * Verify the eth connection outside of the eth library to ensure @@ -9,10 +9,10 @@ import { CliError } from "../params"; export async function verifyEthConnection(ethProvider: string): Promise { if (!ethProvider) throw Error("No ethProvider provided"); - const apm = new Apm(ethProvider); + const pm = getPM(ethProvider); + try { - const isListening = await apm.provider.send("net_listening", []); - if (isListening === false) { + if (!(await pm.isListening())) { throw new CliError(`Eth provider ${ethProvider} is not listening`); } } catch (e) { diff --git a/src/utils/versions/getCurrentLocalVersion.ts b/src/utils/versions/getCurrentLocalVersion.ts deleted file mode 100644 index 72c1ef19..00000000 --- a/src/utils/versions/getCurrentLocalVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { readManifest } from "../manifest"; - -export function getCurrentLocalVersion({ dir }: { dir: string }): string { - // Load manifest - const { manifest } = readManifest({ dir }); - const currentVersion = manifest.version; - - return currentVersion; -} diff --git a/src/utils/versions/getNextVersionFromApm.ts b/src/utils/versions/getNextVersionFromApm.ts deleted file mode 100644 index f4037409..00000000 --- a/src/utils/versions/getNextVersionFromApm.ts +++ /dev/null @@ -1,37 +0,0 @@ -import semver from "semver"; -import { readManifest } from "../manifest"; -import { Apm } from "../Apm"; -import { checkSemverType } from "../checkSemverType"; -import { ReleaseType } from "../../types"; - -export async function getNextVersionFromApm({ - type, - ethProvider, - dir -}: { - type: ReleaseType; - ethProvider: string; - dir: string; -}): Promise { - // Check variables - checkSemverType(type); - - // Init APM instance - const apm = new Apm(ethProvider); - - // Load manifest - const { manifest } = readManifest({ dir }); - const ensName = manifest.name.toLowerCase(); - - // Fetch the latest version from APM - const currentVersion = await apm.getLatestVersion(ensName); - - // Increase the version and log it - const nextVersion = semver.inc(currentVersion, type); - if (!nextVersion) - throw Error( - `Error computing next version, is this increase type correct? type: ${type}` - ); - - return nextVersion; -} diff --git a/src/utils/versions/increaseFromApmVersion.ts b/src/utils/versions/increaseFromApmVersion.ts deleted file mode 100644 index d2decd1d..00000000 --- a/src/utils/versions/increaseFromApmVersion.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { readManifest, writeManifest } from "../manifest"; -import { readCompose, writeCompose, updateComposeImageTags } from "../compose"; -import { getNextVersionFromApm } from "./getNextVersionFromApm"; -import { ReleaseType } from "../../types"; - -export async function increaseFromApmVersion({ - type, - ethProvider, - dir, - composeFileName -}: { - type: ReleaseType; - ethProvider: string; - dir: string; - composeFileName: string; -}): Promise { - // Check variables - const nextVersion = await getNextVersionFromApm({ type, ethProvider, dir }); - - // Load manifest - const { manifest, format } = readManifest({ dir }); - - // Increase the version - manifest.version = nextVersion; - - // Mofidy and write the manifest and docker-compose - writeManifest(manifest, format, { dir }); - const { name, version } = manifest; - const compose = readCompose({ dir, composeFileName }); - const newCompose = updateComposeImageTags(compose, { name, version }); - writeCompose(newCompose, { dir, composeFileName }); - - return nextVersion; -} diff --git a/src/utils/versions/increaseFromLocalVersion.ts b/src/utils/versions/increaseFromLocalVersion.ts deleted file mode 100644 index 5ee1aae6..00000000 --- a/src/utils/versions/increaseFromLocalVersion.ts +++ /dev/null @@ -1,38 +0,0 @@ -import semver from "semver"; -import { readManifest, writeManifest } from "../manifest"; -import { readCompose, writeCompose, updateComposeImageTags } from "../compose"; -import { checkSemverType } from "../checkSemverType"; -import { ReleaseType } from "../../types"; - -export async function increaseFromLocalVersion({ - type, - dir, - compose_file_name -}: { - type: ReleaseType; - dir: string; - compose_file_name: string; -}): Promise { - const composeFileName = compose_file_name; - // Check variables - checkSemverType(type); - - // Load manifest - const { manifest, format } = readManifest({ dir }); - - const currentVersion = manifest.version; - - // Increase the version - const nextVersion = semver.inc(currentVersion, type); - if (!nextVersion) throw Error(`Invalid increase: ${currentVersion} ${type}`); - manifest.version = nextVersion; - - // Mofidy and write the manifest and docker-compose - writeManifest(manifest, format, { dir }); - const { name, version } = manifest; - const compose = readCompose({ dir, composeFileName }); - const newCompose = updateComposeImageTags(compose, { name, version }); - writeCompose(newCompose, { dir, composeFileName }); - - return nextVersion; -}