Files

172 lines
6.9 KiB
TypeScript

import { zeroAddress } from "viem";
import { z } from "zod";
const SerializedSealingPair = z.object({
privateKey: z.string(),
publicKey: z.string(),
});
const zPermitV2WithDefaults = z.object({
name: z.string().optional().default("Unnamed Permit"),
type: z.enum(["self", "sharing", "recipient"]),
issuer: z.string().refine(val => val !== zeroAddress, {
message: "PermitV2 issuer :: must not be zeroAddress",
}),
expiration: z.number().optional().default(1000000000000),
contracts: z.array(z.string()).optional().default([]),
projects: z.array(z.string()).optional().default([]),
recipient: z.string().optional().default(zeroAddress),
validatorId: z.number().optional().default(0),
validatorContract: z.string().optional().default(zeroAddress),
sealingPair: SerializedSealingPair.optional(),
issuerSignature: z.string().optional().default("0x"),
recipientSignature: z.string().optional().default("0x"),
});
type zPermitV2Type = z.infer<typeof zPermitV2WithDefaults>;
/**
* Ensures that this Permit will provide access to a non-zero amount of contracts
* by ensuring that `contracts` and `projects` aren't both empty arrays.
*/
const PermitV2RefineAccess = [
(data: zPermitV2Type) => {
return data.contracts.length + data.projects.length > 0;
},
{
message: "PermitV2 access :: at least one contract or project must be accessible.",
path: ["contracts", "projects"] as string[],
},
] as const;
/**
* Permits allow a hook into an optional external validator contract,
* this check ensures that IF an external validator is applied, that both `validatorId` and `validatorContract` are populated,
* ELSE ensures that both `validatorId` and `validatorContract` are empty
*/
const PermitV2RefineValidator = [
(data: zPermitV2Type) =>
(data.validatorId !== 0 && data.validatorContract !== zeroAddress) ||
(data.validatorId === 0 && data.validatorContract === zeroAddress),
{
message:
"PermitV2 external validator :: validatorId and validatorContract must either both be set or both be unset.",
path: ["validatorId", "validatorContract"] as string[],
},
] as const;
/**
* SuperRefinement that checks a PermitV2s signatures
* checkRecipient - whether to validate that `recipient` is empty for permit with type <self>, and populated for <sharing | recipient>
* checkSealingPair - only the fully formed permit requires the sealing pair, it can be optional for permitV2 create params
* checkExistingSignatures - not optional - checks that the permit's type matches the populated signature fields
* checkSigned - checks that the active user's signature has been signed and added. <self | signed> -> issuerSignature, <recipient> -> recipientSignature
*/
const PermitV2SignaturesSuperRefinement = (options: {
checkRecipient: boolean;
checkSealingPair: boolean;
checkSigned: boolean;
}) => {
return (data: zPermitV2Type, ctx: z.RefinementCtx) => {
// Check Recipient
// If type <self | sharing>, `PermitV2.recipient` must be zeroAddress
// If type <recipient>, `PermitV2.recipient` must not be zeroAddress
if (options.checkRecipient) {
if (data.type === "self" && data.recipient !== zeroAddress)
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["recipient"],
message: `PermitV2 (type '${data.type}') recipient :: must be empty (zeroAddress)`,
});
if ((data.type === "recipient" || data.type === "sharing") && data.recipient === zeroAddress) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["recipient"],
message: `PermitV2 (type '${data.type}') recipient :: must not be empty`,
});
}
}
// Check Sealing Pair
if (options.checkSealingPair && data.sealingPair == null)
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sealingPair"],
message: `PermitV2 sealingPair :: must not be empty`,
});
// Check existing signatures match type (not checking this user's signature, but the other signature)
// If type <self | sharing>, `PermitV2.recipientSignature` must be empty
// If type <recipient>, `PermitV2.issuerSignature` must not be empty
if ((data.type === "self" || data.type === "sharing") && data.recipientSignature !== "0x") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["recipientSignature"],
message: `PermitV2 (type '${data.type}') recipientSignature :: should not be populated by the issuer`,
});
}
if (data.type === "recipient" && data.issuerSignature === "0x") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["issuerSignature"],
message: `PermitV2 (type 'recipient') issuerSignature :: \`issuer\` must sign the PermitV2 before sharing it with \`recipient\``,
});
}
// Check Signed
// If type <self | sharing>, `PermitV2.issuerSignature` must not be empty
// If type <recipient>, `PermitV2.recipientSignature` must not be empty
if (options.checkSigned) {
if ((data.type === "self" || data.type === "sharing") && data.issuerSignature === "0x")
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["issuerSignature"],
message: `PermitV2 (type '${data.type}') issuerSignature :: must be populated with issuer's signature. Use \`PermitV2.sign\` or create permit with \`PermitV2.createAndSign\``,
});
if (data.type === "recipient" && data.recipientSignature === "0x") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["recipientSignature"],
message: `PermitV2 (type 'recipient') recipientSignature :: must be populated with recipient's signature. Use \`PermitV2.sign\` or create permit with \`PermitV2.createAndSign\``,
});
}
}
return;
};
};
/**
* Validator for the params used when creating a fresh PermitV2
* Has defaults added that will be populated in the options object
* Signatures superRefinement checks only the recipient, sealingPair and signatures are not necessary in the Permit params
*/
export const PermitV2ParamsValidator = zPermitV2WithDefaults
.refine(...PermitV2RefineAccess)
.refine(...PermitV2RefineValidator)
.superRefine(
PermitV2SignaturesSuperRefinement({
checkRecipient: true,
checkSealingPair: false, // SealingPair not required when creating a fresh permit
checkSigned: false, // Signature not required when creating a fresh permit
}),
);
/**
* Validator for a PermitV2 that is expected to be fully formed
* Does not allow optional values or offer defaults
* Validates that the correct signatures are populated
* Validates sealingPair is populated
*/
export const FullyFormedPermitV2Validator = zPermitV2WithDefaults
.required()
.refine(...PermitV2RefineAccess)
.refine(...PermitV2RefineValidator)
.superRefine(
PermitV2SignaturesSuperRefinement({
checkRecipient: true,
checkSealingPair: true,
checkSigned: true,
}),
);