diff --git a/packages/sdk/src/annotations/applyAnnotations.ts b/packages/sdk/src/annotations/applyAnnotations.ts deleted file mode 100644 index ecbb47427..000000000 --- a/packages/sdk/src/annotations/applyAnnotations.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { ChainId, Annotation, fetchRole } from "zodiac-roles-deployments" - -import { groupBy } from "../utils/groupBy" - -import { encodeAnnotationsPost } from "./poster" -import { UpdateAnnotationsPost } from "./types" - -type Options = { - address: `0x${string}` - /** The mode to use for updating the set of members of the role: - * - "replace": The role will have only the passed members, meaning that all other current members will be removed from the role - * - "extend": The role will keep its current members and will additionally get the passed members - * - "remove": All passed members will be removed from the role - */ - mode: "replace" | "extend" | "remove" - log?: boolean | ((message: string) => void) -} & ( - | { - currentAnnotations: readonly Annotation[] - } - | { chainId: ChainId } -) - -/** - * Returns a set of encoded call data to be sent to the Roles mod for updating the annotations of the given role. - * - * @param roleKey The key of the role to update - * @param annotations Annotations to apply to the role - * @param options Options for the update - */ -export const applyAnnotations = async ( - roleKey: `0x${string}`, - annotations: readonly Annotation[], - options: Options -) => { - const { address, mode } = options - const log = options.log === true ? console.log : options.log || undefined - - let currentAnnotations: readonly Annotation[] - - if ("currentAnnotations" in options && options.currentAnnotations) { - currentAnnotations = options.currentAnnotations - } else { - if ("chainId" in options && options.chainId) { - const role = await fetchRole({ - chainId: options.chainId, - address: address, - roleKey, - }) - if (!role) { - throw new Error(`Role ${roleKey} not found on chain ${options.chainId}`) - } - currentAnnotations = role.annotations - } else { - throw new Error( - "Either `currentAnnotations` or `chainId` must be specified" - ) - } - } - - let updatePost: UpdateAnnotationsPost - switch (mode) { - case "replace": - updatePost = replaceAnnotations(currentAnnotations, annotations) - break - case "extend": - updatePost = extendAnnotations(currentAnnotations, annotations) - break - case "remove": - updatePost = removeAnnotations(currentAnnotations, annotations) - break - default: - throw new Error(`Invalid mode: ${options.mode}`) - } - - const addCount = - updatePost.addAnnotations?.reduce((acc, add) => acc + add.uris.length, 0) || - 0 - const removeCount = updatePost.removeAnnotations?.length || 0 - - if (addCount === 0 && removeCount === 0) { - return [] - } - - if (log) { - const message = [ - addCount > 0 ? "add " + pluralize(addCount, "annotation") : undefined, - removeCount > 0 - ? "remove " + pluralize(removeCount, "annotation") - : undefined, - ] - .filter(Boolean) - .join(", ") - - log(`💬 ${message[0].toUpperCase()}${message.slice(1)}`) - } - - return [encodeAnnotationsPost(address, roleKey, updatePost)] -} - -const replaceAnnotations = ( - current: readonly Annotation[], - next: readonly Annotation[] -): UpdateAnnotationsPost => { - const removeAnnotations = current - .map((annotation) => annotation.uri) - .filter((uri) => !next.some((nextAnnotation) => nextAnnotation.uri === uri)) - - const toAdd = next.filter( - (annotation) => - !current.some( - (currentAnnotation) => - currentAnnotation.uri === annotation.uri && - currentAnnotation.schema === annotation.schema - ) - ) - - return { - removeAnnotations, - addAnnotations: groupAnnotations(toAdd), - } -} - -export const extendAnnotations = ( - current: readonly Annotation[], - add: readonly Annotation[] -): UpdateAnnotationsPost => { - const toAdd = add.filter( - ({ uri, schema }) => - !current.some( - (annotation) => annotation.uri === uri && annotation.schema === schema - ) - ) - return { - addAnnotations: groupAnnotations(toAdd), - } -} - -const removeAnnotations = ( - current: readonly Annotation[], - remove: readonly Annotation[] -): UpdateAnnotationsPost => { - return { - removeAnnotations: remove - .map((annotation) => annotation.uri) - .filter((uri) => current.some((annotation) => annotation.uri === uri)), - } -} - -const groupAnnotations = (annotations: readonly Annotation[]) => - Object.entries(groupBy(annotations, (annotation) => annotation.schema)).map( - ([schema, annotations]) => ({ - schema, - uris: annotations.map((annotation) => annotation.uri), - }) - ) - -const pluralize = ( - count: number, - singular: string, - plural = `${singular}s` -) => { - if (count === 1) { - return `1 ${singular}` - } - return `${count} ${plural}` -} diff --git a/packages/sdk/src/annotations/index.ts b/packages/sdk/src/annotations/index.ts deleted file mode 100644 index 3ad2a20ab..000000000 --- a/packages/sdk/src/annotations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { applyAnnotations, extendAnnotations } from "./applyAnnotations" diff --git a/packages/sdk/src/annotations/poster.ts b/packages/sdk/src/annotations/poster.ts index 49c6151d3..71836b6e9 100644 --- a/packages/sdk/src/annotations/poster.ts +++ b/packages/sdk/src/annotations/poster.ts @@ -19,7 +19,7 @@ export const encodePost = (content: string) => { } export const encodeAnnotationsPost = ( - rolesMod: `0x${string}`, + rolesMod: string, roleKey: string, updatePost: UpdateAnnotationsPost ) => { diff --git a/packages/sdk/src/calls/encodeCalls.ts b/packages/sdk/src/calls/encodeCalls.ts index ab794c38b..a38427ddf 100644 --- a/packages/sdk/src/calls/encodeCalls.ts +++ b/packages/sdk/src/calls/encodeCalls.ts @@ -1,11 +1,15 @@ import { Roles__factory } from "../../../evm/typechain-types" +import { encodeAnnotationsPost } from "../annotations/poster" import { flattenCondition } from "../conditions" import { Call } from "./types" const rolesInterface = Roles__factory.createInterface() -export const encodeCalls = (calls: Call[]): `0x${string}`[] => { +export const encodeCalls = ( + calls: Call[], + rolesMod: string +): `0x${string}`[] => { return calls.map((call) => { switch (call.call) { case "allowTarget": { @@ -76,6 +80,11 @@ export const encodeCalls = (calls: Call[]): `0x${string}`[] => { call.timestamp, ]) } + + case "postAnnotations": { + const { roleKey, body } = call + return encodeAnnotationsPost(rolesMod, roleKey, body) + } } }) as `0x${string}`[] } diff --git a/packages/sdk/src/calls/logCall.ts b/packages/sdk/src/calls/logCall.ts index f6b9bb83e..dcb466b17 100644 --- a/packages/sdk/src/calls/logCall.ts +++ b/packages/sdk/src/calls/logCall.ts @@ -65,8 +65,23 @@ export const logCall = ( break } - // default: - // log(`Unhandled call ${call.call}`) + case "postAnnotations": { + const { addAnnotations, removeAnnotations } = call.body + const addCount = + addAnnotations?.reduce((acc, add) => acc + add.uris.length, 0) || 0 + const removeCount = removeAnnotations?.length || 0 + const message = [ + addCount > 0 ? "add " + pluralize(addCount, "annotation") : undefined, + removeCount > 0 + ? "remove " + pluralize(removeCount, "annotation") + : undefined, + ] + .filter(Boolean) + .join(", ") + + log(`💬 ${message[0].toUpperCase()}${message.slice(1)}`) + break + } } } @@ -76,3 +91,14 @@ const ExecutionOptionLabel = { [ExecutionOptions.Send]: "call, send", [ExecutionOptions.Both]: "call, delegatecall, send", } + +const pluralize = ( + count: number, + singular: string, + plural = `${singular}s` +) => { + if (count === 1) { + return `1 ${singular}` + } + return `${count} ${plural}` +} diff --git a/packages/sdk/src/calls/types.ts b/packages/sdk/src/calls/types.ts index c79232396..4ad50725c 100644 --- a/packages/sdk/src/calls/types.ts +++ b/packages/sdk/src/calls/types.ts @@ -59,6 +59,18 @@ interface SetAllowanceCall { timestamp: bigint } +interface PostAnnotationsCall { + call: "postAnnotations" + roleKey: string + body: { + addAnnotations?: { + uris: string[] + schema: string + }[] + removeAnnotations?: string[] + } +} + export type Call = | AllowTargetCall | ScopeTargetCall @@ -68,3 +80,4 @@ export type Call = | RevokeFunctionCall | AssignRolesCall | SetAllowanceCall + | PostAnnotationsCall diff --git a/packages/sdk/src/diff/all.ts b/packages/sdk/src/diff/all.ts index f155e60ba..cf99fcb1b 100644 --- a/packages/sdk/src/diff/all.ts +++ b/packages/sdk/src/diff/all.ts @@ -8,8 +8,8 @@ export function diffAll({ prev, next, }: { - prev?: { roles: Role[]; allowances: Allowance[] } - next?: { roles: Role[]; allowances: Allowance[] } + prev: { roles: Role[]; allowances: Allowance[] } | undefined | null + next: { roles: Role[]; allowances: Allowance[] } | undefined | null }): Diff { return merge( diffRoles({ diff --git a/packages/sdk/src/diff/annotations.ts b/packages/sdk/src/diff/annotations.ts new file mode 100644 index 000000000..a31b68d72 --- /dev/null +++ b/packages/sdk/src/diff/annotations.ts @@ -0,0 +1,60 @@ +import { Annotation } from "zodiac-roles-deployments" + +import { groupBy } from "../utils/groupBy" + +import { Diff } from "./helpers" + +export function diffAnnotations({ + roleKey, + prev = [], + next = [], +}: { + roleKey: string + prev?: Annotation[] + next?: Annotation[] +}): Diff { + const removeAnnotations = prev + .map((annotation) => annotation.uri) + .filter((uri) => !next.some((nextAnnotation) => nextAnnotation.uri === uri)) + + const addAnnotations = groupAnnotations( + next.filter( + (annotation) => + !prev.some( + (currentAnnotation) => + currentAnnotation.uri === annotation.uri && + currentAnnotation.schema === annotation.schema + ) + ) + ) + + return { + minus: + removeAnnotations.length > 0 + ? [ + { + call: "postAnnotations", + roleKey, + body: { removeAnnotations }, + }, + ] + : [], + plus: addAnnotations + ? [ + { + call: "postAnnotations", + roleKey, + body: { addAnnotations }, + }, + ] + : [], + } +} + +const groupAnnotations = (annotations: readonly Annotation[]) => + Object.entries(groupBy(annotations, (annotation) => annotation.schema)).map( + ([schema, annotations]) => ({ + schema, + uris: annotations.map((annotation) => annotation.uri), + }) + ) diff --git a/packages/sdk/src/diff/role.ts b/packages/sdk/src/diff/role.ts index c6aecbff3..9bb27abbe 100644 --- a/packages/sdk/src/diff/role.ts +++ b/packages/sdk/src/diff/role.ts @@ -2,6 +2,7 @@ import { invariant } from "@epic-web/invariant" import { ZeroHash } from "ethers" import { Annotation, Target } from "zodiac-roles-deployments" +import { diffAnnotations } from "./annotations" import { diffMembers } from "./members" import { diffTargets } from "./target" @@ -18,8 +19,8 @@ export function diffRoles({ prev, next, }: { - prev?: RoleFragment[] - next?: RoleFragment[] + prev: RoleFragment[] | undefined | null + next: RoleFragment[] | undefined | null }): Diff { const roleKeys = Array.from( new Set([ @@ -42,25 +43,34 @@ export function diffRole({ prev, next, }: { - prev?: RoleFragment - next?: RoleFragment + prev: RoleFragment | undefined | null + next: RoleFragment | undefined | null }): Diff { const roleKey = ensureRoleKey(prev, next) - return merge( + + return [ + diffMembers({ + roleKey, + prev: prev?.members, + next: next?.members, + }), diffTargets({ roleKey, prev: prev?.targets, next: next?.targets, }), - diffMembers({ + diffAnnotations({ roleKey, - prev: prev?.members, - next: next?.members, - }) - ) + prev: prev?.annotations, + next: next?.annotations, + }), + ].reduce((p, n) => merge(p, n), { minus: [], plus: [] }) } -function ensureRoleKey(prev?: RoleFragment, next?: RoleFragment) { +function ensureRoleKey( + prev: RoleFragment | undefined | null, + next: RoleFragment | undefined | null +) { const set = new Set([prev?.key, next?.key].filter(Boolean)) invariant(set.size <= 1, "Invalid Role Comparison") diff --git a/packages/sdk/src/plan.ts b/packages/sdk/src/plan.ts index cdef1986b..998a6f2fa 100644 --- a/packages/sdk/src/plan.ts +++ b/packages/sdk/src/plan.ts @@ -12,29 +12,50 @@ import diff, { diffRole } from "./diff" import { Call, encodeCalls, logCall } from "./calls" +type Options = { + chainId: ChainId + address: `0x${string}` + log?: boolean | ((message: string) => void) +} + export async function planApply( - next: { roles: Role[]; allowances: Allowance[] }, - options: Options + apply: { roles: Role[]; allowances: Allowance[] }, + { + chainId, + address, + current, + log, + }: { + current?: { + roles: Role[] + allowances: Allowance[] + } + } & Options ) { - const prev = await maybeFetchRolesMod(options) - - const { minus, plus } = diff({ prev, next }) + const { minus, plus } = diff({ + prev: current || (await fetchRolesMod({ chainId, address })), + next: apply, + }) const calls = [...minus, ...plus] - logCalls(calls, options) + logCalls(calls, log) - return encodeCalls(calls) + return encodeCalls(calls, address) } -export async function planApplyRole(next: Role, options: RoleOptions) { - const prev = await maybeFetchRole(next.key, options) - - const { minus, plus } = await diffRole({ prev, next }) +export async function planApplyRole( + role: Role, + { chainId, address, current, log }: { current?: Role } & Options +) { + const { minus, plus } = await diffRole({ + prev: current || (await fetchRole({ chainId, address, roleKey: role.key })), + next: role, + }) const calls = [...minus, ...plus] - logCalls(calls, options) + logCalls(calls, log) - return encodeCalls(calls) + return encodeCalls(calls, address) } type RoleFragment = { @@ -44,22 +65,23 @@ type RoleFragment = { annotations?: Annotation[] } -export async function planExtendRole(next: RoleFragment, options: RoleOptions) { - const prev = await maybeFetchRole(next.key, options) - - const { plus } = await diffRole({ prev, next }) +export async function planExtendRole( + role: RoleFragment, + { chainId, address, current, log }: { current?: Role } & Options +) { + const { plus } = await diffRole({ + prev: current || (await fetchRole({ chainId, address, roleKey: role.key })), + next: role, + }) // extend -> just the plus const calls = plus - logCalls(calls, options) + logCalls(calls, log) - return encodeCalls(calls) + return encodeCalls(calls, address) } -function logCalls( - calls: Call[], - { log }: { log?: boolean | ((message: string) => void) } -) { +function logCalls(calls: Call[], log?: boolean | ((message: string) => void)) { if (!log) { return } @@ -68,54 +90,3 @@ function logCalls( logCall(call, log === true ? console.log : log || undefined) } } - -type Options = ( - | { chainId: ChainId; address: `0x${string}` } - | { currentRoles?: Role[]; currentAllowances?: Allowance[] } -) & { - log?: boolean | ((message: string) => void) -} - -async function maybeFetchRolesMod( - options: Options -): Promise<{ roles: Role[]; allowances: Allowance[] } | undefined> { - if ("address" in options) { - return ( - (await fetchRolesMod({ - chainId: options.chainId, - address: options.address, - })) || undefined - ) - } else { - return options.currentRoles || options.currentAllowances - ? { - roles: options.currentRoles || [], - allowances: options.currentAllowances || [], - } - : undefined - } -} - -type RoleOptions = ( - | { chainId: ChainId; address: `0x${string}` } - | { currentRole?: Role } -) & { - log?: boolean | ((message: string) => void) -} - -async function maybeFetchRole( - roleKey: `0x${string}`, - options: RoleOptions -): Promise { - if ("address" in options) { - return ( - (await fetchRole({ - chainId: options.chainId, - address: options.address, - roleKey, - })) || undefined - ) - } else { - return options.currentRole || undefined - } -}