@zodiaceco/sdk 1.12.1
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/LICENSE +165 -0
- package/README.md +175 -0
- package/dist/allow/index.d.mts +2 -0
- package/dist/allow/index.mjs +3 -0
- package/dist/allow-CzPpzclq.mjs +121 -0
- package/dist/allow-CzPpzclq.mjs.map +1 -0
- package/dist/api-C49DoaPC.mjs +144 -0
- package/dist/api-C49DoaPC.mjs.map +1 -0
- package/dist/cli/config.d.mts +90 -0
- package/dist/cli/config.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +574 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/config-D5ArAp07.mjs +126 -0
- package/dist/config-D5ArAp07.mjs.map +1 -0
- package/dist/index-DoZbhpS6.d.mts +63 -0
- package/dist/index.d.mts +256 -0
- package/dist/index.mjs +214 -0
- package/dist/index.mjs.map +1 -0
- package/dist/networks-BTW1qAAa.mjs +77 -0
- package/dist/networks-BTW1qAAa.mjs.map +1 -0
- package/dist/zodiac-os-codegen.d.ts +14 -0
- package/package.json +68 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/cli/config.d.ts
|
|
2
|
+
type Contracts = {
|
|
3
|
+
[chain: string]: ContractsNode;
|
|
4
|
+
};
|
|
5
|
+
type ContractsNode = `0x${string}` | {
|
|
6
|
+
[name: string]: ContractsNode;
|
|
7
|
+
};
|
|
8
|
+
interface ZodiacConfig {
|
|
9
|
+
/**
|
|
10
|
+
* API key authorizing this directory against a Zodiac org.
|
|
11
|
+
*
|
|
12
|
+
* Optional — defaults to `process.env.ZODIAC_API_KEY` (populated by
|
|
13
|
+
* `zodiac init`). Set this explicitly only when you need to override
|
|
14
|
+
* the env var from inside the config file.
|
|
15
|
+
*/
|
|
16
|
+
apiKey?: `zodiac_${string}`;
|
|
17
|
+
/**
|
|
18
|
+
* Contracts the `allow` kit should know about, keyed by chain prefix.
|
|
19
|
+
* Nested objects are allowed for grouping related addresses.
|
|
20
|
+
*/
|
|
21
|
+
contracts?: Contracts;
|
|
22
|
+
/**
|
|
23
|
+
* Directory where fetched ABIs are stored and read from.
|
|
24
|
+
* Resolved relative to the project root (config file's directory).
|
|
25
|
+
* Defaults to `./abis`.
|
|
26
|
+
*/
|
|
27
|
+
abisDir?: string;
|
|
28
|
+
}
|
|
29
|
+
/** User-provided config plus the resolved API key + project root. */
|
|
30
|
+
interface ResolvedConfig extends Omit<ZodiacConfig, 'apiKey'> {
|
|
31
|
+
apiKey: `zodiac_${string}`;
|
|
32
|
+
rootDir: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Loose base used as the *inference* constraint for `defineConfig`.
|
|
36
|
+
* `contracts` is `Record<string, unknown>` here so `const T` can preserve the
|
|
37
|
+
* caller's exact address literals rather than collapsing them into the
|
|
38
|
+
* recursive `ContractsNode` union.
|
|
39
|
+
*/
|
|
40
|
+
type DefineConfigInput = {
|
|
41
|
+
apiKey?: `zodiac_${string}`;
|
|
42
|
+
contracts?: Record<string, unknown>;
|
|
43
|
+
abisDir?: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Recursive leaf-level check: every leaf in `contracts` must be
|
|
47
|
+
* `` `0x${string}` ``; any other value collapses the branch to `never`,
|
|
48
|
+
* which surfaces as a type error at the call site.
|
|
49
|
+
*/
|
|
50
|
+
type ValidateContracts<C> = { [K in keyof C]: C[K] extends `0x${string}` ? C[K] : C[K] extends object ? ValidateContracts<C[K]> : never };
|
|
51
|
+
declare const defineConfig: <const T extends DefineConfigInput>(config: T & {
|
|
52
|
+
contracts?: ValidateContracts<NonNullable<T["contracts"]>>;
|
|
53
|
+
}) => T;
|
|
54
|
+
type LoadConfigOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* Called when neither `config.apiKey` nor `process.env.ZODIAC_API_KEY` is
|
|
57
|
+
* set. Receives the resolved project root and must return a freshly minted
|
|
58
|
+
* API key. Typically wired to the interactive `init()` flow.
|
|
59
|
+
*
|
|
60
|
+
* If omitted, `loadConfig` throws when no key is found.
|
|
61
|
+
*/
|
|
62
|
+
onMissingKey?: (rootDir: string) => Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* When the config file doesn't exist, write a minimal stub before
|
|
65
|
+
* loading. Used by the CLI's `pull` commands so first-time setup
|
|
66
|
+
* works without the user having to scaffold the file themselves.
|
|
67
|
+
*/
|
|
68
|
+
createIfMissing?: boolean;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Write a starter `zodiac.config.ts` if no file exists at `absolutePath`.
|
|
72
|
+
* Returns `true` if a file was written.
|
|
73
|
+
*/
|
|
74
|
+
declare const ensureConfigStub: (absolutePath: string) => boolean;
|
|
75
|
+
declare function loadConfig(configPath?: string, options?: LoadConfigOptions): Promise<ResolvedConfig>;
|
|
76
|
+
declare const DEFAULT_ABIS_DIR = "abis";
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the absolute path to the ABIs directory. Only `rootDir` (and
|
|
79
|
+
* optionally `abisDir`) are read, so the parameter is intentionally narrower
|
|
80
|
+
* than `ResolvedConfig`: userland code can pass `{ ...config, rootDir }`
|
|
81
|
+
* without first satisfying `apiKey` (normally supplied via the
|
|
82
|
+
* `ZODIAC_API_KEY` env var at runtime).
|
|
83
|
+
*/
|
|
84
|
+
declare function resolveAbisDir(config: {
|
|
85
|
+
rootDir: string;
|
|
86
|
+
abisDir?: string;
|
|
87
|
+
}): string;
|
|
88
|
+
//#endregion
|
|
89
|
+
export { Contracts, ContractsNode, DEFAULT_ABIS_DIR, ResolvedConfig, ZodiacConfig, defineConfig, ensureConfigStub, loadConfig, resolveAbisDir };
|
|
90
|
+
//# sourceMappingURL=config.d.mts.map
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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";
|
|
5
|
+
import fs, { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import path, { basename, join, resolve } from "node:path";
|
|
7
|
+
import { invariant } from "@epic-web/invariant";
|
|
8
|
+
import { getAddress } from "ethers";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { config } from "dotenv";
|
|
11
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
import open from "open";
|
|
14
|
+
import { join as join$1 } from "path";
|
|
15
|
+
import { ModuleKind, Project, ScriptTarget, VariableDeclarationKind } from "ts-morph";
|
|
16
|
+
import { mkdirSync, writeFileSync as writeFileSync$1 } from "fs";
|
|
17
|
+
//#region src/cli/commands/init.ts
|
|
18
|
+
const DEFAULT_APP_URL = "https://app.zodiac.eco";
|
|
19
|
+
const CALLBACK_TIMEOUT_MS = 300 * 1e3;
|
|
20
|
+
const init = async (options = {}) => {
|
|
21
|
+
const rootDir = options.rootDir ?? findProjectRoot();
|
|
22
|
+
const appUrl = (options.appUrl ?? process.env.ZODIAC_APP_URL ?? DEFAULT_APP_URL).replace(/\/$/, "");
|
|
23
|
+
const label = basename(resolve(rootDir)) || "zodiac-cli";
|
|
24
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
25
|
+
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
26
|
+
const state = randomBytes(32).toString("base64url");
|
|
27
|
+
const { port, waitForCode, close } = await startCallbackServer({
|
|
28
|
+
expectedState: state,
|
|
29
|
+
appUrl
|
|
30
|
+
});
|
|
31
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
32
|
+
const authUrl = new URL("/cli-auth", appUrl);
|
|
33
|
+
authUrl.searchParams.set("callback", callbackUrl);
|
|
34
|
+
authUrl.searchParams.set("state", state);
|
|
35
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
36
|
+
authUrl.searchParams.set("label", label);
|
|
37
|
+
console.log(`Opening ${authUrl} in your browser. Approve the request to receive an API key.`);
|
|
38
|
+
await open(authUrl.toString());
|
|
39
|
+
let code;
|
|
40
|
+
try {
|
|
41
|
+
code = await waitForCode(CALLBACK_TIMEOUT_MS);
|
|
42
|
+
} finally {
|
|
43
|
+
await close();
|
|
44
|
+
}
|
|
45
|
+
const apiKey = await exchangeCodeForKey(appUrl, code, codeVerifier);
|
|
46
|
+
const envPath = join(rootDir, ".env");
|
|
47
|
+
const apiUrl = `${appUrl}/api/v1`;
|
|
48
|
+
writeEnv(envPath, {
|
|
49
|
+
ZODIAC_API_KEY: apiKey,
|
|
50
|
+
ZODIAC_API_URL: apiUrl
|
|
51
|
+
});
|
|
52
|
+
process.env.ZODIAC_API_KEY = apiKey;
|
|
53
|
+
process.env.ZODIAC_API_URL = apiUrl;
|
|
54
|
+
console.log(`✅ API key written to ${envPath}`);
|
|
55
|
+
const configPath = join(rootDir, "zodiac.config.ts");
|
|
56
|
+
if (ensureConfigStub(configPath)) console.log(`✅ Created ${configPath}`);
|
|
57
|
+
return apiKey;
|
|
58
|
+
};
|
|
59
|
+
const exchangeCodeForKey = async (appUrl, code, codeVerifier) => {
|
|
60
|
+
const response = await fetch(`${appUrl}/cli-auth/exchange`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/json" },
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
code,
|
|
65
|
+
code_verifier: codeVerifier
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
let detail = "";
|
|
70
|
+
try {
|
|
71
|
+
const body = await response.json();
|
|
72
|
+
detail = body.error ? `: ${body.error}` : "";
|
|
73
|
+
} catch {}
|
|
74
|
+
throw new Error(`Failed to exchange auth code (${response.status})${detail}`);
|
|
75
|
+
}
|
|
76
|
+
const { key } = await response.json();
|
|
77
|
+
if (typeof key !== "string" || !key.startsWith("zodiac_")) throw new Error("Exchange endpoint returned an unexpected response (missing or malformed key)");
|
|
78
|
+
return key;
|
|
79
|
+
};
|
|
80
|
+
const startCallbackServer = async ({ expectedState, appUrl }) => {
|
|
81
|
+
let resolveCode = () => {};
|
|
82
|
+
let rejectCode = () => {};
|
|
83
|
+
const codePromise = new Promise((res, rej) => {
|
|
84
|
+
resolveCode = res;
|
|
85
|
+
rejectCode = rej;
|
|
86
|
+
});
|
|
87
|
+
const handler = (req, res) => {
|
|
88
|
+
if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
|
|
89
|
+
res.statusCode = 404;
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const url = new URL(req.url, `http://127.0.0.1`);
|
|
94
|
+
const code = url.searchParams.get("code");
|
|
95
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
96
|
+
respondHtml(res, 403, renderHtml("Mismatched state", `<p>This callback didn't match the running <code>zodiac init</code> session. You can close this tab.</p>`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
100
|
+
respondHtml(res, 400, renderHtml("Missing code", `<p>The callback URL didn't include an auth <code>code</code>. Please re-run <code>zodiac init</code>.</p>`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
respondHtml(res, 200, renderHtml("Authorized", `<p>Your CLI now has its API key. You can close this tab and return to your terminal.</p>
|
|
104
|
+
<p><a href="${escapeHtml(appUrl)}">Back to Zodiac</a></p>`));
|
|
105
|
+
resolveCode(code);
|
|
106
|
+
};
|
|
107
|
+
const server = createServer(handler);
|
|
108
|
+
await new Promise((res) => server.listen(0, "127.0.0.1", res));
|
|
109
|
+
const address = server.address();
|
|
110
|
+
if (address == null || typeof address === "string") throw new Error("Failed to bind loopback callback server");
|
|
111
|
+
const port = address.port;
|
|
112
|
+
const close = () => new Promise((resolve) => {
|
|
113
|
+
server.close(() => resolve());
|
|
114
|
+
});
|
|
115
|
+
const waitForCode = (timeoutMs) => Promise.race([codePromise, new Promise((_, rej) => setTimeout(() => {
|
|
116
|
+
rejectCode(/* @__PURE__ */ new Error(`Timed out waiting for CLI auth callback`));
|
|
117
|
+
rej(/* @__PURE__ */ new Error(`Timed out waiting for CLI auth callback`));
|
|
118
|
+
}, timeoutMs))]);
|
|
119
|
+
return {
|
|
120
|
+
port,
|
|
121
|
+
waitForCode,
|
|
122
|
+
close
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const respondHtml = (res, status, body) => {
|
|
126
|
+
res.setHeader("Connection", "close");
|
|
127
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
128
|
+
res.statusCode = status;
|
|
129
|
+
res.end(body);
|
|
130
|
+
};
|
|
131
|
+
const renderHtml = (title, bodyHtml) => `<!doctype html>
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="utf-8" />
|
|
135
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
136
|
+
<title>${escapeHtml(title)} — Zodiac CLI</title>
|
|
137
|
+
<style>
|
|
138
|
+
body { font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; color: #1f2937; }
|
|
139
|
+
h1 { font-weight: 300; font-size: 1.75rem; margin-bottom: 1rem; }
|
|
140
|
+
code { background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; }
|
|
141
|
+
a { color: #1d4ed8; }
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<h1>${escapeHtml(title)}</h1>
|
|
146
|
+
${bodyHtml}
|
|
147
|
+
</body>
|
|
148
|
+
</html>
|
|
149
|
+
`;
|
|
150
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
151
|
+
const writeEnv = (envPath, vars) => {
|
|
152
|
+
let contents = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
153
|
+
for (const [key, value] of Object.entries(vars)) contents = upsertEnvLine(contents, key, value);
|
|
154
|
+
writeFileSync(envPath, contents, "utf8");
|
|
155
|
+
};
|
|
156
|
+
const upsertEnvLine = (contents, key, value) => {
|
|
157
|
+
const line = `${key}=${value}`;
|
|
158
|
+
const lines = contents.split(/\r?\n/);
|
|
159
|
+
const idx = lines.findIndex((l) => new RegExp(`^\\s*${key}\\s*=`).test(l));
|
|
160
|
+
if (idx >= 0) {
|
|
161
|
+
lines[idx] = line;
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
return contents.length === 0 || contents.endsWith("\n") ? `${contents}${line}\n` : `${contents}\n${line}\n`;
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/cli/commands/pullOrg.ts
|
|
168
|
+
const toLiteral = (value, indent = 0) => {
|
|
169
|
+
const pad = " ".repeat(indent);
|
|
170
|
+
const childPad = " ".repeat(indent + 1);
|
|
171
|
+
if (value === null) return "null";
|
|
172
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
173
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
174
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
if (value.length === 0) return "[]";
|
|
177
|
+
return `[\n${value.map((v) => `${childPad}${toLiteral(v, indent + 1)}`).join(",\n")},\n${pad}]`;
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === "object") {
|
|
180
|
+
const entries = Object.entries(value);
|
|
181
|
+
if (entries.length === 0) return "{}";
|
|
182
|
+
return `{\n${entries.map(([k, v]) => `${childPad}${/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k)}: ${toLiteral(v, indent + 1)}`).join(",\n")},\n${pad}}`;
|
|
183
|
+
}
|
|
184
|
+
return String(value);
|
|
185
|
+
};
|
|
186
|
+
const pullOrg = async (config) => {
|
|
187
|
+
const client = new ApiClient({ apiKey: config.apiKey });
|
|
188
|
+
const [users, workspaceAccounts] = await Promise.all([client.listUsers(), client.listAccounts()]);
|
|
189
|
+
const resolvableAccounts = workspaceAccounts.flatMap((ws) => ws.accounts).filter((a) => a.spec != null || a.vault);
|
|
190
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
191
|
+
if (resolvableAccounts.length > 0) {
|
|
192
|
+
const response = await client.resolveConstellation(workspaceAccounts[0].workspaceId, { specification: resolvableAccounts.map((account, i) => account.spec != null ? account.spec : {
|
|
193
|
+
ref: `vault_${i}`,
|
|
194
|
+
type: "SAFE",
|
|
195
|
+
chain: account.chain,
|
|
196
|
+
address: account.address
|
|
197
|
+
}) });
|
|
198
|
+
invariant(response?.result?.length === resolvableAccounts.length, `resolveConstellation returned ${response?.result?.length ?? 0} accounts for ${resolvableAccounts.length} accounts`);
|
|
199
|
+
resolvableAccounts.forEach((account, i) => {
|
|
200
|
+
resolved.set(account.id, response.result[i]);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const accountsRecord = {};
|
|
204
|
+
for (const ws of workspaceAccounts) {
|
|
205
|
+
const safes = {};
|
|
206
|
+
const rolesMods = {};
|
|
207
|
+
const delays = {};
|
|
208
|
+
const bucketsByType = {
|
|
209
|
+
SAFE: safes,
|
|
210
|
+
ROLES: rolesMods,
|
|
211
|
+
DELAY: delays
|
|
212
|
+
};
|
|
213
|
+
const isNodeType = (type) => type === "SAFE" || type === "ROLES" || type === "DELAY";
|
|
214
|
+
const labelCountByType = {
|
|
215
|
+
SAFE: /* @__PURE__ */ new Map(),
|
|
216
|
+
ROLES: /* @__PURE__ */ new Map(),
|
|
217
|
+
DELAY: /* @__PURE__ */ new Map()
|
|
218
|
+
};
|
|
219
|
+
for (const account of ws.accounts) {
|
|
220
|
+
const { label } = account;
|
|
221
|
+
if (label == null || !isNodeType(account.type)) continue;
|
|
222
|
+
const counts = labelCountByType[account.type];
|
|
223
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
224
|
+
}
|
|
225
|
+
for (const account of ws.accounts) {
|
|
226
|
+
const { label } = account;
|
|
227
|
+
if (label == null || !isNodeType(account.type)) continue;
|
|
228
|
+
const onChain = resolved.get(account.id);
|
|
229
|
+
const key = (labelCountByType[account.type].get(label) ?? 0) > 1 ? `${label} (${getAddress(account.address)})` : label;
|
|
230
|
+
bucketsByType[account.type][key] = {
|
|
231
|
+
id: account.id,
|
|
232
|
+
label,
|
|
233
|
+
address: account.address,
|
|
234
|
+
chain: account.chain,
|
|
235
|
+
vault: account.vault,
|
|
236
|
+
...onChain?.type === "SAFE" && {
|
|
237
|
+
threshold: onChain.threshold,
|
|
238
|
+
owners: [...onChain.owners],
|
|
239
|
+
modules: [...onChain.modules]
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
accountsRecord[ws.workspaceName] = {
|
|
244
|
+
workspaceId: ws.workspaceId,
|
|
245
|
+
workspaceName: ws.workspaceName,
|
|
246
|
+
safes,
|
|
247
|
+
rolesMods,
|
|
248
|
+
delays
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const user of users) nameCount.set(user.fullName, (nameCount.get(user.fullName) ?? 0) + 1);
|
|
253
|
+
const usersRecord = {};
|
|
254
|
+
for (const user of users) {
|
|
255
|
+
const handle = nameCount.get(user.fullName) > 1 ? `${user.fullName} (${user.id})` : user.fullName;
|
|
256
|
+
usersRecord[handle] = {
|
|
257
|
+
id: user.id,
|
|
258
|
+
fullName: user.fullName,
|
|
259
|
+
personalSafes: user.personalSafes
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const outDir = resolveZodiacDir(config.rootDir);
|
|
263
|
+
mkdirSync(outDir, { recursive: true });
|
|
264
|
+
writeFileSync$1(join$1(outDir, "package.json"), JSON.stringify({
|
|
265
|
+
type: "commonjs",
|
|
266
|
+
main: "index.js",
|
|
267
|
+
types: "index.d.ts"
|
|
268
|
+
}, null, 2));
|
|
269
|
+
const sourceFile = new Project({
|
|
270
|
+
compilerOptions: {
|
|
271
|
+
declaration: true,
|
|
272
|
+
module: ModuleKind.CommonJS,
|
|
273
|
+
target: ScriptTarget.ESNext,
|
|
274
|
+
outDir
|
|
275
|
+
},
|
|
276
|
+
useInMemoryFileSystem: true
|
|
277
|
+
}).createSourceFile("index.ts", "");
|
|
278
|
+
sourceFile.addVariableStatement({
|
|
279
|
+
isExported: true,
|
|
280
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
281
|
+
declarations: [{
|
|
282
|
+
name: "users",
|
|
283
|
+
initializer: `${toLiteral(usersRecord)} as const`
|
|
284
|
+
}]
|
|
285
|
+
});
|
|
286
|
+
sourceFile.addVariableStatement({
|
|
287
|
+
isExported: true,
|
|
288
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
289
|
+
declarations: [{
|
|
290
|
+
name: "accounts",
|
|
291
|
+
initializer: `${toLiteral(accountsRecord)} as const`
|
|
292
|
+
}]
|
|
293
|
+
});
|
|
294
|
+
const emitResult = sourceFile.getEmitOutput();
|
|
295
|
+
for (const outputFile of emitResult.getOutputFiles()) {
|
|
296
|
+
const fileName = outputFile.getFilePath().includes(".d.ts") ? "index.d.ts" : "index.js";
|
|
297
|
+
let contents = outputFile.getText();
|
|
298
|
+
if (fileName === "index.d.ts") contents += `
|
|
299
|
+
declare global {
|
|
300
|
+
interface ZodiacGeneratedCodegen {
|
|
301
|
+
users: typeof users;
|
|
302
|
+
accounts: typeof accounts;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
`;
|
|
306
|
+
writeFileSync$1(join$1(outDir, fileName), contents);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/allow/fetch.ts
|
|
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;
|
|
314
|
+
try {
|
|
315
|
+
const resp = await fetch(url);
|
|
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;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/allow/codegen.ts
|
|
332
|
+
function generateAllowTypes(abisDir, contractsConfig) {
|
|
333
|
+
const root = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const node of walkContracts(contractsConfig)) {
|
|
335
|
+
const abi = readAbi(abisDir, node);
|
|
336
|
+
if (!abi) continue;
|
|
337
|
+
insertIntoTree(root, [node.chain, ...node.segments], {
|
|
338
|
+
node,
|
|
339
|
+
abi
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const out = [];
|
|
343
|
+
out.push("// AUTO-GENERATED by `zodiac pull-contracts`. Do not edit.");
|
|
344
|
+
out.push("/* eslint-disable */");
|
|
345
|
+
out.push("");
|
|
346
|
+
out.push(`import type { FunctionPermission, TargetPermission } from "zodiac-roles-sdk";`);
|
|
347
|
+
out.push(`import type { Scoping, Options, EVERYTHING } from "@zodiaceco/sdk/allow";`);
|
|
348
|
+
out.push("");
|
|
349
|
+
out.push("declare global {");
|
|
350
|
+
out.push(" interface AllowKit " + renderTree(root, " "));
|
|
351
|
+
out.push("}");
|
|
352
|
+
out.push("");
|
|
353
|
+
out.push("export {};");
|
|
354
|
+
out.push("");
|
|
355
|
+
return out.join("\n");
|
|
356
|
+
}
|
|
357
|
+
function insertIntoTree(tree, segments, leaf) {
|
|
358
|
+
const [first, ...rest] = segments;
|
|
359
|
+
if (!first) throw new Error("empty segments");
|
|
360
|
+
if (rest.length === 0) {
|
|
361
|
+
tree.set(first, leaf);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
let child = tree.get(first);
|
|
365
|
+
if (!child || child.node) {
|
|
366
|
+
child = /* @__PURE__ */ new Map();
|
|
367
|
+
tree.set(first, child);
|
|
368
|
+
}
|
|
369
|
+
insertIntoTree(child, rest, leaf);
|
|
370
|
+
}
|
|
371
|
+
function renderTree(tree, indent) {
|
|
372
|
+
const lines = ["{"];
|
|
373
|
+
for (const [key, value] of tree) {
|
|
374
|
+
const safeKey = renderPropKey(key);
|
|
375
|
+
if (value instanceof Map) lines.push(`${indent} ${safeKey}: ${renderTree(value, indent + " ")};`);
|
|
376
|
+
else {
|
|
377
|
+
const { node, abi } = value;
|
|
378
|
+
lines.push(`${indent} ${safeKey}: ${renderContractType(node, abi, indent + " ")};`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
lines.push(`${indent}}`);
|
|
382
|
+
return lines.join("\n");
|
|
383
|
+
}
|
|
384
|
+
function renderPropKey(name) {
|
|
385
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
386
|
+
}
|
|
387
|
+
function renderContractType(node, abi, indent) {
|
|
388
|
+
const members = [];
|
|
389
|
+
const seen = /* @__PURE__ */ new Set();
|
|
390
|
+
members.push(`${indent} [EVERYTHING]: (options?: Options) => TargetPermission;`);
|
|
391
|
+
for (const fragment of abi) {
|
|
392
|
+
if (fragment.type !== "function") continue;
|
|
393
|
+
if (fragment.stateMutability === "view" || fragment.stateMutability === "pure") continue;
|
|
394
|
+
const name = fragment.name;
|
|
395
|
+
if (!name || seen.has(name)) continue;
|
|
396
|
+
seen.add(name);
|
|
397
|
+
members.push(renderFunctionSignature(fragment, indent + " "));
|
|
398
|
+
}
|
|
399
|
+
return [
|
|
400
|
+
"{",
|
|
401
|
+
...members,
|
|
402
|
+
`${indent}}`
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
function renderFunctionSignature(fragment, indent) {
|
|
406
|
+
const name = renderPropKey(fragment.name);
|
|
407
|
+
const params = [];
|
|
408
|
+
for (const input of fragment.inputs ?? []) {
|
|
409
|
+
const paramName = sanitizeParamName(input.name || `arg${params.length}`, params.length);
|
|
410
|
+
params.push(`${paramName}?: Scoping<${tsTypeFor(input)}>`);
|
|
411
|
+
}
|
|
412
|
+
params.push(`options?: Options`);
|
|
413
|
+
return `${indent}${name}: (${params.join(", ")}) => FunctionPermission;`;
|
|
414
|
+
}
|
|
415
|
+
function sanitizeParamName(name, index) {
|
|
416
|
+
const cleaned = name.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
417
|
+
if (!cleaned || /^[0-9]/.test(cleaned)) return `arg${index}`;
|
|
418
|
+
return new Set([
|
|
419
|
+
"function",
|
|
420
|
+
"class",
|
|
421
|
+
"new",
|
|
422
|
+
"number",
|
|
423
|
+
"string",
|
|
424
|
+
"object",
|
|
425
|
+
"boolean",
|
|
426
|
+
"symbol",
|
|
427
|
+
"default",
|
|
428
|
+
"return",
|
|
429
|
+
"this",
|
|
430
|
+
"void",
|
|
431
|
+
"delete",
|
|
432
|
+
"in",
|
|
433
|
+
"of",
|
|
434
|
+
"for",
|
|
435
|
+
"while",
|
|
436
|
+
"switch",
|
|
437
|
+
"case",
|
|
438
|
+
"if",
|
|
439
|
+
"else",
|
|
440
|
+
"null",
|
|
441
|
+
"true",
|
|
442
|
+
"false",
|
|
443
|
+
"undefined",
|
|
444
|
+
"any",
|
|
445
|
+
"never",
|
|
446
|
+
"unknown"
|
|
447
|
+
]).has(cleaned) ? `_${cleaned}` : cleaned;
|
|
448
|
+
}
|
|
449
|
+
function tsTypeFor(fragment) {
|
|
450
|
+
const type = fragment.type;
|
|
451
|
+
const arrayMatch = /^(.*)\[(\d*)\]$/.exec(type);
|
|
452
|
+
if (arrayMatch) return `readonly (${tsTypeFor({
|
|
453
|
+
...fragment,
|
|
454
|
+
type: arrayMatch[1]
|
|
455
|
+
})})[]`;
|
|
456
|
+
if (type === "tuple") {
|
|
457
|
+
const components = fragment.components ?? [];
|
|
458
|
+
if (components.length === 0) return "Record<string, unknown>";
|
|
459
|
+
return `{ ${components.map((c, i) => {
|
|
460
|
+
return `${renderPropKey(sanitizeParamName(c.name || `f${i}`, i))}: ${tsTypeFor(c)}`;
|
|
461
|
+
}).join("; ")} }`;
|
|
462
|
+
}
|
|
463
|
+
if (type === "address") return "`0x${string}`";
|
|
464
|
+
if (type === "bool") return "boolean";
|
|
465
|
+
if (type === "string") return "string";
|
|
466
|
+
if (type === "bytes") return "import(\"ethers\").BytesLike";
|
|
467
|
+
if (/^bytes\d+$/.test(type)) return "`0x${string}`";
|
|
468
|
+
if (/^u?int\d*$/.test(type)) return "import(\"ethers\").BigNumberish";
|
|
469
|
+
return "unknown";
|
|
470
|
+
}
|
|
471
|
+
function writeGenerated(outFile, source) {
|
|
472
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
473
|
+
fs.writeFileSync(outFile, source, "utf8");
|
|
474
|
+
}
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/cli/commands/pullContracts.ts
|
|
477
|
+
const pullContracts = async (config) => {
|
|
478
|
+
if (!config.contracts || Object.keys(config.contracts).length === 0) {
|
|
479
|
+
console.log("No contracts defined in config, skipping.");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const abisDir = resolveAbisDir(config);
|
|
483
|
+
const generatedFile = path.join(resolveZodiacDir(config.rootDir), "allow.d.ts");
|
|
484
|
+
let missing = 0;
|
|
485
|
+
let fetched = 0;
|
|
486
|
+
let existing = 0;
|
|
487
|
+
for (const node of walkContracts(config.contracts)) {
|
|
488
|
+
const file = abiFilePath(abisDir, node);
|
|
489
|
+
if (fs.existsSync(file)) {
|
|
490
|
+
existing++;
|
|
491
|
+
report(node.chain, node.segments, node.address, "ok", file);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
let chainId;
|
|
495
|
+
try {
|
|
496
|
+
chainId = chainIdFor(node.chain);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
missing++;
|
|
499
|
+
report(node.chain, node.segments, node.address, "missing", file, error.message);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const abi = await fetchAbi(chainId, node.address);
|
|
503
|
+
if (!abi) {
|
|
504
|
+
missing++;
|
|
505
|
+
report(node.chain, node.segments, node.address, "missing", file, `api.abi.pub returned no ABI for chain ${chainId}`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
writeAbi(abisDir, node, abi);
|
|
509
|
+
fetched++;
|
|
510
|
+
report(node.chain, node.segments, node.address, "fetched", file);
|
|
511
|
+
}
|
|
512
|
+
console.log("");
|
|
513
|
+
console.log(`Contracts summary: ${existing} existing, ${fetched} fetched, ${missing} missing.`);
|
|
514
|
+
if (missing > 0) {
|
|
515
|
+
console.log("");
|
|
516
|
+
console.log("Missing ABIs must be provided manually. Paste the contract");
|
|
517
|
+
console.log("ABI JSON at the paths listed above and re-run `zodiac pull-contracts`.");
|
|
518
|
+
}
|
|
519
|
+
writeGenerated(generatedFile, generateAllowTypes(abisDir, config.contracts));
|
|
520
|
+
console.log("");
|
|
521
|
+
console.log(`Wrote typings to ${path.relative(process.cwd(), generatedFile)}`);
|
|
522
|
+
if (missing > 0) process.exit(1);
|
|
523
|
+
};
|
|
524
|
+
function report(chain, segments, address, status, file, reason) {
|
|
525
|
+
const label = `${chain}.${segments.join(".")}`.padEnd(40, " ");
|
|
526
|
+
const tag = {
|
|
527
|
+
ok: " cached ",
|
|
528
|
+
fetched: " fetched ",
|
|
529
|
+
missing: " MISSING "
|
|
530
|
+
}[status];
|
|
531
|
+
const suffix = reason ? ` — ${reason}` : "";
|
|
532
|
+
console.log(`${tag} ${label} ${address}${suffix}`);
|
|
533
|
+
if (status === "missing") console.log(` → paste ABI at ${file}`);
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/cli/run.ts
|
|
537
|
+
config({ quiet: true });
|
|
538
|
+
const loadConfigOrInit = (configPath) => loadConfig(configPath, {
|
|
539
|
+
createIfMissing: true,
|
|
540
|
+
onMissingKey: async (rootDir) => {
|
|
541
|
+
console.log("No ZODIAC_API_KEY found. Starting authorization to mint one for this directory…");
|
|
542
|
+
return init({ rootDir });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
const run = async (argv = process.argv) => {
|
|
546
|
+
const program = new Command();
|
|
547
|
+
program.name("zodiac").description("Zodiac SDK CLI – pull org data and contract ABIs").version("1.0.0").option("-c, --config <path>", "path to the config file", "zodiac.config.ts");
|
|
548
|
+
program.command("pull").description("Fetch Zodiac org data and contract ABIs, generating typed SDK functions. Authorizes this directory automatically on first run.").action(async (_opts, cmd) => {
|
|
549
|
+
const config = await loadConfigOrInit(cmd.optsWithGlobals().config);
|
|
550
|
+
await Promise.all([pullOrg(config), pullContracts(config)]);
|
|
551
|
+
});
|
|
552
|
+
program.command("pull-org").description("Fetch Zodiac users and accounts, generate TypeScript types").action(async (_opts, cmd) => {
|
|
553
|
+
await pullOrg(await loadConfigOrInit(cmd.optsWithGlobals().config));
|
|
554
|
+
});
|
|
555
|
+
program.command("pull-contracts").description("Fetch contract ABIs, generate typed permissions kit").action(async (_opts, cmd) => {
|
|
556
|
+
await pullContracts(await loadConfigOrInit(cmd.optsWithGlobals().config));
|
|
557
|
+
});
|
|
558
|
+
program.command("init").description("Re-authorize this directory / mint a new API key (run automatically by `pull` on first use).").option("--app-url <url>", "Override the Zodiac app URL (defaults to ZODIAC_APP_URL or app.zodiac.eco)").action(async (opts) => {
|
|
559
|
+
await init({ appUrl: opts.appUrl });
|
|
560
|
+
});
|
|
561
|
+
await program.parseAsync(argv);
|
|
562
|
+
};
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/cli/index.ts
|
|
565
|
+
run().then(() => {
|
|
566
|
+
process.exit(0);
|
|
567
|
+
}, (error) => {
|
|
568
|
+
if (error) console.error(error);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
});
|
|
571
|
+
//#endregion
|
|
572
|
+
export {};
|
|
573
|
+
|
|
574
|
+
//# sourceMappingURL=cli.mjs.map
|