UNPKG

@frak-labs/core-sdk

Version:

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

235 lines (212 loc) 7.21 kB
import type { AttributionParams, FrakContext, FrakContextV1, FrakContextV2, } from "../types"; import { isV2Context } from "../types"; import { base64urlDecode, base64urlEncode } from "../utils/compression/b64"; import { addressToBytes, bytesToAddress, isAddress } from "./address"; import { decodeFrakContextV2, encodeFrakContextV2 } from "./frakContextV2Codec"; /** * URL parameter key for the Frak referral context */ const contextKey = "fCtx"; /** * Compress a Frak context into a URL-safe string. * * - V2 contexts are encoded using a compact binary layout (see * {@link encodeFrakContextV2}) then base64url-encoded. * - V1 contexts encode the wallet address as raw bytes (base64url). * * @param context - The context to compress (V1 or V2) * @returns A compressed base64url string, or undefined on failure */ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined { if (!context) return; try { if (isV2Context(context)) { const encoded = encodeFrakContextV2(context); if (!encoded) return undefined; return base64urlEncode(encoded); } // V1 legacy: compress wallet address as raw bytes const bytes = addressToBytes(context.r); return base64urlEncode(bytes); } catch (e) { console.error("Error compressing Frak context", { e, context }); } return undefined; } /** * Decompress a base64url string back into a Frak context. * * V1 (exactly 20 bytes) and V2 (37, 41, or 57 bytes) are distinguished by * their decoded byte length, so there is no ambiguity. * * @param context - The compressed context string * @returns The decompressed FrakContext, or undefined on failure */ function decompress(context?: string): FrakContext | undefined { if (!context || context.length === 0) return; try { const bytes = base64urlDecode(context); // V1 is a raw 20-byte wallet address; V2 binary is always longer // and starts with a header byte whose low nibble is the version. if (bytes.length !== 20) { const v2 = decodeFrakContextV2(bytes); if (v2) return v2; return undefined; } const hex = bytesToAddress(bytes); if (isAddress(hex)) { return { r: hex }; } } catch (e) { console.error("Error decompressing Frak context", { e, context }); } return undefined; } /** * Parse a URL to extract the Frak referral context from the `fCtx` query parameter. * * @param args * @param args.url - The URL to parse * @returns The parsed FrakContext, or null if absent */ function parse({ url }: { url: string }): FrakContext | null | undefined { if (!url) return null; const urlObj = new URL(url); const frakContext = urlObj.searchParams.get(contextKey); if (!frakContext) return null; return decompress(frakContext); } /** * Default utm_source / via value when attribution is requested. */ const DEFAULT_ATTRIBUTION_SOURCE = "frak"; /** * Resolve attribution defaults from the provided context. * * V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId * (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries * a wallet (`w`), `ref` is intentionally left unset — we don't want wallet * addresses leaking into UTM params. V1 contexts have no equivalent. */ function resolveAttributionValues( overrides: AttributionParams ): Record<string, string | undefined> { return { utm_source: overrides.utmSource ?? DEFAULT_ATTRIBUTION_SOURCE, utm_medium: overrides.utmMedium ?? undefined, utm_campaign: overrides.utmCampaign ?? undefined, utm_content: overrides.utmContent, utm_term: overrides.utmTerm, via: overrides.via ?? undefined, ref: overrides.ref ?? undefined, }; } /** * Append attribution query params to a URL using gap-fill semantics. * * Existing params on the URL are preserved untouched (so merchant-provided * UTMs take precedence). Only missing keys are populated. */ function applyAttributionParams( urlObj: URL, attribution?: AttributionParams ): void { const values = resolveAttributionValues(attribution ?? {}); for (const [key, value] of Object.entries(values)) { if (value === undefined || value === "") continue; if (urlObj.searchParams.has(key)) continue; urlObj.searchParams.set(key, value); } } /** * Add or replace the `fCtx` query parameter in a URL with the given context. * * Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`, * `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing * params on the URL are preserved, defaults are derived from the context when * applicable, and `attribution` overrides take precedence when provided. * * @param args * @param args.url - The URL to update * @param args.context - The context to embed (V1 or V2) * @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted. * @returns The updated URL string, or null on failure */ function update({ url, context, attribution, }: { url?: string; context: FrakContextV1 | FrakContextV2; attribution?: AttributionParams; }): string | null { if (!url) return null; const compressedContext = compress(context); if (!compressedContext) return null; const urlObj = new URL(url); urlObj.searchParams.set(contextKey, compressedContext); applyAttributionParams(urlObj, attribution); return urlObj.toString(); } /** * Remove the `fCtx` query parameter from a URL. * * @param url - The URL to strip the context from * @returns The cleaned URL string */ function remove(url: string): string { const urlObj = new URL(url); urlObj.searchParams.delete(contextKey); return urlObj.toString(); } /** * Replace the current browser URL with an updated Frak context. * * - If `context` is non-null, embeds it via {@link update}. * - If `context` is null, strips the context via {@link remove}. * * @param args * @param args.url - Base URL (defaults to `window.location.href`) * @param args.context - Context to set, or null to remove */ function replaceUrl({ url: baseUrl, context, }: { url?: string; context: FrakContextV1 | FrakContextV2 | null; }) { if (!window.location?.href || typeof window === "undefined") { console.error("No window found, can't update context"); return; } const url = baseUrl ?? window.location.href; let newUrl: string | null; if (context !== null) { newUrl = update({ url, context }); } else { newUrl = remove(url); } if (!newUrl) return; window.history.replaceState(null, "", newUrl.toString()); } /** * Manager for Frak referral context in URLs. * * Handles compression, decompression, URL parsing, and browser history updates * for both V1 (wallet address) and V2 (anonymous clientId) referral contexts. */ export const FrakContextManager = { compress, decompress, parse, update, remove, replaceUrl, };