Notarize AT Protocol records on Ethereum using EAS (experiment)
at main 7.4 kB view raw
1import { describe, it, expect, vi, beforeEach } from 'vitest'; 2import { ATProtocolNotary } from '../../src/lib/notary'; 3 4// Mock external dependencies 5vi.mock('@atproto/api', () => ({ 6 AtpAgent: vi.fn().mockImplementation(() => ({ 7 com: { 8 atproto: { 9 repo: { 10 getRecord: vi.fn().mockResolvedValue({ 11 data: { 12 value: { text: 'Test post', createdAt: '2024-01-01' }, 13 cid: 'bafyreiabc123', 14 }, 15 }), 16 }, 17 }, 18 }, 19 })), 20})); 21 22vi.mock('@ethereum-attestation-service/eas-sdk', () => { 23 const mockEAS = { 24 connect: vi.fn(), 25 attest: vi.fn().mockResolvedValue({ 26 wait: vi.fn().mockResolvedValue('0xattestationuid123'), 27 receipt: { 28 hash: '0xtxhash123', 29 transactionHash: '0xtxhash123', 30 }, 31 }), 32 getAttestation: vi.fn().mockResolvedValue({ 33 uid: '0xattestationuid123', 34 attester: '0xattester', 35 revocationTime: 0n, 36 data: '0xencodeddata', 37 }), 38 }; 39 40 const mockSchemaRegistry = { 41 connect: vi.fn(), 42 register: vi.fn().mockResolvedValue({ 43 wait: vi.fn().mockResolvedValue({ 44 transactionHash: '0xschemahash', 45 hash: '0xschemahash', 46 }), 47 receipt: { 48 hash: '0xschemahash', 49 transactionHash: '0xschemahash', 50 }, 51 }), 52 }; 53 54 return { 55 default: { 56 EAS: vi.fn(() => mockEAS), 57 SchemaRegistry: vi.fn(() => mockSchemaRegistry), 58 SchemaEncoder: vi.fn().mockImplementation(() => ({ 59 encodeData: vi.fn().mockReturnValue('0xencodeddata'), 60 decodeData: vi.fn().mockReturnValue([ 61 { name: 'recordURI', value: { value: 'at://did:plc:test/app.bsky.feed.post/123' } }, 62 { name: 'cid', value: { value: 'bafyreiabc123' } }, 63 { name: 'contentHash', value: { value: '0xa64b286ffd5cc55c57f4e9c74d1122aa081dc5f662648cb5cc5ced74e0e12cd5' } }, 64 { name: 'pds', value: { value: 'https://pds.example.com' } }, 65 { name: 'timestamp', value: { value: 1234567890 } }, 66 ]), 67 })), 68 NO_EXPIRATION: 0n, 69 }, 70 }; 71}); 72 73global.fetch = vi.fn(); 74 75describe('ATProtocolNotary', () => { 76 beforeEach(() => { 77 vi.clearAllMocks(); 78 79 // Mock DID resolution 80 (global.fetch as any).mockResolvedValue({ 81 ok: true, 82 json: async () => ({ 83 service: [ 84 { 85 id: '#atproto_pds', 86 type: 'AtprotoPersonalDataServer', 87 serviceEndpoint: 'https://pds.example.com', 88 }, 89 ], 90 }), 91 }); 92 }); 93 94 describe('constructor', () => { 95 it('should create instance with valid config', () => { 96 const notary = new ATProtocolNotary({ 97 privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', 98 schemaUID: '0xschemauid', 99 }, 'sepolia'); 100 101 expect(notary).toBeInstanceOf(ATProtocolNotary); 102 expect(notary.getAddress()).toBeTruthy(); 103 }); 104 }); 105 106 describe('resolveDIDtoPDS', () => { 107 it('should resolve did:plc to PDS', async () => { 108 const notary = new ATProtocolNotary({ 109 schemaUID: '0xschemauid', 110 }); 111 112 const pds = await notary.resolveDIDtoPDS('did:plc:test123'); 113 114 expect(pds).toBe('https://pds.example.com'); 115 expect(global.fetch).toHaveBeenCalledWith('https://plc.directory/did:plc:test123'); 116 }); 117 118 it('should throw error for unsupported DID method', async () => { 119 const notary = new ATProtocolNotary({ 120 schemaUID: '0xschemauid', 121 }); 122 123 await expect(notary.resolveDIDtoPDS('did:unsupported:test')).rejects.toThrow( 124 'Unsupported DID method' 125 ); 126 }); 127 128 it('should throw error when PDS not found', async () => { 129 (global.fetch as any).mockResolvedValueOnce({ 130 ok: true, 131 json: async () => ({ service: [] }), 132 }); 133 134 const notary = new ATProtocolNotary({ 135 schemaUID: '0xschemauid', 136 }); 137 138 await expect(notary.resolveDIDtoPDS('did:plc:test')).rejects.toThrow( 139 'No PDS endpoint found' 140 ); 141 }); 142 }); 143 144 describe('fetchRecord', () => { 145 it('should fetch record from PDS', async () => { 146 const notary = new ATProtocolNotary({ 147 schemaUID: '0xschemauid', 148 }); 149 150 const result = await notary.fetchRecord('at://did:plc:test/app.bsky.feed.post/123'); 151 152 expect(result.record.value.text).toBe('Test post'); 153 expect(result.record.cid).toBe('bafyreiabc123'); 154 expect(result.pds).toBe('https://pds.example.com'); 155 }); 156 }); 157 158 describe('notarizeRecord', () => { 159 it('should create attestation successfully', async () => { 160 const notary = new ATProtocolNotary({ 161 privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', 162 schemaUID: '0xschemauid123', 163 }); 164 165 const result = await notary.notarizeRecord('at://did:plc:test/app.bsky.feed.post/123'); 166 167 expect(result.attestationUID).toBe('0xattestationuid123'); 168 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123'); 169 expect(result.cid).toBe('bafyreiabc123'); 170 expect(result.pds).toBe('https://pds.example.com'); 171 expect(result.transactionHash).toBeTruthy(); 172 }); 173 }); 174 175 describe('verifyAttestation', () => { 176 it('should verify attestation successfully', async () => { 177 const notary = new ATProtocolNotary({ 178 schemaUID: '0xschemauid', 179 }); 180 181 const result = await notary.verifyAttestation('0xattestationuid123'); 182 183 expect(result.uid).toBe('0xattestationuid123'); 184 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123'); 185 expect(result.cid).toBe('bafyreiabc123'); 186 expect(result.pds).toBe('https://pds.example.com'); 187 expect(result.attester).toBe('0xattester'); 188 expect(result.revoked).toBe(false); 189 }); 190 }); 191 192 describe('compareWithCurrent', () => { 193 it('should detect unchanged content', async () => { 194 const notary = new ATProtocolNotary({ 195 schemaUID: '0xschemauid', 196 }); 197 198 const attestation = await notary.verifyAttestation('0xattestationuid123'); 199 const comparison = await notary.compareWithCurrent(attestation); 200 201 expect(comparison.exists).toBe(true); 202 expect(comparison.cidMatches).toBe(true); 203 expect(comparison.hashMatches).toBe(true); 204 expect(comparison.currentCid).toBeTruthy(); 205 expect(comparison.currentHash).toBeTruthy(); 206 }); 207 208 it('should detect changed content', async () => { 209 // Mock different CID and hash 210 const { AtpAgent } = await import('@atproto/api'); 211 (AtpAgent as any).mockImplementation(() => ({ 212 com: { 213 atproto: { 214 repo: { 215 getRecord: vi.fn().mockResolvedValue({ 216 data: { 217 value: { text: 'Modified post', createdAt: '2024-01-02' }, 218 cid: 'bafyreidifferent123', 219 }, 220 }), 221 }, 222 }, 223 }, 224 })); 225 226 const notary = new ATProtocolNotary({ 227 schemaUID: '0xschemauid', 228 }); 229 230 const attestation = await notary.verifyAttestation('0xattestationuid123'); 231 const comparison = await notary.compareWithCurrent(attestation); 232 233 expect(comparison.exists).toBe(true); 234 expect(comparison.cidMatches).toBe(false); 235 expect(comparison.hashMatches).toBe(false); 236 }); 237 }); 238 239});