@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,1500 @@
|
|
|
1
|
+
/// <reference types="chrome" />
|
|
2
|
+
/**
|
|
3
|
+
* Service Worker: Wallet controller and message router
|
|
4
|
+
* Handles provider requests from content script and popup UI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Vault } from '../shared/vault';
|
|
8
|
+
import { isNockAddress } from '../shared/validators';
|
|
9
|
+
import { initIrisSdkOnce } from '../shared/wasm-utils';
|
|
10
|
+
import {
|
|
11
|
+
PROVIDER_METHODS,
|
|
12
|
+
INTERNAL_METHODS,
|
|
13
|
+
ERROR_CODES,
|
|
14
|
+
ALARM_NAMES,
|
|
15
|
+
AUTOLOCK_MINUTES,
|
|
16
|
+
STORAGE_KEYS,
|
|
17
|
+
SESSION_STORAGE_KEYS,
|
|
18
|
+
USER_ACTIVITY_METHODS,
|
|
19
|
+
UI_CONSTANTS,
|
|
20
|
+
APPROVAL_CONSTANTS,
|
|
21
|
+
RPC_ENDPOINT,
|
|
22
|
+
} from '../shared/constants';
|
|
23
|
+
import type {
|
|
24
|
+
TransactionRequest,
|
|
25
|
+
SignRequest,
|
|
26
|
+
ConnectRequest,
|
|
27
|
+
SignRawTxRequest,
|
|
28
|
+
} from '../shared/types';
|
|
29
|
+
|
|
30
|
+
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
31
|
+
return typeof x === 'object' && x !== null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const vault = new Vault();
|
|
35
|
+
// Ensure WASM is initialized once per service worker context.
|
|
36
|
+
// Some background flows (message routing, tx handling) require WASM to be ready.
|
|
37
|
+
const wasmInitPromise = initIrisSdkOnce();
|
|
38
|
+
let lastActivity = Date.now();
|
|
39
|
+
let autoLockMinutes = AUTOLOCK_MINUTES;
|
|
40
|
+
let manuallyLocked = false; // Track if user manually locked (don't auto-unlock)
|
|
41
|
+
let approvalWindowId: number | null = null; // Track the approval popup window for reuse
|
|
42
|
+
let isCreatingWindow = false; // Prevent race condition when creating window
|
|
43
|
+
let currentRequestId: string | null = null; // Currently displayed request
|
|
44
|
+
let requestQueue: Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
type: 'connect' | 'transaction' | 'sign-message' | 'sign-raw-tx';
|
|
47
|
+
}> = []; // Queued requests
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* In-memory cache of approved origins
|
|
51
|
+
* Loaded from storage on startup, persisted on changes
|
|
52
|
+
*/
|
|
53
|
+
let approvedOrigins = new Set<string>();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Request expiration time (5 minutes)
|
|
57
|
+
* Prevents replay attacks on approval requests
|
|
58
|
+
*/
|
|
59
|
+
const REQUEST_EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* RPC connection status
|
|
63
|
+
* Updated by popup via REPORT_RPC_STATUS when actual gRPC calls succeed/fail
|
|
64
|
+
*/
|
|
65
|
+
let isRpcConnected = true;
|
|
66
|
+
|
|
67
|
+
type UnlockSessionCache = {
|
|
68
|
+
key: number[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let sessionRestorePromise: Promise<void> | null = null;
|
|
72
|
+
|
|
73
|
+
async function clearUnlockSessionCache(): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
await chrome.storage.session?.remove(SESSION_STORAGE_KEYS.UNLOCK_CACHE);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[Background] Failed to clear unlock cache:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function persistUnlockSession(): Promise<void> {
|
|
82
|
+
const sessionStorage = chrome.storage.session;
|
|
83
|
+
if (!sessionStorage || vault.isLocked()) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const encryptionKey = vault.getEncryptionKey();
|
|
88
|
+
if (!encryptionKey) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const rawKey = new Uint8Array(await crypto.subtle.exportKey('raw', encryptionKey));
|
|
94
|
+
await sessionStorage.set({
|
|
95
|
+
[SESSION_STORAGE_KEYS.UNLOCK_CACHE]: Array.from(rawKey),
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[Background] Failed to persist unlock session:', error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function restoreUnlockSession(): Promise<void> {
|
|
103
|
+
const sessionStorage = chrome.storage.session;
|
|
104
|
+
if (!sessionStorage) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const stored = await sessionStorage.get([SESSION_STORAGE_KEYS.UNLOCK_CACHE]);
|
|
109
|
+
const cached = stored[SESSION_STORAGE_KEYS.UNLOCK_CACHE] as UnlockSessionCache['key'] | undefined;
|
|
110
|
+
|
|
111
|
+
if (!cached || cached.length === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Respect manual lock - never auto-unlock if user explicitly locked
|
|
116
|
+
if (manuallyLocked) {
|
|
117
|
+
await clearUnlockSessionCache();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Respect auto-lock timeout window
|
|
122
|
+
if (autoLockMinutes > 0) {
|
|
123
|
+
const idleMs = Date.now() - lastActivity;
|
|
124
|
+
if (idleMs >= autoLockMinutes * 60_000) {
|
|
125
|
+
await clearUnlockSessionCache();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const key = await crypto.subtle.importKey(
|
|
132
|
+
'raw',
|
|
133
|
+
new Uint8Array(cached),
|
|
134
|
+
{ name: 'AES-GCM' },
|
|
135
|
+
false,
|
|
136
|
+
['encrypt', 'decrypt']
|
|
137
|
+
);
|
|
138
|
+
const result = await vault.unlockWithKey(key);
|
|
139
|
+
if ('error' in result) {
|
|
140
|
+
await clearUnlockSessionCache();
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('[Background] Failed to restore unlock session:', error);
|
|
144
|
+
await clearUnlockSessionCache();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function ensureSessionRestored(): Promise<void> {
|
|
149
|
+
if (!vault.isLocked()) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!sessionRestorePromise) {
|
|
154
|
+
sessionRestorePromise = restoreUnlockSession().finally(() => {
|
|
155
|
+
sessionRestorePromise = null;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await sessionRestorePromise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Load approved origins from storage
|
|
164
|
+
*/
|
|
165
|
+
async function loadApprovedOrigins(): Promise<void> {
|
|
166
|
+
const stored = (await chrome.storage.local.get([STORAGE_KEYS.APPROVED_ORIGINS])) as Record<
|
|
167
|
+
string,
|
|
168
|
+
unknown
|
|
169
|
+
>;
|
|
170
|
+
const raw = stored[STORAGE_KEYS.APPROVED_ORIGINS];
|
|
171
|
+
const origins = Array.isArray(raw) ? raw.filter((x): x is string => typeof x === 'string') : [];
|
|
172
|
+
approvedOrigins = new Set<string>(origins);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Save approved origins to storage
|
|
177
|
+
*/
|
|
178
|
+
async function saveApprovedOrigins(): Promise<void> {
|
|
179
|
+
await chrome.storage.local.set({
|
|
180
|
+
[STORAGE_KEYS.APPROVED_ORIGINS]: Array.from(approvedOrigins),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add an origin to the approved list
|
|
186
|
+
*/
|
|
187
|
+
async function approveOrigin(origin: string): Promise<void> {
|
|
188
|
+
approvedOrigins.add(origin);
|
|
189
|
+
await saveApprovedOrigins();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Remove an origin from the approved list
|
|
194
|
+
*/
|
|
195
|
+
async function revokeOrigin(origin: string): Promise<void> {
|
|
196
|
+
approvedOrigins.delete(origin);
|
|
197
|
+
await saveApprovedOrigins();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if origin is approved for provider method access
|
|
202
|
+
*/
|
|
203
|
+
function isOriginApproved(origin: string): boolean {
|
|
204
|
+
// Allow file:// protocol for local testing in development only
|
|
205
|
+
if (import.meta.env.DEV && origin.startsWith('file://')) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if origin is in approved list
|
|
210
|
+
return approvedOrigins.has(origin);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cancelPendingRequest(requestId: string, code?: number, message?: string): void {
|
|
214
|
+
const request = pendingRequests.get(requestId);
|
|
215
|
+
if (!request) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
pendingRequests.delete(requestId);
|
|
219
|
+
request.sendResponse({
|
|
220
|
+
error: { code: code || 4001, message: message || 'Request was cancelled' },
|
|
221
|
+
});
|
|
222
|
+
if (currentRequestId == requestId) {
|
|
223
|
+
currentRequestId = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if a request timestamp has expired
|
|
229
|
+
* @param timestamp - Request creation timestamp
|
|
230
|
+
* @returns true if expired, false if still valid
|
|
231
|
+
*/
|
|
232
|
+
function isRequestExpired(timestamp: number): boolean {
|
|
233
|
+
return Date.now() - timestamp > REQUEST_EXPIRATION_MS;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Pending approval requests
|
|
238
|
+
* Maps request ID to the request data and response callback
|
|
239
|
+
*/
|
|
240
|
+
interface PendingRequest {
|
|
241
|
+
request: TransactionRequest | SignRequest | ConnectRequest | SignRawTxRequest;
|
|
242
|
+
sendResponse: (response: any) => void;
|
|
243
|
+
origin: string;
|
|
244
|
+
needsUnlock?: boolean; // Flag indicating request is waiting for wallet unlock
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const pendingRequests = new Map<string, PendingRequest>();
|
|
248
|
+
// v0 migration provider methods (string-literal; not yet in published rose-sdk)
|
|
249
|
+
const MIGRATE_V0_GET_STATUS = 'nock_migrateV0GetStatus';
|
|
250
|
+
const MIGRATE_V0_SIGN_RAW_TX = 'nock_migrateV0SignRawTx';
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Type guard to check if a request is a ConnectRequest
|
|
254
|
+
*/
|
|
255
|
+
function isConnectRequest(
|
|
256
|
+
request: TransactionRequest | SignRequest | ConnectRequest | SignRawTxRequest
|
|
257
|
+
): request is ConnectRequest {
|
|
258
|
+
return 'timestamp' in request && !('message' in request) && !('to' in request);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Type guard to check if a request is a SignRequest
|
|
263
|
+
*/
|
|
264
|
+
function isSignRequest(
|
|
265
|
+
request: TransactionRequest | SignRequest | ConnectRequest | SignRawTxRequest
|
|
266
|
+
): request is SignRequest {
|
|
267
|
+
return 'message' in request;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Type guard to check if a request is a SignRawTxRequest
|
|
272
|
+
*/
|
|
273
|
+
function isSignRawTxRequest(
|
|
274
|
+
request: TransactionRequest | SignRequest | ConnectRequest | SignRawTxRequest
|
|
275
|
+
): request is SignRawTxRequest {
|
|
276
|
+
return 'rawTx' in request && 'notes' in request && 'spendConditions' in request;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Type guard to check if a request is a TransactionRequest
|
|
281
|
+
*/
|
|
282
|
+
function isTransactionRequest(
|
|
283
|
+
request: TransactionRequest | SignRequest | ConnectRequest | SignRawTxRequest
|
|
284
|
+
): request is TransactionRequest {
|
|
285
|
+
return 'to' in request;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create an approval popup window (or reuse existing one)
|
|
290
|
+
* Uses MetaMask pattern: single popup window for all approval requests
|
|
291
|
+
* Queues requests if user is currently viewing another request
|
|
292
|
+
*/
|
|
293
|
+
async function createApprovalPopup(
|
|
294
|
+
requestId: string,
|
|
295
|
+
type: 'connect' | 'transaction' | 'sign-message' | 'sign-raw-tx'
|
|
296
|
+
) {
|
|
297
|
+
// If user is currently viewing a different request, queue this one
|
|
298
|
+
if (currentRequestId !== null && currentRequestId !== requestId) {
|
|
299
|
+
// Check if already in queue to prevent duplicates
|
|
300
|
+
const alreadyQueued = requestQueue.some(r => r.id === requestId);
|
|
301
|
+
if (!alreadyQueued) {
|
|
302
|
+
requestQueue.push({ id: requestId, type });
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Mark this request as currently displayed
|
|
308
|
+
currentRequestId = requestId;
|
|
309
|
+
|
|
310
|
+
let hashPrefix: string;
|
|
311
|
+
if (type === 'connect') {
|
|
312
|
+
hashPrefix = APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX;
|
|
313
|
+
} else if (type === 'transaction') {
|
|
314
|
+
hashPrefix = APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX;
|
|
315
|
+
} else if (type === 'sign-raw-tx') {
|
|
316
|
+
hashPrefix = APPROVAL_CONSTANTS.SIGN_RAW_TX_HASH_PREFIX;
|
|
317
|
+
} else {
|
|
318
|
+
hashPrefix = APPROVAL_CONSTANTS.SIGN_MESSAGE_HASH_PREFIX;
|
|
319
|
+
}
|
|
320
|
+
const popupUrl = chrome.runtime.getURL(`popup/index.html#${hashPrefix}${requestId}`);
|
|
321
|
+
|
|
322
|
+
// Try to reuse existing approval window
|
|
323
|
+
if (approvalWindowId !== null) {
|
|
324
|
+
try {
|
|
325
|
+
const existingWindow = await chrome.windows.get(approvalWindowId);
|
|
326
|
+
|
|
327
|
+
// Window still exists - update it with new request
|
|
328
|
+
if (existingWindow.tabs && existingWindow.tabs[0]?.id) {
|
|
329
|
+
await chrome.tabs.update(existingWindow.tabs[0].id, { url: popupUrl });
|
|
330
|
+
await chrome.windows.update(approvalWindowId, { focused: true });
|
|
331
|
+
return; // Done - reused existing window
|
|
332
|
+
} else {
|
|
333
|
+
await chrome.windows.remove(approvalWindowId);
|
|
334
|
+
approvalWindowId = null;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// Window was closed or doesn't exist, create new one
|
|
338
|
+
approvalWindowId = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Prevent race condition: if window is being created, wait and retry
|
|
343
|
+
if (isCreatingWindow) {
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
345
|
+
return createApprovalPopup(requestId, type);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Create new approval window
|
|
349
|
+
isCreatingWindow = true;
|
|
350
|
+
try {
|
|
351
|
+
const width = UI_CONSTANTS.POPUP_WIDTH;
|
|
352
|
+
const height = UI_CONSTANTS.POPUP_HEIGHT;
|
|
353
|
+
|
|
354
|
+
// Calculate position near top-right corner (where extension icon typically is)
|
|
355
|
+
// Get the current window to determine screen bounds
|
|
356
|
+
let left = 100;
|
|
357
|
+
let top = 100;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const currentWindow = await chrome.windows.getCurrent();
|
|
361
|
+
if (currentWindow.left !== undefined && currentWindow.width !== undefined) {
|
|
362
|
+
// Position near top-right of current window
|
|
363
|
+
// Place it slightly inward from the edge for better UX
|
|
364
|
+
const marginFromRight = 20;
|
|
365
|
+
const marginFromTop = 80; // Below browser chrome/toolbar
|
|
366
|
+
|
|
367
|
+
left = currentWindow.left + currentWindow.width - width - marginFromRight;
|
|
368
|
+
top = currentWindow.top !== undefined ? currentWindow.top + marginFromTop : 80;
|
|
369
|
+
|
|
370
|
+
// Ensure it's not off-screen (minimum 0)
|
|
371
|
+
left = Math.max(0, left);
|
|
372
|
+
top = Math.max(0, top);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
// If we can't get current window, use safe default position
|
|
376
|
+
console.warn('Could not determine window position, using defaults');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const newWindow = await chrome.windows.create({
|
|
380
|
+
url: popupUrl,
|
|
381
|
+
type: 'popup',
|
|
382
|
+
width,
|
|
383
|
+
height,
|
|
384
|
+
left,
|
|
385
|
+
top,
|
|
386
|
+
focused: true,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
approvalWindowId = newWindow?.id ?? null;
|
|
390
|
+
} finally {
|
|
391
|
+
isCreatingWindow = false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Process next request in queue after current request is resolved
|
|
397
|
+
* Called when user approves or rejects a request
|
|
398
|
+
*/
|
|
399
|
+
function processNextRequest() {
|
|
400
|
+
if (currentRequestId !== null) {
|
|
401
|
+
cancelPendingRequest(currentRequestId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (requestQueue.length > 0) {
|
|
405
|
+
while (true) {
|
|
406
|
+
const next = requestQueue.shift()!;
|
|
407
|
+
if (!pendingRequests.has(next.id)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
createApprovalPopup(next.id, next.type);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Emit a wallet event to all tabs
|
|
418
|
+
* This notifies dApps of wallet state changes (account switches, network changes, etc.)
|
|
419
|
+
*/
|
|
420
|
+
async function emitWalletEvent(eventType: string, data: unknown) {
|
|
421
|
+
const tabs = await chrome.tabs.query({});
|
|
422
|
+
|
|
423
|
+
for (const tab of tabs) {
|
|
424
|
+
if (tab.id) {
|
|
425
|
+
try {
|
|
426
|
+
await chrome.tabs.sendMessage(tab.id, {
|
|
427
|
+
type: 'WALLET_EVENT',
|
|
428
|
+
eventType,
|
|
429
|
+
data,
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// Tab might not have content script, ignore
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Initialize auto-lock setting, load approved origins, vault state, connection monitoring, and schedule alarms.
|
|
439
|
+
// IMPORTANT: this promise is awaited by message and alarm handlers to prevent race conditions on SW start.
|
|
440
|
+
const initPromise = (async () => {
|
|
441
|
+
await wasmInitPromise;
|
|
442
|
+
const stored = (await chrome.storage.local.get([
|
|
443
|
+
STORAGE_KEYS.AUTO_LOCK_MINUTES,
|
|
444
|
+
STORAGE_KEYS.LAST_ACTIVITY,
|
|
445
|
+
STORAGE_KEYS.MANUALLY_LOCKED,
|
|
446
|
+
])) as Record<string, unknown>;
|
|
447
|
+
|
|
448
|
+
const storedMinutes = stored[STORAGE_KEYS.AUTO_LOCK_MINUTES];
|
|
449
|
+
autoLockMinutes = typeof storedMinutes === 'number' ? storedMinutes : Number(storedMinutes) || 0;
|
|
450
|
+
|
|
451
|
+
// Load persisted lastActivity (survives SW restarts), fallback to now if not set
|
|
452
|
+
const storedLastActivity = stored[STORAGE_KEYS.LAST_ACTIVITY];
|
|
453
|
+
lastActivity =
|
|
454
|
+
typeof storedLastActivity === 'number'
|
|
455
|
+
? storedLastActivity
|
|
456
|
+
: Number(storedLastActivity) || Date.now();
|
|
457
|
+
|
|
458
|
+
// Load persisted manuallyLocked state
|
|
459
|
+
manuallyLocked = Boolean(stored[STORAGE_KEYS.MANUALLY_LOCKED]);
|
|
460
|
+
|
|
461
|
+
await loadApprovedOrigins();
|
|
462
|
+
await vault.init(); // Load encrypted vault header to detect vault existence
|
|
463
|
+
await restoreUnlockSession(); // Rehydrate unlock state if still within auto-lock window
|
|
464
|
+
|
|
465
|
+
// Only schedule alarm if auto-lock is enabled, otherwise ensure any stale alarm is cleared
|
|
466
|
+
if (autoLockMinutes > 0) {
|
|
467
|
+
scheduleAlarm();
|
|
468
|
+
} else {
|
|
469
|
+
chrome.alarms.clear(ALARM_NAMES.AUTO_LOCK);
|
|
470
|
+
}
|
|
471
|
+
})();
|
|
472
|
+
|
|
473
|
+
// Clean up approval window ID when window is closed
|
|
474
|
+
chrome.windows.onRemoved.addListener(windowId => {
|
|
475
|
+
if (windowId === approvalWindowId) {
|
|
476
|
+
approvalWindowId = null;
|
|
477
|
+
// If user closed window, process next request in queue
|
|
478
|
+
processNextRequest();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Track user activity for auto-lock timer
|
|
484
|
+
* Only counts user-initiated actions, not passive polling
|
|
485
|
+
* Persists to storage so it survives service worker restarts
|
|
486
|
+
*/
|
|
487
|
+
async function touchActivity(method?: string) {
|
|
488
|
+
if (method && USER_ACTIVITY_METHODS.has(method as any)) {
|
|
489
|
+
lastActivity = Date.now();
|
|
490
|
+
// Persist to storage (await to ensure it's saved)
|
|
491
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.LAST_ACTIVITY]: lastActivity });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Check if message is from popup/extension page (not content script)
|
|
497
|
+
* Extension pages have chrome-extension:// URLs; content scripts have web URLs
|
|
498
|
+
*/
|
|
499
|
+
function isFromPopup(sender: chrome.runtime.MessageSender): boolean {
|
|
500
|
+
// Check if the sender URL is from our extension
|
|
501
|
+
const url = sender.url || '';
|
|
502
|
+
const extensionId = chrome.runtime.id;
|
|
503
|
+
return url.startsWith(`chrome-extension://${extensionId}/`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Handle messages from content script and popup
|
|
508
|
+
*/
|
|
509
|
+
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
510
|
+
(async () => {
|
|
511
|
+
await initPromise;
|
|
512
|
+
await ensureSessionRestored();
|
|
513
|
+
const { payload } = msg || {};
|
|
514
|
+
await touchActivity(payload?.method);
|
|
515
|
+
|
|
516
|
+
// Guard: internal methods (wallet:*) can only be called from popup/extension pages
|
|
517
|
+
if (payload?.method?.startsWith('wallet:') && !isFromPopup(_sender)) {
|
|
518
|
+
sendResponse({ error: ERROR_CODES.UNAUTHORIZED });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
switch (payload?.method) {
|
|
523
|
+
// Provider methods (called from injected provider via content script)
|
|
524
|
+
case PROVIDER_METHODS.CONNECT:
|
|
525
|
+
const connectOrigin = _sender.url || _sender.origin || '';
|
|
526
|
+
|
|
527
|
+
// Check if origin is already approved
|
|
528
|
+
if (!isOriginApproved(connectOrigin) || vault.isLocked()) {
|
|
529
|
+
// Clear any existing pending unlock requests from same origin to prevent duplicates
|
|
530
|
+
for (const [existingId, existingData] of pendingRequests.entries()) {
|
|
531
|
+
if (existingData.origin === connectOrigin) {
|
|
532
|
+
cancelPendingRequest(existingId);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Origin not approved - show connection approval popup
|
|
537
|
+
const connectRequestId = crypto.randomUUID();
|
|
538
|
+
const connectRequest: ConnectRequest = {
|
|
539
|
+
id: connectRequestId,
|
|
540
|
+
origin: connectOrigin,
|
|
541
|
+
timestamp: Date.now(),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Store pending request with response callback
|
|
545
|
+
pendingRequests.set(connectRequestId, {
|
|
546
|
+
request: connectRequest,
|
|
547
|
+
sendResponse,
|
|
548
|
+
origin: connectRequest.origin,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Create approval popup
|
|
552
|
+
await createApprovalPopup(connectRequestId, 'connect');
|
|
553
|
+
|
|
554
|
+
// Response will be sent when user approves/rejects
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Origin approved - return address
|
|
559
|
+
sendResponse({
|
|
560
|
+
pkh: vault.getAddress(),
|
|
561
|
+
grpcEndpoint: RPC_ENDPOINT,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Emit connect event when dApp connects successfully
|
|
565
|
+
await emitWalletEvent('connect', { chainId: 'nockchain-1' });
|
|
566
|
+
return;
|
|
567
|
+
|
|
568
|
+
case PROVIDER_METHODS.SIGN_MESSAGE:
|
|
569
|
+
// Validate origin
|
|
570
|
+
const signMessageOrigin = _sender.url || _sender.origin || '';
|
|
571
|
+
if (!isOriginApproved(signMessageOrigin)) {
|
|
572
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (vault.isLocked()) {
|
|
577
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Create sign message approval request
|
|
582
|
+
const newSignRequestId = crypto.randomUUID();
|
|
583
|
+
const signRequest: SignRequest = {
|
|
584
|
+
id: newSignRequestId,
|
|
585
|
+
origin: signMessageOrigin,
|
|
586
|
+
message: payload.params?.[0] || '',
|
|
587
|
+
timestamp: Date.now(),
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// Store pending request with response callback
|
|
591
|
+
pendingRequests.set(newSignRequestId, {
|
|
592
|
+
request: signRequest,
|
|
593
|
+
sendResponse,
|
|
594
|
+
origin: signRequest.origin,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Create approval popup
|
|
598
|
+
await createApprovalPopup(newSignRequestId, 'sign-message');
|
|
599
|
+
|
|
600
|
+
// Response will be sent when user approves/rejects
|
|
601
|
+
return;
|
|
602
|
+
|
|
603
|
+
case MIGRATE_V0_GET_STATUS: {
|
|
604
|
+
const origin = _sender.url || _sender.origin || '';
|
|
605
|
+
if (!isOriginApproved(origin)) {
|
|
606
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (vault.isLocked()) {
|
|
610
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
sendResponse({ ok: true, hasV0Mnemonic: vault.hasV0Mnemonic() });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
case MIGRATE_V0_SIGN_RAW_TX: {
|
|
618
|
+
const origin = _sender.url || _sender.origin || '';
|
|
619
|
+
if (!isOriginApproved(origin)) {
|
|
620
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (vault.isLocked()) {
|
|
624
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const rawTxParams = payload.params?.[0];
|
|
628
|
+
if (
|
|
629
|
+
!rawTxParams ||
|
|
630
|
+
!rawTxParams.rawTx ||
|
|
631
|
+
!rawTxParams.notes ||
|
|
632
|
+
!rawTxParams.spendConditions
|
|
633
|
+
) {
|
|
634
|
+
sendResponse({ error: { code: -32602, message: 'Invalid params' } });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const derivation = rawTxParams.derivation || 'master';
|
|
638
|
+
const outputs = await vault.computeOutputs(rawTxParams.rawTx);
|
|
639
|
+
|
|
640
|
+
const signRawTxId = crypto.randomUUID();
|
|
641
|
+
const signRawTxRequest: any = {
|
|
642
|
+
id: signRawTxId,
|
|
643
|
+
origin,
|
|
644
|
+
rawTx: rawTxParams.rawTx,
|
|
645
|
+
notes: rawTxParams.notes,
|
|
646
|
+
spendConditions: rawTxParams.spendConditions,
|
|
647
|
+
outputs: outputs,
|
|
648
|
+
timestamp: Date.now(),
|
|
649
|
+
signWith: 'v0',
|
|
650
|
+
v0Derivation: derivation,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
pendingRequests.set(signRawTxId, {
|
|
654
|
+
request: signRawTxRequest,
|
|
655
|
+
sendResponse,
|
|
656
|
+
origin: signRawTxRequest.origin,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
await createApprovalPopup(signRawTxId, 'sign-raw-tx');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
case PROVIDER_METHODS.SIGN_RAW_TX:
|
|
664
|
+
// Validate origin
|
|
665
|
+
const signRawTxOrigin = _sender.url || _sender.origin || '';
|
|
666
|
+
if (!isOriginApproved(signRawTxOrigin)) {
|
|
667
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (vault.isLocked()) {
|
|
672
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const rawTxParams = payload.params?.[0];
|
|
677
|
+
if (
|
|
678
|
+
!rawTxParams ||
|
|
679
|
+
!rawTxParams.rawTx ||
|
|
680
|
+
!rawTxParams.notes ||
|
|
681
|
+
!rawTxParams.spendConditions
|
|
682
|
+
) {
|
|
683
|
+
sendResponse({ error: { code: -32602, message: 'Invalid params' } });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const outputs = await vault.computeOutputs(rawTxParams.rawTx);
|
|
688
|
+
|
|
689
|
+
// Create sign raw tx approval request
|
|
690
|
+
const signRawTxId = crypto.randomUUID();
|
|
691
|
+
const signRawTxRequest: SignRawTxRequest = {
|
|
692
|
+
id: signRawTxId,
|
|
693
|
+
origin: signRawTxOrigin,
|
|
694
|
+
rawTx: rawTxParams.rawTx,
|
|
695
|
+
notes: rawTxParams.notes,
|
|
696
|
+
spendConditions: rawTxParams.spendConditions,
|
|
697
|
+
outputs: outputs,
|
|
698
|
+
timestamp: Date.now(),
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Store pending request with response callback
|
|
702
|
+
pendingRequests.set(signRawTxId, {
|
|
703
|
+
request: signRawTxRequest,
|
|
704
|
+
sendResponse,
|
|
705
|
+
origin: signRawTxRequest.origin,
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Create approval popup
|
|
709
|
+
await createApprovalPopup(signRawTxId, 'sign-raw-tx');
|
|
710
|
+
|
|
711
|
+
// Response will be sent when user approves/rejects
|
|
712
|
+
return;
|
|
713
|
+
|
|
714
|
+
case PROVIDER_METHODS.SEND_TRANSACTION:
|
|
715
|
+
// Validate origin
|
|
716
|
+
const sendTxOrigin = _sender.url || _sender.origin || '';
|
|
717
|
+
if (!isOriginApproved(sendTxOrigin)) {
|
|
718
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (vault.isLocked()) {
|
|
723
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const { to, amount, fee } = payload.params?.[0] ?? {};
|
|
727
|
+
if (!isNockAddress(to)) {
|
|
728
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Create transaction approval request
|
|
733
|
+
const txRequestId = crypto.randomUUID();
|
|
734
|
+
const txRequest: TransactionRequest = {
|
|
735
|
+
id: txRequestId,
|
|
736
|
+
origin: sendTxOrigin,
|
|
737
|
+
to,
|
|
738
|
+
amount,
|
|
739
|
+
fee,
|
|
740
|
+
timestamp: Date.now(),
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// Store pending request with response callback
|
|
744
|
+
pendingRequests.set(txRequestId, {
|
|
745
|
+
request: txRequest,
|
|
746
|
+
sendResponse,
|
|
747
|
+
origin: txRequest.origin,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Create approval popup
|
|
751
|
+
await createApprovalPopup(txRequestId, 'transaction');
|
|
752
|
+
|
|
753
|
+
// Response will be sent when user approves/rejects
|
|
754
|
+
return;
|
|
755
|
+
|
|
756
|
+
case PROVIDER_METHODS.GET_WALLET_INFO:
|
|
757
|
+
// Validate origin
|
|
758
|
+
const getInfoOrigin = _sender.url || _sender.origin || '';
|
|
759
|
+
if (!isOriginApproved(getInfoOrigin)) {
|
|
760
|
+
sendResponse({ error: { code: 4100, message: 'Unauthorized origin' } });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (vault.isLocked()) {
|
|
765
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
sendResponse({
|
|
770
|
+
pkh: vault.getAddress(),
|
|
771
|
+
grpcEndpoint: RPC_ENDPOINT,
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
|
|
775
|
+
// Internal methods (called from popup)
|
|
776
|
+
case INTERNAL_METHODS.SET_AUTO_LOCK:
|
|
777
|
+
const newMinutes = payload.params?.[0];
|
|
778
|
+
autoLockMinutes = typeof newMinutes === 'number' ? newMinutes : Number(newMinutes) || 0;
|
|
779
|
+
|
|
780
|
+
await chrome.storage.local.set({
|
|
781
|
+
[STORAGE_KEYS.AUTO_LOCK_MINUTES]: autoLockMinutes,
|
|
782
|
+
});
|
|
783
|
+
// Start or stop alarm based on setting
|
|
784
|
+
if (autoLockMinutes > 0) {
|
|
785
|
+
// Reset activity timestamp when enabling auto-lock
|
|
786
|
+
lastActivity = Date.now();
|
|
787
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.LAST_ACTIVITY]: lastActivity });
|
|
788
|
+
scheduleAlarm();
|
|
789
|
+
} else {
|
|
790
|
+
// Clear alarm when disabling auto-lock
|
|
791
|
+
chrome.alarms.clear(ALARM_NAMES.AUTO_LOCK);
|
|
792
|
+
}
|
|
793
|
+
sendResponse({ ok: true });
|
|
794
|
+
return;
|
|
795
|
+
|
|
796
|
+
case INTERNAL_METHODS.UNLOCK:
|
|
797
|
+
const unlockResult = await vault.unlock(payload.params?.[0]); // password
|
|
798
|
+
sendResponse(unlockResult);
|
|
799
|
+
|
|
800
|
+
// Emit connect event when unlock succeeds
|
|
801
|
+
if ('ok' in unlockResult && unlockResult.ok) {
|
|
802
|
+
// Clear manual lock flag when successfully unlocked
|
|
803
|
+
manuallyLocked = false;
|
|
804
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.MANUALLY_LOCKED]: false });
|
|
805
|
+
await persistUnlockSession();
|
|
806
|
+
await emitWalletEvent('connect', { chainId: 'nockchain-1' });
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
|
|
810
|
+
case INTERNAL_METHODS.LOCK:
|
|
811
|
+
// Set manual lock flag - user explicitly locked, don't auto-unlock
|
|
812
|
+
manuallyLocked = true;
|
|
813
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.MANUALLY_LOCKED]: true });
|
|
814
|
+
await vault.lock();
|
|
815
|
+
await clearUnlockSessionCache();
|
|
816
|
+
sendResponse({ ok: true });
|
|
817
|
+
|
|
818
|
+
// Emit disconnect event when wallet locks
|
|
819
|
+
await emitWalletEvent('disconnect', { code: 1013, message: 'Wallet locked' });
|
|
820
|
+
return;
|
|
821
|
+
|
|
822
|
+
case INTERNAL_METHODS.RESET_WALLET:
|
|
823
|
+
// Reset the wallet completely - clears all data
|
|
824
|
+
await vault.reset();
|
|
825
|
+
await clearUnlockSessionCache();
|
|
826
|
+
manuallyLocked = false;
|
|
827
|
+
sendResponse({ ok: true });
|
|
828
|
+
|
|
829
|
+
// Emit disconnect event
|
|
830
|
+
await emitWalletEvent('disconnect', { code: 1013, message: 'Wallet reset' });
|
|
831
|
+
return;
|
|
832
|
+
|
|
833
|
+
case INTERNAL_METHODS.SETUP:
|
|
834
|
+
// params: password, mnemonic (optional). If no mnemonic, generates one automatically.
|
|
835
|
+
const setupResult = await vault.setup(
|
|
836
|
+
payload.params?.[0],
|
|
837
|
+
payload.params?.[1],
|
|
838
|
+
payload.params?.[2]
|
|
839
|
+
);
|
|
840
|
+
sendResponse(setupResult);
|
|
841
|
+
|
|
842
|
+
if ('ok' in setupResult && setupResult.ok) {
|
|
843
|
+
manuallyLocked = false;
|
|
844
|
+
await chrome.storage.local.set({ [STORAGE_KEYS.MANUALLY_LOCKED]: false });
|
|
845
|
+
await persistUnlockSession();
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
|
|
849
|
+
case INTERNAL_METHODS.GET_STATE:
|
|
850
|
+
// Initialize vault state from storage before checking status
|
|
851
|
+
// This ensures hasVault is accurate even after service worker restart
|
|
852
|
+
await vault.init();
|
|
853
|
+
|
|
854
|
+
const uiStatus = vault.getUiStatus();
|
|
855
|
+
sendResponse({
|
|
856
|
+
locked: uiStatus.locked,
|
|
857
|
+
hasVault: uiStatus.hasVault,
|
|
858
|
+
address: await vault.getAddressSafe(),
|
|
859
|
+
accounts: vault.getAccounts(),
|
|
860
|
+
currentAccount: vault.getCurrentAccount(),
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
|
|
864
|
+
case INTERNAL_METHODS.GET_ACCOUNTS:
|
|
865
|
+
sendResponse({
|
|
866
|
+
accounts: vault.getAccounts(),
|
|
867
|
+
currentAccount: vault.getCurrentAccount(),
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
|
|
871
|
+
case INTERNAL_METHODS.SWITCH_ACCOUNT:
|
|
872
|
+
const switchResult = await vault.switchAccount(payload.params?.[0]);
|
|
873
|
+
sendResponse(switchResult);
|
|
874
|
+
|
|
875
|
+
// Emit accountsChanged event to all tabs if successful
|
|
876
|
+
if ('ok' in switchResult && switchResult.ok) {
|
|
877
|
+
await emitWalletEvent('accountsChanged', [switchResult.account.address]);
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
|
|
881
|
+
case INTERNAL_METHODS.RENAME_ACCOUNT:
|
|
882
|
+
sendResponse(await vault.renameAccount(payload.params?.[0], payload.params?.[1]));
|
|
883
|
+
return;
|
|
884
|
+
|
|
885
|
+
case INTERNAL_METHODS.UPDATE_ACCOUNT_STYLING:
|
|
886
|
+
sendResponse(
|
|
887
|
+
await vault.updateAccountStyling(
|
|
888
|
+
payload.params?.[0],
|
|
889
|
+
payload.params?.[1],
|
|
890
|
+
payload.params?.[2]
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
return;
|
|
894
|
+
|
|
895
|
+
case INTERNAL_METHODS.HIDE_ACCOUNT:
|
|
896
|
+
// params: [accountIndex]
|
|
897
|
+
const hideResult = await vault.hideAccount(payload.params?.[0]);
|
|
898
|
+
sendResponse(hideResult);
|
|
899
|
+
|
|
900
|
+
// Emit accountsChanged event to all tabs if successful
|
|
901
|
+
if ('ok' in hideResult && hideResult.ok) {
|
|
902
|
+
const currentAccount = vault.getCurrentAccount();
|
|
903
|
+
if (currentAccount) {
|
|
904
|
+
await emitWalletEvent('accountsChanged', [currentAccount.address]);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
|
|
909
|
+
case INTERNAL_METHODS.CREATE_ACCOUNT:
|
|
910
|
+
// params: name (optional)
|
|
911
|
+
const createResult = await vault.createAccount(payload.params?.[0]);
|
|
912
|
+
sendResponse(createResult);
|
|
913
|
+
|
|
914
|
+
// Emit accountsChanged event to all tabs if successful
|
|
915
|
+
// New account is automatically set as current
|
|
916
|
+
if ('ok' in createResult && createResult.ok) {
|
|
917
|
+
await emitWalletEvent('accountsChanged', [createResult.account.address]);
|
|
918
|
+
}
|
|
919
|
+
return;
|
|
920
|
+
|
|
921
|
+
case INTERNAL_METHODS.HAS_V0_MNEMONIC:
|
|
922
|
+
if (vault.isLocked()) {
|
|
923
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
sendResponse({ ok: true, has: vault.hasV0Mnemonic() });
|
|
927
|
+
return;
|
|
928
|
+
|
|
929
|
+
case INTERNAL_METHODS.CLEAR_V0_MNEMONIC: {
|
|
930
|
+
if (vault.isLocked()) {
|
|
931
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const res = (await vault.clearMnemonicV0()) as { ok: true } | { error: string };
|
|
935
|
+
sendResponse(res);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
case INTERNAL_METHODS.SET_V0_MNEMONIC:
|
|
940
|
+
if (vault.isLocked()) {
|
|
941
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const res = await vault.setMnemonicV0(payload.params?.[0]);
|
|
945
|
+
sendResponse(res);
|
|
946
|
+
return;
|
|
947
|
+
case INTERNAL_METHODS.GET_MNEMONIC:
|
|
948
|
+
// params: password (required for verification)
|
|
949
|
+
sendResponse(await vault.getMnemonic(payload.params?.[0]));
|
|
950
|
+
return;
|
|
951
|
+
|
|
952
|
+
case INTERNAL_METHODS.GET_AUTO_LOCK:
|
|
953
|
+
sendResponse({ minutes: autoLockMinutes });
|
|
954
|
+
return;
|
|
955
|
+
|
|
956
|
+
case INTERNAL_METHODS.REPORT_ACTIVITY:
|
|
957
|
+
// Just acknowledge - activity tracking already handled above
|
|
958
|
+
sendResponse({ ok: true });
|
|
959
|
+
return;
|
|
960
|
+
|
|
961
|
+
case INTERNAL_METHODS.GET_BALANCE_FROM_STORE:
|
|
962
|
+
// Get balance from UTXO store - excludes in-flight notes
|
|
963
|
+
// Optional param: account address. If not provided, uses current account.
|
|
964
|
+
const balanceAccountAddress = payload.params?.[0] || vault.getCurrentAccount()?.address;
|
|
965
|
+
if (!balanceAccountAddress) {
|
|
966
|
+
sendResponse({ error: 'No account selected' });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
const storeBalance = await vault.getBalanceFromStore(balanceAccountAddress);
|
|
971
|
+
sendResponse(storeBalance);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
console.error('[Background] Error getting balance from store:', err);
|
|
974
|
+
sendResponse({ error: 'Failed to get balance from store' });
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
|
|
978
|
+
case INTERNAL_METHODS.GET_CONNECTION_STATUS:
|
|
979
|
+
sendResponse({ connected: isRpcConnected });
|
|
980
|
+
return;
|
|
981
|
+
|
|
982
|
+
case INTERNAL_METHODS.REPORT_RPC_STATUS:
|
|
983
|
+
// Popup reports actual gRPC call success/failure
|
|
984
|
+
const rpcHealthy = payload.params?.[0] as boolean;
|
|
985
|
+
if (typeof rpcHealthy === 'boolean' && rpcHealthy !== isRpcConnected) {
|
|
986
|
+
isRpcConnected = rpcHealthy;
|
|
987
|
+
}
|
|
988
|
+
sendResponse({ ok: true });
|
|
989
|
+
return;
|
|
990
|
+
|
|
991
|
+
// Note: GET_WALLET_TRANSACTIONS is called directly from popup context
|
|
992
|
+
// to avoid service worker limitations with dynamic imports
|
|
993
|
+
|
|
994
|
+
// Approval request handlers
|
|
995
|
+
case INTERNAL_METHODS.GET_PENDING_TRANSACTION:
|
|
996
|
+
const getPendingTxId = payload.params?.[0];
|
|
997
|
+
const txPending = pendingRequests.get(getPendingTxId);
|
|
998
|
+
if (txPending && isTransactionRequest(txPending.request)) {
|
|
999
|
+
sendResponse(txPending.request);
|
|
1000
|
+
} else {
|
|
1001
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
|
|
1005
|
+
case INTERNAL_METHODS.GET_PENDING_SIGN_REQUEST:
|
|
1006
|
+
const getPendingSignId = payload.params?.[0];
|
|
1007
|
+
const signPending = pendingRequests.get(getPendingSignId);
|
|
1008
|
+
if (signPending && isSignRequest(signPending.request)) {
|
|
1009
|
+
sendResponse(signPending.request);
|
|
1010
|
+
} else {
|
|
1011
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1012
|
+
}
|
|
1013
|
+
return;
|
|
1014
|
+
|
|
1015
|
+
case INTERNAL_METHODS.GET_PENDING_SIGN_RAW_TX_REQUEST:
|
|
1016
|
+
const getPendingSignRawTxId = payload.params?.[0];
|
|
1017
|
+
const signRawTxPending = pendingRequests.get(getPendingSignRawTxId);
|
|
1018
|
+
if (signRawTxPending && isSignRawTxRequest(signRawTxPending.request)) {
|
|
1019
|
+
sendResponse(signRawTxPending.request);
|
|
1020
|
+
} else {
|
|
1021
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
|
|
1025
|
+
case INTERNAL_METHODS.APPROVE_TRANSACTION:
|
|
1026
|
+
const approveTxId = payload.params?.[0];
|
|
1027
|
+
const approveTxPending = pendingRequests.get(approveTxId);
|
|
1028
|
+
if (approveTxPending && isTransactionRequest(approveTxPending.request)) {
|
|
1029
|
+
const txRequest = approveTxPending.request;
|
|
1030
|
+
|
|
1031
|
+
// Check if request has expired (replay prevention)
|
|
1032
|
+
if (isRequestExpired(txRequest.timestamp)) {
|
|
1033
|
+
cancelPendingRequest(approveTxId, 4003, 'Request expired');
|
|
1034
|
+
sendResponse({ error: 'Request expired' });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
// Sign the transaction using the vault
|
|
1040
|
+
const txIdHex = await vault.signTransaction(
|
|
1041
|
+
txRequest.to,
|
|
1042
|
+
txRequest.amount,
|
|
1043
|
+
txRequest.fee
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
approveTxPending.sendResponse({
|
|
1047
|
+
txid: txIdHex,
|
|
1048
|
+
amount: txRequest.amount,
|
|
1049
|
+
fee: txRequest.fee,
|
|
1050
|
+
});
|
|
1051
|
+
cancelPendingRequest(approveTxId);
|
|
1052
|
+
processNextRequest();
|
|
1053
|
+
sendResponse({ success: true });
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
console.error('Transaction signing failed:', error);
|
|
1056
|
+
approveTxPending.sendResponse({
|
|
1057
|
+
error: {
|
|
1058
|
+
code: 4900,
|
|
1059
|
+
message: error instanceof Error ? error.message : 'Transaction signing failed',
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
cancelPendingRequest(approveTxId);
|
|
1063
|
+
processNextRequest();
|
|
1064
|
+
sendResponse({
|
|
1065
|
+
error: error instanceof Error ? error.message : 'Transaction signing failed',
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
} else {
|
|
1069
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1070
|
+
}
|
|
1071
|
+
return;
|
|
1072
|
+
|
|
1073
|
+
case INTERNAL_METHODS.REJECT_TRANSACTION:
|
|
1074
|
+
const rejectTxId = payload.params?.[0];
|
|
1075
|
+
const rejectTxPending = pendingRequests.get(rejectTxId);
|
|
1076
|
+
if (rejectTxPending) {
|
|
1077
|
+
cancelPendingRequest(rejectTxId, 4001, 'User rejected the transaction');
|
|
1078
|
+
processNextRequest();
|
|
1079
|
+
sendResponse({ success: true });
|
|
1080
|
+
} else {
|
|
1081
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1082
|
+
}
|
|
1083
|
+
return;
|
|
1084
|
+
|
|
1085
|
+
case INTERNAL_METHODS.APPROVE_SIGN_MESSAGE:
|
|
1086
|
+
const approveSignId = payload.params?.[0];
|
|
1087
|
+
const approveSignPending = pendingRequests.get(approveSignId);
|
|
1088
|
+
if (approveSignPending && isSignRequest(approveSignPending.request)) {
|
|
1089
|
+
const signRequest = approveSignPending.request;
|
|
1090
|
+
|
|
1091
|
+
// Check if request has expired (replay prevention)
|
|
1092
|
+
if (isRequestExpired(signRequest.timestamp)) {
|
|
1093
|
+
cancelPendingRequest(approveSignId, 4003, 'Request expired');
|
|
1094
|
+
sendResponse({ error: 'Request expired' });
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
try {
|
|
1099
|
+
const { signature, publicKeyHex } = await vault.signMessage([signRequest.message]);
|
|
1100
|
+
approveSignPending.sendResponse({ signature, publicKeyHex });
|
|
1101
|
+
cancelPendingRequest(approveSignId);
|
|
1102
|
+
processNextRequest();
|
|
1103
|
+
sendResponse({ success: true });
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to sign message';
|
|
1106
|
+
cancelPendingRequest(approveSignId, 4001, errorMessage);
|
|
1107
|
+
processNextRequest();
|
|
1108
|
+
sendResponse({ error: errorMessage });
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1112
|
+
}
|
|
1113
|
+
return;
|
|
1114
|
+
|
|
1115
|
+
case INTERNAL_METHODS.REJECT_SIGN_MESSAGE:
|
|
1116
|
+
const rejectSignId = payload.params?.[0];
|
|
1117
|
+
const rejectSignPending = pendingRequests.get(rejectSignId);
|
|
1118
|
+
if (rejectSignPending) {
|
|
1119
|
+
cancelPendingRequest(rejectSignId, 4001, 'User rejected the signature request');
|
|
1120
|
+
processNextRequest();
|
|
1121
|
+
sendResponse({ success: true });
|
|
1122
|
+
} else {
|
|
1123
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
|
|
1127
|
+
case INTERNAL_METHODS.APPROVE_SIGN_RAW_TX:
|
|
1128
|
+
const approveSignRawTxId = payload.params?.[0];
|
|
1129
|
+
const approveSignRawTxPending = pendingRequests.get(approveSignRawTxId);
|
|
1130
|
+
|
|
1131
|
+
if (approveSignRawTxPending && isSignRawTxRequest(approveSignRawTxPending.request)) {
|
|
1132
|
+
const signRawTxRequest = approveSignRawTxPending.request;
|
|
1133
|
+
|
|
1134
|
+
// Check if request has expired (replay prevention)
|
|
1135
|
+
if (isRequestExpired(signRawTxRequest.timestamp)) {
|
|
1136
|
+
cancelPendingRequest(approveSignRawTxId, 4003, 'Request expired');
|
|
1137
|
+
sendResponse({ error: 'Request expired' });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
const signature =
|
|
1143
|
+
signRawTxRequest.signWith === 'v0'
|
|
1144
|
+
? await vault.signRawTxV0({
|
|
1145
|
+
rawTx: signRawTxRequest.rawTx,
|
|
1146
|
+
notes: signRawTxRequest.notes,
|
|
1147
|
+
spendConditions: signRawTxRequest.spendConditions,
|
|
1148
|
+
derivation: signRawTxRequest.v0Derivation || 'master',
|
|
1149
|
+
})
|
|
1150
|
+
: await vault.signRawTx({
|
|
1151
|
+
rawTx: signRawTxRequest.rawTx,
|
|
1152
|
+
notes: signRawTxRequest.notes,
|
|
1153
|
+
spendConditions: signRawTxRequest.spendConditions,
|
|
1154
|
+
});
|
|
1155
|
+
approveSignRawTxPending.sendResponse(signature);
|
|
1156
|
+
cancelPendingRequest(approveSignRawTxId);
|
|
1157
|
+
processNextRequest();
|
|
1158
|
+
sendResponse({ success: true });
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
console.error('Failed to sign raw transaction:', err);
|
|
1161
|
+
const errorMessage =
|
|
1162
|
+
err instanceof Error ? err.message : 'Failed to sign raw transaction';
|
|
1163
|
+
cancelPendingRequest(approveSignRawTxId, 4001, errorMessage);
|
|
1164
|
+
processNextRequest();
|
|
1165
|
+
sendResponse({ error: errorMessage });
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1169
|
+
}
|
|
1170
|
+
return;
|
|
1171
|
+
|
|
1172
|
+
case INTERNAL_METHODS.REJECT_SIGN_RAW_TX:
|
|
1173
|
+
const rejectSignRawTxId = payload.params?.[0];
|
|
1174
|
+
const rejectSignRawTxPending = pendingRequests.get(rejectSignRawTxId);
|
|
1175
|
+
if (rejectSignRawTxPending) {
|
|
1176
|
+
cancelPendingRequest(rejectSignRawTxId, 4001, 'User rejected the signature request');
|
|
1177
|
+
processNextRequest();
|
|
1178
|
+
sendResponse({ success: true });
|
|
1179
|
+
} else {
|
|
1180
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
|
|
1184
|
+
case INTERNAL_METHODS.GET_PENDING_CONNECTION:
|
|
1185
|
+
const getPendingConnectId = payload.params?.[0];
|
|
1186
|
+
const connectPending = pendingRequests.get(getPendingConnectId);
|
|
1187
|
+
if (connectPending && isConnectRequest(connectPending.request)) {
|
|
1188
|
+
sendResponse(connectPending.request);
|
|
1189
|
+
} else {
|
|
1190
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1191
|
+
}
|
|
1192
|
+
return;
|
|
1193
|
+
|
|
1194
|
+
case INTERNAL_METHODS.APPROVE_CONNECTION:
|
|
1195
|
+
const approveConnectId = payload.params?.[0];
|
|
1196
|
+
const approveConnectPending = pendingRequests.get(approveConnectId);
|
|
1197
|
+
if (approveConnectPending && isConnectRequest(approveConnectPending.request)) {
|
|
1198
|
+
const connectRequest = approveConnectPending.request;
|
|
1199
|
+
|
|
1200
|
+
// Check if request has expired (replay prevention)
|
|
1201
|
+
if (isRequestExpired(connectRequest.timestamp)) {
|
|
1202
|
+
cancelPendingRequest(approveConnectId, 4003, 'Request expired');
|
|
1203
|
+
sendResponse({ error: 'Request expired' });
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Add origin to approved list
|
|
1208
|
+
await approveOrigin(connectRequest.origin);
|
|
1209
|
+
|
|
1210
|
+
// Return wallet info
|
|
1211
|
+
approveConnectPending.sendResponse({
|
|
1212
|
+
pkh: vault.getAddress(),
|
|
1213
|
+
grpcEndpoint: RPC_ENDPOINT,
|
|
1214
|
+
});
|
|
1215
|
+
cancelPendingRequest(approveConnectId);
|
|
1216
|
+
processNextRequest();
|
|
1217
|
+
sendResponse({ success: true });
|
|
1218
|
+
|
|
1219
|
+
// Emit connect event
|
|
1220
|
+
await emitWalletEvent('connect', { chainId: 'nockchain-1' });
|
|
1221
|
+
} else {
|
|
1222
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
|
|
1226
|
+
case INTERNAL_METHODS.REJECT_CONNECTION:
|
|
1227
|
+
const rejectConnectId = payload.params?.[0];
|
|
1228
|
+
const rejectConnectPending = pendingRequests.get(rejectConnectId);
|
|
1229
|
+
if (rejectConnectPending) {
|
|
1230
|
+
cancelPendingRequest(rejectConnectId, 4001, 'User rejected the connection');
|
|
1231
|
+
processNextRequest();
|
|
1232
|
+
sendResponse({ success: true });
|
|
1233
|
+
} else {
|
|
1234
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1235
|
+
}
|
|
1236
|
+
return;
|
|
1237
|
+
|
|
1238
|
+
case INTERNAL_METHODS.GET_PENDING_RAW_TX_REQUEST:
|
|
1239
|
+
const getPendingRawTxId = payload.params?.[0];
|
|
1240
|
+
const rawTxPending = pendingRequests.get(getPendingRawTxId);
|
|
1241
|
+
|
|
1242
|
+
if (rawTxPending && isSignRawTxRequest(rawTxPending.request)) {
|
|
1243
|
+
sendResponse(rawTxPending.request);
|
|
1244
|
+
} else {
|
|
1245
|
+
sendResponse({ error: ERROR_CODES.NOT_FOUND });
|
|
1246
|
+
}
|
|
1247
|
+
return;
|
|
1248
|
+
|
|
1249
|
+
case INTERNAL_METHODS.REVOKE_ORIGIN:
|
|
1250
|
+
const revokeOriginParam = payload.params?.[0];
|
|
1251
|
+
if (
|
|
1252
|
+
revokeOriginParam &&
|
|
1253
|
+
typeof revokeOriginParam === 'object' &&
|
|
1254
|
+
'origin' in revokeOriginParam
|
|
1255
|
+
) {
|
|
1256
|
+
await revokeOrigin(revokeOriginParam.origin as string);
|
|
1257
|
+
sendResponse({ success: true });
|
|
1258
|
+
} else {
|
|
1259
|
+
sendResponse({ error: ERROR_CODES.INVALID_PARAMS });
|
|
1260
|
+
}
|
|
1261
|
+
return;
|
|
1262
|
+
|
|
1263
|
+
case INTERNAL_METHODS.SIGN_TRANSACTION:
|
|
1264
|
+
// params: [to, amount, fee]
|
|
1265
|
+
// Called from popup Send screen (not dApp transactions)
|
|
1266
|
+
if (vault.isLocked()) {
|
|
1267
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const [signTo, signAmount, signFee] = payload.params || [];
|
|
1272
|
+
if (!isNockAddress(signTo)) {
|
|
1273
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
try {
|
|
1278
|
+
const txid = await vault.signTransaction(signTo, signAmount, signFee);
|
|
1279
|
+
sendResponse({ txid });
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
console.error('[Background] Transaction signing failed:', error);
|
|
1282
|
+
sendResponse({
|
|
1283
|
+
error: error instanceof Error ? error.message : 'Transaction signing failed',
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return;
|
|
1287
|
+
|
|
1288
|
+
case INTERNAL_METHODS.ESTIMATE_TRANSACTION_FEE:
|
|
1289
|
+
// params: [to, amount] - amount in nicks
|
|
1290
|
+
if (vault.isLocked()) {
|
|
1291
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const [estimateTo, estimateAmount] = payload.params || [];
|
|
1296
|
+
if (!isNockAddress(estimateTo)) {
|
|
1297
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (typeof estimateAmount !== 'number' || estimateAmount <= 0) {
|
|
1302
|
+
sendResponse({ error: 'Invalid amount' });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
const result = await vault.estimateTransactionFee(estimateTo, estimateAmount);
|
|
1308
|
+
|
|
1309
|
+
if ('error' in result) {
|
|
1310
|
+
sendResponse({ error: result.error });
|
|
1311
|
+
} else {
|
|
1312
|
+
sendResponse({ fee: result.fee });
|
|
1313
|
+
}
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
console.error('[Background] Fee estimation error:', error);
|
|
1316
|
+
sendResponse({
|
|
1317
|
+
error: error instanceof Error ? error.message : 'Fee estimation failed',
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
return;
|
|
1321
|
+
|
|
1322
|
+
case INTERNAL_METHODS.ESTIMATE_MAX_SEND:
|
|
1323
|
+
// params: [to] - estimates max sendable amount for "send max" feature
|
|
1324
|
+
if (vault.isLocked()) {
|
|
1325
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const [maxSendTo] = payload.params || [];
|
|
1330
|
+
if (!isNockAddress(maxSendTo)) {
|
|
1331
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
try {
|
|
1336
|
+
const maxResult = await vault.estimateMaxSendAmount(maxSendTo);
|
|
1337
|
+
|
|
1338
|
+
if ('error' in maxResult) {
|
|
1339
|
+
sendResponse({ error: maxResult.error });
|
|
1340
|
+
} else {
|
|
1341
|
+
sendResponse({
|
|
1342
|
+
maxAmount: maxResult.maxAmount,
|
|
1343
|
+
fee: maxResult.fee,
|
|
1344
|
+
totalAvailable: maxResult.totalAvailable,
|
|
1345
|
+
utxoCount: maxResult.utxoCount,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
console.error('[Background] Max send estimation error:', error);
|
|
1350
|
+
sendResponse({
|
|
1351
|
+
error: error instanceof Error ? error.message : 'Max send estimation failed',
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
return;
|
|
1355
|
+
|
|
1356
|
+
case INTERNAL_METHODS.SEND_TRANSACTION_V2:
|
|
1357
|
+
// params: [to, amount, fee?, sendMax?, priceUsdAtTime?] - amount and fee in nicks
|
|
1358
|
+
// Uses UTXO store for proper note locking and successive transaction support
|
|
1359
|
+
// sendMax: if true, uses all available UTXOs and sets refundPKH = recipient for sweep
|
|
1360
|
+
// priceUsdAtTime: USD price per NOCK at time of transaction (for historical display)
|
|
1361
|
+
if (vault.isLocked()) {
|
|
1362
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const [sendToV2, sendAmountV2, sendFeeV2, sendMaxV2, priceUsdAtTimeV2] =
|
|
1367
|
+
payload.params || [];
|
|
1368
|
+
if (!isNockAddress(sendToV2)) {
|
|
1369
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (typeof sendAmountV2 !== 'number' || sendAmountV2 <= 0) {
|
|
1374
|
+
sendResponse({ error: 'Invalid amount' });
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
try {
|
|
1379
|
+
const v2Result = await vault.sendTransactionV2(
|
|
1380
|
+
sendToV2,
|
|
1381
|
+
sendAmountV2,
|
|
1382
|
+
sendFeeV2, // optional, can be undefined
|
|
1383
|
+
sendMaxV2, // optional, sweep all UTXOs to recipient
|
|
1384
|
+
priceUsdAtTimeV2 // optional, USD price at time of tx
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
if ('error' in v2Result) {
|
|
1388
|
+
sendResponse({ error: v2Result.error });
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
sendResponse({
|
|
1393
|
+
txid: v2Result.txId,
|
|
1394
|
+
broadcasted: v2Result.broadcasted,
|
|
1395
|
+
walletTx: v2Result.walletTx,
|
|
1396
|
+
});
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
console.error('[Background] SendTransactionV2 failed:', error);
|
|
1399
|
+
sendResponse({
|
|
1400
|
+
error: error instanceof Error ? error.message : 'Transaction failed',
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return;
|
|
1404
|
+
|
|
1405
|
+
case INTERNAL_METHODS.SEND_TRANSACTION:
|
|
1406
|
+
// params: [to, amount, fee] - amount and fee in nicks
|
|
1407
|
+
// Called from popup Send screen - builds, signs, and broadcasts transaction
|
|
1408
|
+
if (vault.isLocked()) {
|
|
1409
|
+
sendResponse({ error: ERROR_CODES.LOCKED });
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const [sendTo, sendAmount, sendFee] = payload.params || [];
|
|
1414
|
+
if (!isNockAddress(sendTo)) {
|
|
1415
|
+
sendResponse({ error: ERROR_CODES.BAD_ADDRESS });
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (typeof sendAmount !== 'number' || sendAmount <= 0) {
|
|
1420
|
+
sendResponse({ error: 'Invalid amount' });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (typeof sendFee !== 'number' || sendFee < 0) {
|
|
1425
|
+
sendResponse({ error: 'Invalid fee' });
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
try {
|
|
1430
|
+
const result = await vault.sendTransaction(sendTo, sendAmount, sendFee);
|
|
1431
|
+
|
|
1432
|
+
if ('error' in result) {
|
|
1433
|
+
sendResponse({ error: result.error });
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
sendResponse({
|
|
1438
|
+
txid: result.txId,
|
|
1439
|
+
broadcasted: result.broadcasted,
|
|
1440
|
+
protobufTx: result.protobufTx, // For dev/debugging - export to file
|
|
1441
|
+
});
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
console.error('[Background] Transaction sending failed:', error);
|
|
1444
|
+
sendResponse({
|
|
1445
|
+
error: error instanceof Error ? error.message : 'Transaction sending failed',
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
return;
|
|
1449
|
+
|
|
1450
|
+
default:
|
|
1451
|
+
sendResponse({ error: ERROR_CODES.METHOD_NOT_SUPPORTED });
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
})();
|
|
1455
|
+
// Required: tells Chrome we'll call sendResponse asynchronously from the IIFE
|
|
1456
|
+
return true;
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Handle auto-lock alarm
|
|
1461
|
+
*/
|
|
1462
|
+
chrome.alarms.onAlarm.addListener(async alarm => {
|
|
1463
|
+
if (alarm.name !== ALARM_NAMES.AUTO_LOCK) return;
|
|
1464
|
+
|
|
1465
|
+
await initPromise;
|
|
1466
|
+
await ensureSessionRestored();
|
|
1467
|
+
|
|
1468
|
+
// Don't auto-lock if set to "never" (0 minutes) - stop the alarm cycle
|
|
1469
|
+
if (autoLockMinutes <= 0) {
|
|
1470
|
+
chrome.alarms.clear(ALARM_NAMES.AUTO_LOCK);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Don't auto-lock if user manually locked - respect their choice
|
|
1475
|
+
if (manuallyLocked) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const idleMs = Date.now() - lastActivity;
|
|
1480
|
+
if (idleMs >= autoLockMinutes * 60_000) {
|
|
1481
|
+
try {
|
|
1482
|
+
await vault.lock();
|
|
1483
|
+
await clearUnlockSessionCache();
|
|
1484
|
+
// Notify popup to update UI immediately
|
|
1485
|
+
await emitWalletEvent('LOCKED', { reason: 'auto-lock' });
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
console.error('Auto-lock failed:', error);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Schedule the auto-lock alarm (runs every minute)
|
|
1494
|
+
*/
|
|
1495
|
+
function scheduleAlarm() {
|
|
1496
|
+
chrome.alarms.create(ALARM_NAMES.AUTO_LOCK, {
|
|
1497
|
+
delayInMinutes: 1,
|
|
1498
|
+
periodInMinutes: 1,
|
|
1499
|
+
});
|
|
1500
|
+
}
|