@nockchain/rose 0.1.4-nightly.5

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.
Files changed (205) hide show
  1. package/.github/workflows/artifacts.yml +33 -0
  2. package/.github/workflows/ci.yml +68 -0
  3. package/.github/workflows/publish-sdk.yml +35 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +8 -0
  7. package/LICENSE +22 -0
  8. package/README.md +117 -0
  9. package/extension/background/index.ts +1500 -0
  10. package/extension/content/index.ts +59 -0
  11. package/extension/icons/rose.svg +27 -0
  12. package/extension/icons/rose128.png +0 -0
  13. package/extension/icons/rose16.png +0 -0
  14. package/extension/icons/rose256.png +0 -0
  15. package/extension/icons/rose32.png +0 -0
  16. package/extension/icons/rose48.png +0 -0
  17. package/extension/icons/rose512.png +0 -0
  18. package/extension/inpage/index.ts +86 -0
  19. package/extension/manifest.json +48 -0
  20. package/extension/popup/Popup.tsx +94 -0
  21. package/extension/popup/Router.tsx +121 -0
  22. package/extension/popup/assets/arrow-down-icon.svg +3 -0
  23. package/extension/popup/assets/arrow-left-icon.svg +3 -0
  24. package/extension/popup/assets/arrow-right-icon.svg +3 -0
  25. package/extension/popup/assets/arrow-up-icon.svg +3 -0
  26. package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
  27. package/extension/popup/assets/checkmark-icon.svg +3 -0
  28. package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
  29. package/extension/popup/assets/checkmark-success-icon.svg +3 -0
  30. package/extension/popup/assets/clock-icon.svg +3 -0
  31. package/extension/popup/assets/close-x-icon.svg +3 -0
  32. package/extension/popup/assets/copy-icon.svg +6 -0
  33. package/extension/popup/assets/explorer-icon.svg +3 -0
  34. package/extension/popup/assets/eye-off-icon.svg +3 -0
  35. package/extension/popup/assets/eye-open-icon.svg +4 -0
  36. package/extension/popup/assets/feedback-icon.svg +3 -0
  37. package/extension/popup/assets/green-status-dot.svg +3 -0
  38. package/extension/popup/assets/info-icon.svg +3 -0
  39. package/extension/popup/assets/iris-logo-40.svg +27 -0
  40. package/extension/popup/assets/iris-logo-96.svg +27 -0
  41. package/extension/popup/assets/iris-logo-blue.svg +27 -0
  42. package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
  43. package/extension/popup/assets/iris-logo-orange.svg +27 -0
  44. package/extension/popup/assets/iris-logo.svg +27 -0
  45. package/extension/popup/assets/key-icon.svg +3 -0
  46. package/extension/popup/assets/lock-icon-yellow.svg +3 -0
  47. package/extension/popup/assets/lock-icon.svg +3 -0
  48. package/extension/popup/assets/pencil-edit-icon.svg +3 -0
  49. package/extension/popup/assets/permissions-icon.svg +3 -0
  50. package/extension/popup/assets/receipt-icon.svg +5 -0
  51. package/extension/popup/assets/refresh-icon.svg +3 -0
  52. package/extension/popup/assets/settings-gear-icon.svg +8 -0
  53. package/extension/popup/assets/settings-icon.svg +3 -0
  54. package/extension/popup/assets/theme-icon.svg +3 -0
  55. package/extension/popup/assets/trash-bin-icon.svg +3 -0
  56. package/extension/popup/assets/trend-down-arrow.svg +5 -0
  57. package/extension/popup/assets/trend-up-arrow.svg +5 -0
  58. package/extension/popup/assets/user-account-icon.svg +3 -0
  59. package/extension/popup/assets/vector-bottom-left.svg +9 -0
  60. package/extension/popup/assets/vector-left.svg +9 -0
  61. package/extension/popup/assets/vector-right.svg +9 -0
  62. package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
  63. package/extension/popup/assets/vector-top-right.svg +9 -0
  64. package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
  65. package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
  66. package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
  67. package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
  68. package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
  69. package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
  70. package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
  71. package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
  72. package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
  73. package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
  74. package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
  75. package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
  76. package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
  77. package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
  78. package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
  79. package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
  80. package/extension/popup/components/AccountIcon.tsx +78 -0
  81. package/extension/popup/components/AccountSelector.tsx +246 -0
  82. package/extension/popup/components/Alert.tsx +48 -0
  83. package/extension/popup/components/ConfirmModal.tsx +81 -0
  84. package/extension/popup/components/PasswordInput.tsx +49 -0
  85. package/extension/popup/components/ScreenContainer.tsx +17 -0
  86. package/extension/popup/components/SiteIcon.tsx +60 -0
  87. package/extension/popup/components/ThemeToggle.tsx +44 -0
  88. package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
  89. package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
  90. package/extension/popup/components/icons/CheckIcon.tsx +20 -0
  91. package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
  92. package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
  93. package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
  94. package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
  95. package/extension/popup/components/icons/CloseIcon.tsx +26 -0
  96. package/extension/popup/components/icons/CopyIcon.tsx +20 -0
  97. package/extension/popup/components/icons/EditIcon.tsx +20 -0
  98. package/extension/popup/components/icons/EyeIcon.tsx +13 -0
  99. package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
  100. package/extension/popup/components/icons/InfoIcon.tsx +20 -0
  101. package/extension/popup/components/icons/LockIcon.tsx +20 -0
  102. package/extension/popup/components/icons/PlusIcon.tsx +15 -0
  103. package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
  104. package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
  105. package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
  106. package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
  107. package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
  108. package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
  109. package/extension/popup/components/icons/UploadIcon.tsx +20 -0
  110. package/extension/popup/components/icons/WalletIcon.tsx +20 -0
  111. package/extension/popup/contexts/ThemeContext.tsx +105 -0
  112. package/extension/popup/hooks/useApprovalDetection.ts +128 -0
  113. package/extension/popup/hooks/useAutoFocus.ts +36 -0
  114. package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
  115. package/extension/popup/hooks/useClickOutside.ts +33 -0
  116. package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
  117. package/extension/popup/hooks/useFavicon.ts +64 -0
  118. package/extension/popup/hooks/useNumericInput.ts +93 -0
  119. package/extension/popup/index.html +13 -0
  120. package/extension/popup/index.tsx +24 -0
  121. package/extension/popup/screens/AboutScreen.tsx +118 -0
  122. package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
  123. package/extension/popup/screens/HomeScreen.tsx +902 -0
  124. package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
  125. package/extension/popup/screens/LockTimeScreen.tsx +155 -0
  126. package/extension/popup/screens/ReceiveScreen.tsx +149 -0
  127. package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
  128. package/extension/popup/screens/SendReviewScreen.tsx +308 -0
  129. package/extension/popup/screens/SendScreen.tsx +825 -0
  130. package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
  131. package/extension/popup/screens/SettingsScreen.tsx +116 -0
  132. package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
  133. package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
  134. package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
  135. package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
  136. package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
  137. package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
  138. package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
  139. package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
  140. package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
  141. package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
  142. package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
  143. package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
  144. package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
  145. package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
  146. package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
  147. package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
  148. package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
  149. package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
  150. package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
  151. package/extension/popup/screens/system/LockedScreen.tsx +288 -0
  152. package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
  153. package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
  154. package/extension/popup/store.ts +482 -0
  155. package/extension/popup/styles.css +246 -0
  156. package/extension/popup/utils/format.ts +58 -0
  157. package/extension/popup/utils/formatWalletError.ts +36 -0
  158. package/extension/popup/utils/memo.ts +299 -0
  159. package/extension/popup/utils/messaging.ts +16 -0
  160. package/extension/shared/address-encoding.ts +69 -0
  161. package/extension/shared/balance-query.ts +123 -0
  162. package/extension/shared/constants.ts +386 -0
  163. package/extension/shared/currency.ts +128 -0
  164. package/extension/shared/first-name-derivation.ts +128 -0
  165. package/extension/shared/keyfile.ts +58 -0
  166. package/extension/shared/onboarding.ts +78 -0
  167. package/extension/shared/price-api.ts +79 -0
  168. package/extension/shared/rpc-client-browser.ts +315 -0
  169. package/extension/shared/transaction-builder.ts +443 -0
  170. package/extension/shared/types.ts +450 -0
  171. package/extension/shared/utxo-diff.ts +212 -0
  172. package/extension/shared/utxo-store.ts +548 -0
  173. package/extension/shared/utxo-sync.ts +343 -0
  174. package/extension/shared/validators.ts +26 -0
  175. package/extension/shared/vault.ts +1580 -0
  176. package/extension/shared/wallet-crypto.ts +77 -0
  177. package/extension/shared/wasm-utils.ts +76 -0
  178. package/extension/shared/webcrypto.ts +67 -0
  179. package/extension/types/wasm.d.ts +13 -0
  180. package/package.json +39 -0
  181. package/postcss.config.js +6 -0
  182. package/rose-extension-dist.zip +0 -0
  183. package/sdk/README.md +88 -0
  184. package/sdk/examples/app.ts +166 -0
  185. package/sdk/examples/index.html +51 -0
  186. package/sdk/examples/tsconfig.json +15 -0
  187. package/sdk/examples/tx-builder.html +532 -0
  188. package/sdk/examples/tx-builder.ts +1766 -0
  189. package/sdk/package-lock.json +424 -0
  190. package/sdk/package.json +68 -0
  191. package/sdk/src/constants.ts +28 -0
  192. package/sdk/src/errors.ts +74 -0
  193. package/sdk/src/hooks/index.ts +1 -0
  194. package/sdk/src/hooks/use-rose.ts +94 -0
  195. package/sdk/src/index.ts +12 -0
  196. package/sdk/src/provider.ts +396 -0
  197. package/sdk/src/transaction.ts +163 -0
  198. package/sdk/src/types/rose-wasm.d.ts +14 -0
  199. package/sdk/src/types.ts +97 -0
  200. package/sdk/src/wasm.ts +13 -0
  201. package/sdk/tsconfig.json +20 -0
  202. package/sdk/vite.config.examples.ts +32 -0
  203. package/tailwind.config.ts +38 -0
  204. package/tsconfig.json +20 -0
  205. package/vite.config.ts +60 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * UTXO Store - Manages local UTXO state for successive transactions
