diff --git a/modules/client-common/CHANGELOG.md b/modules/client-common/CHANGELOG.md index 3975deebf..271e8c72b 100644 --- a/modules/client-common/CHANGELOG.md +++ b/modules/client-common/CHANGELOG.md @@ -18,6 +18,11 @@ TEMPLATE: --> ## [UPCOMING] +### Added + +- Add common schemas for validations + +## 1.6.0 ### Added diff --git a/modules/client-common/package.json b/modules/client-common/package.json index 89435aa9d..5b372396a 100644 --- a/modules/client-common/package.json +++ b/modules/client-common/package.json @@ -68,6 +68,7 @@ "@ethersproject/providers": "^5.5.0", "@ethersproject/wallet": "^5.6.0", "graphql": "^16.5.0", - "graphql-request": "^4.3.0" + "graphql-request": "^4.3.0", + "yup": "^1.2.0" } } diff --git a/modules/client-common/src/constants.ts b/modules/client-common/src/constants.ts index 5bc39764a..e1a72e377 100644 --- a/modules/client-common/src/constants.ts +++ b/modules/client-common/src/constants.ts @@ -3,6 +3,8 @@ import { activeContractsList as activeContractsListV1_0_0 } from "@aragon/osx-et import { ProposalMetadata, SupportedNetwork, SupportedVersion } from "./types"; import { NetworkDeployment } from "./internal"; import { Network } from "@ethersproject/networks"; +import { keccak256 } from "@ethersproject/keccak256"; +import { toUtf8Bytes } from "@ethersproject/strings"; /** Timeout that will be applied to operations involving * many fetch requests that could take a long time */ @@ -344,3 +346,30 @@ export const ADDITIONAL_NETWORKS: Network[] = [ chainId: 31337, }, ]; + +const Permissions = { + UPGRADE_PERMISSION: "UPGRADE_PERMISSION", + SET_METADATA_PERMISSION: "SET_METADATA_PERMISSION", + EXECUTE_PERMISSION: "EXECUTE_PERMISSION", + WITHDRAW_PERMISSION: "WITHDRAW_PERMISSION", + SET_SIGNATURE_VALIDATOR_PERMISSION: "SET_SIGNATURE_VALIDATOR_PERMISSION", + SET_TRUSTED_FORWARDER_PERMISSION: "SET_TRUSTED_FORWARDER_PERMISSION", + ROOT_PERMISSION: "ROOT_PERMISSION", + CREATE_VERSION_PERMISSION: "CREATE_VERSION_PERMISSION", + REGISTER_PERMISSION: "REGISTER_PERMISSION", + REGISTER_DAO_PERMISSION: "REGISTER_DAO_PERMISSION", + REGISTER_ENS_SUBDOMAIN_PERMISSION: "REGISTER_ENS_SUBDOMAIN_PERMISSION", + MINT_PERMISSION: "MINT_PERMISSION", + MERKLE_MINT_PERMISSION: "MERKLE_MINT_PERMISSION", + MODIFY_ALLOWLIST_PERMISSION: "MODIFY_ALLOWLIST_PERMISSION", + SET_CONFIGURATION_PERMISSION: "SET_CONFIGURATION_PERMISSION", +}; + +const PermissionIds = Object.entries(Permissions).reduce( + (acc, [k, v]) => ({ ...acc, [k + "_ID"]: keccak256(toUtf8Bytes(v)) }), + {} as { [k: string]: string }, +); +Object.freeze(Permissions); +export { Permissions }; +Object.freeze(PermissionIds); +export { PermissionIds }; diff --git a/modules/client-common/src/index.ts b/modules/client-common/src/index.ts index c1e76dfcd..9aed50762 100644 --- a/modules/client-common/src/index.ts +++ b/modules/client-common/src/index.ts @@ -4,3 +4,4 @@ export * from "./context"; export * from "./constants"; export * from "./types"; export * from "./utils"; +export * from "./schemas"; diff --git a/modules/client-common/src/internal/constants.ts b/modules/client-common/src/internal/constants.ts new file mode 100644 index 000000000..3932e42ce --- /dev/null +++ b/modules/client-common/src/internal/constants.ts @@ -0,0 +1 @@ +export const ANY_ADDRESS = "0xffffffffffffffffffffffffffffffffffffffff"; diff --git a/modules/client-common/src/schemas.ts b/modules/client-common/src/schemas.ts new file mode 100644 index 000000000..34a67ef1b --- /dev/null +++ b/modules/client-common/src/schemas.ts @@ -0,0 +1,99 @@ +import { + InvalidAddressOrEnsError, + InvalidCidError, + InvalidContractAbiError, + InvalidParameter, + InvalidSubdomainError, + isEnsName, + isIpfsUri, + isSubdomain, +} from "@aragon/sdk-common"; +import { array, mixed, number, object, string } from "yup"; +import { isAddress } from "@ethersproject/address"; +import { ANY_ADDRESS } from "./internal/constants"; + +export const BigintSchema = mixed().test( + "isBigint", + new InvalidParameter("bigint").message, + (value) => typeof value === "bigint", +); +export const AddressOrEnsSchema = string().notRequired().test( + "isAddressOrEns", + new InvalidAddressOrEnsError().message, + (value) => value ? isAddress(value) || isEnsName(value) : true, +); +export const AddressOrEnsWithoutAnySchema = string().notRequired().test( + "isAddressOrEnsWithoutAny", + new InvalidAddressOrEnsError().message, + (value) => value ? (isAddress(value) || isEnsName(value)) && value !== ANY_ADDRESS : true + ); +export const VersionTagSchema = object({ + build: number().moreThan(0).required(), + release: number().moreThan(0).required(), +}); +export const AbiSchema = array().notRequired().test( + "isValidAbi", + new InvalidContractAbiError().message, + // TODO: validate abi + () => true, +); +export const Uint8ArraySchema = mixed().test( + "isUint8Array", + new InvalidParameter("Uint8Array").message, + (value) => value ? value instanceof Uint8Array : true, +); +export const IpfsUriSchema = string().test( + "isIpfsUri", + new InvalidCidError().message, + (value) => value ? isIpfsUri(value) : true, +); +export const SubdomainSchema = string().test( + "isSubdomain", + new InvalidSubdomainError().message, + (value) => value ? isSubdomain(value) : true, +); + +export const PaginationSchema = object({ + skip: number().min(0).notRequired(), + limit: number().min(1).notRequired(), + direction: string().oneOf(["asc", "desc"]).notRequired(), +}); + +export const PrepareUninstallationSchema = object({ + daoAddressOrEns: AddressOrEnsSchema.required(), + pluginAddress: AddressOrEnsSchema.required(), + pluginInstallationIndex: number().notRequired().min(0), + uninstallationParams: array().notRequired(), + uninstallationAbi: AbiSchema.notRequired(), +}); +export const MultiTargetPermissionSchema = object({ + operation: number().required().oneOf([0, 1, 2]), + permissionId: string().required(), + where: AddressOrEnsWithoutAnySchema.required(), + who: AddressOrEnsWithoutAnySchema.required(), + condition: string().notRequired(), +}); + +export const PrepareInstallationSchema = object({ + daoAddressOrEns: AddressOrEnsSchema.required(), + pluginRepo: AddressOrEnsSchema.required(), + version: VersionTagSchema.notRequired(), + installationParams: array().notRequired(), + installationAbi: AbiSchema.notRequired(), +}); + +export const PluginInstallItemSchema = object({ + id: AddressOrEnsSchema.required(), + data: Uint8ArraySchema.required(), +}); + +export const ApplyUninstallationSchema = object({ + pluginAddress: AddressOrEnsSchema.required(), + pluginRepo: AddressOrEnsSchema.required(), + versionTag: VersionTagSchema.required(), + permissions: array(MultiTargetPermissionSchema).required(), +}); + +export const ApplyInstallationSchema = ApplyUninstallationSchema.concat(object({ + helpers: array(AddressOrEnsSchema).required(), +})); diff --git a/modules/client-common/src/types.ts b/modules/client-common/src/types.ts index b9ba29327..684aae96e 100644 --- a/modules/client-common/src/types.ts +++ b/modules/client-common/src/types.ts @@ -108,6 +108,27 @@ export type DecodedApplyInstallationParams = ApplyInstallationParamsBase & { helpersHash: string; }; +/* Uninstallation */ +export type PrepareUninstallationParams = { + daoAddressOrEns: string; + pluginAddress: string; + pluginInstallationIndex?: number; + uninstallationParams?: any[]; + uninstallationAbi?: string[]; +}; +export enum PrepareUninstallationSteps { + PREPARING = "preparing", + DONE = "done", +} +export type PrepareUninstallationStepValue = + | { key: PrepareUninstallationSteps.PREPARING; txHash: string } + | { + key: PrepareUninstallationSteps.DONE; + } & ApplyUninstallationParams; + +export type ApplyUninstallationParams = ApplyInstallationParamsBase; +export type DecodedApplyUninstallationParams = ApplyInstallationParamsBase; + export type VersionTag = { build: number; release: number; diff --git a/modules/client-common/test/constants.ts b/modules/client-common/test/constants.ts index 4c5934e1b..3d0327425 100644 --- a/modules/client-common/test/constants.ts +++ b/modules/client-common/test/constants.ts @@ -109,3 +109,24 @@ export const TEST_ABI: MetadataAbiInput[] = [ ], }, ]; + + +export const TEST_ADDRESS = "0x0000000000000000000000000000000000000001"; +export const TEST_INVALID_ADDRESS = + "0x000000000000000000000000000000000000000P"; + +export const TEST_ENS_NAME = "test.eth"; +export const TEST_INVALID_ENS_NAME = "test.invalid"; + +export const TEST_IPFS_URI_V0 = + "ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"; +export const TEST_IPFS_URI_V1 = + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"; +export const TEST_INVALID_IPFS_URI = + "ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR-invalid"; + +export const TEST_HTTP_URI = "https://test.com"; +export const TEST_INVALID_HTTP_URI = "https://te?st.com-invalid"; + +export const TEST_SUBDOMAIN = "test"; +export const TEST_INVALID_SUBDOMAIN = "test.invalid"; diff --git a/modules/client-common/test/schemas.test.ts b/modules/client-common/test/schemas.test.ts new file mode 100644 index 000000000..b8bb89c75 --- /dev/null +++ b/modules/client-common/test/schemas.test.ts @@ -0,0 +1,910 @@ +import { ValidationError } from "yup"; +import { + AddressOrEnsSchema, + ApplyInstallationParams, + ApplyInstallationSchema, + ApplyUninstallationParams, + ApplyUninstallationSchema, + BigintSchema, + IpfsUriSchema, + MultiTargetPermission, + MultiTargetPermissionSchema, + Pagination, + PaginationSchema, + PermissionIds, + PermissionOperationType, + PrepareInstallationParams, + PrepareInstallationSchema, + PrepareUninstallationParams, + PrepareUninstallationSchema, + SortDirection, + SubdomainSchema, + Uint8ArraySchema, + VersionTagSchema, +} from "../src"; +import { + InvalidAddressOrEnsError, + InvalidCidError, + InvalidParameter, + InvalidSubdomainError, +} from "@aragon/sdk-common"; +import { + TEST_ADDRESS, + TEST_ENS_NAME, + TEST_INVALID_ADDRESS, + TEST_INVALID_ENS_NAME, + TEST_INVALID_IPFS_URI, + TEST_INVALID_SUBDOMAIN, + TEST_IPFS_URI_V0, + TEST_IPFS_URI_V1, + TEST_SUBDOMAIN, +} from "./constants"; + +describe("Test client schemas", () => { + describe("Test bigints", () => { + it("should validate a valid bigint", () => { + const number = BigInt(1); + BigintSchema.validateSync(number); + }); + it("should throw an error if the bigint is TEST_invalid", () => { + const number = 1; + expect(() => BigintSchema.validateSync(number)).toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + }); + + describe("Test address or ens", () => { + it("should validate a valid address", () => { + const address = TEST_ADDRESS; + AddressOrEnsSchema.strict().validateSync(address); + }); + it("should validate a valid ens", () => { + const ens = TEST_ENS_NAME; + AddressOrEnsSchema.strict().validateSync(ens); + }); + it("should throw an error if the address is invalid", () => { + const address = TEST_INVALID_ADDRESS; + expect(() => AddressOrEnsSchema.strict().validateSync(address)).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the ens is invalid", () => { + const ens = TEST_INVALID_ENS_NAME; + expect(() => AddressOrEnsSchema.strict().validateSync(ens)).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + + describe("Test version tag", () => { + it("should validate a valid version tag", () => { + const versionTag = { + release: 1, + build: 1, + }; + VersionTagSchema.strict().validateSync(versionTag); + }); + it("should throw an error if the release is invalid", () => { + const versionTag = { + release: 0, + build: 1, + }; + expect(() => VersionTagSchema.strict().validateSync(versionTag)).toThrow( + new ValidationError( + "release must be greater than 0", + ), + ); + }); + it("should throw an error if the release is invalid", () => { + const versionTag = { + release: 1, + build: 0, + }; + expect(() => VersionTagSchema.strict().validateSync(versionTag)).toThrow( + new ValidationError( + "build must be greater than 0", + ), + ); + }); + it("should throw an error if the build is missing", () => { + const versionTag = { + release: 1, + }; + expect(() => VersionTagSchema.strict().validateSync(versionTag)).toThrow( + new ValidationError( + "build is a required field", + ), + ); + }); + it("should throw an error if the release is missing", () => { + const versionTag = { + build: 1, + }; + expect(() => VersionTagSchema.strict().validateSync(versionTag)).toThrow( + new ValidationError( + "release is a required field", + ), + ); + }); + }); + + test.todo("Abi Schema"); + + describe("Test Uint8Array", () => { + it("should validate a valid Uint8Array", () => { + const uint8Array = new Uint8Array(); + Uint8ArraySchema.strict().validateSync(uint8Array); + }); + it("should throw an error if the Uint8Array is invalid", () => { + const uint8Array: number[] = []; + expect(() => Uint8ArraySchema.strict().validateSync(uint8Array)).toThrow( + new ValidationError(new InvalidParameter("Uint8Array").message), + ); + }); + }); + + describe("Test IpfsCid", () => { + it("should validate a valid v0 ipfs CID", () => { + const ipfsUri = TEST_IPFS_URI_V0; + IpfsUriSchema.strict().validateSync(ipfsUri); + }); + it("should validate a valid v1 ipfs CID", () => { + const ipfsUri = TEST_IPFS_URI_V1; + IpfsUriSchema.strict().validateSync(ipfsUri); + }); + it("should throw an error if the ipfs CID is invalid", () => { + const ipfsUri = TEST_INVALID_IPFS_URI; + expect(() => IpfsUriSchema.strict().validateSync(ipfsUri)).toThrow( + new ValidationError(new InvalidCidError().message), + ); + }); + }); + describe("Test Subdomain", () => { + it("should validate a valid subdomain", () => { + const subdomain = TEST_SUBDOMAIN; + SubdomainSchema.strict().validateSync(subdomain); + }); + it("should throw an error if the subdomain is invalid", () => { + const subdomain = TEST_INVALID_SUBDOMAIN; + expect(() => SubdomainSchema.strict().validateSync(subdomain)).toThrow( + new ValidationError(new InvalidSubdomainError().message), + ); + }); + }); + + describe("Test Pagination", () => { + it("should validate a valid Pagination", () => { + const pagination: Pagination = { + skip: 0, + limit: 1, + direction: SortDirection.ASC, + }; + PaginationSchema.strict().validateSync(pagination); + }); + it("should validate a valid Pagination without optional params", () => { + const pagination: Pagination = {}; + PaginationSchema.strict().validateSync(pagination); + }); + it("should throw an error if the skip is invalid", () => { + const pagination: Pagination = { + skip: -1, + limit: 1, + }; + expect(() => PaginationSchema.strict().validateSync(pagination)).toThrow( + new ValidationError( + "skip must be greater than or equal to 0", + ), + ); + }); + it("should throw an error if the limit is invalid", () => { + const pagination: Pagination = { + skip: 0, + limit: 0, + }; + expect(() => PaginationSchema.strict().validateSync(pagination)).toThrow( + new ValidationError( + "limit must be greater than or equal to 1", + ), + ); + }); + it("should throw an error if the direction is invalid", () => { + const pagination = { + skip: 0, + limit: 1, + direction: "invalid", + }; + expect(() => PaginationSchema.strict().validateSync(pagination)).toThrow( + new ValidationError( + "direction must be one of the following values: asc, desc", + ), + ); + }); + }); + + describe("Test MultiTargetPermission", () => { + it("should validate a valid MultiTargetPermission", () => { + const multiTargetPermission: MultiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_ADDRESS, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + condition: TEST_ADDRESS, + }; + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ); + }); + it("should validate a valid MultiTargetPermission without optional params", () => { + const multiTargetPermission: MultiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_ADDRESS, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ); + }); + it("should throw an error if the operation is invalid", () => { + const multiTargetPermission = { + operation: 3, + where: TEST_ADDRESS, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError( + "operation must be one of the following values: 0, 1, 2", + ), + ); + }); + it("should throw an error if the operation is missing", () => { + const multiTargetPermission = { + where: TEST_ADDRESS, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError("operation is a required field"), + ); + }); + it("should throw an error if the where is invalid", () => { + const multiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_INVALID_ADDRESS, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the who is invalid", () => { + const multiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_ADDRESS, + who: TEST_INVALID_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the permissionId is missing", () => { + const multiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_ADDRESS, + who: TEST_ADDRESS, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError("permissionId is a required field"), + ); + }); + it("should throw if the where is missing", () => { + const multiTargetPermission = { + operation: PermissionOperationType.GRANT, + who: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError("where is a required field"), + ); + }); + it("should throw if the who is missing", () => { + const multiTargetPermission = { + operation: PermissionOperationType.GRANT, + where: TEST_ADDRESS, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError("who is a required field"), + ); + }); + it("should throw if the operation is missing", () => { + const multiTargetPermission = { + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + where: TEST_ADDRESS, + who: TEST_ADDRESS, + }; + expect(() => + MultiTargetPermissionSchema.strict().validateSync( + multiTargetPermission, + ) + ).toThrow( + new ValidationError("operation is a required field"), + ); + }); + }); + + describe("Test PrepareInstallation", () => { + it("should validate a valid PrepareInstallation", () => { + const prepareInstallationParams: PrepareInstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + version: { + release: 1, + build: 1, + }, + installationParams: [], + installationAbi: [], + }; + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ); + }); + it("should validate a valid PrepareInstallation without optional params", () => { + const prepareInstallationParams: PrepareInstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + }; + + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const prepareInstallationParams = { + pluginRepo: TEST_ADDRESS, + }; + expect(() => + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ) + ).toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the pluginRepo is missing", () => { + const prepareInstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + }; + expect(() => + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ) + ).toThrow( + new ValidationError("pluginRepo is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const prepareInstallationParams: PrepareInstallationParams = { + daoAddressOrEns: TEST_INVALID_ADDRESS, + pluginRepo: TEST_ADDRESS, + }; + expect(() => + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the pluginRepo is invalid", () => { + const prepareInstallationParams: PrepareInstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginRepo: TEST_INVALID_ADDRESS, + }; + expect(() => + PrepareInstallationSchema.strict().validateSync( + prepareInstallationParams, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Test PrepareUninstallation", () => { + it("should validate a valid PrepareUninstallation", () => { + const prepareUninstallationParams: PrepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginAddress: TEST_ADDRESS, + pluginInstallationIndex: 0, + uninstallationAbi: [], + uninstallationParams: [], + }; + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ); + }); + it("should validate a valid PrepareUninstallation without optional params", () => { + const prepareUninstallationParams: PrepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginAddress: TEST_ADDRESS, + pluginInstallationIndex: 0, + }; + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const prepareUninstallationParams = { + pluginAddress: TEST_ADDRESS, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the pluginAddress is missing", () => { + const prepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError("pluginAddress is a required field"), + ); + }); + it("should throw an error if the pluginInstallationIndex is invalid", () => { + const prepareUninstallationParams: PrepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginAddress: TEST_ADDRESS, + pluginInstallationIndex: -1, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError( + "pluginInstallationIndex must be greater than or equal to 0", + ), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const prepareUninstallationParams: PrepareUninstallationParams = { + daoAddressOrEns: TEST_INVALID_ADDRESS, + pluginAddress: TEST_ADDRESS, + pluginInstallationIndex: 0, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the pluginAddress is invalid", () => { + const prepareUninstallationParams: PrepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginAddress: TEST_INVALID_ADDRESS, + pluginInstallationIndex: 0, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the uninstallationParams is invalid", () => { + const prepareUninstallationParams = { + daoAddressOrEns: TEST_ADDRESS, + pluginAddress: TEST_ADDRESS, + uninstallationParams: {}, + }; + expect(() => + PrepareUninstallationSchema.strict().validateSync( + prepareUninstallationParams, + ) + ).toThrow( + new ValidationError( + "uninstallationParams must be a `array` type, but the final value was: `{}`.", + ), + ); + }); + test.todo("should throw an error if the uninstallationAbi is invalid"); + }); + describe("Test ApplyUninstallation", () => { + it("should validate a valid ApplyUninstallation", () => { + const applyUninstallationParams: ApplyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ); + }); + it("should throw an error if the pluginAddress is missing", () => { + const applyUninstallationParams = { + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError("pluginAddress is a required field"), + ); + }); + it("should throw an error if the pluginRepo is missing", () => { + const applyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError("pluginRepo is a required field"), + ); + }); + it("should throw an error if the versionTag is missing", () => { + const applyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError("versionTag is a required field"), + ); + }); + it("should throw an error if the permissions is missing", () => { + const applyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError("permissions is a required field"), + ); + }); + it("should throw an error if the pluginAddress is invalid", () => { + const applyUninstallationParams: ApplyUninstallationParams = { + pluginAddress: TEST_INVALID_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the pluginRepo is invalid", () => { + const applyUninstallationParams: ApplyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_INVALID_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the versionTag is invalid", () => { + const applyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 0, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError( + "versionTag.build must be greater than 0", + ), + ); + }); + it("should throw an error if the permissions is invalid", () => { + const applyUninstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: {}, + }; + expect(() => + ApplyUninstallationSchema.strict().validateSync( + applyUninstallationParams, + ) + ) + .toThrow( + new ValidationError( + "permissions must be a `array` type, but the final value was: `{}`.", + ), + ); + }); + }); + describe("Test ApplyInstallation", () => { + it("should validate a valid ApplyInstallation", () => { + const applyInstallationParams: ApplyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: [], + }; + ApplyInstallationSchema.strict().validateSync(applyInstallationParams); + }); + it("should throw an error if the pluginAddress is missing", () => { + const applyInstallationParams = { + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError("pluginAddress is a required field"), + ); + }); + it("should throw an error if the pluginRepo is missing", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError("pluginRepo is a required field"), + ); + }); + it("should throw an error if the versionTag is missing", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + permissions: [], + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError("versionTag is a required field"), + ); + }); + it("should throw an error if the permissions is missing", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError("permissions is a required field"), + ); + }); + it("should throw an error if the helpers is missing", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError("helpers is a required field"), + ); + }); + it("should throw an error if the pluginAddress is invalid", () => { + const applyInstallationParams: ApplyInstallationParams = { + pluginAddress: TEST_INVALID_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the pluginRepo is invalid", () => { + const applyInstallationParams: ApplyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_INVALID_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the permissions is invalid", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: {}, + helpers: [], + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError( + "permissions must be a `array` type, but the final value was: `{}`.", + ), + ); + }); + it("should throw an error if the helpers is invalid", () => { + const applyInstallationParams = { + pluginAddress: TEST_ADDRESS, + pluginRepo: TEST_ADDRESS, + versionTag: { + build: 1, + release: 1, + }, + permissions: [], + helpers: {}, + }; + expect(() => + ApplyInstallationSchema.strict().validateSync( + applyInstallationParams, + ) + ) + .toThrow( + new ValidationError( + "helpers must be a `array` type, but the final value was: `{}`.", + ), + ); + }); + }); +}); diff --git a/modules/client/CHANGELOG.md b/modules/client/CHANGELOG.md index ace1de223..fef5f1279 100644 --- a/modules/client/CHANGELOG.md +++ b/modules/client/CHANGELOG.md @@ -18,6 +18,10 @@ TEMPLATE: --> ## [UPCOMING] +## [1.14.0] +### Added + +- Add input validation on client functions ### Added @@ -29,7 +33,6 @@ TEMPLATE: - All addresses are now lowercased on subgraph methods - Use signer only when transactions need to be signed, else use provider - ## [1.13.1-rc1] ### Fixes diff --git a/modules/client/package.json b/modules/client/package.json index 68543449e..68297685c 100644 --- a/modules/client/package.json +++ b/modules/client/package.json @@ -74,7 +74,8 @@ "@openzeppelin/contracts": "^4.8.1", "@openzeppelin/contracts-upgradeable": "^4.8.1", "graphql": "^16.5.0", - "graphql-request": "^4.3.0" + "graphql-request": "^4.3.0", + "yup": "^1.2.0" }, "jest": { "testEnvironment": "../../test-environment.js", diff --git a/modules/client/src/addresslistVoting/internal/client/estimation.ts b/modules/client/src/addresslistVoting/internal/client/estimation.ts index 4ba8ee4c3..f0565422b 100644 --- a/modules/client/src/addresslistVoting/internal/client/estimation.ts +++ b/modules/client/src/addresslistVoting/internal/client/estimation.ts @@ -40,7 +40,7 @@ export class AddresslistVotingClientEstimation extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/addresslistVoting/internal/client/methods.ts b/modules/client/src/addresslistVoting/internal/client/methods.ts index 37c2dd677..738ec83c8 100644 --- a/modules/client/src/addresslistVoting/internal/client/methods.ts +++ b/modules/client/src/addresslistVoting/internal/client/methods.ts @@ -109,7 +109,7 @@ export class AddresslistVotingClientMethods extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/constants.ts b/modules/client/src/constants.ts deleted file mode 100644 index 203d07705..000000000 --- a/modules/client/src/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { keccak256 } from "@ethersproject/keccak256"; -import { toUtf8Bytes } from "@ethersproject/strings"; - -const Permissions = { - UPGRADE_PERMISSION: "UPGRADE_PERMISSION", - SET_METADATA_PERMISSION: "SET_METADATA_PERMISSION", - EXECUTE_PERMISSION: "EXECUTE_PERMISSION", - WITHDRAW_PERMISSION: "WITHDRAW_PERMISSION", - SET_SIGNATURE_VALIDATOR_PERMISSION: "SET_SIGNATURE_VALIDATOR_PERMISSION", - SET_TRUSTED_FORWARDER_PERMISSION: "SET_TRUSTED_FORWARDER_PERMISSION", - ROOT_PERMISSION: "ROOT_PERMISSION", - CREATE_VERSION_PERMISSION: "CREATE_VERSION_PERMISSION", - REGISTER_PERMISSION: "REGISTER_PERMISSION", - REGISTER_DAO_PERMISSION: "REGISTER_DAO_PERMISSION", - REGISTER_ENS_SUBDOMAIN_PERMISSION: "REGISTER_ENS_SUBDOMAIN_PERMISSION", - MINT_PERMISSION: "MINT_PERMISSION", - MERKLE_MINT_PERMISSION: "MERKLE_MINT_PERMISSION", - MODIFY_ALLOWLIST_PERMISSION: "MODIFY_ALLOWLIST_PERMISSION", - SET_CONFIGURATION_PERMISSION: "SET_CONFIGURATION_PERMISSION", -}; - -const PermissionIds = Object.entries(Permissions).reduce( - (acc, [k, v]) => ({ ...acc, [k + "_ID"]: keccak256(toUtf8Bytes(v)) }), - {} as { [k: string]: string }, -); -Object.freeze(Permissions); -export { Permissions }; -Object.freeze(PermissionIds); -export { PermissionIds }; diff --git a/modules/client/src/index.ts b/modules/client/src/index.ts index 10fa97437..2c747d911 100644 --- a/modules/client/src/index.ts +++ b/modules/client/src/index.ts @@ -4,18 +4,19 @@ export * from "./tokenVoting"; export * from "./client-common"; export * from "./multisig"; export * from "./types"; -export * from "./constants"; // Selective reexports for completeness export { ApplyInstallationParams, ApplyUpdateParams, - Context, - ContextParams, DecodedApplyInstallationParams, DecodedApplyUpdateParams, PrepareInstallationParams, PrepareInstallationStep, PrepareUpdateParams, PrepareUpdateStep, + Context, + ContextParams, + PermissionIds, + Permissions, } from "@aragon/sdk-client-common"; diff --git a/modules/client/src/internal/client/decoding.ts b/modules/client/src/internal/client/decoding.ts index 164637bc7..1089801e5 100644 --- a/modules/client/src/internal/client/decoding.ts +++ b/modules/client/src/internal/client/decoding.ts @@ -1,6 +1,5 @@ import { DaoMetadata, - DecodedApplyUninstallationParams, GrantPermissionDecodedParams, GrantPermissionWithConditionParams, InitializeFromParams, @@ -40,11 +39,15 @@ import { AddressZero } from "@ethersproject/constants"; import { toUtf8String } from "@ethersproject/strings"; import { IClientDecoding } from "../interfaces"; import { + AddressOrEnsSchema, + BigintSchema, ClientCore, DecodedApplyInstallationParams, + DecodedApplyUninstallationParams, DecodedApplyUpdateParams, InterfaceParams, TokenType, + Uint8ArraySchema, } from "@aragon/sdk-client-common"; /** @@ -59,6 +62,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public applyInstallationAction( data: Uint8Array, ): DecodedApplyInstallationParams { + Uint8ArraySchema.strict().validateSync(data); const pspInterface = PluginSetupProcessor__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = pspInterface.getFunction("applyInstallation"); @@ -73,6 +77,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public applyUninstallationAction( data: Uint8Array, ): DecodedApplyUninstallationParams { + Uint8ArraySchema.strict().validateSync(data); const pspInterface = PluginSetupProcessor__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = pspInterface.getFunction("applyUninstallation"); @@ -101,6 +106,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public grantAction(data: Uint8Array): GrantPermissionDecodedParams { + Uint8ArraySchema.strict().validate(data); return decodeGrantAction(data); } /** @@ -113,6 +119,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public grantWithConditionAction( data: Uint8Array, ): GrantPermissionWithConditionParams { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction("grantWithCondition"); @@ -127,6 +134,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public revokeAction(data: Uint8Array): RevokePermissionDecodedParams { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction("revoke"); @@ -145,6 +153,9 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { value: bigint, data: Uint8Array, ): WithdrawParams { + AddressOrEnsSchema.strict().validateSync(to); + BigintSchema.strict().validateSync(value); + Uint8ArraySchema.strict().validateSync(data); // Native if (!data?.length) { return { @@ -209,6 +220,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public updateDaoMetadataRawAction(data: Uint8Array): string { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction("setMetadata"); @@ -225,6 +237,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public async updateDaoMetadataAction(data: Uint8Array): Promise { + await Uint8ArraySchema.strict().validate(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction("setMetadata"); @@ -246,6 +259,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public setDaoUriAction(data: Uint8Array): string { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction("setDaoURI"); @@ -262,6 +276,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public registerStandardCallbackAction( data: Uint8Array, ): RegisterStandardCallbackParams { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction( @@ -284,6 +299,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public setSignatureValidatorAction( data: Uint8Array, ): string { + Uint8ArraySchema.strict().validateSync(data); const daoInterface = DAO__factory.createInterface(); const hexBytes = bytesToHex(data); const expectedFunction = daoInterface.getFunction( @@ -311,6 +327,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { public upgradeToAndCallAction( data: Uint8Array, ): UpgradeToAndCallParams { + Uint8ArraySchema.strict().validate(data); return decodeUpgradeToAndCallAction(data); } @@ -322,6 +339,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public initializeFromAction(data: Uint8Array): InitializeFromParams { + Uint8ArraySchema.strict().validate(data); return decodeInitializeFromAction(data); } @@ -333,6 +351,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { * @memberof ClientDecoding */ public findInterface(data: Uint8Array): InterfaceParams | null { + Uint8ArraySchema.strict().validate(data); return findInterface(data, AVAILABLE_FUNCTION_SIGNATURES); } } diff --git a/modules/client/src/internal/client/encoding.ts b/modules/client/src/internal/client/encoding.ts index 974400d35..7a5a6c9e9 100644 --- a/modules/client/src/internal/client/encoding.ts +++ b/modules/client/src/internal/client/encoding.ts @@ -1,5 +1,4 @@ import { - ApplyUninstallationParams, GrantPermissionParams, GrantPermissionWithConditionParams, InitializeFromParams, @@ -29,21 +28,36 @@ import { InvalidAddressError, InvalidAddressOrEnsError, InvalidEnsError, - InvalidParameter, NotImplementedError, - SizeMismatchError, } from "@aragon/sdk-common"; import { toUtf8Bytes } from "@ethersproject/strings"; import { IClientEncoding } from "../interfaces"; -import { Permissions } from "../../constants"; import { + AddressOrEnsSchema, ApplyInstallationParams, ApplyUpdateParams, + ApplyInstallationSchema, + ApplyUninstallationParams, + ApplyUninstallationSchema, ClientCore, DaoAction, + IpfsUriSchema, + Permissions, TokenType, } from "@aragon/sdk-client-common"; import { Interface } from "@ethersproject/abi"; +import { + InitializeFromSchema, + PermissionBaseSchema, + PermissionWithConditionSchema, + RegisterStandardCallbackSchema, + UpgradeToAndCallSchema, + WithdrawErc1155Schema, + WithdrawErc20Schema, + WithdrawErc721Schema, + WithdrawEthSchema, +} from "../schemas"; +import { string } from "yup"; /** * Encoding module the SDK Generic Client @@ -59,9 +73,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddress: string, params: ApplyInstallationParams, ): DaoAction[] { - if (!isAddress(daoAddress)) { - throw new InvalidAddressError(); - } + AddressOrEnsSchema.strict().validateSync(daoAddress); + ApplyInstallationSchema.strict().validateSync(params); const pspInterface = PluginSetupProcessor__factory.createInterface(); const args = applyInstallatonParamsToContract(params); @@ -98,6 +111,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddress: string, params: ApplyUninstallationParams, ): DaoAction[] { + AddressOrEnsSchema.strict().validateSync(daoAddress); + ApplyUninstallationSchema.strict().validateSync(params); const pspInterface = PluginSetupProcessor__factory.createInterface(); const args = applyUninstallationParamsToContract(params); const hexBytes = pspInterface.encodeFunctionData("applyUninstallation", [ @@ -183,6 +198,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddress: string, params: GrantPermissionParams, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddress); + PermissionBaseSchema.strict().validateSync(params); const { where, who } = params; if ( !isAddress(where) || !isAddress(who) || !isAddress(daoAddress) @@ -217,6 +234,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddress: string, params: GrantPermissionWithConditionParams, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddress); + PermissionWithConditionSchema.strict().validateSync(params); const { where, who } = params; if ( !isAddress(where) || !isAddress(who) || !isAddress(daoAddress) @@ -255,6 +274,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddress: string, params: RevokePermissionParams, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddress); + PermissionBaseSchema.strict().validateSync(params); const { where, who } = params; if ( !isAddress(where) || !isAddress(who) || !isAddress(daoAddress) @@ -300,11 +321,10 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { let data: string; switch (params.type) { case TokenType.NATIVE: + await WithdrawEthSchema.strict().validate(params); return { to, value: params.amount, data: new Uint8Array() }; case TokenType.ERC20: - if (!params.tokenAddress) { - throw new InvalidAddressError(); - } + await WithdrawErc20Schema.strict().validate(params); iface = new Contract( params.tokenAddress, @@ -320,12 +340,7 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { data: hexToBytes(data), }; case TokenType.ERC721: - if ( - !params.tokenAddress || !params.daoAddressOrEns || - !params.recipientAddressOrEns - ) { - throw new InvalidAddressError(); - } + await WithdrawErc721Schema.strict().validate(params); iface = new Contract( params.tokenAddress, ERC721_ABI, @@ -344,18 +359,7 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { data: hexToBytes(data), }; case TokenType.ERC1155: - if (params.tokenIds.length !== params.amounts.length) { - throw new SizeMismatchError(); - } - if (params.tokenIds.length === 0 || params.amounts.length === 0) { - throw new InvalidParameter("tokenIds or amounts cannot be empty"); - } - if ( - !params.tokenAddress || !params.recipientAddressOrEns || - !params.daoAddressOrEns - ) { - throw new InvalidAddressError(); - } + await WithdrawErc1155Schema.strict().validate(params); iface = new Contract( params.tokenAddress, ERC1155_ABI, @@ -404,6 +408,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, metadataUri: string, ): Promise { + await AddressOrEnsSchema.strict().validate(daoAddressOrEns); + await IpfsUriSchema.strict().validate(metadataUri); let address = daoAddressOrEns; if (!isAddress(daoAddressOrEns)) { const resolvedAddress = await this.web3.getSigner()?.resolveName( @@ -436,6 +442,9 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, daoUri: string, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + string().url().strict().validateSync(daoUri); + const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData("setDaoURI", [daoUri]); return { @@ -456,6 +465,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, params: RegisterStandardCallbackParams, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + RegisterStandardCallbackSchema.strict().validateSync(params); const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData( "registerStandardCallback", @@ -479,6 +490,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, signatureValidator: string, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + AddressOrEnsSchema.strict().validateSync(signatureValidator); const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData("setSignatureValidator", [ signatureValidator, @@ -501,6 +514,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, implementationAddress: string, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + AddressOrEnsSchema.strict().validateSync(implementationAddress); const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData("upgradeTo", [ implementationAddress, @@ -523,6 +538,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, params: UpgradeToAndCallParams, ): DaoAction { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + UpgradeToAndCallSchema.strict().validateSync(params); const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData("upgradeToAndCall", [ params.implementationAddress, @@ -547,6 +564,8 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { daoAddressOrEns: string, params: InitializeFromParams, ) { + AddressOrEnsSchema.strict().validateSync(daoAddressOrEns); + InitializeFromSchema.strict().validateSync(params); const daoInterface = DAO__factory.createInterface(); const hexBytes = daoInterface.encodeFunctionData("initializeFrom", [ params.previousVersion, diff --git a/modules/client/src/internal/client/estimation.ts b/modules/client/src/internal/client/estimation.ts index dade90ca6..af5e3fe93 100644 --- a/modules/client/src/internal/client/estimation.ts +++ b/modules/client/src/internal/client/estimation.ts @@ -5,7 +5,6 @@ import { } from "@aragon/osx-ethers"; import { InvalidAddressOrEnsError, - InvalidSubdomainError, NoProviderError, NotImplementedError, } from "@aragon/sdk-common"; @@ -21,6 +20,7 @@ import { estimateErc1155Deposit, estimateErc20Deposit, estimateErc721Deposit, + estimateNativeDeposit, } from "../utils"; import { isAddress } from "@ethersproject/address"; import { toUtf8Bytes } from "@ethersproject/strings"; @@ -32,9 +32,18 @@ import { prepareGenericUpdateEstimation, PrepareInstallationParams, PrepareUpdateParams, + PrepareInstallationSchema, TokenType, } from "@aragon/sdk-client-common"; import { BigNumber } from "@ethersproject/bignumber"; +import { + CreateDaoSchema, + DepositErc1155Schema, + DepositErc20Schema, + DepositErc721Schema, + DepositEthSchema, + SetAllowanceSchema, +} from "../schemas"; /** * Estimation module the SDK Generic Client @@ -43,6 +52,7 @@ export class ClientEstimation extends ClientCore implements IClientEstimation { public async prepareInstallation( params: PrepareInstallationParams, ): Promise { + await PrepareInstallationSchema.strict().validate(params); return prepareGenericInstallationEstimation(this.web3, params); } /** @@ -53,12 +63,8 @@ export class ClientEstimation extends ClientCore implements IClientEstimation { * @memberof ClientEstimation */ public async createDao(params: CreateDaoParams): Promise { + await CreateDaoSchema.strict().validate(params); const provider = this.web3.getProvider(); - if ( - params.ensSubdomain && !params.ensSubdomain.match(/^[a-z0-9\-]+$/) - ) { - throw new InvalidSubdomainError(); - } const daoInstance = DAOFactory__factory.connect( this.web3.getAddress("daoFactoryAddress"), @@ -108,13 +114,19 @@ export class ClientEstimation extends ClientCore implements IClientEstimation { let estimation: BigNumber; switch (params.type) { case TokenType.NATIVE: + await DepositEthSchema.strict().validate(params); + estimation = await estimateNativeDeposit(signer, params); + break; case TokenType.ERC20: + await DepositErc20Schema.strict().validate(params); estimation = await estimateErc20Deposit(signer, params); break; case TokenType.ERC721: + await DepositErc721Schema.strict().validate(params); estimation = await estimateErc721Deposit(signer, params); break; case TokenType.ERC1155: + await DepositErc1155Schema.strict().validate(params); estimation = await estimateErc1155Deposit(signer, params); break; default: @@ -135,6 +147,7 @@ export class ClientEstimation extends ClientCore implements IClientEstimation { public async setAllowance( params: SetAllowanceParams, ): Promise { + await SetAllowanceSchema.strict().validate(params); const signer = this.web3.getConnectedSigner(); // resolve ens let daoAddress = params.spender; diff --git a/modules/client/src/internal/client/methods.ts b/modules/client/src/internal/client/methods.ts index aac8cb44d..55a8b40ef 100644 --- a/modules/client/src/internal/client/methods.ts +++ b/modules/client/src/internal/client/methods.ts @@ -12,11 +12,8 @@ import { DaoCreationError, FailedDepositError, InstallationNotFoundError, - InvalidAddressError, InvalidAddressOrEnsError, InvalidCidError, - InvalidEnsError, - InvalidParameter, IpfsPinError, MissingExecPermissionError, NoProviderError, @@ -25,7 +22,6 @@ import { promiseWithTimeout, ProposalNotFoundError, resolveIpfsCid, - SizeMismatchError, UpdateAllowanceError, } from "@aragon/sdk-common"; @@ -81,9 +77,6 @@ import { PluginSortBy, PluginUpdateProposalInValidityCause, PluginUpdateProposalValidity, - PrepareUninstallationParams, - PrepareUninstallationSteps, - PrepareUninstallationStepValue, RevokePermissionDecodedParams, SetAllowanceParams, SetAllowanceSteps, @@ -140,8 +133,8 @@ import { UNSUPPORTED_RELEASE_METADATA_LINK, } from "../constants"; import { IClientMethods } from "../interfaces"; -import { PermissionIds, Permissions } from "../../constants"; import { + AddressOrEnsSchema, ClientCore, DaoAction, DecodedApplyUpdateParams, @@ -150,16 +143,35 @@ import { LIVE_CONTRACTS, MULTI_FETCH_TIMEOUT, MultiTargetPermission, + PermissionIds, + Permissions, prepareGenericInstallation, prepareGenericUpdate, PrepareInstallationParams, + PrepareInstallationSchema, PrepareInstallationStepValue, + PrepareUninstallationParams, + PrepareUninstallationSchema, + PrepareUninstallationSteps, + PrepareUninstallationStepValue, PrepareUpdateParams, PrepareUpdateStepValue, SortDirection, SupportedVersion, TokenType, } from "@aragon/sdk-client-common"; +import { + CreateDaoSchema, + DaoBalancesQuerySchema, + DaoMetadataSchema, + DaoQuerySchema, + DepositErc1155Schema, + DepositErc20Schema, + DepositErc721Schema, + DepositEthSchema, + HasPermissionSchema, + PluginQuerySchema, +} from "../schemas"; /** * Methods module the SDK Generic Client @@ -168,6 +180,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { public async *prepareInstallation( params: PrepareInstallationParams, ): AsyncGenerator { + await PrepareInstallationSchema.strict().validate(params); yield* prepareGenericInstallation(this.web3, { ...params, pluginSetupProcessorAddress: this.web3.getAddress( @@ -177,7 +190,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { } /** * Creates a DAO with the given settings and plugins - * + * @param {CreateDaoParams} params * @return {*} {AsyncGenerator} * @memberof ClientMethods @@ -185,12 +198,8 @@ export class ClientMethods extends ClientCore implements IClientMethods { public async *createDao( params: CreateDaoParams, ): AsyncGenerator { + await CreateDaoSchema.strict().validate(params); const signer = this.web3.getConnectedSigner(); - if ( - params.ensSubdomain && !params.ensSubdomain.match(/^[a-z0-9\-]+$/) - ) { - throw new InvalidEnsError(); - } const daoFactoryInstance = DAOFactory__factory.connect( this.web3.getAddress("daoFactoryAddress"), @@ -303,6 +312,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { * @memberof ClientMethods */ public async pinMetadata(params: DaoMetadata): Promise { + await DaoMetadataSchema.strict().validate(params); try { const cid = await this.ipfs.add(JSON.stringify(params)); await this.ipfs.pin(cid); @@ -344,6 +354,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { private async *depositNative( params: DepositEthParams, ): AsyncGenerator { + await DepositEthSchema.strict().validate(params); const signer = this.web3.getConnectedSigner(); const { daoAddressOrEns, amount } = params; const override: { value?: bigint } = { value: params.amount }; @@ -378,6 +389,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { private async *depositErc20( params: DepositErc20Params, ): AsyncGenerator { + await DepositErc20Schema.strict().validate(params); const signer = this.web3.getConnectedSigner(); const { tokenAddress, daoAddressOrEns, amount } = params; // check current allowance @@ -437,6 +449,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { private async *depositErc721( params: DepositErc721Params, ): AsyncGenerator { + await DepositErc721Schema.strict().validate(params) as DepositErc721Params; const signer = this.web3.getConnectedSigner(); const erc721Contract = new Contract( params.tokenAddress, @@ -474,17 +487,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { private async *depositErc1155( params: DepositErc1155Params, ): AsyncGenerator { - // if length is 0, throw - if (!params.tokenIds.length || !params.amounts.length) { - throw new InvalidParameter("tokenIds or amounts cannot be empty"); - } - // if tokenIds and amounts length are different, throw - if ( - params.tokenIds.length !== params.amounts.length - ) { - throw new SizeMismatchError(); - } - + await DepositErc1155Schema.strict().validate(params); const signer = this.web3.getConnectedSigner(); const erc1155Contract = new Contract( params.tokenAddress, @@ -593,6 +596,9 @@ export class ClientMethods extends ClientCore implements IClientMethods { public async *prepareUninstallation( params: PrepareUninstallationParams, ): AsyncGenerator { + await PrepareUninstallationSchema.strict().validate( + params, + ); const signer = this.web3.getConnectedSigner(); type T = { iplugin: { installations: SubgraphPluginInstallation[] }; @@ -701,6 +707,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { * @memberof ClientMethods */ public async hasPermission(params: HasPermissionParams): Promise { + await HasPermissionSchema.strict().validate(params); const provider = this.web3.getProvider(); // connect to the managing dao const daoInstance = DAO__factory.connect(params.daoAddressOrEns, provider); @@ -719,6 +726,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { * @memberof ClientMethods */ public async getDao(daoAddressOrEns: string): Promise { + await AddressOrEnsSchema.strict().validate(daoAddressOrEns); let address = daoAddressOrEns.toLowerCase(); if (!isAddress(address)) { await this.web3.ensureOnline(); @@ -779,6 +787,12 @@ export class ClientMethods extends ClientCore implements IClientMethods { direction = SortDirection.ASC, sortBy = DaoSortBy.CREATED_AT, }: DaoQueryParams): Promise { + await DaoQuerySchema.strict().validate({ + limit, + skip, + direction, + sortBy, + }); const query = QueryDaos; const params = { limit, @@ -837,6 +851,13 @@ export class ClientMethods extends ClientCore implements IClientMethods { direction = SortDirection.ASC, sortBy = AssetBalanceSortBy.LAST_UPDATED, }: DaoBalancesQueryParams): Promise { + await DaoBalancesQuerySchema.strict().validate({ + daoAddressOrEns, + limit, + skip, + direction, + sortBy, + }); let where = {}; let address = daoAddressOrEns; if (address) { @@ -966,6 +987,14 @@ export class ClientMethods extends ClientCore implements IClientMethods { sortBy = PluginSortBy.SUBDOMAIN, subdomain, }: PluginQueryParams = {}): Promise { + await PluginQuerySchema.strict().validate({ + limit, + skip, + direction, + sortBy, + subdomain, + }); + let where = {}; if (subdomain) { where = { subdomain_contains_nocase: subdomain }; @@ -1030,6 +1059,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { * @memberof ClientMethods */ public async getPlugin(pluginAddress: string): Promise { + await AddressOrEnsSchema.strict().validate(pluginAddress); const name = "plugin version"; const query = QueryPlugin; type T = { pluginRepo: SubgraphPluginRepo }; @@ -1087,9 +1117,7 @@ export class ClientMethods extends ClientCore implements IClientMethods { public async getProtocolVersion( contractAddress: string, ): Promise<[number, number, number]> { - if (!isAddress(contractAddress)) { - throw new InvalidAddressError(); - } + await AddressOrEnsSchema.strict().validate(contractAddress); const provider = this.web3.getProvider(); const protocolInstance = IProtocolVersion__factory.connect( contractAddress, diff --git a/modules/client/src/internal/schemas.ts b/modules/client/src/internal/schemas.ts new file mode 100644 index 000000000..ce8d0eb82 --- /dev/null +++ b/modules/client/src/internal/schemas.ts @@ -0,0 +1,165 @@ +import { + AddressOrEnsSchema, + BigintSchema, + IpfsUriSchema, + PaginationSchema, + PluginInstallItemSchema, + SubdomainSchema, + Uint8ArraySchema, +} from "@aragon/sdk-client-common"; +import { SizeMismatchError } from "@aragon/sdk-common"; +import { array, mixed, number, object, string } from "yup"; + +export const CreateDaoSchema = object({ + metadataUri: IpfsUriSchema.required(), + daoUri: string().url().notRequired(), + ensSubdomain: SubdomainSchema.required(), + trustedForwarder: AddressOrEnsSchema.notRequired(), + plugins: array(PluginInstallItemSchema).min(1).required(), +}); + +export const DaoMetadataSchema = object({ + name: string().required(), + description: string().required(), + avatar: mixed().test((item) => { + return [IpfsUriSchema, string().url()].some((schema) => + schema.strict().isValidSync(item) + ); + }).notRequired(), + links: array(object({ + name: string().required(), + url: string().required(), + })).required(), +}); + +export const DepositEthSchema = object({ + type: string().required().oneOf(["native"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + amount: BigintSchema.required(), +}); + +export const DepositErc20Schema = object({ + type: string().required().oneOf(["erc20"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + amount: BigintSchema.required(), +}); + +export const DepositErc721Schema = object({ + type: string().required().oneOf(["erc721"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + tokenId: BigintSchema.required(), +}); + +export const DepositErc1155Schema = object({ + type: string().required().oneOf(["erc1155"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + tokenIds: array(BigintSchema).required().min(1), + amounts: array(BigintSchema).required().min(1), +}).test( + "isSameLength", + new SizeMismatchError("tokenIds", "amounts").message, + function (value) { + const v = value as any; + return v.tokenIds && v.amounts + ? v.tokenIds.length === v.amounts.length + : true; + }, +); + +export const SetAllowanceSchema = object({ + tokenAddress: AddressOrEnsSchema.required(), + amount: BigintSchema.required(), + spender: AddressOrEnsSchema.required(), +}); + +export const HasPermissionSchema = object({ + who: AddressOrEnsSchema.required(), + where: AddressOrEnsSchema.required(), + permission: string().required(), + daoAddressOrEns: AddressOrEnsSchema.required(), + data: Uint8ArraySchema.notRequired(), +}); + +export const DaoQuerySchema = PaginationSchema.concat(object({ + sortBy: string().notRequired().oneOf(["createdAt", "subdomain"]), +})); + +export const DaoBalancesQuerySchema = PaginationSchema.concat(object({ + sortBy: string().notRequired().oneOf(["lastUpdated"]), + daoAddressOrEns: AddressOrEnsSchema.notRequired(), +})); + +export const PluginQuerySchema = PaginationSchema.concat(object({ + sortBy: string().notRequired().oneOf(["subdomain"]), + subdomain: SubdomainSchema.notRequired(), +})); + +export const PermissionBaseSchema = object({ + who: AddressOrEnsSchema.required(), + where: AddressOrEnsSchema.required(), + permission: string().required(), +}); + +export const PermissionWithConditionSchema = PermissionBaseSchema.concat( + object({ + condition: AddressOrEnsSchema.required(), + }), +); + +export const WithdrawEthSchema = object({ + type: string().required().oneOf(["native"]), + recipientAddressOrEns: AddressOrEnsSchema.required(), + amount: BigintSchema.required(), +}); + +export const WithdrawErc20Schema = object({ + type: string().required().oneOf(["erc20"]), + recipientAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + amount: BigintSchema.required(), +}); + +export const WithdrawErc721Schema = object({ + type: string().required().oneOf(["erc721"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + recipientAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + tokenId: BigintSchema.required(), +}); + +export const WithdrawErc1155Schema = object({ + type: string().required().oneOf(["erc1155"]), + daoAddressOrEns: AddressOrEnsSchema.required(), + recipientAddressOrEns: AddressOrEnsSchema.required(), + tokenAddress: AddressOrEnsSchema.required(), + tokenIds: array(BigintSchema).required().min(1), + amounts: array(BigintSchema).required().min(1), +}).test( + "isSameLength", + new SizeMismatchError("tokenIds", "amounts").message, + function (value) { + const v = value as any; + return v.tokenIds && v.amounts + ? v.tokenIds.length === v.amounts.length + : true; + }, +); + +export const RegisterStandardCallbackSchema = object({ + interfaceId: string().required(), + callbackSelector: string().required(), + magicNumber: string().required(), +}); + +export const UpgradeToAndCallSchema = object({ + implementationAddress: AddressOrEnsSchema.required(), + data: Uint8ArraySchema.required(), +}); + +export const InitializeFromSchema = object({ + previousVersion: array().of(number()).length(3).required(), + initData: Uint8ArraySchema.notRequired(), +}); diff --git a/modules/client/src/internal/utils.ts b/modules/client/src/internal/utils.ts index dfdb9e0b7..db78295ee 100644 --- a/modules/client/src/internal/utils.ts +++ b/modules/client/src/internal/utils.ts @@ -1,5 +1,4 @@ import { - ApplyUninstallationParams, AssetBalance, DaoDetails, DaoListItem, @@ -56,9 +55,15 @@ import { PluginSetupProcessor, PluginSetupProcessor__factory, } from "@aragon/osx-ethers"; -import { PermissionIds } from "../constants"; +import { + bytesToHex, + hexToBytes, + InvalidParameter, + NotImplementedError, +} from "@aragon/sdk-common"; import { ApplyInstallationParams, + ApplyUninstallationParams, ApplyUpdateParams, DaoAction, DecodedApplyInstallationParams, @@ -66,15 +71,9 @@ import { getFunctionFragment, InterfaceParams, MultiTargetPermission, + PermissionIds, TokenType, } from "@aragon/sdk-client-common"; -import { - bytesToHex, - hexToBytes, - InvalidParameter, - NotImplementedError, - SizeMismatchError, -} from "@aragon/sdk-common"; import { Signer } from "@ethersproject/abstract-signer"; import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; @@ -82,6 +81,12 @@ import { abi as ERC721_ABI } from "@openzeppelin/contracts/build/contracts/ERC72 import { abi as ERC1155_ABI } from "@openzeppelin/contracts/build/contracts/ERC1155.json"; import { SubgraphAction } from "../client-common"; import { PreparationType, ZERO_BYTES_HASH } from "./constants"; +import { + DepositErc1155Schema, + DepositErc20Schema, + DepositErc721Schema, + DepositEthSchema, +} from "./schemas"; export function unwrapDepositParams( params: DepositEthParams | DepositErc20Params, @@ -582,19 +587,27 @@ export function withdrawParamsFromContract( throw new NotImplementedError("Token standard not supported"); } +export async function estimateNativeDeposit( + signer: Signer, + params: DepositEthParams, +): Promise { + await DepositEthSchema.strict().validate(params); + const daoInstance = DAO__factory.connect(params.daoAddressOrEns, signer); + return await daoInstance.estimateGas.deposit( + AddressZero, + params.amount, + "", + ); +} + export async function estimateErc20Deposit( signer: Signer, - params: DepositErc20Params | DepositEthParams, + params: DepositErc20Params, ): Promise { - let tokenAddress; - if (params.type === TokenType.NATIVE) { - tokenAddress = AddressZero; - } else { - tokenAddress = params.tokenAddress; - } + await DepositErc20Schema.strict().validate(params); const daoInstance = DAO__factory.connect(params.daoAddressOrEns, signer); return await daoInstance.estimateGas.deposit( - tokenAddress, + params.tokenAddress, params.amount, "", ); @@ -604,6 +617,7 @@ export async function estimateErc721Deposit( signer: Signer, params: DepositErc721Params, ): Promise { + await DepositErc721Schema.strict().validate(params); const erc721Contract = new Contract( params.tokenAddress, ERC721_ABI, @@ -621,16 +635,7 @@ export async function estimateErc1155Deposit( signer: Signer, params: DepositErc1155Params, ): Promise { - // if length is 0, throw - if (!params.tokenIds.length || !params.amounts.length) { - throw new InvalidParameter("tokenIds or amounts cannot be empty"); - } - // if tokenIds and amounts length are different, throw - if ( - params.tokenIds.length !== params.amounts.length - ) { - throw new SizeMismatchError(); - } + await DepositErc1155Schema.strict().validate(params); const erc1155Contract = new Contract( params.tokenAddress, ERC1155_ABI, diff --git a/modules/client/src/multisig/internal/client/estimation.ts b/modules/client/src/multisig/internal/client/estimation.ts index cb2f9efd7..7d78b35bf 100644 --- a/modules/client/src/multisig/internal/client/estimation.ts +++ b/modules/client/src/multisig/internal/client/estimation.ts @@ -42,7 +42,7 @@ export class MultisigClientEstimation extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/multisig/internal/client/methods.ts b/modules/client/src/multisig/internal/client/methods.ts index 5442a6bfe..94d8cc901 100644 --- a/modules/client/src/multisig/internal/client/methods.ts +++ b/modules/client/src/multisig/internal/client/methods.ts @@ -105,7 +105,7 @@ export class MultisigClientMethods extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/tokenVoting/internal/client/estimation.ts b/modules/client/src/tokenVoting/internal/client/estimation.ts index 9f8338550..1f5fbe6aa 100644 --- a/modules/client/src/tokenVoting/internal/client/estimation.ts +++ b/modules/client/src/tokenVoting/internal/client/estimation.ts @@ -48,7 +48,7 @@ export class TokenVotingClientEstimation extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/tokenVoting/internal/client/methods.ts b/modules/client/src/tokenVoting/internal/client/methods.ts index 04ae612d5..0b480b6bb 100644 --- a/modules/client/src/tokenVoting/internal/client/methods.ts +++ b/modules/client/src/tokenVoting/internal/client/methods.ts @@ -139,7 +139,7 @@ export class TokenVotingClientMethods extends ClientCore params.failSafeActions?.length && params.failSafeActions.length !== params.actions?.length ) { - throw new SizeMismatchError(); + throw new SizeMismatchError("failSafeActions", "actions"); } const allowFailureMap = boolArrayToBitmap(params.failSafeActions); diff --git a/modules/client/src/types.ts b/modules/client/src/types.ts index f713abaf9..9cb7ddb67 100644 --- a/modules/client/src/types.ts +++ b/modules/client/src/types.ts @@ -1,5 +1,4 @@ import { - ApplyInstallationParamsBase, MetadataAbiInput, Pagination, PluginInstallItem, @@ -366,27 +365,6 @@ export type SetAllowanceStepValue = | { key: SetAllowanceSteps.SETTING_ALLOWANCE; txHash: string } | { key: SetAllowanceSteps.ALLOWANCE_SET; allowance: bigint }; -/* Uninstallation */ -export type PrepareUninstallationParams = { - daoAddressOrEns: string; - pluginAddress: string; - pluginInstallationIndex?: number; - uninstallationParams?: any[]; - uninstallationAbi?: string[]; -}; -export enum PrepareUninstallationSteps { - PREPARING = "preparing", - DONE = "done", -} -export type PrepareUninstallationStepValue = - | { key: PrepareUninstallationSteps.PREPARING; txHash: string } - | { - key: PrepareUninstallationSteps.DONE; - } & ApplyUninstallationParams; - -export type ApplyUninstallationParams = ApplyInstallationParamsBase; -export type DecodedApplyUninstallationParams = ApplyInstallationParamsBase; - /* Permissions */ type PermissionParamsBase = { where: string; diff --git a/modules/client/test/constants.ts b/modules/client/test/constants.ts new file mode 100644 index 000000000..98f89223c --- /dev/null +++ b/modules/client/test/constants.ts @@ -0,0 +1,19 @@ +export const TEST_ADDRESS = "0x0000000000000000000000000000000000000001"; +export const TEST_INVALID_ADDRESS = + "0x000000000000000000000000000000000000000P"; + +export const TEST_ENS_NAME = "test.eth"; +export const TEST_INVALID_ENS_NAME = "test.invalid"; + +export const TEST_IPFS_URI_V0 = + "ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"; +export const TEST_IPFS_URI_V1 = + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"; +export const TEST_INVALID_IPFS_URI = + "ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR-invalid"; + +export const TEST_HTTP_URI = "https://test.com"; +export const TEST_INVALID_HTTP_URI = "https://te?st.com-invalid"; + +export const TEST_SUBDOMAIN = "test"; +export const TEST_INVALID_SUBDOMAIN = "test.invalid"; diff --git a/modules/client/test/helpers/build-daos.ts b/modules/client/test/helpers/build-daos.ts index 58240afe5..c02cea32b 100644 --- a/modules/client/test/helpers/build-daos.ts +++ b/modules/client/test/helpers/build-daos.ts @@ -15,6 +15,7 @@ import { } from "../integration/constants"; import { TokenVoting__factory } from "@aragon/osx-ethers"; import { Context } from "@aragon/sdk-client-common"; +import { TEST_HTTP_URI, TEST_IPFS_URI_V0 } from "../constants"; const NETWORK_NAME = "local"; @@ -32,14 +33,14 @@ export async function buildMultisigDAO(pluginRepoAddress: string) { const createDaoParams: CreateDaoParams = { ensSubdomain: "teting-" + Math.random().toString().slice(2), - metadataUri: "ipfs://", + metadataUri: TEST_IPFS_URI_V0, plugins: [ { id: pluginRepoAddress, // TODO: Rename data: pluginInstallItem.data, }, ], - daoUri: "https://", + daoUri: TEST_HTTP_URI, trustedForwarder: AddressZero, }; for await ( @@ -87,14 +88,14 @@ export async function buildTokenVotingDAO( const createDaoParams: CreateDaoParams = { ensSubdomain: "teting-" + Math.random().toString().slice(2), - metadataUri: "ipfs://", + metadataUri: TEST_IPFS_URI_V0, plugins: [ { id: pluginRepoAddress, // TODO: Rename data: pluginInstallItem.data, }, ], - daoUri: "https://", + daoUri: TEST_HTTP_URI, trustedForwarder: AddressZero, }; let dao, plugin; @@ -150,14 +151,14 @@ export async function buildExistingTokenVotingDAO( const createDaoParams: CreateDaoParams = { ensSubdomain: "teting-" + Math.random().toString().slice(2), - metadataUri: "ipfs://", + metadataUri: TEST_IPFS_URI_V0, plugins: [ { id: pluginRepoAddress, // TODO: Rename data: pluginInstallItem.data, }, ], - daoUri: "https://", + daoUri: TEST_HTTP_URI, trustedForwarder: AddressZero, }; for await ( @@ -196,14 +197,14 @@ export async function buildAddressListVotingDAO( const createDaoParams: CreateDaoParams = { ensSubdomain: "teting-" + Math.random().toString().slice(2), - metadataUri: "ipfs://", + metadataUri: TEST_IPFS_URI_V0, plugins: [ { id: pluginRepoAddress, // TODO: Rename data: pluginInstallItem.data, }, ], - daoUri: "https://", + daoUri: TEST_HTTP_URI, trustedForwarder: AddressZero, }; for await ( diff --git a/modules/client/test/integration/client/decoding.test.ts b/modules/client/test/integration/client/decoding.test.ts index 30fe26c4f..d581dd90c 100644 --- a/modules/client/test/integration/client/decoding.test.ts +++ b/modules/client/test/integration/client/decoding.test.ts @@ -5,15 +5,12 @@ declare const describe, it, expect; import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs"; import { - ApplyUninstallationParams, Client, DaoMetadata, GrantPermissionDecodedParams, GrantPermissionParams, GrantPermissionWithConditionParams, InitializeFromParams, - PermissionIds, - Permissions, RegisterStandardCallbackParams, RevokePermissionDecodedParams, RevokePermissionParams, @@ -34,8 +31,11 @@ import { AddressZero } from "@ethersproject/constants"; import { ApplyInstallationParams, ApplyUpdateParams, + ApplyUninstallationParams, Context, + PermissionIds, PermissionOperationType, + Permissions, TokenType, } from "@aragon/sdk-client-common"; diff --git a/modules/client/test/integration/client/encoding.test.ts b/modules/client/test/integration/client/encoding.test.ts index b83d65f15..9e5ceaa67 100644 --- a/modules/client/test/integration/client/encoding.test.ts +++ b/modules/client/test/integration/client/encoding.test.ts @@ -9,13 +9,10 @@ import { } from "@aragon/osx-ethers"; import { - ApplyUninstallationParams, Client, DaoMetadata, GrantPermissionParams, InitializeFromParams, - PermissionIds, - Permissions, RegisterStandardCallbackParams, RevokePermissionParams, UpgradeToAndCallParams, @@ -34,8 +31,11 @@ import { AddressZero } from "@ethersproject/constants"; import { ApplyInstallationParams, ApplyUpdateParams, + ApplyUninstallationParams, Context, DaoAction, + PermissionIds, + Permissions, TokenType, } from "@aragon/sdk-client-common"; diff --git a/modules/client/test/integration/client/methods.test.ts b/modules/client/test/integration/client/methods.test.ts index ec2e6d380..ca0c0f208 100644 --- a/modules/client/test/integration/client/methods.test.ts +++ b/modules/client/test/integration/client/methods.test.ts @@ -35,11 +35,9 @@ import { DepositParams, HasPermissionParams, InitializeFromParams, - Permissions, PluginQueryParams, PluginSortBy, PluginUpdateProposalInValidityCause, - PrepareUninstallationSteps, SetAllowanceParams, SetAllowanceSteps, SubgraphAction, @@ -50,8 +48,6 @@ import { VotingMode, } from "../../../src"; import { - InvalidAddressError, - MissingExecPermissionError, ProposalNotFoundError, } from "@aragon/sdk-common"; import { Server } from "ganache"; @@ -81,8 +77,10 @@ import { Context, DaoAction, LIVE_CONTRACTS, - PrepareInstallationStep, PrepareUpdateStep, + Permissions, + PrepareInstallationStep, + PrepareUninstallationSteps, SortDirection, SupportedVersion, TokenType, @@ -97,6 +95,7 @@ import { import { JsonRpcProvider } from "@ethersproject/providers"; import { toSubgraphAction } from "../../helpers/subgraph"; import { SupportedPluginRepo } from "../../../src/internal/constants"; +import { ValidationError } from "yup"; describe("Client", () => { let daoAddress: string; @@ -166,7 +165,6 @@ describe("Client", () => { const ipfsUri = await client.methods.pinMetadata({ name: daoName, description: "this is a dao", - avatar: "https://...", links: [], }); const pluginParams: AddresslistVotingPluginInstall = { @@ -212,7 +210,7 @@ describe("Client", () => { } }); - it("should fail if no execute_permission is requested", async () => { + it("should fail if no plugins are specified", async () => { const context = new Context(contextParamsLocalChain); const client = new Client(context); @@ -226,7 +224,7 @@ describe("Client", () => { }; await expect(client.methods.createDao(daoCreationParams).next()).rejects - .toMatchObject(new MissingExecPermissionError()); + .toEqual(new ValidationError("plugins field must have at least 1 items")); }); }); @@ -335,6 +333,7 @@ describe("Client", () => { expect(await erc721Contract.ownerOf(tokenId)).toBe( await client.web3.getConnectedSigner().getAddress(), ); + const steps = client.methods.deposit({ type: TokenType.ERC721, daoAddressOrEns: daoAddress, @@ -1674,7 +1673,7 @@ describe("Client", () => { const client = new Client(ctx); const daoAddress = TEST_INVALID_ADDRESS; await expect(() => client.methods.getProtocolVersion(daoAddress)) - .rejects.toThrow(InvalidAddressError); + .rejects.toThrow(ValidationError); }); it("Should get the protocol version of a dao", async () => { diff --git a/modules/client/test/unit/client/schemas.test.ts b/modules/client/test/unit/client/schemas.test.ts new file mode 100644 index 000000000..b06c88343 --- /dev/null +++ b/modules/client/test/unit/client/schemas.test.ts @@ -0,0 +1,1876 @@ +import { + InvalidAddressOrEnsError, + InvalidCidError, + InvalidParameter, + InvalidSubdomainError, + SizeMismatchError, +} from "@aragon/sdk-common"; +import { + CreateDaoSchema, + DaoBalancesQuerySchema, + DaoMetadataSchema, + DaoQuerySchema, + DepositErc1155Schema, + DepositErc20Schema, + DepositErc721Schema, + DepositEthSchema, + InitializeFromSchema, + PermissionBaseSchema, + PermissionWithConditionSchema, + PluginQuerySchema, + RegisterStandardCallbackSchema, + SetAllowanceSchema, + UpgradeToAndCallSchema, + WithdrawErc1155Schema, + WithdrawErc20Schema, + WithdrawErc721Schema, + WithdrawEthSchema, +} from "../../../src/internal/schemas"; +import { + TEST_ADDRESS, + TEST_HTTP_URI, + TEST_INVALID_ADDRESS, + TEST_INVALID_IPFS_URI, + TEST_INVALID_SUBDOMAIN, + TEST_IPFS_URI_V0, + TEST_SUBDOMAIN, +} from "../../constants"; +import { + Permissions, + SortDirection, + TokenType, +} from "@aragon/sdk-client-common"; +import { + AssetBalanceSortBy, + CreateDaoParams, + DaoBalancesQueryParams, + DaoMetadata, + DaoQueryParams, + DaoSortBy, + DepositParams, + GrantPermissionParams, + GrantPermissionWithConditionParams, + InitializeFromParams, + PluginQueryParams, + PluginSortBy, + RegisterStandardCallbackParams, + SetAllowanceParams, + UpgradeToAndCallParams, + WithdrawParams, +} from "../../../src"; +import { ValidationError } from "yup"; + +describe("Test client schemas", () => { + describe("Test CreateDaoParams", () => { + it("should validate a valid CreateDaoParams", () => { + const createDaoParams: CreateDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + daoUri: TEST_HTTP_URI, + ensSubdomain: TEST_SUBDOMAIN, + trustedForwarder: TEST_ADDRESS, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + CreateDaoSchema.strict().validateSync(createDaoParams); + }); + it("should validate a valid CreateDaoParams without optional params", () => { + const createDaoParams: CreateDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + ensSubdomain: TEST_SUBDOMAIN, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + CreateDaoSchema.strict().validateSync(createDaoParams); + }); + it("should throw an error if the metadataUri is missing", () => { + const createDaoParams = { + ensSubdomain: TEST_SUBDOMAIN, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError("metadataUri is a required field"), + ); + }); + it("should throw an error if the ensSubdomain is missing", () => { + const createDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError("ensSubdomain is a required field"), + ); + }); + it("should throw an error if the plugins is missing", () => { + const createDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + ensSubdomain: TEST_SUBDOMAIN, + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError("plugins is a required field"), + ); + }); + it("should throw an error if the plugins length is 0", () => { + const createDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + ensSubdomain: TEST_SUBDOMAIN, + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError("plugins is a required field"), + ); + }); + it("should throw an error if the metadataUri is invalid", () => { + const createDaoParams: CreateDaoParams = { + metadataUri: TEST_INVALID_IPFS_URI, + ensSubdomain: TEST_SUBDOMAIN, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError(new InvalidCidError().message), + ); + }); + it("should throw an error if the ensSubdomain is invalid", () => { + const createDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + ensSubdomain: TEST_INVALID_SUBDOMAIN, + plugins: [ + { + id: TEST_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError(new InvalidSubdomainError().message), + ); + }); + it("should throw an error if the plugins are invalid", () => { + const createDaoParams = { + metadataUri: TEST_IPFS_URI_V0, + ensSubdomain: TEST_SUBDOMAIN, + plugins: [ + { + id: TEST_INVALID_ADDRESS, + data: new Uint8Array(), + }, + ], + }; + expect(() => CreateDaoSchema.strict().validateSync(createDaoParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Test DaoMetadata", () => { + it("should validate a valid DaoMetadata", () => { + const daoMetadata: DaoMetadata = { + name: "test", + description: "test", + avatar: TEST_IPFS_URI_V0, + links: [ + { + name: "test", + url: TEST_HTTP_URI, + }, + ], + }; + DaoMetadataSchema.strict().validateSync(daoMetadata); + }); + it("should validate a valid DaoMetadata without optional params", () => { + const daoMetadata: DaoMetadata = { + name: "test", + description: "test", + links: [ + { + name: "test", + url: TEST_HTTP_URI, + }, + ], + }; + DaoMetadataSchema.strict().validateSync(daoMetadata); + }); + it("should throw an error if the name is missing", () => { + const daoMetadata = { + description: "test", + links: [ + { + name: "test", + url: TEST_HTTP_URI, + }, + ], + }; + expect(() => DaoMetadataSchema.strict().validateSync(daoMetadata)) + .toThrow( + new ValidationError("name is a required field"), + ); + }); + it("should throw an error if the description is missing", () => { + const daoMetadata = { + name: "test", + links: [ + { + name: "test", + url: TEST_HTTP_URI, + }, + ], + }; + expect(() => DaoMetadataSchema.strict().validateSync(daoMetadata)) + .toThrow( + new ValidationError("description is a required field"), + ); + }); + it("should throw an error if the links is missing", () => { + const daoMetadata = { + name: "test", + description: "test", + }; + expect(() => DaoMetadataSchema.strict().validateSync(daoMetadata)) + .toThrow( + new ValidationError("links is a required field"), + ); + }); + }); + describe("Test DepositParams", () => { + describe("Test DepositNativeParams", () => { + it("should validate a valid DepositNativeParams", () => { + const depositNativeParams: DepositParams = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + amount: BigInt(1), + }; + DepositEthSchema.strict().validateSync(depositNativeParams); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const depositNativeParams = { + type: TokenType.NATIVE, + amount: BigInt(1), + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the amount is missing", () => { + const depositNativeParams = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError("amount is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const depositNativeParams: DepositParams = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_INVALID_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the amount is invalid", () => { + const depositNativeParams = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + amount: 1, + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const depositNativeParams = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: native", + ), + ); + }); + it("should throw an error if the type is missing", () => { + const depositNativeParams = { + daoAddressOrEns: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositEthSchema.strict().validateSync(depositNativeParams) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + }); + describe("Test DepositErc20Params", () => { + it("should validate a valid DepositErc20Params", () => { + const depositErc20Params: DepositParams = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + DepositErc20Schema.strict().validateSync(depositErc20Params); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const depositErc20Params = { + type: TokenType.ERC20, + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const depositErc20Params = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the amount is missing", () => { + const depositErc20Params = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError("amount is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const depositErc20Params: DepositParams = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const depositErc20Params: DepositParams = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the amount is invalid", () => { + const depositErc20Params = { + type: TokenType.ERC20, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amount: 1, + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const depositErc20Params = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc20", + ), + ); + }); + it("should throw an error if the type is missing", () => { + const depositErc20Params = { + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => + DepositErc20Schema.strict().validateSync(depositErc20Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + }); + describe("Test DepositErc721Params", () => { + it("should validate a valid DepositErc721Params", () => { + const depositErc721Params: DepositParams = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + DepositErc721Schema.strict().validateSync(depositErc721Params); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const depositErc721Params = { + type: TokenType.ERC721, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const depositErc721Params = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the tokenId is missing", () => { + const depositErc721Params = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError("tokenId is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const depositErc721Params: DepositParams = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const depositErc721Params: DepositParams = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenId is invalid", () => { + const depositErc721Params = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: 1, + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const depositErc721Params = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc721", + ), + ); + }); + it("should throw an error if the type is missing", () => { + const depositErc721Params = { + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + DepositErc721Schema.strict().validateSync(depositErc721Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + }); + describe("Test DepositErc1155Params", () => { + it("should validate a valid DepositErc1155Params", () => { + const depositErc1155Params: DepositParams = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + DepositErc1155Schema.strict().validateSync(depositErc1155Params); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the tokenIds is missing", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError("tokenIds is a required field"), + ); + }); + it("should throw an error if the amounts is missing", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError("amounts is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const depositErc1155Params: DepositParams = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const depositErc1155Params: DepositParams = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenIds are invalid", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [1], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the amounts are invalid", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [1], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const depositErc1155Params = { + type: TokenType.NATIVE, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc1155", + ), + ); + }); + it("should throw an error if the type is missing", () => { + const depositErc1155Params = { + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + it("should throw an error if the tokenIds length is 0", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [], + amounts: [BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError( + new SizeMismatchError("tokenIds", "amounts").message, + ), + ); + }); + it("should throw an error if the amounts length is 0", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError( + new SizeMismatchError("tokenIds", "amounts").message, + ), + ); + }); + it("should throw an error if the tokenIds or amounts length is 0", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [], + amounts: [], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError( + "amounts field must have at least 1 items", + ), + ); + }); + it("should throw an error if the tokenIds and amounts length are different", () => { + const depositErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1), BigInt(1)], + }; + expect(() => + DepositErc1155Schema.strict().validateSync(depositErc1155Params) + ) + .toThrow( + new ValidationError( + new SizeMismatchError("tokenIds", "amounts").message, + ), + ); + }); + }); + }); + describe("Test SetAllowance", () => { + it("should validate a valid SetAllowance", () => { + const setAllowanceParams: SetAllowanceParams = { + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + spender: TEST_ADDRESS, + }; + SetAllowanceSchema.strict().validateSync(setAllowanceParams); + }); + it("should throw an error if the tokenAddress is missing", () => { + const setAllowanceParams = { + amount: BigInt(1), + spender: TEST_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the amount is missing", () => { + const setAllowanceParams = { + tokenAddress: TEST_ADDRESS, + spender: TEST_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError("amount is a required field"), + ); + }); + it("should throw an error if the spender is missing", () => { + const setAllowanceParams = { + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError("spender is a required field"), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const setAllowanceParams: SetAllowanceParams = { + tokenAddress: TEST_INVALID_ADDRESS, + amount: BigInt(1), + spender: TEST_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the amount is invalid", () => { + const setAllowanceParams = { + tokenAddress: TEST_ADDRESS, + amount: 1, + spender: TEST_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the spender is invalid", () => { + const setAllowanceParams: SetAllowanceParams = { + tokenAddress: TEST_ADDRESS, + amount: BigInt(1), + spender: TEST_INVALID_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const setAllowanceParams = { + amount: BigInt(1), + spender: TEST_ADDRESS, + }; + expect(() => SetAllowanceSchema.strict().validateSync(setAllowanceParams)) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + }); + + describe("Test Dao Query", () => { + it("should validate a valid DaoQuery", () => { + const daoQueryParams: DaoQueryParams = { + sortBy: DaoSortBy.CREATED_AT, + skip: 0, + limit: 1, + direction: SortDirection.ASC, + }; + DaoQuerySchema.strict().validateSync(daoQueryParams); + }); + it("should validate a valid DaoQuery without optional params", () => { + const daoQueryParams: DaoQueryParams = {}; + DaoQuerySchema.strict().validateSync(daoQueryParams); + }); + it("should throw an error if the sortBy is invalid", () => { + const daoQueryParams = { + sortBy: "invalid", + }; + expect(() => DaoQuerySchema.strict().validateSync(daoQueryParams)) + .toThrow( + new ValidationError( + "sortBy must be one of the following values: createdAt, subdomain", + ), + ); + }); + }); + describe("Test DaoBalances Query", () => { + it("should validate a valid DaoBalancesQuery", () => { + const daoBalancesQueryParams: DaoBalancesQueryParams = { + sortBy: AssetBalanceSortBy.LAST_UPDATED, + skip: 0, + limit: 1, + direction: SortDirection.ASC, + daoAddressOrEns: TEST_ADDRESS, + }; + DaoBalancesQuerySchema.strict().validateSync(daoBalancesQueryParams); + }); + it("should validate a valid DaoBalancesQuery without optional params", () => { + const daoBalancesQueryParams: DaoBalancesQueryParams = {}; + DaoBalancesQuerySchema.strict().validateSync(daoBalancesQueryParams); + }); + it("should throw an error if the sortBy is invalid", () => { + const daoBalancesQueryParams = { + sortBy: "invalid", + }; + expect(() => + DaoBalancesQuerySchema.strict().validateSync(daoBalancesQueryParams) + ) + .toThrow( + new ValidationError( + "sortBy must be one of the following values: lastUpdated", + ), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const daoBalancesQueryParams: DaoBalancesQueryParams = { + daoAddressOrEns: TEST_INVALID_ADDRESS, + }; + expect(() => + DaoBalancesQuerySchema.strict().validateSync(daoBalancesQueryParams) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Test PluginQuery Query", () => { + it("should validate a valid PluginQuery", () => { + const pluginQueryParams: PluginQueryParams = { + sortBy: PluginSortBy.SUBDOMAIN, + skip: 0, + limit: 1, + direction: SortDirection.ASC, + subdomain: TEST_SUBDOMAIN, + }; + PluginQuerySchema.strict().validateSync(pluginQueryParams); + }); + it("should validate a valid PluginQuery without optional params", () => { + const pluginQueryParams: PluginQueryParams = {}; + PluginQuerySchema.strict().validateSync(pluginQueryParams); + }); + it("should throw an error if the sortBy is invalid", () => { + const pluginQueryParams = { + sortBy: "invalid", + }; + expect(() => PluginQuerySchema.strict().validateSync(pluginQueryParams)) + .toThrow( + new ValidationError( + "sortBy must be one of the following values: subdomain", + ), + ); + }); + it("should throw an error if the subdomain is invalid", () => { + const pluginQueryParams = { + subdomain: TEST_INVALID_SUBDOMAIN, + }; + expect(() => PluginQuerySchema.strict().validateSync(pluginQueryParams)) + .toThrow( + new ValidationError(new InvalidSubdomainError().message), + ); + }); + }); + + describe("Test Permission Schema", () => { + it("should validate a valid Permission", () => { + const permissionParams: GrantPermissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + where: TEST_ADDRESS, + }; + PermissionBaseSchema.strict().validateSync(permissionParams); + }); + it("should throw an error if the permission is missing", () => { + const permissionParams = { + who: TEST_ADDRESS, + where: TEST_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError("permission is a required field"), + ); + }); + it("should throw an error if the who is missing", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + where: TEST_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError("who is a required field"), + ); + }); + it("should throw an error if the where is missing", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError("where is a required field"), + ); + }); + it("should throw an error if the permission is invalid", () => { + const permissionParams = { + permission: 1, + who: TEST_ADDRESS, + where: TEST_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError( + "permission must be a `string` type, but the final value was: `1`.", + ), + ); + }); + it("should throw an error if the who is invalid", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_INVALID_ADDRESS, + where: TEST_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the where is invalid", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + where: TEST_INVALID_ADDRESS, + }; + expect(() => PermissionBaseSchema.strict().validateSync(permissionParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Permission with condition", () => { + it("should validate a valid Permission with condition", () => { + const permissionParams: GrantPermissionWithConditionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + where: TEST_ADDRESS, + condition: TEST_ADDRESS, + }; + PermissionWithConditionSchema.strict().validateSync(permissionParams); + }); + it("should throw an error if the condition is invalid", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + where: TEST_ADDRESS, + condition: {}, + }; + expect(() => + PermissionWithConditionSchema.strict().validateSync(permissionParams) + ) + .toThrow( + new ValidationError( + "condition must be a `string` type, but the final value was: `{}`.", + ), + ); + }); + it("should throw an error if the condition is missing", () => { + const permissionParams = { + permission: Permissions.WITHDRAW_PERMISSION, + who: TEST_ADDRESS, + where: TEST_ADDRESS, + }; + expect(() => + PermissionWithConditionSchema.strict().validateSync(permissionParams) + ) + .toThrow( + new ValidationError("condition is a required field"), + ); + }); + }); + describe("Test Withdraw", () => { + describe("Test withdraw eth", () => { + it("should validate a valid WithdrawEth", () => { + const withdrawEthParams: WithdrawParams = { + amount: BigInt(1), + type: TokenType.NATIVE, + recipientAddressOrEns: TEST_ADDRESS, + }; + WithdrawEthSchema.strict().validateSync(withdrawEthParams); + }); + it("should throw an error if the amount is missing", () => { + const withdrawEthParams = { + type: TokenType.NATIVE, + recipientAddressOrEns: TEST_ADDRESS, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError("amount is a required field"), + ); + }); + it("should throw an error if the type is missing", () => { + const withdrawEthParams = { + amount: BigInt(1), + recipientAddressOrEns: TEST_ADDRESS, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + it("should throw an error if the recipientAddressOrEns is missing", () => { + const withdrawEthParams = { + amount: BigInt(1), + type: TokenType.NATIVE, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError("recipientAddressOrEns is a required field"), + ); + }); + it("should throw an error if the amount is invalid", () => { + const withdrawEthParams = { + amount: 1, + type: TokenType.NATIVE, + recipientAddressOrEns: TEST_ADDRESS, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const withdrawEthParams = { + amount: BigInt(1), + type: "invalid", + recipientAddressOrEns: TEST_ADDRESS, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError( + "type must be one of the following values: native", + ), + ); + }); + it("should throw an error if the recipientAddressOrEns is invalid", () => { + const withdrawEthParams = { + amount: BigInt(1), + type: TokenType.NATIVE, + recipientAddressOrEns: TEST_INVALID_ADDRESS, + }; + expect(() => WithdrawEthSchema.strict().validateSync(withdrawEthParams)) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Test withdraw erc20", () => { + it("should validate a valid WithdrawErc20", () => { + const withdrawErc20Params: WithdrawParams = { + amount: BigInt(1), + type: TokenType.ERC20, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params); + }); + it("should throw an error if the amount is missing", () => { + const withdrawErc20Params = { + type: TokenType.ERC20, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError("amount is a required field"), + ); + }); + it("should throw an error if the type is missing", () => { + const withdrawErc20Params = { + amount: BigInt(1), + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + it("should throw an error if the recipientAddressOrEns is missing", () => { + const withdrawErc20Params = { + amount: BigInt(1), + type: TokenType.ERC20, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError("recipientAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const withdrawErc20Params = { + amount: BigInt(1), + type: TokenType.ERC20, + recipientAddressOrEns: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the amount is invalid", () => { + const withdrawErc20Params = { + amount: 1, + type: TokenType.ERC20, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the type is invalid", () => { + const withdrawErc20Params = { + amount: BigInt(1), + type: "invalid", + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc20", + ), + ); + }); + it("should throw an error if the recipientAddressOrEns is invalid", () => { + const withdrawErc20Params = { + amount: BigInt(1), + type: TokenType.ERC20, + recipientAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const withdrawErc20Params = { + amount: BigInt(1), + type: TokenType.ERC20, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + }; + expect(() => + WithdrawErc20Schema.strict().validateSync(withdrawErc20Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + }); + describe("Test withdraw erc721", () => { + it("should validate a valid WithdrawErc721", () => { + const withdrawErc721Params: WithdrawParams = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params); + }); + it("should throw an error if the type is missing", () => { + const withdrawErc721Params = { + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + it("should throw an error if the recipientAddressOrEns is missing", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError("recipientAddressOrEns is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the tokenId is missing", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError("tokenId is a required field"), + ); + }); + it("should throw an error if the type is invalid", () => { + const withdrawErc721Params = { + type: "invalid", + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc721", + ), + ); + }); + it("should throw an error if the recipientAddressOrEns is invalid", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_INVALID_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + tokenId: BigInt(1), + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenId is invalid", () => { + const withdrawErc721Params = { + type: TokenType.ERC721, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenId: 1, + }; + expect(() => + WithdrawErc721Schema.strict().validateSync(withdrawErc721Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + }); + describe("Test withdraw erc1155", () => { + it("should validate a valid WithdrawErc1155", () => { + const withdrawErc1155Params: WithdrawParams = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params); + }); + it("should throw an error if the type is missing", () => { + const withdrawErc1155Params = { + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("type is a required field"), + ); + }); + it("should throw an error if the recipientAddressOrEns is missing", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("recipientAddressOrEns is a required field"), + ); + }); + it("should throw an error if the daoAddressOrEns is missing", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("daoAddressOrEns is a required field"), + ); + }); + it("should throw an error if the tokenAddress is missing", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("tokenAddress is a required field"), + ); + }); + it("should throw an error if the tokenIds is missing", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("tokenIds is a required field"), + ); + }); + it("should throw an error if the amounts is missing", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError("amounts is a required field"), + ); + }); + it("should throw an error if the type is invalid", () => { + const withdrawErc1155Params = { + type: "invalid", + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError( + "type must be one of the following values: erc1155", + ), + ); + }); + it("should throw an error if the recipientAddressOrEns is invalid", () => { + const withdrawErc1155Params: WithdrawParams = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_INVALID_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the daoAddressOrEns is invalid", () => { + const withdrawErc1155Params: WithdrawParams = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_INVALID_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenAddress is invalid", () => { + const withdrawErc1155Params: WithdrawParams = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_INVALID_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the tokenIds are invalid", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [1], + amounts: [BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the amounts are invalid", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [1], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError(new InvalidParameter("bigint").message), + ); + }); + it("should throw an error if the tokenIds and amounts are not the same length", () => { + const withdrawErc1155Params = { + type: TokenType.ERC1155, + recipientAddressOrEns: TEST_ADDRESS, + daoAddressOrEns: TEST_ADDRESS, + tokenAddress: TEST_ADDRESS, + tokenIds: [BigInt(1)], + amounts: [BigInt(1), BigInt(1)], + }; + expect(() => + WithdrawErc1155Schema.strict().validateSync(withdrawErc1155Params) + ) + .toThrow( + new ValidationError( + new SizeMismatchError("tokenIds", "amounts").message, + ), + ); + }); + }); + }); + describe("Test register standard callback", () => { + it("should validate a valid RegisterStandardCallback", () => { + const registerStandardCallbackParams: RegisterStandardCallbackParams = { + interfaceId: TEST_ADDRESS, + callbackSelector: TEST_ADDRESS, + magicNumber: TEST_ADDRESS, + }; + RegisterStandardCallbackSchema.strict().validateSync( + registerStandardCallbackParams, + ); + }); + it("should throw an error if the interfaceId is missing", () => { + const registerStandardCallbackParams = { + callbackSelector: TEST_ADDRESS, + magicNumber: TEST_ADDRESS, + }; + expect(() => + RegisterStandardCallbackSchema.strict().validateSync( + registerStandardCallbackParams, + ) + ) + .toThrow( + new ValidationError("interfaceId is a required field"), + ); + }); + it("should throw an error if the callbackSelector is missing", () => { + const registerStandardCallbackParams = { + interfaceId: TEST_ADDRESS, + magicNumber: TEST_ADDRESS, + }; + expect(() => + RegisterStandardCallbackSchema.strict().validateSync( + registerStandardCallbackParams, + ) + ) + .toThrow( + new ValidationError("callbackSelector is a required field"), + ); + }); + it("should throw an error if the magicNumber is missing", () => { + const registerStandardCallbackParams = { + interfaceId: TEST_ADDRESS, + callbackSelector: TEST_ADDRESS, + }; + expect(() => + RegisterStandardCallbackSchema.strict().validateSync( + registerStandardCallbackParams, + ) + ) + .toThrow( + new ValidationError("magicNumber is a required field"), + ); + }); + }); + describe("Test upgradeToAndCall", () => { + it("should validate a valid UpgradeToAndCall", () => { + const upgradeToAndCallParams: UpgradeToAndCallParams = { + implementationAddress: TEST_ADDRESS, + data: new Uint8Array(), + }; + UpgradeToAndCallSchema.strict().validateSync(upgradeToAndCallParams); + }); + it("should throw an error if the newImplementation is missing", () => { + const upgradeToAndCallParams = { + data: new Uint8Array(), + }; + expect(() => + UpgradeToAndCallSchema.strict().validateSync( + upgradeToAndCallParams, + ) + ) + .toThrow( + new ValidationError("implementationAddress is a required field"), + ); + }); + it("should throw an error if the data is missing", () => { + const upgradeToAndCallParams = { + implementationAddress: TEST_ADDRESS, + }; + expect(() => + UpgradeToAndCallSchema.strict().validateSync( + upgradeToAndCallParams, + ) + ) + .toThrow( + new ValidationError("data is a required field"), + ); + }); + it("should throw an error if the newImplementation is invalid", () => { + const upgradeToAndCallParams: UpgradeToAndCallParams = { + implementationAddress: TEST_INVALID_ADDRESS, + data: new Uint8Array(), + }; + expect(() => + UpgradeToAndCallSchema.strict().validateSync( + upgradeToAndCallParams, + ) + ) + .toThrow( + new ValidationError(new InvalidAddressOrEnsError().message), + ); + }); + it("should throw an error if the data is invalid", () => { + const upgradeToAndCallParams = { + implementationAddress: TEST_ADDRESS, + data: {}, + }; + expect(() => + UpgradeToAndCallSchema.strict().validateSync( + upgradeToAndCallParams, + ) + ) + .toThrow( + new ValidationError( + "Invalid parameter:Uint8Array", + ), + ); + }); + }); + describe("Test InitializeFrom", () => { + it("should validate a valid InitializeFrom", () => { + const initializeFromParams: InitializeFromParams = { + previousVersion: [1, 0, 0], + initData: new Uint8Array(), + }; + InitializeFromSchema.strict().validateSync(initializeFromParams); + }); + it("should validate without optional fields", () => { + const initializeFromParams: InitializeFromParams = { + previousVersion: [1, 0, 0], + }; + InitializeFromSchema.strict().validateSync(initializeFromParams); + }); + it("should throw an error if the previousVersion is missing", () => { + const initializeFromParams = { + initData: new Uint8Array(), + }; + expect(() => + InitializeFromSchema.strict().validateSync( + initializeFromParams, + ) + ) + .toThrow( + new ValidationError("previousVersion is a required field"), + ); + }); + it("should throw an error if the previousVersion is invalid", () => { + const initializeFromParams = { + previousVersion: [1, 0], + initData: new Uint8Array(), + }; + expect(() => + InitializeFromSchema.strict().validateSync( + initializeFromParams, + ) + ) + .toThrow( + new ValidationError( + "previousVersion must have 3 items", + ), + ); + }); + it("should throw an error if the initData is invalid", () => { + const initializeFromParams = { + previousVersion: [1, 0, 0], + initData: {}, + }; + expect(() => + InitializeFromSchema.strict().validateSync( + initializeFromParams, + ) + ) + .toThrow( + new ValidationError( + "Invalid parameter:Uint8Array", + ), + ); + }); + }); +}); diff --git a/modules/common/CHANGELOG.md b/modules/common/CHANGELOG.md index 3e3c7d6b8..650a6aa5b 100644 --- a/modules/common/CHANGELOG.md +++ b/modules/common/CHANGELOG.md @@ -22,6 +22,8 @@ TEMPLATE: ### Added - New error classes `InvalidVersionError`, `PluginUpdatePreparationError` and `ProposalNotFoundError` +- Add `isSubdomain`, `isIpfsUri` and `isEnsName` functions +- Update SizeMismatchError error class ## 1.6.0 @@ -29,7 +31,6 @@ TEMPLATE: - New error classes - `getInterfaceId` function - ## 1.5.0 ### Added diff --git a/modules/common/src/errors.ts b/modules/common/src/errors.ts index 4ed703424..31fd2d7b1 100644 --- a/modules/common/src/errors.ts +++ b/modules/common/src/errors.ts @@ -231,8 +231,8 @@ export class AlwaysFailingProposalError extends SdkError { } export class SizeMismatchError extends SdkError { - constructor(cause?: Error) { - super("Size mismatch: actions and failSafeActions should match", cause); + constructor(field1: string, field2: string, cause?: Error) { + super(`Size mismatch: ${field1} and ${field2} should match`, cause); } } diff --git a/modules/common/src/utils.ts b/modules/common/src/utils.ts index 60a0c24a3..bb243a82c 100644 --- a/modules/common/src/utils.ts +++ b/modules/common/src/utils.ts @@ -1,10 +1,28 @@ -import { Interface } from '@ethersproject/abi'; -import { Zero } from '@ethersproject/constants'; +import { Interface } from "@ethersproject/abi"; +import { Zero } from "@ethersproject/constants"; export function isProposalId(propoosalId: string): boolean { const regex = new RegExp(/^0x[A-Fa-f0-9]{40}_0x[A-Fa-f0-9]{1,64}$/i); return regex.test(propoosalId); } + +export function isEnsName(name: string): boolean { + const regex = new RegExp(/^[a-z0-9-]+\.eth$/i); + return regex.test(name); +} + +export function isIpfsUri(cid: string): boolean { + const regex = new RegExp( + /^ipfs:\/\/(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})$/, + ); + return regex.test(cid); +} + +export function isSubdomain(name: string): boolean { + const regex = new RegExp(/^[a-z0-9-]+$/i); + return regex.test(name); +} + export function getInterfaceId(iface: Interface): string { let interfaceId = Zero; const functions: string[] = Object.keys(iface.functions); diff --git a/yarn.lock b/yarn.lock index 131e23b84..6fa2a4629 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,13 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@aragon/osx-ethers-v1.0.0@npm:@aragon/osx-ethers@1.2.1", "osx-ethers-v1@npm:@aragon/osx-ethers@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@aragon/osx-ethers/-/osx-ethers-1.2.1.tgz#a442048137153ed5a3ca4eff3f3927b45a5134b5" + integrity sha512-3Fscq8C9elIktiI6OT7fR5iaAvim+ghU6IUvZF3P/phvWm9roNp/GXAROhA/Vx41NQxeqmfXokgFo6KOWt4drA== + dependencies: + ethers "^5.6.2" + "@aragon/osx-ethers@1.3.0-rc0.2", "@aragon/osx-ethers@^1.3.0-rc0.2": version "1.3.0-rc0.2" resolved "https://registry.yarnpkg.com/@aragon/osx-ethers/-/osx-ethers-1.3.0-rc0.2.tgz#820f325ee8514bc6e598135bba2501f124d92d31" @@ -8078,13 +8085,6 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -"osx-ethers-v1@npm:@aragon/osx-ethers@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@aragon/osx-ethers/-/osx-ethers-1.2.1.tgz#a442048137153ed5a3ca4eff3f3927b45a5134b5" - integrity sha512-3Fscq8C9elIktiI6OT7fR5iaAvim+ghU6IUvZF3P/phvWm9roNp/GXAROhA/Vx41NQxeqmfXokgFo6KOWt4drA== - dependencies: - ethers "^5.6.2" - p-cancelable@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" @@ -8417,6 +8417,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +property-expr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -9832,6 +9837,11 @@ timed-out@^4.0.1: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tiny-glob@^0.2.6: version "0.2.9" resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" @@ -9902,6 +9912,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -10122,6 +10137,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -11027,3 +11047,13 @@ yargs@^4.7.1: window-size "^0.2.0" y18n "^3.2.1" yargs-parser "^2.4.1" + +yup@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6" + integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0"