diff --git a/docs/admin.md b/docs/admin.md
new file mode 100644
index 000000000..42627b0a8
--- /dev/null
+++ b/docs/admin.md
@@ -0,0 +1,104 @@
+## Classes
+
+
+- AdminClient
+Provides a generic client with high level methods to manage and interact an Admin plugin installed in a DAO
+- AdminClientEncoding
+Encoding module for the SDK Admin Client
+- AdminClientEstimation
+Estimation module for the SDK Admin Client
+- AdminClientMethods
+Methods module for the SDK Admin Client
+
+
+
+
+## AdminClient
+Provides a generic client with high level methods to manage and interact an Admin plugin installed in a DAO
+
+**Kind**: global class
+
+
+### AdminClient.getPluginInstallItem(addressOrEns) ⇒ \*
+Computes the parameters to be given when creating the DAO,
+so that the plugin is configured
+
+**Kind**: static method of [AdminClient
](#AdminClient)
+**Returns**: \*
- {IPluginInstallItem}
+
+| Param | Type |
+| --- | --- |
+| addressOrEns | string
|
+
+
+
+## AdminClientEncoding
+Encoding module for the SDK Admin Client
+
+**Kind**: global class
+
+
+## AdminClientEstimation
+Estimation module for the SDK Admin Client
+
+**Kind**: global class
+
+
+### adminClientEstimation.executeProposal(params) ⇒ \*
+Estimates the gas fee of executing a proposal
+
+**Kind**: instance method of [AdminClientEstimation
](#AdminClientEstimation)
+**Returns**: \*
- {Promise}
+
+| Param | Type |
+| --- | --- |
+| params | ExecuteProposalParams
|
+
+
+
+## AdminClientMethods
+Methods module for the SDK Admin Client
+
+**Kind**: global class
+
+* [AdminClientMethods](#AdminClientMethods)
+ * [.executeProposal(params)](#AdminClientMethods+executeProposal) ⇒ \*
+ * [.pinMetadata(params)](#AdminClientMethods+pinMetadata) ⇒ \*
+ * [.getProposals()](#AdminClientMethods+getProposals) ⇒ \*
+
+
+
+### adminClientMethods.executeProposal(params) ⇒ \*
+Executes the given proposal if the user has
+
+**Kind**: instance method of [AdminClientMethods
](#AdminClientMethods)
+**Returns**: \*
- {AsyncGenerator}
+
+| Param | Type |
+| --- | --- |
+| params | ExecuteProposalParams
|
+
+
+
+### adminClientMethods.pinMetadata(params) ⇒ \*
+Pins a metadata object into IPFS and retruns the generated hash
+
+**Kind**: instance method of [AdminClientMethods
](#AdminClientMethods)
+**Returns**: \*
- {Promise}
+
+| Param | Type |
+| --- | --- |
+| params | ProposalMetadata
|
+
+
+
+### adminClientMethods.getProposals() ⇒ \*
+Returns a list of proposals on the Plugin, filtered by the given criteria
+
+**Kind**: instance method of [AdminClientMethods
](#AdminClientMethods)
+**Returns**: \*
- {Promise<AdminProposalListItem[]>}
+
+| Param | Type |
+| --- | --- |
+| | IAdminProposalQueryParams
|
+
diff --git a/modules/client/examples/05-client-admin/00-installation.ts b/modules/client/examples/05-client-admin/00-installation.ts
new file mode 100644
index 000000000..9fd92532b
--- /dev/null
+++ b/modules/client/examples/05-client-admin/00-installation.ts
@@ -0,0 +1,58 @@
+/* MARKDOWN
+## Admin governance plugin client
+### Creating a DAO with an admin plugin
+*/
+import {
+ Client,
+ AdminClient,
+ Context,
+ DaoCreationSteps,
+ GasFeeEstimation,
+ ICreateParams,
+} from "@aragon/sdk-client";
+import { contextParams } from "../00-client/00-context";
+
+const context: Context = new Context(contextParams);
+const client: Client = new Client(context);
+
+const adminInstallPluginItem = AdminClient.encoding
+ .getPluginInstallItem("0x1234567890123456789012345678901234567890");
+
+const metadataUri = await client.methods.pinMetadata({
+ name: "My DAO",
+ description: "This is a description",
+ avatar: "",
+ links: [{
+ name: "Web site",
+ url: "https://...",
+ }],
+});
+
+const createParams: ICreateParams = {
+ metadataUri,
+ ensSubdomain: "my-org", // my-org.dao.eth
+ plugins: [adminInstallPluginItem],
+};
+
+// gas estimation
+const estimatedGas: GasFeeEstimation = await client.estimation.create(
+ createParams,
+);
+console.log(estimatedGas.average);
+console.log(estimatedGas.max);
+
+const steps = client.methods.create(createParams);
+for await (const step of steps) {
+ try {
+ switch (step.key) {
+ case DaoCreationSteps.CREATING:
+ console.log(step.txHash);
+ break;
+ case DaoCreationSteps.DONE:
+ console.log(step.address);
+ break;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+}
diff --git a/modules/client/examples/05-client-admin/01-context.ts b/modules/client/examples/05-client-admin/01-context.ts
new file mode 100644
index 000000000..0dda17bc0
--- /dev/null
+++ b/modules/client/examples/05-client-admin/01-context.ts
@@ -0,0 +1,16 @@
+/* MARKDOWN
+### Create an Admin context
+*/
+import { Context, ContextPlugin } from "@aragon/sdk-client";
+import { Wallet } from "@ethersproject/wallet";
+import { contextParams } from "../00-client/00-context";
+
+const context = new Context(contextParams);
+const contextPlugin: ContextPlugin = ContextPlugin.fromContext(context);
+
+// update
+contextPlugin.set({ network: 1 });
+contextPlugin.set({ signer: new Wallet("other private key") });
+contextPlugin.setFull(contextParams);
+
+console.log(contextPlugin)
diff --git a/modules/client/examples/05-client-admin/02-client.ts b/modules/client/examples/05-client-admin/02-client.ts
new file mode 100644
index 000000000..a7bf3350b
--- /dev/null
+++ b/modules/client/examples/05-client-admin/02-client.ts
@@ -0,0 +1,12 @@
+/* MARKDOWN
+### Create an Admin client
+*/
+import { AdminClient, Context, ContextPlugin } from "@aragon/sdk-client";
+import { contextParams } from "../00-client/00-context";
+
+const context = new Context(contextParams);
+const contextPlugin: ContextPlugin = ContextPlugin.fromContext(context);
+
+const client = new AdminClient(contextPlugin);
+
+console.log(client);
diff --git a/modules/client/examples/05-client-admin/03-execute-proposal.ts b/modules/client/examples/05-client-admin/03-execute-proposal.ts
new file mode 100644
index 000000000..668d01eca
--- /dev/null
+++ b/modules/client/examples/05-client-admin/03-execute-proposal.ts
@@ -0,0 +1,77 @@
+/* MARKDOWN
+### Creating and executing an admin proposal right away
+
+*/
+import {
+ Client,
+ AdminClient,
+ Context,
+ ContextPlugin,
+ ExecuteProposalParams,
+ ExecuteProposalStep,
+ IWithdrawParams,
+ ProposalCreationSteps,
+ ProposalMetadata,
+ VoteValues,
+} from "@aragon/sdk-client";
+import { contextParams } from "../00-client/00-context";
+
+// Create a simple context
+const context: Context = new Context(contextParams);
+// Create a plugin context from the simple context
+const contextPlugin: ContextPlugin = ContextPlugin.fromContext(context);
+// Create an address list client
+const AdminClient = new AdminClient(contextPlugin);
+const client = new Client(context);
+
+const metadata: ProposalMetadata = {
+ title: "Test Proposal",
+ summary: "This is a short description",
+ description: "This is a long description",
+ resources: [
+ {
+ name: "Discord",
+ url: "https://discord.com/...",
+ },
+ {
+ name: "Website",
+ url: "https://website...",
+ },
+ ],
+ media: {
+ logo: "https://...",
+ header: "https://...",
+ },
+};
+
+const ipfsUri = await AdminClient.methods.pinMetadata(metadata);
+
+const withdrawParams: IWithdrawParams = {
+ recipientAddress: "0x1234567890123456789012345678901234567890",
+ amount: BigInt(5),
+};
+const withdrawAction = await client.encoding.withdrawAction(
+ "0x0987654321098765432109876543210987654321",
+ withdrawParams,
+);
+
+const proposalParams: ExecuteProposalParams = {
+ pluginAddress: "0x1234567890123456789012345678901234567890",
+ metadataUri: ipfsUri,
+ actions: [withdrawAction],
+};
+
+const steps = AdminClient.methods.executeProposal(proposalParams);
+for await (const step of steps) {
+ try {
+ switch (step.key) {
+ case ExecuteProposalStep.EXECUTING:
+ console.log(step.txHash);
+ break;
+ case ExecuteProposalStep.DONE:
+ break;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+}
diff --git a/modules/client/examples/05-client-admin/08-get-proposal.ts b/modules/client/examples/05-client-admin/08-get-proposal.ts
new file mode 100644
index 000000000..d6b174539
--- /dev/null
+++ b/modules/client/examples/05-client-admin/08-get-proposal.ts
@@ -0,0 +1,65 @@
+/* MARKDOWN
+### Loading the a proposal by proposalId (admin plugin)
+*/
+import {
+ AdminProposal,
+ AdminClient,
+ Context,
+ ContextPlugin,
+} from "@aragon/sdk-client";
+import { contextParams } from "../00-client/00-context";
+
+// Create a simple context
+const context: Context = new Context(contextParams);
+// Create a plugin context from the simple context
+const contextPlugin: ContextPlugin = ContextPlugin.fromContext(context);
+// Create an ERC20 client
+const client = new AdminClient(contextPlugin);
+
+const proposalId = "0x12345...";
+
+const proposal: AdminProposal | null = await client.methods.getProposal(
+ proposalId,
+);
+console.log(proposal);
+/*
+{
+ id: "0x12345...",
+ dao: {
+ address: "0x1234567890123456789012345678901234567890",
+ name: "Cool DAO"
+ };
+ creatorAddress: "0x1234567890123456789012345678901234567890",
+ metadata: {
+ title: "Test Proposal",
+ summary: "test proposal summary",
+ description: "this is a long description",
+ resources: [
+ {
+ url: "https://dicord.com/...",
+ name: "Discord"
+ },
+ {
+ url: "https://docs.com/...",
+ name: "Document"
+ }
+ ],
+ media: {
+ header: "https://.../image.jpeg",
+ logo: "https://.../image.jpeg"
+ }
+ };
+ creationDate: ,
+ actions: [
+ {
+ to: "0x12345..."
+ value: 10n
+ data: [12,13,154...]
+ }
+ ],
+ status: "Executed",
+ adminAddress: "0x1234567890123456789012345678901234567890"
+ pluginAddress: "0x1234567890123456789012345678901234567890"
+ proposalId: 0n
+}
+*/
diff --git a/modules/client/examples/05-client-admin/09-get-proposals.ts b/modules/client/examples/05-client-admin/09-get-proposals.ts
new file mode 100644
index 000000000..89876a530
--- /dev/null
+++ b/modules/client/examples/05-client-admin/09-get-proposals.ts
@@ -0,0 +1,67 @@
+/* MARKDOWN
+### Loading the list of proposals (address list plugin)
+*/
+import {
+ AdminProposalListItem,
+ AdminClient,
+ Context,
+ ContextPlugin,
+ IProposalQueryParams,
+ ProposalSortBy,
+ ProposalStatus,
+ SortDirection,
+} from "@aragon/sdk-client";
+import { contextParams } from "../00-client/00-context";
+
+// Create a simple context
+const context: Context = new Context(contextParams);
+// Create a plugin context from the simple context
+const contextPlugin: ContextPlugin = ContextPlugin.fromContext(context);
+// Create an address list client
+const client = new AdminClient(contextPlugin);
+
+const queryParams: IProposalQueryParams = {
+ skip: 0, // optional
+ limit: 10, // optional,
+ direction: SortDirection.ASC, // optional
+ sortBy: ProposalSortBy.POPULARITY, //optional
+ status: ProposalStatus.ACTIVE, // optional
+};
+
+const proposals: AdminProposalListItem[] = await client.methods
+ .getProposals(queryParams);
+console.log(proposals);
+/*
+[
+ {
+ id: "0x12345...",
+ dao: {
+ address: "0x1234567890123456789012345678901234567890",
+ name: "Cool DAO"
+ };
+ creatorAddress: "0x1234567890123456789012345678901234567890",
+ metadata: {
+ title: "Test Proposal",
+ summary: "test proposal summary"
+ };
+ creationDate: ,
+ adminAddress: "0x1234567890123456789012345678901234567890",
+ status: "Executed"
+ },
+ {
+ id: "0x12345...",
+ dao: {
+ address: "0x1234567890123456789012345678901234567890",
+ name: "Cool DAO"
+ };
+ creatorAddress: "0x1234567890123456789012345678901234567890",
+ metadata: {
+ title: "Test Proposal 2",
+ summary: "test proposal summary 2"
+ };
+ creationDate: ,
+ adminAddress: "0x1234567890123456789012345678901234567890",
+ status: "Executed"
+ }
+]
+*/
diff --git a/modules/client/package.json b/modules/client/package.json
index ba3dd3f30..0cff1b7b2 100644
--- a/modules/client/package.json
+++ b/modules/client/package.json
@@ -60,7 +60,7 @@
},
"dependencies": {
"@aragon/core-contracts-ethers": "^0.6.0-alpha",
- "@aragon/sdk-common": "^0.9.2-alpha",
+ "@aragon/sdk-common": "^0.9.3-alpha",
"@aragon/sdk-ipfs": "^0.2.0-alpha",
"@ethersproject/abstract-signer": "^5.5.0",
"@ethersproject/bignumber": "^5.6.0",
diff --git a/modules/client/src/admin/client.ts b/modules/client/src/admin/client.ts
new file mode 100644
index 000000000..ef98d87a9
--- /dev/null
+++ b/modules/client/src/admin/client.ts
@@ -0,0 +1,39 @@
+import { ClientCore, ContextPlugin, IPluginInstallItem } from "../client-common";
+import {
+ IAdminClient,
+ IAdminClientEncoding,
+ IAdminClientEstimation,
+ IAdminClientMethods,
+} from "./interfaces";
+import { AdminClientEncoding } from "./internal/client/encoding";
+import { AdminClientEstimation } from "./internal/client/estimation";
+import { AdminClientMethods } from "./internal/client/methods";
+
+/**
+ * Provides a generic client with high level methods to manage and interact an Admin plugin installed in a DAO
+ */
+export class AdminClient extends ClientCore implements IAdminClient {
+ public methods: IAdminClientMethods;
+ public encoding: IAdminClientEncoding;
+ public estimation: IAdminClientEstimation;
+
+ constructor(context: ContextPlugin) {
+ super(context);
+ this.methods = new AdminClientMethods(context);
+ this.encoding = new AdminClientEncoding(context);
+ this.estimation = new AdminClientEstimation(context);
+ }
+static encoding = {
+
+ /**
+ * Computes the parameters to be given when creating the DAO,
+ * so that the plugin is configured
+ *
+ * @param {string} addressOrEns
+ * @return {*} {IPluginInstallItem}
+ * @memberof AdminClient
+ */
+ getPluginInstallItem: (addressOrEns: string): IPluginInstallItem =>
+ AdminClientEncoding.getPluginInstallItem(addressOrEns),
+ };
+}
diff --git a/modules/client/src/admin/index.ts b/modules/client/src/admin/index.ts
new file mode 100644
index 000000000..1ff37104f
--- /dev/null
+++ b/modules/client/src/admin/index.ts
@@ -0,0 +1,2 @@
+export * from './interfaces'
+export * from './client'
\ No newline at end of file
diff --git a/modules/client/src/admin/interfaces.ts b/modules/client/src/admin/interfaces.ts
new file mode 100644
index 000000000..ac33169d3
--- /dev/null
+++ b/modules/client/src/admin/interfaces.ts
@@ -0,0 +1,94 @@
+import {
+ DaoAction,
+ ExecuteProposalStepValue,
+ GasFeeEstimation,
+ IClientCore,
+ IPagination,
+ ProposalMetadata,
+ ProposalSortBy,
+ ProposalStatus,
+ SubgraphAction,
+} from "../client-common";
+
+export interface IAdminClientMethods extends IClientCore {
+ executeProposal: (
+ params: ExecuteAdminProposalParams,
+ ) => AsyncGenerator;
+ pinMetadata: (
+ params: ProposalMetadata,
+ ) => Promise;
+ getProposal: (proposalId: string) => Promise;
+ getProposals: (
+ params: IAdminProposalQueryParams,
+ ) => Promise;
+}
+
+export interface IAdminClientEncoding extends IClientCore {}
+
+export interface IAdminClientEstimation extends IClientCore {
+ executeProposal: (parms: ExecuteAdminProposalParams) => Promise;
+}
+
+export interface IAdminClient {
+ methods: IAdminClientMethods;
+ encoding: IAdminClientEncoding;
+ estimation: IAdminClientEstimation;
+}
+
+export type ExecuteAdminProposalParams = {
+ pluginAddress: string;
+ metadataUri: string;
+ actions: DaoAction[];
+};
+
+type ProposalBase = {
+ id: string;
+ dao: {
+ address: string;
+ name: string;
+ };
+ creatorAddress: string;
+ metadata: ProposalMetadata;
+ creationDate: Date;
+ adminAddress: string;
+ status: ProposalStatus;
+};
+export type AdminProposal = ProposalBase & {
+ actions: DaoAction[];
+ pluginAddress: string;
+ proposalId: bigint;
+};
+export type AdminProposalListItem = ProposalBase;
+
+export interface IAdminProposalQueryParams extends IPagination {
+ sortBy?: ProposalSortBy;
+ status?: ProposalStatus;
+ adminAddressOrEns?: string;
+}
+
+type SubgraphAdminProposalBase = {
+ id: string;
+ dao: {
+ id: string;
+ name: string;
+ };
+ creator: string;
+ metadata: string;
+ createdAt: string;
+ executed: boolean;
+ // TODO
+ // fix typo
+ adminstrator: {
+ address: string;
+ };
+};
+
+export type SubgraphAdminProposalListItem = SubgraphAdminProposalBase;
+
+export type SubgraphAdminProposal = SubgraphAdminProposalBase & {
+ actions: SubgraphAction[];
+ plugin: {
+ id: string;
+ };
+ proposalId: string;
+};
diff --git a/modules/client/src/admin/internal/client/encoding.ts b/modules/client/src/admin/internal/client/encoding.ts
new file mode 100644
index 000000000..cc7f26bd3
--- /dev/null
+++ b/modules/client/src/admin/internal/client/encoding.ts
@@ -0,0 +1,49 @@
+import {
+ ClientCore,
+ ContextPlugin,
+ IPluginInstallItem,
+} from "../../../client-common";
+import { IAdminClientEncoding } from "../../interfaces";
+import { ADMIN_PLUGIN_ID } from "../constants";
+import { defaultAbiCoder } from "@ethersproject/abi";
+import { toUtf8Bytes } from "@ethersproject/strings";
+import { isAddress } from "@ethersproject/address";
+import { InvalidAddressOrEnsError } from "@aragon/sdk-common";
+
+/**
+ * Encoding module for the SDK Admin Client
+ */
+export class AdminClientEncoding extends ClientCore
+ implements IAdminClientEncoding {
+ constructor(context: ContextPlugin) {
+ super(context);
+ }
+
+ /**
+ * Computes the parameters to be given when creating the DAO,
+ * so that the plugin is configured
+ *
+ * @param {string} addressOrEns
+ * @return {*} {IPluginInstallItem}
+ * @memberof AdminClientEncoding
+ */
+ static getPluginInstallItem(
+ addressOrEns: string,
+ ): IPluginInstallItem {
+ if (!isAddress(addressOrEns)) {
+ throw new InvalidAddressOrEnsError();
+ }
+ const hexBytes = defaultAbiCoder.encode(
+ [
+ "address",
+ ],
+ [
+ addressOrEns,
+ ],
+ );
+ return {
+ id: ADMIN_PLUGIN_ID,
+ data: toUtf8Bytes(hexBytes),
+ };
+ }
+}
diff --git a/modules/client/src/admin/internal/client/estimation.ts b/modules/client/src/admin/internal/client/estimation.ts
new file mode 100644
index 000000000..8ac582182
--- /dev/null
+++ b/modules/client/src/admin/internal/client/estimation.ts
@@ -0,0 +1,48 @@
+import {
+ ExecuteAdminProposalParams,
+ IAdminClientEstimation,
+} from "../../interfaces";
+import {
+ ClientCore,
+ ContextPlugin,
+ GasFeeEstimation,
+} from "../../../client-common";
+import { NoProviderError, NoSignerError } from "@aragon/sdk-common";
+import { Admin__factory } from "@aragon/core-contracts-ethers";
+import { toUtf8Bytes } from "@ethersproject/strings"
+
+/**
+ * Estimation module for the SDK Admin Client
+ */
+export class AdminClientEstimation extends ClientCore
+ implements IAdminClientEstimation {
+ constructor(context: ContextPlugin) {
+ super(context);
+ }
+ /**
+ * Estimates the gas fee of executing a proposal
+ *
+ * @param {ExecuteAdminProposalParams} params
+ * @return {*} {Promise}
+ * @memberof AdminClientEstimation
+ */
+ public async executeProposal(
+ params: ExecuteAdminProposalParams,
+ ): Promise {
+ const signer = this.web3.getConnectedSigner();
+ if (!signer) {
+ throw new NoSignerError();
+ } else if (!signer.provider) {
+ throw new NoProviderError();
+ }
+ const adminContract = Admin__factory.connect(
+ params.pluginAddress,
+ signer,
+ );
+ const estimatedGasFee = await adminContract.estimateGas.executeProposal(
+ toUtf8Bytes(params.metadataUri),
+ params.actions,
+ );
+ return this.web3.getApproximateGasFee(estimatedGasFee.toBigInt());
+ }
+}
diff --git a/modules/client/src/admin/internal/client/methods.ts b/modules/client/src/admin/internal/client/methods.ts
new file mode 100644
index 000000000..85be7ca6f
--- /dev/null
+++ b/modules/client/src/admin/internal/client/methods.ts
@@ -0,0 +1,240 @@
+import {
+ GraphQLError,
+ InvalidAddressOrEnsError,
+ InvalidCidError,
+ InvalidProposalIdError,
+ IpfsPinError,
+ NoProviderError,
+ NoSignerError,
+ resolveIpfsCid,
+} from "@aragon/sdk-common";
+import {
+ ClientCore,
+ ContextPlugin,
+ ExecuteProposalStep,
+ ExecuteProposalStepValue,
+ ProposalMetadata,
+ ProposalSortBy,
+ SortDirection,
+} from "../../../client-common";
+import {
+ UNAVAILABLE_PROPOSAL_METADATA,
+ UNSUPPORTED_PROPOSAL_METADATA_LINK,
+} from "../../../client-common/constants";
+import {
+ AdminProposal,
+ AdminProposalListItem,
+ ExecuteAdminProposalParams,
+ IAdminClientMethods,
+ IAdminProposalQueryParams,
+ SubgraphAdminProposal,
+ SubgraphAdminProposalListItem,
+} from "../../interfaces";
+import {
+ QueryAdminProposal,
+ QueryAdminProposals,
+} from "../graphql-queries/proposal";
+import {
+ computeProposalStatusFilter,
+ toAdminProposal,
+ toAdminProposalListItem,
+} from "../utils";
+import { isAddress } from "@ethersproject/address";
+import { Admin__factory } from "@aragon/core-contracts-ethers";
+import { toUtf8Bytes } from "@ethersproject/strings";
+
+/**
+ * Methods module for the SDK Admin Client
+ */
+export class AdminClientMethods extends ClientCore
+ implements IAdminClientMethods {
+ constructor(context: ContextPlugin) {
+ super(context);
+ }
+ /**
+ * Executes the given proposal if the user has
+ *
+ * @param {ExecuteAdminProposalParams} params
+ * @return {*} {AsyncGenerator}
+ * @memberof AdminClientMethods
+ */
+ public async *executeProposal(
+ params: ExecuteAdminProposalParams,
+ ): AsyncGenerator {
+ const signer = this.web3.getConnectedSigner();
+ if (!signer) {
+ throw new NoSignerError();
+ } else if (!signer.provider) {
+ throw new NoProviderError();
+ }
+ const adminContract = Admin__factory.connect(
+ params.pluginAddress,
+ signer,
+ );
+
+ // const actions = params.actions.map((action) => {
+ // return {
+ // to: action.to,
+ // value: action.value,
+ // data: "0x" + bytesToHex(action.data),
+ // };
+ // });
+
+ const tx = await adminContract.executeProposal(
+ toUtf8Bytes(params.metadataUri),
+ params.actions,
+ );
+ yield {
+ key: ExecuteProposalStep.EXECUTING,
+ txHash: tx.hash,
+ };
+ await tx.wait();
+ yield {
+ key: ExecuteProposalStep.DONE,
+ };
+ }
+
+ /**
+ * Pins a metadata object into IPFS and retruns the generated hash
+ *
+ * @param {ProposalMetadata} params
+ * @return {*} {Promise}
+ * @memberof AdminClientMethods
+ */
+ public async pinMetadata(
+ params: ProposalMetadata,
+ ): Promise {
+ try {
+ const cid = await this.ipfs.add(JSON.stringify(params));
+ await this.ipfs.pin(cid);
+ return `ipfs://${cid}`;
+ } catch {
+ throw new IpfsPinError();
+ }
+ }
+
+ public async getProposal(
+ proposalId: string,
+ ): Promise {
+ if (!proposalId) {
+ throw new InvalidProposalIdError();
+ }
+ try {
+ await this.graphql.ensureOnline();
+ const client = this.graphql.getClient();
+ const {
+ adminProposal,
+ }: { adminProposal: SubgraphAdminProposal } = await client.request(
+ QueryAdminProposal,
+ { proposalId },
+ );
+
+ if (!adminProposal) {
+ return null;
+ }
+
+ try {
+ const metadataCid = resolveIpfsCid(adminProposal.metadata);
+ const metadataString = await this.ipfs.fetchString(metadataCid);
+ const metadata = JSON.parse(metadataString) as ProposalMetadata;
+ return toAdminProposal(adminProposal, metadata);
+ // TODO: Parse and validate schema
+ } catch (err) {
+ if (err instanceof InvalidCidError) {
+ return toAdminProposal(
+ adminProposal,
+ UNSUPPORTED_PROPOSAL_METADATA_LINK,
+ );
+ }
+ return toAdminProposal(
+ adminProposal,
+ UNAVAILABLE_PROPOSAL_METADATA,
+ );
+ }
+ } catch (err) {
+ throw new GraphQLError("Admin proposal");
+ }
+ }
+ /**
+ * Returns a list of proposals on the Plugin, filtered by the given criteria
+ *
+ * @param {IAdminProposalQueryParams}
+ * @return {*} {Promise}
+ * @memberof AdminClientMethods
+ */
+ public async getProposals(
+ {
+ adminAddressOrEns,
+ limit = 10,
+ status,
+ skip = 0,
+ direction = SortDirection.ASC,
+ sortBy = ProposalSortBy.CREATED_AT,
+ }: IAdminProposalQueryParams,
+ ): Promise {
+ let where = {};
+ let address = adminAddressOrEns;
+ if (address) {
+ if (!isAddress(address)) {
+ await this.web3.ensureOnline();
+ const provider = this.web3.getProvider();
+ if (!provider) {
+ throw new NoProviderError();
+ }
+ const resolvedAddress = await provider.resolveName(address);
+ if (!resolvedAddress) {
+ throw new InvalidAddressOrEnsError();
+ }
+ address = resolvedAddress;
+ }
+ where = { dao: address };
+ }
+ if (status) {
+ where = { ...where, ...computeProposalStatusFilter(status) };
+ }
+ try {
+ await this.graphql.ensureOnline();
+ const client = this.graphql.getClient();
+ const {
+ adminProposals,
+ }: {
+ adminProposals: SubgraphAdminProposalListItem[];
+ } = await client.request(QueryAdminProposals, {
+ where,
+ limit,
+ skip,
+ direction,
+ sortBy,
+ });
+ await this.ipfs.ensureOnline();
+ return Promise.all(
+ adminProposals.map(
+ async (
+ proposal: SubgraphAdminProposalListItem,
+ ): Promise => {
+ // format in the metadata field
+ try {
+ const metadataCid = resolveIpfsCid(proposal.metadata);
+ const stringMetadata = await this.ipfs.fetchString(metadataCid);
+ const metadata = JSON.parse(stringMetadata) as ProposalMetadata;
+ return toAdminProposalListItem(proposal, metadata);
+ } catch (err) {
+ if (err instanceof InvalidCidError) {
+ return toAdminProposalListItem(
+ proposal,
+ UNSUPPORTED_PROPOSAL_METADATA_LINK,
+ );
+ }
+ return toAdminProposalListItem(
+ proposal,
+ UNAVAILABLE_PROPOSAL_METADATA,
+ );
+ }
+ },
+ ),
+ );
+ } catch {
+ throw new GraphQLError("Admin proposals");
+ }
+ }
+}
diff --git a/modules/client/src/admin/internal/constants.ts b/modules/client/src/admin/internal/constants.ts
new file mode 100644
index 000000000..937d3f8a2
--- /dev/null
+++ b/modules/client/src/admin/internal/constants.ts
@@ -0,0 +1 @@
+export const ADMIN_PLUGIN_ID = "0x2fcfca7b76ee366881da238507ffe01d8c206ba1"
\ No newline at end of file
diff --git a/modules/client/src/admin/internal/graphql-queries/proposal.ts b/modules/client/src/admin/internal/graphql-queries/proposal.ts
new file mode 100644
index 000000000..c7582aeec
--- /dev/null
+++ b/modules/client/src/admin/internal/graphql-queries/proposal.ts
@@ -0,0 +1,50 @@
+import { gql } from "graphql-request";
+
+// TODO
+// fix adminstrator typo once is fixed in subgraph
+
+export const QueryAdminProposal = gql`
+query adminProposal($proposalId: ID!) {
+ adminProposal(id: $proposalId) {
+ id
+ dao {
+ id
+ name
+ }
+ creator
+ metadata
+ createdAt
+ actions {
+ to
+ value
+ data
+ }
+ executed
+ plugin {
+ id
+ }
+ adminstrator {
+ address
+ }
+ proposalId
+ }
+}
+`;
+export const QueryAdminProposals = gql`
+query adminProposals($where: AdminProposal_filter!, $limit:Int!, $skip: Int!, $direction: OrderDirection!, $sortBy: AdminProposal_orderBy!) {
+ adminProposals(where: $where, first: $limit, skip: $skip, orderDirection: $direction, orderBy: $sortBy){
+ id
+ dao {
+ id
+ name
+ }
+ creator
+ metadata
+ createdAt
+ executed
+ adminstrator {
+ address
+ }
+ }
+}
+`;
diff --git a/modules/client/src/admin/internal/utils.ts b/modules/client/src/admin/internal/utils.ts
new file mode 100644
index 000000000..b2fbf9b3c
--- /dev/null
+++ b/modules/client/src/admin/internal/utils.ts
@@ -0,0 +1,86 @@
+import { hexToBytes, InvalidProposalStatus, strip0x } from "@aragon/sdk-common";
+import {
+ DaoAction,
+ ProposalMetadata,
+ ProposalStatus,
+ SubgraphAction,
+} from "../../client-common";
+import {
+ AdminProposal,
+ AdminProposalListItem,
+ SubgraphAdminProposal,
+ SubgraphAdminProposalListItem,
+} from "../interfaces";
+
+export function toAdminProposal(
+ proposal: SubgraphAdminProposal,
+ metadata: ProposalMetadata,
+): AdminProposal {
+ const creationDate = new Date(
+ parseInt(proposal.createdAt) * 1000,
+ );
+ return {
+ id: proposal.id,
+ dao: {
+ address: proposal.dao.id,
+ name: proposal.dao.name,
+ },
+ creatorAddress: proposal.creator,
+ metadata,
+ creationDate,
+ adminAddress: proposal.adminstrator.address,
+ status: proposal.executed
+ ? ProposalStatus.EXECUTED
+ : ProposalStatus.SUCCEEDED,
+ actions: proposal.actions.map(
+ (action: SubgraphAction): DaoAction => {
+ return {
+ data: hexToBytes(strip0x(action.data)),
+ to: action.to,
+ value: BigInt(action.value),
+ };
+ },
+ ),
+ pluginAddress: proposal.plugin.id,
+ proposalId: BigInt(proposal.proposalId),
+ };
+}
+export function toAdminProposalListItem(
+ proposal: SubgraphAdminProposalListItem,
+ metadata: ProposalMetadata,
+): AdminProposalListItem {
+ const creationDate = new Date(
+ parseInt(proposal.createdAt) * 1000,
+ );
+ return {
+ id: proposal.id,
+ dao: {
+ address: proposal.dao.id,
+ name: proposal.dao.name,
+ },
+ creatorAddress: proposal.creator,
+ metadata,
+ creationDate,
+ adminAddress: proposal.adminstrator.address,
+ status: proposal.executed
+ ? ProposalStatus.EXECUTED
+ : ProposalStatus.SUCCEEDED,
+ };
+}
+
+export function computeProposalStatusFilter(
+ status: ProposalStatus,
+): Object {
+ let where = {};
+ switch (status) {
+ case ProposalStatus.EXECUTED:
+ where = { executed: true };
+ break;
+ case ProposalStatus.SUCCEEDED:
+ where = { executed: false };
+ break;
+ default:
+ throw new InvalidProposalStatus();
+ }
+ return where;
+}
diff --git a/modules/client/src/client-common/interfaces/plugin.ts b/modules/client/src/client-common/interfaces/plugin.ts
index ea6e60f9b..f0bd9cb06 100644
--- a/modules/client/src/client-common/interfaces/plugin.ts
+++ b/modules/client/src/client-common/interfaces/plugin.ts
@@ -91,7 +91,7 @@ export interface ICanVoteParams {
export type CanExecuteParams = {
proposalId: bigint;
- pluginAddress: string
+ pluginAddress: string;
};
/**
diff --git a/modules/client/src/client-common/utils.ts b/modules/client/src/client-common/utils.ts
index a95baa4ab..4e349a825 100644
--- a/modules/client/src/client-common/utils.ts
+++ b/modules/client/src/client-common/utils.ts
@@ -1,12 +1,11 @@
import { IDAO } from "@aragon/core-contracts-ethers";
-import { ContractReceipt } from "@ethersproject/contracts";
import { VoteValues, VotingMode } from "../client-common/interfaces/plugin";
import {
IComputeStatusProposal,
ICreateProposalParams,
ProposalStatus,
} from "./interfaces/plugin";
-
+import { ContractReceipt } from "@ethersproject/contracts";
import { Interface } from "@ethersproject/abi";
import { id } from "@ethersproject/hash";
import { Log } from "@ethersproject/providers";
diff --git a/modules/client/src/index.ts b/modules/client/src/index.ts
index 9c4c58583..8636bac7b 100644
--- a/modules/client/src/index.ts
+++ b/modules/client/src/index.ts
@@ -3,6 +3,7 @@ export * from "./addresslistVoting";
export * from "./tokenVoting";
export * from "./client-common";
export * from "./multisig";
+export * from "./admin";
export {
AssetBalance,
DaoCreationSteps,
diff --git a/modules/client/test/helpers/deployContracts.ts b/modules/client/test/helpers/deployContracts.ts
index 46d9d0da0..bd7ce1fde 100644
--- a/modules/client/test/helpers/deployContracts.ts
+++ b/modules/client/test/helpers/deployContracts.ts
@@ -23,6 +23,8 @@ export interface Deployment {
addresslistVotingPluginSetup: aragonContracts.AddresslistVotingSetup;
multisigRepo: aragonContracts.PluginRepo;
multisigPluginSetup: aragonContracts.AddresslistVotingSetup;
+ adminRepo: aragonContracts.PluginRepo;
+ adminPluginSetup: aragonContracts.AdminSetup;
}
export async function deploy(): Promise {
@@ -137,6 +139,7 @@ export async function deploy(): Promise {
const pluginRepo_Factory = new aragonContracts.PluginRepo__factory();
+ // token
const tokenVotingSetupFactory = new aragonContracts
.TokenVotingSetup__factory();
const tokenVotingPluginSetup = await tokenVotingSetupFactory
@@ -169,9 +172,8 @@ export async function deploy(): Promise {
.connect(deployOwnerWallet)
.attach(addresslistVotingRepoAddr);
+ // multisig
const multisigFactory = new aragonContracts
- // @ts-ignore
- // TODO update contracts-ethers
.MultisigSetup__factory();
const multisigPluginSetup = await multisigFactory
.connect(deployOwnerWallet)
@@ -186,6 +188,22 @@ export async function deploy(): Promise {
const multisigRepo = pluginRepo_Factory
.connect(deployOwnerWallet)
.attach(multisigRepoAddr);
+
+ // ADMIN
+ const adminSetupFactory = new aragonContracts.AdminSetup__factory();
+ const adminPluginSetup = await adminSetupFactory
+ .connect(deployOwnerWallet)
+ .deploy();
+ const adminRepoAddr = await deployPlugin(
+ pluginRepoFactory,
+ adminPluginSetup.address,
+ "Admin",
+ [1, 0, 0],
+ deployOwnerWallet,
+ );
+ const adminRepo = pluginRepo_Factory
+ .connect(deployOwnerWallet)
+ .attach(adminRepoAddr);
// send ETH to hardcoded wallet in tests
await deployOwnerWallet.sendTransaction({
@@ -203,6 +221,8 @@ export async function deploy(): Promise {
addresslistVotingPluginSetup,
multisigRepo,
multisigPluginSetup,
+ adminRepo,
+ adminPluginSetup,
};
} catch (e) {
throw e;
@@ -416,3 +436,34 @@ export async function createMultisigDAO(
],
);
}
+
+export async function createAdminDAO(
+ deployment: Deployment,
+ name: string,
+ admin: string,
+) {
+ return createDAO(
+ deployment.daoFactory,
+ {
+ metadata: "0x0000",
+ name,
+ trustedForwarder: AddressZero,
+ },
+ [
+ {
+ pluginSetup: deployment.adminPluginSetup.address,
+ pluginSetupRepo: deployment.adminRepo.address,
+ data: defaultAbiCoder.encode(
+ [
+ "address",
+ ],
+ [
+ admin,
+ ],
+ ),
+ },
+ ],
+ );
+}
+
+
diff --git a/modules/client/test/integration/admin/encoding.test.ts b/modules/client/test/integration/admin/encoding.test.ts
new file mode 100644
index 000000000..5084c663f
--- /dev/null
+++ b/modules/client/test/integration/admin/encoding.test.ts
@@ -0,0 +1,31 @@
+// @ts-ignore
+declare const describe, it, expect;
+
+import {
+ AdminClient,
+} from "../../../src";
+import {
+ InvalidAddressOrEnsError,
+} from "@aragon/sdk-common";
+import { TEST_INVALID_ADDRESS } from "../constants";
+describe("Client Admin", () => {
+ describe("Action generators", () => {
+ it("Should create an Admin client and generate a install entry", async () => {
+ const installPluginItemItem = AdminClient.encoding
+ .getPluginInstallItem(
+ "0x0123456789012345678901234567890123456789",
+ );
+
+ expect(typeof installPluginItemItem).toBe("object");
+ expect(installPluginItemItem.data).toBeInstanceOf(Uint8Array);
+ });
+ it("Should create an Admin client and fail to generate a install entry", async () => {
+ expect(() =>
+ AdminClient.encoding
+ .getPluginInstallItem(
+ TEST_INVALID_ADDRESS,
+ )
+ ).toThrow(new InvalidAddressOrEnsError());
+ });
+ });
+});
diff --git a/modules/client/test/integration/admin/estimation.test.ts b/modules/client/test/integration/admin/estimation.test.ts
new file mode 100644
index 000000000..c133b16d8
--- /dev/null
+++ b/modules/client/test/integration/admin/estimation.test.ts
@@ -0,0 +1,52 @@
+// @ts-ignore
+declare const describe, it, expect, beforeAll, afterAll;
+
+// mocks need to be at the top of the imports
+import "../../mocks/aragon-sdk-ipfs";
+
+import {
+ AdminClient,
+ Context,
+ ContextPlugin,
+ ExecuteAdminProposalParams,
+} from "../../../src";
+import { contextParamsLocalChain } from "../constants";
+import * as ganacheSetup from "../../helpers/ganache-setup";
+import * as deployContracts from "../../helpers/deployContracts";
+import { Server } from "ganache";
+
+describe("Client Admin", () => {
+ describe("Estimation module", () => {
+ let server: Server;
+
+ beforeAll(async () => {
+ server = await ganacheSetup.start();
+ const deployment = await deployContracts.deploy();
+ contextParamsLocalChain.daoFactoryAddress = deployment.daoFactory.address;
+ });
+
+ afterAll(async () => {
+ await server.close();
+ });
+
+ it("Should estimate the gas fees for executing a ", async () => {
+ const ctx = new Context(contextParamsLocalChain);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+
+ const proposalParams: ExecuteAdminProposalParams = {
+ pluginAddress: "0x1234567890123456789012345678901234567890",
+ metadataUri: "ipfs://QmeJ4kRW21RRgjywi9ydvY44kfx71x2WbRq7ik5xh5zBZK",
+ actions: [],
+ };
+
+ const estimation = await client.estimation.executeProposal(proposalParams);
+
+ expect(typeof estimation).toEqual("object");
+ expect(typeof estimation.average).toEqual("bigint");
+ expect(typeof estimation.max).toEqual("bigint");
+ expect(estimation.max).toBeGreaterThan(BigInt(0));
+ expect(estimation.max).toBeGreaterThan(estimation.average);
+ });
+ });
+});
diff --git a/modules/client/test/integration/admin/index.test.ts b/modules/client/test/integration/admin/index.test.ts
new file mode 100644
index 000000000..149265b82
--- /dev/null
+++ b/modules/client/test/integration/admin/index.test.ts
@@ -0,0 +1,64 @@
+// @ts-ignore
+declare const describe, it, expect;
+
+// mocks need to be at the top of the imports
+import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs";
+
+import { JsonRpcProvider } from "@ethersproject/providers";
+import { Wallet } from "@ethersproject/wallet";
+import { AdminClient, Context, ContextPlugin } from "../../../src";
+import { Client as IpfsClient } from "@aragon/sdk-ipfs";
+import { GraphQLClient } from "graphql-request";
+
+import { contextParams, contextParamsFailing } from "../constants";
+
+describe("Client Address List", () => {
+ describe("Client instances", () => {
+ it("Should create a working client", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+
+ expect(client).toBeInstanceOf(AdminClient);
+ expect(client.web3.getProvider()).toBeInstanceOf(JsonRpcProvider);
+ expect(client.web3.getConnectedSigner()).toBeInstanceOf(Wallet);
+ expect(client.ipfs.getClient()).toBeInstanceOf(IpfsClient);
+ expect(client.graphql.getClient()).toBeInstanceOf(GraphQLClient);
+
+ // Web3
+ const web3status = await client.web3.isUp();
+ expect(web3status).toEqual(true);
+ // IPFS
+ await client.ipfs.ensureOnline();
+ const ipfsStatus = await client.ipfs.isUp();
+ expect(ipfsStatus).toEqual(true);
+ // GraqphQl
+ await client.graphql.ensureOnline();
+ const graphqlStatus = await client.graphql.isUp();
+ expect(graphqlStatus).toEqual(true);
+ });
+
+ it("Should create a failing client", async () => {
+ const ctx = new Context(contextParamsFailing);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+
+ expect(client).toBeInstanceOf(AdminClient);
+ expect(client.web3.getProvider()).toBeInstanceOf(JsonRpcProvider);
+ expect(client.web3.getConnectedSigner()).toBeInstanceOf(Wallet);
+ expect(client.ipfs.getClient()).toBeInstanceOf(IpfsClient);
+ expect(client.graphql.getClient()).toBeInstanceOf(GraphQLClient);
+
+ // Web3
+ const web3status = await client.web3.isUp();
+ expect(web3status).toEqual(false);
+ // IPFS
+ mockedIPFSClient.nodeInfo.mockRejectedValueOnce(false);
+ const ipfsStatus = await client.ipfs.isUp();
+ expect(ipfsStatus).toEqual(false);
+ // GraqphQl
+ const graphqlStatus = await client.graphql.isUp();
+ expect(graphqlStatus).toEqual(false);
+ });
+ });
+});
diff --git a/modules/client/test/integration/admin/methods.test.ts b/modules/client/test/integration/admin/methods.test.ts
new file mode 100644
index 000000000..6ac38afc4
--- /dev/null
+++ b/modules/client/test/integration/admin/methods.test.ts
@@ -0,0 +1,274 @@
+// @ts-ignore
+declare const describe, it, beforeAll, afterAll, expect;
+
+// mocks need to be at the top of the imports
+import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs";
+
+import * as ganacheSetup from "../../helpers/ganache-setup";
+import * as deployContracts from "../../helpers/deployContracts";
+
+import {
+ AdminClient,
+ Client,
+ Context,
+ ContextPlugin,
+ ExecuteProposalStep,
+ Permissions,
+ ProposalMetadata,
+ ProposalSortBy,
+ ProposalStatus,
+ SortDirection,
+} from "../../../src";
+import { GraphQLError, InvalidAddressOrEnsError } from "@aragon/sdk-common";
+import {
+ contextParams,
+ contextParamsLocalChain,
+ TEST_ADMIN_DAO_ADDRESS,
+ TEST_ADMIN_PROPOSAL_ID,
+ TEST_INVALID_ADDRESS,
+ TEST_NON_EXISTING_ADDRESS,
+ TEST_WALLET_ADDRESS,
+} from "../constants";
+import { Server } from "ganache";
+import {
+ ExecuteAdminProposalParams,
+ IAdminProposalQueryParams,
+} from "../../../src/admin";
+
+describe("Client Admin", () => {
+ let pluginAddress: string;
+ let server: Server;
+ let daoAddr: string
+
+ beforeAll(async () => {
+ server = await ganacheSetup.start();
+ const deployment = await deployContracts.deploy();
+ contextParamsLocalChain.daoFactoryAddress = deployment.daoFactory.address;
+ const daoCreation = await deployContracts.createAdminDAO(
+ deployment,
+ "testDAO",
+ TEST_WALLET_ADDRESS,
+ );
+ pluginAddress = daoCreation.pluginAddrs[0];
+ daoAddr = daoCreation.daoAddr
+ });
+
+ afterAll(async () => {
+ await server.close();
+ });
+
+ describe("Proposal Execution", () => {
+ it("Should execute a new proposal locally", async () => {
+ const ctx = new Context(contextParamsLocalChain);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const adminClient = new AdminClient(ctxPlugin);
+ const client = new Client(ctx);
+
+ // const metadataUri = await client.methods.pinMetadata({
+ // name: "New DAO Name",
+ // description: "new description",
+ // links: [],
+ // });
+
+ // generate actions
+ // const action = await client.encoding.updateMetadataAction(
+ // pluginAddress,
+ // metadataUri,
+ // );
+
+ const metadata: ProposalMetadata = {
+ title: "Best Proposal",
+ summary: "this is the sumnary",
+ description: "This is a very long description",
+ resources: [
+ {
+ name: "Website",
+ url: "https://the.website",
+ },
+ ],
+ media: {
+ header: "https://no.media/media.jpeg",
+ logo: "https://no.media/media.jpeg",
+ },
+ };
+
+ const action = client.encoding.grantAction(pluginAddress, {
+ who: "0x1234567890123456789012345678901234567890",
+ where: daoAddr,
+ permission: Permissions.EXECUTE_PERMISSION
+ });
+ const ipfsUri = await adminClient.methods.pinMetadata(metadata);
+
+ const proposalParams: ExecuteAdminProposalParams = {
+ pluginAddress,
+ metadataUri: ipfsUri,
+ actions: [action],
+ };
+
+ for await (
+ const step of adminClient.methods.executeProposal(
+ proposalParams,
+ )
+ ) {
+ switch (step.key) {
+ case ExecuteProposalStep.EXECUTING:
+ expect(typeof step.txHash).toBe("string");
+ expect(step.txHash).toMatch(/^0x[A-Fa-f0-9]{64}$/i);
+ break;
+ case ExecuteProposalStep.DONE:
+ break;
+ default:
+ throw new Error(
+ "Unexpected proposal execution step: " +
+ Object.keys(step).join(", "),
+ );
+ }
+ }
+ });
+ });
+
+ describe("Data retrieval", () => {
+ it("Should fetch the given proposal", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+
+ const proposalId = TEST_ADMIN_PROPOSAL_ID;
+
+ mockedIPFSClient.cat.mockResolvedValue(
+ Buffer.from(
+ JSON.stringify({
+ title: "Title",
+ summary: "Summary",
+ description: "Description",
+ resources: [{
+ name: "Name",
+ url: "URL",
+ }],
+ }),
+ ),
+ );
+
+ const proposal = await client.methods.getProposal(proposalId);
+
+ expect(typeof proposal).toBe("object");
+ expect(proposal === null).toBe(false);
+ if (!proposal) throw new GraphQLError("Admin proposal");
+ expect(proposal.id).toBe(proposalId);
+ expect(typeof proposal.id).toBe("string");
+ expect(proposal.id).toMatch(/^0x[A-Fa-f0-9]{40}_0x[A-Fa-f0-9]{1,}$/i);
+ expect(typeof proposal.dao.address).toBe("string");
+ expect(proposal.dao.address).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ expect(typeof proposal.dao.name).toBe("string");
+ expect(typeof proposal.creatorAddress).toBe("string");
+ expect(proposal.creatorAddress).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ // check metadata
+ expect(typeof proposal.metadata.title).toBe("string");
+ expect(typeof proposal.metadata.summary).toBe("string");
+ expect(typeof proposal.metadata.description).toBe("string");
+ expect(Array.isArray(proposal.metadata.resources)).toBe(true);
+ for (let i = 0; i < proposal.metadata.resources.length; i++) {
+ const resource = proposal.metadata.resources[i];
+ expect(typeof resource.name).toBe("string");
+ expect(typeof resource.url).toBe("string");
+ }
+ if (proposal.metadata.media) {
+ if (proposal.metadata.media.header) {
+ expect(typeof proposal.metadata.media.header).toBe("string");
+ }
+ if (proposal.metadata.media.logo) {
+ expect(typeof proposal.metadata.media.logo).toBe("string");
+ }
+ }
+ expect(proposal.creationDate instanceof Date).toBe(true);
+ expect(Array.isArray(proposal.actions)).toBe(true);
+ expect(typeof proposal.proposalId === "bigint").toBe(true);
+ expect(typeof proposal.pluginAddress === "string").toBe(true);
+ expect(proposal.pluginAddress).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ expect(typeof proposal.adminAddress === "string").toBe(true);
+ expect(proposal.adminAddress).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ // actions
+ for (let i = 0; i < proposal.actions.length; i++) {
+ const action = proposal.actions[i];
+ expect(action.data instanceof Uint8Array).toBe(true);
+ expect(typeof action.to).toBe("string");
+ expect(typeof action.value).toBe("bigint");
+ }
+ });
+ it("Should fetch the given proposal and fail because the proposal does not exist", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+
+ const proposalId = TEST_NON_EXISTING_ADDRESS + "_0x0";
+ const proposal = await client.methods.getProposal(proposalId);
+
+ expect(proposal === null).toBe(true);
+ });
+ it("Should get a list of proposals filtered by the given criteria", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+ const limit = 5;
+ const status = ProposalStatus.EXECUTED;
+ const params: IAdminProposalQueryParams = {
+ limit,
+ sortBy: ProposalSortBy.CREATED_AT,
+ direction: SortDirection.ASC,
+ status,
+ };
+ const proposals = await client.methods.getProposals(params);
+
+ expect(Array.isArray(proposals)).toBe(true);
+ expect(proposals.length <= limit).toBe(true);
+ for (let i = 0; i < proposals.length; i++) {
+ const proposal = proposals[i];
+ expect(typeof proposal.id).toBe("string");
+ expect(proposal.id).toMatch(/^0x[A-Fa-f0-9]{40}_0x[A-Fa-f0-9]{1,}$/i);
+ expect(typeof proposal.dao.address).toBe("string");
+ expect(proposal.dao.address).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ expect(typeof proposal.dao.name).toBe("string");
+ expect(typeof proposal.creatorAddress).toBe("string");
+ expect(proposal.creatorAddress).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ expect(typeof proposal.metadata.title).toBe("string");
+ expect(typeof proposal.metadata.summary).toBe("string");
+ expect(typeof proposal.adminAddress === "string").toBe(true);
+ expect(proposal.adminAddress).toMatch(/^0x[A-Fa-f0-9]{40}$/i);
+ expect(proposal.status).toBe(status);
+ }
+ });
+ it("Should get a list of proposals from a specific admin", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+ const limit = 5;
+ const address = TEST_ADMIN_DAO_ADDRESS;
+ const params: IAdminProposalQueryParams = {
+ limit,
+ sortBy: ProposalSortBy.CREATED_AT,
+ direction: SortDirection.ASC,
+ adminAddressOrEns: address,
+ };
+ const proposals = await client.methods.getProposals(params);
+
+ expect(Array.isArray(proposals)).toBe(true);
+ expect(proposals.length > 0 && proposals.length <= limit).toBe(true);
+ });
+ it("Should get a list of proposals from an invalid address", async () => {
+ const ctx = new Context(contextParams);
+ const ctxPlugin = ContextPlugin.fromContext(ctx);
+ const client = new AdminClient(ctxPlugin);
+ const limit = 5;
+ const address = TEST_INVALID_ADDRESS;
+ const params: IAdminProposalQueryParams = {
+ limit,
+ sortBy: ProposalSortBy.CREATED_AT,
+ direction: SortDirection.ASC,
+ adminAddressOrEns: address,
+ };
+ await expect(() => client.methods.getProposals(params)).rejects.toThrow(
+ new InvalidAddressOrEnsError(),
+ );
+ });
+ });
+});
diff --git a/modules/client/test/integration/constants.ts b/modules/client/test/integration/constants.ts
index 04d45a7a1..3e3c7e8ea 100644
--- a/modules/client/test/integration/constants.ts
+++ b/modules/client/test/integration/constants.ts
@@ -50,7 +50,8 @@ export const TEST_WALLET =
"0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e";
// Token
-export const TEST_TOKEN_VOTING_DAO_ADDRESS = "0xa893a2b4c4372dea2877ecfc0d079676e637985f";
+export const TEST_TOKEN_VOTING_DAO_ADDRESS =
+ "0xa893a2b4c4372dea2877ecfc0d079676e637985f";
export const TEST_TOKEN_VOTING_PLUGIN_ADDRESS =
"0x6bafbdb8d8b68ba08cc8c2f6f014b22ce54abfcd";
export const TEST_TOKEN_VOTING_PROPOSAL_ID = TEST_TOKEN_VOTING_PLUGIN_ADDRESS +
@@ -65,19 +66,26 @@ export const TEST_ADDRESSLIST_PROPOSAL_ID = TEST_ADDRESSLIST_PLUGIN_ADDRESS +
"_0x0";
// Multisig
-export const TEST_MULTISIG_DAO_ADDRESS = "0x84432686c0d14f362e0e7c08c780682116d6bc44"
+export const TEST_MULTISIG_DAO_ADDRESS =
+ "0x84432686c0d14f362e0e7c08c780682116d6bc44";
export const TEST_MULTISIG_PLUGIN_ADDRESS =
"0xfdb81a1be7feae875088d5d9ab7953824ba69adf";
-export const TEST_MULTISIG_PROPOSAL_ID = TEST_MULTISIG_PLUGIN_ADDRESS + "_0x0"
+export const TEST_MULTISIG_PROPOSAL_ID = TEST_MULTISIG_PLUGIN_ADDRESS + "_0x0";
export const TEST_DAO_ADDRESS = TEST_TOKEN_VOTING_DAO_ADDRESS;
-// TODO FIX
export const TEST_NO_BALANCES_DAO_ADDRESS =
"0x95acd075a4519edb30d4138d0fafea2d1a1f74e6";
export const TEST_INVALID_ADDRESS = "0x1nv4l1d_4ddr355";
export const TEST_NON_EXISTING_ADDRESS =
"0x1234567890123456789012345678901234567890";
+// admin
+export const TEST_ADMIN_DAO_ADDRESS =
+ "0xe460c07278905e15ce1f94a48d2602ca113fbd4e";
+export const TEST_ADMIN_PLUGIN_ADDRESS =
+ "0x9d1ac5838fdc143bd827a878cd01eab8ff4da0e4";
+export const TEST_ADMIN_PROPOSAL_ID = TEST_ADMIN_PLUGIN_ADDRESS + "_0x0";
+
export const contextParams: ContextParams = {
network: "mainnet",
signer: new Wallet(TEST_WALLET),
diff --git a/modules/common/CHANGELOG.md b/modules/common/CHANGELOG.md
index 8b09058cb..d7713a67f 100644
--- a/modules/common/CHANGELOG.md
+++ b/modules/common/CHANGELOG.md
@@ -17,6 +17,7 @@ TEMPLATE:
-->
## [UPCOMING]
+- Adds new error `ProposalExecutionError`
- Adds new error `EnsureAllowanceError`
- Adds new error `InvalidPrecisionError`
diff --git a/modules/common/package.json b/modules/common/package.json
index 4fb21114b..33ed5b479 100644
--- a/modules/common/package.json
+++ b/modules/common/package.json
@@ -1,7 +1,7 @@
{
"name": "@aragon/sdk-common",
"author": "Aragon Association",
- "version": "0.9.2-alpha",
+ "version": "0.9.3-alpha",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/sdk-common.esm.js",
diff --git a/modules/common/src/errors.ts b/modules/common/src/errors.ts
index 038ee8143..d06697ecb 100644
--- a/modules/common/src/errors.ts
+++ b/modules/common/src/errors.ts
@@ -84,6 +84,11 @@ export class ProposalCreationError extends Error {
super("Failed to create proposal");
}
}
+export class ProposalExecutionError extends Error {
+ constructor() {
+ super("Failed to execute proposal");
+ }
+}
export class MissingExecPermissionError extends Error {
constructor() {
@@ -95,6 +100,11 @@ export class IpfsFetchError extends Error {
super("Failed to fetch data from IPFS");
}
}
+export class InvalidProposalStatus extends Error {
+ constructor() {
+ super("Invalid proposal status");
+ }
+}
export class InvalidVotingModeError extends Error {
constructor() {
super("Invalid voting mode");
diff --git a/package.json b/package.json
index c6dad5b01..622a9adec 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,8 @@
"docs:client": "jsdoc2md --files ./modules/client/src/*.ts ./modules/client/src/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/client.md",
"docs:addresslistVoting": "jsdoc2md --files ./modules/client/src/addresslistVoting/*.ts ./modules/client/src/addresslistVoting/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/addresslistVoting.md",
"docs:tokenVoting": "jsdoc2md --files ./modules/client/src/tokenVoting/*.ts ./modules/client/src/tokenVoting/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/tokenVoting.md",
- "docs:multisig": "jsdoc2md --files ./modules/client/src/multisig/*.ts ./modules/client/src/multisig/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/multisig.md"
+ "docs:multisig": "jsdoc2md --files ./modules/client/src/multisig/*.ts ./modules/client/src/multisig/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/multisig.md",
+ "docs:admin": "jsdoc2md --files ./modules/client/src/admin/*.ts ./modules/client/src/admin/internal/client/*.ts --configure ./jsdoc2md.json > ./docs/admin.md"
},
"devDependencies": {
"@babel/preset-typescript": "^7.18.6",