@zodiaceco/sdk 1.13.1 → 2.0.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 CHANGED
@@ -1,4 +1,4 @@
1
- # Zodiac OS SDK
1
+ # Zodiac SDK
2
2
 
3
3
  Programmatically manage [Zodiac](https://www.zodiac.eco) account constellations.
4
4
 
@@ -137,6 +137,90 @@ References are resolved by label at `push()` time, so both sides of the cycle mu
137
137
  const aliceAddress = eth.user['Alice Sample']
138
138
  ```
139
139
 
140
+ ### Describing what a role may do
141
+
142
+ A Roles mod carries roles, and every role lists `permissions` — entries that
143
+ describe what the role is allowed to do. Entries carry parameters and a label,
144
+ never compiled permissions: they are compiled when the constellation is
145
+ deployed, so a stored revision always goes through the current compilers
146
+ instead of replaying a copy made when it was pushed.
147
+
148
+ ```ts
149
+ import { swap, transfer, custom, defikit } from '@zodiaceco/sdk/actions'
150
+ // `allow` is your project's generated permission kit — a global in template
151
+ // projects, created by `zodiac pull-contracts`.
152
+
153
+ const treasuryRoles = eth.roles['GG Treasury Roles']({
154
+ nonce: 0n,
155
+ target: ggTreasury,
156
+ allowances: { usdc_payouts },
157
+ roles: {
158
+ treasury_ops: {
159
+ members: [eth.user['Alice Sample']],
160
+ permissions: [
161
+ swap({ label: 'Rebalance stables', sell: [USDC, DAI], buy: [WETH] }),
162
+
163
+ transfer({
164
+ label: 'Grant payouts',
165
+ tokens: [USDC],
166
+ to: [eth.safe['Grants Safe']],
167
+ bridge: [{ to: [gno.safe['Ops Safe']], receive: [GNO_USDC] }],
168
+ allowance: usdc_payouts,
169
+ }),
170
+
171
+ defikit.aave_v3.deposit({
172
+ label: 'Aave deposits',
173
+ market: 'Core',
174
+ targets: ['WETH'],
175
+ }),
176
+
177
+ custom({
178
+ label: 'Bot ops',
179
+ permissions: [
180
+ allow.eth.weth.deposit({ send: true }),
181
+ allow.eth.weth.withdraw(),
182
+ ],
183
+ }),
184
+ ],
185
+ },
186
+ },
187
+ })
188
+ ```
189
+
190
+ Each helper covers a different kind of action:
191
+
192
+ - `swap()` allows signing CoW orders between the tokens it names.
193
+ - `transfer()` allows sending tokens to the addresses it names, optionally
194
+ capped by an allowance declared on the same Roles mod. Pass the zero address
195
+ to allow sending the native token. `bridge` names destinations on other
196
+ chains, sent over Across: each target pins both the recipients and the tokens
197
+ they may receive there. A target takes its chain from its recipient nodes, or
198
+ name one with `chain` when the recipients are plain addresses. Tokens without
199
+ an Across route to a target are skipped, the same way the app skips them —
200
+ routes change between writing a spec and deploying it — but a target nothing
201
+ can reach at all is refused at deploy rather than deployed half-working.
202
+ - `defikit` mirrors the DeFi Kit allow kit — same protocols, verbs and
203
+ parameters, plus a `label`. A DeFi Kit entry is nothing but its annotation;
204
+ the permissions behind it are fetched from the annotation's uri at deploy, so
205
+ `push()` fetches nothing. Protocols and parameters are typed against the
206
+ Ethereum kit, the widest of the chains DeFi Kit serves.
207
+ - `custom()` labels a bag of plain `allow`-kit permissions — everything the
208
+ other helpers don't cover. It takes permissions, not other actions.
209
+
210
+ Every helper takes a `label`. The label names the action in Zodiac and never
211
+ reaches the chain. A bare permission with no enclosing helper stays valid, but
212
+ it has nowhere to appear in the app beyond the targets it allows.
213
+
214
+ Allowance keys are plain labels — `key: 'usdc_payouts'` on the declaration, and
215
+ `allowance: usdc_payouts` on the transfer, which reads the key off it. They are
216
+ encoded to bytes32 when the constellation is deployed, so nothing calls
217
+ `encodeKey` by hand. A label has to fit in 32 bytes.
218
+
219
+ Tokens are named by address, not by symbol. A `transfer()` recipient may also
220
+ be a node — an account from your codegen, or one bound by address — which
221
+ stands for the address it lives at. A node whose address is only known once the
222
+ constellation is deployed is rejected at compile time.
223
+
140
224
  ### Pushing the constellation
141
225
 
142
226
  The `push()` function takes all nodes and sends them to the Zodiac OS API. Pass either a named object (keys become refs) or an array:
@@ -0,0 +1,152 @@
1
+ import { Permission } from "zodiac-roles-sdk";
2
+ import { Address, ApplyConstellationPayload, ChainId } from "@zodiaceco/api-types";
3
+ import { allow } from "defi-kit/eth";
4
+
5
+ //#region src/permissionEntries.d.ts
6
+ /**
7
+ * An allowance key. Authored as a plain label, the way a role key is, and
8
+ * encoded to bytes32 when the constellation is deployed. An already-encoded
9
+ * key still works — `encodeKey` passes 32-byte hex through unchanged.
10
+ */
11
+ type AllowanceKey = string;
12
+ type SwapEntry = {
13
+ label: string;
14
+ action: {
15
+ type: 'swap';
16
+ sell: Address[];
17
+ buy: Address[];
18
+ };
19
+ };
20
+ type TransferEntry = {
21
+ label: string;
22
+ action: {
23
+ type: 'transfer';
24
+ tokens: Address[];
25
+ to: Address[];
26
+ bridge?: {
27
+ chain: ChainId;
28
+ to: Address[];
29
+ receive: Address[];
30
+ }[];
31
+ allowance?: AllowanceKey;
32
+ };
33
+ };
34
+ type LabelledPermissions = {
35
+ label: string;
36
+ permissions: Permission[];
37
+ };
38
+ type DeFiKitEntry = {
39
+ label: string;
40
+ defiKit: {
41
+ protocol: string;
42
+ verb: string;
43
+ params: Record<string, DeFiKitParamValue>;
44
+ };
45
+ };
46
+ type DeFiKitParamValue = string | number | boolean | readonly (string | number | boolean)[] | null | undefined;
47
+ /**
48
+ * One entry of a role's `permissions`. A bare permission stays valid, but every
49
+ * labelled form names something the app can show as a single card.
50
+ */
51
+ type PermissionEntry = Permission | LabelledPermissions | SwapEntry | TransferEntry | DeFiKitEntry;
52
+ /** A role's full permission list — the type `permissions.ts` files satisfy. */
53
+ type Permissions = readonly PermissionEntry[];
54
+ //#endregion
55
+ //#region src/types.d.ts
56
+ type ApiNewRolesSpec = Extract<ApplyConstellationPayload['specification'][number], {
57
+ type: 'ROLES';
58
+ nonce: string;
59
+ }>;
60
+ /** A role definition in the API's serialized format (bigints as template literal strings). */
61
+ /** An allowance definition in the API's serialized format (bigints as template literal strings). */
62
+ type ApiAllowanceSpec = Extract<NonNullable<ApiNewRolesSpec['allowances']>, readonly any[]>[number];
63
+ /** Recursively converts `${bigint}` template literal strings to `bigint`. */
64
+ type RealBigints<T> = T extends `${bigint}` ? bigint : T extends readonly (infer U)[] ? RealBigints<U>[] : T extends Record<string, any> ? { [K in keyof T]: RealBigints<T[K]> } : T;
65
+ /** A role definition with bigint fields as actual bigints. */
66
+ /** An allowance definition with bigint fields as actual bigints. */
67
+ type AllowanceSpec = RealBigints<ApiAllowanceSpec>;
68
+ //#endregion
69
+ //#region src/actions.d.ts
70
+ /**
71
+ * Allows signing CoW orders selling any of `sell` for any of `buy`. The
72
+ * parameters are compiled into permissions when the constellation is deployed,
73
+ * never here — a stored revision that carried its own copy would go stale as
74
+ * soon as the compiler changed.
75
+ */
76
+ declare const swap: ({
77
+ label,
78
+ sell,
79
+ buy
80
+ }: SwapParams) => SwapEntry;
81
+ /**
82
+ * Allows transferring any of `tokens` to any of `to` on the role's own chain,
83
+ * and — through `bridge` — to recipients on other chains over Across. Both are
84
+ * optional on their own; a transfer names at least one recipient somewhere.
85
+ */
86
+ declare const transfer: ({
87
+ label,
88
+ tokens,
89
+ to,
90
+ bridge,
91
+ allowance
92
+ }: TransferParams) => TransferEntry;
93
+ /**
94
+ * A labelled bag of `allow`-kit permissions — the code-side counterpart of the
95
+ * app's Custom action, for everything the other action types don't cover. It
96
+ * takes plain permissions, not other actions.
97
+ */
98
+ declare const custom: ({
99
+ label,
100
+ permissions
101
+ }: CustomParams) => LabelledPermissions;
102
+ /**
103
+ * The DeFi Kit allow kit — same protocol, verb and parameter surface, plus a
104
+ * `label`. A DeFi Kit entry is nothing but its annotation: the permissions are
105
+ * fetched from the annotation's uri at deploy, so nothing is fetched here.
106
+ *
107
+ * Protocols and parameters are typed against the Ethereum kit, the widest of
108
+ * the chains DeFi Kit serves. Values that only exist on another chain still
109
+ * type-check; the API validates them when the annotation is resolved.
110
+ */
111
+ declare const defikit: DeFiKit;
112
+ type SwapParams = {
113
+ /** Names the action in the app. Never reaches the chain. */label: string; /** Tokens the role may sell. */
114
+ sell: readonly Address[]; /** Tokens the role may buy. */
115
+ buy: readonly Address[];
116
+ };
117
+ type TransferParams = {
118
+ /** Names the action in the app. Never reaches the chain. */label: string; /** Tokens the role may transfer. Use the zero address for the native token. */
119
+ tokens: readonly Address[];
120
+ /** Addresses the role may transfer to on the role's own chain. A node stands
121
+ * for its address, so only nodes that already have one — an account from your
122
+ * codegen, or one bound by address — can be named here. */
123
+ to?: readonly Recipient[]; /** Destinations on other chains, bridged over Across. */
124
+ bridge?: readonly BridgeTarget[];
125
+ /** Allowance capping what may be sent. Pass the allowance declared on the
126
+ * roles modifier, or its key. */
127
+ allowance?: AllowanceSpec | AllowanceKey;
128
+ };
129
+ type CustomParams = {
130
+ /** Names the action in the app. Never reaches the chain. */label: string;
131
+ permissions: readonly Permission[];
132
+ };
133
+ /** An address, or a node that already knows the address it lives at. */
134
+ type Recipient = Address | {
135
+ readonly address: Address;
136
+ readonly chain?: ChainId;
137
+ };
138
+ type BridgeTarget = {
139
+ /** The destination chain. Inferred from the recipients when they are nodes. */chain?: ChainId; /** Addresses the role may bridge to on the destination chain. */
140
+ to: readonly Recipient[]; /** Tokens the role may receive on the destination chain. */
141
+ receive: readonly Address[];
142
+ };
143
+ type EthereumKit = typeof allow;
144
+ type DeFiKit = { readonly [Protocol in keyof EthereumKit]: { readonly [Verb in keyof EthereumKit[Protocol]]: DeFiKitVerb<EthereumKit[Protocol][Verb]> } };
145
+ type DeFiKitVerb<Verb> = Verb extends ((params: infer Params) => unknown) ? (params: Params & {
146
+ label: string;
147
+ }) => DeFiKitEntry : (params: {
148
+ label: string;
149
+ }) => DeFiKitEntry;
150
+ //#endregion
151
+ export { AllowanceSpec as a, transfer as i, defikit as n, PermissionEntry as o, swap as r, Permissions as s, custom as t };
152
+ //# sourceMappingURL=actions-DS5XoMiq.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { i as transfer, n as defikit, o as PermissionEntry, r as swap, s as Permissions, t as custom } from "./actions-DS5XoMiq.mjs";
2
+ export { PermissionEntry, Permissions, custom, defikit, swap, transfer };
@@ -0,0 +1,92 @@
1
+ import { r as recipientChains } from "./permissionEntries-D3fSVhwO.mjs";
2
+ //#region src/actions.ts
3
+ /**
4
+ * Allows signing CoW orders selling any of `sell` for any of `buy`. The
5
+ * parameters are compiled into permissions when the constellation is deployed,
6
+ * never here — a stored revision that carried its own copy would go stale as
7
+ * soon as the compiler changed.
8
+ */
9
+ const swap = ({ label, sell, buy }) => ({
10
+ label,
11
+ action: {
12
+ type: "swap",
13
+ sell: [...sell],
14
+ buy: [...buy]
15
+ }
16
+ });
17
+ /**
18
+ * Allows transferring any of `tokens` to any of `to` on the role's own chain,
19
+ * and — through `bridge` — to recipients on other chains over Across. Both are
20
+ * optional on their own; a transfer names at least one recipient somewhere.
21
+ */
22
+ const transfer = ({ label, tokens, to = [], bridge = [], allowance }) => {
23
+ const entry = {
24
+ label,
25
+ action: {
26
+ type: "transfer",
27
+ tokens: [...tokens],
28
+ to: to.map(toAddress),
29
+ ...bridge.length > 0 && { bridge: bridge.map(toBridgeTarget) },
30
+ ...allowance != null && { allowance: allowanceKey(allowance) }
31
+ }
32
+ };
33
+ const chains = to.flatMap((recipient) => typeof recipient === "string" || recipient.chain == null ? [] : [recipient.chain]);
34
+ if (chains.length > 0) Object.defineProperty(entry, recipientChains, {
35
+ value: chains,
36
+ enumerable: false
37
+ });
38
+ return entry;
39
+ };
40
+ /**
41
+ * A labelled bag of `allow`-kit permissions — the code-side counterpart of the
42
+ * app's Custom action, for everything the other action types don't cover. It
43
+ * takes plain permissions, not other actions.
44
+ */
45
+ const custom = ({ label, permissions }) => ({
46
+ label,
47
+ permissions: [...permissions]
48
+ });
49
+ /**
50
+ * The DeFi Kit allow kit — same protocol, verb and parameter surface, plus a
51
+ * `label`. A DeFi Kit entry is nothing but its annotation: the permissions are
52
+ * fetched from the annotation's uri at deploy, so nothing is fetched here.
53
+ *
54
+ * Protocols and parameters are typed against the Ethereum kit, the widest of
55
+ * the chains DeFi Kit serves. Values that only exist on another chain still
56
+ * type-check; the API validates them when the annotation is resolved.
57
+ */
58
+ const defikit = new Proxy({}, { get: (_, protocol) => new Proxy({}, { get: (_, verb) => ({ label, ...params }) => ({
59
+ label,
60
+ defiKit: {
61
+ protocol,
62
+ verb,
63
+ params
64
+ }
65
+ }) }) });
66
+ const allowanceKey = (allowance) => typeof allowance === "string" ? allowance : allowance.key;
67
+ const toAddress = (recipient) => typeof recipient === "string" ? recipient : recipient.address;
68
+ /**
69
+ * A node knows the chain it lives on, so a bridge target written with nodes
70
+ * needs no `chain` of its own. Naming one anyway is allowed — a plain address
71
+ * carries no chain — but a node that disagrees with it is a mistake worth
72
+ * catching here rather than at deploy.
73
+ */
74
+ const toBridgeTarget = ({ chain, to, receive }) => ({
75
+ chain: destinationChain(chain, to),
76
+ to: to.map(toAddress),
77
+ receive: [...receive]
78
+ });
79
+ const destinationChain = (chain, recipients) => {
80
+ let destination = chain;
81
+ for (const recipient of recipients) {
82
+ if (typeof recipient === "string" || recipient.chain == null) continue;
83
+ if (destination != null && destination !== recipient.chain) throw new Error(`A bridge target has one destination, but this one spans chains "${destination}" and "${recipient.chain}"`);
84
+ destination = recipient.chain;
85
+ }
86
+ if (destination == null) throw new Error("A bridge target needs a `chain` unless its recipients are nodes that know one");
87
+ return destination;
88
+ };
89
+ //#endregion
90
+ export { custom, defikit, swap, transfer };
91
+
92
+ //# sourceMappingURL=actions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.mjs","names":[],"sources":["../src/actions.ts"],"sourcesContent":["import type { Address, ChainId } from '@zodiaceco/api-types'\nimport type { allow as ethereumKit } from 'defi-kit/eth'\nimport type { Permission } from 'zodiac-roles-sdk'\nimport { recipientChains } from './permissionEntries'\nimport type {\n AllowanceKey,\n DeFiKitEntry,\n LabelledPermissions,\n PermissionEntry,\n SwapEntry,\n TransferEntry,\n} from './permissionEntries'\nimport type { AllowanceSpec } from './types'\n\nexport type { PermissionEntry }\nexport type { Permissions } from './permissionEntries'\n\n/**\n * Allows signing CoW orders selling any of `sell` for any of `buy`. The\n * parameters are compiled into permissions when the constellation is deployed,\n * never here — a stored revision that carried its own copy would go stale as\n * soon as the compiler changed.\n */\nexport const swap = ({ label, sell, buy }: SwapParams): SwapEntry => ({\n label,\n action: { type: 'swap', sell: [...sell], buy: [...buy] },\n})\n\n/**\n * Allows transferring any of `tokens` to any of `to` on the role's own chain,\n * and — through `bridge` — to recipients on other chains over Across. Both are\n * optional on their own; a transfer names at least one recipient somewhere.\n */\nexport const transfer = ({\n label,\n tokens,\n to = [],\n bridge = [],\n allowance,\n}: TransferParams): TransferEntry => {\n const entry: TransferEntry = {\n label,\n action: {\n type: 'transfer',\n tokens: [...tokens],\n to: to.map(toAddress),\n ...(bridge.length > 0 && { bridge: bridge.map(toBridgeTarget) }),\n ...(allowance != null && { allowance: allowanceKey(allowance) }),\n },\n }\n\n // Remember which chains the `to` nodes live on so push() can refuse a\n // recipient from another chain. Symbol-keyed: never serialized.\n const chains = to.flatMap((recipient) =>\n typeof recipient === 'string' || recipient.chain == null\n ? []\n : [recipient.chain]\n )\n\n if (chains.length > 0) {\n Object.defineProperty(entry, recipientChains, {\n value: chains,\n enumerable: false,\n })\n }\n\n return entry\n}\n\n/**\n * A labelled bag of `allow`-kit permissions — the code-side counterpart of the\n * app's Custom action, for everything the other action types don't cover. It\n * takes plain permissions, not other actions.\n */\nexport const custom = ({\n label,\n permissions,\n}: CustomParams): LabelledPermissions => ({\n label,\n permissions: [...permissions],\n})\n\n/**\n * The DeFi Kit allow kit — same protocol, verb and parameter surface, plus a\n * `label`. A DeFi Kit entry is nothing but its annotation: the permissions are\n * fetched from the annotation's uri at deploy, so nothing is fetched here.\n *\n * Protocols and parameters are typed against the Ethereum kit, the widest of\n * the chains DeFi Kit serves. Values that only exist on another chain still\n * type-check; the API validates them when the annotation is resolved.\n */\nexport const defikit: DeFiKit = new Proxy({} as DeFiKit, {\n get: (_, protocol: string) =>\n new Proxy(\n {},\n {\n get:\n (_, verb: string) =>\n ({ label, ...params }: { label: string }) => ({\n label,\n defiKit: { protocol, verb, params },\n }),\n }\n ),\n})\n\nconst allowanceKey = (allowance: AllowanceSpec | AllowanceKey) =>\n typeof allowance === 'string' ? allowance : allowance.key\n\nconst toAddress = (recipient: Recipient): Address =>\n typeof recipient === 'string' ? recipient : recipient.address\n\n/**\n * A node knows the chain it lives on, so a bridge target written with nodes\n * needs no `chain` of its own. Naming one anyway is allowed — a plain address\n * carries no chain — but a node that disagrees with it is a mistake worth\n * catching here rather than at deploy.\n */\nconst toBridgeTarget = ({ chain, to, receive }: BridgeTarget) => ({\n chain: destinationChain(chain, to),\n to: to.map(toAddress),\n receive: [...receive],\n})\n\nconst destinationChain = (\n chain: ChainId | undefined,\n recipients: readonly Recipient[]\n): ChainId => {\n let destination = chain\n\n for (const recipient of recipients) {\n if (typeof recipient === 'string' || recipient.chain == null) {\n continue\n }\n\n if (destination != null && destination !== recipient.chain) {\n throw new Error(\n `A bridge target has one destination, but this one spans chains \"${destination}\" and \"${recipient.chain}\"`\n )\n }\n\n destination = recipient.chain\n }\n\n if (destination == null) {\n throw new Error(\n 'A bridge target needs a `chain` unless its recipients are nodes that know one'\n )\n }\n\n return destination\n}\n\ntype SwapParams = {\n /** Names the action in the app. Never reaches the chain. */\n label: string\n /** Tokens the role may sell. */\n sell: readonly Address[]\n /** Tokens the role may buy. */\n buy: readonly Address[]\n}\n\ntype TransferParams = {\n /** Names the action in the app. Never reaches the chain. */\n label: string\n /** Tokens the role may transfer. Use the zero address for the native token. */\n tokens: readonly Address[]\n /** Addresses the role may transfer to on the role's own chain. A node stands\n * for its address, so only nodes that already have one — an account from your\n * codegen, or one bound by address — can be named here. */\n to?: readonly Recipient[]\n /** Destinations on other chains, bridged over Across. */\n bridge?: readonly BridgeTarget[]\n /** Allowance capping what may be sent. Pass the allowance declared on the\n * roles modifier, or its key. */\n allowance?: AllowanceSpec | AllowanceKey\n}\n\ntype CustomParams = {\n /** Names the action in the app. Never reaches the chain. */\n label: string\n permissions: readonly Permission[]\n}\n\n/** An address, or a node that already knows the address it lives at. */\ntype Recipient =\n | Address\n | { readonly address: Address; readonly chain?: ChainId }\n\ntype BridgeTarget = {\n /** The destination chain. Inferred from the recipients when they are nodes. */\n chain?: ChainId\n /** Addresses the role may bridge to on the destination chain. */\n to: readonly Recipient[]\n /** Tokens the role may receive on the destination chain. */\n receive: readonly Address[]\n}\n\ntype EthereumKit = typeof ethereumKit\n\ntype DeFiKit = {\n readonly [Protocol in keyof EthereumKit]: {\n readonly [Verb in keyof EthereumKit[Protocol]]: DeFiKitVerb<\n EthereumKit[Protocol][Verb]\n >\n }\n}\n\ntype DeFiKitVerb<Verb> = Verb extends (params: infer Params) => unknown\n ? (params: Params & { label: string }) => DeFiKitEntry\n : (params: { label: string }) => DeFiKitEntry\n"],"mappings":";;;;;;;;AAuBA,MAAa,QAAQ,EAAE,OAAO,MAAM,WAAkC;CACpE;CACA,QAAQ;EAAE,MAAM;EAAQ,MAAM,CAAC,GAAG,KAAK;EAAE,KAAK,CAAC,GAAG,IAAI;EAAE;CACzD;;;;;;AAOD,MAAa,YAAY,EACvB,OACA,QACA,KAAK,EAAE,EACP,SAAS,EAAE,EACX,gBACmC;CACnC,MAAM,QAAuB;EAC3B;EACA,QAAQ;GACN,MAAM;GACN,QAAQ,CAAC,GAAG,OAAO;GACnB,IAAI,GAAG,IAAI,UAAU;GACrB,GAAI,OAAO,SAAS,KAAK,EAAE,QAAQ,OAAO,IAAI,eAAe,EAAE;GAC/D,GAAI,aAAa,QAAQ,EAAE,WAAW,aAAa,UAAU,EAAE;GAChE;EACF;CAID,MAAM,SAAS,GAAG,SAAS,cACzB,OAAO,cAAc,YAAY,UAAU,SAAS,OAChD,EAAE,GACF,CAAC,UAAU,MAAM,CACtB;AAED,KAAI,OAAO,SAAS,EAClB,QAAO,eAAe,OAAO,iBAAiB;EAC5C,OAAO;EACP,YAAY;EACb,CAAC;AAGJ,QAAO;;;;;;;AAQT,MAAa,UAAU,EACrB,OACA,mBACwC;CACxC;CACA,aAAa,CAAC,GAAG,YAAY;CAC9B;;;;;;;;;;AAWD,MAAa,UAAmB,IAAI,MAAM,EAAE,EAAa,EACvD,MAAM,GAAG,aACP,IAAI,MACF,EAAE,EACF,EACE,MACG,GAAG,UACH,EAAE,OAAO,GAAG,cAAiC;CAC5C;CACA,SAAS;EAAE;EAAU;EAAM;EAAQ;CACpC,GACJ,CACF,EACJ,CAAC;AAEF,MAAM,gBAAgB,cACpB,OAAO,cAAc,WAAW,YAAY,UAAU;AAExD,MAAM,aAAa,cACjB,OAAO,cAAc,WAAW,YAAY,UAAU;;;;;;;AAQxD,MAAM,kBAAkB,EAAE,OAAO,IAAI,eAA6B;CAChE,OAAO,iBAAiB,OAAO,GAAG;CAClC,IAAI,GAAG,IAAI,UAAU;CACrB,SAAS,CAAC,GAAG,QAAQ;CACtB;AAED,MAAM,oBACJ,OACA,eACY;CACZ,IAAI,cAAc;AAElB,MAAK,MAAM,aAAa,YAAY;AAClC,MAAI,OAAO,cAAc,YAAY,UAAU,SAAS,KACtD;AAGF,MAAI,eAAe,QAAQ,gBAAgB,UAAU,MACnD,OAAM,IAAI,MACR,mEAAmE,YAAY,SAAS,UAAU,MAAM,GACzG;AAGH,gBAAc,UAAU;;AAG1B,KAAI,eAAe,KACjB,OAAM,IAAI,MACR,gFACD;AAGH,QAAO"}
@@ -1,2 +1,2 @@
1
- import { a as EVERYTHING, c as Scoping, i as ConditionFunction, l as TargetPermission, n as ChainPrefix, o as FunctionPermission, r as chainIdFor, s as Options, t as CHAIN_IDS, u as buildAllowKit } from "../index-DoZbhpS6.mjs";
1
+ import { a as EVERYTHING, c as Scoping, i as ConditionFunction, l as TargetPermission, n as ChainPrefix, o as FunctionPermission, r as chainIdFor, s as Options, t as CHAIN_IDS, u as buildAllowKit } from "../index-CnH26Z_A.mjs";
2
2
  export { CHAIN_IDS, ChainPrefix, ConditionFunction, EVERYTHING, FunctionPermission, Options, Scoping, TargetPermission, buildAllowKit, chainIdFor };
