Notarize AT Protocol records on Ethereum using EAS (experiment)
at main 8.4 kB view raw
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();