UNPKG

shield-bridge-sdk

Version:
112 lines 5.97 kB
/** * Incremental sapling-diff cache (the FETCH layer). * * octez.js's SaplingTransactionViewer re-fetches a pool's ENTIRE `single_sapling_get_diff` * on every balance/transaction read (O(pool size), no offset, no cache). A pool's diff only * ever grows (commitment/nullifier trees are append-only), so this cost climbs without bound * and is paid on every scan, for every account, for every asset viewed. * * This module makes that fetch incremental WITHOUT touching the audited decrypt/spend logic: * a caching read-provider wraps the real RPC adapter and reconstructs the head diff from a * persisted FINALIZED prefix plus a freshly-fetched UNCONFIRMED tail. The viewer still runs * its normal `getBalance()` over the (identical) reconstructed diff, so there is zero risk of * a wrong balance — the only thing that changes is how many bytes cross the wire. * * today: get_diff(head) → 525 KB every scan (XTZ pool, and growing) * incremental: get_diff(head~2, offset=cached) → ~125 B when nothing finalized since last * + get_diff(head, offset=finalized) → only the unconfirmed tail (≤2 blocks) * * Cached data is PUBLIC pool state (encrypted commitments + nullifiers) keyed by * `(rpc host, set contract / sapling id)` — account-agnostic, so it is shared across every * shielded account on the device and carries no decrypted/private material. * * Reorg safety: Tezos (Tenderbake) finalizes a block after 2 confirmations, so the diff at * `head~2` is immutable — safe to persist. The unconfirmed tail (`head~2`..`head`) is fetched * fresh every scan and NEVER persisted, so a reorg of a recent block self-heals on the next * scan, and an account's own just-submitted note still shows immediately (it's in the tail). * * Foolproof by construction: any error (offset unsupported, short chain, store failure, …) * falls back to the wrapped adapter's full fetch — never a wrong or missing result. */ /** The shape octez.js's get_diff RPC returns (snake_case, passed through verbatim). */ export interface SaplingDiffResponse { root: string; commitments_and_ciphertexts: unknown[]; nullifiers: unknown[]; } /** Persisted FINALIZED state for one pool. Public data only. */ export interface CachedSaplingDiff { /** count of finalized commitments cached (the next offset_commitment to fetch from) */ offC: number; /** count of finalized nullifiers cached (the next offset_nullifier to fetch from) */ offN: number; commitments: unknown[]; nullifiers: unknown[]; } /** * Pluggable persistent store for the diff cache. The browser uses an IndexedDB-backed * implementation automatically; Node/Lambda/tests can inject {@link MemoryDiffStore} or a * custom store. Methods may be sync or async. */ export interface SaplingDiffStore { get(key: string): Promise<unknown> | unknown; set(key: string, value: unknown): Promise<void> | void; delete(key: string): Promise<void> | void; /** Optional: delete every key with the given prefix (used to evict an account's balance cache). */ deleteByPrefix?(prefix: string): Promise<void> | void; } /** In-memory store (process lifetime). For Node/Lambda/tests; browsers should use IndexedDB. */ export declare class MemoryDiffStore implements SaplingDiffStore { private readonly map; get(key: string): unknown; set(key: string, value: unknown): void; delete(key: string): void; deleteByPrefix(prefix: string): void; } /** IndexedDB-backed store — works on the main thread AND inside Web Workers (same origin DB). */ export declare class IndexedDbDiffStore implements SaplingDiffStore { private readonly dbName; private readonly storeName; private dbPromise; private open; get(key: string): Promise<unknown>; set(key: string, value: unknown): Promise<void>; delete(key: string): Promise<void>; deleteByPrefix(prefix: string): Promise<void>; } /** The default store for the current runtime: IndexedDB if available (browser / web worker), else none. */ export declare function createDefaultDiffStore(): SaplingDiffStore | null; export type DiffTarget = { kind: 'contract' | 'id'; id: string; }; /** * A pool's diff at head, split into the immutable FINALIZED prefix (head~2, persisted) and the * UNCONFIRMED tail (head~2..head, never persisted). `finalized*` are the FULL finalized arrays * (indices 0..finalizedCount); the tail arrays continue from there. Positions are absolute and * stable (commitment/nullifier trees are append-only), which is what lets a decrypted-note * cache key off the position. */ export interface PoolDiffSplit { root: string; finalizedCommitments: unknown[]; finalizedNullifiers: unknown[]; tailCommitments: unknown[]; tailNullifiers: unknown[]; } /** * Sync a pool's diff incrementally: extend the persisted finalized prefix by its offset, then * fetch the fresh unconfirmed tail. Persists ONLY the finalized prefix. The finalized fetch * transfers ~nothing in steady state; the tail is ≤2 blocks. This is the shared core used both * by the v1 read-provider (which merges the two) and the v2 balance cache (which needs them * separately so it can persist decrypted notes for the finalized prefix only). */ export declare function syncPoolDiff(store: SaplingDiffStore, rpcUrl: string, target: DiffTarget): Promise<PoolDiffSplit>; /** * Wrap an octez.js read provider so the viewer's `get_diff` calls at `head` are served * incrementally. Every other provider method, and any non-`head` block read, delegates to the * real adapter unchanged. Any failure in the incremental path also delegates (full fetch), so * the wrapper can only ever make reads cheaper — never wrong, never failed. */ export declare function makeCachingReadProvider<T extends object>(adapter: T, rpcUrl: string, store: SaplingDiffStore): T; //# sourceMappingURL=saplingDiffCache.d.ts.map