Notarize AT Protocol records on Ethereum using EAS (experiment)

update

+3 -1
.gitignore
··· 1 .env 2 .DS_Store 3 - node_modules
··· 1 .env 2 .DS_Store 3 + node_modules 4 + bun.lock 5 + dist
+1 -1
package.json
··· 25 "dependencies": { 26 "@atproto/api": "^0.12.29", 27 "@ethereum-attestation-service/eas-sdk": "^2.9.0", 28 - "viem": "^2.38.0" 29 }, 30 "devDependencies": { 31 "@types/node": "^20.19.21",
··· 25 "dependencies": { 26 "@atproto/api": "^0.12.29", 27 "@ethereum-attestation-service/eas-sdk": "^2.9.0", 28 + "ethers": "^6.15.0" 29 }, 30 "devDependencies": { 31 "@types/node": "^20.19.21",
+3 -3
pnpm-lock.yaml
··· 14 '@ethereum-attestation-service/eas-sdk': 15 specifier: ^2.9.0 16 version: 2.9.0(typescript@5.9.3)(zod@3.25.76) 17 - viem: 18 - specifier: ^2.38.0 19 - version: 2.38.0(typescript@5.9.3)(zod@3.25.76) 20 devDependencies: 21 '@types/node': 22 specifier: ^20.19.21
··· 14 '@ethereum-attestation-service/eas-sdk': 15 specifier: ^2.9.0 16 version: 2.9.0(typescript@5.9.3)(zod@3.25.76) 17 + ethers: 18 + specifier: ^6.15.0 19 + version: 6.15.0 20 devDependencies: 21 '@types/node': 22 specifier: ^20.19.21
+25 -17
src/cli.ts
··· 66 67 spinner.succeed('Initialized'); 68 69 - spinner.start('Fetching record from AT Protocol...'); 70 - const record = await notary.fetchRecord(recordURI); 71 - spinner.succeed('Record fetched'); 72 73 console.log(chalk.gray('\nRecord preview:')); 74 console.log(chalk.gray(JSON.stringify(record.value, null, 2).substring(0, 200) + '...\n')); 75 76 - spinner.start('Creating attestation...'); 77 const result = await notary.notarizeRecord(recordURI); 78 spinner.succeed('Attestation created!'); 79 80 console.log(chalk.green('\n✅ Notarization Complete!\n')); 81 console.log(chalk.blue('Attestation UID:'), chalk.cyan(result.attestationUID)); 82 console.log(chalk.blue('Record URI:'), chalk.gray(result.recordURI)); 83 console.log(chalk.blue('Content Hash:'), chalk.gray(result.contentHash)); 84 - console.log(chalk.blue('Lexicon:'), chalk.gray(result.lexicon)); 85 console.log(chalk.blue('Transaction:'), chalk.gray(result.transactionHash)); 86 87 console.log(chalk.yellow('\n📋 View on EAS Explorer:')); ··· 96 } 97 }); 98 99 // Verify command 100 program 101 .command('verify <attestationUID>') ··· 119 console.log(chalk.green('\n✅ Attestation Valid\n')); 120 console.log(chalk.blue('UID:'), chalk.cyan(attestation.uid)); 121 console.log(chalk.blue('Record URI:'), chalk.gray(attestation.recordURI)); 122 console.log(chalk.blue('Content Hash:'), chalk.gray(attestation.contentHash)); 123 - console.log(chalk.blue('Lexicon:'), chalk.gray(attestation.lexicon)); 124 console.log(chalk.blue('Timestamp:'), chalk.gray(new Date(attestation.timestamp * 1000).toISOString())); 125 console.log(chalk.blue('Attester:'), chalk.gray(attestation.attester)); 126 - console.log(chalk.blue('Revoked:'), attestation.revoked ? chalk.red('Yes') : chalk.green('No')); 127 128 // Compare with current if requested 129 if (options.compare) { 130 console.log(chalk.yellow('\n🔄 Comparing with current record...')); 131 132 - const comparison = await notary.compareWithCurrent(attestation); 133 - 134 - if (!comparison.exists) { 135 - console.log(chalk.red('⚠ Record has been deleted')); 136 - } else if (comparison.matches) { 137 - console.log(chalk.green('✓ Content matches attestation (unchanged)')); 138 - } else { 139 - console.log(chalk.yellow('⚠ Content has changed since attestation')); 140 - console.log(chalk.gray(` Original: ${attestation.contentHash.substring(0, 20)}...`)); 141 - console.log(chalk.gray(` Current: ${comparison.currentHash!.substring(0, 20)}...`)); 142 } 143 } 144 ··· 150 process.exit(1); 151 } 152 }); 153 154 program.parse();
··· 66 67 spinner.succeed('Initialized'); 68 69 + spinner.start('Resolving DID to PDS...'); 70 + const { record, pds } = await notary.fetchRecord(recordURI); 71 + spinner.succeed(`Record fetched from PDS: ${pds}`); 72 73 console.log(chalk.gray('\nRecord preview:')); 74 console.log(chalk.gray(JSON.stringify(record.value, null, 2).substring(0, 200) + '...\n')); 75 76 + spinner.start('Creating attestation on Ethereum...'); 77 const result = await notary.notarizeRecord(recordURI); 78 spinner.succeed('Attestation created!'); 79 80 console.log(chalk.green('\n✅ Notarization Complete!\n')); 81 console.log(chalk.blue('Attestation UID:'), chalk.cyan(result.attestationUID)); 82 console.log(chalk.blue('Record URI:'), chalk.gray(result.recordURI)); 83 + console.log(chalk.blue('CID:'), chalk.gray(result.cid)); 84 console.log(chalk.blue('Content Hash:'), chalk.gray(result.contentHash)); 85 + console.log(chalk.blue('PDS:'), chalk.gray(result.pds)); 86 console.log(chalk.blue('Transaction:'), chalk.gray(result.transactionHash)); 87 88 console.log(chalk.yellow('\n📋 View on EAS Explorer:')); ··· 97 } 98 }); 99 100 + 101 // Verify command 102 program 103 .command('verify <attestationUID>') ··· 121 console.log(chalk.green('\n✅ Attestation Valid\n')); 122 console.log(chalk.blue('UID:'), chalk.cyan(attestation.uid)); 123 console.log(chalk.blue('Record URI:'), chalk.gray(attestation.recordURI)); 124 + console.log(chalk.blue('CID:'), chalk.gray(attestation.cid)); 125 console.log(chalk.blue('Content Hash:'), chalk.gray(attestation.contentHash)); 126 + console.log(chalk.blue('PDS:'), chalk.gray(attestation.pds)); 127 console.log(chalk.blue('Timestamp:'), chalk.gray(new Date(attestation.timestamp * 1000).toISOString())); 128 console.log(chalk.blue('Attester:'), chalk.gray(attestation.attester)); 129 130 // Compare with current if requested 131 if (options.compare) { 132 console.log(chalk.yellow('\n🔄 Comparing with current record...')); 133 134 + try { 135 + const comparison = await notary.compareWithCurrent(attestation); 136 + 137 + if (!comparison.exists) { 138 + console.log(chalk.red('⚠ Record has been deleted')); 139 + } else if (comparison.matches) { 140 + console.log(chalk.green('✓ Content matches attestation (unchanged)')); 141 + } else { 142 + console.log(chalk.yellow('⚠ Content has changed since attestation')); 143 + console.log(chalk.gray(` Attested CID: ${attestation.cid}`)); 144 + console.log(chalk.gray(` Attested Hash: ${attestation.contentHash.substring(0, 20)}...`)); 145 + console.log(chalk.gray(` Current Hash: ${comparison.currentHash!.substring(0, 20)}...`)); 146 + } 147 + } catch (err: any) { 148 + console.log(chalk.red(`⚠ Could not fetch current record: ${err.message}`)); 149 } 150 } 151 ··· 157 process.exit(1); 158 } 159 }); 160 + 161 162 program.parse();
+158 -248
src/lib/notary.ts
··· 1 import { AtpAgent } from '@atproto/api'; 2 - import { 3 - createPublicClient, 4 - createWalletClient, 5 - http, 6 - type Address, 7 - type Hash, 8 - type PublicClient, 9 - type WalletClient, 10 - zeroAddress, 11 - encodeAbiParameters, 12 - parseAbiParameters, 13 - decodeAbiParameters, 14 - keccak256, 15 - toHex, 16 - } from 'viem'; 17 - import { privateKeyToAccount } from 'viem/accounts'; 18 - import { sepolia, base, baseSepolia } from 'viem/chains'; 19 import type { NotaryConfig, NotarizationResult, AttestationData } from './types'; 20 import { parseRecordURI, hashContent, getExplorerURL } from './utils'; 21 22 // Chain configurations 23 const CHAIN_CONFIG = { 24 'sepolia': { 25 - chain: sepolia, 26 rpcUrl: 'https://1rpc.io/sepolia', 27 - easContractAddress: '0xC2679fBD37d54388Ce493F1DB75320D236e1815e' as Address, 28 - schemaRegistryAddress: '0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0' as Address, 29 }, 30 'base-sepolia': { 31 - chain: baseSepolia, 32 rpcUrl: 'https://sepolia.base.org', 33 - easContractAddress: '0x4200000000000000000000000000000000000021' as Address, 34 - schemaRegistryAddress: '0x4200000000000000000000000000000000000020' as Address, 35 }, 36 'base': { 37 - chain: base, 38 rpcUrl: 'https://mainnet.base.org', 39 - easContractAddress: '0x4200000000000000000000000000000000000021' as Address, 40 - schemaRegistryAddress: '0x4200000000000000000000000000000000000020' as Address, 41 } 42 }; 43 44 - const SCHEMA_STRING = "string recordURI,bytes32 contentHash,string lexicon,uint256 timestamp"; 45 - 46 - // EAS Contract ABIs 47 - const SCHEMA_REGISTRY_ABI = [ 48 - { 49 - type: 'function', 50 - name: 'register', 51 - inputs: [ 52 - { name: 'schema', type: 'string' }, 53 - { name: 'resolver', type: 'address' }, 54 - { name: 'revocable', type: 'bool' } 55 - ], 56 - outputs: [{ name: 'uid', type: 'bytes32' }], 57 - stateMutability: 'nonpayable', 58 - }, 59 - ] as const; 60 - 61 - const EAS_ABI = [ 62 - { 63 - type: 'function', 64 - name: 'attest', 65 - inputs: [ 66 - { 67 - name: 'request', 68 - type: 'tuple', 69 - components: [ 70 - { name: 'schema', type: 'bytes32' }, 71 - { 72 - name: 'data', 73 - type: 'tuple', 74 - components: [ 75 - { name: 'recipient', type: 'address' }, 76 - { name: 'expirationTime', type: 'uint64' }, 77 - { name: 'revocable', type: 'bool' }, 78 - { name: 'refUID', type: 'bytes32' }, 79 - { name: 'data', type: 'bytes' }, 80 - { name: 'value', type: 'uint256' }, 81 - ], 82 - }, 83 - ], 84 - }, 85 - ], 86 - outputs: [{ name: 'uid', type: 'bytes32' }], 87 - stateMutability: 'payable', 88 - }, 89 - { 90 - type: 'function', 91 - name: 'getAttestation', 92 - inputs: [{ name: 'uid', type: 'bytes32' }], 93 - outputs: [ 94 - { 95 - name: 'attestation', 96 - type: 'tuple', 97 - components: [ 98 - { name: 'uid', type: 'bytes32' }, 99 - { name: 'schema', type: 'bytes32' }, 100 - { name: 'time', type: 'uint64' }, 101 - { name: 'expirationTime', type: 'uint64' }, 102 - { name: 'revocationTime', type: 'uint64' }, 103 - { name: 'refUID', type: 'bytes32' }, 104 - { name: 'recipient', type: 'address' }, 105 - { name: 'attester', type: 'address' }, 106 - { name: 'revocable', type: 'bool' }, 107 - { name: 'data', type: 'bytes' }, 108 - ], 109 - }, 110 - ], 111 - stateMutability: 'view', 112 - }, 113 - ] as const; 114 - 115 - // Helper to encode attestation data 116 - function encodeAttestationData(params: { 117 - recordURI: string; 118 - contentHash: Hash; 119 - lexicon: string; 120 - timestamp: number; 121 - }): Hash { 122 - return encodeAbiParameters( 123 - parseAbiParameters(SCHEMA_STRING), 124 - [params.recordURI, params.contentHash, params.lexicon, BigInt(params.timestamp)] 125 - ); 126 - } 127 - 128 - // Helper to decode attestation data 129 - function decodeAttestationData(data: Hash): { 130 - recordURI: string; 131 - contentHash: Hash; 132 - lexicon: string; 133 - timestamp: number; 134 - } { 135 - const [recordURI, contentHash, lexicon, timestamp] = decodeAbiParameters( 136 - parseAbiParameters(SCHEMA_STRING), 137 - data 138 - ); 139 - 140 - return { 141 - recordURI: recordURI as string, 142 - contentHash: contentHash as Hash, 143 - lexicon: lexicon as string, 144 - timestamp: Number(timestamp), 145 - }; 146 - } 147 148 export class ATProtocolNotary { 149 private config: Required<NotaryConfig>; 150 - private publicClient: PublicClient; 151 - private walletClient: WalletClient; 152 - private account: ReturnType<typeof privateKeyToAccount>; 153 private network: string; 154 private chainConfig: typeof CHAIN_CONFIG[keyof typeof CHAIN_CONFIG]; 155 156 constructor(config: NotaryConfig, network: string = 'sepolia') { 157 this.network = network; ··· 160 this.config = { 161 privateKey: config.privateKey, 162 rpcUrl: config.rpcUrl || this.chainConfig.rpcUrl, 163 - easContractAddress: (config.easContractAddress || this.chainConfig.easContractAddress) as Address, 164 - schemaRegistryAddress: (config.schemaRegistryAddress || this.chainConfig.schemaRegistryAddress) as Address, 165 schemaUID: config.schemaUID || '', 166 }; 167 ··· 169 throw new Error('Private key is required'); 170 } 171 172 - // Create account from private key 173 - this.account = privateKeyToAccount(this.config.privateKey as Hash); 174 175 - // Create public client (for reading) 176 - this.publicClient = createPublicClient({ 177 - chain: this.chainConfig.chain, 178 - transport: http(this.config.rpcUrl), 179 - }); 180 181 - // Create wallet client (for writing) 182 - this.walletClient = createWalletClient({ 183 - account: this.account, 184 - chain: this.chainConfig.chain, 185 - transport: http(this.config.rpcUrl), 186 - }); 187 } 188 189 /** 190 * Initialize: Create EAS schema (one-time setup) 191 */ 192 async initializeSchema(): Promise<string> { 193 - 194 - // Call register function on SchemaRegistry 195 - const hash = await this.walletClient.writeContract({ 196 - address: this.config.schemaRegistryAddress, 197 - abi: SCHEMA_REGISTRY_ABI, 198 - functionName: 'register', 199 - args: [SCHEMA_STRING, zeroAddress, true], 200 }); 201 202 - // Wait for transaction receipt 203 - const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); 204 205 - // Extract schema UID from logs 206 - // The SchemaRegistered event emits the schema UID 207 - if (receipt.logs.length > 0) { 208 - // The first topic after the event signature is the schema UID 209 - const schemaUID = receipt.logs[0].topics[1]; 210 - if (schemaUID) { 211 - return schemaUID; 212 } 213 } 214 - 215 - // Fallback: return transaction hash 216 - return hash; 217 } 218 219 /** 220 - * Fetch a record from AT Protocol 221 */ 222 - async fetchRecord(recordURI: string): Promise<any> { 223 const { did, collection, rkey } = parseRecordURI(recordURI); 224 225 - const agent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 226 227 const response = await agent.com.atproto.repo.getRecord({ 228 repo: did, ··· 230 rkey: rkey 231 }); 232 233 - return response.data; 234 } 235 236 - /** 237 - * Notarize an AT Protocol record on Ethereum 238 - */ 239 - async notarizeRecord(recordURI: string): Promise<NotarizationResult> { 240 - if (!this.config.schemaUID) { 241 - throw new Error('Schema UID not set. Run initializeSchema() first.'); 242 - } 243 244 - // Parse URI 245 - const { collection } = parseRecordURI(recordURI); 246 247 - // Fetch record 248 - const record = await this.fetchRecord(recordURI); 249 250 - // Generate content hash 251 - const contentHash = hashContent(record.value); 252 253 - // Encode attestation data 254 - const encodedData = encodeAttestationData({ 255 - recordURI, 256 - contentHash: contentHash as Hash, 257 - lexicon: collection, 258 - timestamp: Math.floor(Date.now() / 1000), 259 - }); 260 261 - // Create attestation request 262 - const attestationRequest = { 263 - schema: this.config.schemaUID as Hash, 264 - data: { 265 - recipient: zeroAddress, 266 - expirationTime: 0n, 267 - revocable: true, 268 - refUID: '0x0000000000000000000000000000000000000000000000000000000000000000' as Hash, 269 - data: encodedData, 270 - value: 0n, 271 - }, 272 - }; 273 274 - // First simulate to get the return value (UID) 275 - const { result: uid } = await this.publicClient.simulateContract({ 276 - address: this.config.easContractAddress, 277 - abi: EAS_ABI, 278 - functionName: 'attest', 279 - args: [attestationRequest], 280 - account: this.account, 281 - }); 282 283 - // Then submit the actual transaction 284 - const hash = await this.walletClient.writeContract({ 285 - address: this.config.easContractAddress, 286 - abi: EAS_ABI, 287 - functionName: 'attest', 288 - args: [attestationRequest], 289 - }); 290 291 - // Wait for transaction 292 - await this.publicClient.waitForTransactionReceipt({ hash }); 293 294 - // Use the UID from simulation 295 - const attestationUID = uid; 296 - 297 - return { 298 - attestationUID, 299 - recordURI, 300 - contentHash, 301 - lexicon: collection, 302 - transactionHash: hash, 303 - explorerURL: getExplorerURL(attestationUID, this.network), 304 - }; 305 - } 306 307 /** 308 * Verify an attestation 309 */ 310 async verifyAttestation(attestationUID: string): Promise<AttestationData> { 311 - // Read attestation from contract 312 - const attestation = await this.publicClient.readContract({ 313 - address: this.config.easContractAddress, 314 - abi: EAS_ABI, 315 - functionName: 'getAttestation', 316 - args: [attestationUID as Hash], 317 - }); 318 319 - // Check if attestation exists 320 - if (attestation.uid === '0x0000000000000000000000000000000000000000000000000000000000000000') { 321 throw new Error('Attestation not found'); 322 } 323 324 // Decode attestation data 325 - const decodedData = decodeAttestationData(attestation.data as Hash); 326 327 return { 328 uid: attestationUID, 329 - recordURI: decodedData.recordURI, 330 - contentHash: decodedData.contentHash, 331 - lexicon: decodedData.lexicon, 332 - timestamp: decodedData.timestamp, 333 attester: attestation.attester, 334 revoked: attestation.revocationTime > 0n, 335 explorerURL: getExplorerURL(attestationUID, this.network), 336 }; 337 } 338 339 /** 340 * Compare attestation with current record state ··· 345 currentHash?: string; 346 }> { 347 try { 348 - const record = await this.fetchRecord(attestationData.recordURI); 349 const currentHash = hashContent(record.value); 350 351 return { ··· 364 /** 365 * Get signer address 366 */ 367 - getAddress(): Address { 368 - return this.account.address; 369 } 370 }
··· 1 import { AtpAgent } from '@atproto/api'; 2 + import { ethers } from 'ethers'; 3 + import EASPackage from '@ethereum-attestation-service/eas-sdk'; 4 + const { EAS, SchemaEncoder, SchemaRegistry, NO_EXPIRATION } = EASPackage; 5 + 6 import type { NotaryConfig, NotarizationResult, AttestationData } from './types'; 7 import { parseRecordURI, hashContent, getExplorerURL } from './utils'; 8 9 // Chain configurations 10 const CHAIN_CONFIG = { 11 'sepolia': { 12 rpcUrl: 'https://1rpc.io/sepolia', 13 + easContractAddress: '0xC2679fBD37d54388Ce493F1DB75320D236e1815e', 14 + schemaRegistryAddress: '0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0', 15 }, 16 'base-sepolia': { 17 rpcUrl: 'https://sepolia.base.org', 18 + easContractAddress: '0x4200000000000000000000000000000000000021', 19 + schemaRegistryAddress: '0x4200000000000000000000000000000000000020', 20 }, 21 'base': { 22 rpcUrl: 'https://mainnet.base.org', 23 + easContractAddress: '0x4200000000000000000000000000000000000021', 24 + schemaRegistryAddress: '0x4200000000000000000000000000000000000020', 25 } 26 }; 27 28 + const SCHEMA_STRING = "string recordURI,string cid,bytes32 contentHash,string pds,uint256 timestamp"; 29 30 export class ATProtocolNotary { 31 private config: Required<NotaryConfig>; 32 + private provider: ethers.JsonRpcProvider; 33 + private signer: ethers.Wallet; 34 private network: string; 35 private chainConfig: typeof CHAIN_CONFIG[keyof typeof CHAIN_CONFIG]; 36 + private eas: any; 37 + private schemaRegistry: any; 38 39 constructor(config: NotaryConfig, network: string = 'sepolia') { 40 this.network = network; ··· 43 this.config = { 44 privateKey: config.privateKey, 45 rpcUrl: config.rpcUrl || this.chainConfig.rpcUrl, 46 + easContractAddress: config.easContractAddress || this.chainConfig.easContractAddress, 47 + schemaRegistryAddress: config.schemaRegistryAddress || this.chainConfig.schemaRegistryAddress, 48 schemaUID: config.schemaUID || '', 49 }; 50 ··· 52 throw new Error('Private key is required'); 53 } 54 55 + // Create ethers provider and signer 56 + this.provider = new ethers.JsonRpcProvider(this.config.rpcUrl); 57 + this.signer = new ethers.Wallet(this.config.privateKey, this.provider); 58 59 + // Initialize EAS SDK 60 + this.eas = new EAS(this.config.easContractAddress); 61 + this.eas.connect(this.signer); 62 63 + this.schemaRegistry = new SchemaRegistry(this.config.schemaRegistryAddress); 64 + this.schemaRegistry.connect(this.signer); 65 } 66 67 /** 68 * Initialize: Create EAS schema (one-time setup) 69 */ 70 async initializeSchema(): Promise<string> { 71 + const transaction = await this.schemaRegistry.register({ 72 + schema: SCHEMA_STRING, 73 + resolverAddress: ethers.ZeroAddress, 74 + revocable: true, 75 }); 76 77 + await transaction.wait(); 78 + 79 + // Return the transaction hash as schema UID placeholder 80 + return transaction.tx?.hash; 81 + } 82 83 + /** 84 + * Resolve DID to PDS endpoint 85 + */ 86 + async resolveDIDtoPDS(did: string): Promise<string> { 87 + // For did:plc, resolve via PLC directory 88 + if (did.startsWith('did:plc:')) { 89 + const response = await fetch(`https://plc.directory/${did}`); 90 + if (!response.ok) { 91 + throw new Error(`Failed to resolve DID: ${did}`); 92 } 93 + 94 + const didDocument: any = await response.json(); 95 + 96 + // Find the PDS service endpoint 97 + const pdsService = didDocument.service?.find( 98 + (s: any) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer' 99 + ); 100 + 101 + if (!pdsService?.serviceEndpoint) { 102 + throw new Error(`No PDS endpoint found for DID: ${did}`); 103 + } 104 + 105 + return pdsService.serviceEndpoint; 106 } 107 + 108 + // For did:web, resolve via web 109 + if (did.startsWith('did:web:')) { 110 + const domain = did.replace('did:web:', ''); 111 + const response = await fetch(`https://${domain}/.well-known/did.json`); 112 + if (!response.ok) { 113 + throw new Error(`Failed to resolve DID: ${did}`); 114 + } 115 + 116 + const didDocument: any = await response.json(); 117 + const pdsService = didDocument.service?.find( 118 + (s: any) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer' 119 + ); 120 + 121 + if (!pdsService?.serviceEndpoint) { 122 + throw new Error(`No PDS endpoint found for DID: ${did}`); 123 + } 124 + 125 + return pdsService.serviceEndpoint; 126 + } 127 + 128 + throw new Error(`Unsupported DID method: ${did}`); 129 } 130 131 /** 132 + * Fetch a record from AT Protocol (directly from PDS) 133 */ 134 + async fetchRecord(recordURI: string): Promise<{ record: any; pds: string }> { 135 const { did, collection, rkey } = parseRecordURI(recordURI); 136 137 + // Resolve DID to PDS 138 + const pds = await this.resolveDIDtoPDS(did); 139 + 140 + // Create agent pointing to user's PDS 141 + const agent = new AtpAgent({ service: pds }); 142 143 const response = await agent.com.atproto.repo.getRecord({ 144 repo: did, ··· 146 rkey: rkey 147 }); 148 149 + return { 150 + record: response.data, 151 + pds: pds 152 + }; 153 } 154 155 + /** 156 + * Notarize an AT Protocol record on Ethereum 157 + */ 158 + async notarizeRecord(recordURI: string): Promise<NotarizationResult> { 159 + if (!this.config.schemaUID) { 160 + throw new Error('Schema UID not set. Run initializeSchema() first.'); 161 + } 162 163 + // Parse URI 164 + const { collection } = parseRecordURI(recordURI); 165 166 + // Fetch record directly from PDS 167 + const { record, pds } = await this.fetchRecord(recordURI); 168 169 + // Generate content hash 170 + const contentHash = hashContent(record.value); 171 172 + // Get CID from the record response 173 + const recordCID = record.cid; 174 175 + // Initialize SchemaEncoder with the schema string 176 + const schemaEncoder = new SchemaEncoder(SCHEMA_STRING); 177 + const encodedData = schemaEncoder.encodeData([ 178 + { name: "recordURI", value: recordURI, type: "string" }, 179 + { name: "cid", value: recordCID, type: "string" }, 180 + { name: "contentHash", value: contentHash, type: "bytes32" }, 181 + { name: "pds", value: pds, type: "string" }, 182 + { name: "timestamp", value: Math.floor(Date.now() / 1000), type: "uint256" } 183 + ]); 184 185 + const transaction = await this.eas.attest({ 186 + schema: this.config.schemaUID, 187 + data: { 188 + recipient: ethers.ZeroAddress, 189 + expirationTime: NO_EXPIRATION, 190 + revocable: false, 191 + data: encodedData, 192 + }, 193 + }); 194 195 + const newAttestationUID = await transaction.wait(); 196 197 + return { 198 + attestationUID: newAttestationUID, 199 + recordURI, 200 + cid: recordCID, 201 + contentHash, 202 + pds, 203 + lexicon: collection, 204 + transactionHash: transaction.tx?.hash, 205 + explorerURL: getExplorerURL(newAttestationUID, this.network), 206 + }; 207 + } 208 209 210 /** 211 * Verify an attestation 212 */ 213 async verifyAttestation(attestationUID: string): Promise<AttestationData> { 214 + const attestation = await this.eas.getAttestation(attestationUID); 215 216 + if (!attestation || attestation.uid === '0x0000000000000000000000000000000000000000000000000000000000000000') { 217 throw new Error('Attestation not found'); 218 } 219 220 // Decode attestation data 221 + const schemaEncoder = new SchemaEncoder(SCHEMA_STRING); 222 + const decodedData = schemaEncoder.decodeData(attestation.data); 223 + 224 + const recordURI = decodedData.find(d => d.name === 'recordURI')?.value.value as string; 225 + const cid = decodedData.find(d => d.name === 'cid')?.value.value as string; 226 + const contentHash = decodedData.find(d => d.name === 'contentHash')?.value.value as string; 227 + const pds = decodedData.find(d => d.name === 'pds')?.value.value as string; 228 + const timestamp = Number(decodedData.find(d => d.name === 'timestamp')?.value.value); 229 + 230 + // Parse lexicon from recordURI (since it's not in schema) 231 + const { collection: lexicon } = parseRecordURI(recordURI); 232 233 return { 234 uid: attestationUID, 235 + recordURI, 236 + cid, 237 + contentHash, 238 + pds, 239 + lexicon, 240 + timestamp, 241 attester: attestation.attester, 242 revoked: attestation.revocationTime > 0n, 243 explorerURL: getExplorerURL(attestationUID, this.network), 244 }; 245 } 246 + 247 248 /** 249 * Compare attestation with current record state ··· 254 currentHash?: string; 255 }> { 256 try { 257 + // fetchRecord now returns { record, pds }, so we need to destructure 258 + const { record } = await this.fetchRecord(attestationData.recordURI); 259 const currentHash = hashContent(record.value); 260 261 return { ··· 274 /** 275 * Get signer address 276 */ 277 + async getAddress(): Promise<string> { 278 + return this.signer.getAddress(); 279 } 280 }
+4
src/lib/types.ts
··· 9 export interface NotarizationResult { 10 attestationUID: string; 11 recordURI: string; 12 contentHash: string; 13 lexicon: string; 14 transactionHash: string; 15 explorerURL: string; ··· 18 export interface AttestationData { 19 uid: string; 20 recordURI: string; 21 contentHash: string; 22 lexicon: string; 23 timestamp: number; 24 attester: string;
··· 9 export interface NotarizationResult { 10 attestationUID: string; 11 recordURI: string; 12 + cid: string; 13 contentHash: string; 14 + pds: string; 15 lexicon: string; 16 transactionHash: string; 17 explorerURL: string; ··· 20 export interface AttestationData { 21 uid: string; 22 recordURI: string; 23 + cid: string; 24 contentHash: string; 25 + pds: string; 26 lexicon: string; 27 timestamp: number; 28 attester: string;