Notarize AT Protocol records on Ethereum using EAS (experiment)

more updates

+2 -1
.gitignore
··· 3 node_modules 4 bun.lock 5 dist 6 - coverage
··· 3 node_modules 4 bun.lock 5 dist 6 + coverage 7 + .atnotary*
+14 -21
README.md
··· 10 - **Infrastructure** - explorers, indexers, and tooling already exist 11 - **Multi-chain** - works on Ethereum, Base, Optimism, Arbitrum 12 13 - **Why AT Protocol:** 14 - Unlike Twitter, AT Protocol is federated and open. Users own their data, can switch servers, and the protocol is public. This makes it possible to build open infrastructure for accountability that isn't controlled by any company. 15 - 16 - **Principles:** 17 - - Transparency without judgment - just preserves facts, doesn't label "good" or "bad" 18 - - Decentralized - fetches directly from user's PDS, not centralized APIs 19 - - User choice - opt-in system, you choose what to notarize 20 - - Open infrastructure - anyone can verify, build on, or run their own instance 21 - 22 - Think of it as combining two open protocols: **AT Protocol** (decentralized social) + **EAS** (decentralized attestations) = transparent, accountable social web. 23 - 24 ## What Gets Attested 25 26 - `recordURI` - Full AT Protocol URI 27 - `cid` - AT Protocol's content identifier 28 - - `contentHash` - SHA-256 hash 29 - `pds` - Personal Data Server URL 30 - `timestamp` - When attested 31 ··· 37 38 ## Setup 39 40 - 1. **Create `.env` file:** 41 42 ```bash 43 - PRIVATE_KEY="0x..." 44 - # SCHEMA_UID is optional - default schemas are provided 45 ``` 46 47 - 2. **Get testnet ETH:** 48 - Sepolia: https://sepoliafaucet.com/ 49 - Base Sepolia: https://bridge.base.org/ 50 ··· 58 atnotary init --network sepolia 59 ``` 60 61 - Then add the `SCHEMA_UID` to your `.env` file. 62 63 ## Usage 64 ··· 79 import { ATProtocolNotary } from 'atnotary'; 80 81 const notary = new ATProtocolNotary({ 82 - privateKey: process.env.PRIVATE_KEY!, 83 - schemaUID: process.env.SCHEMA_UID!, 84 }, 'sepolia'); 85 86 const result = await notary.notarizeRecord('at://...'); ··· 104 105 ## License 106 107 - MIT 108 -
··· 10 - **Infrastructure** - explorers, indexers, and tooling already exist 11 - **Multi-chain** - works on Ethereum, Base, Optimism, Arbitrum 12 13 ## What Gets Attested 14 15 - `recordURI` - Full AT Protocol URI 16 - `cid` - AT Protocol's content identifier 17 + - `contentHash` - DAG-CBOR hash 18 - `pds` - Personal Data Server URL 19 - `timestamp` - When attested 20 ··· 26 27 ## Setup 28 29 + 1. **Create config file:** 30 31 ```bash 32 + atnotary config 33 + ``` 34 + 35 + 2. **Edit `.atnotary.yaml`:** 36 + 37 + ```yaml 38 + privateKey: "0x..." # private key for writing 39 + network: base-sepolia # default network 40 ``` 41 42 + 3. **Get testnet ETH:** 43 - Sepolia: https://sepoliafaucet.com/ 44 - Base Sepolia: https://bridge.base.org/ 45 ··· 53 atnotary init --network sepolia 54 ``` 55 56 + Then add the `schemaUID` to your `.atnotary.yaml` file. 57 58 ## Usage 59 ··· 74 import { ATProtocolNotary } from 'atnotary'; 75 76 const notary = new ATProtocolNotary({ 77 + privateKey: "0x...", // optional, just for writing 78 }, 'sepolia'); 79 80 const result = await notary.notarizeRecord('at://...'); ··· 98 99 ## License 100 101 + MIT
+3 -2
package.json
··· 30 "@atproto/api": "^0.12.29", 31 "@ethereum-attestation-service/eas-sdk": "^2.9.0", 32 "@ipld/dag-cbor": "^9.2.5", 33 - "ethers": "^6.15.0" 34 }, 35 "devDependencies": { 36 "@types/node": "^20.19.21", 37 "@vitest/coverage-v8": "^3.2.4", 38 "chalk": "^5.6.2", 39 "commander": "^11.1.0", 40 - "dotenv": "^16.6.1", 41 "ora": "^8.2.0", 42 "tsx": "^4.20.6", 43 "typescript": "^5.9.3",
··· 30 "@atproto/api": "^0.12.29", 31 "@ethereum-attestation-service/eas-sdk": "^2.9.0", 32 "@ipld/dag-cbor": "^9.2.5", 33 + "ethers": "^6.15.0", 34 + "js-yaml": "^4.1.0" 35 }, 36 "devDependencies": { 37 + "@types/js-yaml": "^4.0.9", 38 "@types/node": "^20.19.21", 39 "@vitest/coverage-v8": "^3.2.4", 40 "chalk": "^5.6.2", 41 "commander": "^11.1.0", 42 "ora": "^8.2.0", 43 "tsx": "^4.20.6", 44 "typescript": "^5.9.3",
+11 -9
pnpm-lock.yaml
··· 20 ethers: 21 specifier: ^6.15.0 22 version: 6.15.0 23 devDependencies: 24 '@types/node': 25 specifier: ^20.19.21 26 version: 20.19.21 ··· 33 commander: 34 specifier: ^11.1.0 35 version: 11.1.0 36 - dotenv: 37 - specifier: ^16.6.1 38 - version: 16.6.1 39 ora: 40 specifier: ^8.2.0 41 version: 8.2.0 ··· 693 '@types/estree@1.0.8': 694 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 695 696 '@types/lru-cache@5.1.1': 697 resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} 698 ··· 1035 diff@5.2.0: 1036 resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} 1037 engines: {node: '>=0.3.1'} 1038 - 1039 - dotenv@16.6.1: 1040 - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} 1041 - engines: {node: '>=12'} 1042 1043 dunder-proto@1.0.1: 1044 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} ··· 2830 2831 '@types/estree@1.0.8': {} 2832 2833 '@types/lru-cache@5.1.1': {} 2834 2835 '@types/ms@2.1.0': {} ··· 3193 depd@2.0.0: {} 3194 3195 diff@5.2.0: {} 3196 - 3197 - dotenv@16.6.1: {} 3198 3199 dunder-proto@1.0.1: 3200 dependencies:
··· 20 ethers: 21 specifier: ^6.15.0 22 version: 6.15.0 23 + js-yaml: 24 + specifier: ^4.1.0 25 + version: 4.1.0 26 devDependencies: 27 + '@types/js-yaml': 28 + specifier: ^4.0.9 29 + version: 4.0.9 30 '@types/node': 31 specifier: ^20.19.21 32 version: 20.19.21 ··· 39 commander: 40 specifier: ^11.1.0 41 version: 11.1.0 42 ora: 43 specifier: ^8.2.0 44 version: 8.2.0 ··· 696 '@types/estree@1.0.8': 697 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 698 699 + '@types/js-yaml@4.0.9': 700 + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} 701 + 702 '@types/lru-cache@5.1.1': 703 resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} 704 ··· 1041 diff@5.2.0: 1042 resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} 1043 engines: {node: '>=0.3.1'} 1044 1045 dunder-proto@1.0.1: 1046 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} ··· 2832 2833 '@types/estree@1.0.8': {} 2834 2835 + '@types/js-yaml@4.0.9': {} 2836 + 2837 '@types/lru-cache@5.1.1': {} 2838 2839 '@types/ms@2.1.0': {} ··· 3197 depd@2.0.0: {} 3198 3199 diff@5.2.0: {} 3200 3201 dunder-proto@1.0.1: 3202 dependencies:
+64 -33
src/cli.ts
··· 3 import { Command } from 'commander'; 4 import chalk from 'chalk'; 5 import ora from 'ora'; 6 - import * as dotenv from 'dotenv'; 7 import { ATProtocolNotary } from './lib/index.js'; 8 - 9 - dotenv.config(); 10 11 const program = new Command(); 12 13 program 14 .name('atnotary') 15 .description('Notarize AT Protocol records on Ethereum using EAS') 16 - .version('0.1.0'); 17 18 // Initialize command 19 program ··· 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(); ··· 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); ··· 53 program 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); ··· 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)); ··· 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 104 program 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)); ··· 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 ··· 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 { ··· 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'));
··· 3 import { Command } from 'commander'; 4 import chalk from 'chalk'; 5 import ora from 'ora'; 6 + import * as path from 'path'; 7 + import * as os from 'os'; 8 + import * as fs from 'node:fs'; 9 import { ATProtocolNotary } from './lib/index.js'; 10 + import { loadConfig, getSchemaUID, createConfigTemplate } from './lib/config.js'; 11 12 const program = new Command(); 13 14 program 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 21 + program 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 44 program ··· 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(); ··· 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); ··· 81 program 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); ··· 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)); ··· 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 139 program 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)); ··· 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 ··· 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 { ··· 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'));
+101
src/lib/config.ts
···
··· 1 + import * as fs from 'fs'; 2 + import * as path from 'path'; 3 + import * as os from 'os'; 4 + import yaml from 'js-yaml'; 5 + 6 + export interface Config { 7 + privateKey?: string; 8 + network?: string; 9 + networks?: { 10 + [network: string]: { 11 + schemaUID?: string; 12 + rpcUrl?: string; 13 + easContractAddress?: string; 14 + schemaRegistryAddress?: string; 15 + }; 16 + }; 17 + } 18 + 19 + /** 20 + * Load config from multiple sources (priority order): 21 + * 1. Custom config path (--config flag) 22 + * 2. ./.atnotary.yaml (current directory) 23 + * 3. ~/.atnotary.yaml (home directory) 24 + * 4. .env file (fallback) 25 + */ 26 + export function loadConfig(customPath?: string): Config { 27 + let config: Config = {}; 28 + 29 + // Try custom path first 30 + if (customPath) { 31 + if (fs.existsSync(customPath)) { 32 + config = loadYamlConfig(customPath); 33 + return config; 34 + } else { 35 + throw new Error(`Config file not found: ${customPath}`); 36 + } 37 + } 38 + 39 + // Try current directory 40 + const localConfig = path.join(process.cwd(), '.atnotary.yaml'); 41 + if (fs.existsSync(localConfig)) { 42 + config = loadYamlConfig(localConfig); 43 + return config; 44 + } 45 + 46 + // Try home directory 47 + const homeConfig = path.join(os.homedir(), '.atnotary.yaml'); 48 + if (fs.existsSync(homeConfig)) { 49 + config = loadYamlConfig(homeConfig); 50 + return config; 51 + } 52 + 53 + return config; 54 + } 55 + 56 + function loadYamlConfig(filePath: string): Config { 57 + try { 58 + const fileContents = fs.readFileSync(filePath, 'utf8'); 59 + const config = yaml.load(fileContents) as Config; 60 + return config; 61 + } catch (error: any) { 62 + throw new Error(`Failed to load config from ${filePath}: ${error.message}`); 63 + } 64 + } 65 + 66 + /** 67 + * Get schema UID for specific network 68 + */ 69 + export function getSchemaUID(config: Config, network: string): string | undefined { 70 + return config.networks?.[network]?.schemaUID; 71 + } 72 + 73 + /** 74 + * Create default config template 75 + */ 76 + export function createConfigTemplate(outputPath: string): void { 77 + const template = `# atnotary configuration 78 + # Private key for signing transactions (required for notarization) 79 + privateKey: "0x..." 80 + 81 + # Default network (sepolia, base-sepolia, base) 82 + network: sepolia 83 + 84 + # Network-specific configuration 85 + networks: 86 + sepolia: 87 + schemaUID: "0x..." 88 + # Optional: custom RPC URL 89 + # rpcUrl: "https://..." 90 + 91 + base-sepolia: 92 + schemaUID: "0x..." 93 + # rpcUrl: "https://sepolia.base.org" 94 + 95 + base: 96 + schemaUID: "0x..." 97 + # rpcUrl: "https://mainnet.base.org" 98 + `; 99 + 100 + fs.writeFileSync(outputPath, template, 'utf8'); 101 + }
+3 -5
src/lib/notary.ts
··· 9 // Default schemas (deployed by atnotary maintainers) 10 const DEFAULT_SCHEMAS = { 11 'sepolia': '0x2a39517604107c79acbb962fe809795a87b7e47b8682fd9fbd3f62694fcca47c', 12 - 'base-sepolia': '0x...', // TODO: Deploy and add schema UID 13 'base': '0x...', // TODO: Deploy and add schema UID 14 }; 15 ··· 37 export class ATProtocolNotary { 38 private config: Required<NotaryConfig>; 39 private provider: ethers.JsonRpcProvider; 40 - private signer: ethers.Wallet; 41 private network: string; 42 private chainConfig: typeof CHAIN_CONFIG[keyof typeof CHAIN_CONFIG]; 43 private eas: any; 44 - private schemaRegistry: any; 45 46 constructor(config: NotaryConfig = {}, network: string = 'sepolia') { 47 this.network = network; ··· 247 const contentHash = decodedData.find(d => d.name === 'contentHash')?.value.value as string; 248 const pds = decodedData.find(d => d.name === 'pds')?.value.value as string; 249 const timestamp = Number(decodedData.find(d => d.name === 'timestamp')?.value.value); 250 - 251 - console.log({ extracted: extractHashFromCID(cid), real: contentHash }) 252 253 // Parse lexicon from recordURI (since it's not in schema) 254 const { collection: lexicon } = parseRecordURI(recordURI);
··· 9 // Default schemas (deployed by atnotary maintainers) 10 const DEFAULT_SCHEMAS = { 11 'sepolia': '0x2a39517604107c79acbb962fe809795a87b7e47b8682fd9fbd3f62694fcca47c', 12 + 'base-sepolia': '0x2a39517604107c79acbb962fe809795a87b7e47b8682fd9fbd3f62694fcca47c', 13 'base': '0x...', // TODO: Deploy and add schema UID 14 }; 15 ··· 37 export class ATProtocolNotary { 38 private config: Required<NotaryConfig>; 39 private provider: ethers.JsonRpcProvider; 40 + private signer?: ethers.Wallet; 41 private network: string; 42 private chainConfig: typeof CHAIN_CONFIG[keyof typeof CHAIN_CONFIG]; 43 private eas: any; 44 + private schemaRegistry?: any; 45 46 constructor(config: NotaryConfig = {}, network: string = 'sepolia') { 47 this.network = network; ··· 247 const contentHash = decodedData.find(d => d.name === 'contentHash')?.value.value as string; 248 const pds = decodedData.find(d => d.name === 'pds')?.value.value as string; 249 const timestamp = Number(decodedData.find(d => d.name === 'timestamp')?.value.value); 250 251 // Parse lexicon from recordURI (since it's not in schema) 252 const { collection: lexicon } = parseRecordURI(recordURI);