@@ -1,3 +1,3 @@
1
- import { n as chainIdFor, t as CHAIN_IDS } from "../networks-BTW1qAAa.mjs";
2
- import { n as EVERYTHING, t as buildAllowKit } from "../allow-CzPpzclq.mjs";
1
+ import { n as chainIdFor, t as CHAIN_IDS } from "../networks-B0H4Uoyh.mjs";
2
+ import { n as EVERYTHING, t as buildAllowKit } from "../allow-CrDzDtxx.mjs";
3
3
  export { CHAIN_IDS, EVERYTHING, buildAllowKit, chainIdFor };
@@ -1,4 +1,4 @@
1
- import { a as walkContracts, i as readAbi } from "./networks-BTW1qAAa.mjs";
1
+ import { a as walkContracts, i as readAbi } from "./networks-B0H4Uoyh.mjs";
2
2
  import { Operator, ParameterType, c, coercePermission } from "zodiac-roles-sdk";
3
3
  import { Interface, isError } from "ethers";
4
4
  //#region src/allow/types.ts
@@ -13,7 +13,7 @@ function buildAllowKit(abisDir, contractsConfig) {
13
13
  attachAt(kit, [node.chain, ...node.segments], missingAbiProxy(node));
14
14
  continue;
15
15
  }
16
- attachAt(kit, [node.chain, ...node.segments], makeAllowContract(node.address, abi));
16
+ attachAt(kit, [node.chain, ...node.segments], makeAllowContract(node.address, abi, [node.chain, ...node.segments].join(".")));
17
17
  }
