UNPKG

@frak-labs/core-sdk

Version:

Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.

198 lines (169 loc) 6.22 kB
/** * Binary codec for {@link FrakContextV2}. * * Produces a compact, URL-safe byte layout (~65% smaller than the previous * JSON+base64url format). See the layout below. * * ## Wire layout * * ```text * byte 0: header * bits 0-3 version (= 2) * bit 4 has_c flag * bit 5 has_w flag * bits 6-7 reserved (must be 0) * bytes 1..16: merchant UUID (16 bytes, mandatory) * bytes 17..20: timestamp (uint32 big-endian, Unix seconds) * bytes 21..36: client UUID (16 bytes, only when has_c is set) * bytes 37..56: wallet address (20 bytes, only when has_w is set) * ``` * * Size variants (before base64url): * - has_c only: 37 bytes * - has_w only: 41 bytes * - has_c + has_w: 57 bytes * * V1 payloads are exactly 20 bytes (raw wallet address); the byte lengths never * overlap, so the outer decoder can disambiguate purely on length. * * @ignore */ import type { Address } from "viem"; import type { FrakContextV2 } from "../types"; import { addressToBytes, bytesToAddress, isAddress } from "./address"; const VERSION_V2 = 0x02; const VERSION_MASK = 0x0f; const FLAG_HAS_C = 1 << 4; const FLAG_HAS_W = 1 << 5; const RESERVED_MASK = 0xc0; const UUID_BYTES = 16; const TIMESTAMP_BYTES = 4; const ADDRESS_BYTES = 20; const HEADER_BYTES = 1; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** Strict lower-case UUID validation (RFC 4122 shape, any version/variant). */ function isUuid(value: unknown): value is string { return typeof value === "string" && UUID_RE.test(value); } /** Parse a canonical UUID string into 16 raw bytes. */ function uuidToBytes(uuid: string): Uint8Array { const hex = uuid.replace(/-/g, ""); const out = new Uint8Array(UUID_BYTES); for (let i = 0; i < UUID_BYTES; i++) { out[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16); } return out; } /** Format 16 raw bytes as a canonical 8-4-4-4-12 UUID string. */ function bytesToUuid(bytes: Uint8Array): string { let hex = ""; for (let i = 0; i < UUID_BYTES; i++) { hex += bytes[i].toString(16).padStart(2, "0"); } return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; } /** * Encode a {@link FrakContextV2} into its binary wire format. * * Returns `null` when the context fails runtime validation (missing fields, * malformed UUIDs, timestamp outside uint32 range, invalid wallet). */ export function encodeFrakContextV2(ctx: FrakContextV2): Uint8Array | null { if (!isUuid(ctx.m)) return null; if (!Number.isInteger(ctx.t) || ctx.t < 0 || ctx.t > 0xff_ff_ff_ff) return null; const hasC = typeof ctx.c === "string" && ctx.c.length > 0; const hasW = typeof ctx.w === "string" && isAddress(ctx.w); if (!hasC && !hasW) return null; if (hasC && !isUuid(ctx.c)) return null; const size = HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + (hasC ? UUID_BYTES : 0) + (hasW ? ADDRESS_BYTES : 0); const buf = new Uint8Array(size); const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); let offset = 0; buf[offset++] = VERSION_V2 | (hasC ? FLAG_HAS_C : 0) | (hasW ? FLAG_HAS_W : 0); buf.set(uuidToBytes(ctx.m), offset); offset += UUID_BYTES; view.setUint32(offset, ctx.t, false); offset += TIMESTAMP_BYTES; if (hasC) { buf.set(uuidToBytes(ctx.c as string), offset); offset += UUID_BYTES; } if (hasW) { buf.set(addressToBytes(ctx.w as Address), offset); offset += ADDRESS_BYTES; } return buf; } /** * Decode a binary {@link FrakContextV2} payload. * * Returns `null` when: * - the header version nibble is not V2 * - reserved header bits are set (guards against future-version payloads) * - neither flag is set (invalid: V2 must carry `c` and/or `w`) * - the byte length does not match the length implied by the header flags * - the decoded wallet does not pass `isAddress` (defense-in-depth against * crafted payloads that round-trip by length but carry junk) */ export function decodeFrakContextV2(buf: Uint8Array): FrakContextV2 | null { if (buf.length < HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES) return null; const header = buf[0]; if ((header & VERSION_MASK) !== VERSION_V2) return null; if ((header & RESERVED_MASK) !== 0) return null; const hasC = (header & FLAG_HAS_C) !== 0; const hasW = (header & FLAG_HAS_W) !== 0; if (!hasC && !hasW) return null; const expected = HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + (hasC ? UUID_BYTES : 0) + (hasW ? ADDRESS_BYTES : 0); if (buf.length !== expected) return null; let offset = HEADER_BYTES; const m = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES)); offset += UUID_BYTES; const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); const t = view.getUint32(offset, false); offset += TIMESTAMP_BYTES; const out: FrakContextV2 = { v: 2, m, t }; if (hasC) { out.c = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES)); offset += UUID_BYTES; } if (hasW) { const walletHex = bytesToAddress( buf.subarray(offset, offset + ADDRESS_BYTES) ); if (!isAddress(walletHex)) return null; out.w = walletHex; offset += ADDRESS_BYTES; } return out; } /** * Quick length-based probe to tell V1 (20-byte wallet address) apart from a V2 * binary payload. Exposed so the outer decoder can branch without re-parsing. */ export function isV2BinaryLength(byteLength: number): boolean { return ( byteLength === HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + UUID_BYTES || byteLength === HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + ADDRESS_BYTES || byteLength === HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + UUID_BYTES + ADDRESS_BYTES ); }