@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,343 @@
1
+ /**
2
+ * UTXO Sync - Orchestrates syncing local UTXO store with chain state
3
+ *
4
+ * This module coordinates between:
5
+ * - RPC client (fetches chain state)
6
+ * - UTXO store (local state management)
7
+ * - UTXO diff (pure diff computation)
8
+ * - Wallet transactions (tx lifecycle)
9
+ */
10
+
11
+ import { queryV1Balance } from './balance-query';
12
+ import type { NockchainBrowserRPCClient } from './rpc-client-browser';
13
+ import type { StoredNote, FetchedUTXO, Note } from './types';
14
+ import {
15
+ getAccountNotes,
16
+ saveNotes,
17
+ markNotesSpent,
18
+ releaseInFlightNotes,
19
+ noteToStoredNote,
20
+ fetchedToStoredNote,
21
+ withAccountLock,
22
+ getWalletTransactions,
23
+ updateWalletTransaction,
24
+ addWalletTransaction,
25
+ getPendingOutgoingTransactions,
26
+ getAllOutgoingTransactions,
27
+ generateNoteId,
28
+ } from './utxo-store';
29
+ import {
30
+ computeUTXODiff,
31
+ classifyNewUTXO,
32
+ findExpiredTransactions,
33
+ areTransactionInputsSpent,
34
+ matchChangeOutputs,
35
+ } from './utxo-diff';
36
+ import { NOCK_TO_NICKS } from './constants';
37
+ import { base58 } from '@scure/base';
38
+
39
+ /** Transaction expiry timeout: 6 hours */
40
+ const TX_EXPIRY_MS = 6 * 60 * 60 * 1000;
41
+
42
+ /**
43
+ * Convert Note (from RPC) to FetchedUTXO (for diff computation)
44
+ */
45
+ function noteToFetchedUTXO(note: Note): FetchedUTXO {
46
+ const nameFirst = note.nameFirstBase58 || base58.encode(note.nameFirst);
47
+ const nameLast = note.nameLastBase58 || base58.encode(note.nameLast);
48
+ const sourceHash = note.sourceHash?.length > 0 ? base58.encode(note.sourceHash) : '';
49
+
50
+ return {
51
+ noteId: generateNoteId(nameFirst, nameLast),
52
+ sourceHash,
53
+ originPage: Number(note.originPage),
54
+ assets: note.assets,
55
+ nameFirst,
56
+ nameLast,
57
+ noteDataHashBase58: note.noteDataHashBase58 || '',
58
+ protoNote: note.protoNote,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Sync UTXOs for a single account
64
+ * This is the main sync function called by the background polling service
65
+ *
66
+ * @param accountAddress - Account to sync
67
+ * @param rpcClient - RPC client for chain queries
68
+ * @returns Summary of what changed
69
+ */
70
+ export async function syncAccountUTXOs(
71
+ accountAddress: string,
72
+ rpcClient: NockchainBrowserRPCClient
73
+ ): Promise<{
74
+ newIncoming: number;
75
+ newChange: number;
76
+ spent: number;
77
+ confirmed: number;
78
+ expired: number;
79
+ }> {
80
+ return withAccountLock(accountAddress, async () => {
81
+ // 1. Fetch current UTXOs from chain
82
+ const balanceResult = await queryV1Balance(accountAddress, rpcClient);
83
+ const chainNotes = [...balanceResult.simpleNotes, ...balanceResult.coinbaseNotes];
84
+ const fetchedUTXOs = chainNotes.map(noteToFetchedUTXO);
85
+
86
+ // 2. Get local state
87
+ const localNotes = await getAccountNotes(accountAddress);
88
+ const pendingTxs = await getPendingOutgoingTransactions(accountAddress);
89
+ const allOutgoingTxs = await getAllOutgoingTransactions(accountAddress);
90
+
91
+ // 3. Compute diff (pass all outgoing txs for change detection)
92
+ const diff = computeUTXODiff(localNotes, fetchedUTXOs, pendingTxs, allOutgoingTxs);
93
+
94
+ // 4. Process spent notes
95
+ if (diff.nowSpent.length > 0) {
96
+ const spentNoteIds = diff.nowSpent.map(n => n.noteId);
97
+ await markNotesSpent(accountAddress, spentNoteIds);
98
+
99
+ // Check if any pending transactions are now confirmed
100
+ for (const tx of pendingTxs) {
101
+ if (areTransactionInputsSpent(tx, diff.nowSpent)) {
102
+ // Find change outputs for this transaction
103
+ const changeNoteIds = matchChangeOutputs(tx, diff.newUTXOs, diff.isChangeMap);
104
+
105
+ await updateWalletTransaction(accountAddress, tx.id, {
106
+ status: 'confirmed',
107
+ expectedChangeNoteIds: changeNoteIds,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ // 5. Process new UTXOs
114
+ let newIncoming = 0;
115
+ let newChange = 0;
116
+ const newStoredNotes: StoredNote[] = [];
117
+
118
+ for (const newUTXO of diff.newUTXOs) {
119
+ const { isChange, walletTxId } = classifyNewUTXO(newUTXO, diff.isChangeMap);
120
+
121
+ const storedNote = fetchedToStoredNote(newUTXO, accountAddress, 'available', isChange);
122
+
123
+ if (isChange && walletTxId) {
124
+ storedNote.pendingTxId = walletTxId; // Link to originating tx
125
+ newChange++;
126
+ } else {
127
+ // This is a true incoming transaction (not change from our own tx)
128
+ // Create a WalletTransaction record so it appears in the transaction list
129
+ const incomingTxId = crypto.randomUUID();
130
+ const now = Date.now();
131
+
132
+ await addWalletTransaction({
133
+ id: incomingTxId,
134
+ txHash: newUTXO.sourceHash, // The transaction that created this UTXO
135
+ accountAddress,
136
+ direction: 'incoming',
137
+ createdAt: now,
138
+ updatedAt: now,
139
+ status: 'confirmed', // Incoming UTXOs are already confirmed when we see them
140
+ amount: newUTXO.assets,
141
+ receivedNoteIds: [newUTXO.noteId],
142
+ });
143
+
144
+ newIncoming++;
145
+ }
146
+
147
+ newStoredNotes.push(storedNote);
148
+ }
149
+
150
+ // Save new notes to store
151
+ if (newStoredNotes.length > 0) {
152
+ await saveNotes(accountAddress, newStoredNotes);
153
+ }
154
+
155
+ // 5b. Check for pending transactions whose inputs are ALREADY spent
156
+ // This handles the case where inputs were marked spent in a previous sync
157
+ // but the transaction wasn't confirmed (e.g., laptop closed mid-sync)
158
+ let confirmedFromPreviousSpent = 0;
159
+ const stillPendingTxs = pendingTxs.filter(tx => !areTransactionInputsSpent(tx, diff.nowSpent));
160
+
161
+ if (stillPendingTxs.length > 0) {
162
+ // Get fresh notes to check against already-spent inputs
163
+ const currentNotes = await getAccountNotes(accountAddress);
164
+ const spentNoteIds = new Set(
165
+ currentNotes.filter(n => n.state === 'spent').map(n => n.noteId)
166
+ );
167
+
168
+ for (const tx of stillPendingTxs) {
169
+ if (!tx.inputNoteIds || tx.inputNoteIds.length === 0) continue;
170
+
171
+ // Check if ALL inputs are already marked as spent in storage
172
+ const allInputsSpent = tx.inputNoteIds.every(noteId => spentNoteIds.has(noteId));
173
+
174
+ if (allInputsSpent) {
175
+ await updateWalletTransaction(accountAddress, tx.id, {
176
+ status: 'confirmed',
177
+ });
178
+ confirmedFromPreviousSpent++;
179
+ }
180
+ }
181
+ }
182
+
183
+ // 6. Handle expired transactions
184
+ const allTxs = await getWalletTransactions(accountAddress);
185
+ const expiredTxs = findExpiredTransactions(allTxs, TX_EXPIRY_MS);
186
+
187
+ for (const expiredTx of expiredTxs) {
188
+ // Release locked notes
189
+ if (expiredTx.inputNoteIds && expiredTx.inputNoteIds.length > 0) {
190
+ await releaseInFlightNotes(accountAddress, expiredTx.inputNoteIds);
191
+ }
192
+
193
+ // Mark transaction as expired
194
+ await updateWalletTransaction(accountAddress, expiredTx.id, {
195
+ status: 'expired',
196
+ });
197
+ }
198
+
199
+ const confirmedFromNewSpent = pendingTxs.filter(tx =>
200
+ areTransactionInputsSpent(tx, diff.nowSpent)
201
+ ).length;
202
+
203
+ return {
204
+ newIncoming,
205
+ newChange,
206
+ spent: diff.nowSpent.length,
207
+ confirmed: confirmedFromNewSpent + confirmedFromPreviousSpent,
208
+ expired: expiredTxs.length,
209
+ };
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Initialize UTXO store for a newly created/imported account
215
+ * Called on first unlock to bootstrap the local store
216
+ *
217
+ * @param accountAddress - Account to initialize
218
+ * @param rpcClient - RPC client for chain queries
219
+ */
220
+ export async function initializeAccountUTXOs(
221
+ accountAddress: string,
222
+ rpcClient: NockchainBrowserRPCClient
223
+ ): Promise<void> {
224
+ return withAccountLock(accountAddress, async () => {
225
+ // Check if already initialized
226
+ const existingNotes = await getAccountNotes(accountAddress);
227
+ if (existingNotes.length > 0) {
228
+ return;
229
+ }
230
+
231
+ // Fetch current UTXOs from chain
232
+ const balanceResult = await queryV1Balance(accountAddress, rpcClient);
233
+ const chainNotes = [...balanceResult.simpleNotes, ...balanceResult.coinbaseNotes];
234
+
235
+ // Convert to stored notes (all available, no incoming tx records on first init)
236
+ const storedNotes: StoredNote[] = chainNotes.map(note =>
237
+ noteToStoredNote(note, accountAddress, 'available')
238
+ );
239
+
240
+ // Save to store
241
+ if (storedNotes.length > 0) {
242
+ await saveNotes(accountAddress, storedNotes);
243
+ }
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Get balance summary for an account from local store
249
+ * This is the source of truth for UI display
250
+ *
251
+ * Follows Bitcoin wallet convention: includes expected change from pending
252
+ * transactions in the available balance. This makes the balance immediately
253
+ * reflect (total - sent - fee) rather than (total - full_input_utxo).
254
+ */
255
+ export async function getAccountBalanceSummary(accountAddress: string): Promise<{
256
+ available: number;
257
+ spendableNow: number;
258
+ pendingOut: number;
259
+ pendingChange: number;
260
+ total: number;
261
+ utxoCount: number;
262
+ availableUtxoCount: number;
263
+ }> {
264
+ const notes = await getAccountNotes(accountAddress);
265
+ const pendingTxs = await getPendingOutgoingTransactions(accountAddress);
266
+
267
+ const availableNotes = notes.filter(n => n.state === 'available');
268
+ const pendingNotes = notes.filter(n => n.state === 'in_flight');
269
+
270
+ const availableFromNotes = availableNotes.reduce((sum, n) => sum + n.assets, 0);
271
+ const pendingOut = pendingNotes.reduce((sum, n) => sum + n.assets, 0);
272
+
273
+ // Sum expected change from pending outgoing transactions
274
+ // This is change that will come back to us after confirmation
275
+ const pendingChange = pendingTxs.reduce((sum, tx) => sum + (tx.expectedChange || 0), 0);
276
+
277
+ // Available balance includes expected change (Bitcoin wallet convention)
278
+ const available = availableFromNotes + pendingChange;
279
+
280
+ // Spendable now: only UTXOs that are actually available (not in_flight)
281
+ // This is what can be used as inputs for a new transaction RIGHT NOW
282
+ const spendableNow = availableFromNotes;
283
+
284
+ return {
285
+ available,
286
+ spendableNow,
287
+ pendingOut,
288
+ pendingChange,
289
+ total: availableFromNotes + pendingOut, // True total of all non-spent notes
290
+ utxoCount: notes.filter(n => n.state !== 'spent').length,
291
+ availableUtxoCount: availableNotes.length,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Force a full resync of an account's UTXOs
297
+ * Useful for recovery scenarios or user-initiated refresh
298
+ */
299
+ export async function forceResyncAccount(
300
+ accountAddress: string,
301
+ rpcClient: NockchainBrowserRPCClient
302
+ ): Promise<void> {
303
+ return withAccountLock(accountAddress, async () => {
304
+ // Fetch current UTXOs from chain
305
+ const balanceResult = await queryV1Balance(accountAddress, rpcClient);
306
+ const chainNotes = [...balanceResult.simpleNotes, ...balanceResult.coinbaseNotes];
307
+ const fetchedUTXOs = chainNotes.map(noteToFetchedUTXO);
308
+
309
+ // Get existing notes to preserve pending state
310
+ const existingNotes = await getAccountNotes(accountAddress);
311
+
312
+ // Build map of note IDs that are currently in pending transactions
313
+ const pendingNoteIds = new Map<string, { state: StoredNote['state']; txId: string }>();
314
+ for (const note of existingNotes) {
315
+ if (note.state === 'in_flight' && note.pendingTxId) {
316
+ pendingNoteIds.set(note.noteId, {
317
+ state: note.state,
318
+ txId: note.pendingTxId,
319
+ });
320
+ }
321
+ }
322
+
323
+ // Rebuild stored notes from chain state
324
+ const newStoredNotes: StoredNote[] = [];
325
+
326
+ for (const fetched of fetchedUTXOs) {
327
+ const pending = pendingNoteIds.get(fetched.noteId);
328
+
329
+ if (pending) {
330
+ // Preserve pending state
331
+ const storedNote = fetchedToStoredNote(fetched, accountAddress, pending.state);
332
+ storedNote.pendingTxId = pending.txId;
333
+ newStoredNotes.push(storedNote);
334
+ } else {
335
+ // New or available
336
+ newStoredNotes.push(fetchedToStoredNote(fetched, accountAddress, 'available'));
337
+ }
338
+ }
339
+
340
+ // Replace all notes (but keep pending state)
341
+ await saveNotes(accountAddress, newStoredNotes);
342
+ });
343
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Validators for Nockchain addresses and other data
3
+ */
4
+
5
+ import { base58 } from '@scure/base';
6
+
7
+ /**
8
+ * Validates a Nockchain V1 PKH address
9
+ * V1 PKH addresses are TIP5 hash (40 bytes) of public key, base58 encoded
10
+ * Base58 encoding of 40 bytes results in 54-55 characters (typically 55)
11
+ *
12
+ * Validates by decoding the base58 string and checking for exactly 40 bytes
13
+ * rather than relying on character count which can vary
14
+ */
15
+ export const isNockAddress = (s: string): boolean => {
16
+ try {
17
+ const trimmed = (s || '').trim();
18
+ if (trimmed.length === 0) return false;
19
+
20
+ const bytes = base58.decode(trimmed);
21
+ return bytes.length === 40;
22
+ } catch {
23
+ // Invalid base58 encoding
24
+ return false;
25
+ }
26
+ };