// Founding Charter attestation verifier. Runs in the browser and in Node 20 unchanged.
import { EAS_ADDRESS, NETWORKS, DEFAULT_IPFS_GATEWAY } from './config.js';
import { encodeGetAttestation, decodeAttestation, decodeCharterData } from './abi.js';
export { decodeCharterData } from './abi.js';
export { NETWORKS, EAS_ADDRESS, CHARTER_SCHEMA, KOGNAI_REHEARSAL } from './config.js';
const ZERO_UID = '0x' + '0'.repeat(64);
export async function sha256Hex(bytes) {
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
return '0x' + Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
}
const message = (e) => (e && e.message ? String(e.message) : String(e));
// verifyCharter never throws: every failure is reported in reason or in a text's error.
export async function verifyCharter({ uid, network, rpc, ipfs } = {}) {
const attestation = { uid: typeof uid === 'string' ? uid.toLowerCase() : uid, attester: null, time: null, revoked: false };
const fail = (reason, texts = []) => ({ ok: false, reason, attestation, texts });
if (!NETWORKS[network]) return fail(`unknown network "${network}": use base or base-sepolia`);
if (typeof rpc !== 'function' || typeof ipfs !== 'function') return fail('rpc and ipfs must be functions');
let call;
try {
call = encodeGetAttestation(uid);
} catch (e) {
return fail(message(e));
}
let raw;
try {
raw = await rpc('eth_call', [{ to: EAS_ADDRESS, data: call }, 'latest']);
} catch (e) {
return fail(`the RPC call failed: ${message(e)}`);
}
let att;
try {
att = decodeAttestation(raw);
} catch (e) {
return fail(message(e));
}
if (att.uid === ZERO_UID) return fail(`no attestation with this UID on ${network}`);
attestation.attester = att.attester;
attestation.time = att.time;
attestation.revoked = att.revocationTime !== 0;
let charter;
try {
charter = decodeCharterData(att.data);
} catch (e) {
const why = `the attestation data is not a Charter attestation: ${message(e)}`;
return fail(attestation.revoked ? `the attestation was revoked, and ${why}` : why);
}
const texts = await Promise.all(
charter.names.map(async (name, i) => {
const entry = { name, cid: charter.cids[i], expected: charter.digests[i].toLowerCase(), actual: null, match: false };
try {
const bytes = await ipfs(entry.cid);
if (!(bytes instanceof Uint8Array)) throw new Error('the text did not come back as bytes');
entry.actual = await sha256Hex(bytes);
entry.match = entry.actual === entry.expected;
} catch (e) {
entry.error = `the text could not be fetched: ${message(e)}`;
}
return entry;
}),
);
const result = { ok: false, attestation, texts, charter: { version: charter.version, date: charter.date } };
if (attestation.revoked) return { ...result, reason: 'the attestation was revoked' };
if (texts.length === 0) return { ...result, reason: 'the attestation lists no texts' };
const bad = texts.filter((t) => !t.match);
if (bad.length) {
return { ...result, reason: `${bad.length} of ${texts.length} texts do not match: ${bad.map((t) => t.name).join(', ')}` };
}
return { ...result, ok: true };
}
// Fetch based helpers the page uses. Tests pass their own rpc and ipfs.
export function httpRpc(url, fetchImpl = globalThis.fetch) {
let id = 0;
return async (method, params) => {
const res = await fetchImpl(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
});
if (!res.ok) throw new Error(`HTTP ${res.status} from the RPC`);
const body = await res.json();
if (body.error) throw new Error(body.error.message || 'RPC error');
return body.result;
};
}
export function httpIpfs(gateway = DEFAULT_IPFS_GATEWAY, fetchImpl = globalThis.fetch) {
const base = gateway.endsWith('/') ? gateway : gateway + '/';
return async (cid) => {
if (typeof cid !== 'string' || !/^[A-Za-z0-9]+(\/[^?#]*)?$/.test(cid)) throw new Error('bad CID');
const res = await fetchImpl(base + cid);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${new URL(base).host}`);
return new Uint8Array(await res.arrayBuffer());
};
}
agentgate-source: code mirror on Cloudflare Pages. All repos