@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,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
|
+
}
|