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