diff --git a/src/commands/build.ts b/src/commands/build.ts index 2dbfd8d4..f26887b7 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -5,11 +5,11 @@ 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"; import { defaultComposeFileName, defaultDir } from "../params"; +import { readManifest } from "../utils/manifest"; interface CliCommandOptions extends CliGlobalOptions { provider: string; @@ -91,7 +91,7 @@ export async function buildHandler({ const skipSave = skip_save; const skipUpload = skip_save || skip_upload; const composeFileName = compose_file_name; - const nextVersion = getCurrentLocalVersion({ dir }); + const nextVersion = readManifest({ dir }).manifest.version; const buildDir = path.join(dir, `build_${nextVersion}`); const buildTasks = new Listr( diff --git a/src/commands/increase.ts b/src/commands/increase.ts index 80b50155..3fe7cb79 100644 --- a/src/commands/increase.ts +++ b/src/commands/increase.ts @@ -1,7 +1,13 @@ import { CommandModule } from "yargs"; -import { increaseFromLocalVersion } from "../utils/versions/increaseFromLocalVersion"; -import { CliGlobalOptions, ReleaseType } from "../types"; +import semver, { ReleaseType } from "semver"; +import { CliGlobalOptions } from "../types"; import { defaultComposeFileName, defaultDir } from "../params"; +import { readManifest, writeManifest } from "../utils/manifest"; +import { + readCompose, + updateComposeImageTags, + writeCompose +} from "../utils/compose"; export const command = "increase [type]"; @@ -38,9 +44,24 @@ export async function increaseHandler({ dir = defaultDir, compose_file_name = defaultComposeFileName }: CliCommandOptions): Promise { - return await increaseFromLocalVersion({ - type: type as ReleaseType, - dir, - compose_file_name - }); + const composeFileName = compose_file_name; + + // Load manifest + const { manifest, format } = readManifest({ dir }); + + const currentVersion = manifest.version; + + // Increase the version + const nextVersion = semver.inc(currentVersion, type as ReleaseType); + 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; } diff --git a/src/commands/next.ts b/src/commands/next.ts index d35956d6..d017141a 100644 --- a/src/commands/next.ts +++ b/src/commands/next.ts @@ -1,8 +1,9 @@ import { CommandModule } from "yargs"; -import { getNextVersionFromApm } from "../utils/versions/getNextVersionFromApm"; -import { verifyEthConnection } from "../utils/verifyEthConnection"; +import semver from "semver"; import { CliGlobalOptions, ReleaseType } from "../types"; import { defaultDir } from "../params"; +import { getPM, verifyEthConnection } from "../providers/pm"; +import { readManifest } from "../utils/manifest"; interface CliCommandOptions extends CliGlobalOptions { type: string; @@ -11,7 +12,7 @@ interface CliCommandOptions extends CliGlobalOptions { export const next: CommandModule = { command: "next [type]", - describe: "Compute the next release version from local", + describe: "Compute the next release version from published repo", builder: yargs => yargs @@ -45,12 +46,18 @@ export async function nextHandler({ }: CliCommandOptions): Promise { const ethProvider = provider; - await verifyEthConnection(ethProvider); + const pm = getPM(ethProvider); + await verifyEthConnection(pm); + + const { manifest } = readManifest({ dir }); + const latestVersion = await pm.getLatestVersion(manifest.name); + + const nextVersion = semver.inc(latestVersion, type as ReleaseType); + if (!nextVersion) + throw Error( + `Error computing next version, is this increase type correct? type: ${type}` + ); // Execute command - return await getNextVersionFromApm({ - type: type as ReleaseType, - ethProvider, - dir - }); + return nextVersion; } diff --git a/src/commands/publish.ts b/src/commands/publish.ts index b37922d9..f61992b6 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -7,14 +7,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 { increaseFromRemoteVersion } from "../utils/increaseFromRemoteVersion"; +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 { getPM, verifyEthConnection } from "../providers/pm"; const typesList = releaseTypes.join(" | "); @@ -88,9 +87,12 @@ export const publish: CommandModule = { }), handler: async args => { - const { txData, nextVersion, releaseMultiHash } = await publishHanlder( - args - ); + const { + txData, + nextVersion, + releaseMultiHash, + txPublishLink + } = await publishHanlder(args); if (!args.silent) { const txDataToPrint = { @@ -113,7 +115,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)} `); } } @@ -143,6 +145,7 @@ export async function publishHanlder({ txData: TxData; nextVersion: string; releaseMultiHash: string; + txPublishLink: string; }> { // Parse optionsalias: "release", let ethProvider = provider || eth_provider; @@ -195,7 +198,8 @@ export async function publishHanlder({ `Invalid release type "${type}", must be: ${typesList}` ); - await verifyEthConnection(ethProvider); + const pm = getPM(ethProvider); + await verifyEthConnection(pm); const publishTasks = new Listr( [ @@ -203,19 +207,13 @@ export async function publishHanlder({ { 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; - } + const nextVersion = await increaseFromRemoteVersion({ + type: type as ReleaseType, + pm, + dir, + composeFileName + }); + ctx.nextVersion = nextVersion; ctx.buildDir = path.join(dir, `build_${nextVersion}`); task.title = task.title + ` (next version: ${nextVersion})`; @@ -276,6 +274,11 @@ export async function publishHanlder({ ); const tasksFinalCtx = await publishTasks.run(); - const { txData, nextVersion, releaseMultiHash } = tasksFinalCtx; - return { txData, nextVersion, releaseMultiHash }; + const { + txData, + nextVersion, + releaseMultiHash, + txPublishLink + } = tasksFinalCtx; + return { txData, nextVersion, releaseMultiHash, txPublishLink }; } 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/apm/index.ts b/src/providers/pm/apm/index.ts new file mode 100644 index 00000000..a45b829c --- /dev/null +++ b/src/providers/pm/apm/index.ts @@ -0,0 +1,229 @@ +import { ethers } from "ethers"; +import repoAbi from "./RepoAbi.json"; +import registryAbi from "./ApmRegistryAbi.json"; +import { arrayToSemver } from "../../../utils/arrayToSemver"; +import { semverToArray } from "../../../utils/semverToArray"; +import { IPM, TxInputs, TxSummary } from "../interface"; +import { YargsError } from "../../../params"; + +function getEthereumProviderUrl(provider = "dappnode"): string { + if (provider === "dappnode") { + return "http://fullnode.dappnode:8545"; + } else if (provider === "remote") { + return "https://web3.dappnode.net"; + } else if (provider === "infura") { + // Make sure to change this common Infura token + // if it stops working or you prefer to use your own + return "https://mainnet.infura.io/v3/bb15bacfcdbe45819caede241dcf8b0d"; + } else { + return provider; + } +} + +const zeroAddress = "0x0000000000000000000000000000000000000000"; + +/** + * @param provider user selected provider. Possible values: + * - null + * - "dappnode" + * - "infura" + * - "http://localhost:8545" + * - "ws://localhost:8546" + * @return apm instance + */ +export class Apm implements IPM { + private provider: ethers.providers.JsonRpcProvider; + + constructor(readonly ethProvider: string) { + // Initialize ens and web3 instances + // Use http Ids to avoid opened websocket connection + // This application does not need subscriptions and performs very few requests per use + const providerUrl = getEthereumProviderUrl(ethProvider); + + 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() + // ==> 0xee66c4765696c922078e8670aa9e6d4f6ffcc455 + // ens.resolver('fake.dnp.dappnode.eth').addr() + // ==> Unhandled rejection Error: ENS name not found + // + // Change behaviour to return null if not found + async resolve(ensDomain: string): Promise { + try { + return await this.provider.resolveName(ensDomain); + } catch (e) { + // This error is particular for ethjs + if ((e as Error).message.includes("ENS name not defined")) return null; + else throw e; + } + } + + /** + * Get the lastest version of an APM repo contract for an ENS domain. + * + * @param ensName: "admin.dnp.dappnode.eth" + * @return latest semver version = '0.1.0' + */ + async getLatestVersion(ensName: string): Promise { + if (!ensName) + throw Error("getLatestVersion first argument ensName must be defined"); + + const repository = await this.getRepoContract(ensName); + if (!repository) { + const registry = await this.getRegistryContract(ensName); + if (registry) + throw Error( + `Error NOREPO: you must first deploy the repo of ${ensName} using the command publish` + ); + else + throw Error( + `Error: there must exist a registry for DNP name ${ensName}` + ); + } + + try { + const res = await repository.getLatest(); + return arrayToSemver(res.semanticVersion); + } catch (e) { + // Rename error for user comprehension + (e as Error).message = `Error getting latest version of ${ensName}: ${ + (e as Error).message + }`; + throw e; + } + } + + /** + * Get the APM repo contract for an ENS domain. + * ENS domain: admin.dnp.dappnode.eth + * + * @param ensName: "admin.dnp.dappnode.eth" + * @return contract instance of the Repo "admin.dnp.dappnode.eth" + */ + private async getRepoContract( + ensName: string + ): Promise { + const repoAddress = await this.resolve(ensName); + if (!repoAddress) return null; + return new ethers.Contract(repoAddress, repoAbi, this.provider); + } + + /** + * Get the APM registry contract for an ENS domain. + * It will slice the first subdomain and query the rest as: + * ENS domain: admin.dnp.dappnode.eth + * Registry domain: dnp.dappnode.eth + * + * @param ensName: "admin.dnp.dappnode.eth" + * @return contract instance of the Registry "dnp.dappnode.eth" + */ + private async getRegistryContract( + ensName: string + ): Promise { + const repoId = ensName.split(".").slice(1).join("."); + const registryAddress = await this.resolve(repoId); + if (!registryAddress) return null; + return new ethers.Contract(registryAddress, registryAbi, this.provider); + } +} + +/** Short name is the last part of an ENS name */ +function getShortName(dnpName: string): string { + return dnpName.split(".")[0]; +} + +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/providers/pm/index.ts b/src/providers/pm/index.ts new file mode 100644 index 00000000..b8cdd9ed --- /dev/null +++ b/src/providers/pm/index.ts @@ -0,0 +1,34 @@ +import { CliError } from "../../params"; +import { Apm } from "./apm"; +import { IPM } from "./interface"; +export { IPM }; + +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); +} + +/** + * Verify the eth connection outside of the eth library to ensure + * capturing HTTP errors + */ +export async function verifyEthConnection(pm: IPM): Promise { + try { + if (!(await pm.isListening())) { + throw new CliError(`Eth provider ${pm.ethProvider} is not listening`); + } + } catch (e) { + if (pm.ethProvider === "dappnode") { + throw new CliError( + `Can't connect to DAppNode, check your VPN connection` + ); + } else if (pm.ethProvider === "infura") { + throw new CliError(`Can't connect to Infura's mainnet endpoint`); + } else { + throw new CliError(`Could not reach ETH provider at ${pm.ethProvider}`); + } + } +} diff --git a/src/providers/pm/interface.ts b/src/providers/pm/interface.ts new file mode 100644 index 00000000..29a85648 --- /dev/null +++ b/src/providers/pm/interface.ts @@ -0,0 +1,30 @@ +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 { + readonly ethProvider: string; + + 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..58f9b6dd 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"; @@ -83,7 +79,7 @@ export function createGithubRelease({ task: async (ctx, task) => { // console.log(res); // Get next version from context, fir - const { nextVersion, txData } = ctx; + const { nextVersion } = ctx; if (!nextVersion) throw Error("Missing ctx.nextVersion"); const tag = `v${nextVersion}`; @@ -110,7 +106,7 @@ export function createGithubRelease({ task.output = `Creating release for tag ${tag}...`; await github.createReleaseAndUploadAssets(tag, { - body: getReleaseBody(txData), + body: getReleaseBody(ctx), // Tag as pre-release until it is actually published in APM mainnet prerelease: true, assetsDir: buildDir, @@ -134,10 +130,13 @@ export function createGithubRelease({ * Write the release body * #### TODO: Extend this to automatically write the body */ -function getReleaseBody(txData: TxData) { - const link = getPublishTxLink(txData); +function getReleaseBody({ + txData, + txPublishLink, + releaseMultiHash +}: ListrContextBuildAndPublish) { const changelog = ""; - const installLink = getInstallDnpLink(txData.releaseMultiHash); + const installLink = getInstallDnpLink(releaseMultiHash); return ` ##### Changelog @@ -147,7 +146,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](${txPublishLink})[.](${installLink})
Release details

