diff --git a/src/tasks/generatePublishTx.ts b/src/tasks/generatePublishTx.ts index f7e9d7ec..1b0abf78 100644 --- a/src/tasks/generatePublishTx.ts +++ b/src/tasks/generatePublishTx.ts @@ -1,19 +1,14 @@ import Listr from "listr"; import { ethers } from "ethers"; -import { - - - getEthereumUrl -} from "../utils/getEthereumUrl.js"; import { getPublishTxLink } from "../utils/getLinks.js"; import { addReleaseTx } from "../utils/releaseRecord.js"; import { defaultDir, YargsError } from "../params.js"; import { CliGlobalOptions, ListrContextBuildAndPublish } from "../types.js"; import { readManifest } from "../files/index.js"; -import { ApmRepository } from "@dappnode/toolkit"; import registryAbi from "../contracts/ApmRegistryAbi.json" assert { type: "json" }; import { semverToArray } from "../utils/semverToArray.js"; import repoAbi from "../contracts/RepoAbi.json" assert { type: "json" }; +import { getEthereumUrl } from "../utils/getEthereumUrl.js"; const isZeroAddress = (address: string): boolean => parseInt(address) === 0; @@ -40,9 +35,8 @@ export function generatePublishTx({ developerAddress?: string; ethProvider: string; } & CliGlobalOptions): Listr { - // Init APM instance - const ethereumUrl = getEthereumUrl(ethProvider); - const apm = new ApmRepository(ethereumUrl); + + const provider = new ethers.JsonRpcProvider(getEthereumUrl(ethProvider)) // Load manifest ##### Verify manifest object const { manifest } = readManifest({ dir }); @@ -60,10 +54,11 @@ export function generatePublishTx({ { title: "Generate transaction", task: async ctx => { - try { - const repository = await apm.getRepoContract(ensName); + isValidENSName(ensName); + const repository = await provider.resolveName(ensName); + if (repository) { ctx.txData = { - to: await repository.getAddress(), + to: repository, value: 0, data: encodeNewVersionCall({ version: currentVersion, @@ -75,24 +70,22 @@ export function generatePublishTx({ currentVersion, releaseMultiHash }; - } catch (e) { - if (e.message.includes("Could not resolve name")) { - try { - const registryAddress = await new ethers.JsonRpcProvider( - ethereumUrl - ).resolveName(ensName.split(".").slice(1).join(".")); - if (!registryAddress) - throw new Error("Registry not found for " + 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.isAddress(developerAddress) || - isZeroAddress(developerAddress) - ) { - throw new YargsError( - `A new Aragon Package Manager Repo for ${ensName} must be created. + } else { + const registryAddress = await provider.resolveName(ensName.split(".").slice(1).join(".")); + if (!registryAddress) + 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.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: @@ -103,31 +96,24 @@ with command option: dappnodesdk publish [type] --developer_address 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B ` - ); - } - - ctx.txData = { - to: registryAddress, - value: 0, - data: encodeNewRepoWithVersionCall({ - name: shortName, - developerAddress, - version: currentVersion, - contractAddress, - contentURI - }), - gasLimit: 1100000, - ensName, - currentVersion, - releaseMultiHash, - developerAddress - }; - } catch (e) { - throw Error( - `There must exist a registry for DNP name ${ensName}` - ); - } - } else throw e; + ); + } + ctx.txData = { + to: registryAddress, + value: 0, + data: encodeNewRepoWithVersionCall({ + name: shortName, + developerAddress, + version: currentVersion, + contractAddress, + contentURI + }), + gasLimit: 1100000, + ensName, + currentVersion, + releaseMultiHash, + developerAddress + }; } /** @@ -145,9 +131,6 @@ with command option: ); } -// Utils - - /** * newVersion( * uint16[3] _newSemanticVersion, @@ -155,7 +138,7 @@ with command option: * bytes _contentURI * ) */ -export function encodeNewVersionCall({ + export function encodeNewVersionCall({ version, contractAddress, contentURI @@ -203,3 +186,41 @@ export function encodeNewRepoWithVersionCall({ contentURI // bytes _contentURI ]); } +/** + *checks if the name is a valid ENS name. Returns all reasons why the name is not valid if it is not valid. + * - ENS name must be between 1 and 63 characters. + * - ENS name must contain only lowercase alphanumeric characters(a-z), hyphens(-) and dots(.). + * - Labels must not start or end with a hyphen. + * - Labels must not contain consecutive hyphens. + * - Last label must be ".eth". + + */ +export function isValidENSName(name: string): void { + const invalidMessages: string[] = []; + + // Length Check + if (name.length < 3 || name.length > 63) { + invalidMessages.push('Length must be between 3 and 63 characters.'); + } + + // Character Check + if (!/^[a-z0-9-.]+$/.test(name)) { + invalidMessages.push('Contains forbidden characters.'); + } + + // Hyphen Placement Check + if (name.startsWith("-") || name.endsWith("-") || name.includes("--")) { + invalidMessages.push('Hyphen placement is not allowed.'); + } + + const labels = name.split("."); + // Last Label Check + const tld = labels[labels.length - 1]; + if (tld.toLowerCase() !== "eth") { + invalidMessages.push('Last label must be ".eth".'); + } + + if (invalidMessages.length > 0) { + throw new Error(`Invalid ENS name: ${invalidMessages.join(' ')}`); + } +} \ No newline at end of file diff --git a/test/utils/resolve.test.ts b/test/utils/resolve.test.ts new file mode 100644 index 00000000..3fd400ec --- /dev/null +++ b/test/utils/resolve.test.ts @@ -0,0 +1,40 @@ +import semver from "semver"; +import { expect } from "chai"; +import { getEthereumUrl } from "../../src/utils/getEthereumUrl"; +import { ethers } from "ethers"; +import { ApmRepository } from "@dappnode/toolkit"; +describe("Apm constructor", function () { + this.timeout(60_000); + + const dnpName = "admin.dnp.dappnode.eth"; + const parsedProvider = new ethers.JsonRpcProvider(getEthereumUrl("infura")) + + it("Should get the contract the registry contract of a DNP name", async () => { + const registry = await parsedProvider.resolveName(dnpName.split(".").slice(1).join(".")) + if (!registry) throw Error("no registry"); + expect(registry).to.be.a("string", "Contract instance changed"); + expect((registry).toLowerCase()).to.equal( + "0x266BFdb2124A68beB6769dC887BD655f78778923".toLowerCase() + ); + }); + + it("Should get the contract the repo contract of a DNP name", async () => { + const repo = await parsedProvider.resolveName(dnpName) + if (!repo) throw Error("no repo"); + expect(repo).to.be.a("string", "Contract instance changed"); + expect(repo.toLowerCase()).to.equal( + "0xEe66C4765696C922078e8670aA9E6d4F6fFcc455".toLowerCase() + ); + }); + + it("Should get the latest of a DNP name", async () => { + const apm = new ApmRepository(getEthereumUrl("infura")) + const latestVersion = await apm.getVersionAndIpfsHash({ dnpName }) + + expect(latestVersion.version).to.be.a("string", "Contract instance changed"); + expect(Boolean(semver.valid(latestVersion.version))).to.equal( + true, + `Resulting version is not a valid semver: ${semver}` + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index deb3aa99..91ee8364 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3961,4 +3961,4 @@ yn@3.1.1: yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== \ No newline at end of file