1#!/usr/bin/env node
2
3import { Command } from 'commander';
4import chalk from 'chalk';
5import ora from 'ora';
6import * as path from 'path';
7import * as os from 'os';
8import * as fs from 'node:fs';
9import { ATProtocolNotary } from './lib/index.js';
10import { loadConfig, getSchemaUID, createConfigTemplate } from './lib/config.js';
11
12const program = new Command();
13
14program
15 .name('atnotary')
16 .description('Notarize AT Protocol records on Ethereum using EAS')
17 .version('0.1.0')
18 .option('-c, --config <path>', 'Path to config file');
19
20// Config command
21program
22 .command('config')
23 .description('Create config template')
24 .option('-g, --global', 'Create in home directory')
25 .option('-f, --force', 'Overwrite existing config file')
26 .action((options) => {
27 const configPath = options.global
28 ? path.join(os.homedir(), '.atnotary.yaml')
29 : path.join(process.cwd(), '.atnotary.yaml');
30
31 // Check if file exists
32 if (fs.existsSync(configPath) && !options.force) {
33 console.log(chalk.yellow(`\n⚠️ Config file already exists: ${configPath}`));
34 console.log(chalk.gray('Use --force to overwrite\n'));
35 process.exit(1);
36 }
37
38 createConfigTemplate(configPath);
39 console.log(chalk.green(`\n✅ Config template created: ${configPath}`));
40 console.log(chalk.yellow('\nEdit the file and add your private key and schema UIDs.\n'));
41 });
42
43// Initialize command
44program
45 .command('init')
46 .description('Initialize: create EAS schema (one-time setup)')
47 .option('-n, --network <network>', 'Network to use', 'sepolia')
48 .action(async (options) => {
49 try {
50 const config = loadConfig(program.opts().config);
51
52 console.log(chalk.blue('🔧 Initializing EAS Schema'));
53 console.log(chalk.gray(`Network: ${options.network}\n`));
54
55 const spinner = ora(`Connecting to ${options.network}...`).start();
56
57 const notary = new ATProtocolNotary({
58 privateKey: config.privateKey,
59 }, options.network);
60
61 const address = await notary.getAddress();
62 spinner.succeed(`Connected to ${options.network}: ${address}`);
63 console.log(chalk.blue('Account:'), chalk.cyan(address));
64
65 spinner.start(`Creating schema on EAS (${options.network})...`);
66 const schemaUID = await notary.initializeSchema();
67
68 spinner.succeed('Schema created!');
69
70 console.log(chalk.green('\n✅ Schema UID:'), chalk.cyan(schemaUID));
71 console.log(chalk.yellow('\n⚠️ Add this to your .atnotary.json:'));
72 console.log(chalk.gray(`\n"networks": {\n "${options.network}": {\n "schemaUID": "${schemaUID}"\n }\n}\n`));
73
74 } catch (error: any) {
75 console.error(chalk.red('Error:'), error.message);
76 process.exit(1);
77 }
78 });
79
80// Notarize command
81program
82 .command('notarize <recordURI>')
83 .description('Notarize an AT Protocol record')
84 .option('-n, --network <network>', 'Network to use')
85 .action(async (recordURI: string, options) => {
86 try {
87 const config = loadConfig(program.opts().config);
88 const network = options.network || config.network || 'sepolia';
89
90 console.log(chalk.blue('🔐 AT Protocol Notary'));
91 console.log(chalk.gray(`Network: ${network}\n`));
92
93 const spinner = ora('Initializing...').start();
94
95 const notary = new ATProtocolNotary({
96 privateKey: config.privateKey,
97 schemaUID: getSchemaUID(config, network),
98 }, network);
99
100 // Show account info
101 const address = await notary.getAddress();
102 spinner.succeed(`Initialized with account: ${chalk.cyan(address)}`);
103
104 spinner.start('Resolving DID to PDS...');
105 const { record, pds } = await notary.fetchRecord(recordURI);
106 spinner.succeed(`Record fetched from PDS: ${pds}`);
107
108 console.log(chalk.gray('\nRecord preview:'));
109 console.log(chalk.gray(JSON.stringify(record.value, null, 2).substring(0, 200) + '...\n'));
110
111 spinner.start(`Creating attestation on ${network}...`);
112 const result = await notary.notarizeRecord(recordURI);
113 spinner.succeed('Attestation created!');
114
115 console.log(chalk.green('\n✅ Notarization Complete!\n'));
116 console.log(chalk.blue('Network:'), chalk.gray(network));
117 console.log(chalk.blue('Attester:'), chalk.cyan(address));
118 console.log(chalk.blue('Attestation UID:'), chalk.cyan(result.attestationUID));
119 console.log(chalk.blue('Record URI:'), chalk.gray(result.recordURI));
120 console.log(chalk.blue('CID:'), chalk.gray(result.cid));
121 console.log(chalk.blue('Content Hash:'), chalk.gray(result.contentHash));
122 console.log(chalk.blue('PDS:'), chalk.gray(result.pds));
123 console.log(chalk.blue('Transaction:'), chalk.gray(result.transactionHash));
124
125 console.log(chalk.yellow('\n📋 View on EAS Explorer:'));
126 console.log(chalk.cyan(result.explorerURL + '\n'));
127
128 console.log(chalk.yellow('🔍 Verify with:'));
129 console.log(chalk.gray(`atnotary verify ${result.attestationUID} --network ${network}\n`));
130
131 } catch (error: any) {
132 console.error(chalk.red('Error:'), error.message);
133 process.exit(1);
134 }
135 });
136
137
138// Verify command
139program
140 .command('verify <attestationUID>')
141 .description('Verify an attestation')
142 .option('-n, --network <network>', 'Network to use')
143 .option('--compare', 'Compare with current record state')
144 .action(async (attestationUID: string, options) => {
145 try {
146 const config = loadConfig(program.opts().config);
147 const network = options.network || config.network || 'sepolia';
148
149 console.log(chalk.blue('🔍 Verifying Attestation'));
150 console.log(chalk.gray(`Network: ${network}\n`));
151
152 const spinner = ora(`Fetching attestation from EAS (${network})...`).start();
153
154 const notary = new ATProtocolNotary({
155 schemaUID: getSchemaUID(config, network),
156 }, network);
157
158 const attestation = await notary.verifyAttestation(attestationUID);
159 spinner.succeed('Attestation found');
160
161 console.log(chalk.green('\n✅ Attestation Valid\n'));
162 console.log(chalk.blue('Network:'), chalk.gray(network));
163 console.log(chalk.blue('UID:'), chalk.cyan(attestation.uid));
164 console.log(chalk.blue('Record URI:'), chalk.gray(attestation.recordURI));
165 console.log(chalk.blue('CID:'), chalk.gray(attestation.cid));
166 console.log(chalk.blue('Content Hash:'), chalk.gray(attestation.contentHash));
167 console.log(chalk.blue('PDS:'), chalk.gray(attestation.pds));
168 console.log(chalk.blue('Timestamp:'), chalk.gray(new Date(attestation.timestamp * 1000).toISOString()));
169 console.log(chalk.blue('Attester:'), chalk.gray(attestation.attester));
170
171 if (options.compare) {
172 console.log(chalk.yellow('\n🔄 Comparing with current record...'));
173
174 try {
175 const comparison = await notary.compareWithCurrent(attestation);
176
177 if (!comparison.exists) {
178 console.log(chalk.red('⚠ Record has been deleted'));
179 } else {
180 if (comparison.cidMatches) {
181 console.log(chalk.green('✓ CID matches (content identical)'));
182 } else {
183 console.log(chalk.red('✗ CID changed (content modified)'));
184 console.log(chalk.gray(` Attested: ${attestation.cid}`));
185 console.log(chalk.gray(` Current: ${comparison.currentCid}`));
186 }
187
188 if (comparison.hashMatches) {
189 console.log(chalk.green('✓ Content hash matches'));
190 } else {
191 console.log(chalk.red('✗ Content hash changed'));
192 }
193
194 if (comparison.cidMatches && comparison.hashMatches) {
195 console.log(chalk.green('\n✅ Record unchanged since attestation'));
196 } else {
197 console.log(chalk.yellow('\n⚠️ Record has been modified since attestation'));
198 }
199 }
200 } catch (err: any) {
201 console.log(chalk.red(`⚠ Could not fetch current record: ${err.message}`));
202 }
203 }
204
205 console.log(chalk.blue('\n📋 View on explorer:'));
206 console.log(chalk.cyan(attestation.explorerURL + '\n'));
207
208 } catch (error: any) {
209 console.error(chalk.red('Error:'), error.message);
210 process.exit(1);
211 }
212 });
213
214program.parse();