@zodiaceco/sdk 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -6
- package/dist/{actions-tA_Oe_kC.d.mts → actions-Cri7InV-.d.mts} +57 -4
- package/dist/actions.d.mts +1 -1
- package/dist/actions.mjs +18 -2
- package/dist/actions.mjs.map +1 -1
- package/dist/allow/index.d.mts +1 -1
- package/dist/allow/index.mjs +2 -2
- package/dist/{allow-CrDzDtxx.mjs → allow-DX3iyizB.mjs} +2 -2
- package/dist/{allow-CrDzDtxx.mjs.map → allow-DX3iyizB.mjs.map} +1 -1
- package/dist/cli/config.mjs +1 -1
- package/dist/cli/push.d.mts +24 -0
- package/dist/cli/push.mjs +36 -0
- package/dist/cli/push.mjs.map +1 -0
- package/dist/cli.mjs +15 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-D0FqysME.mjs → config-BaChz2bq.mjs} +10 -5
- package/dist/config-BaChz2bq.mjs.map +1 -0
- package/dist/{index-CnH26Z_A.d.mts → index-D49x1ozw.d.mts} +1 -1
- package/dist/index.d.mts +34 -5
- package/dist/index.mjs +8 -180
- package/dist/index.mjs.map +1 -1
- package/dist/{networks-B0H4Uoyh.mjs → networks-UKtJ8QDG.mjs} +1 -1
- package/dist/{networks-B0H4Uoyh.mjs.map → networks-UKtJ8QDG.mjs.map} +1 -1
- package/dist/paths-LsD6ULk0.mjs +34 -0
- package/dist/paths-LsD6ULk0.mjs.map +1 -0
- package/dist/{permissionEntries-D3fSVhwO.mjs → permissionEntries-C4dAwZBb.mjs} +3 -1
- package/dist/permissionEntries-C4dAwZBb.mjs.map +1 -0
- package/dist/push-DdnHsLpO.mjs +356 -0
- package/dist/push-DdnHsLpO.mjs.map +1 -0
- package/package.json +7 -3
- package/dist/api-kKgzHQlu.mjs +0 -158
- package/dist/api-kKgzHQlu.mjs.map +0 -1
- package/dist/config-D0FqysME.mjs.map +0 -1
- package/dist/permissionEntries-D3fSVhwO.mjs.map +0 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constellation.ts","../src/push.ts"],"sourcesContent":["/// <reference path=\"./zodiac-os-codegen.d.ts\" />\nimport type { Address, ChainId } from '@zodiaceco/api-types'\nimport type { PermissionEntry } from './actions'\nimport type { AllowanceSpec } from './types'\nimport { createRequire } from 'module'\nimport { resolveZodiacDir } from './paths'\nimport { UUID } from 'crypto'\n\n/**\n * A role definition keyed by role name. Entries describe what the role may do —\n * parameters and labels, never compiled permissions. They are expanded into\n * `{ targets, annotations }` when the constellation is deployed, so that a\n * stored revision always compiles through the current compilers.\n */\nexport type RoleDef = {\n members: readonly AddressOrRef[]\n permissions: readonly PermissionEntry[]\n}\n\ntype User = {\n id: UUID\n fullName: string\n personalSafes: Record<\n number,\n { address: Lowercase<Address>; active: boolean }\n >\n}\n\ntype Account = {\n id: UUID\n label: string\n address: Lowercase<Address>\n chain: ChainId\n /** True for accounts promoted to a workspace vault. */\n vault: boolean\n}\n\ntype AccountsByLabel = Readonly<Record<string, Account>>\n\n/**\n * A workspace's accounts, grouped by node type and then by chain.\n *\n * Both groupings keep bracket-accessor namespaces apart, so that a label is\n * only ever ambiguous between accounts a constellation could actually choose\n * between: a SAFE and a ROLES mod sharing a label don't collide, and neither\n * do two safes of the same name on different chains.\n */\ntype WorkspaceAccounts = {\n workspaceId: UUID\n workspaceName: string\n safes: AccountsByChain\n rolesMods: AccountsByChain\n delays: AccountsByChain\n}\n\ntype AccountsByChain = { readonly [chain in ChainId]?: AccountsByLabel }\n\n/** Shape of the codegen data produced by `zodiac pull-org`. */\nexport type CodegenData = {\n users: Readonly<Record<string, User>>\n accounts: Readonly<Record<string, WorkspaceAccounts>>\n}\n\n// If `pull-org` has been run, the consumer's `.zodiac/index.d.ts` augments\n// `ZodiacGeneratedCodegen` with literal `users`/`accounts` shapes. Otherwise\n// the interface is empty and we fall back to the wide `CodegenData`.\ntype GeneratedCodegen = ZodiacGeneratedCodegen extends CodegenData\n ? ZodiacGeneratedCodegen\n : CodegenData\n\ntype ConstellationOpts<C extends CodegenData> = {\n /** Workspace to scope accounts and roles to. */\n workspace: keyof C['accounts'] & string\n /** Human-readable label for this constellation. */\n label: string\n /** Target chain for all nodes in this constellation. */\n chain: ChainId\n}\n\ntype ConstellationInternalOpts<C extends CodegenData> = {\n /** Injected codegen data (used for testing). */\n codegen?: C\n}\n\ntype Prettify<T> = { readonly [K in keyof T]: T[K] } & {}\n\n/**\n * The accounts of one type that a constellation on `Ch` can name. Accounts on\n * every other chain are not in scope: their addresses mean nothing here, so\n * offering them would only invite a node that carries a foreign address under\n * this constellation's chain.\n */\ntype ChainEntries<A, Ch extends ChainId> = Ch extends keyof A\n ? NonNullable<A[Ch]>\n : {}\n\ntype SafeEntries<\n C extends CodegenData,\n W extends keyof C['accounts'],\n Ch extends ChainId,\n> = ChainEntries<C['accounts'][W]['safes'], Ch>\n\ntype RolesEntries<\n C extends CodegenData,\n W extends keyof C['accounts'],\n Ch extends ChainId,\n> = ChainEntries<C['accounts'][W]['rolesMods'], Ch>\n\ntype NodeType = 'SAFE' | 'ROLES' | 'DELAY'\n\n/** A reference to a node used in `owners`, `modules`, `target`, etc. */\nexport type NodeRef = Readonly<{\n type: NodeType\n label: string\n chain: ChainId\n}>\n\n/** A blockchain address (checksummed or lowercase) or a reference to another\n * node in the constellation. Values are normalized to lowercase before being\n * sent to the API. */\ntype AddressOrRef = Address | NodeRef\n\ntype NodeBase = Readonly<{\n /** Human-readable identifier, unique within the constellation. */\n label: string\n /** Chain the node is deployed on. */\n chain: ChainId\n /** Set for existing nodes (from codegen or bound by address), absent for new\n * nodes. Accepts checksummed or lowercase; normalized to lowercase on push. */\n address?: Address\n /** Deployment nonce — required for new nodes, optional for existing. */\n nonce?: bigint\n}>\n\n/** A safe node — a reference to one the workspace already has, or a new one\n * with the config it is deployed with. */\nexport type SafeNode = NodeBase &\n Readonly<{\n /** Discriminator identifying this node as a Safe. */\n type: 'SAFE'\n /** Number of owner signatures required to execute a transaction. */\n threshold: number\n /** Safe owner addresses or node references. */\n owners: readonly (string | NodeRef)[]\n /** Module addresses or node references enabled on the safe. */\n modules?: readonly (string | NodeRef)[]\n /** Whether this safe shall appear as a vault in the workspace. @default false */\n vault?: boolean\n }>\n\n/** A roles modifier node — a reference to one the workspace already has, or a\n * new one with the config it is deployed with. */\nexport type RolesNode = NodeBase &\n Readonly<{\n /** Discriminator identifying this node as a Roles modifier. */\n type: 'ROLES'\n /** The safe that this roles modifier controls. */\n target?: AddressOrRef\n /** The account that is allowed to update the configuration of the Roles mod. */\n owner?: AddressOrRef\n /** The account that calls will be executed from. */\n avatar?: AddressOrRef\n /** MultiSend contract addresses for batched transactions. */\n multisend?: readonly Address[]\n /** Role definitions configured on this modifier. Pass `null` for a key to\n * clear that role; unmentioned roles are left untouched. */\n roles?: Record<string, RoleDef | null>\n /** Spending allowances configured on this modifier, keyed by name. Pass\n * `null` for a key to clear that allowance; unmentioned allowances are\n * left untouched. */\n allowances?: Record<string, AllowanceSpec | null>\n }>\n\n/** Any complete node that can be passed to `push()`. */\nexport type ConstellationNode = SafeNode | RolesNode\nexport type ConstellationNodeInternal = ConstellationNode & {\n _constellation: ConstellationMeta\n /**\n * The state fields this node actually declares.\n *\n * A referenced node carries everything `pull` read from chain so it reads\n * well and completes in an editor, but a reference is not a declaration —\n * only what was passed explicitly is pushed. Empty for a bare reference.\n */\n _declared: readonly string[]\n}\n\ntype NewSafeProps = {\n /** Deployment nonce for CREATE2 address derivation. */\n nonce: bigint\n /** Number of owner signatures required to execute a transaction. */\n threshold: number\n /** Safe owner addresses or node references. */\n owners: readonly AddressOrRef[]\n /** Module addresses or node references to enable on the safe. */\n modules?: readonly AddressOrRef[]\n /** Whether this safe is a workspace vault. @default false */\n vault?: boolean\n}\n\n/** Configuration shared by both ways of declaring a roles modifier. */\ntype RolesConfig = {\n /** The safe that this roles modifier controls. Defaults to the new safe with the same label, when one exists. */\n target?: AddressOrRef\n /** The account that calls will be executed from. Defaults to `target` value */\n avatar?: AddressOrRef\n /** The account that is allowed to update the configuration of the Roles Mod. Defaults to `target` value */\n owner?: AddressOrRef\n /** MultiSend contract addresses for batched transactions. Defaults to `['0x38869bf66a61cf6bdb996a6ae40d5853fd43b526', '0x9641d764fc13c8b624c04430c7356c1c7c8102e2']` */\n multisend?: readonly Address[]\n /** Role definitions to configure on this modifier. Pass `null` for a key to\n * clear that role; unmentioned roles are left untouched. */\n roles?: Record<string, RoleDef | null>\n /** Spending allowances to configure on this modifier, keyed by name. Pass\n * `null` for a key to clear that allowance; unmentioned allowances are\n * left untouched. */\n allowances?: Record<string, AllowanceSpec | null>\n}\n\n/** Declare a brand-new roles modifier (address derived via CREATE2 from `nonce`). */\ntype NewRolesByNonce = RolesConfig & {\n /** Deployment nonce for CREATE2 address derivation. */\n nonce: bigint\n}\n\n/** Bind to a roles modifier already deployed on-chain at a known address, to\n * reconfigure its roles/allowances. Use this for mods not (yet) imported into\n * the workspace — pass the address instead of a deployment `nonce`. */\ntype ExistingRolesByAddress = RolesConfig & {\n /** Address of the existing Roles mod to bind to and reconfigure. */\n address: Address\n}\n\ntype NewRolesProps = NewRolesByNonce | ExistingRolesByAddress\n\ntype ExistingNodeAccessor<\n Type extends string,\n K extends string,\n E,\n Ch extends ChainId,\n NP extends Record<string, any>,\n> = Readonly<Prettify<E & { type: Type; label: K; chain: Ch }>> &\n (<\n const O extends {\n [P in Exclude<keyof E & string, 'id' | 'label'>]?: any\n } & Partial<NP> = {},\n >(\n overrides?: {\n [P in Exclude<keyof E & string, 'id' | 'label'>]?: any\n } & Partial<NP> &\n O\n ) => Readonly<\n Prettify<\n Omit<E, keyof O> & O & Partial<NP> & { type: Type; label: K; chain: Ch }\n >\n >)\n\ntype NewNodeAccessor<\n Type extends string,\n Ch extends ChainId,\n NP extends Record<string, any>,\n> = Readonly<Prettify<{ type: Type; label: string; chain: Ch }>> &\n // `const` so a node reads back the values it was written with — `threshold: 2`\n // rather than `number`. Every prop it widens into is already `readonly`, so\n // the tuples this infers stay assignable.\n (<const P extends NP>(\n props: P\n ) => Readonly<Prettify<P & { type: Type; label: string; chain: Ch }>>)\n\ntype EntityAccessor<\n Type extends string,\n Entries extends Record<string, any>,\n Ch extends ChainId = ChainId,\n NP extends Record<string, any> = Record<string, any>,\n> = {\n readonly [K in keyof Entries & string]: ExistingNodeAccessor<\n Type,\n K,\n Entries[K],\n Ch,\n NP\n >\n} & {\n readonly [key: string]: NewNodeAccessor<Type, Ch, NP>\n}\n\ntype UserAccessor<C extends CodegenData, Ch extends number> = {\n readonly [K in keyof C['users'] &\n string]: C['users'][K]['personalSafes'][Ch]['address']\n}\n\ntype ConstellationResult<\n C extends CodegenData,\n W extends keyof C['accounts'] = keyof C['accounts'],\n Ch extends ChainId = ChainId,\n> = {\n /** Access existing safes by label or create new ones with a new label.\n * Only SAFE-typed accounts are suggested in IntelliSense. */\n safe: EntityAccessor<'SAFE', SafeEntries<C, W, Ch>, Ch, NewSafeProps>\n /** Access existing roles modifiers by label or create new ones with a\n * new label. Only ROLES-typed accounts are suggested in IntelliSense. */\n roles: EntityAccessor<'ROLES', RolesEntries<C, W, Ch>, Ch, NewRolesProps>\n /** Resolve a user's personal safe address on the constellation's chain. */\n user: UserAccessor<C, Ch>\n}\n\n/** @internal */\nexport type ConstellationMeta = {\n label: string\n chain: ChainId\n workspaceId: UUID\n}\n\nfunction loadCodegen(): CodegenData {\n const require = createRequire(import.meta.url)\n return require(resolveZodiacDir()) as CodegenData\n}\n\n/**\n * Creates a constellation scoped to a workspace and chain.\n *\n * Bracket access names an account of that workspace **on that chain** — a\n * vault, or a node an earlier constellation deployed. A label the chain has no\n * account for reads as a new node instead, so it has to be given the config it\n * is deployed with.\n *\n * ```ts\n * const eth = constellation({ workspace: 'GG', label: 'my constellation', chain: 1 })\n *\n * const dao = eth.safe['GG DAO'] // the safe of that name on chain 1\n * const roles = eth.roles['GG DAO'] // the roles mod of that name\n * const newSafe = eth.safe['New Safe']({ nonce: 0n, threshold: 2, owners: [...], modules: [...] })\n * ```\n *\n * Names come from the last `pull-org`, so an account deployed since then is\n * not one yet.\n */\nexport function constellation<\n const C extends CodegenData = GeneratedCodegen,\n const W extends keyof C['accounts'] & string = keyof C['accounts'] & string,\n const Ch extends ChainId = ChainId,\n>(\n opts: ConstellationOpts<C> & { workspace: W; chain: Ch },\n internal?: ConstellationInternalOpts<C>\n): ConstellationResult<C, W, Ch> {\n const codegen: CodegenData = internal?.codegen ?? loadCodegen()\n\n const ws = codegen.accounts[opts.workspace]\n const safesByLabel: Record<string, Account> = {}\n const rolesByLabel: Record<string, Account> = {}\n if (ws) {\n // Only this chain's accounts are in scope. A label resolved from another\n // chain would hand back an address that names nothing here, and the node\n // would still be pushed under this constellation's chain.\n const onThisChain = (byChain: AccountsByChain): AccountsByLabel =>\n byChain[opts.chain] ?? {}\n\n for (const [label, account] of Object.entries(onThisChain(ws.safes))) {\n safesByLabel[label] = account\n }\n for (const [label, account] of Object.entries(onThisChain(ws.rolesMods))) {\n rolesByLabel[label] = account\n }\n }\n\n const meta: ConstellationMeta = {\n label: opts.label,\n chain: opts.chain,\n workspaceId: (ws?.workspaceId ?? '') as UUID,\n }\n\n function makeNodeRef(\n data: Record<string, any>,\n declared: readonly string[]\n ): Readonly<Record<string, any>> {\n return Object.freeze({\n ...data,\n chain: opts.chain,\n _constellation: meta,\n _declared: declared,\n })\n }\n\n function entityAccessor(\n registry: Record<string, Record<string, any>>,\n type: string\n ) {\n const cache = new Map<string, Record<string, any>>()\n return new Proxy({} as Record<string, any>, {\n get(_target: any, name: string) {\n if (typeof name !== 'string') return undefined\n const cached = cache.get(name)\n if (cached) return cached\n const existing = registry[name]\n // Bracket-access keys in the generated codegen carry a\n // ` (0xChecksummed…)` suffix when multiple workspace accounts share\n // a label. The label sent in the push spec should be the clean\n // original, so prefer `existing.label` when it's available.\n const specLabel: string = existing?.label ?? name\n const fn = (overrides?: Record<string, any>) =>\n makeNodeRef(\n {\n type,\n ...(existing || {}),\n ...overrides,\n label: specLabel,\n },\n Object.keys(overrides ?? {})\n )\n // Bare access — `eth.safe['Treasury']` rather than a call — declares\n // nothing. It reads as the account it names, and pushes as a reference.\n Object.assign(fn, {\n type,\n ...(existing || {}),\n label: specLabel,\n chain: opts.chain,\n _constellation: meta,\n _declared: [],\n })\n cache.set(name, fn)\n return fn\n },\n })\n }\n\n function userAccessor() {\n return new Proxy(\n {},\n {\n get(_target: any, name: string) {\n const user = codegen.users[name]\n if (!user) throw new Error(`Unknown user: ${name}`)\n const personalSafe = user.personalSafes[opts.chain]\n if (!personalSafe) {\n throw new Error(\n `User ${name} has no personal safe on chain ${opts.chain}`\n )\n }\n return personalSafe.address\n },\n }\n )\n }\n\n const safe = entityAccessor(safesByLabel, 'SAFE')\n const roles = entityAccessor(rolesByLabel, 'ROLES')\n\n return {\n safe,\n roles,\n user: userAccessor(),\n } as ConstellationResult<C, W, Ch>\n}\n","import type {\n ApplyConstellationPayload,\n ApplyConstellationResult,\n ChainId,\n} from '@zodiaceco/api-types'\nimport { invariant } from '@epic-web/invariant'\nimport {\n assertRecipientChains,\n isDeFiKitEntry,\n toAnnotation,\n} from './permissionEntries'\nimport { ApiClient } from './api'\nimport type {\n ConstellationMeta,\n ConstellationNode,\n ConstellationNodeInternal,\n RoleDef,\n} from './constellation'\n\ntype PushOpts = {\n /** API client instance. Defaults to a client configured from environment variables. */\n api?: ApiClient\n}\n\n/**\n * Applies the given nodes as the next revision of the constellations they\n * belong to, and answers with a url per constellation to review and deploy.\n *\n * Each node travels with what it was actually given, never with the onchain\n * state a `pull` filled it with for reading — so a node passed as a bare\n * reference declares nothing and leaves the account it names alone. Nodes\n * naming each other travel as references and are substituted for addresses\n * once those are derived at deploy.\n *\n * Passing an object names each node by its key; passing an array names them by\n * position. Either way the name is the node's `ref`, which is how the app\n * follows a node from one revision to the next.\n *\n * ```ts\n * const eth = constellation({ workspace: 'GG', label: 'my constellation', chain: 1 })\n * const dao = eth.safe['GG DAO']\n * const roles = eth.roles['New Roles']({ nonce: 0n, target: dao, owner: dao, avatar: dao })\n *\n * await push({ dao, roles })\n * ```\n */\nexport async function push(\n nodes: ConstellationNode[] | { [key: string]: ConstellationNode },\n opts?: PushOpts\n): Promise<ApplyConstellationResult[]> {\n const api = opts?.api ?? new ApiClient()\n const refs = deriveRefs(nodes)\n\n // Group nodes by constellation (multiple constellations can be applied with a single call)\n const groups = new Map<\n string,\n { meta: ConstellationMeta; nodes: ConstellationNodeInternal[] }\n >()\n\n for (const node of refs.byIdentity.keys()) {\n const meta = node._constellation\n invariant(\n meta,\n `Node \"${node.label}\" is not associated with a constellation`\n )\n const key = `${meta.workspaceId}:${meta.chain}:${meta.label}`\n let group = groups.get(key)\n if (!group) {\n group = { meta, nodes: [] }\n groups.set(key, group)\n }\n group.nodes.push(node)\n }\n\n const results: ApplyConstellationResult[] = []\n for (const { meta, nodes: groupNodes } of groups.values()) {\n const specification = groupNodes.map((n) => nodeToSpec(n, refs))\n const result = await api.applyConstellation(meta.workspaceId, {\n label: meta.label,\n chain: meta.chain,\n specification,\n })\n results.push(result)\n }\n\n return results\n}\n\n/**\n * What a ref may be spelled like. Refs come from export names and object keys,\n * so any identifier a node can be declared under has to survive the push —\n * except one opening with `$`, which is how a node references another. A list\n * of nodes refs them by position, so a plain index counts too.\n */\nconst IDENTIFIER = /^(?:[a-zA-Z_][a-zA-Z0-9_$]*|\\d+)$/\n\ntype RefsIndex = {\n byIdentity: Map<ConstellationNodeInternal, string>\n byLabel: Map<string, string>\n}\n\nfunction labelKey(node: ConstellationNodeInternal): string {\n return `${node._constellation.workspaceId}:${node.type}:${node.label}`\n}\n\nfunction deriveRefs(\n nodes: ConstellationNode[] | { [key: string]: ConstellationNode }\n): RefsIndex {\n const byIdentity = new Map<ConstellationNodeInternal, string>()\n const byLabel = new Map<string, string>()\n\n const register = (node: ConstellationNodeInternal, ref: string) => {\n invariant(\n IDENTIFIER.test(ref),\n `Invalid ref \"${ref}\": a ref is a JavaScript identifier, and cannot start with \"$\"`\n )\n byIdentity.set(node, ref)\n byLabel.set(labelKey(node), ref)\n }\n\n if (Array.isArray(nodes)) {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i]\n invariant(\n isConstellationNode(node),\n `unexpected node input at index: ${i}`\n )\n register(node, `${i}`)\n }\n } else {\n for (const [key, node] of Object.entries(nodes)) {\n invariant(\n isConstellationNode(node),\n `unexpected node input under key: ${key}`\n )\n register(node, key)\n }\n }\n\n return { byIdentity, byLabel }\n}\n\n/**\n * What situates a node rather than configures it: which account it is, and how\n * the workspace holds it. Always sent, even for a reference that declares\n * nothing — none of it reaches the onchain diff, and leaving it out would lose\n * the label and the vault the node belongs to.\n *\n * `address` above all. It is what the pull resolved the label to, and the only\n * thing that says which account this is: a node without one is a creation, and\n * its address is derived from whoever deploys. Drop it and a second deployer\n * provisions a duplicate under the same label rather than reconfiguring the\n * account that is already there.\n */\nconst IDENTITY_FIELDS = new Set(['type', 'chain', 'address', 'label', 'vault'])\n\nfunction nodeToSpec(\n node: ConstellationNodeInternal,\n refs: RefsIndex\n): ApplyConstellationPayload['specification'][number] {\n const { id, _constellation, _declared, ...rest } = node as Record<string, any>\n // A node built by hand rather than through an accessor says nothing about\n // what it declares, so there is nothing to go on and everything travels.\n const declared = Array.isArray(_declared) ? new Set<string>(_declared) : null\n const spec: Record<string, any> = {}\n\n for (const [key, value] of Object.entries(rest)) {\n // A referenced node carries the state `pull` read from chain. Sending it\n // back would declare it: an owner added since the pull would be removed\n // again by the deployment, without anyone having written that down. So\n // state travels only when this node actually declares it.\n if (declared != null && !IDENTITY_FIELDS.has(key) && !declared.has(key)) {\n continue\n }\n\n if (node.type === 'ROLES' && key === 'roles' && value != null) {\n spec.roles = describeRoles(\n value as Record<string, RoleDef | null>,\n node.chain,\n refs\n )\n continue\n }\n spec[key] = resolveRefs(value, refs)\n }\n\n spec.ref = refs.byIdentity.get(node)\n invariant(spec.ref != null, 'ref not found')\n\n return stringifyBigints(\n spec\n ) as ApplyConstellationPayload['specification'][number]\n}\n\n/**\n * Entries travel to the API as they were written — parameters and labels — and\n * are compiled when the constellation is deployed. Only a DeFi Kit entry is\n * rendered here, into the annotation uri that names the chain it was written\n * for; the permissions behind it are fetched from that uri at deploy.\n */\nfunction describeRoles(\n roles: Record<string, RoleDef | null>,\n chain: ChainId,\n refs: RefsIndex\n): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(roles).map(([key, def]) => {\n if (def == null) {\n return [key, null] as const\n }\n\n return [\n key,\n {\n key,\n members: resolveRefs(def.members, refs),\n permissions: def.permissions.map((entry) => {\n assertRecipientChains(entry, chain)\n\n return resolveRefs(\n isDeFiKitEntry(entry)\n ? { label: entry.label, annotation: toAnnotation(entry, chain) }\n : entry,\n refs\n )\n }),\n },\n ] as const\n })\n )\n}\n\nfunction resolveRefs(value: unknown, refs: RefsIndex): unknown {\n if (isConstellationNode(value)) {\n const ref = refs.byIdentity.get(value) ?? refs.byLabel.get(labelKey(value))\n invariant(\n ref != null,\n `Node \"${value.label}\" is referenced but not included in the push() call`\n )\n return `$${ref}`\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => resolveRefs(v, refs))\n }\n\n if (typeof value === 'object' && value !== null) {\n const resolved: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n resolved[k] = resolveRefs(v, refs)\n }\n return resolved\n }\n\n // API expects lowercase addresses\n if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {\n return value.toLowerCase()\n }\n\n return value\n}\n\nfunction stringifyBigints(value: unknown): unknown {\n if (typeof value === 'bigint') {\n return value.toString()\n }\n\n if (Array.isArray(value)) {\n return value.map(stringifyBigints)\n }\n\n if (typeof value === 'object' && value !== null) {\n const result: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n result[k] = stringifyBigints(v)\n }\n return result\n }\n\n return value\n}\n\nfunction isConstellationNode(\n value: unknown\n): value is ConstellationNodeInternal {\n if (typeof value === 'function' || typeof value === 'object') {\n const obj = value as any\n return (\n obj != null &&\n typeof obj.type === 'string' &&\n typeof obj.chain === 'number' &&\n typeof obj._constellation === 'object'\n )\n }\n return false\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constellation.ts"],"sourcesContent":["/// <reference path=\"./zodiac-os-codegen.d.ts\" />\nimport type { Address, ChainId } from '@zodiaceco/api-types'\nimport type { PermissionEntry } from './actions'\nimport type { AllowanceSpec } from './types'\nimport { createRequire } from 'module'\nimport { resolveZodiacDir } from './paths'\nimport { UUID } from 'crypto'\n\n/**\n * A role definition keyed by role name. Entries describe what the role may do —\n * parameters and labels, never compiled permissions. They are expanded into\n * `{ targets, annotations }` when the constellation is deployed, so that a\n * stored revision always compiles through the current compilers.\n *\n * A role carrying `policy` is a policy in Zodiac. Its key is the policy's\n * identity, so the same key on several Roles mods is one policy, active on\n * each of their vaults. Only a policy has a `description`.\n */\nexport type RoleDef = {\n members: readonly AddressOrRef[]\n permissions: readonly PermissionEntry[]\n} & (\n | {\n /** Marks the role as a policy, labelled with this value. Never reaches\n * the chain. */\n policy: string\n /** Describes the policy in Zodiac. Never reaches the chain. */\n description?: string\n }\n | { policy?: undefined; description?: undefined }\n)\n\ntype User = {\n id: UUID\n fullName: string\n personalSafes: Record<\n number,\n { address: Lowercase<Address>; active: boolean }\n >\n}\n\ntype Account = {\n id: UUID\n label: string\n address: Lowercase<Address>\n chain: ChainId\n /** True for accounts promoted to a workspace vault. */\n vault: boolean\n}\n\ntype AccountsByLabel = Readonly<Record<string, Account>>\n\n/**\n * A workspace's accounts, grouped by node type and then by chain.\n *\n * Both groupings keep bracket-accessor namespaces apart, so that a label is\n * only ever ambiguous between accounts a constellation could actually choose\n * between: a SAFE and a ROLES mod sharing a label don't collide, and neither\n * do two safes of the same name on different chains.\n */\ntype WorkspaceAccounts = {\n workspaceId: UUID\n workspaceName: string\n safes: AccountsByChain\n rolesMods: AccountsByChain\n delays: AccountsByChain\n}\n\ntype AccountsByChain = { readonly [chain in ChainId]?: AccountsByLabel }\n\n/** Shape of the codegen data produced by `zodiac pull-org`. */\nexport type CodegenData = {\n users: Readonly<Record<string, User>>\n accounts: Readonly<Record<string, WorkspaceAccounts>>\n}\n\n// If `pull-org` has been run, the consumer's `.zodiac/index.d.ts` augments\n// `ZodiacGeneratedCodegen` with literal `users`/`accounts` shapes. Otherwise\n// the interface is empty and we fall back to the wide `CodegenData`.\ntype GeneratedCodegen = ZodiacGeneratedCodegen extends CodegenData\n ? ZodiacGeneratedCodegen\n : CodegenData\n\ntype ConstellationOpts<C extends CodegenData> = {\n /** Workspace to scope accounts and roles to. */\n workspace: keyof C['accounts'] & string\n /** Human-readable label for this constellation. */\n label: string\n /** Target chain for all nodes in this constellation. */\n chain: ChainId\n}\n\ntype ConstellationInternalOpts<C extends CodegenData> = {\n /** Injected codegen data (used for testing). */\n codegen?: C\n}\n\ntype Prettify<T> = { readonly [K in keyof T]: T[K] } & {}\n\n/**\n * The accounts of one type that a constellation on `Ch` can name. Accounts on\n * every other chain are not in scope: their addresses mean nothing here, so\n * offering them would only invite a node that carries a foreign address under\n * this constellation's chain.\n */\ntype ChainEntries<A, Ch extends ChainId> = Ch extends keyof A\n ? NonNullable<A[Ch]>\n : {}\n\ntype SafeEntries<\n C extends CodegenData,\n W extends keyof C['accounts'],\n Ch extends ChainId,\n> = ChainEntries<C['accounts'][W]['safes'], Ch>\n\ntype RolesEntries<\n C extends CodegenData,\n W extends keyof C['accounts'],\n Ch extends ChainId,\n> = ChainEntries<C['accounts'][W]['rolesMods'], Ch>\n\ntype DelayEntries<\n C extends CodegenData,\n W extends keyof C['accounts'],\n Ch extends ChainId,\n> = ChainEntries<C['accounts'][W]['delays'], Ch>\n\ntype NodeType = 'SAFE' | 'ROLES' | 'DELAY'\n\n/** A reference to a node used in `owners`, `modules`, `target`, etc. */\nexport type NodeRef = Readonly<{\n type: NodeType\n label: string\n chain: ChainId\n}>\n\n/** A blockchain address (checksummed or lowercase) or a reference to another\n * node in the constellation. Values are normalized to lowercase before being\n * sent to the API. */\ntype AddressOrRef = Address | NodeRef\n\ntype NodeBase = Readonly<{\n /** Human-readable identifier, unique within the constellation. */\n label: string\n /** Chain the node is deployed on. */\n chain: ChainId\n /** Set for existing nodes (from codegen or bound by address), absent for new\n * nodes. Accepts checksummed or lowercase; normalized to lowercase on push. */\n address?: Address\n /** Deployment nonce — required for new nodes, optional for existing. */\n nonce?: bigint\n}>\n\n/** A safe node — a reference to one the workspace already has, or a new one\n * with the config it is deployed with. */\nexport type SafeNode = NodeBase &\n Readonly<{\n /** Discriminator identifying this node as a Safe. */\n type: 'SAFE'\n /** Number of owner signatures required to execute a transaction. */\n threshold: number\n /** Safe owner addresses or node references. */\n owners: readonly (string | NodeRef)[]\n /** Module addresses or node references enabled on the safe. */\n modules?: readonly (string | NodeRef)[]\n /** Whether this safe shall appear as a vault in the workspace. @default false */\n vault?: boolean\n }>\n\n/** A roles modifier node — a reference to one the workspace already has, or a\n * new one with the config it is deployed with. */\nexport type RolesNode = NodeBase &\n Readonly<{\n /** Discriminator identifying this node as a Roles modifier. */\n type: 'ROLES'\n /** The safe that this roles modifier controls. */\n target?: AddressOrRef\n /** The account that is allowed to update the configuration of the Roles mod. */\n owner?: AddressOrRef\n /** The account that calls will be executed from. */\n avatar?: AddressOrRef\n /** MultiSend contract addresses for batched transactions. */\n multisend?: readonly Address[]\n /** Role definitions configured on this modifier. Pass `null` for a key to\n * clear that role; unmentioned roles are left untouched. */\n roles?: Record<string, RoleDef | null>\n /** Spending allowances configured on this modifier, keyed by name. Pass\n * `null` for a key to clear that allowance; unmentioned allowances are\n * left untouched. */\n allowances?: Record<string, AllowanceSpec | null>\n }>\n\n/** A delay modifier node — a reference to one the workspace already has, or a\n * new one with the config it is deployed with. */\nexport type DelayNode = NodeBase &\n Readonly<{\n /** Discriminator identifying this node as a Delay modifier. */\n type: 'DELAY'\n /** The safe that this delay modifier controls. */\n target?: AddressOrRef\n /** The account that is allowed to update the configuration of the Delay mod. */\n owner?: AddressOrRef\n /** The account that calls will be executed from. */\n avatar?: AddressOrRef\n /** Seconds a queued transaction waits before it becomes executable. */\n cooldown?: bigint\n /** Seconds a transaction stays executable once its cooldown has passed. */\n expiration?: bigint\n /** Module addresses or node references enabled on the delay. */\n modules?: readonly (string | NodeRef)[]\n }>\n\n/** Any complete node that can be passed to `push()`. */\nexport type ConstellationNode = SafeNode | RolesNode | DelayNode\nexport type ConstellationNodeInternal = ConstellationNode & {\n _constellation: ConstellationMeta\n /**\n * The state fields this node actually declares.\n *\n * A referenced node carries everything `pull` read from chain so it reads\n * well and completes in an editor, but a reference is not a declaration —\n * only what was passed explicitly is pushed. Empty for a bare reference.\n */\n _declared: readonly string[]\n}\n\ntype NewSafeProps = {\n /** Deployment nonce for CREATE2 address derivation. */\n nonce: bigint\n /** Number of owner signatures required to execute a transaction. */\n threshold: number\n /** Safe owner addresses or node references. */\n owners: readonly AddressOrRef[]\n /** Module addresses or node references to enable on the safe. */\n modules?: readonly AddressOrRef[]\n /** Whether this safe is a workspace vault. @default false */\n vault?: boolean\n}\n\n/** Configuration shared by both ways of declaring a roles modifier. */\ntype RolesConfig = {\n /** The safe that this roles modifier controls. Defaults to the new safe with the same label, when one exists. */\n target?: AddressOrRef\n /** The account that calls will be executed from. Defaults to `target` value */\n avatar?: AddressOrRef\n /** The account that is allowed to update the configuration of the Roles Mod. Defaults to `target` value */\n owner?: AddressOrRef\n /** MultiSend contract addresses for batched transactions. Defaults to `['0x38869bf66a61cf6bdb996a6ae40d5853fd43b526', '0x9641d764fc13c8b624c04430c7356c1c7c8102e2']` */\n multisend?: readonly Address[]\n /** Role definitions to configure on this modifier. Pass `null` for a key to\n * clear that role; unmentioned roles are left untouched. */\n roles?: Record<string, RoleDef | null>\n /** Spending allowances to configure on this modifier, keyed by name. Pass\n * `null` for a key to clear that allowance; unmentioned allowances are\n * left untouched. */\n allowances?: Record<string, AllowanceSpec | null>\n}\n\n/** Declare a brand-new roles modifier (address derived via CREATE2 from `nonce`). */\ntype NewRolesByNonce = RolesConfig & {\n /** Deployment nonce for CREATE2 address derivation. */\n nonce: bigint\n}\n\n/** Bind to a roles modifier already deployed on-chain at a known address, to\n * reconfigure its roles/allowances. Use this for mods not (yet) imported into\n * the workspace — pass the address instead of a deployment `nonce`. */\ntype ExistingRolesByAddress = RolesConfig & {\n /** Address of the existing Roles mod to bind to and reconfigure. */\n address: Address\n}\n\ntype NewRolesProps = NewRolesByNonce | ExistingRolesByAddress\n\n/** Configuration shared by both ways of declaring a delay modifier. */\ntype DelayConfig = {\n /** The safe that this delay modifier controls. Defaults to the new safe with the same label, when one exists. */\n target?: AddressOrRef\n /** The account that calls will be executed from. Defaults to `target` value */\n avatar?: AddressOrRef\n /** The account that is allowed to update the configuration of the Delay Mod. Defaults to `target` value */\n owner?: AddressOrRef\n /** Module addresses or node references allowed to queue transactions through\n * this delay. A complete array replaces the enabled modules. */\n modules?: readonly AddressOrRef[]\n}\n\n/** Declare a brand-new delay modifier (address derived via CREATE2 from `nonce`). */\ntype NewDelayByNonce = DelayConfig & {\n /** Deployment nonce for CREATE2 address derivation. */\n nonce: bigint\n /** Seconds a queued transaction waits before it becomes executable. */\n cooldown: bigint\n /** Seconds a transaction stays executable once its cooldown has passed. */\n expiration: bigint\n}\n\n/** Bind to a delay modifier already deployed on-chain at a known address, to\n * reconfigure its cooldown, expiration or modules. Use this for mods not (yet)\n * imported into the workspace — pass the address instead of a deployment\n * `nonce`. */\ntype ExistingDelayByAddress = DelayConfig & {\n /** Address of the existing Delay mod to bind to and reconfigure. */\n address: Address\n /** Seconds a queued transaction waits before it becomes executable. */\n cooldown?: bigint\n /** Seconds a transaction stays executable once its cooldown has passed. */\n expiration?: bigint\n}\n\ntype NewDelayProps = NewDelayByNonce | ExistingDelayByAddress\n\ntype ExistingNodeAccessor<\n Type extends string,\n K extends string,\n E,\n Ch extends ChainId,\n NP extends Record<string, any>,\n> = Readonly<Prettify<E & { type: Type; label: K; chain: Ch }>> &\n (<\n const O extends {\n [P in Exclude<keyof E & string, 'id' | 'label'>]?: any\n } & Partial<NP> = {},\n >(\n overrides?: {\n [P in Exclude<keyof E & string, 'id' | 'label'>]?: any\n } & Partial<NP> &\n O\n ) => Readonly<\n Prettify<\n Omit<E, keyof O> & O & Partial<NP> & { type: Type; label: K; chain: Ch }\n >\n >)\n\ntype NewNodeAccessor<\n Type extends string,\n Ch extends ChainId,\n NP extends Record<string, any>,\n> = Readonly<Prettify<{ type: Type; label: string; chain: Ch }>> &\n // `const` so a node reads back the values it was written with — `threshold: 2`\n // rather than `number`. Every prop it widens into is already `readonly`, so\n // the tuples this infers stay assignable.\n (<const P extends NP>(\n props: P\n ) => Readonly<Prettify<P & { type: Type; label: string; chain: Ch }>>)\n\ntype EntityAccessor<\n Type extends string,\n Entries extends Record<string, any>,\n Ch extends ChainId = ChainId,\n NP extends Record<string, any> = Record<string, any>,\n> = {\n readonly [K in keyof Entries & string]: ExistingNodeAccessor<\n Type,\n K,\n Entries[K],\n Ch,\n NP\n >\n} & {\n readonly [key: string]: NewNodeAccessor<Type, Ch, NP>\n}\n\ntype UserAccessor<C extends CodegenData, Ch extends number> = {\n readonly [K in keyof C['users'] &\n string]: C['users'][K]['personalSafes'][Ch]['address']\n}\n\ntype ConstellationResult<\n C extends CodegenData,\n W extends keyof C['accounts'] = keyof C['accounts'],\n Ch extends ChainId = ChainId,\n> = {\n /** Access existing safes by label or create new ones with a new label.\n * Only SAFE-typed accounts are suggested in IntelliSense. */\n safe: EntityAccessor<'SAFE', SafeEntries<C, W, Ch>, Ch, NewSafeProps>\n /** Access existing roles modifiers by label or create new ones with a\n * new label. Only ROLES-typed accounts are suggested in IntelliSense. */\n roles: EntityAccessor<'ROLES', RolesEntries<C, W, Ch>, Ch, NewRolesProps>\n /** Access existing delay modifiers by label or create new ones with a\n * new label. Only DELAY-typed accounts are suggested in IntelliSense. */\n delay: EntityAccessor<'DELAY', DelayEntries<C, W, Ch>, Ch, NewDelayProps>\n /** Resolve a user's personal safe address on the constellation's chain. */\n user: UserAccessor<C, Ch>\n}\n\n/** @internal */\nexport type ConstellationMeta = {\n label: string\n chain: ChainId\n workspaceId: UUID\n}\n\nfunction loadCodegen(): CodegenData {\n const require = createRequire(import.meta.url)\n return require(resolveZodiacDir()) as CodegenData\n}\n\n/**\n * Creates a constellation scoped to a workspace and chain.\n *\n * Bracket access names an account of that workspace **on that chain** — a\n * vault, or a node an earlier constellation deployed. A label the chain has no\n * account for reads as a new node instead, so it has to be given the config it\n * is deployed with.\n *\n * ```ts\n * const eth = constellation({ workspace: 'GG', label: 'my constellation', chain: 1 })\n *\n * const dao = eth.safe['GG DAO'] // the safe of that name on chain 1\n * const roles = eth.roles['GG DAO'] // the roles mod of that name\n * const newSafe = eth.safe['New Safe']({ nonce: 0n, threshold: 2, owners: [...], modules: [...] })\n * const timelock = eth.delay['Timelock']({ nonce: 0n, target: dao, cooldown: 86400n, expiration: 0n })\n * ```\n *\n * Names come from the last `pull-org`, so an account deployed since then is\n * not one yet.\n */\nexport function constellation<\n const C extends CodegenData = GeneratedCodegen,\n const W extends keyof C['accounts'] & string = keyof C['accounts'] & string,\n const Ch extends ChainId = ChainId,\n>(\n opts: ConstellationOpts<C> & { workspace: W; chain: Ch },\n internal?: ConstellationInternalOpts<C>\n): ConstellationResult<C, W, Ch> {\n const codegen: CodegenData = internal?.codegen ?? loadCodegen()\n\n const ws = codegen.accounts[opts.workspace]\n const safesByLabel: Record<string, Account> = {}\n const rolesByLabel: Record<string, Account> = {}\n const delaysByLabel: Record<string, Account> = {}\n if (ws) {\n // Only this chain's accounts are in scope. A label resolved from another\n // chain would hand back an address that names nothing here, and the node\n // would still be pushed under this constellation's chain.\n const onThisChain = (byChain: AccountsByChain): AccountsByLabel =>\n byChain[opts.chain] ?? {}\n\n for (const [label, account] of Object.entries(onThisChain(ws.safes))) {\n safesByLabel[label] = account\n }\n for (const [label, account] of Object.entries(onThisChain(ws.rolesMods))) {\n rolesByLabel[label] = account\n }\n for (const [label, account] of Object.entries(onThisChain(ws.delays))) {\n delaysByLabel[label] = account\n }\n }\n\n const meta: ConstellationMeta = {\n label: opts.label,\n chain: opts.chain,\n workspaceId: (ws?.workspaceId ?? '') as UUID,\n }\n\n function makeNodeRef(\n data: Record<string, any>,\n declared: readonly string[]\n ): Readonly<Record<string, any>> {\n return Object.freeze({\n ...data,\n chain: opts.chain,\n _constellation: meta,\n _declared: declared,\n })\n }\n\n function entityAccessor(\n registry: Record<string, Record<string, any>>,\n type: string\n ) {\n const cache = new Map<string, Record<string, any>>()\n return new Proxy({} as Record<string, any>, {\n get(_target: any, name: string) {\n if (typeof name !== 'string') return undefined\n const cached = cache.get(name)\n if (cached) return cached\n const existing = registry[name]\n // Bracket-access keys in the generated codegen carry a\n // ` (0xChecksummed…)` suffix when multiple workspace accounts share\n // a label. The label sent in the push spec should be the clean\n // original, so prefer `existing.label` when it's available.\n const specLabel: string = existing?.label ?? name\n const fn = (overrides?: Record<string, any>) =>\n makeNodeRef(\n {\n type,\n ...(existing || {}),\n ...overrides,\n label: specLabel,\n },\n Object.keys(overrides ?? {})\n )\n // Bare access — `eth.safe['Treasury']` rather than a call — declares\n // nothing. It reads as the account it names, and pushes as a reference.\n Object.assign(fn, {\n type,\n ...(existing || {}),\n label: specLabel,\n chain: opts.chain,\n _constellation: meta,\n _declared: [],\n })\n cache.set(name, fn)\n return fn\n },\n })\n }\n\n function userAccessor() {\n return new Proxy(\n {},\n {\n get(_target: any, name: string) {\n const user = codegen.users[name]\n if (!user) throw new Error(`Unknown user: ${name}`)\n const personalSafe = user.personalSafes[opts.chain]\n if (!personalSafe) {\n throw new Error(\n `User ${name} has no personal safe on chain ${opts.chain}`\n )\n }\n return personalSafe.address\n },\n }\n )\n }\n\n const safe = entityAccessor(safesByLabel, 'SAFE')\n const roles = entityAccessor(rolesByLabel, 'ROLES')\n const delay = entityAccessor(delaysByLabel, 'DELAY')\n\n return {\n safe,\n roles,\n delay,\n user: userAccessor(),\n } as ConstellationResult<C, W, Ch>\n}\n"],"mappings":";;;;;;AAyYA,SAAS,cAA2B;AAElC,QADgB,cAAc,OAAO,KAAK,IAAI,CAC/B,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBpC,SAAgB,cAKd,MACA,UAC+B;CAC/B,MAAM,UAAuB,UAAU,WAAW,aAAa;CAE/D,MAAM,KAAK,QAAQ,SAAS,KAAK;CACjC,MAAM,eAAwC,EAAE;CAChD,MAAM,eAAwC,EAAE;CAChD,MAAM,gBAAyC,EAAE;AACjD,KAAI,IAAI;EAIN,MAAM,eAAe,YACnB,QAAQ,KAAK,UAAU,EAAE;AAE3B,OAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,YAAY,GAAG,MAAM,CAAC,CAClE,cAAa,SAAS;AAExB,OAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,YAAY,GAAG,UAAU,CAAC,CACtE,cAAa,SAAS;AAExB,OAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,YAAY,GAAG,OAAO,CAAC,CACnE,eAAc,SAAS;;CAI3B,MAAM,OAA0B;EAC9B,OAAO,KAAK;EACZ,OAAO,KAAK;EACZ,aAAc,IAAI,eAAe;EAClC;CAED,SAAS,YACP,MACA,UAC+B;AAC/B,SAAO,OAAO,OAAO;GACnB,GAAG;GACH,OAAO,KAAK;GACZ,gBAAgB;GAChB,WAAW;GACZ,CAAC;;CAGJ,SAAS,eACP,UACA,MACA;EACA,MAAM,wBAAQ,IAAI,KAAkC;AACpD,SAAO,IAAI,MAAM,EAAE,EAAyB,EAC1C,IAAI,SAAc,MAAc;AAC9B,OAAI,OAAO,SAAS,SAAU,QAAO,KAAA;GACrC,MAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,OAAI,OAAQ,QAAO;GACnB,MAAM,WAAW,SAAS;GAK1B,MAAM,YAAoB,UAAU,SAAS;GAC7C,MAAM,MAAM,cACV,YACE;IACE;IACA,GAAI,YAAY,EAAE;IAClB,GAAG;IACH,OAAO;IACR,EACD,OAAO,KAAK,aAAa,EAAE,CAAC,CAC7B;AAGH,UAAO,OAAO,IAAI;IAChB;IACA,GAAI,YAAY,EAAE;IAClB,OAAO;IACP,OAAO,KAAK;IACZ,gBAAgB;IAChB,WAAW,EAAE;IACd,CAAC;AACF,SAAM,IAAI,MAAM,GAAG;AACnB,UAAO;KAEV,CAAC;;CAGJ,SAAS,eAAe;AACtB,SAAO,IAAI,MACT,EAAE,EACF,EACE,IAAI,SAAc,MAAc;GAC9B,MAAM,OAAO,QAAQ,MAAM;AAC3B,OAAI,CAAC,KAAM,OAAM,IAAI,MAAM,iBAAiB,OAAO;GACnD,MAAM,eAAe,KAAK,cAAc,KAAK;AAC7C,OAAI,CAAC,aACH,OAAM,IAAI,MACR,QAAQ,KAAK,iCAAiC,KAAK,QACpD;AAEH,UAAO,aAAa;KAEvB,CACF;;AAOH,QAAO;EACL,MALW,eAAe,cAAc,OAAO;EAM/C,OALY,eAAe,cAAc,QAAQ;EAMjD,OALY,eAAe,eAAe,QAAQ;EAMlD,MAAM,cAAc;EACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"networks-
|
|
1
|
+
{"version":3,"file":"networks-UKtJ8QDG.mjs","names":[],"sources":["../src/allow/abi.ts","../src/allow/networks.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport type { Abi } from './fetch'\n\nexport type ContractAddress = `0x${string}`\n\nexport type ContractNode = {\n chain: string\n segments: string[]\n address: ContractAddress\n}\n\nexport function* walkContracts(\n config: Record<string, any>\n): Generator<ContractNode> {\n for (const [chain, contracts] of Object.entries(config)) {\n yield* walkLevel(chain, [], contracts)\n }\n}\n\nfunction* walkLevel(\n chain: string,\n segments: string[],\n node: unknown\n): Generator<ContractNode> {\n if (typeof node === 'string') {\n if (segments.length === 0) {\n throw new Error(`Contract at ${chain} is missing a name`)\n }\n if (!node.startsWith('0x')) {\n throw new Error(\n `Invalid address for ${chain}.${segments.join('.')}: ${node}`\n )\n }\n yield { chain, segments, address: node as ContractAddress }\n return\n }\n if (node && typeof node === 'object') {\n for (const [key, value] of Object.entries(node as Record<string, any>)) {\n yield* walkLevel(chain, [...segments, key], value)\n }\n return\n }\n throw new Error(\n `Invalid contracts entry at ${chain}.${segments.join('.')}: ${JSON.stringify(node)}`\n )\n}\n\nexport const abiFilePath = (abisDir: string, node: ContractNode): string =>\n path.join(abisDir, node.chain, ...node.segments) + '.json'\n\nexport const readAbi = (abisDir: string, node: ContractNode): Abi | null => {\n const file = abiFilePath(abisDir, node)\n if (!fs.existsSync(file)) return null\n const raw = fs.readFileSync(file, 'utf8')\n const parsed = JSON.parse(raw)\n if (!Array.isArray(parsed)) {\n throw new Error(`ABI at ${file} is not a JSON array`)\n }\n return parsed as Abi\n}\n\nexport const writeAbi = (\n abisDir: string,\n node: ContractNode,\n abi: Abi\n): void => {\n const file = abiFilePath(abisDir, node)\n fs.mkdirSync(path.dirname(file), { recursive: true })\n fs.writeFileSync(file, JSON.stringify(abi, null, 2) + '\\n', 'utf8')\n}\n","export const CHAIN_IDS = {\n eth: 1,\n oeth: 10,\n gno: 100,\n sep: 11155111,\n matic: 137,\n zkevm: 1101,\n arb1: 42161,\n avax: 43114,\n base: 8453,\n basesep: 84532,\n bnb: 56,\n celo: 42220,\n sonic: 146,\n berachain: 80094,\n unichain: 130,\n worldchain: 480,\n bob: 60808,\n mantle: 5000,\n hemi: 43111,\n katana: 747474,\n linea: 59144,\n ink: 57073,\n hyperevm: 999,\n flare: 14,\n scroll: 534352,\n plasma: 9745,\n megaeth: 4326,\n} as const satisfies Record<string, number>\n\nexport type ChainPrefix = keyof typeof CHAIN_IDS\n\nexport const chainIdFor = (prefix: string): number => {\n const id = (CHAIN_IDS as Record<string, number>)[prefix]\n if (id === undefined) {\n throw new Error(`Unknown chain prefix: ${prefix}`)\n }\n return id\n}\n"],"mappings":";;;AAYA,UAAiB,cACf,QACyB;AACzB,MAAK,MAAM,CAAC,OAAO,cAAc,OAAO,QAAQ,OAAO,CACrD,QAAO,UAAU,OAAO,EAAE,EAAE,UAAU;;AAI1C,UAAU,UACR,OACA,UACA,MACyB;AACzB,KAAI,OAAO,SAAS,UAAU;AAC5B,MAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,eAAe,MAAM,oBAAoB;AAE3D,MAAI,CAAC,KAAK,WAAW,KAAK,CACxB,OAAM,IAAI,MACR,uBAAuB,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,IAAI,OACxD;AAEH,QAAM;GAAE;GAAO;GAAU,SAAS;GAAyB;AAC3D;;AAEF,KAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAA4B,CACpE,QAAO,UAAU,OAAO,CAAC,GAAG,UAAU,IAAI,EAAE,MAAM;AAEpD;;AAEF,OAAM,IAAI,MACR,8BAA8B,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,IAAI,KAAK,UAAU,KAAK,GACnF;;AAGH,MAAa,eAAe,SAAiB,SAC3C,KAAK,KAAK,SAAS,KAAK,OAAO,GAAG,KAAK,SAAS,GAAG;AAErD,MAAa,WAAW,SAAiB,SAAmC;CAC1E,MAAM,OAAO,YAAY,SAAS,KAAK;AACvC,KAAI,CAAC,GAAG,WAAW,KAAK,CAAE,QAAO;CACjC,MAAM,MAAM,GAAG,aAAa,MAAM,OAAO;CACzC,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,MAAM,UAAU,KAAK,sBAAsB;AAEvD,QAAO;;AAGT,MAAa,YACX,SACA,MACA,QACS;CACT,MAAM,OAAO,YAAY,SAAS,KAAK;AACvC,IAAG,UAAU,KAAK,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACrD,IAAG,cAAc,MAAM,KAAK,UAAU,KAAK,MAAM,EAAE,GAAG,MAAM,OAAO;;;;ACrErE,MAAa,YAAY;CACvB,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,MAAM;CACN,SAAS;CACT,KAAK;CACL,MAAM;CACN,OAAO;CACP,WAAW;CACX,UAAU;CACV,YAAY;CACZ,KAAK;CACL,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,OAAO;CACP,KAAK;CACL,UAAU;CACV,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACV;AAID,MAAa,cAAc,WAA2B;CACpD,MAAM,KAAM,UAAqC;AACjD,KAAI,OAAO,KAAA,EACT,OAAM,IAAI,MAAM,yBAAyB,SAAS;AAEpD,QAAO"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
//#region src/paths.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the project's `.zodiac/` directory — the shared home for SDK
|
|
6
|
+
* codegen (`pull-org` emits the importable module here, `pull-contracts`
|
|
7
|
+
* writes `allow.d.ts` alongside it).
|
|
8
|
+
*
|
|
9
|
+
* Pass `rootDir` to anchor explicitly (CLI commands do this using the
|
|
10
|
+
* directory of the loaded `zodiac.config.ts`). Without a `rootDir`, walks
|
|
11
|
+
* up from `cwd` to the nearest `zodiac.config.{ts,js,mjs,cjs}` so runtime
|
|
12
|
+
* callers like `constellation()` work even when invoked from a subdirectory.
|
|
13
|
+
*/
|
|
14
|
+
function resolveZodiacDir(rootDir) {
|
|
15
|
+
return join(rootDir ?? findProjectRoot(), ".zodiac");
|
|
16
|
+
}
|
|
17
|
+
const CONFIG_FILENAMES = [
|
|
18
|
+
"zodiac.config.ts",
|
|
19
|
+
"zodiac.config.js",
|
|
20
|
+
"zodiac.config.mjs",
|
|
21
|
+
"zodiac.config.cjs"
|
|
22
|
+
];
|
|
23
|
+
function findProjectRoot() {
|
|
24
|
+
let dir = process.cwd();
|
|
25
|
+
while (dir !== dirname(dir)) {
|
|
26
|
+
if (CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)))) return dir;
|
|
27
|
+
dir = dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
return process.cwd();
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { resolveZodiacDir as t };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=paths-LsD6ULk0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths-LsD6ULk0.mjs","names":[],"sources":["../src/paths.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\n/**\n * Resolve the project's `.zodiac/` directory — the shared home for SDK\n * codegen (`pull-org` emits the importable module here, `pull-contracts`\n * writes `allow.d.ts` alongside it).\n *\n * Pass `rootDir` to anchor explicitly (CLI commands do this using the\n * directory of the loaded `zodiac.config.ts`). Without a `rootDir`, walks\n * up from `cwd` to the nearest `zodiac.config.{ts,js,mjs,cjs}` so runtime\n * callers like `constellation()` work even when invoked from a subdirectory.\n */\nexport function resolveZodiacDir(rootDir?: string): string {\n return join(rootDir ?? findProjectRoot(), '.zodiac')\n}\n\nconst CONFIG_FILENAMES = [\n 'zodiac.config.ts',\n 'zodiac.config.js',\n 'zodiac.config.mjs',\n 'zodiac.config.cjs',\n]\n\nfunction findProjectRoot(): string {\n let dir = process.cwd()\n while (dir !== dirname(dir)) {\n if (CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)))) return dir\n dir = dirname(dir)\n }\n return process.cwd()\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,iBAAiB,SAA0B;AACzD,QAAO,KAAK,WAAW,iBAAiB,EAAE,UAAU;;AAGtD,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACD;AAED,SAAS,kBAA0B;CACjC,IAAI,MAAM,QAAQ,KAAK;AACvB,QAAO,QAAQ,QAAQ,IAAI,EAAE;AAC3B,MAAI,iBAAiB,MAAM,SAAS,WAAW,KAAK,KAAK,KAAK,CAAC,CAAC,CAAE,QAAO;AACzE,QAAM,QAAQ,IAAI;;AAEpB,QAAO,QAAQ,KAAK"}
|
|
@@ -42,6 +42,8 @@ const assertRecipientChains = (entry, chain) => {
|
|
|
42
42
|
const label = "label" in entry ? `"${entry.label}"` : "a transfer";
|
|
43
43
|
throw new Error(`A recipient of ${label} lives on chain "${recipientChain}", but the role is on chain "${chain}". Use \`bridge\` for cross-chain recipients.`);
|
|
44
44
|
}
|
|
45
|
+
if (!("action" in entry) || entry.action.type !== "transfer") return;
|
|
46
|
+
for (const target of entry.action.bridge ?? []) if (target.chain === chain) throw new Error(`"${entry.label}" bridges to chain "${chain}", the role's own chain. Name those recipients in \`to\` instead.`);
|
|
45
47
|
};
|
|
46
48
|
/**
|
|
47
49
|
* Search params are appended one value at a time, the form annotation uris are
|
|
@@ -58,4 +60,4 @@ const searchParams = (params) => {
|
|
|
58
60
|
//#endregion
|
|
59
61
|
export { toAnnotation as i, isDeFiKitEntry as n, recipientChains as r, assertRecipientChains as t };
|
|
60
62
|
|
|
61
|
-
//# sourceMappingURL=permissionEntries-
|
|
63
|
+
//# sourceMappingURL=permissionEntries-C4dAwZBb.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionEntries-C4dAwZBb.mjs","names":[],"sources":["../src/permissionEntries.ts"],"sourcesContent":["import type { Address, ChainId } from '@zodiaceco/api-types'\nimport type { Permission } from 'zodiac-roles-sdk'\nimport type { NodeRef } from './constellation'\n\n/**\n * The shapes a role's `permissions` travel to the API in, and the one piece of\n * rendering `push()` still does. Internal: the helpers that build these live in\n * `./actions`, and nothing here is part of the published interface beyond the\n * `PermissionEntry` type that module re-exports.\n */\n\n/**\n * The base url of the DeFi Kit API. Annotations point back at it so that a role\n * can be read as presets again, and so that a DeFi Kit entry written in code is\n * indistinguishable from one configured in the app.\n */\nconst DEFI_KIT_BASE_URL = 'https://kit.kpk.io/api/v1'\n\n/** The chains DeFi Kit serves, by the prefix it addresses them with. */\nconst DEFI_KIT_CHAIN_PREFIXES: Record<number, string> = {\n 1: 'eth',\n 10: 'oeth',\n 100: 'gno',\n 8453: 'base',\n 42161: 'arb1',\n}\n\n/** Renders a DeFi Kit entry into the annotation it stands for on `chain`. */\nexport const toAnnotation = (\n { defiKit: { protocol, verb, params } }: DeFiKitEntry,\n chain: ChainId\n) => {\n const prefix = DEFI_KIT_CHAIN_PREFIXES[chain]\n\n if (prefix == null) {\n throw new Error(`DeFi Kit does not serve chain \"${chain}\"`)\n }\n\n return {\n uri: `${DEFI_KIT_BASE_URL}/permissions/${prefix}/${protocol}/${verb}?${searchParams(params)}`,\n schema: `${DEFI_KIT_BASE_URL}/openapi.json`,\n }\n}\n\nexport const isDeFiKitEntry = (entry: PermissionEntry): entry is DeFiKitEntry =>\n typeof entry === 'object' && entry !== null && 'defiKit' in entry\n\n/**\n * The chains of the node recipients a `transfer()`'s same-chain `to` named.\n * Symbol-keyed so it never serializes; `push()` checks it against the chain of\n * the roles node the entry ends up on. A transfer to a node on another chain\n * would permit sending funds to an address that may not exist there.\n */\nexport const recipientChains = Symbol('recipientChains')\n\nexport const assertRecipientChains = (\n entry: PermissionEntry,\n chain: ChainId\n): void => {\n const chains = (entry as { [recipientChains]?: readonly ChainId[] })[\n recipientChains\n ]\n\n for (const recipientChain of chains ?? []) {\n if (recipientChain !== chain) {\n const label = 'label' in entry ? `\"${entry.label}\"` : 'a transfer'\n throw new Error(\n `A recipient of ${label} lives on chain \"${recipientChain}\", but the role is on chain \"${chain}\". Use \\`bridge\\` for cross-chain recipients.`\n )\n }\n }\n\n if (!('action' in entry) || entry.action.type !== 'transfer') {\n return\n }\n\n for (const target of entry.action.bridge ?? []) {\n if (target.chain === chain) {\n throw new Error(\n `\"${entry.label}\" bridges to chain \"${chain}\", the role's own chain. Name those recipients in \\`to\\` instead.`\n )\n }\n }\n}\n\n/**\n * Search params are appended one value at a time, the form annotation uris are\n * normalized to before they are matched back against a preset.\n */\nconst searchParams = (params: Record<string, DeFiKitParamValue>) => {\n const searchParams = new URLSearchParams()\n\n for (const [name, value] of Object.entries(params)) {\n if (value == null) {\n continue\n }\n\n for (const entry of Array.isArray(value) ? value : [value]) {\n searchParams.append(name, String(entry))\n }\n }\n\n return searchParams\n}\n\n/**\n * An allowance key. Authored as a plain label, the way a role key is, and\n * encoded to bytes32 when the constellation is deployed. An already-encoded\n * key still works — `encodeKey` passes 32-byte hex through unchanged.\n */\nexport type AllowanceKey = string\n\nexport type SwapEntry = {\n label: string\n action: { type: 'swap'; sell: Address[]; buy: Address[] }\n}\n\nexport type TransferEntry = {\n label: string\n action: {\n type: 'transfer'\n tokens: Address[]\n to: Address[]\n bridge?: { chain: ChainId; to: Address[]; receive: Address[] }[]\n allowance?: AllowanceKey\n }\n}\n\n/**\n * A permission's target, written either as an address or as another node in the\n * same constellation.\n *\n * A policy is usually pushed alongside the accounts it governs, so the address\n * a permission points at often does not exist yet. Naming the node instead\n * sends a reference, and the address is substituted once it is derived at\n * deploy — the same way a node is named as an owner, a module or an avatar.\n *\n * `NodeRef` for the same reason those fields use it: an uninvoked accessor is\n * a forward reference to a node, and is how two nodes that need each other are\n * written. It carries the label a reference resolves by without being a\n * complete node.\n */\nexport type PermissionTarget = `0x${string}` | NodeRef\n\ntype WithNodeTarget<P> = P extends { targetAddress: `0x${string}` }\n ? Omit<P, 'targetAddress'> & { targetAddress: PermissionTarget }\n : P\n\n/** A `zodiac-roles-sdk` permission that may name a node as its target. */\nexport type ConstellationPermission = WithNodeTarget<Permission>\n\nexport type LabelledPermissions = {\n label: string\n permissions: ConstellationPermission[]\n}\n\nexport type DeFiKitEntry = {\n label: string\n defiKit: {\n protocol: string\n verb: string\n params: Record<string, DeFiKitParamValue>\n }\n}\n\nexport type DeFiKitParamValue =\n | string\n | number\n | boolean\n | readonly (string | number | boolean)[]\n | null\n | undefined\n\n/**\n * One entry of a role's `permissions`. A bare permission stays valid, but every\n * labelled form names something the app can show as a single card.\n */\nexport type PermissionEntry =\n | ConstellationPermission\n | LabelledPermissions\n | SwapEntry\n | TransferEntry\n | DeFiKitEntry\n\n/** A role's full permission list — the type `permissions.ts` files satisfy. */\nexport type Permissions = readonly PermissionEntry[]\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAM,oBAAoB;;AAG1B,MAAM,0BAAkD;CACtD,GAAG;CACH,IAAI;CACJ,KAAK;CACL,MAAM;CACN,OAAO;CACR;;AAGD,MAAa,gBACX,EAAE,SAAS,EAAE,UAAU,MAAM,YAC7B,UACG;CACH,MAAM,SAAS,wBAAwB;AAEvC,KAAI,UAAU,KACZ,OAAM,IAAI,MAAM,kCAAkC,MAAM,GAAG;AAG7D,QAAO;EACL,KAAK,GAAG,kBAAkB,eAAe,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,OAAO;EAC3F,QAAQ,GAAG,kBAAkB;EAC9B;;AAGH,MAAa,kBAAkB,UAC7B,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa;;;;;;;AAQ9D,MAAa,kBAAkB,OAAO,kBAAkB;AAExD,MAAa,yBACX,OACA,UACS;CACT,MAAM,SAAU,MACd;AAGF,MAAK,MAAM,kBAAkB,UAAU,EAAE,CACvC,KAAI,mBAAmB,OAAO;EAC5B,MAAM,QAAQ,WAAW,QAAQ,IAAI,MAAM,MAAM,KAAK;AACtD,QAAM,IAAI,MACR,kBAAkB,MAAM,mBAAmB,eAAe,+BAA+B,MAAM,+CAChG;;AAIL,KAAI,EAAE,YAAY,UAAU,MAAM,OAAO,SAAS,WAChD;AAGF,MAAK,MAAM,UAAU,MAAM,OAAO,UAAU,EAAE,CAC5C,KAAI,OAAO,UAAU,MACnB,OAAM,IAAI,MACR,IAAI,MAAM,MAAM,sBAAsB,MAAM,mEAC7C;;;;;;AASP,MAAM,gBAAgB,WAA8C;CAClE,MAAM,eAAe,IAAI,iBAAiB;AAE1C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;AAClD,MAAI,SAAS,KACX;AAGF,OAAK,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM,CACxD,cAAa,OAAO,MAAM,OAAO,MAAM,CAAC;;AAI5C,QAAO"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { i as toAnnotation, n as isDeFiKitEntry, t as assertRecipientChains } from "./permissionEntries-C4dAwZBb.mjs";
|
|
2
|
+
import { invariant } from "@epic-web/invariant";
|
|
3
|
+
import assert from "assert";
|
|
4
|
+
//#region src/api.ts
|
|
5
|
+
const DEFAULT_API_URL = "https://app.zodiac.eco/api/v1";
|
|
6
|
+
var ApiClient = class {
|
|
7
|
+
apiKey;
|
|
8
|
+
baseUrl;
|
|
9
|
+
_fetch;
|
|
10
|
+
headers;
|
|
11
|
+
constructor({ baseUrl = process.env.ZODIAC_API_URL ?? DEFAULT_API_URL, fetch: customFetch = fetch, headers = {}, apiKey = process.env.ZODIAC_API_KEY } = {}) {
|
|
12
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
13
|
+
this._fetch = customFetch;
|
|
14
|
+
this.headers = headers;
|
|
15
|
+
assert(apiKey, "No API key provided to the API client. Either pass it as the \"apiKey\" option or set the ZODIAC_API_KEY environment variable.");
|
|
16
|
+
this.apiKey = apiKey;
|
|
17
|
+
}
|
|
18
|
+
async postJson(endpoint, payload) {
|
|
19
|
+
const res = await this._fetch(`${this.baseUrl}/${endpoint}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
...this.headers,
|
|
23
|
+
"content-type": "application/json",
|
|
24
|
+
authorization: `Bearer ${this.apiKey}`
|
|
25
|
+
},
|
|
26
|
+
body: jsonStringify(payload)
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) await handleApiError(res);
|
|
29
|
+
return res.json();
|
|
30
|
+
}
|
|
31
|
+
async get(endpoint) {
|
|
32
|
+
const res = await this._fetch(`${this.baseUrl}/${endpoint}`, { headers: {
|
|
33
|
+
...this.headers,
|
|
34
|
+
authorization: `Bearer ${this.apiKey}`
|
|
35
|
+
} });
|
|
36
|
+
if (!res.ok) await handleApiError(res);
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every account the workspaces of this org hold, grouped by workspace.
|
|
41
|
+
*
|
|
42
|
+
* Only accounts that exist on chain are listed, so every entry carries an
|
|
43
|
+
* address something was deployed to.
|
|
44
|
+
*/
|
|
45
|
+
listAccounts() {
|
|
46
|
+
return this.get("accounts");
|
|
47
|
+
}
|
|
48
|
+
/** Every member of this org, with the personal safes they have activated. */
|
|
49
|
+
listUsers() {
|
|
50
|
+
return this.get("users");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Stores a specification as the constellation's next revision, and answers
|
|
54
|
+
* with a url to review and deploy it. Applying does not deploy anything, and
|
|
55
|
+
* re-applying an undeployed revision replaces it rather than stacking.
|
|
56
|
+
*/
|
|
57
|
+
applyConstellation(workspaceId, payload) {
|
|
58
|
+
return this.postJson(`workspace/${workspaceId}/constellation/apply`, payload);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reports what the accounts a specification names look like on chain.
|
|
62
|
+
*
|
|
63
|
+
* Declarations are merged over what is read, and a node with no address is
|
|
64
|
+
* resolved against the setup safe of whoever's API key asks — describing the
|
|
65
|
+
* account *that* caller would deploy. Name accounts by address and declare
|
|
66
|
+
* nothing to be told only what is there, the same for every key.
|
|
67
|
+
*/
|
|
68
|
+
resolveConstellation(workspaceId, payload) {
|
|
69
|
+
return this.postJson(`workspace/${workspaceId}/constellation/resolve`, payload);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var ApiRequestError = class ApiRequestError extends Error {
|
|
73
|
+
status;
|
|
74
|
+
statusText;
|
|
75
|
+
details;
|
|
76
|
+
/** What the API refused, when it could name each problem. */
|
|
77
|
+
issues;
|
|
78
|
+
/** The API's own summary of the problem. */
|
|
79
|
+
reason;
|
|
80
|
+
constructor(message, opts) {
|
|
81
|
+
const issues = readIssues(opts.details);
|
|
82
|
+
super(issues.length > 0 ? formatIssues(message, issues) : ApiRequestError.composeMessage(message, opts.details));
|
|
83
|
+
this.name = "ApiRequestError";
|
|
84
|
+
this.status = opts.status;
|
|
85
|
+
this.statusText = opts.statusText;
|
|
86
|
+
this.details = opts.details;
|
|
87
|
+
this.issues = issues;
|
|
88
|
+
this.reason = message;
|
|
89
|
+
if (opts.cause !== void 0) this.cause = opts.cause;
|
|
90
|
+
}
|
|
91
|
+
static composeMessage(message, details) {
|
|
92
|
+
if (details == null) return message;
|
|
93
|
+
let detailsString;
|
|
94
|
+
try {
|
|
95
|
+
detailsString = typeof details === "string" ? details : jsonStringify(details, 2);
|
|
96
|
+
} catch (_err) {
|
|
97
|
+
detailsString = String(details);
|
|
98
|
+
}
|
|
99
|
+
return `${message}\nDetails: ${detailsString}`;
|
|
100
|
+
}
|
|
101
|
+
toString() {
|
|
102
|
+
return `${this.name}: ${this.message}`;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
async function handleApiError(response) {
|
|
106
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
107
|
+
const errorData = await response.json();
|
|
108
|
+
let error;
|
|
109
|
+
try {
|
|
110
|
+
error = new ApiRequestError(errorData.error.message, {
|
|
111
|
+
status: response.status,
|
|
112
|
+
statusText: response.statusText,
|
|
113
|
+
details: errorData.error.details
|
|
114
|
+
});
|
|
115
|
+
} catch (jsonShapeError) {
|
|
116
|
+
error = new ApiRequestError(`Failed parsing error response: ${jsonShapeError}`, {
|
|
117
|
+
status: response.status,
|
|
118
|
+
statusText: response.statusText,
|
|
119
|
+
details: errorData
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
} else throw new ApiRequestError(`${response.status} ${response.statusText}: ${response.url}`, {
|
|
124
|
+
status: response.status,
|
|
125
|
+
statusText: response.statusText
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The issues of an error response, each naming where in the request it is.
|
|
130
|
+
* Anything that does not look like one is left to the raw details.
|
|
131
|
+
*/
|
|
132
|
+
const readIssues = (details) => {
|
|
133
|
+
if (typeof details !== "object" || details == null || !("issues" in details)) return [];
|
|
134
|
+
const { issues } = details;
|
|
135
|
+
if (!Array.isArray(issues)) return [];
|
|
136
|
+
return issues.flatMap((issue) => typeof issue === "object" && issue != null && Array.isArray(issue.path) && typeof issue.message === "string" ? [{
|
|
137
|
+
path: issue.path,
|
|
138
|
+
message: issue.message
|
|
139
|
+
}] : []);
|
|
140
|
+
};
|
|
141
|
+
/** A headline and one line per issue, the way a terminal reads best. */
|
|
142
|
+
const formatIssues = (headline, issues) => [`${headline}:`, ...issues.map(({ path, message }) => {
|
|
143
|
+
const where = Array.isArray(path) ? path.join(" › ") : path;
|
|
144
|
+
return where === "" ? ` • ${message}` : ` • ${where}: ${message}`;
|
|
145
|
+
})].join("\n");
|
|
146
|
+
/** JSON.stringify with bigint support */
|
|
147
|
+
const jsonStringify = (value, indent) => JSON.stringify(value, (_, value) => {
|
|
148
|
+
if (typeof value === "bigint") return value.toString();
|
|
149
|
+
return value;
|
|
150
|
+
}, indent);
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/push.ts
|
|
153
|
+
/**
|
|
154
|
+
* Applies the given nodes as the next revision of the constellations they
|
|
155
|
+
* belong to, and answers with a url per constellation to review and deploy.
|
|
156
|
+
*
|
|
157
|
+
* Each node travels with what it was actually given, never with the onchain
|
|
158
|
+
* state a `pull` filled it with for reading — so a node passed as a bare
|
|
159
|
+
* reference declares nothing and leaves the account it names alone. Nodes
|
|
160
|
+
* naming each other travel as references and are substituted for addresses
|
|
161
|
+
* once those are derived at deploy.
|
|
162
|
+
*
|
|
163
|
+
* Passing an object names each node by its key; passing an array names them by
|
|
164
|
+
* position. Either way the name is the node's `ref`, which is how the app
|
|
165
|
+
* follows a node from one revision to the next.
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* const eth = constellation({ workspace: 'GG', label: 'my constellation', chain: 1 })
|
|
169
|
+
* const dao = eth.safe['GG DAO']
|
|
170
|
+
* const roles = eth.roles['New Roles']({ nonce: 0n, target: dao, owner: dao, avatar: dao })
|
|
171
|
+
*
|
|
172
|
+
* await push({ dao, roles })
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
async function push(nodes, opts) {
|
|
176
|
+
const api = opts?.api ?? new ApiClient({ apiKey: opts?.apiKey });
|
|
177
|
+
const refs = deriveRefs(nodes);
|
|
178
|
+
const groups = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const node of refs.byIdentity.keys()) {
|
|
180
|
+
const meta = node._constellation;
|
|
181
|
+
invariant(meta, `Node "${node.label}" is not associated with a constellation`);
|
|
182
|
+
const key = `${meta.workspaceId}:${meta.chain}:${meta.label}`;
|
|
183
|
+
let group = groups.get(key);
|
|
184
|
+
if (!group) {
|
|
185
|
+
group = {
|
|
186
|
+
meta,
|
|
187
|
+
nodes: []
|
|
188
|
+
};
|
|
189
|
+
groups.set(key, group);
|
|
190
|
+
}
|
|
191
|
+
group.nodes.push(node);
|
|
192
|
+
}
|
|
193
|
+
const results = [];
|
|
194
|
+
for (const { meta, nodes: groupNodes } of groups.values()) {
|
|
195
|
+
const specification = groupNodes.map((n) => nodeToSpec(n, refs));
|
|
196
|
+
const result = await api.applyConstellation(meta.workspaceId, {
|
|
197
|
+
label: meta.label,
|
|
198
|
+
chain: meta.chain,
|
|
199
|
+
specification
|
|
200
|
+
}).catch((error) => {
|
|
201
|
+
if (error instanceof ApiRequestError && error.issues.length > 0) throw new ConstellationRejectedError(meta.label, error, (index) => {
|
|
202
|
+
const node = groupNodes[index];
|
|
203
|
+
return node == null ? `node ${index}` : `${refs.byIdentity.get(node)} ("${node.label}")`;
|
|
204
|
+
});
|
|
205
|
+
throw error;
|
|
206
|
+
});
|
|
207
|
+
results.push(result);
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Zodiac refused a constellation as it was pushed. Each issue names the node
|
|
213
|
+
* it concerns by the name it was pushed under, followed by the path within it.
|
|
214
|
+
*/
|
|
215
|
+
var ConstellationRejectedError = class extends Error {
|
|
216
|
+
issues;
|
|
217
|
+
constructor(label, error, nameNode) {
|
|
218
|
+
const issues = error.issues.map(({ path, message }) => {
|
|
219
|
+
const [root, index, ...rest] = path;
|
|
220
|
+
return {
|
|
221
|
+
path: root === "specification" && typeof index === "number" ? [nameNode(index), ...rest].join(" › ") : path.join(" › "),
|
|
222
|
+
message
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
super(formatIssues(`Zodiac refused the constellation "${label}". ${error.reason}`, issues));
|
|
226
|
+
this.name = "ConstellationRejectedError";
|
|
227
|
+
this.issues = issues;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* What a ref may be spelled like. Refs come from export names and object keys,
|
|
232
|
+
* so any identifier a node can be declared under has to survive the push —
|
|
233
|
+
* except one opening with `$`, which is how a node references another. A list
|
|
234
|
+
* of nodes refs them by position, so a plain index counts too.
|
|
235
|
+
*/
|
|
236
|
+
const IDENTIFIER = /^(?:[a-zA-Z_][a-zA-Z0-9_$]*|\d+)$/;
|
|
237
|
+
function labelKey(node) {
|
|
238
|
+
return `${node._constellation.workspaceId}:${node.type}:${node.label}`;
|
|
239
|
+
}
|
|
240
|
+
function deriveRefs(nodes) {
|
|
241
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
242
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
243
|
+
const register = (node, ref) => {
|
|
244
|
+
invariant(IDENTIFIER.test(ref), `Invalid ref "${ref}": a ref is a JavaScript identifier, and cannot start with "$"`);
|
|
245
|
+
byIdentity.set(node, ref);
|
|
246
|
+
byLabel.set(labelKey(node), ref);
|
|
247
|
+
};
|
|
248
|
+
if (Array.isArray(nodes)) for (let i = 0; i < nodes.length; i++) {
|
|
249
|
+
const node = nodes[i];
|
|
250
|
+
invariant(isConstellationNode(node), `unexpected node input at index: ${i}`);
|
|
251
|
+
register(node, `${i}`);
|
|
252
|
+
}
|
|
253
|
+
else for (const [key, node] of Object.entries(nodes)) {
|
|
254
|
+
invariant(isConstellationNode(node), `unexpected node input under key: ${key}`);
|
|
255
|
+
register(node, key);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
byIdentity,
|
|
259
|
+
byLabel
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* What situates a node rather than configures it: which account it is, and how
|
|
264
|
+
* the workspace holds it. Always sent, even for a reference that declares
|
|
265
|
+
* nothing — none of it reaches the onchain diff, and leaving it out would lose
|
|
266
|
+
* the label and the vault the node belongs to.
|
|
267
|
+
*
|
|
268
|
+
* `address` above all. It is what the pull resolved the label to, and the only
|
|
269
|
+
* thing that says which account this is: a node without one is a creation, and
|
|
270
|
+
* its address is derived from whoever deploys. Drop it and a second deployer
|
|
271
|
+
* provisions a duplicate under the same label rather than reconfiguring the
|
|
272
|
+
* account that is already there.
|
|
273
|
+
*/
|
|
274
|
+
const IDENTITY_FIELDS = new Set([
|
|
275
|
+
"type",
|
|
276
|
+
"chain",
|
|
277
|
+
"address",
|
|
278
|
+
"label",
|
|
279
|
+
"vault"
|
|
280
|
+
]);
|
|
281
|
+
function nodeToSpec(node, refs) {
|
|
282
|
+
const { id, _constellation, _declared, ...rest } = node;
|
|
283
|
+
const declared = Array.isArray(_declared) ? new Set(_declared) : null;
|
|
284
|
+
const spec = {};
|
|
285
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
286
|
+
if (declared != null && !IDENTITY_FIELDS.has(key) && !declared.has(key)) continue;
|
|
287
|
+
if (node.type === "ROLES" && key === "roles" && value != null) {
|
|
288
|
+
spec.roles = describeRoles(value, node.chain, refs);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
spec[key] = resolveRefs(value, refs);
|
|
292
|
+
}
|
|
293
|
+
spec.ref = refs.byIdentity.get(node);
|
|
294
|
+
invariant(spec.ref != null, "ref not found");
|
|
295
|
+
return stringifyBigints(spec);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Entries travel to the API as they were written — parameters and labels — and
|
|
299
|
+
* are compiled when the constellation is deployed. Only a DeFi Kit entry is
|
|
300
|
+
* rendered here, into the annotation uri that names the chain it was written
|
|
301
|
+
* for; the permissions behind it are fetched from that uri at deploy.
|
|
302
|
+
*/
|
|
303
|
+
function describeRoles(roles, chain, refs) {
|
|
304
|
+
return Object.fromEntries(Object.entries(roles).map(([key, def]) => {
|
|
305
|
+
if (def == null) return [key, null];
|
|
306
|
+
return [key, {
|
|
307
|
+
key,
|
|
308
|
+
members: resolveRefs(def.members, refs),
|
|
309
|
+
...def.policy != null && { policy: def.policy },
|
|
310
|
+
...def.description != null && { description: def.description },
|
|
311
|
+
permissions: def.permissions.map((entry) => {
|
|
312
|
+
assertRecipientChains(entry, chain);
|
|
313
|
+
return resolveRefs(isDeFiKitEntry(entry) ? {
|
|
314
|
+
label: entry.label,
|
|
315
|
+
annotation: toAnnotation(entry, chain)
|
|
316
|
+
} : entry, refs);
|
|
317
|
+
})
|
|
318
|
+
}];
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
function resolveRefs(value, refs) {
|
|
322
|
+
if (isConstellationNode(value)) {
|
|
323
|
+
const ref = refs.byIdentity.get(value) ?? refs.byLabel.get(labelKey(value));
|
|
324
|
+
invariant(ref != null, `Node "${value.label}" is referenced but not included in the push() call`);
|
|
325
|
+
return `$${ref}`;
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(value)) return value.map((v) => resolveRefs(v, refs));
|
|
328
|
+
if (typeof value === "object" && value !== null) {
|
|
329
|
+
const resolved = {};
|
|
330
|
+
for (const [k, v] of Object.entries(value)) resolved[k] = resolveRefs(v, refs);
|
|
331
|
+
return resolved;
|
|
332
|
+
}
|
|
333
|
+
if (typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value)) return value.toLowerCase();
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
function stringifyBigints(value) {
|
|
337
|
+
if (typeof value === "bigint") return value.toString();
|
|
338
|
+
if (Array.isArray(value)) return value.map(stringifyBigints);
|
|
339
|
+
if (typeof value === "object" && value !== null) {
|
|
340
|
+
const result = {};
|
|
341
|
+
for (const [k, v] of Object.entries(value)) result[k] = stringifyBigints(v);
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
function isConstellationNode(value) {
|
|
347
|
+
if (typeof value === "function" || typeof value === "object") {
|
|
348
|
+
const obj = value;
|
|
349
|
+
return obj != null && typeof obj.type === "string" && typeof obj.chain === "number" && typeof obj._constellation === "object";
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
export { push as n, ApiClient as r, ConstellationRejectedError as t };
|
|
355
|
+
|
|
356
|
+
//# sourceMappingURL=push-DdnHsLpO.mjs.map
|