@unicitylabs/sphere-sdk 0.9.1-dev.10 → 0.9.1-dev.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1890,6 +1890,26 @@ type WalletApiPaymentRequestsPage = {
1890
1890
  cursor: string | null;
1891
1891
  syncEpoch: bigint;
1892
1892
  };
1893
+ /**
1894
+ * One §16 history wire record (§10 — the server never writes history rows).
1895
+ * `memo` / `counterpartyNametag` are S6 `enc1.` envelopes, verbatim (§8.3) —
1896
+ * encrypted on POST, returned encrypted on GET, decrypted by the owner only.
1897
+ */
1898
+ interface WalletApiHistoryRecord {
1899
+ dedupKey: string;
1900
+ id: string;
1901
+ type: string;
1902
+ ts: string;
1903
+ assets: {
1904
+ coinId: string;
1905
+ amount: string;
1906
+ }[];
1907
+ transferId?: string;
1908
+ tokenId?: string;
1909
+ counterpartyPubkey?: string;
1910
+ memo?: string;
1911
+ counterpartyNametag?: string;
1912
+ }
1893
1913
  /**
1894
1914
  * The narrow, STRUCTURAL slice of the wallet-api client this module needs:
1895
1915
  * the E.3 intent lifecycle, blob uploads for spend outputs, and the §10
@@ -1924,21 +1944,24 @@ interface PaymentsWalletApiPort {
1924
1944
  /** Upload to a presigned PUT (a 412 = already present = success — §5.2). */
1925
1945
  uploadBlob(putUrl: string, bytes: Uint8Array): Promise<void>;
1926
1946
  /** §10: client-asserted history records (§16 wire shape), deduped by dedupKey server-side. */
1927
- postHistoryRecords(records: {
1928
- dedupKey: string;
1929
- id: string;
1930
- type: string;
1931
- ts: string;
1932
- assets: {
1933
- coinId: string;
1934
- amount: string;
1935
- }[];
1936
- transferId?: string;
1937
- tokenId?: string;
1938
- counterpartyPubkey?: string;
1939
- memo?: string;
1940
- counterpartyNametag?: string;
1941
- }[]): Promise<void>;
1947
+ postHistoryRecords(records: WalletApiHistoryRecord[]): Promise<void>;
1948
+ /**
1949
+ * §10/§16: read the client-written history log back — newest-first keyset
1950
+ * pages (`{records, more, cursor, syncEpoch}`). The READ side of the §10 log:
1951
+ * a reloaded thin wallet rebuilds its history from here (the in-memory cache
1952
+ * is process-lifetime; the durable log lives on the server). `memo` /
1953
+ * `counterpartyNametag` come back as the verbatim S6 `enc1.` envelopes — the
1954
+ * owner decrypts them with its own field key on display (§8.3).
1955
+ */
1956
+ listHistory(options?: {
1957
+ before?: string;
1958
+ limit?: number;
1959
+ }): Promise<{
1960
+ records: WalletApiHistoryRecord[];
1961
+ more: boolean;
1962
+ cursor: string | null;
1963
+ syncEpoch: bigint;
1964
+ }>;
1942
1965
  /** Network name — scopes the persisted payment-request cursor (mirrors the mailbox cursor). */
1943
1966
  readonly network?: string;
1944
1967
  /** `POST /v1/payment-requests` (§16) — `memo` MUST already be an S6 envelope (§8.3). */
