// Minimal ABI encoding and decoding for the few ERC-8004 calls the viewer makes. No dependency.
export const SELECTORS = {
ownerOf: '0x6352211e', // ownerOf(uint256)
tokenURI: '0xc87b56dd', // tokenURI(uint256)
readAllFeedback: '0xd9d84224', // readAllFeedback(uint256,address[],string,string,bool)
};
const ZERO_WORD = '0'.repeat(64);
export function toUint256(value) {
let n;
try {
n = BigInt(value);
} catch {
throw new Error('agentId must be a whole number');
}
if (n < 0n || n >= 2n ** 256n) throw new Error('agentId is out of range');
return n.toString(16).padStart(64, '0');
}
export function encodeOwnerOf(agentId) {
return SELECTORS.ownerOf + toUint256(agentId);
}
export function encodeTokenURI(agentId) {
return SELECTORS.tokenURI + toUint256(agentId);
}
// readAllFeedback(agentId, [], '', '', false): every client, no tag filter, revoked left out.
export function encodeReadAllFeedback(agentId) {
const head = [toUint256(agentId), word(0xa0), word(0xc0), word(0xe0), ZERO_WORD];
const tail = [ZERO_WORD, ZERO_WORD, ZERO_WORD];
return SELECTORS.readAllFeedback + head.join('') + tail.join('');
}
function word(n) {
return BigInt(n).toString(16).padStart(64, '0');
}
function strip(hex) {
if (typeof hex !== 'string' || !/^0x[0-9a-fA-F]*$/.test(hex)) throw new Error('the RPC answer is not hex data');
return hex.slice(2);
}
function reader(hex) {
const h = strip(hex);
const wordAt = (byteOffset) => {
const s = h.slice(byteOffset * 2, byteOffset * 2 + 64);
if (s.length !== 64) throw new Error('the RPC answer is too short');
return s;
};
const uint = (byteOffset) => BigInt('0x' + wordAt(byteOffset));
const num = (byteOffset) => {
const v = uint(byteOffset);
if (v > BigInt(h.length / 2)) throw new Error('the RPC answer has a bad offset');
return Number(v);
};
const bytesAt = (byteOffset) => {
const len = num(byteOffset);
const s = h.slice((byteOffset + 32) * 2, (byteOffset + 32 + len) * 2);
if (s.length !== len * 2) throw new Error('the RPC answer is too short');
return s;
};
return { h, wordAt, uint, num, bytesAt };
}
function hexToUtf8(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return new TextDecoder().decode(bytes);
}
function wordToAddress(w) {
return '0x' + w.slice(24);
}
function wordToInt(w, bits) {
let v = BigInt('0x' + w);
if (v >= 2n ** 255n) v -= 2n ** 256n; // two's complement over 256 bits
const limit = 2n ** BigInt(bits - 1);
if (v >= limit || v < -limit) throw new Error('the RPC answer has an out of range number');
return v;
}
export function decodeAddress(hex) {
const r = reader(hex);
return wordToAddress(r.wordAt(0)).toLowerCase();
}
export function decodeString(hex) {
const r = reader(hex);
return hexToUtf8(r.bytesAt(r.num(0)));
}
// The seven arrays of readAllFeedback, zipped into entries.
export function decodeFeedback(hex) {
const r = reader(hex);
const arr = (i) => {
const off = r.num(i * 32);
const len = r.num(off);
return { off, len, base: off + 32 };
};
const words = (i, map) => {
const a = arr(i);
return Array.from({ length: a.len }, (_, k) => map(r.wordAt(a.base + k * 32)));
};
const strings = (i) => {
const a = arr(i);
return Array.from({ length: a.len }, (_, k) => hexToUtf8(r.bytesAt(a.base + r.num(a.base + k * 32))));
};
const clients = words(0, (w) => wordToAddress(w).toLowerCase());
const indexes = words(1, (w) => Number(BigInt('0x' + w)));
const values = words(2, (w) => wordToInt(w, 128));
const decimals = words(3, (w) => Number(BigInt('0x' + w)));
const tag1s = strings(4);
const tag2s = strings(5);
const revoked = words(6, (w) => BigInt('0x' + w) !== 0n);
const n = clients.length;
for (const a of [indexes, values, decimals, tag1s, tag2s, revoked]) {
if (a.length !== n) throw new Error('the feedback arrays do not line up');
}
return clients.map((client, k) => ({
client,
feedbackIndex: indexes[k],
value: values[k],
valueDecimals: decimals[k],
tag1: tag1s[k],
tag2: tag2s[k],
revoked: revoked[k],
}));
}
agentgate-source: code mirror on Cloudflare Pages. All repos