@@ -160,7 +159,7 @@ Gas limit: ${txData.gasLimit} \`\`\` \`\`\` -${txData.releaseMultiHash} +${releaseMultiHash} \`\`\`

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/Apm.ts b/src/utils/Apm.ts deleted file mode 100644 index e80582fa..00000000 --- a/src/utils/Apm.ts +++ /dev/null @@ -1,177 +0,0 @@ -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"; - -function getEthereumProviderUrl(provider = "dappnode"): string { - if (provider === "dappnode") { - return "http://fullnode.dappnode:8545"; - } else if (provider === "remote") { - return "https://web3.dappnode.net"; - } else if (provider === "infura") { - // Make sure to change this common Infura token - // if it stops working or you prefer to use your own - return "https://mainnet.infura.io/v3/bb15bacfcdbe45819caede241dcf8b0d"; - } else { - return provider; - } -} - -/** - * @param provider user selected provider. Possible values: - * - null - * - "dappnode" - * - "infura" - * - "http://localhost:8545" - * - "ws://localhost:8546" - * @return apm instance - */ -export class Apm { - provider: ethers.providers.JsonRpcProvider; - - constructor(providerId: string) { - // Initialize ens and web3 instances - // Use http Ids to avoid opened websocket connection - // This application does not need subscriptions and performs very few requests per use - const providerUrl = getEthereumProviderUrl(providerId); - - this.provider = new ethers.providers.JsonRpcProvider(providerUrl); - } - - // Ens throws if a node is not found - // - // ens.resolver('admin.dnp.dappnode.eth').addr() - // ==> 0xee66c4765696c922078e8670aa9e6d4f6ffcc455 - // ens.resolver('fake.dnp.dappnode.eth').addr() - // ==> Unhandled rejection Error: ENS name not found - // - // Change behaviour to return null if not found - async resolve(ensDomain: string): Promise { - try { - return await this.provider.resolveName(ensDomain); - } catch (e) { - // This error is particular for ethjs - if (e.message.includes("ENS name not defined")) return null; - else throw e; - } - } - - /** - * Get the lastest version of an APM repo contract for an ENS domain. - * - * @param ensName: "admin.dnp.dappnode.eth" - * @return latest semver version = '0.1.0' - */ - async getLatestVersion(ensName: string): Promise { - if (!ensName) - throw Error("getLatestVersion first argument ensName must be defined"); - - const repository = await this.getRepoContract(ensName); - if (!repository) { - const registry = await this.getRegistryContract(ensName); - if (registry) - throw Error( - `Error NOREPO: you must first deploy the repo of ${ensName} using the command publish` - ); - else - throw Error( - `Error: there must exist a registry for DNP name ${ensName}` - ); - } - - try { - const res = await repository.getLatest(); - return arrayToSemver(res.semanticVersion); - } catch (e) { - // Rename error for user comprehension - e.message = `Error getting latest version of ${ensName}: ${e.message}`; - throw e; - } - } - - /** - * Get the APM repo contract for an ENS domain. - * ENS domain: admin.dnp.dappnode.eth - * - * @param ensName: "admin.dnp.dappnode.eth" - * @return contract instance of the Repo "admin.dnp.dappnode.eth" - */ - async getRepoContract(ensName: string): Promise { - const repoAddress = await this.resolve(ensName); - if (!repoAddress) return null; - return new ethers.Contract(repoAddress, repoAbi, this.provider); - } - - /** - * Get the APM registry contract for an ENS domain. - * It will slice the first subdomain and query the rest as: - * ENS domain: admin.dnp.dappnode.eth - * Registry domain: dnp.dappnode.eth - * - * @param ensName: "admin.dnp.dappnode.eth" - * @return contract instance of the Registry "dnp.dappnode.eth" - */ - async getRegistryContract(ensName: string): Promise { - const repoId = ensName.split(".").slice(1).join("."); - const registryAddress = await this.resolve(repoId); - if (!registryAddress) return null; - return new ethers.Contract(registryAddress, registryAbi, this.provider); - } -} - -/** - * 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 - ]); -} - -/** - * 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 - ]); -} 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/versions/increaseFromApmVersion.ts b/src/utils/increaseFromRemoteVersion.ts similarity index 57% rename from src/utils/versions/increaseFromApmVersion.ts rename to src/utils/increaseFromRemoteVersion.ts index d2decd1d..ec92bf53 100644 --- a/src/utils/versions/increaseFromApmVersion.ts +++ b/src/utils/increaseFromRemoteVersion.ts @@ -1,25 +1,31 @@ -import { readManifest, writeManifest } from "../manifest"; -import { readCompose, writeCompose, updateComposeImageTags } from "../compose"; -import { getNextVersionFromApm } from "./getNextVersionFromApm"; -import { ReleaseType } from "../../types"; +import semver from "semver"; +import { readManifest, writeManifest } from "./manifest"; +import { readCompose, writeCompose, updateComposeImageTags } from "./compose"; +import { ReleaseType } from "../types"; +import { IPM } from "../providers/pm"; -export async function increaseFromApmVersion({ +export async function increaseFromRemoteVersion({ type, - ethProvider, + pm, dir, composeFileName }: { type: ReleaseType; - ethProvider: string; + pm: IPM; dir: string; composeFileName: string; }): Promise { - // Check variables - const nextVersion = await getNextVersionFromApm({ type, ethProvider, dir }); - // Load manifest const { manifest, format } = readManifest({ dir }); + const curretVersion = await pm.getLatestVersion(manifest.name); + + const nextVersion = semver.inc(curretVersion, type); + if (!nextVersion) + throw Error( + `Error computing next version, is this increase type correct? type: ${type}` + ); + // Increase the version manifest.version = nextVersion; 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 deleted file mode 100644 index f98a85fe..00000000 --- a/src/utils/verifyEthConnection.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Apm } from "./Apm"; -import { CliError } from "../params"; - -/** - * Verify the eth connection outside of the eth library to ensure - * capturing HTTP errors - * @param ethProvider - */ -export async function verifyEthConnection(ethProvider: string): Promise { - if (!ethProvider) throw Error("No ethProvider provided"); - - const apm = new Apm(ethProvider); - try { - const isListening = await apm.provider.send("net_listening", []); - if (isListening === false) { - throw new CliError(`Eth provider ${ethProvider} is not listening`); - } - } catch (e) { - if (ethProvider === "dappnode") { - throw new CliError( - `Can't connect to DAppNode, check your VPN connection` - ); - } else if (ethProvider === "infura") { - throw new CliError(`Can't connect to Infura's mainnet endpoint`); - } else { - throw new CliError(`Could not reach ETH provider at ${ethProvider}`); - } - } -} 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/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; -} diff --git a/test/utils/versions/increaseFromLocalVersion.test.ts b/test/commands/increase.test.ts similarity index 82% rename from test/utils/versions/increaseFromLocalVersion.test.ts rename to test/commands/increase.test.ts index 0791a9a7..b542beb4 100644 --- a/test/utils/versions/increaseFromLocalVersion.test.ts +++ b/test/commands/increase.test.ts @@ -1,12 +1,12 @@ import { expect } from "chai"; -import { increaseFromLocalVersion } from "../../../src/utils/versions/increaseFromLocalVersion"; -import { readCompose, writeCompose } from "../../../src/utils/compose"; -import { readManifest, writeManifest } from "../../../src/utils/manifest"; -import { cleanTestDir, generateCompose, testDir } from "../../testUtils"; +import { increaseHandler } from "../../src/commands/increase"; +import { readCompose, writeCompose } from "../../src/utils/compose"; +import { readManifest, writeManifest } from "../../src/utils/manifest"; +import { cleanTestDir, generateCompose, testDir } from "../testUtils"; import { defaultComposeFileName, defaultManifestFormat -} from "../../../src/params"; +} from "../../src/params"; // This test will create the following fake files // ./dappnode_package.json => fake manifest @@ -40,7 +40,7 @@ describe("increaseFromLocalVersion", function () { writeManifest(manifest, defaultManifestFormat, { dir: testDir }); writeCompose(generateCompose(manifest), { dir: testDir }); - const nextVersion = await increaseFromLocalVersion({ + const nextVersion = await increaseHandler({ type: "patch", compose_file_name: defaultComposeFileName, dir: testDir diff --git a/test/utils/versions/increaseFromApmVersion.test.ts b/test/utils/increaseFromApmVersion.test.ts similarity index 74% rename from test/utils/versions/increaseFromApmVersion.test.ts rename to test/utils/increaseFromApmVersion.test.ts index 0942b942..8d862630 100644 --- a/test/utils/versions/increaseFromApmVersion.test.ts +++ b/test/utils/increaseFromApmVersion.test.ts @@ -1,14 +1,15 @@ import { expect } from "chai"; import semver from "semver"; -import { increaseFromApmVersion } from "../../../src/utils/versions/increaseFromApmVersion"; -import { Manifest } from "../../../src/types"; -import { cleanTestDir, generateCompose, testDir } from "../../testUtils"; -import { readManifest, writeManifest } from "../../../src/utils/manifest"; -import { readCompose, writeCompose } from "../../../src/utils/compose"; +import { increaseFromRemoteVersion } from "../../src/utils/increaseFromRemoteVersion"; +import { Manifest } from "../../src/types"; +import { cleanTestDir, generateCompose, testDir } from "../testUtils"; +import { readManifest, writeManifest } from "../../src/utils/manifest"; +import { readCompose, writeCompose } from "../../src/utils/compose"; import { defaultComposeFileName, defaultManifestFormat -} from "../../../src/params"; +} from "../../src/params"; +import { getPM } from "../../src/providers/pm"; // This test will create the following fake files // ./dappnode_package.json => fake manifest @@ -18,7 +19,7 @@ import { // - modify the existing manifest and increase its version // - generate a docker compose with the next version -describe("increaseFromApmVersion", function () { +describe("increaseFromRemoteVersion", function () { this.timeout(60 * 1000); const dnpName = "admin.dnp.dappnode.eth"; @@ -35,9 +36,9 @@ describe("increaseFromApmVersion", function () { writeManifest(manifest, defaultManifestFormat, { dir: testDir }); writeCompose(generateCompose(manifest), { dir: testDir }); - const nextVersion = await increaseFromApmVersion({ + const nextVersion = await increaseFromRemoteVersion({ type: "patch", - ethProvider: "infura", + pm: getPM("infura"), composeFileName: defaultComposeFileName, dir: testDir }); diff --git a/test/utils/versions/getNextVersionFromApm.test.ts b/test/utils/versions/getNextVersionFromApm.test.ts deleted file mode 100644 index c828f691..00000000 --- a/test/utils/versions/getNextVersionFromApm.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect } from "chai"; -import semver from "semver"; -import { getNextVersionFromApm } from "../../../src/utils/versions/getNextVersionFromApm"; -import { writeManifest } from "../../../src/utils/manifest"; -import { cleanTestDir, testDir } from "../../testUtils"; -import { defaultManifestFormat } from "../../../src/params"; - -// This test will create the following fake files -// ./dappnode_package.json => fake manifest -// -// Then it will expect the function to fetch the latest version from APM and log it - -describe("getNextVersionFromApm", function () { - this.timeout(60 * 1000); - - const manifest = { - name: "admin.dnp.dappnode.eth", - version: "0.1.0" - }; - - before("Clean testDir", () => cleanTestDir()); - after("Clean testDir", () => cleanTestDir()); - - it("Should get the last version from APM", async () => { - writeManifest(manifest, defaultManifestFormat, { dir: testDir }); - - const nextVersion = await getNextVersionFromApm({ - type: "patch", - ethProvider: "infura", - dir: testDir - }); - // Check that the console output contains a valid semver version - expect(semver.valid(nextVersion)).to.be.ok; - }); -});