Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3import fs from 'node:fs';
4import path from 'node:path';
5import process from 'node:process';
6import { fileURLToPath } from 'node:url';
7
8const __filename = fileURLToPath(import.meta.url);
9const __dirname = path.dirname(__filename);
10const ROOT_DIR = path.resolve(__dirname, '..');
11const REPORTS_DIR = path.join(ROOT_DIR, 'reports');
12
13const ADMIN_ENTRYPOINTS = new Set([
14 'set_administrator',
15 'set_contract_metadata',
16 'lock_contract_metadata',
17 'set_keep_fee',
18 'set_treasury',
19 'set_royalty_split',
20 'pause',
21 'unpause',
22 'withdraw_fees',
23]);
24
25function getArg(flag, fallback = null) {
26 const prefix = `${flag}=`;
27 for (const arg of process.argv.slice(2)) {
28 if (arg.startsWith(prefix)) return arg.slice(prefix.length);
29 }
30 return fallback;
31}
32
33function hasFlag(flag) {
34 return process.argv.slice(2).includes(flag);
35}
36
37function num(value, fallback = 0) {
38 const parsed = Number(value);
39 return Number.isFinite(parsed) ? parsed : fallback;
40}
41
42function pct(part, total) {
43 if (!total) return 0;
44 return (part / total) * 100;
45}
46
47function tzktBase(network) {
48 return network === 'mainnet' ? 'https://api.tzkt.io' : `https://api.${network}.tzkt.io`;
49}
50
51function loadDefaultContract(network) {
52 const candidate = path.join(__dirname, `contract-address-${network}.txt`);
53 if (fs.existsSync(candidate)) {
54 return fs.readFileSync(candidate, 'utf8').trim();
55 }
56 const legacy = path.join(__dirname, 'contract-address.txt');
57 if (fs.existsSync(legacy)) {
58 return fs.readFileSync(legacy, 'utf8').trim();
59 }
60 return null;
61}
62
63async function fetchJson(url) {
64 const response = await fetch(url);
65 if (!response.ok) {
66 throw new Error(`Request failed ${response.status}: ${url}`);
67 }
68 return response.json();
69}
70
71function sortByCountDesc(a, b) {
72 return b.count - a.count;
73}
74
75function buildAlerts({
76 storage,
77 contractMeta,
78 topHolders,
79 recentAdminOps,
80 expectedAdmin,
81 expectedCodeHash,
82 expectedTypeHash,
83}) {
84 const alerts = [];
85
86 if (storage?.paused === true) {
87 alerts.push({
88 severity: 'warning',
89 message: 'Contract is paused (new keeps + metadata edits are disabled).',
90 });
91 }
92
93 if (storage?.contract_metadata_locked !== true) {
94 alerts.push({
95 severity: 'warning',
96 message: 'Contract metadata is not locked.',
97 });
98 }
99
100 if (expectedAdmin && storage?.administrator !== expectedAdmin) {
101 alerts.push({
102 severity: 'critical',
103 message: `Administrator mismatch: expected ${expectedAdmin}, observed ${storage?.administrator || 'unset'}.`,
104 });
105 }
106
107 if (Number.isFinite(expectedCodeHash) && num(contractMeta?.codeHash, -1) !== expectedCodeHash) {
108 alerts.push({
109 severity: 'critical',
110 message: `codeHash mismatch: expected ${expectedCodeHash}, observed ${contractMeta?.codeHash}.`,
111 });
112 }
113
114 if (Number.isFinite(expectedTypeHash) && num(contractMeta?.typeHash, -1) !== expectedTypeHash) {
115 alerts.push({
116 severity: 'critical',
117 message: `typeHash mismatch: expected ${expectedTypeHash}, observed ${contractMeta?.typeHash}.`,
118 });
119 }
120
121 if (topHolders.length > 0) {
122 const top = topHolders[0];
123 if (top.sharePct >= 50) {
124 alerts.push({
125 severity: 'warning',
126 message: `Holder concentration is high: top holder controls ${top.sharePct.toFixed(2)}%.`,
127 });
128 }
129 }
130
131 if (recentAdminOps.length > 0) {
132 for (const op of recentAdminOps.slice(0, 5)) {
133 alerts.push({
134 severity: expectedAdmin && op.sender !== expectedAdmin ? 'critical' : 'warning',
135 message: `Recent admin entrypoint call: ${op.entrypoint} by ${op.sender} at ${op.timestamp}.`,
136 });
137 }
138 }
139
140 return alerts;
141}
142
143function toMarkdown({
144 generatedAt,
145 network,
146 contract,
147 contractMeta,
148 storage,
149 mintedCount,
150 ownersCount,
151 topHolders,
152 recentOps,
153 recentAdminOps,
154 alerts,
155}) {
156 const alertLines = alerts.length === 0
157 ? ['- none']
158 : alerts.map((alert) => `- [${alert.severity}] ${alert.message}`);
159
160 const topHolderLines = topHolders.length === 0
161 ? ['- none']
162 : topHolders.slice(0, 10).map((holder) => {
163 const alias = holder.alias ? ` (${holder.alias})` : '';
164 return `- ${holder.owner}${alias}: ${holder.count} tokens (${holder.sharePct.toFixed(2)}%)`;
165 });
166
167 const recentOpLines = recentOps.length === 0
168 ? ['- none']
169 : recentOps.slice(0, 12).map((op) => `- ${op.timestamp} · ${op.entrypoint} · ${op.sender} · ${op.hash}`);
170
171 const adminOpLines = recentAdminOps.length === 0
172 ? ['- none']
173 : recentAdminOps.map((op) => `- ${op.timestamp} · ${op.entrypoint} · ${op.sender} · ${op.hash}`);
174
175 return [
176 `# Keeps Contract Audit (${network})`,
177 '',
178 `Generated: ${generatedAt}`,
179 '',
180 '## Snapshot',
181 `- Contract: ${contract}`,
182 `- codeHash: ${contractMeta?.codeHash ?? 'n/a'}`,
183 `- typeHash: ${contractMeta?.typeHash ?? 'n/a'}`,
184 `- Admin: ${storage?.administrator ?? 'n/a'}`,
185 `- Treasury: ${storage?.treasury_address ?? 'n/a'}`,
186 `- Keep fee: ${(num(storage?.keep_fee, 0) / 1_000_000).toFixed(6)} XTZ`,
187 `- Paused: ${storage?.paused === true ? 'true' : 'false'}`,
188 `- Contract metadata locked: ${storage?.contract_metadata_locked === true ? 'true' : 'false'}`,
189 `- Royalty split: artist ${storage?.artist_royalty_bps ?? 'n/a'} bps, platform ${storage?.platform_royalty_bps ?? 'n/a'} bps`,
190 `- Minted tokens (next_token_id): ${mintedCount}`,
191 `- Current owners: ${ownersCount}`,
192 '',
193 '## Alerts',
194 ...alertLines,
195 '',
196 '## Holder Concentration',
197 ...topHolderLines,
198 '',
199 '## Recent Contract Operations',
200 ...recentOpLines,
201 '',
202 '## Recent Admin Operations',
203 ...adminOpLines,
204 '',
205 '## Notes',
206 '- This report is chain-derived (TzKT).',
207 '- Use with the v11 moat checks in keep-mint for runtime enforcement.',
208 '',
209 ].join('\n');
210}
211
212async function main() {
213 const network = getArg('--network', 'mainnet');
214 const contract = getArg('--contract', loadDefaultContract(network));
215 const limit = Math.max(10, Math.min(500, num(getArg('--limit', '120'), 120)));
216 const expectedAdmin = getArg('--expected-admin', null);
217 const expectedCodeHashRaw = getArg('--expected-code-hash', null);
218 const expectedTypeHashRaw = getArg('--expected-type-hash', null);
219 const expectedCodeHash = expectedCodeHashRaw === null ? NaN : num(expectedCodeHashRaw, NaN);
220 const expectedTypeHash = expectedTypeHashRaw === null ? NaN : num(expectedTypeHashRaw, NaN);
221 const noWrite = hasFlag('--no-write');
222 const outArg = getArg('--out', null);
223
224 if (!contract) {
225 throw new Error('No contract address found. Use --contract=KT1... or set tezos/contract-address-<network>.txt');
226 }
227
228 const apiBase = tzktBase(network);
229 const [
230 contractMeta,
231 storage,
232 balances,
233 recentOpsRaw,
234 ] = await Promise.all([
235 fetchJson(`${apiBase}/v1/contracts/${contract}`),
236 fetchJson(`${apiBase}/v1/contracts/${contract}/storage`),
237 fetchJson(`${apiBase}/v1/tokens/balances?token.contract=${contract}&balance.gt=0&limit=10000`),
238 fetchJson(`${apiBase}/v1/operations/transactions?target=${contract}&status=applied&limit=${limit}&sort.desc=level`),
239 ]);
240
241 const ownerMap = new Map();
242 for (const row of balances) {
243 const owner = row?.account?.address;
244 if (!owner) continue;
245 const entry = ownerMap.get(owner) || {
246 owner,
247 alias: row?.account?.alias || null,
248 count: 0,
249 tokenIds: [],
250 };
251 entry.count += 1;
252 entry.tokenIds.push(num(row?.token?.tokenId, -1));
253 ownerMap.set(owner, entry);
254 }
255
256 const owners = [...ownerMap.values()].sort(sortByCountDesc);
257 const mintedCount = num(storage?.next_token_id, 0);
258 const ownersCount = owners.length;
259 const topHolders = owners.map((holder) => ({
260 ...holder,
261 tokenIds: holder.tokenIds.sort((a, b) => a - b),
262 sharePct: pct(holder.count, mintedCount),
263 }));
264
265 const recentOps = recentOpsRaw.map((op) => ({
266 level: num(op?.level, 0),
267 timestamp: op?.timestamp || 'n/a',
268 entrypoint: op?.parameter?.entrypoint || 'unknown',
269 sender: op?.sender?.address || 'unknown',
270 hash: op?.hash || 'n/a',
271 }));
272
273 const recentAdminOps = recentOps.filter((op) => ADMIN_ENTRYPOINTS.has(op.entrypoint));
274
275 const alerts = buildAlerts({
276 storage,
277 contractMeta,
278 topHolders,
279 recentAdminOps,
280 expectedAdmin,
281 expectedCodeHash,
282 expectedTypeHash,
283 });
284
285 const generatedAt = new Date().toISOString();
286 const markdown = toMarkdown({
287 generatedAt,
288 network,
289 contract,
290 contractMeta,
291 storage,
292 mintedCount,
293 ownersCount,
294 topHolders,
295 recentOps,
296 recentAdminOps,
297 alerts,
298 });
299
300 const dateLabel = generatedAt.slice(0, 10);
301 const outPath = outArg
302 ? path.resolve(process.cwd(), outArg)
303 : path.join(REPORTS_DIR, `${dateLabel}-keeps-audit-${network}.md`);
304
305 console.log(`Network: ${network}`);
306 console.log(`Contract: ${contract}`);
307 console.log(`Minted: ${mintedCount}`);
308 console.log(`Owners: ${ownersCount}`);
309 console.log(`Alerts: ${alerts.length}`);
310
311 if (!noWrite) {
312 fs.mkdirSync(path.dirname(outPath), { recursive: true });
313 fs.writeFileSync(outPath, markdown, 'utf8');
314 console.log(`Report written: ${outPath}`);
315 }
316
317 if (hasFlag('--print')) {
318 console.log('\n' + markdown);
319 }
320
321 if (alerts.some((alert) => alert.severity === 'critical')) {
322 process.exitCode = 2;
323 }
324}
325
326main().catch((error) => {
327 console.error(`Audit failed: ${error.message}`);
328 process.exit(1);
329});
330