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