3
+ *
4
+ * This module is the source of truth for spendable balance.
5
+ * It tracks note state (available, in_flight, spent) and
6
+ * provides thread-safe operations via per-account mutexes.
7
+ */
8
+
9
+ import { STORAGE_KEYS } from './constants';
10
+ import type {
11
+ StoredNote,
12
+ UTXOStore,
13
+ NoteState,
14
+ WalletTransaction,
15
+ WalletTxStore,
16
+ AccountSyncState,
17
+ SyncStateStore,
18
+ FetchedUTXO,
19
+ Note,
20
+ } from './types';
21
+ import { base58 } from '@scure/base';
22
+
23
+ function isRecord(x: unknown): x is Record<string, unknown> {
24
+ return typeof x === 'object' && x !== null;
25
+ }
26
+
27
+ // ============================================================================
28
+ // Per-Account Mutex - Prevents race conditions on rapid sends
29
+ // ============================================================================
30
+
31
+ const accountLocks = new Map<string, Promise<void>>();
32
+
33
+ /**
34
+ * Execute a function with exclusive access to an account's UTXO state
35
+ * Prevents race conditions when building multiple transactions rapidly
36
+ */
37
+ export async function withAccountLock<T>(accountAddress: string, fn: () => Promise<T>): Promise<T> {
38
+ const prev = accountLocks.get(accountAddress) ?? Promise.resolve();
39
+ let resolveNext: () => void;
40
+ const next = new Promise<void>(res => {
41
+ resolveNext = res;
42
+ });
43
+ accountLocks.set(
44
+ accountAddress,
45
+ prev.then(() => next)
46
+ );
47
+
48
+ await prev; // Wait for previous holder
49
+
50
+ try {
51
+ return await fn();
52
+ } finally {
53
+ resolveNext!();
54
+ }
55
+ }
56
+
57
+ // ============================================================================
58
+ // Note ID Generation
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Generate a unique note ID from name components
63
+ * Format: nameFirst:nameLast (both in base58)
64
+ */
65
+ export function generateNoteId(nameFirst: string, nameLast: string): string {
66
+ return `${nameFirst}:${nameLast}`;
67
+ }
68
+
69
+ /**
70
+ * Generate note ID from a Note (RPC response format)
71
+ */
72
+ export function noteIdFromNote(note: Note): string {
73
+ const first = note.nameFirstBase58 || base58.encode(note.nameFirst);
74
+ const last = note.nameLastBase58 || base58.encode(note.nameLast);
75
+ return generateNoteId(first, last);
76
+ }
77
+
78
+ /**
79
+ * Convert Uint8Array to base58 string
80
+ */
81
+ function uint8ArrayToBase58(bytes: Uint8Array): string {
82
+ return base58.encode(bytes);
83
+ }
84
+
85
+ // ============================================================================
86
+ // UTXO Store Operations
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Get the UTXO store from chrome storage
91
+ */
92
+ async function getUTXOStore(): Promise<UTXOStore> {
93
+ const result = (await chrome.storage.local.get([STORAGE_KEYS.UTXO_STORE])) as Record<
94
+ string,
95
+ unknown
96
+ >;
97
+ const raw = result[STORAGE_KEYS.UTXO_STORE];
98
+ return (isRecord(raw) ? raw : {}) as UTXOStore;
99
+ }
100
+
101
+ /**
102
+ * Save the UTXO store to chrome storage
103
+ */
104
+ async function saveUTXOStore(store: UTXOStore): Promise<void> {
105
+ await chrome.storage.local.set({
106
+ [STORAGE_KEYS.UTXO_STORE]: store,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Get all notes for an account
112
+ */
113
+ export async function getAccountNotes(accountAddress: string): Promise<StoredNote[]> {
114
+ const store = await getUTXOStore();
115
+ return store[accountAddress]?.notes || [];
116
+ }
117
+
118
+ /**
119
+ * Get only available (spendable) notes for an account
120
+ * This is the ONLY function that should be used when selecting inputs for transactions
121
+ */
122
+ export async function getAvailableNotes(accountAddress: string): Promise<StoredNote[]> {
123
+ const notes = await getAccountNotes(accountAddress);
124
+ return notes.filter(n => n.state === 'available');
125
+ }
126
+
127
+ /**
128
+ * Get spendable balance for an account (sum of available notes)
129
+ * This is the source of truth for what the user can spend
130
+ */
131
+ export async function getSpendableBalance(accountAddress: string): Promise<number> {
132
+ const available = await getAvailableNotes(accountAddress);
133
+ return available.reduce((sum, note) => sum + note.assets, 0);
134
+ }
135
+
136
+ /**
137
+ * Get pending outgoing balance (sum of in_flight notes)
138
+ */
139
+ export async function getPendingOutgoingBalance(accountAddress: string): Promise<number> {
140
+ const notes = await getAccountNotes(accountAddress);
141
+ return notes.filter(n => n.state === 'in_flight').reduce((sum, note) => sum + note.assets, 0);
142
+ }
143
+
144
+ /**
145
+ * Get total known balance (available + pending)
146
+ */
147
+ export async function getTotalKnownBalance(accountAddress: string): Promise<{
148
+ available: number;
149
+ pending: number;
150
+ total: number;
151
+ }> {
152
+ const notes = await getAccountNotes(accountAddress);
153
+ const available = notes
154
+ .filter(n => n.state === 'available')
155
+ .reduce((sum, n) => sum + n.assets, 0);
156
+ const pending = notes.filter(n => n.state === 'in_flight').reduce((sum, n) => sum + n.assets, 0);
157
+
158
+ return {
159
+ available,
160
+ pending,
161
+ total: available + pending,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Save/update notes for an account
167
+ * Merges with existing notes, updating state for known notes
168
+ */
169
+ export async function saveNotes(accountAddress: string, newNotes: StoredNote[]): Promise<void> {
170
+ const store = await getUTXOStore();
171
+
172
+ if (!store[accountAddress]) {
173
+ store[accountAddress] = { notes: [], version: 0 };
174
+ }
175
+
176
+ const existingNotes = store[accountAddress].notes;
177
+ const existingMap = new Map(existingNotes.map(n => [n.noteId, n]));
178
+
179
+ // Merge: new notes override existing ones
180
+ for (const note of newNotes) {
181
+ existingMap.set(note.noteId, note);
182
+ }
183
+
184
+ store[accountAddress].notes = Array.from(existingMap.values());
185
+ store[accountAddress].version += 1;
186
+
187
+ await saveUTXOStore(store);
188
+ }
189
+
190
+ /**
191
+ * Mark notes as in_flight (reserved for a pending transaction)
192
+ * Called at the START of transaction building to prevent double-spend
193
+ */
194
+ export async function markNotesInFlight(
195
+ accountAddress: string,
196
+ noteIds: string[],
197
+ walletTxId: string
198
+ ): Promise<void> {
199
+ const store = await getUTXOStore();
200
+
201
+ if (!store[accountAddress]) {
202
+ throw new Error(`No UTXO store for account ${accountAddress}`);
203
+ }
204
+
205
+ const noteIdSet = new Set(noteIds);
206
+ let lockedCount = 0;
207
+
208
+ for (const note of store[accountAddress].notes) {
209
+ if (noteIdSet.has(note.noteId)) {
210
+ if (note.state !== 'available') {
211
+ throw new Error(
212
+ `Cannot lock note ${note.noteId}: current state is ${note.state}, expected available`
213
+ );
214
+ }
215
+ note.state = 'in_flight';
216
+ note.pendingTxId = walletTxId;
217
+ lockedCount++;
218
+ }
219
+ }
220
+
221
+ if (lockedCount !== noteIds.length) {
222
+ throw new Error(`Failed to lock all notes: expected ${noteIds.length}, found ${lockedCount}`);
223
+ }
224
+
225
+ store[accountAddress].version += 1;
226
+ await saveUTXOStore(store);
227
+ }
228
+
229
+ /**
230
+ * Mark notes as spent (transaction confirmed)
231
+ * Called when sync detects the notes are no longer on-chain
232
+ */
233
+ export async function markNotesSpent(accountAddress: string, noteIds: string[]): Promise<void> {
234
+ const store = await getUTXOStore();
235
+
236
+ if (!store[accountAddress]) {
237
+ return; // Nothing to do
238
+ }
239
+
240
+ const noteIdSet = new Set(noteIds);
241
+
242
+ for (const note of store[accountAddress].notes) {
243
+ if (noteIdSet.has(note.noteId)) {
244
+ note.state = 'spent';
245
+ // Keep pendingTxId for reference
246
+ }
247
+ }
248
+
249
+ store[accountAddress].version += 1;
250
+ await saveUTXOStore(store);
251
+ }
252
+
253
+ /**
254
+ * Release in_flight notes back to available (if transaction fails or expires)
255
+ * Called when transaction building or broadcast fails
256
+ */
257
+ export async function releaseInFlightNotes(
258
+ accountAddress: string,
259
+ noteIds: string[]
260
+ ): Promise<void> {
261
+ const store = await getUTXOStore();
262
+
263
+ if (!store[accountAddress]) {
264
+ return; // Nothing to do
265
+ }
266
+
267
+ const noteIdSet = new Set(noteIds);
268
+
269
+ for (const note of store[accountAddress].notes) {
270
+ if (noteIdSet.has(note.noteId)) {
271
+ if (note.state === 'in_flight') {
272
+ note.state = 'available';
273
+ delete note.pendingTxId;
274
+ }
275
+ }
276
+ }
277
+
278
+ store[accountAddress].version += 1;
279
+ await saveUTXOStore(store);
280
+ }
281
+
282
+ /**
283
+ * Remove spent notes from storage (cleanup)
284
+ * Called periodically to prevent storage bloat
285
+ */
286
+ export async function removeSpentNotes(accountAddress: string): Promise<number> {
287
+ const store = await getUTXOStore();
288
+
289
+ if (!store[accountAddress]) {
290
+ return 0;
291
+ }
292
+
293
+ const before = store[accountAddress].notes.length;
294
+ store[accountAddress].notes = store[accountAddress].notes.filter(n => n.state !== 'spent');
295
+ const removed = before - store[accountAddress].notes.length;
296
+
297
+ if (removed > 0) {
298
+ store[accountAddress].version += 1;
299
+ await saveUTXOStore(store);
300
+ }
301
+
302
+ return removed;
303
+ }
304
+
305
+ /**
306
+ * Clear all notes for an account (for testing/reset)
307
+ */
308
+ export async function clearAccountNotes(accountAddress: string): Promise<void> {
309
+ const store = await getUTXOStore();
310
+ delete store[accountAddress];
311
+ await saveUTXOStore(store);
312
+ }
313
+
314
+ // ============================================================================
315
+ // Conversion: Note (RPC) -> StoredNote
316
+ // ============================================================================
317
+
318
+ /**
319
+ * Convert a Note from RPC response to StoredNote for storage
320
+ */
321
+ export function noteToStoredNote(
322
+ note: Note,
323
+ accountAddress: string,
324
+ state: NoteState = 'available'
325
+ ): StoredNote {
326
+ const nameFirst = note.nameFirstBase58 || uint8ArrayToBase58(note.nameFirst);
327
+ const nameLast = note.nameLastBase58 || uint8ArrayToBase58(note.nameLast);
328
+ const noteId = generateNoteId(nameFirst, nameLast);
329
+ const sourceHash = note.sourceHash?.length > 0 ? uint8ArrayToBase58(note.sourceHash) : '';
330
+
331
+ return {
332
+ noteId,
333
+ accountAddress,
334
+ sourceHash,
335
+ originPage: Number(note.originPage),
336
+ assets: note.assets,
337
+ nameFirst,
338
+ nameLast,
339
+ noteDataHashBase58: note.noteDataHashBase58 || '',
340
+ protoNote: note.protoNote,
341
+ state,
342
+ discoveredAt: Date.now(),
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Convert a FetchedUTXO to StoredNote
348
+ */
349
+ export function fetchedToStoredNote(
350
+ fetched: FetchedUTXO,
351
+ accountAddress: string,
352
+ state: NoteState = 'available',
353
+ isChange?: boolean
354
+ ): StoredNote {
355
+ return {
356
+ noteId: fetched.noteId,
357
+ accountAddress,
358
+ sourceHash: fetched.sourceHash,
359
+ originPage: fetched.originPage,
360
+ assets: fetched.assets,
361
+ nameFirst: fetched.nameFirst,
362
+ nameLast: fetched.nameLast,
363
+ noteDataHashBase58: fetched.noteDataHashBase58,
364
+ protoNote: fetched.protoNote,
365
+ state,
366
+ isChange,
367
+ discoveredAt: Date.now(),
368
+ };
369
+ }
370
+
371
+ // ============================================================================
372
+ // Sync State Operations
373
+ // ============================================================================
374
+
375
+ /**
376
+ * Get sync state for all accounts
377
+ */
378
+ async function getSyncStateStore(): Promise<SyncStateStore> {
379
+ const result = (await chrome.storage.local.get([STORAGE_KEYS.ACCOUNT_SYNC_STATE])) as Record<
380
+ string,
381
+ unknown
382
+ >;
383
+ const raw = result[STORAGE_KEYS.ACCOUNT_SYNC_STATE];
384
+ return (isRecord(raw) ? raw : {}) as SyncStateStore;
385
+ }
386
+
387
+ /**
388
+ * Save sync state store
389
+ */
390
+ async function saveSyncStateStore(store: SyncStateStore): Promise<void> {
391
+ await chrome.storage.local.set({
392
+ [STORAGE_KEYS.ACCOUNT_SYNC_STATE]: store,
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Get sync state for an account
398
+ */
399
+ export async function getAccountSyncState(
400
+ accountAddress: string
401
+ ): Promise<AccountSyncState | null> {
402
+ const store = await getSyncStateStore();
403
+ return store[accountAddress] || null;
404
+ }
405
+
406
+ /**
407
+ * Update sync state for an account
408
+ */
409
+ export async function updateAccountSyncState(
410
+ accountAddress: string,
411
+ lastSyncedHeight: number
412
+ ): Promise<void> {
413
+ const store = await getSyncStateStore();
414
+ store[accountAddress] = {
415
+ accountAddress,
416
+ lastSyncedHeight,
417
+ lastSyncedAt: Date.now(),
418
+ };
419
+ await saveSyncStateStore(store);
420
+ }
421
+
422
+ // ============================================================================
423
+ // Wallet Transaction Store Operations
424
+ // ============================================================================
425
+
426
+ /**
427
+ * Get wallet transaction store
428
+ */
429
+ async function getWalletTxStore(): Promise<WalletTxStore> {
430
+ const result = (await chrome.storage.local.get([STORAGE_KEYS.WALLET_TX_STORE])) as Record<
431
+ string,
432
+ unknown
433
+ >;
434
+ const raw = result[STORAGE_KEYS.WALLET_TX_STORE];
435
+ return (isRecord(raw) ? raw : {}) as WalletTxStore;
436
+ }
437
+
438
+ /**
439
+ * Save wallet transaction store
440
+ */
441
+ async function saveWalletTxStore(store: WalletTxStore): Promise<void> {
442
+ await chrome.storage.local.set({
443
+ [STORAGE_KEYS.WALLET_TX_STORE]: store,
444
+ });
445
+ }
446
+
447
+ /**
448
+ * Get all wallet transactions for an account
449
+ */
450
+ export async function getWalletTransactions(accountAddress: string): Promise<WalletTransaction[]> {
451
+ const store = await getWalletTxStore();
452
+ return store[accountAddress] || [];
453
+ }
454
+
455
+ /**
456
+ * Add a new wallet transaction
457
+ */
458
+ export async function addWalletTransaction(tx: WalletTransaction): Promise<void> {
459
+ const store = await getWalletTxStore();
460
+
461
+ if (!store[tx.accountAddress]) {
462
+ store[tx.accountAddress] = [];
463
+ }
464
+
465
+ // Check for duplicate
466
+ const exists = store[tx.accountAddress].some(t => t.id === tx.id);
467
+ if (exists) {
468
+ console.warn(`[UTXO Store] Transaction ${tx.id} already exists, skipping add`);
469
+ return;
470
+ }
471
+
472
+ // Add to beginning (most recent first)
473
+ store[tx.accountAddress].unshift(tx);
474
+
475
+ // Limit to 200 transactions per account
476
+ if (store[tx.accountAddress].length > 200) {
477
+ store[tx.accountAddress] = store[tx.accountAddress].slice(0, 200);
478
+ }
479
+
480
+ await saveWalletTxStore(store);
481
+ }
482
+
483
+ /**
484
+ * Update a wallet transaction
485
+ */
486
+ export async function updateWalletTransaction(
487
+ accountAddress: string,
488
+ txId: string,
489
+ updates: Partial<WalletTransaction>
490
+ ): Promise<void> {
491
+ const store = await getWalletTxStore();
492
+
493
+ if (!store[accountAddress]) {
494
+ return;
495
+ }
496
+
497
+ const txIndex = store[accountAddress].findIndex(t => t.id === txId);
498
+ if (txIndex === -1) {
499
+ console.warn(`[UTXO Store] Transaction ${txId} not found for update`);
500
+ return;
501
+ }
502
+
503
+ store[accountAddress][txIndex] = {
504
+ ...store[accountAddress][txIndex],
505
+ ...updates,
506
+ updatedAt: Date.now(),
507
+ };
508
+
509
+ await saveWalletTxStore(store);
510
+ }
511
+
512
+ /**
513
+ * Find a wallet transaction by its on-chain hash
514
+ */
515
+ export async function findWalletTxByHash(
516
+ accountAddress: string,
517
+ txHash: string
518
+ ): Promise<WalletTransaction | null> {
519
+ const transactions = await getWalletTransactions(accountAddress);
520
+ return transactions.find(t => t.txHash === txHash) || null;
521
+ }
522
+
523
+ /**
524
+ * Get pending outgoing transactions (for expiry checking)
525
+ */
526
+ export async function getPendingOutgoingTransactions(
527
+ accountAddress: string
528
+ ): Promise<WalletTransaction[]> {
529
+ const transactions = await getWalletTransactions(accountAddress);
530
+ return transactions.filter(
531
+ t =>
532
+ t.direction === 'outgoing' &&
533
+ (t.status === 'created' ||
534
+ t.status === 'broadcast_pending' ||
535
+ t.status === 'broadcasted_unconfirmed')
536
+ );
537
+ }
538
+
539
+ /**
540
+ * Get all outgoing transactions (pending + confirmed) for change detection
541
+ * This is needed to identify change UTXOs even after a transaction is confirmed
542
+ */
543
+ export async function getAllOutgoingTransactions(
544
+ accountAddress: string
545
+ ): Promise<WalletTransaction[]> {
546
+ const transactions = await getWalletTransactions(accountAddress);
547
+ return transactions.filter(t => t.direction === 'outgoing');
548
+ }