+3
-1
.gitignore
+3
-1
.gitignore
+1
-1
package.json
+1
-1
package.json
+3
-3
pnpm-lock.yaml
+3
-3
pnpm-lock.yaml
+25
-17
src/cli.ts
+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
+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
+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;