@@ -2538,10 +2561,39 @@ declare class PaymentsModule {
2538
2561
  */
2539
2562
  addToHistory(entry: Omit<TransactionHistoryEntry, 'id' | 'dedupKey'>): Promise<void>;
2540
2563
  /**
2541
- * Load history from the local token storage provider into the in-memory cache.
2542
- * Also performs one-time migration from legacy KV storage.
2564
+ * Load history into the in-memory cache.
2565
+ *
2566
+ * In the wallet-api composition (the `walletApi` client is present) the
2567
+ * durable §10 history log lives on the SERVER — the thin storage provider
2568
+ * keeps none — so the cache is rebuilt from `walletApi.listHistory()`. The
2569
+ * twin of the #521 inventory reload bug: `_historyCache` is process-lifetime,
2570
+ * so a reload (tab refresh) must re-pull it or render an empty history.
2571
+ * Compositions WITHOUT `walletApi` keep the legacy local path below.
2543
2572
  */
2544
2573
  loadHistory(): Promise<void>;
2574
+ /**
2575
+ * Rebuild `_historyCache` from the server's §10 history log (the wallet-api
2576
+ * composition). Pages newest-first via the keyset cursor until `more:false`
2577
+ * or the page cap; dedups by `dedupKey` (a hydrate-then-receive in the same
2578
+ * session must not double-list). The S6 `memo` / `counterpartyNametag`
2579
+ * envelopes are decrypted with the owner's own field key on the way in.
2580
+ *
2581
+ * Best-effort, like the §10 history POST: history is untrusted DISPLAY data,
2582
+ * so a backend outage during hydration must NEVER fail `load()` (the money
2583
+ * path) — the in-session cache is left intact and the pull retries next load.
2584
+ */
2585
+ private hydrateHistoryFromServer;
2586
+ /**
2587
+ * Map one §16 history wire record onto the display
2588
+ * {@link TransactionHistoryEntry}. `counterpartyNametag` lands on the role-
2589
+ * appropriate field (sender for RECEIVED, recipient otherwise); the S6 memo +
2590
+ * nametag envelopes decrypt under THIS wallet's field key (self-scoped at
2591
+ * rest — §8.3), surfaced as absent if they don't decrypt rather than as
2592
+ * ciphertext (same rule as mailbox/payment-request memos).
2593
+ */
2594
+ private historyEntryFromWire;
2595
+ /** S6 field decrypt that surfaces an undecryptable envelope as absent (§8.3). */
2596
+ private tryDecryptField;
2545
2597
  /**
2546
2598
  * Import history entries from remote TXF data into local store.
2547
2599
  * Delegates to the local TokenStorageProvider's importHistoryEntries() for
package/dist/index.d.ts CHANGED
@@ -1890,6 +1890,26 @@ type WalletApiPaymentRequestsPage = {
1890
1890
  cursor: string | null;
1891
1891
  syncEpoch: bigint;
1892
1892
  };
1893
+ /**
1894
+ * One §16 history wire record (§10 — the server never writes history rows).
1895
+ * `memo` / `counterpartyNametag` are S6 `enc1.` envelopes, verbatim (§8.3) —
1896
+ * encrypted on POST, returned encrypted on GET, decrypted by the owner only.
1897
+ */
1898
+ interface WalletApiHistoryRecord {
1899
+ dedupKey: string;
1900
+ id: string;
1901
+ type: string;
1902
+ ts: string;
1903
+ assets: {
1904
+ coinId: string;
1905
+ amount: string;
1906
+ }[];
1907
+ transferId?: string;
1908
+ tokenId?: string;
1909
+ counterpartyPubkey?: string;
1910
+ memo?: string;
1911
+ counterpartyNametag?: string;
1912
+ }
1893
1913
  /**
1894
1914
  * The narrow, STRUCTURAL slice of the wallet-api client this module needs:
1895
1915
  * the E.3 intent lifecycle, blob uploads for spend outputs, and the §10
@@ -1924,21 +1944,24 @@ interface PaymentsWalletApiPort {
1924
1944
  /** Upload to a presigned PUT (a 412 = already present = success — §5.2). */
1925
1945
  uploadBlob(putUrl: string, bytes: Uint8Array): Promise<void>;
1926
1946
  /** §10: client-asserted history records (§16 wire shape), deduped by dedupKey server-side. */
1927
- postHistoryRecords(records: {
1928
- dedupKey: string;
1929
- id: string;
1930
- type: string;
1931
- ts: string;
1932
- assets: {
1933
- coinId: string;
1934
- amount: string;
1935
- }[];
1936
- transferId?: string;
1937
- tokenId?: string;
1938
- counterpartyPubkey?: string;
1939
- memo?: string;
1940
- counterpartyNametag?: string;
1941
- }[]): Promise<void>;
1947
+ postHistoryRecords(records: WalletApiHistoryRecord[]): Promise<void>;
1948
+ /**
1949
+ * §10/§16: read the client-written history log back — newest-first keyset
1950
+ * pages (`{records, more, cursor, syncEpoch}`). The READ side of the §10 log:
1951
+ * a reloaded thin wallet rebuilds its history from here (the in-memory cache
1952
+ * is process-lifetime; the durable log lives on the server). `memo` /
1953
+ * `counterpartyNametag` come back as the verbatim S6 `enc1.` envelopes — the
1954
+ * owner decrypts them with its own field key on display (§8.3).
1955
+ */
1956
+ listHistory(options?: {
1957
+ before?: string;
1958
+ limit?: number;
1959
+ }): Promise<{
1960
+ records: WalletApiHistoryRecord[];
1961
+ more: boolean;
1962
+ cursor: string | null;
1963
+ syncEpoch: bigint;
1964
+ }>;
1942
1965
  /** Network name — scopes the persisted payment-request cursor (mirrors the mailbox cursor). */
1943
1966
  readonly network?: string;
1944
1967
  /** `POST /v1/payment-requests` (§16) — `memo` MUST already be an S6 envelope (§8.3). */
@@ -2538,10 +2561,39 @@ declare class PaymentsModule {
2538
2561
  */
2539
2562
  addToHistory(entry: Omit<TransactionHistoryEntry, 'id' | 'dedupKey'>): Promise<void>;
2540
2563
  /**
2541
- * Load history from the local token storage provider into the in-memory cache.
2542
- * Also performs one-time migration from legacy KV storage.
2564
+ * Load history into the in-memory cache.
2565
+ *
2566
+ * In the wallet-api composition (the `walletApi` client is present) the
2567
+ * durable §10 history log lives on the SERVER — the thin storage provider
2568
+ * keeps none — so the cache is rebuilt from `walletApi.listHistory()`. The
2569
+ * twin of the #521 inventory reload bug: `_historyCache` is process-lifetime,
2570
+ * so a reload (tab refresh) must re-pull it or render an empty history.
2571
+ * Compositions WITHOUT `walletApi` keep the legacy local path below.
2543
2572
  */
2544
2573
  loadHistory(): Promise<void>;
2574
+ /**
2575
+ * Rebuild `_historyCache` from the server's §10 history log (the wallet-api
2576
+ * composition). Pages newest-first via the keyset cursor until `more:false`
2577
+ * or the page cap; dedups by `dedupKey` (a hydrate-then-receive in the same
2578
+ * session must not double-list). The S6 `memo` / `counterpartyNametag`
2579
+ * envelopes are decrypted with the owner's own field key on the way in.
2580
+ *
2581
+ * Best-effort, like the §10 history POST: history is untrusted DISPLAY data,
2582
+ * so a backend outage during hydration must NEVER fail `load()` (the money
2583
+ * path) — the in-session cache is left intact and the pull retries next load.
2584
+ */
2585
+ private hydrateHistoryFromServer;
2586
+ /**
2587
+ * Map one §16 history wire record onto the display
2588
+ * {@link TransactionHistoryEntry}. `counterpartyNametag` lands on the role-
2589
+ * appropriate field (sender for RECEIVED, recipient otherwise); the S6 memo +
2590
+ * nametag envelopes decrypt under THIS wallet's field key (self-scoped at
2591
+ * rest — §8.3), surfaced as absent if they don't decrypt rather than as
2592
+ * ciphertext (same rule as mailbox/payment-request memos).
2593
+ */
2594
+ private historyEntryFromWire;
2595
+ /** S6 field decrypt that surfaces an undecryptable envelope as absent (§8.3). */
2596
+ private tryDecryptField;
2545
2597
  /**
2546
2598
  * Import history entries from remote TXF data into local store.
2547
2599
  * Delegates to the local TokenStorageProvider's importHistoryEntries() for
package/dist/index.js CHANGED
@@ -9814,6 +9814,7 @@ function computeHistoryDedupKey(type, tokenId, transferId) {
9814
9814
  return `${type}_${crypto.randomUUID()}`;
9815
9815
  }
9816
9816
  var MAX_SYNCED_HISTORY_ENTRIES = 5e3;
9817
+ var MAX_HISTORY_HYDRATION_PAGES = 100;
9817
9818
  var SEND_ENGINE_OP_TIMEOUT_MS = 6e4;
9818
9819
  var DELIVERY_POLL_INTERVAL_MS = 3e4;
9819
9820
  function enrichWithRegistry(info) {
@@ -12103,10 +12104,20 @@ var PaymentsModule = class _PaymentsModule {
12103
12104
  this.deps.emitEvent("history:updated", historyEntry);
12104
12105
  }
12105
12106
  /**
12106
- * Load history from the local token storage provider into the in-memory cache.
12107
- * Also performs one-time migration from legacy KV storage.
12107
+ * Load history into the in-memory cache.
12108
+ *
12109
+ * In the wallet-api composition (the `walletApi` client is present) the
12110
+ * durable §10 history log lives on the SERVER — the thin storage provider
12111
+ * keeps none — so the cache is rebuilt from `walletApi.listHistory()`. The
12112
+ * twin of the #521 inventory reload bug: `_historyCache` is process-lifetime,
12113
+ * so a reload (tab refresh) must re-pull it or render an empty history.
12114
+ * Compositions WITHOUT `walletApi` keep the legacy local path below.
12108
12115
  */
12109
12116
  async loadHistory() {
12117
+ if (this.deps.walletApi?.listHistory) {
12118
+ await this.hydrateHistoryFromServer(this.deps.walletApi);
12119
+ return;
12120
+ }
12110
12121
  const provider = this.getLocalTokenStorageProvider();
12111
12122
  if (provider?.getHistoryEntries) {
12112
12123
  this._historyCache = await provider.getHistoryEntries();
@@ -12138,6 +12149,75 @@ var PaymentsModule = class _PaymentsModule {
12138
12149
  }
12139
12150
  }
12140
12151
  }
12152
+ /**
12153
+ * Rebuild `_historyCache` from the server's §10 history log (the wallet-api
12154
+ * composition). Pages newest-first via the keyset cursor until `more:false`
12155
+ * or the page cap; dedups by `dedupKey` (a hydrate-then-receive in the same
12156
+ * session must not double-list). The S6 `memo` / `counterpartyNametag`
12157
+ * envelopes are decrypted with the owner's own field key on the way in.
12158
+ *
12159
+ * Best-effort, like the §10 history POST: history is untrusted DISPLAY data,
12160
+ * so a backend outage during hydration must NEVER fail `load()` (the money
12161
+ * path) — the in-session cache is left intact and the pull retries next load.
12162
+ */
12163
+ async hydrateHistoryFromServer(api) {
12164
+ const byDedupKey = /* @__PURE__ */ new Map();
12165
+ try {
12166
+ let before;
12167
+ for (let page = 0; page < MAX_HISTORY_HYDRATION_PAGES; page++) {
12168
+ const result = await api.listHistory(before !== void 0 ? { before } : {});
12169
+ for (const wire of result.records) {
12170
+ if (!byDedupKey.has(wire.dedupKey)) {
12171
+ byDedupKey.set(wire.dedupKey, this.historyEntryFromWire(wire));
12172
+ }
12173
+ }
12174
+ if (!result.more || result.cursor === null) break;
12175
+ before = result.cursor;
12176
+ }
12177
+ } catch (err) {
12178
+ logger.warn("Payments", "history hydration from server failed (kept in-memory; retries next load):", err);
12179
+ return;
12180
+ }
12181
+ this._historyCache = [...byDedupKey.values()];
12182
+ }
12183
+ /**
12184
+ * Map one §16 history wire record onto the display
12185
+ * {@link TransactionHistoryEntry}. `counterpartyNametag` lands on the role-
12186
+ * appropriate field (sender for RECEIVED, recipient otherwise); the S6 memo +
12187
+ * nametag envelopes decrypt under THIS wallet's field key (self-scoped at
12188
+ * rest — §8.3), surfaced as absent if they don't decrypt rather than as
12189
+ * ciphertext (same rule as mailbox/payment-request memos).
12190
+ */
12191
+ historyEntryFromWire(wire) {
12192
+ const asset = wire.assets[0];
12193
+ const coinId = asset?.coinId ?? "";
12194
+ const received = wire.type === "RECEIVED";
12195
+ const memo = this.tryDecryptField(wire.memo);
12196
+ const nametag = this.tryDecryptField(wire.counterpartyNametag);
12197
+ return {
12198
+ id: wire.id,
12199
+ dedupKey: wire.dedupKey,
12200
+ type: wire.type,
12201
+ amount: asset?.amount ?? "0",
12202
+ coinId,
12203
+ symbol: this.getCoinSymbol(coinId),
12204
+ timestamp: Date.parse(wire.ts),
12205
+ ...wire.transferId !== void 0 ? { transferId: wire.transferId } : {},
12206
+ ...wire.tokenId !== void 0 ? { tokenId: wire.tokenId } : {},
12207
+ ...wire.counterpartyPubkey !== void 0 ? received ? { senderPubkey: wire.counterpartyPubkey } : { recipientPubkey: wire.counterpartyPubkey } : {},
12208
+ ...nametag !== void 0 ? received ? { senderNametag: nametag } : { recipientNametag: nametag } : {},
12209
+ ...memo !== void 0 ? { memo } : {}
12210
+ };
12211
+ }
12212
+ /** S6 field decrypt that surfaces an undecryptable envelope as absent (§8.3). */
12213
+ tryDecryptField(envelope) {
12214
+ if (envelope === void 0) return void 0;
12215
+ try {
12216
+ return decryptField(this.getFieldEncryptionKey(), envelope);
12217
+ } catch {
12218
+ return void 0;
12219
+ }
12220
+ }
12141
12221
  /**
12142
12222
  * Import history entries from remote TXF data into local store.
12143
12223
  * Delegates to the local TokenStorageProvider's importHistoryEntries() for