#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'node:fs'; import { ATProtocolNotary } from './lib/index.js'; import { loadConfig, getSchemaUID, createConfigTemplate } from './lib/config.js'; const program = new Command(); program .name('atnotary') .description('Notarize AT Protocol records on Ethereum using EAS') .version('0.1.0') .option('-c, --config ', 'Path to config file'); // Config command program .command('config') .description('Create config template') .option('-g, --global', 'Create in home directory') .option('-f, --force', 'Overwrite existing config file') .action((options) => { const configPath = options.global ? path.join(os.homedir(), '.atnotary.yaml') : path.join(process.cwd(), '.atnotary.yaml'); // Check if file exists if (fs.existsSync(configPath) && !options.force) { console.log(chalk.yellow(`\nāš ļø Config file already exists: ${configPath}`)); console.log(chalk.gray('Use --force to overwrite\n')); process.exit(1); } createConfigTemplate(configPath); console.log(chalk.green(`\nāœ… Config template created: ${configPath}`)); console.log(chalk.yellow('\nEdit the file and add your private key and schema UIDs.\n')); }); // Initialize command program .command('init') .description('Initialize: create EAS schema (one-time setup)') .option('-n, --network ', 'Network to use', 'sepolia') .action(async (options) => { try { const config = loadConfig(program.opts().config); console.log(chalk.blue('šŸ”§ Initializing EAS Schema')); console.log(chalk.gray(`Network: ${options.network}\n`)); const spinner = ora(`Connecting to ${options.network}...`).start(); const notary = new ATProtocolNotary({ privateKey: config.privateKey, }, options.network); const address = await notary.getAddress(); spinner.succeed(`Connected to ${options.network}: ${address}`); console.log(chalk.blue('Account:'), chalk.cyan(address)); spinner.start(`Creating schema on EAS (${options.network})...`); const schemaUID = await notary.initializeSchema(); spinner.succeed('Schema created!'); console.log(chalk.green('\nāœ… Schema UID:'), chalk.cyan(schemaUID)); console.log(chalk.yellow('\nāš ļø Add this to your .atnotary.json:')); console.log(chalk.gray(`\n"networks": {\n "${options.network}": {\n "schemaUID": "${schemaUID}"\n }\n}\n`)); } catch (error: any) { console.error(chalk.red('Error:'), error.message); process.exit(1); } }); // Notarize command program .command('notarize ') .description('Notarize an AT Protocol record') .option('-n, --network ', 'Network to use') .action(async (recordURI: string, options) => { try { const config = loadConfig(program.opts().config); const network = options.network || config.network || 'sepolia'; console.log(chalk.blue('šŸ” AT Protocol Notary')); console.log(chalk.gray(`Network: ${network}\n`)); const spinner = ora('Initializing...').start(); const notary = new ATProtocolNotary({ privateKey: config.privateKey, schemaUID: getSchemaUID(config, network), }, network); // Show account info const address = await notary.getAddress(); spinner.succeed(`Initialized with account: ${chalk.cyan(address)}`); spinner.start('Resolving DID to PDS...'); const { record, pds } = await notary.fetchRecord(recordURI); spinner.succeed(`Record fetched from PDS: ${pds}`); console.log(chalk.gray('\nRecord preview:')); console.log(chalk.gray(JSON.stringify(record.value, null, 2).substring(0, 200) + '...\n')); spinner.start(`Creating attestation on ${network}...`); const result = await notary.notarizeRecord(recordURI); spinner.succeed('Attestation created!'); console.log(chalk.green('\nāœ… Notarization Complete!\n')); console.log(chalk.blue('Network:'), chalk.gray(network)); console.log(chalk.blue('Attester:'), chalk.cyan(address)); console.log(chalk.blue('Attestation UID:'), chalk.cyan(result.attestationUID)); console.log(chalk.blue('Record URI:'), chalk.gray(result.recordURI)); console.log(chalk.blue('CID:'), chalk.gray(result.cid)); console.log(chalk.blue('Content Hash:'), chalk.gray(result.contentHash)); console.log(chalk.blue('PDS:'), chalk.gray(result.pds)); console.log(chalk.blue('Transaction:'), chalk.gray(result.transactionHash)); console.log(chalk.yellow('\nšŸ“‹ View on EAS Explorer:')); console.log(chalk.cyan(result.explorerURL + '\n')); console.log(chalk.yellow('šŸ” Verify with:')); console.log(chalk.gray(`atnotary verify ${result.attestationUID} --network ${network}\n`)); } catch (error: any) { console.error(chalk.red('Error:'), error.message); process.exit(1); } }); // Verify command program .command('verify ') .description('Verify an attestation') .option('-n, --network ', 'Network to use') .option('--compare', 'Compare with current record state') .action(async (attestationUID: string, options) => { try { const config = loadConfig(program.opts().config); const network = options.network || config.network || 'sepolia'; console.log(chalk.blue('šŸ” Verifying Attestation')); console.log(chalk.gray(`Network: ${network}\n`)); const spinner = ora(`Fetching attestation from EAS (${network})...`).start(); const notary = new ATProtocolNotary({ schemaUID: getSchemaUID(config, network), }, network); const attestation = await notary.verifyAttestation(attestationUID); spinner.succeed('Attestation found'); console.log(chalk.green('\nāœ… Attestation Valid\n')); console.log(chalk.blue('Network:'), chalk.gray(network)); console.log(chalk.blue('UID:'), chalk.cyan(attestation.uid)); console.log(chalk.blue('Record URI:'), chalk.gray(attestation.recordURI)); console.log(chalk.blue('CID:'), chalk.gray(attestation.cid)); console.log(chalk.blue('Content Hash:'), chalk.gray(attestation.contentHash)); console.log(chalk.blue('PDS:'), chalk.gray(attestation.pds)); console.log(chalk.blue('Timestamp:'), chalk.gray(new Date(attestation.timestamp * 1000).toISOString())); console.log(chalk.blue('Attester:'), chalk.gray(attestation.attester)); if (options.compare) { console.log(chalk.yellow('\nšŸ”„ Comparing with current record...')); try { const comparison = await notary.compareWithCurrent(attestation); if (!comparison.exists) { console.log(chalk.red('⚠ Record has been deleted')); } else { if (comparison.cidMatches) { console.log(chalk.green('āœ“ CID matches (content identical)')); } else { console.log(chalk.red('āœ— CID changed (content modified)')); console.log(chalk.gray(` Attested: ${attestation.cid}`)); console.log(chalk.gray(` Current: ${comparison.currentCid}`)); } if (comparison.hashMatches) { console.log(chalk.green('āœ“ Content hash matches')); } else { console.log(chalk.red('āœ— Content hash changed')); } if (comparison.cidMatches && comparison.hashMatches) { console.log(chalk.green('\nāœ… Record unchanged since attestation')); } else { console.log(chalk.yellow('\nāš ļø Record has been modified since attestation')); } } } catch (err: any) { console.log(chalk.red(`⚠ Could not fetch current record: ${err.message}`)); } } console.log(chalk.blue('\nšŸ“‹ View on explorer:')); console.log(chalk.cyan(attestation.explorerURL + '\n')); } catch (error: any) { console.error(chalk.red('Error:'), error.message); process.exit(1); } }); program.parse();