@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.
- package/.github/workflows/artifacts.yml +33 -0
- package/.github/workflows/ci.yml +68 -0
- package/.github/workflows/publish-sdk.yml +35 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/LICENSE +22 -0
- package/README.md +117 -0
- package/extension/background/index.ts +1500 -0
- package/extension/content/index.ts +59 -0
- package/extension/icons/rose.svg +27 -0
- package/extension/icons/rose128.png +0 -0
- package/extension/icons/rose16.png +0 -0
- package/extension/icons/rose256.png +0 -0
- package/extension/icons/rose32.png +0 -0
- package/extension/icons/rose48.png +0 -0
- package/extension/icons/rose512.png +0 -0
- package/extension/inpage/index.ts +86 -0
- package/extension/manifest.json +48 -0
- package/extension/popup/Popup.tsx +94 -0
- package/extension/popup/Router.tsx +121 -0
- package/extension/popup/assets/arrow-down-icon.svg +3 -0
- package/extension/popup/assets/arrow-left-icon.svg +3 -0
- package/extension/popup/assets/arrow-right-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
- package/extension/popup/assets/checkmark-icon.svg +3 -0
- package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
- package/extension/popup/assets/checkmark-success-icon.svg +3 -0
- package/extension/popup/assets/clock-icon.svg +3 -0
- package/extension/popup/assets/close-x-icon.svg +3 -0
- package/extension/popup/assets/copy-icon.svg +6 -0
- package/extension/popup/assets/explorer-icon.svg +3 -0
- package/extension/popup/assets/eye-off-icon.svg +3 -0
- package/extension/popup/assets/eye-open-icon.svg +4 -0
- package/extension/popup/assets/feedback-icon.svg +3 -0
- package/extension/popup/assets/green-status-dot.svg +3 -0
- package/extension/popup/assets/info-icon.svg +3 -0
- package/extension/popup/assets/iris-logo-40.svg +27 -0
- package/extension/popup/assets/iris-logo-96.svg +27 -0
- package/extension/popup/assets/iris-logo-blue.svg +27 -0
- package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
- package/extension/popup/assets/iris-logo-orange.svg +27 -0
- package/extension/popup/assets/iris-logo.svg +27 -0
- package/extension/popup/assets/key-icon.svg +3 -0
- package/extension/popup/assets/lock-icon-yellow.svg +3 -0
- package/extension/popup/assets/lock-icon.svg +3 -0
- package/extension/popup/assets/pencil-edit-icon.svg +3 -0
- package/extension/popup/assets/permissions-icon.svg +3 -0
- package/extension/popup/assets/receipt-icon.svg +5 -0
- package/extension/popup/assets/refresh-icon.svg +3 -0
- package/extension/popup/assets/settings-gear-icon.svg +8 -0
- package/extension/popup/assets/settings-icon.svg +3 -0
- package/extension/popup/assets/theme-icon.svg +3 -0
- package/extension/popup/assets/trash-bin-icon.svg +3 -0
- package/extension/popup/assets/trend-down-arrow.svg +5 -0
- package/extension/popup/assets/trend-up-arrow.svg +5 -0
- package/extension/popup/assets/user-account-icon.svg +3 -0
- package/extension/popup/assets/vector-bottom-left.svg +9 -0
- package/extension/popup/assets/vector-left.svg +9 -0
- package/extension/popup/assets/vector-right.svg +9 -0
- package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
- package/extension/popup/assets/vector-top-right.svg +9 -0
- package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
- package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
- package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
- package/extension/popup/components/AccountIcon.tsx +78 -0
- package/extension/popup/components/AccountSelector.tsx +246 -0
- package/extension/popup/components/Alert.tsx +48 -0
- package/extension/popup/components/ConfirmModal.tsx +81 -0
- package/extension/popup/components/PasswordInput.tsx +49 -0
- package/extension/popup/components/ScreenContainer.tsx +17 -0
- package/extension/popup/components/SiteIcon.tsx +60 -0
- package/extension/popup/components/ThemeToggle.tsx +44 -0
- package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
- package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
- package/extension/popup/components/icons/CheckIcon.tsx +20 -0
- package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
- package/extension/popup/components/icons/CloseIcon.tsx +26 -0
- package/extension/popup/components/icons/CopyIcon.tsx +20 -0
- package/extension/popup/components/icons/EditIcon.tsx +20 -0
- package/extension/popup/components/icons/EyeIcon.tsx +13 -0
- package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
- package/extension/popup/components/icons/InfoIcon.tsx +20 -0
- package/extension/popup/components/icons/LockIcon.tsx +20 -0
- package/extension/popup/components/icons/PlusIcon.tsx +15 -0
- package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
- package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
- package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
- package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
- package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
- package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
- package/extension/popup/components/icons/UploadIcon.tsx +20 -0
- package/extension/popup/components/icons/WalletIcon.tsx +20 -0
- package/extension/popup/contexts/ThemeContext.tsx +105 -0
- package/extension/popup/hooks/useApprovalDetection.ts +128 -0
- package/extension/popup/hooks/useAutoFocus.ts +36 -0
- package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
- package/extension/popup/hooks/useClickOutside.ts +33 -0
- package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
- package/extension/popup/hooks/useFavicon.ts +64 -0
- package/extension/popup/hooks/useNumericInput.ts +93 -0
- package/extension/popup/index.html +13 -0
- package/extension/popup/index.tsx +24 -0
- package/extension/popup/screens/AboutScreen.tsx +118 -0
- package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
- package/extension/popup/screens/HomeScreen.tsx +902 -0
- package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
- package/extension/popup/screens/LockTimeScreen.tsx +155 -0
- package/extension/popup/screens/ReceiveScreen.tsx +149 -0
- package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
- package/extension/popup/screens/SendReviewScreen.tsx +308 -0
- package/extension/popup/screens/SendScreen.tsx +825 -0
- package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
- package/extension/popup/screens/SettingsScreen.tsx +116 -0
- package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
- package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
- package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
- package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
- package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
- package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
- package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
- package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
- package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
- package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
- package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
- package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
- package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
- package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
- package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
- package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
- package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
- package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
- package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
- package/extension/popup/screens/system/LockedScreen.tsx +288 -0
- package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
- package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
- package/extension/popup/store.ts +482 -0
- package/extension/popup/styles.css +246 -0
- package/extension/popup/utils/format.ts +58 -0
- package/extension/popup/utils/formatWalletError.ts +36 -0
- package/extension/popup/utils/memo.ts +299 -0
- package/extension/popup/utils/messaging.ts +16 -0
- package/extension/shared/address-encoding.ts +69 -0
- package/extension/shared/balance-query.ts +123 -0
- package/extension/shared/constants.ts +386 -0
- package/extension/shared/currency.ts +128 -0
- package/extension/shared/first-name-derivation.ts +128 -0
- package/extension/shared/keyfile.ts +58 -0
- package/extension/shared/onboarding.ts +78 -0
- package/extension/shared/price-api.ts +79 -0
- package/extension/shared/rpc-client-browser.ts +315 -0
- package/extension/shared/transaction-builder.ts +443 -0
- package/extension/shared/types.ts +450 -0
- package/extension/shared/utxo-diff.ts +212 -0
- package/extension/shared/utxo-store.ts +548 -0
- package/extension/shared/utxo-sync.ts +343 -0
- package/extension/shared/validators.ts +26 -0
- package/extension/shared/vault.ts +1580 -0
- package/extension/shared/wallet-crypto.ts +77 -0
- package/extension/shared/wasm-utils.ts +76 -0
- package/extension/shared/webcrypto.ts +67 -0
- package/extension/types/wasm.d.ts +13 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/rose-extension-dist.zip +0 -0
- package/sdk/README.md +88 -0
- package/sdk/examples/app.ts +166 -0
- package/sdk/examples/index.html +51 -0
- package/sdk/examples/tsconfig.json +15 -0
- package/sdk/examples/tx-builder.html +532 -0
- package/sdk/examples/tx-builder.ts +1766 -0
- package/sdk/package-lock.json +424 -0
- package/sdk/package.json +68 -0
- package/sdk/src/constants.ts +28 -0
- package/sdk/src/errors.ts +74 -0
- package/sdk/src/hooks/index.ts +1 -0
- package/sdk/src/hooks/use-rose.ts +94 -0
- package/sdk/src/index.ts +12 -0
- package/sdk/src/provider.ts +396 -0
- package/sdk/src/transaction.ts +163 -0
- package/sdk/src/types/rose-wasm.d.ts +14 -0
- package/sdk/src/types.ts +97 -0
- package/sdk/src/wasm.ts +13 -0
- package/sdk/tsconfig.json +20 -0
- package/sdk/vite.config.examples.ts +32 -0
- package/tailwind.config.ts +38 -0
- package/tsconfig.json +20 -0
- 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
|
+
};
|