@nockchain/rose 0.1.4-nightly.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.github/workflows/artifacts.yml +33 -0
  2. package/.github/workflows/ci.yml +68 -0
  3. package/.github/workflows/publish-sdk.yml +35 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +8 -0
  7. package/LICENSE +22 -0
  8. package/README.md +117 -0
  9. package/extension/background/index.ts +1500 -0
  10. package/extension/content/index.ts +59 -0
  11. package/extension/icons/rose.svg +27 -0
  12. package/extension/icons/rose128.png +0 -0
  13. package/extension/icons/rose16.png +0 -0
  14. package/extension/icons/rose256.png +0 -0
  15. package/extension/icons/rose32.png +0 -0
  16. package/extension/icons/rose48.png +0 -0
  17. package/extension/icons/rose512.png +0 -0
  18. package/extension/inpage/index.ts +86 -0
  19. package/extension/manifest.json +48 -0
  20. package/extension/popup/Popup.tsx +94 -0
  21. package/extension/popup/Router.tsx +121 -0
  22. package/extension/popup/assets/arrow-down-icon.svg +3 -0
  23. package/extension/popup/assets/arrow-left-icon.svg +3 -0
  24. package/extension/popup/assets/arrow-right-icon.svg +3 -0
  25. package/extension/popup/assets/arrow-up-icon.svg +3 -0
  26. package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
  27. package/extension/popup/assets/checkmark-icon.svg +3 -0
  28. package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
  29. package/extension/popup/assets/checkmark-success-icon.svg +3 -0
  30. package/extension/popup/assets/clock-icon.svg +3 -0
  31. package/extension/popup/assets/close-x-icon.svg +3 -0
  32. package/extension/popup/assets/copy-icon.svg +6 -0
  33. package/extension/popup/assets/explorer-icon.svg +3 -0
  34. package/extension/popup/assets/eye-off-icon.svg +3 -0
  35. package/extension/popup/assets/eye-open-icon.svg +4 -0
  36. package/extension/popup/assets/feedback-icon.svg +3 -0
  37. package/extension/popup/assets/green-status-dot.svg +3 -0
  38. package/extension/popup/assets/info-icon.svg +3 -0
  39. package/extension/popup/assets/iris-logo-40.svg +27 -0
  40. package/extension/popup/assets/iris-logo-96.svg +27 -0
  41. package/extension/popup/assets/iris-logo-blue.svg +27 -0
  42. package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
  43. package/extension/popup/assets/iris-logo-orange.svg +27 -0
  44. package/extension/popup/assets/iris-logo.svg +27 -0
  45. package/extension/popup/assets/key-icon.svg +3 -0
  46. package/extension/popup/assets/lock-icon-yellow.svg +3 -0
  47. package/extension/popup/assets/lock-icon.svg +3 -0
  48. package/extension/popup/assets/pencil-edit-icon.svg +3 -0
  49. package/extension/popup/assets/permissions-icon.svg +3 -0
  50. package/extension/popup/assets/receipt-icon.svg +5 -0
  51. package/extension/popup/assets/refresh-icon.svg +3 -0
  52. package/extension/popup/assets/settings-gear-icon.svg +8 -0
  53. package/extension/popup/assets/settings-icon.svg +3 -0
  54. package/extension/popup/assets/theme-icon.svg +3 -0
  55. package/extension/popup/assets/trash-bin-icon.svg +3 -0
  56. package/extension/popup/assets/trend-down-arrow.svg +5 -0
  57. package/extension/popup/assets/trend-up-arrow.svg +5 -0
  58. package/extension/popup/assets/user-account-icon.svg +3 -0
  59. package/extension/popup/assets/vector-bottom-left.svg +9 -0
  60. package/extension/popup/assets/vector-left.svg +9 -0
  61. package/extension/popup/assets/vector-right.svg +9 -0
  62. package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
  63. package/extension/popup/assets/vector-top-right.svg +9 -0
  64. package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
  65. package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
  66. package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
  67. package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
  68. package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
  69. package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
  70. package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
  71. package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
  72. package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
  73. package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
  74. package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
  75. package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
  76. package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
  77. package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
  78. package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
  79. package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
  80. package/extension/popup/components/AccountIcon.tsx +78 -0
  81. package/extension/popup/components/AccountSelector.tsx +246 -0
  82. package/extension/popup/components/Alert.tsx +48 -0
  83. package/extension/popup/components/ConfirmModal.tsx +81 -0
  84. package/extension/popup/components/PasswordInput.tsx +49 -0
  85. package/extension/popup/components/ScreenContainer.tsx +17 -0
  86. package/extension/popup/components/SiteIcon.tsx +60 -0
  87. package/extension/popup/components/ThemeToggle.tsx +44 -0
  88. package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
  89. package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
  90. package/extension/popup/components/icons/CheckIcon.tsx +20 -0
  91. package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
  92. package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
  93. package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
  94. package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
  95. package/extension/popup/components/icons/CloseIcon.tsx +26 -0
  96. package/extension/popup/components/icons/CopyIcon.tsx +20 -0
  97. package/extension/popup/components/icons/EditIcon.tsx +20 -0
  98. package/extension/popup/components/icons/EyeIcon.tsx +13 -0
  99. package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
  100. package/extension/popup/components/icons/InfoIcon.tsx +20 -0
  101. package/extension/popup/components/icons/LockIcon.tsx +20 -0
  102. package/extension/popup/components/icons/PlusIcon.tsx +15 -0
  103. package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
  104. package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
  105. package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
  106. package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
  107. package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
  108. package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
  109. package/extension/popup/components/icons/UploadIcon.tsx +20 -0
  110. package/extension/popup/components/icons/WalletIcon.tsx +20 -0
  111. package/extension/popup/contexts/ThemeContext.tsx +105 -0
  112. package/extension/popup/hooks/useApprovalDetection.ts +128 -0
  113. package/extension/popup/hooks/useAutoFocus.ts +36 -0
  114. package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
  115. package/extension/popup/hooks/useClickOutside.ts +33 -0
  116. package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
  117. package/extension/popup/hooks/useFavicon.ts +64 -0
  118. package/extension/popup/hooks/useNumericInput.ts +93 -0
  119. package/extension/popup/index.html +13 -0
  120. package/extension/popup/index.tsx +24 -0
  121. package/extension/popup/screens/AboutScreen.tsx +118 -0
  122. package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
  123. package/extension/popup/screens/HomeScreen.tsx +902 -0
  124. package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
  125. package/extension/popup/screens/LockTimeScreen.tsx +155 -0
  126. package/extension/popup/screens/ReceiveScreen.tsx +149 -0
  127. package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
  128. package/extension/popup/screens/SendReviewScreen.tsx +308 -0
  129. package/extension/popup/screens/SendScreen.tsx +825 -0
  130. package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
  131. package/extension/popup/screens/SettingsScreen.tsx +116 -0
  132. package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
  133. package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
  134. package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
  135. package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
  136. package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
  137. package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
  138. package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
  139. package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
  140. package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
  141. package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
  142. package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
  143. package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
  144. package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
  145. package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
  146. package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
  147. package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
  148. package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
  149. package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
  150. package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
  151. package/extension/popup/screens/system/LockedScreen.tsx +288 -0
  152. package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
  153. package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
  154. package/extension/popup/store.ts +482 -0
  155. package/extension/popup/styles.css +246 -0
  156. package/extension/popup/utils/format.ts +58 -0
  157. package/extension/popup/utils/formatWalletError.ts +36 -0
  158. package/extension/popup/utils/memo.ts +299 -0
  159. package/extension/popup/utils/messaging.ts +16 -0
  160. package/extension/shared/address-encoding.ts +69 -0
  161. package/extension/shared/balance-query.ts +123 -0
  162. package/extension/shared/constants.ts +386 -0
  163. package/extension/shared/currency.ts +128 -0
  164. package/extension/shared/first-name-derivation.ts +128 -0
  165. package/extension/shared/keyfile.ts +58 -0
  166. package/extension/shared/onboarding.ts +78 -0
  167. package/extension/shared/price-api.ts +79 -0
  168. package/extension/shared/rpc-client-browser.ts +315 -0
  169. package/extension/shared/transaction-builder.ts +443 -0
  170. package/extension/shared/types.ts +450 -0
  171. package/extension/shared/utxo-diff.ts +212 -0
  172. package/extension/shared/utxo-store.ts +548 -0
  173. package/extension/shared/utxo-sync.ts +343 -0
  174. package/extension/shared/validators.ts +26 -0
  175. package/extension/shared/vault.ts +1580 -0
  176. package/extension/shared/wallet-crypto.ts +77 -0
  177. package/extension/shared/wasm-utils.ts +76 -0
  178. package/extension/shared/webcrypto.ts +67 -0
  179. package/extension/types/wasm.d.ts +13 -0
  180. package/package.json +39 -0
  181. package/postcss.config.js +6 -0
  182. package/rose-extension-dist.zip +0 -0
  183. package/sdk/README.md +88 -0
  184. package/sdk/examples/app.ts +166 -0
  185. package/sdk/examples/index.html +51 -0
  186. package/sdk/examples/tsconfig.json +15 -0
  187. package/sdk/examples/tx-builder.html +532 -0
  188. package/sdk/examples/tx-builder.ts +1766 -0
  189. package/sdk/package-lock.json +424 -0
  190. package/sdk/package.json +68 -0
  191. package/sdk/src/constants.ts +28 -0
  192. package/sdk/src/errors.ts +74 -0
  193. package/sdk/src/hooks/index.ts +1 -0
  194. package/sdk/src/hooks/use-rose.ts +94 -0
  195. package/sdk/src/index.ts +12 -0
  196. package/sdk/src/provider.ts +396 -0
  197. package/sdk/src/transaction.ts +163 -0
  198. package/sdk/src/types/rose-wasm.d.ts +14 -0
  199. package/sdk/src/types.ts +97 -0
  200. package/sdk/src/wasm.ts +13 -0
  201. package/sdk/tsconfig.json +20 -0
  202. package/sdk/vite.config.examples.ts +32 -0
  203. package/tailwind.config.ts +38 -0
  204. package/tsconfig.json +20 -0
  205. package/vite.config.ts +60 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * First-name derivation utilities for Nockchain v1 notes
