UNPKG

@frak-labs/core-sdk

Version:

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

242 lines (222 loc) 8.2 kB
import type { Address } from "viem"; import { describe, expect, it } from "../../tests/vitest-fixtures"; import type { FrakContextV2 } from "../types"; import { base64urlEncode } from "../utils/compression/b64"; import { decodeFrakContextV2, encodeFrakContextV2, isV2BinaryLength, } from "./frakContextV2Codec"; const MERCHANT = "550e8400-e29b-41d4-a716-446655440000"; const CLIENT = "550e8400-e29b-41d4-a716-446655440001"; const WALLET = "0x1234567890123456789012345678901234567890" as Address; describe("frakContextV2Codec", () => { describe("encodeFrakContextV2 / decodeFrakContextV2 round-trip", () => { it("round-trips a context with clientId only (37 bytes)", () => { const ctx: FrakContextV2 = { v: 2, m: MERCHANT, t: 1709654400, c: CLIENT, }; const encoded = encodeFrakContextV2(ctx); expect(encoded).toBeInstanceOf(Uint8Array); expect(encoded?.length).toBe(37); expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx); }); it("round-trips a context with wallet only (41 bytes)", () => { const ctx: FrakContextV2 = { v: 2, m: MERCHANT, t: 1709654400, w: WALLET, }; const encoded = encodeFrakContextV2(ctx); expect(encoded?.length).toBe(41); expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx); }); it("round-trips a context with clientId + wallet (57 bytes)", () => { const ctx: FrakContextV2 = { v: 2, m: MERCHANT, t: 1709654400, c: CLIENT, w: WALLET, }; const encoded = encodeFrakContextV2(ctx); expect(encoded?.length).toBe(57); expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx); }); it("produces a base64url string shorter than the legacy JSON format", () => { // Legacy reference: a typical anonymous context is ~115 JSON bytes // \u2192 ~154 base64url chars. Wallet variant is ~165 \u2192 ~220 chars. const ctxBoth: FrakContextV2 = { v: 2, m: MERCHANT, t: 1709654400, c: CLIENT, w: WALLET, }; const encoded = base64urlEncode( encodeFrakContextV2(ctxBoth) as Uint8Array ); // 57 bytes encodes to 76 chars (no padding). expect(encoded.length).toBe(76); // Sanity: far below the legacy ~220-char payload. expect(encoded.length).toBeLessThan(100); }); it("preserves UUID case insensitivity on decode", () => { const ctx: FrakContextV2 = { v: 2, m: MERCHANT.toUpperCase(), t: 1, c: CLIENT, }; const encoded = encodeFrakContextV2(ctx); const decoded = decodeFrakContextV2(encoded as Uint8Array); // Decoded UUIDs are lower-case canonical. expect(decoded?.m).toBe(MERCHANT); }); it("preserves timestamp at the uint32 boundary", () => { const ctx: FrakContextV2 = { v: 2, m: MERCHANT, t: 0xff_ff_ff_ff, c: CLIENT, }; const decoded = decodeFrakContextV2( encodeFrakContextV2(ctx) as Uint8Array ); expect(decoded?.t).toBe(0xff_ff_ff_ff); }); }); describe("encodeFrakContextV2 validation", () => { it("rejects non-UUID merchant id", () => { expect( encodeFrakContextV2({ v: 2, m: "not-a-uuid", t: 1, c: CLIENT, }) ).toBeNull(); }); it("rejects non-UUID client id", () => { expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, c: "not-a-uuid", }) ).toBeNull(); }); it("rejects malformed wallet address", () => { expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, w: "0xnot-a-wallet" as Address, }) ).toBeNull(); }); it("rejects contexts missing both c and w", () => { expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, } as FrakContextV2) ).toBeNull(); }); it("rejects timestamps outside uint32 range", () => { expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: -1, c: CLIENT, }) ).toBeNull(); expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: 0x1_00_00_00_00, c: CLIENT, }) ).toBeNull(); expect( encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1.5, c: CLIENT, }) ).toBeNull(); }); }); describe("decodeFrakContextV2 validation", () => { it("returns null on wrong version nibble", () => { const encoded = encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, c: CLIENT, }) as Uint8Array; const tampered = new Uint8Array(encoded); tampered[0] = (tampered[0] & 0xf0) | 0x03; // flip version to 3 expect(decodeFrakContextV2(tampered)).toBeNull(); }); it("returns null when reserved header bits are set", () => { const encoded = encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, c: CLIENT, }) as Uint8Array; const tampered = new Uint8Array(encoded); tampered[0] |= 0x80; expect(decodeFrakContextV2(tampered)).toBeNull(); }); it("returns null when neither flag is set", () => { const encoded = encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, c: CLIENT, }) as Uint8Array; const tampered = new Uint8Array(encoded); tampered[0] &= 0x0f; // clear flags, keep version expect(decodeFrakContextV2(tampered)).toBeNull(); }); it("returns null when byte length disagrees with flags", () => { const encoded = encodeFrakContextV2({ v: 2, m: MERCHANT, t: 1, c: CLIENT, }) as Uint8Array; // Drop the trailing byte to break the expected length. const truncated = encoded.subarray(0, encoded.length - 1); expect(decodeFrakContextV2(truncated)).toBeNull(); }); it("returns null on an empty buffer", () => { expect(decodeFrakContextV2(new Uint8Array(0))).toBeNull(); }); }); describe("isV2BinaryLength", () => { it("matches exactly the three valid V2 sizes", () => { expect(isV2BinaryLength(37)).toBe(true); expect(isV2BinaryLength(41)).toBe(true); expect(isV2BinaryLength(57)).toBe(true); }); it("rejects V1 size and everything else", () => { expect(isV2BinaryLength(20)).toBe(false); expect(isV2BinaryLength(0)).toBe(false); expect(isV2BinaryLength(36)).toBe(false); expect(isV2BinaryLength(58)).toBe(false); }); }); });