@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
text/typescript
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);
});
});
});