const TE = new TextEncoder(); export const ANS104_ED25519_SIGNATURE_TYPE = 2; function concatBytes(...chunks) { const len = chunks.reduce((n, c) => n + c.length, 0); const out = new Uint8Array(len); let p = 0; for (const c of chunks) { out.set(c, p); p += c.length; } return out; } function uint16LE(value) { const out = new Uint8Array(2); new DataView(out.buffer).setUint16(0, Number(value), true); return out; } function uint64LE(value) { let v = BigInt(value); if (v < 0n) throw new Error('uint64LE: negative value'); const out = new Uint8Array(8); for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; } function encodeAvroLong(value) { let n = BigInt(value); // Avro zig-zag signed long. let u = n >= 0n ? (n << 1n) : ((-n << 1n) - 1n); const bytes = []; do { let b = Number(u & 0x7fn); u >>= 7n; if (u !== 0n) b |= 0x80; bytes.push(b); } while (u !== 0n); return Uint8Array.from(bytes); } function encodeAvroString(value) { const bytes = TE.encode(String(value)); return concatBytes(encodeAvroLong(bytes.length), bytes); } /** Serialize ANS-104 tags as the Avro array used by arbundles/Turbo. */ export function serializeAns104Tags(tags = []) { const normalized = tags.map(({ name, value }) => ({ name: String(name ?? ''), value: String(value ?? ''), })); if (normalized.length > 128) throw new Error('ANS-104 supports at most 128 tags'); for (const tag of normalized) { if (!tag.name || !tag.value) throw new Error('ANS-104 tag name/value must be non-empty'); if (TE.encode(tag.name).length > 1024) throw new Error('ANS-104 tag name exceeds 1024 bytes'); if (TE.encode(tag.value).length > 3072) throw new Error('ANS-104 tag value exceeds 3072 bytes'); } if (normalized.length === 0) return Uint8Array.of(0); return concatBytes( encodeAvroLong(normalized.length), ...normalized.flatMap((tag) => [encodeAvroString(tag.name), encodeAvroString(tag.value)]), Uint8Array.of(0), ); } async function digest(name, bytes) { return new Uint8Array(await crypto.subtle.digest(name, bytes)); } async function deepHashBlob(bytes) { const tag = TE.encode(`blob${bytes.length}`); return digest('SHA-384', concatBytes(await digest('SHA-384', tag), await digest('SHA-384', bytes))); } async function deepHash(item) { if (item instanceof Uint8Array) return deepHashBlob(item); if (Array.isArray(item)) { let acc = await digest('SHA-384', TE.encode(`list${item.length}`)); for (const child of item) { acc = await digest('SHA-384', concatBytes(acc, await deepHash(child))); } return acc; } throw new Error('Unsupported ANS-104 deepHash item'); } /** * Build the exact signing message used by current arbundles/Turbo DataItems. * SHiNE intentionally uses no target and no anchor. */ export async function ans104SigningMessage({ owner32, tags = [], data }) { if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes'); if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array'); const rawTags = serializeAns104Tags(tags); return deepHash([ TE.encode('dataitem'), TE.encode('1'), TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)), owner32, new Uint8Array(0), new Uint8Array(0), rawTags, data, ]); } /** Create one complete serialized ANS-104 DataItem signed with Ed25519. */ export async function createAns104DataItem({ owner32, privateKey, data, tags = [] }) { if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array'); const rawTags = serializeAns104Tags(tags); const signingMessage = await ans104SigningMessage({ owner32, tags, data }); const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage)); if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`); return concatBytes( uint16LE(ANS104_ED25519_SIGNATURE_TYPE), signature, owner32, Uint8Array.of(0), // target absent Uint8Array.of(0), // anchor absent uint64LE(tags.length), uint64LE(rawTags.length), rawTags, data, ); }