Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,21 @@ import {
} from "./settings";
// Imgs
import metamaskIcon from "./img/metamask-white.png";
import { RequestStatus, RepoAddresses, Manifest, FormField } from "types";
import { IPFS_GATEWAY, SDK_INSTALL_URL } from "params";
import {
RequestStatus,
RepoAddresses,
Manifest,
FormField,
NetworkId,
} from "types";
import {
dappnodeKnownDpmRegistries,
IPFS_GATEWAY,
SDK_INSTALL_URL,
} from "params";
import { signRelease } from "utils/signRelease";
import { fetchReleaseSignature } from "utils/fetchRelease";
import { publishXDaiTx } from "utils/dpm/publishXDAITx";

const fetchManifestMem = memoizee(fetchManifest, { promise: true });
const resolveDnpNameMem = memoizee(resolveDnpName, { promise: true });
Expand Down Expand Up @@ -82,6 +93,12 @@ export function App() {
useEffect(() => {
const urlParams = parseUrlQuery(window.location.search);
console.log("URL params", urlParams);
// r: **repo** human readable ENS dnpName: 'geth.dnp.dappnode.eth'
// v: **version** semver version: '0.5.6'
// d: **developer address** for new Repo only, address that will control the package: '0xf35960302a07022aba880dffaec2fdd64d5bf1c1'
// h: **contentURI** hash for the package:
// - APM: '/ipfs/QmdjrkKfD8ZAA8zHBAFC9y162R52qKcikuVXDNMKMhEsUr'
// - DPM: 'ipfs://QmRAQB6YaCyidP37UdDnjFY5vQuiBrcqdyoW1CuDgwxkD'
if (urlParams.r) setDnpName(urlParams.r);
if (urlParams.v) setVersion(urlParams.v);
if (urlParams.d) setDeveloperAddress(urlParams.d);
Expand Down Expand Up @@ -186,7 +203,7 @@ export function App() {
}
}

async function publish() {
async function publishMainnet() {
try {
if (!dnpName) throw Error("Must provide a dnpName");
if (!version) throw Error("Must provide a version");
Expand All @@ -197,7 +214,7 @@ export function App() {
if (!provider) throw Error(`Must connect to Metamask first`);
const network = await provider.getNetwork();

if (network && String(network.chainId) !== "1")
if (network && String(network.chainId) !== NetworkId.Mainnet)
throw Error("Transactions must be published on Ethereum Mainnet");

const accounts = await provider.listAccounts();
Expand Down Expand Up @@ -228,6 +245,29 @@ export function App() {
}
}

async function publishXDAI() {
try {
if (!dnpName) throw Error("Must provide a dnpName");
if (!version) throw Error("Must provide a version");
if (!releaseHash) throw Error("Must provide a manifestHash");
if (!provider) throw Error(`Must connect to Metamask first`);

setPublishReqStatus({ loading: true });

const { txHash } = await publishXDaiTx(
{ dnpName, version, releaseHash, developerAddress },
provider,
// TODO: Extend with user provided input
dappnodeKnownDpmRegistries
);

setPublishReqStatus({ result: txHash });
} catch (e) {
console.error(e);
setPublishReqStatus({ error: e as Error });
}
}

/**
* Description of the input fields
*/
Expand Down Expand Up @@ -399,13 +439,22 @@ export function App() {
{/* Publish button */}
<div className="bottom-buttons">
{provider ? (
<button
className="btn btn-dappnode"
disabled={publishReqStatus.loading}
onClick={publish}
>
Publish
</button>
<>
<button
className="btn btn-dappnode"
disabled={publishReqStatus.loading}
onClick={publishMainnet}
>
Publish mainnet
</button>
<button
className="btn btn-dappnode"
disabled={publishReqStatus.loading}
onClick={publishXDAI}
>
Publish xDAI
</button>
</>
) : (
<button
className="btn btn-dappnode"
Expand Down
6 changes: 6 additions & 0 deletions src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ export const SDK_INSTALL_URL = "https://github.com/dappnode/DAppNodeSDK";

export const signatureFileName = "signature.json";
export const manifestFileName = "dappnode_package.json";

export const dappnodeKnownDpmRegistries: Record<string, string> = {
// EIP-3770: Chain-specific addresses https://eips.ethereum.org/EIPS/eip-3770
"dnp.dappnode.eth": "xdai:0x01c58A553F92A61Fd713e6006fa7D1d82044c389",
"public.dappnode.eth": "xdai:0xE8addD62feD354203d079926a8e563BC1A7FE81e",
};
36 changes: 36 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ declare global {
}
}

export enum RegistryMode {
APM = "APM",
DPM = "DPM",
}

export enum NetworkId {
Mainnet = "1",
xDAI = "100",
}

export enum ChainId {
Ethereum = "eth",
xDAI = "xdai",
}

/**
* EIP-3770: Chain-specific addresses https://eips.ethereum.org/EIPS/eip-3770
* ```
* xdai:0x01c58A553F92A61Fd713e6006fa7D1d82044c389
* ```
*/
export type EIP3770AddressStr = string;

/**
* EIP-3770: Chain-specific addresses https://eips.ethereum.org/EIPS/eip-3770
* ```
* xdai:0x01c58A553F92A61Fd713e6006fa7D1d82044c389
* ```
*/
export type EIP3770Address = {
/** `xdai` */
chainId: string;
/** `0x01c58A553F92A61Fd713e6006fa7D1d82044c389` */
address: string;
};

export interface RequestStatus<R = unknown> {
result?: R;
loading?: boolean;
Expand Down
83 changes: 83 additions & 0 deletions src/utils/dpm/checkPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ethers } from "ethers";
import { registryAbi } from "./registryAbi";
import { repoAbi } from "./repoAbi";

export type DmpStatus =
| {
repoExists: true;
repoAddress: string;
}
| {
repoExists: false;
registryAddress: string;
};

/**
* - Check registry exists
* - Check registry name matches
* - Check if repo is published
* - Check if address has permissions for Repo
* - Check if address has permissions for Registry
*/
export async function checkDpmPermissions(
registryName: string,
registryAddress: string,
repoName: string,
userAddress: string,
provider: ethers.providers.Provider
): Promise<DmpStatus> {
const registry = new ethers.Contract(registryAddress, registryAbi, provider);

let actualRegistryName: string;
try {
actualRegistryName = await registry.registryName();
} catch (e) {
(e as Error).message = `Registry ${registryAddress} not found: ${
(e as Error).message
}`;
throw e;
}

if (actualRegistryName !== registryName) {
throw Error(
`Registry at address ${registryAddress} has registryName ${actualRegistryName} instead of expected ${registryName}`
);
}

const registryPackage = await registry.getPackage(repoName).catch(() => null);

if (registryPackage === null) {
// Package not published

const ADD_PACKAGE_ROLE = await registry.ADD_PACKAGE_ROLE();
const hasRole = await registry.hasRole(ADD_PACKAGE_ROLE, userAddress);

if (!hasRole) {
throw Error(
`Address ${userAddress} not allowed to publish to registry ${registryName}`
);
}

return {
repoExists: false,
registryAddress,
};
} else {
// Package published

const repoAddress = registryPackage.repo;
const repo = new ethers.Contract(repoAddress, repoAbi, provider);
const CREATE_VERSION_ROLE = await repo.CREATE_VERSION_ROLE();
const hasRole = await repo.hasRole(CREATE_VERSION_ROLE, userAddress);

if (!hasRole) {
throw Error(
`Address ${userAddress} not allowed to publish to repo ${repoName}`
);
}
return {
repoExists: true,
repoAddress,
};
}
}
73 changes: 73 additions & 0 deletions src/utils/dpm/executeDpmPublishTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ethers } from "ethers";
import { registryAbi } from "./registryAbi";
import { repoAbi } from "./repoAbi";
import { DmpStatus } from "./checkPermissions";
import { parseDpmDnpName } from "./utils";
import { normalizeIpfsPath } from "utils/isIpfsHash";

export async function executeDpmPublishTx(
status: DmpStatus,
{
dnpName,
version,
manifestHash,
developerAddress,
}: {
dnpName: string;
version: string;
manifestHash: string;
developerAddress?: string;
},
provider: ethers.providers.Web3Provider
) {
if (!dnpName) throw Error("dnpName must be defined");
if (!version) throw Error("version must be defined");
if (!manifestHash) throw Error("manifestHash must be defined");

const signer = provider.getSigner();

// Compute tx data
// contentURI in ENSIP-7 format
const ipfsHash = normalizeIpfsPath(manifestHash);
const contentURI = "ipfs://" + ipfsHash;

let unsignedTx: ethers.PopulatedTransaction;
// If repository exists, push new version to it
if (status.repoExists) {
const repo = new ethers.Contract(status.repoAddress, repoAbi, signer);
unsignedTx = await repo.populateTransaction.newVersion(
version, // string _version
[contentURI] // string[] _contentURIs
);
}

// If repo does not exist, create a new repo and push version
else {
// A developer address can be provided by the option developerAddress.
// If it is not provided a prompt will ask for it

const { repoName } = parseDpmDnpName(dnpName);

if (!developerAddress)
throw Error("developerAddress must be defined for new repos");

// TODO: Ask user for flags if repo allows customizing those
const defaultFlags = 0b000;

const registry = new ethers.Contract(
status.registryAddress,
registryAbi,
signer
);
unsignedTx = await registry.populateTransaction.newPackageWithVersion(
repoName, // string _name
developerAddress, // address _dev
defaultFlags, // uint8 flags
version, // string _version
[contentURI] // string[] _contentURIs
);
}

const txResponse = await signer.sendTransaction(unsignedTx);
return txResponse.hash;
}
67 changes: 67 additions & 0 deletions src/utils/dpm/publishXDAITx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ethers } from "ethers";
import { ChainId, NetworkId } from "types";
import { checkDpmPermissions } from "./checkPermissions";
import { executeDpmPublishTx } from "./executeDpmPublishTx";
import { parseDpmDnpName, parseEIP3770Address } from "./utils";

export async function publishXDaiTx(
{
dnpName,
version,
releaseHash,
developerAddress,
}: {
dnpName: string;
version: string;
releaseHash: string;
developerAddress?: string;
},
provider: ethers.providers.Web3Provider,
dpmRegistries: Record<string, string>
): Promise<{ txHash: string }> {
// Resolve registry and its network from the name
const { repoName, registryName } = parseDpmDnpName(dnpName);

const registryEIP3770Address = dpmRegistries[registryName];
if (!registryEIP3770Address) {
throw Error(`Unknown registry ${registryName}`);
}

const { chainId, address: registryAddress } = parseEIP3770Address(
registryEIP3770Address
);

const network = await provider.getNetwork();

if (String(network.chainId) !== NetworkId.xDAI)
throw Error(
`Must connect to xDAI network, current networkID: ${network.chainId}`
);

if (chainId !== ChainId.xDAI) {
throw Error(`Registry chainID ${chainId} not supported`);
}

const accounts = await provider.listAccounts();
const userAddress = accounts[0];

if (!userAddress) {
throw Error("provider.listAccounts returned an empty list");
}

const dmpStatus = await checkDpmPermissions(
registryName,
registryAddress,
repoName,
userAddress,
provider
);

const txHash = await executeDpmPublishTx(
dmpStatus,
{ dnpName, version, manifestHash: releaseHash, developerAddress },
provider
);

return { txHash };
}
Loading