3
+ *
4
+ * In Nockchain v1, notes are indexed by "first-names" which are deterministically
5
+ * derived from their lock conditions. This module provides functions to calculate
6
+ * the expected first-name for standard lock types (simple PKH and coinbase).
7
+ */
8
+
9
+ import * as wasm from '@nockchain/rose-wasm/rose_wasm.js';
10
+ import { initIrisSdkOnce } from './wasm-utils.js';
11
+
12
+ /**
13
+ * Derives the first-name for a simple PKH-locked note
14
+ *
15
+ * This is used for regular transaction outputs (non-coinbase).
16
+ * The lock structure is: [(pkh, m=1, hashes=[your_pkh])]
17
+ *
18
+ * @param pkhBase58 - The base58-encoded PKH digest (~55 chars)
19
+ * @returns The base58-encoded first-name hash (40 bytes encoded)
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const myPKH = "2R7Z8p..."; // Your v1 PKH address
24
+ * const firstName = await deriveSimpleFirstName(myPKH);
25
+ * // Use firstName to query notes via gRPC API
26
+ * ```
27
+ */
28
+ export async function deriveSimpleFirstName(pkhBase58: string): Promise<string> {
29
+ await initIrisSdkOnce();
30
+
31
+ // Validate PKH is a non-empty string
32
+ if (!pkhBase58 || typeof pkhBase58 !== 'string') {
33
+ throw new Error('PKH must be a non-empty base58 string');
34
+ }
35
+
36
+ // Create a simple PKH-only spend condition
37
+ const pkh = wasm.Pkh.single(pkhBase58);
38
+ const condition = wasm.SpendCondition.newPkh(pkh);
39
+
40
+ // Get the first-name from the spend condition
41
+ const firstNameDigest = condition.firstName();
42
+ const firstNameBase58 = firstNameDigest.value;
43
+
44
+ // Verify it's the right length (40 bytes → ~55 chars base58)
45
+ if (firstNameBase58.length < 50 || firstNameBase58.length > 60) {
46
+ console.warn(
47
+ `[First-Name] ⚠️ WARNING: First-name length ${firstNameBase58.length} is outside expected range 50-60 chars`
48
+ );
49
+ }
50
+
51
+ return firstNameBase58;
52
+ }
53
+
54
+ /**
55
+ * Derives the first-name for a coinbase (mining reward) note
56
+ *
57
+ * This is used for mining rewards which include both a PKH lock and a timelock.
58
+ * The lock structure is: [(pkh, m=1, hashes=[your_pkh]), (tim, timelock)]
59
+ *
60
+ * @param pkhBase58 - The base58-encoded PKH digest (~55 chars)
61
+ * @returns The base58-encoded first-name hash (40 bytes encoded)
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const myPKH = "2R7Z8p..."; // Your v1 PKH address
66
+ * const firstName = await deriveCoinbaseFirstName(myPKH);
67
+ * // Use firstName to query mining rewards via gRPC API
68
+ * ```
69
+ */
70
+ export async function deriveCoinbaseFirstName(pkhBase58: string): Promise<string> {
71
+ await initIrisSdkOnce();
72
+
73
+ // Validate PKH is a non-empty string
74
+ if (!pkhBase58 || typeof pkhBase58 !== 'string') {
75
+ throw new Error('PKH must be a non-empty base58 string');
76
+ }
77
+
78
+ // Create PKH + TIM (coinbase) spend condition
79
+ const pkhLeaf = wasm.LockPrimitive.newPkh(wasm.Pkh.single(pkhBase58));
80
+ const timLeaf = wasm.LockPrimitive.newTim(wasm.LockTim.coinbase());
81
+ const condition = new wasm.SpendCondition([pkhLeaf, timLeaf]);
82
+
83
+ // Get the first-name from the spend condition
84
+ const firstNameDigest = condition.firstName();
85
+ const firstNameBase58 = firstNameDigest.value;
86
+
87
+ // Verify it's the right length (40 bytes → ~55 chars base58)
88
+ if (firstNameBase58.length < 50 || firstNameBase58.length > 60) {
89
+ console.warn(
90
+ `[First-Name] Coinbase first-name length ${firstNameBase58.length} is outside expected range 50-60 chars`
91
+ );
92
+ }
93
+
94
+ return firstNameBase58;
95
+ }
96
+
97
+ /**
98
+ * Helper to get both first-names for a given PKH address
99
+ *
100
+ * Returns both simple and coinbase first-names so you can query
101
+ * both regular transaction outputs and mining rewards in a single call.
102
+ *
103
+ * @param pkhBase58 - The base58-encoded PKH address (~55 chars)
104
+ * @returns Object containing both first-names
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const myPKH = "2R7Z8p...";
109
+ * const { simple, coinbase } = await getBothFirstNames(myPKH);
110
+ *
111
+ * // Query both types of notes
112
+ * const [simpleNotes, coinbaseNotes] = await Promise.all([
113
+ * queryNotesByFirstName(simple),
114
+ * queryNotesByFirstName(coinbase),
115
+ * ]);
116
+ * ```
117
+ */
118
+ export async function getBothFirstNames(pkhBase58: string): Promise<{
119
+ simple: string;
120
+ coinbase: string;
121
+ }> {
122
+ await initIrisSdkOnce();
123
+
124
+ return {
125
+ simple: await deriveSimpleFirstName(pkhBase58),
126
+ coinbase: await deriveCoinbaseFirstName(pkhBase58),
127
+ };
128
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Keyfile import/export utilities
3
+ * Plain JSON format for backup/restore
4
+ */
5
+
6
+ export interface Keyfile {
7
+ version: string;
8
+ mnemonic: string;
9
+ created: string;
10
+ }
11
+
12
+ /**
13
+ * Export mnemonic to plain JSON keyfile
14
+ */
15
+ export function exportKeyfile(mnemonic: string): Keyfile {
16
+ return {
17
+ version: '1',
18
+ mnemonic,
19
+ created: new Date().toISOString(),
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Import keyfile to get mnemonic
25
+ */
26
+ export function importKeyfile(keyfile: Keyfile): string {
27
+ // Validate keyfile format
28
+ if (!keyfile.version) {
29
+ throw new Error('Invalid keyfile format: missing version');
30
+ }
31
+
32
+ if (keyfile.version !== '1') {
33
+ throw new Error('Unsupported keyfile version');
34
+ }
35
+
36
+ if (!keyfile.mnemonic || typeof keyfile.mnemonic !== 'string') {
37
+ throw new Error('Invalid keyfile format: missing or invalid mnemonic');
38
+ }
39
+
40
+ return keyfile.mnemonic;
41
+ }
42
+
43
+ /**
44
+ * Download keyfile as JSON file
45
+ */
46
+ export function downloadKeyfile(keyfile: Keyfile, filename: string = 'nockchain-keyfile.json') {
47
+ const json = JSON.stringify(keyfile, null, 2);
48
+ const blob = new Blob([json], { type: 'application/json' });
49
+ const url = URL.createObjectURL(blob);
50
+
51
+ const a = document.createElement('a');
52
+ a.href = url;
53
+ a.download = filename;
54
+ document.body.appendChild(a);
55
+ a.click();
56
+ document.body.removeChild(a);
57
+ URL.revokeObjectURL(url);
58
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Onboarding State Management
3
+ * Handles persisting and retrieving onboarding progress to ensure users
4
+ * complete their secret phrase backup even if they close the popup mid-flow.
5
+ */
6
+
7
+ import { STORAGE_KEYS } from './constants';
8
+
9
+ function isRecord(x: unknown): x is Record<string, unknown> {
10
+ return typeof x === 'object' && x !== null;
11
+ }
12
+
13
+ /**
14
+ * Onboarding state stored in chrome.storage.local
15
+ */
16
+ export interface OnboardingState {
17
+ /** Whether secret phrase backup has been completed */
18
+ completed: boolean;
19
+ /** Current onboarding step (only present if not completed) */
20
+ step?: 'backup' | 'verify';
21
+ }
22
+
23
+ /**
24
+ * Set the onboarding state to in-progress at the backup step
25
+ * Called when a new wallet is created
26
+ */
27
+ export async function setOnboardingInProgress(): Promise<void> {
28
+ const state: OnboardingState = {
29
+ completed: false,
30
+ step: 'backup',
31
+ };
32
+ await chrome.storage.local.set({ [STORAGE_KEYS.ONBOARDING_STATE]: state });
33
+ }
34
+
35
+ /**
36
+ * Mark onboarding as complete
37
+ * Called when user successfully verifies their secret phrase
38
+ */
39
+ export async function markOnboardingComplete(): Promise<void> {
40
+ const state: OnboardingState = {
41
+ completed: true,
42
+ };
43
+ await chrome.storage.local.set({ [STORAGE_KEYS.ONBOARDING_STATE]: state });
44
+ }
45
+
46
+ /**
47
+ * Get current onboarding state
48
+ * Returns null if no onboarding state exists (fresh install or pre-migration)
49
+ */
50
+ export async function getOnboardingState(): Promise<OnboardingState | null> {
51
+ const result = (await chrome.storage.local.get([STORAGE_KEYS.ONBOARDING_STATE])) as Record<
52
+ string,
53
+ unknown
54
+ >;
55
+ const raw = result[STORAGE_KEYS.ONBOARDING_STATE];
56
+ if (!isRecord(raw)) return null;
57
+ if (typeof raw.completed !== 'boolean') return null;
58
+ const step = raw.step;
59
+ if (step === 'backup' || step === 'verify') return { completed: raw.completed, step };
60
+ if (step === undefined) return { completed: raw.completed };
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Check if user has incomplete onboarding (created wallet but didn't complete backup)
66
+ */
67
+ export async function hasIncompleteOnboarding(): Promise<boolean> {
68
+ const state = await getOnboardingState();
69
+ return state !== null && !state.completed;
70
+ }
71
+
72
+ /**
73
+ * Clear onboarding state
74
+ * Used if user explicitly skips backup (not recommended) or for cleanup
75
+ */
76
+ export async function clearOnboardingState(): Promise<void> {
77
+ await chrome.storage.local.remove(STORAGE_KEYS.ONBOARDING_STATE);
78
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Price API for fetching NOCK token price from CoinGecko
3
+ */
4
+
5
+ // CoinGecko API configuration
6
+ const COINGECKO_API_BASE = 'https://api.coingecko.com/api/v3';
7
+
8
+ const NOCK_COIN_ID = 'nockchain';
9
+
10
+ export interface PriceData {
11
+ usd: number;
12
+ usd_24h_change: number;
13
+ }
14
+
15
+ /**
16
+ * Fetch current NOCK price from CoinGecko
17
+ * @returns Price in USD and 24h change percentage
18
+ */
19
+ export async function fetchNockPrice(): Promise<PriceData> {
20
+ try {
21
+ const response = await fetch(
22
+ `${COINGECKO_API_BASE}/simple/price?ids=${NOCK_COIN_ID}&vs_currencies=usd&include_24hr_change=true`
23
+ );
24
+
25
+ if (!response.ok) {
26
+ throw new Error(`CoinGecko API error: ${response.status}`);
27
+ }
28
+
29
+ const data = await response.json();
30
+
31
+ // Check if the coin exists in the response
32
+ if (!data[NOCK_COIN_ID]) {
33
+ throw new Error(`Coin ${NOCK_COIN_ID} not found on CoinGecko`);
34
+ }
35
+
36
+ const coinData = data[NOCK_COIN_ID];
37
+
38
+ return {
39
+ usd: coinData.usd || 0,
40
+ usd_24h_change: coinData.usd_24h_change || 0,
41
+ };
42
+ } catch (error) {
43
+ console.error('[PriceAPI] Failed to fetch price:', error);
44
+
45
+ // Return fallback data instead of throwing
46
+ // This prevents the UI from breaking if the API is down
47
+ return {
48
+ usd: 0,
49
+ usd_24h_change: 0,
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Format price for display
56
+ * @param price - Price in USD
57
+ * @returns Formatted price string (e.g., "$1.23")
58
+ */
59
+ export function formatPrice(price: number): string {
60
+ if (price === 0) return '$0.00';
61
+
62
+ // For prices < $0.01, show more decimals
63
+ if (price < 0.01) {
64
+ return `$${price.toFixed(6)}`;
65
+ }
66
+
67
+ // For normal prices, show 2 decimals
68
+ return `$${price.toFixed(2)}`;
69
+ }
70
+
71
+ /**
72
+ * Format percentage change for display
73
+ * @param change - Percentage change
74
+ * @returns Formatted percentage string (e.g., "+5.23%")
75
+ */
76
+ export function formatPercentChange(change: number): string {
77
+ const formatted = Math.abs(change).toFixed(2);
78
+ return change >= 0 ? `+${formatted}%` : `-${formatted}%`;
79
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Browser gRPC-web client for Nockchain
3
+ * Uses WASM-based tonic-web-wasm-client for proper bigint handling
4
+ */
5
+
6
+ import { GrpcClient } from '@nockchain/rose-wasm/rose_wasm.js';
7
+ import type { Note } from './types';
8
+ import { base58 } from '@scure/base';
9
+ import { initIrisSdkOnce } from './wasm-utils.js';
10
+ import { RPC_ENDPOINT, INTERNAL_METHODS } from './constants.js';
11
+
12
+ /**
13
+ * Report RPC connection status to background service worker
14
+ * This updates the connection indicator (green/red dot) in the UI
15
+ */
16
+ async function reportRpcStatus(healthy: boolean): Promise<void> {
17
+ try {
18
+ await chrome.runtime.sendMessage({
19
+ payload: {
20
+ method: INTERNAL_METHODS.REPORT_RPC_STATUS,
21
+ params: [healthy],
22
+ },
23
+ });
24
+ } catch {
25
+ // Ignore errors - background may not be ready
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Browser RPC client for Nockchain blockchain
31
+ * Compatible with Chrome extensions and web browsers
32
+ * Uses Rust WASM client for proper bigint serialization
33
+ */
34
+ export class NockchainBrowserRPCClient {
35
+ private client: GrpcClient | null = null;
36
+ private endpoint: string;
37
+
38
+ constructor(endpoint: string = RPC_ENDPOINT) {
39
+ this.endpoint = endpoint;
40
+ }
41
+
42
+ /**
43
+ * Ensure the WASM client is initialized
44
+ */
45
+ private async ensureClient(): Promise<GrpcClient> {
46
+ if (this.client) {
47
+ return this.client;
48
+ }
49
+
50
+ await initIrisSdkOnce();
51
+ this.client = new GrpcClient(this.endpoint);
52
+ return this.client;
53
+ }
54
+
55
+ /**
56
+ * Get balance (UTXOs/notes) for an address
57
+ * @param address - Base58-encoded V1 address
58
+ */
59
+ async getBalance(address: string): Promise<Note[]> {
60
+ try {
61
+ const client = await this.ensureClient();
62
+ const response = await client.getBalanceByAddress(address);
63
+
64
+ // Report successful RPC call
65
+ reportRpcStatus(true);
66
+
67
+ return this.convertBalanceToNotes(response);
68
+ } catch (error) {
69
+ console.error('[RPC Browser] Error fetching balance:', error);
70
+ // Report failed RPC call
71
+ reportRpcStatus(false);
72
+ throw new Error(
73
+ `Failed to fetch balance: ${error instanceof Error ? error.message : String(error)}`
74
+ );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get notes by first-name (v1 query method)
80
+ * @param firstNameBase58 - Base58-encoded first-name hash (~55 characters)
81
+ * @returns Array of notes with matching first-name
82
+ */
83
+ async getNotesByFirstName(firstNameBase58: string): Promise<Note[]> {
84
+ try {
85
+ const client = await this.ensureClient();
86
+ const response = await client.getBalanceByFirstName(firstNameBase58);
87
+
88
+ // Report successful RPC call
89
+ reportRpcStatus(true);
90
+
91
+ return this.convertBalanceToNotes(response);
92
+ } catch (error) {
93
+ console.error('[RPC Browser] Error fetching notes by first-name:', error);
94
+ // Report failed RPC call
95
+ reportRpcStatus(false);
96
+ throw new Error(
97
+ `Failed to fetch notes by first-name: ${error instanceof Error ? error.message : String(error)}`
98
+ );
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get current block height from balance query
104
+ */
105
+ async getCurrentBlockHeight(address?: string): Promise<bigint> {
106
+ try {
107
+ const client = await this.ensureClient();
108
+ // Use a dummy address to get chain info
109
+ const dummyAddress = '1'.repeat(132);
110
+ const response = await client.getBalanceByAddress(dummyAddress);
111
+
112
+ if (response.height?.value) {
113
+ return BigInt(response.height.value);
114
+ }
115
+
116
+ return BigInt(0);
117
+ } catch (error) {
118
+ console.error('[RPC Browser] Error getting block height:', error);
119
+ return BigInt(0);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Send a transaction to the network
125
+ * @param rawTx - The signed raw transaction object
126
+ * @returns Transaction ID if successful
127
+ */
128
+ async sendTransaction(rawTx: any): Promise<string> {
129
+ try {
130
+ const client = await this.ensureClient();
131
+ const response = await client.sendTransaction(rawTx);
132
+
133
+ // Report successful RPC call
134
+ reportRpcStatus(true);
135
+ return response;
136
+ } catch (error) {
137
+ console.error('[RPC Browser] Error sending transaction:', error);
138
+ // Report failed RPC call
139
+ reportRpcStatus(false);
140
+ throw new Error(
141
+ `Failed to send transaction: ${error instanceof Error ? error.message : String(error)}`
142
+ );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Check if a transaction was accepted by the network
148
+ * @param txId - Base58-encoded transaction ID
149
+ * @returns true if accepted, false otherwise
150
+ */
151
+ async isTransactionAccepted(txId: string): Promise<boolean> {
152
+ try {
153
+ const client = await this.ensureClient();
154
+ const accepted = await client.transactionAccepted(txId);
155
+ // Report successful RPC call
156
+ reportRpcStatus(true);
157
+ return accepted;
158
+ } catch (error) {
159
+ console.error('[RPC Browser] Error checking transaction status:', error);
160
+ // Report failed RPC call
161
+ reportRpcStatus(false);
162
+ return false;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Convert proto Balance format to our Note[] interface
168
+ */
169
+ private convertBalanceToNotes(balance: any): Note[] {
170
+ if (!balance.notes || balance.notes.length === 0) {
171
+ return [];
172
+ }
173
+
174
+ const notes: Note[] = [];
175
+
176
+ for (const entry of balance.notes) {
177
+ try {
178
+ const note = this.convertProtoNote(entry);
179
+ if (note) {
180
+ notes.push(note);
181
+ }
182
+ } catch (error) {
183
+ console.error('[RPC Browser] Error converting note:', error, entry);
184
+ }
185
+ }
186
+
187
+ return notes;
188
+ }
189
+
190
+ /**
191
+ * Convert a single proto note to our Note interface
192
+ */
193
+ private convertProtoNote(balanceEntry: any): Note | null {
194
+ if (!balanceEntry.note) {
195
+ return null;
196
+ }
197
+
198
+ const protoNote = balanceEntry.note;
199
+ const name = balanceEntry.name;
200
+ const noteDataHash = balanceEntry.note_data_hash; // May be base58 string or undefined
201
+
202
+ // WASM client returns note_version with V1 format
203
+ const noteVersion = protoNote.note_version;
204
+ const noteData = noteVersion?.V1 || protoNote.v1;
205
+
206
+ if (!noteData) {
207
+ console.warn('[RPC Browser] Unknown note format:', protoNote);
208
+ return null;
209
+ }
210
+
211
+ // WASM client returns name as { first: "base58", last: "base58" } instead of bytes
212
+ let nameFirst: Uint8Array;
213
+ let nameLast: Uint8Array;
214
+ let nameFirstBase58: string | undefined;
215
+ let nameLastBase58: string | undefined;
216
+
217
+ if (name?.first && name?.last && typeof name.first === 'string') {
218
+ // WASM format: base58 strings
219
+ // Store the base58 strings for later use in transactions
220
+ nameFirstBase58 = name.first;
221
+ nameLastBase58 = name.last;
222
+
223
+ // Also decode to bytes for compatibility
224
+ nameFirst = base58.decode(name.first);
225
+ nameLast = base58.decode(name.last);
226
+ } else {
227
+ // Old format: bytes
228
+ const nameBytes = name?.bytes || new Uint8Array(80);
229
+ nameFirst = nameBytes.slice(0, 40);
230
+ nameLast = nameBytes.slice(40, 80);
231
+ }
232
+
233
+ // WASM client handles bigints properly, but we still need to convert to number for assets
234
+ // The WASM deserializer already handles proper bigint conversion
235
+ const safeToNumber = (value: any): number => {
236
+ if (typeof value === 'bigint') {
237
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
238
+ console.warn(
239
+ '[RPC Browser] Value exceeds MAX_SAFE_INTEGER, precision may be lost:',
240
+ value
241
+ );
242
+ }
243
+ return Number(value);
244
+ }
245
+ if (typeof value === 'string') {
246
+ const bigIntValue = BigInt(value);
247
+ if (bigIntValue > BigInt(Number.MAX_SAFE_INTEGER)) {
248
+ console.warn(
249
+ '[RPC Browser] Value exceeds MAX_SAFE_INTEGER, precision may be lost:',
250
+ value
251
+ );
252
+ }
253
+ return Number(bigIntValue);
254
+ }
255
+ return Number(value || 0);
256
+ };
257
+
258
+ // Extract assets - WASM format has { value: "123" }, old format is direct
259
+ const assetsValue = noteData.assets?.value || noteData.assets || 0;
260
+
261
+ // Extract version - WASM format has { value: "1" }, old format is direct
262
+ const versionValue = noteData.version?.value
263
+ ? parseInt(noteData.version.value)
264
+ : noteData.version || 1;
265
+
266
+ // Extract originPage - WASM format has { value: "123" }, old format is direct
267
+ const originPageValue = noteData.origin_page?.value || noteData.originPage || 0;
268
+
269
+ if (noteVersion?.Legacy || protoNote.legacy) {
270
+ return {
271
+ version: 0,
272
+ originPage: BigInt(originPageValue),
273
+ timelockMin: noteData.timelockMin ? BigInt(noteData.timelockMin) : undefined,
274
+ timelockMax: noteData.timelockMax ? BigInt(noteData.timelockMax) : undefined,
275
+ nameFirst,
276
+ nameLast,
277
+ nameFirstBase58,
278
+ nameLastBase58,
279
+ noteDataHashBase58: noteDataHash,
280
+ lockPubkeys: noteData.lock?.pubkeys || [],
281
+ lockKeysRequired: BigInt(noteData.lock?.keysRequired || 1),
282
+ sourceHash: noteData.source?.hash?.bytes || new Uint8Array(40),
283
+ sourceIsCoinbase: noteData.source?.isCoinbase || false,
284
+ assets: safeToNumber(assetsValue),
285
+ protoNote: balanceEntry.note, // Store raw protobuf for Note.fromProtobuf()
286
+ };
287
+ } else {
288
+ return {
289
+ version: versionValue,
290
+ originPage: BigInt(originPageValue),
291
+ timelockMin: undefined,
292
+ timelockMax: undefined,
293
+ nameFirst,
294
+ nameLast,
295
+ nameFirstBase58,
296
+ nameLastBase58,
297
+ noteDataHashBase58: noteDataHash,
298
+ lockPubkeys: [],
299
+ lockKeysRequired: BigInt(1),
300
+ sourceHash: new Uint8Array(40),
301
+ sourceIsCoinbase: false,
302
+ assets: safeToNumber(assetsValue),
303
+ protoNote: balanceEntry.note, // Store raw protobuf for Note.fromProtobuf()
304
+ };
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Create a browser client instance
311
+ * @param endpoint - gRPC-web endpoint URL (defaults to RPC_ENDPOINT)
312
+ */
313
+ export function createBrowserClient(endpoint = RPC_ENDPOINT): NockchainBrowserRPCClient {
314
+ return new NockchainBrowserRPCClient(endpoint);
315
+ }