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",