18
18
  return kit;
19
19
  }
@@ -35,9 +35,19 @@ function missingAbiProxy(node) {
35
35
  has: explain
36
36
  });
37
37
  }
38
- function makeAllowContract(address, abi) {
38
+ function makeAllowContract(address, abi, path) {
39
39
  const iface = Interface.from(abi);
40
40
  const lowerAddr = address.toLowerCase();
41
+ const overloads = /* @__PURE__ */ new Map();
42
+ iface.forEachFunction((fn) => {
43
+ if (fn.stateMutability === "view" || fn.stateMutability === "pure") return;
44
+ overloads.set(fn.name, [...overloads.get(fn.name) ?? [], fn]);
45
+ });
46
+ const ambiguous = (prop) => (overloads.get(prop)?.length ?? 0) > 1;
47
+ const explainAmbiguity = (prop) => {
48
+ const signatures = overloads.get(prop).map((fn) => fn.format("sighash"));
49
+ throw new Error(`\`allow.${path}.${prop}\` is ambiguous: ${address} has ${signatures.length} functions named "${prop}". Name the overload you mean by its full signature — ` + signatures.map((sig) => `allow.${path}[${JSON.stringify(sig)}]`).join(" or ") + `.`);
50
+ };
41
51
  const allowEverything = (options) => ({
42
52
  targetAddress: lowerAddr,
43
53
  send: options?.send,
@@ -57,12 +67,13 @@ function makeAllowContract(address, abi) {
57
67
  get: (_target, prop) => {
58
68
  if (prop === EVERYTHING) return allowEverything;
59
69
  if (typeof prop !== "string") return void 0;
70
+ if (ambiguous(prop)) explainAmbiguity(prop);
60
71
  if (!has(prop)) return void 0;
61
72
  return makeAllowFunction(iface.getFunction(prop), lowerAddr);
62
73
  },
63
74
  has: (_target, prop) => {
64
75
  if (prop === EVERYTHING) return true;
65
- return typeof prop === "string" && has(prop);
76
+ return typeof prop === "string" && !ambiguous(prop) && has(prop);
66
77
  }
67
78
  });
68
79
  }
@@ -118,4 +129,4 @@ const applyOptions = (permission, options) => {
118
129
  //#endregion
119
130
  export { EVERYTHING as n, buildAllowKit as t };
120
131
 
121
- //# sourceMappingURL=allow-CzPpzclq.mjs.map
132
+ //# sourceMappingURL=allow-CrDzDtxx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allow-CrDzDtxx.mjs","names":[],"sources":["../src/allow/types.ts","../src/allow/runtime.ts"],"sourcesContent":["import type { BigNumberish, BytesLike, ParamType } from 'ethers'\nimport type {\n Condition,\n FunctionPermission,\n TargetPermission,\n} from 'zodiac-roles-sdk'\n\nexport type Options = {\n send?: boolean\n delegatecall?: boolean\n etherWithinAllowance?: `0x${string}`\n callWithinAllowance?: `0x${string}`\n}\n\nexport type PrimitiveValue = BigNumberish | BytesLike | string | boolean\n\n// Signature matches `zodiac-roles-sdk` so values from `c.*` are assignable.\nexport type ConditionFunction<T = unknown> = (\n abiType: ParamType,\n _?: T\n) => Condition\n\ntype RequireAtLeastOne<T> = {\n [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>\n}[keyof T]\n\ntype ArrayElement<T extends readonly unknown[]> = T extends readonly (infer U)[]\n ? U\n : never\n\nexport type PrimitiveScoping<T extends PrimitiveValue> =\n | T\n | ConditionFunction<T>\n\nexport type ArrayScoping<T extends readonly any[]> =\n | readonly Scoping<ArrayElement<T>>[]\n | ConditionFunction<T>\n\nexport type StructScoping<T extends { [key: string]: any }> =\n | RequireAtLeastOne<{ [K in keyof T]?: Scoping<T[K]> }>\n | ConditionFunction<T>\n\nexport type Scoping<T> = T extends PrimitiveValue\n ? PrimitiveScoping<T>\n : T extends readonly any[]\n ? ArrayScoping<T>\n : T extends { [key: string]: any }\n ? StructScoping<T>\n : unknown\n\nexport type { FunctionPermission, TargetPermission }\n\nexport const EVERYTHING = Symbol.for('@zodiaceco/allow-kit/EVERYTHING')\nexport type EVERYTHING = typeof EVERYTHING\n","import { Interface, FunctionFragment, isError, type InterfaceAbi } from 'ethers'\nimport { c, coercePermission, ParameterType, Operator } from 'zodiac-roles-sdk'\nimport type {\n Condition,\n FunctionPermission,\n TargetPermission,\n} from 'zodiac-roles-sdk'\nimport { readAbi, walkContracts, type ContractNode } from './abi'\nimport { EVERYTHING, type Options } from './types'\n\nexport function buildAllowKit(\n abisDir: string,\n contractsConfig: Record<string, any>\n): Record<string, any> {\n const kit: Record<string, any> = {}\n for (const node of walkContracts(contractsConfig)) {\n const abi = readAbi(abisDir, node)\n if (!abi) {\n // Defer the error until the user touches this contract — otherwise an\n // ABI missing for one chain crashes all unrelated role definitions.\n attachAt(kit, [node.chain, ...node.segments], missingAbiProxy(node))\n continue\n }\n attachAt(\n kit,\n [node.chain, ...node.segments],\n makeAllowContract(\n node.address,\n abi as InterfaceAbi,\n [node.chain, ...node.segments].join('.')\n )\n )\n }\n return kit\n}\n\nfunction attachAt(root: Record<string, any>, segments: string[], value: any) {\n let cursor = root\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!\n if (!(seg in cursor)) cursor[seg] = {}\n cursor = cursor[seg]\n }\n cursor[segments[segments.length - 1]!] = value\n}\n\nfunction missingAbiProxy(node: ContractNode) {\n const explain = () => {\n throw new Error(\n `ABI missing for ${node.chain}.${node.segments.join('.')} ` +\n `(${node.address}). Run \\`zodiac pull-contracts\\` to fetch it, or ` +\n `paste the ABI JSON manually at <abisDir>/${node.chain}/${node.segments.join('/')}.json`\n )\n }\n return new Proxy(\n {},\n {\n get: explain,\n has: explain,\n }\n )\n}\n\nfunction makeAllowContract(\n address: `0x${string}`,\n abi: InterfaceAbi,\n path: string\n): Record<string | symbol, any> {\n const iface = Interface.from(abi)\n const lowerAddr = address.toLowerCase() as `0x${string}`\n\n // Permissionable functions grouped by bare name, so an ambiguous one can be\n // told apart from an unknown one. `Interface.getFunction` reports both as\n // INVALID_ARGUMENT, and the two want opposite answers: an unknown name is\n // simply not a member, an ambiguous one is a member the caller has not\n // finished naming.\n const overloads = new Map<string, FunctionFragment[]>()\n iface.forEachFunction((fn) => {\n if (fn.stateMutability === 'view' || fn.stateMutability === 'pure') return\n overloads.set(fn.name, [...(overloads.get(fn.name) ?? []), fn])\n })\n\n const ambiguous = (prop: string) => (overloads.get(prop)?.length ?? 0) > 1\n\n const explainAmbiguity = (prop: string): never => {\n const signatures = overloads.get(prop)!.map((fn) => fn.format('sighash'))\n throw new Error(\n `\\`allow.${path}.${prop}\\` is ambiguous: ${address} has ` +\n `${signatures.length} functions named \"${prop}\". Name the overload ` +\n `you mean by its full signature — ` +\n signatures\n .map((sig) => `allow.${path}[${JSON.stringify(sig)}]`)\n .join(' or ') +\n `.`\n )\n }\n\n const allowEverything = (options?: Options): TargetPermission => ({\n targetAddress: lowerAddr,\n send: options?.send,\n delegatecall: options?.delegatecall,\n })\n\n const has = (name: string) => {\n try {\n const fn = iface.getFunction(name)\n if (!fn) return false\n return fn.stateMutability !== 'view' && fn.stateMutability !== 'pure'\n } catch (error) {\n if (!isError(error as any, 'INVALID_ARGUMENT')) throw error\n return false\n }\n }\n\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n if (prop === EVERYTHING) return allowEverything\n if (typeof prop !== 'string') return undefined\n if (ambiguous(prop)) explainAmbiguity(prop)\n if (!has(prop)) return undefined\n const fn = iface.getFunction(prop)!\n return makeAllowFunction(fn, lowerAddr)\n },\n // An ambiguous bare name is not a member — only the full signatures are.\n // `in` answers that question; reaching for the property explains it.\n has: (_target, prop) => {\n if (prop === EVERYTHING) return true\n return typeof prop === 'string' && !ambiguous(prop) && has(prop)\n },\n }\n )\n}\n\nfunction makeAllowFunction(\n fn: FunctionFragment,\n targetAddress: `0x${string}`\n): (...args: any[]) => FunctionPermission {\n const inputs = fn.inputs\n return (...args: any[]) => {\n const scopings = args.slice(0, inputs.length)\n const hasScopings = scopings.some((s) => s !== undefined && s !== null)\n const options: Options = args[inputs.length] ?? {}\n const condition = hasScopings\n ? c.calldataMatches(scopings, inputs)()\n : undefined\n const preset = {\n targetAddress,\n signature: fn.format('sighash'),\n condition,\n }\n return applyOptions(coercePermission(preset as any) as any, options)\n }\n}\n\nconst emptyCalldataMatches: Condition = {\n paramType: ParameterType.Calldata,\n operator: Operator.Matches,\n children: [],\n}\n\nconst applyGlobalAllowance = (\n condition: Condition | undefined,\n allowanceCondition: Condition\n): Condition => {\n const base = condition ?? emptyCalldataMatches\n if (\n base.paramType !== ParameterType.Calldata ||\n base.operator !== Operator.Matches\n ) {\n throw new Error(\n 'Global allowance can only be applied to calldata matches nodes'\n )\n }\n return {\n ...base,\n children: [...(base.children ?? []), allowanceCondition],\n }\n}\n\nconst applyOptions = (\n permission: FunctionPermission & { condition?: Condition },\n options: Options\n): FunctionPermission => {\n let condition = permission.condition\n if (options.etherWithinAllowance) {\n if (!options.send) {\n throw new Error(\n '`etherWithinAllowance` can only be used if `send` is allowed'\n )\n }\n condition = applyGlobalAllowance(condition, {\n paramType: ParameterType.None,\n operator: Operator.EtherWithinAllowance,\n compValue: options.etherWithinAllowance,\n })\n }\n if (options.callWithinAllowance) {\n condition = applyGlobalAllowance(condition, {\n paramType: ParameterType.None,\n operator: Operator.CallWithinAllowance,\n compValue: options.callWithinAllowance,\n })\n }\n return {\n ...permission,\n send: options.send,\n delegatecall: options.delegatecall,\n condition,\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as resolveAbisDir, i as loadConfig, n as defineConfig, r as ensureConfigStub, t as DEFAULT_ABIS_DIR } from "../config-D5ArAp07.mjs";
1
+ import { a as resolveAbisDir, i as loadConfig, n as defineConfig, r as ensureConfigStub, t as DEFAULT_ABIS_DIR } from "../config-D0FqysME.mjs";
2
2
  export { DEFAULT_ABIS_DIR, defineConfig, ensureConfigStub, loadConfig, resolveAbisDir };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as resolveZodiacDir, t as ApiClient } from "./api-C49DoaPC.mjs";
3
- import { a as walkContracts, i as readAbi, n as chainIdFor, o as writeAbi, r as abiFilePath } from "./networks-BTW1qAAa.mjs";
4
- import { a as resolveAbisDir, i as loadConfig, o as findProjectRoot, r as ensureConfigStub } from "./config-D5ArAp07.mjs";
3
+ import { a as walkContracts, i as readAbi, n as chainIdFor, o as writeAbi, r as abiFilePath } from "./networks-B0H4Uoyh.mjs";
4
+ import { a as resolveAbisDir, i as loadConfig, o as findProjectRoot, r as ensureConfigStub } from "./config-D0FqysME.mjs";
5
5
  import fs, { existsSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import path, { basename, join, resolve } from "node:path";
7
7
  import { invariant } from "@epic-web/invariant";
@@ -309,23 +309,19 @@ declare global {
309
309
  //#endregion
310
310
  //#region src/allow/fetch.ts
311
311
  async function fetchAbi(chainId, address) {
312
- const url = `https://api.abi.pub/v1/chains/${chainId}/etherscan?module=contract&action=getabi&address=${address}`;
313
- let body;
312
+ const url = `https://api.abi.pub/v1/chains/${chainId}/accounts/${address}`;
313
+ let account;
314
314
  try {
315
315
  const resp = await fetch(url);
316
316
  if (!resp.ok) return null;
317
- body = await resp.json();
318
- } catch {
319
- return null;
320
- }
321
- if (body.status !== "1" || typeof body.result !== "string") return null;
322
- try {
323
- const parsed = JSON.parse(body.result);
324
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
325
- return parsed;
317
+ account = await resp.json();
326
318
  } catch {
327
319
  return null;
328
320
  }
321
+ if (account == null || typeof account !== "object") return null;
322
+ const abi = account.proxy ? account.implementation?.abi : account.abi;
323
+ if (!Array.isArray(abi) || abi.length === 0) return null;
324
+ return abi;
329
325
  }
330
326
  //#endregion
331
327
  //#region src/allow/codegen.ts
@@ -385,15 +381,13 @@ function renderPropKey(name) {
385
381
  }
386
382
  function renderContractType(node, abi, indent) {
387
383
  const members = [];
388
- const seen = /* @__PURE__ */ new Set();
389
384
  members.push(`${indent} [EVERYTHING]: (options?: Options) => TargetPermission;`);
390
- for (const fragment of abi) {
391
- if (fragment.type !== "function") continue;
392
- if (fragment.stateMutability === "view" || fragment.stateMutability === "pure") continue;
393
- const name = fragment.name;
394
- if (!name || seen.has(name)) continue;
395
- seen.add(name);
396
- members.push(renderFunctionSignature(fragment, indent + " "));
385
+ for (const overloads of groupByName(abi).values()) {
386
+ const overloaded = overloads.length > 1;
387
+ for (const fragment of overloads) {
388
+ const key = overloaded ? signatureFor(fragment) : fragment.name;
389
+ members.push(renderFunctionSignature(fragment, key, indent + " "));
390
+ }
397
391
  }
398
392
  return [
399
393
  "{",
@@ -401,8 +395,42 @@ function renderContractType(node, abi, indent) {
401
395
  `${indent}}`
402
396
  ].join("\n");
403
397
  }
404
- function renderFunctionSignature(fragment, indent) {
405
- const name = renderPropKey(fragment.name);
398
+ /**
399
+ * Permissionable fragments grouped by function name, in ABI order. Two
400
+ * fragments that render the same signature are the same function listed twice,
401
+ * so they collapse — emitting the key twice would not compile.
402
+ */
403
+ function groupByName(abi) {
404
+ const byName = /* @__PURE__ */ new Map();
405
+ for (const fragment of abi) {
406
+ if (fragment.type !== "function") continue;
407
+ if (fragment.stateMutability === "view" || fragment.stateMutability === "pure") continue;
408
+ const name = fragment.name;
409
+ if (!name) continue;
410
+ const overloads = byName.get(name) ?? [];
411
+ if (overloads.some((f) => signatureFor(f) === signatureFor(fragment))) continue;
412
+ byName.set(name, [...overloads, fragment]);
413
+ }
414
+ return byName;
415
+ }
416
+ /** The canonical `name(type,…)` form, which is how `Interface.getFunction`
417
+ * addresses one overload of a name. */
418
+ function signatureFor(fragment) {
419
+ const inputs = fragment.inputs ?? [];
420
+ return `${fragment.name}(${inputs.map(canonicalTypeFor).join(",")})`;
421
+ }
422
+ function canonicalTypeFor(fragment) {
423
+ const type = fragment.type;
424
+ const arrayMatch = /^(.*)\[(\d*)\]$/.exec(type);
425
+ if (arrayMatch) return `${canonicalTypeFor({
426
+ ...fragment,
427
+ type: arrayMatch[1]
428
+ })}[${arrayMatch[2]}]`;
429
+ if (type === "tuple") return `(${(fragment.components ?? []).map(canonicalTypeFor).join(",")})`;
430
+ return type;
431
+ }
432
+ function renderFunctionSignature(fragment, key, indent) {
433
+ const name = renderPropKey(key);
406
434
  const params = [];
407
435
  for (const input of fragment.inputs ?? []) {
408
436
  const paramName = sanitizeParamName(input.name || `arg${params.length}`, params.length);