Files
SHiNE-server/shine-UI/js/services/ans104-data-item.js
T

126 lines
4.1 KiB
JavaScript

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,
);
}