@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,902 @@
|
|
|
1
|
+
import { useState, useLayoutEffect, useEffect, useRef } from 'react';
|
|
2
|
+
import { useStore } from '../store';
|
|
3
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
4
|
+
import { truncateAddress } from '../utils/format';
|
|
5
|
+
import { send } from '../utils/messaging';
|
|
6
|
+
import { INTERNAL_METHODS, NOCK_TO_NICKS, STORAGE_KEYS } from '../../shared/constants';
|
|
7
|
+
import type { Account } from '../../shared/types';
|
|
8
|
+
import { AccountIcon } from '../components/AccountIcon';
|
|
9
|
+
import { EyeIcon } from '../components/icons/EyeIcon';
|
|
10
|
+
import { EyeOffIcon } from '../components/icons/EyeOffIcon';
|
|
11
|
+
import { CheckIcon } from '../components/icons/CheckIcon';
|
|
12
|
+
import { SendPaperPlaneIcon } from '../components/icons/SendPaperPlaneIcon';
|
|
13
|
+
import { ReceiveCircleIcon } from '../components/icons/ReceiveCircleIcon';
|
|
14
|
+
import { ReceiveArrowIcon } from '../components/icons/ReceiveArrowIcon';
|
|
15
|
+
import { SentArrowIcon } from '../components/icons/SentArrowIcon';
|
|
16
|
+
import { ArrowUpRightIcon } from '../components/icons/ArrowUpRightIcon';
|
|
17
|
+
|
|
18
|
+
import WalletDropdownArrow from '../assets/wallet-dropdown-arrow.svg';
|
|
19
|
+
import LockIconAsset from '../assets/lock-icon.svg';
|
|
20
|
+
import SettingsIconAsset from '../assets/settings-icon.svg';
|
|
21
|
+
import TrendUpArrow from '../assets/trend-up-arrow.svg';
|
|
22
|
+
import TrendDownArrow from '../assets/trend-down-arrow.svg';
|
|
23
|
+
import ExplorerIcon from '../assets/explorer-icon.svg';
|
|
24
|
+
import PermissionsIcon from '../assets/permissions-icon.svg';
|
|
25
|
+
import FeedbackIcon from '../assets/feedback-icon.svg';
|
|
26
|
+
import CopyIcon from '../assets/copy-icon.svg';
|
|
27
|
+
import KeyIcon from '../assets/key-icon.svg';
|
|
28
|
+
import PencilEditIcon from '../assets/pencil-edit-icon.svg';
|
|
29
|
+
import RefreshIcon from '../assets/refresh-icon.svg';
|
|
30
|
+
import ReceiptIcon from '../assets/receipt-icon.svg';
|
|
31
|
+
|
|
32
|
+
import './HomeScreen.tailwind.css';
|
|
33
|
+
|
|
34
|
+
/** HomeScreen */
|
|
35
|
+
export function HomeScreen() {
|
|
36
|
+
const {
|
|
37
|
+
navigate,
|
|
38
|
+
wallet,
|
|
39
|
+
syncWallet,
|
|
40
|
+
fetchBalance,
|
|
41
|
+
fetchPrice,
|
|
42
|
+
walletTransactions,
|
|
43
|
+
fetchWalletTransactions,
|
|
44
|
+
setSelectedTransaction,
|
|
45
|
+
isBalanceFetching,
|
|
46
|
+
isInitialized,
|
|
47
|
+
priceUsd,
|
|
48
|
+
priceChange24h,
|
|
49
|
+
isPriceFetching,
|
|
50
|
+
} = useStore();
|
|
51
|
+
const { theme } = useTheme();
|
|
52
|
+
const [balanceHidden, setBalanceHidden] = useState(false);
|
|
53
|
+
const [walletDropdownOpen, setWalletDropdownOpen] = useState(false);
|
|
54
|
+
const [settingsDropdownOpen, setSettingsDropdownOpen] = useState(false);
|
|
55
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
56
|
+
const [copiedAddress, setCopiedAddress] = useState(false);
|
|
57
|
+
const [isConnected, setIsConnected] = useState(true);
|
|
58
|
+
const [hasV0, setHasV0] = useState(false);
|
|
59
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const [isTransactionsStuck, setIsTransactionsStuck] = useState(false);
|
|
62
|
+
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const el = headerRef.current;
|
|
65
|
+
const container = scrollContainerRef.current;
|
|
66
|
+
const updateHeaderHeight = () => {
|
|
67
|
+
const h = el?.offsetHeight ?? 0;
|
|
68
|
+
container?.style.setProperty('--header-h', `${h}px`);
|
|
69
|
+
};
|
|
70
|
+
updateHeaderHeight();
|
|
71
|
+
window.addEventListener('resize', updateHeaderHeight);
|
|
72
|
+
return () => window.removeEventListener('resize', updateHeaderHeight);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// Detect when transactions section is stuck at top
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
const container = scrollContainerRef.current;
|
|
78
|
+
if (!container) return;
|
|
79
|
+
|
|
80
|
+
const handleScroll = () => {
|
|
81
|
+
const headerHeight = headerRef.current?.offsetHeight ?? 64;
|
|
82
|
+
const balanceSectionHeight = 140;
|
|
83
|
+
// When scrolled past the balance section, snap to full width
|
|
84
|
+
const isStuck = container.scrollTop >= balanceSectionHeight;
|
|
85
|
+
setIsTransactionsStuck(isStuck);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
container.addEventListener('scroll', handleScroll);
|
|
89
|
+
handleScroll(); // Call once on mount
|
|
90
|
+
return () => container.removeEventListener('scroll', handleScroll);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Fetch wallet transactions on mount and when account changes
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
fetchWalletTransactions();
|
|
96
|
+
}, [wallet.currentAccount?.address]);
|
|
97
|
+
|
|
98
|
+
// Listen for storage changes to wallet transactions and auto-refresh UI
|
|
99
|
+
// This keeps the UI in sync when background sync updates transaction status
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => {
|
|
102
|
+
if (changes[STORAGE_KEYS.WALLET_TX_STORE]) {
|
|
103
|
+
fetchWalletTransactions();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
chrome.storage.onChanged.addListener(listener);
|
|
107
|
+
return () => chrome.storage.onChanged.removeListener(listener);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
// Check RPC connection status on mount and after balance fetching completes
|
|
111
|
+
// (RPC calls update the status in background, so re-check after they finish)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
async function checkConnection() {
|
|
114
|
+
const result = await send<{ connected?: boolean }>(
|
|
115
|
+
INTERNAL_METHODS.GET_CONNECTION_STATUS,
|
|
116
|
+
[]
|
|
117
|
+
);
|
|
118
|
+
setIsConnected(result?.connected ?? true);
|
|
119
|
+
}
|
|
120
|
+
// Check when balance fetching completes (not while fetching)
|
|
121
|
+
if (!isBalanceFetching) {
|
|
122
|
+
checkConnection();
|
|
123
|
+
}
|
|
124
|
+
}, [isBalanceFetching]);
|
|
125
|
+
|
|
126
|
+
// Load balance hidden preference on mount
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
chrome.storage.local.get([STORAGE_KEYS.BALANCE_HIDDEN]).then(result => {
|
|
129
|
+
const raw = (result as Record<string, unknown>)[STORAGE_KEYS.BALANCE_HIDDEN];
|
|
130
|
+
setBalanceHidden(typeof raw === 'boolean' ? raw : Boolean(raw));
|
|
131
|
+
});
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// Toggle balance visibility and persist setting
|
|
135
|
+
function toggleBalanceHidden() {
|
|
136
|
+
const newValue = !balanceHidden;
|
|
137
|
+
setBalanceHidden(newValue);
|
|
138
|
+
chrome.storage.local.set({ [STORAGE_KEYS.BALANCE_HIDDEN]: newValue });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get accounts from vault (filter out hidden accounts)
|
|
142
|
+
const accounts = (wallet.accounts || []).filter(acc => !acc.hidden);
|
|
143
|
+
const currentAccount = wallet.currentAccount || accounts[0];
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
let isMounted = true;
|
|
147
|
+
async function checkHasV0Mnemonic() {
|
|
148
|
+
const result = await send<{ ok?: boolean; has?: boolean; error?: string }>(
|
|
149
|
+
INTERNAL_METHODS.HAS_V0_MNEMONIC,
|
|
150
|
+
[]
|
|
151
|
+
);
|
|
152
|
+
if (result?.ok && isMounted) {
|
|
153
|
+
setHasV0(Boolean(result.has));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
checkHasV0Mnemonic();
|
|
157
|
+
return () => {
|
|
158
|
+
isMounted = false;
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// Lock wallet handler
|
|
163
|
+
async function handleLockWallet() {
|
|
164
|
+
const result = await send<{ ok?: boolean }>(INTERNAL_METHODS.LOCK, []);
|
|
165
|
+
|
|
166
|
+
if (result?.ok) {
|
|
167
|
+
syncWallet({
|
|
168
|
+
...wallet,
|
|
169
|
+
locked: true,
|
|
170
|
+
});
|
|
171
|
+
navigate('locked');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Account switching handler
|
|
176
|
+
async function handleSwitchAccount(index: number) {
|
|
177
|
+
const result = await send<{ ok?: boolean; account?: Account; error?: string }>(
|
|
178
|
+
INTERNAL_METHODS.SWITCH_ACCOUNT,
|
|
179
|
+
[index]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (result?.ok && result.account) {
|
|
183
|
+
// Get cached balance for the new account (or 0 if not cached)
|
|
184
|
+
const cachedBalance = wallet.accountBalances[result.account.address] ?? 0;
|
|
185
|
+
|
|
186
|
+
const updatedWallet = {
|
|
187
|
+
...wallet,
|
|
188
|
+
currentAccount: result.account,
|
|
189
|
+
address: result.account.address,
|
|
190
|
+
balance: cachedBalance,
|
|
191
|
+
availableBalance: cachedBalance,
|
|
192
|
+
};
|
|
193
|
+
syncWallet(updatedWallet);
|
|
194
|
+
|
|
195
|
+
// Fetch balance and transactions for the switched account
|
|
196
|
+
fetchBalance();
|
|
197
|
+
fetchWalletTransactions();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setWalletDropdownOpen(false);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Account creation handler
|
|
204
|
+
async function handleAddAccount() {
|
|
205
|
+
const result = await send<{ ok?: boolean; account?: Account; error?: string }>(
|
|
206
|
+
INTERNAL_METHODS.CREATE_ACCOUNT,
|
|
207
|
+
[]
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (result?.ok && result.account) {
|
|
211
|
+
const updatedWallet = {
|
|
212
|
+
...wallet,
|
|
213
|
+
accounts: [...wallet.accounts, result.account],
|
|
214
|
+
currentAccount: result.account,
|
|
215
|
+
address: result.account.address,
|
|
216
|
+
balance: 0, // Reset balance to 0 for new account
|
|
217
|
+
accountBalances: {
|
|
218
|
+
...wallet.accountBalances,
|
|
219
|
+
[result.account.address]: 0, // Initialize new account balance to 0
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
syncWallet(updatedWallet);
|
|
223
|
+
|
|
224
|
+
// Fetch balance for the newly created account
|
|
225
|
+
fetchBalance();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setWalletDropdownOpen(false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Refresh balance handler
|
|
232
|
+
async function handleRefreshBalance() {
|
|
233
|
+
setIsRefreshing(true);
|
|
234
|
+
try {
|
|
235
|
+
// Fetch balance (which syncs UTXOs from chain)
|
|
236
|
+
await fetchBalance();
|
|
237
|
+
|
|
238
|
+
// Fetch latest wallet transactions
|
|
239
|
+
await fetchWalletTransactions();
|
|
240
|
+
} finally {
|
|
241
|
+
setIsRefreshing(false);
|
|
242
|
+
// Connection status is automatically re-checked by useEffect when isBalanceFetching changes
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Use available balance (confirmed - pending outflow) as the primary display
|
|
247
|
+
const displayBalance = wallet.availableBalance;
|
|
248
|
+
const balance = displayBalance.toLocaleString('en-US', {
|
|
249
|
+
minimumFractionDigits: 2,
|
|
250
|
+
maximumFractionDigits: 2,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Calculate USD value based on available balance and price
|
|
254
|
+
const totalBalanceUsd = displayBalance * priceUsd;
|
|
255
|
+
const formattedUsdValue = totalBalanceUsd.toLocaleString('en-US', {
|
|
256
|
+
minimumFractionDigits: 2,
|
|
257
|
+
maximumFractionDigits: 2,
|
|
258
|
+
});
|
|
259
|
+
const formattedPercentChange = Math.abs(priceChange24h).toFixed(2);
|
|
260
|
+
|
|
261
|
+
const walletName = currentAccount?.name || 'Wallet';
|
|
262
|
+
|
|
263
|
+
// Fetch price on mount and when account changes
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
fetchPrice();
|
|
266
|
+
}, [fetchPrice]);
|
|
267
|
+
const walletAddress = truncateAddress(currentAccount?.address);
|
|
268
|
+
const fullAddress = currentAccount?.address || '';
|
|
269
|
+
|
|
270
|
+
// Helper to convert WalletTransaction status to display status
|
|
271
|
+
const getDisplayStatus = (status: string): 'pending' | 'confirmed' | 'failed' | 'expired' => {
|
|
272
|
+
switch (status) {
|
|
273
|
+
case 'confirmed':
|
|
274
|
+
return 'confirmed';
|
|
275
|
+
case 'failed':
|
|
276
|
+
return 'failed';
|
|
277
|
+
case 'expired':
|
|
278
|
+
return 'expired';
|
|
279
|
+
default:
|
|
280
|
+
return 'pending';
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Filter to show outgoing and incoming transactions (exclude 'self' which is internal transfers)
|
|
285
|
+
const displayTransactions = walletTransactions.filter(
|
|
286
|
+
tx => tx.direction === 'outgoing' || tx.direction === 'incoming'
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Group wallet transactions by date
|
|
290
|
+
const transactionsByDate = displayTransactions.reduce(
|
|
291
|
+
(acc, tx) => {
|
|
292
|
+
const date = new Date(tx.createdAt).toLocaleDateString('en-US', {
|
|
293
|
+
day: 'numeric',
|
|
294
|
+
month: 'short',
|
|
295
|
+
year: 'numeric',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!acc[date]) {
|
|
299
|
+
acc[date] = [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Convert amount from nicks to NOCK
|
|
303
|
+
const amountNock = (tx.amount || 0) / NOCK_TO_NICKS;
|
|
304
|
+
const type = tx.direction === 'outgoing' ? 'sent' : 'received';
|
|
305
|
+
// For incoming transactions, show sender if known, otherwise leave empty
|
|
306
|
+
const address = tx.direction === 'outgoing' ? tx.recipient : tx.sender;
|
|
307
|
+
|
|
308
|
+
// Only show USD value if we have historical price stored
|
|
309
|
+
const usdValue = tx.priceUsdAtTime
|
|
310
|
+
? `$${(amountNock * tx.priceUsdAtTime).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
311
|
+
: null;
|
|
312
|
+
|
|
313
|
+
acc[date].push({
|
|
314
|
+
type,
|
|
315
|
+
from: truncateAddress(address || ''),
|
|
316
|
+
amount:
|
|
317
|
+
type === 'sent'
|
|
318
|
+
? `-${amountNock.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} NOCK`
|
|
319
|
+
: `${amountNock.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} NOCK`,
|
|
320
|
+
usdValue,
|
|
321
|
+
status: getDisplayStatus(tx.status),
|
|
322
|
+
confirmations: tx.confirmations,
|
|
323
|
+
txid: tx.txHash || tx.id,
|
|
324
|
+
originalTx: tx, // Keep reference to original transaction
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return acc;
|
|
328
|
+
},
|
|
329
|
+
{} as Record<string, any[]>
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const transactions = Object.entries(transactionsByDate).map(([date, items]) => ({
|
|
333
|
+
date,
|
|
334
|
+
items,
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
className="w-[357px] h-[600px] overflow-hidden relative"
|
|
340
|
+
style={{ backgroundColor: 'var(--color-home-fill)', color: 'var(--color-text-primary)' }}
|
|
341
|
+
>
|
|
342
|
+
{/* Scroll container */}
|
|
343
|
+
<div
|
|
344
|
+
ref={scrollContainerRef}
|
|
345
|
+
className="relative h-full overflow-y-auto scroll-thin flex flex-col"
|
|
346
|
+
>
|
|
347
|
+
{/* Sticky header */}
|
|
348
|
+
<header
|
|
349
|
+
ref={headerRef}
|
|
350
|
+
className="sticky top-0 z-40 backdrop-blur"
|
|
351
|
+
style={{ backgroundColor: 'var(--color-home-fill)' }}
|
|
352
|
+
>
|
|
353
|
+
<div className="px-4 py-3 flex items-center justify-between min-h-[64px]">
|
|
354
|
+
<div
|
|
355
|
+
className="flex items-center gap-2"
|
|
356
|
+
role="button"
|
|
357
|
+
tabIndex={0}
|
|
358
|
+
onClick={() => setWalletDropdownOpen(o => !o)}
|
|
359
|
+
onKeyDown={e => {
|
|
360
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
setWalletDropdownOpen(o => !o);
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
aria-label="Wallet menu"
|
|
366
|
+
>
|
|
367
|
+
<div
|
|
368
|
+
className="relative h-10 w-10 rounded-tile grid place-items-center"
|
|
369
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
370
|
+
>
|
|
371
|
+
<AccountIcon
|
|
372
|
+
styleId={currentAccount?.iconStyleId}
|
|
373
|
+
color={currentAccount?.iconColor}
|
|
374
|
+
className="h-6 w-6"
|
|
375
|
+
/>
|
|
376
|
+
<div
|
|
377
|
+
className="absolute -bottom-px -right-0.5 h-2 w-2 rounded-full"
|
|
378
|
+
style={{
|
|
379
|
+
backgroundColor: isConnected ? 'var(--color-green)' : 'var(--color-red)',
|
|
380
|
+
}}
|
|
381
|
+
title={isConnected ? 'Connected' : 'Disconnected'}
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="flex flex-col min-w-0">
|
|
385
|
+
<div
|
|
386
|
+
className="font-sans text-[14px] font-medium leading-[18px] tracking-[0.14px] flex items-center gap-1"
|
|
387
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
388
|
+
>
|
|
389
|
+
{walletName}
|
|
390
|
+
<img src={WalletDropdownArrow} alt="" className="h-3 w-3" />
|
|
391
|
+
</div>
|
|
392
|
+
<div
|
|
393
|
+
className="font-sans text-[13px] leading-[18px] tracking-[0.26px] flex items-center gap-2"
|
|
394
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
395
|
+
>
|
|
396
|
+
<span className="truncate">{walletAddress}</span>
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
className="shrink-0 opacity-70 hover:opacity-40"
|
|
400
|
+
onClick={async e => {
|
|
401
|
+
e.stopPropagation();
|
|
402
|
+
try {
|
|
403
|
+
await navigator.clipboard.writeText(fullAddress);
|
|
404
|
+
setCopiedAddress(true);
|
|
405
|
+
setTimeout(() => setCopiedAddress(false), 2000);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.error('Failed to copy address:', err);
|
|
408
|
+
}
|
|
409
|
+
}}
|
|
410
|
+
aria-label="Copy address"
|
|
411
|
+
>
|
|
412
|
+
{copiedAddress ? (
|
|
413
|
+
<CheckIcon className="h-3 w-3" />
|
|
414
|
+
) : (
|
|
415
|
+
<img src={CopyIcon} alt="" className="h-3 w-3" />
|
|
416
|
+
)}
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className="flex items-center gap-2">
|
|
423
|
+
<button
|
|
424
|
+
className="h-8 w-8 rounded-tile hover:bg-black/5 grid place-items-center"
|
|
425
|
+
onClick={handleLockWallet}
|
|
426
|
+
>
|
|
427
|
+
<img src={LockIconAsset} alt="Lock" className="h-5 w-5" />
|
|
428
|
+
</button>
|
|
429
|
+
<button
|
|
430
|
+
className="h-8 w-8 rounded-tile hover:bg-black/5 grid place-items-center"
|
|
431
|
+
onClick={() => setSettingsDropdownOpen(o => !o)}
|
|
432
|
+
>
|
|
433
|
+
<img src={SettingsIconAsset} alt="Settings" className="h-5 w-5" />
|
|
434
|
+
</button>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</header>
|
|
438
|
+
|
|
439
|
+
{/* Wallet dropdown */}
|
|
440
|
+
{walletDropdownOpen && (
|
|
441
|
+
<>
|
|
442
|
+
<div className="fixed inset-0 z-40" onClick={() => setWalletDropdownOpen(false)} />
|
|
443
|
+
<div
|
|
444
|
+
className="fixed top-[64px] left-2 right-2 rounded-xl z-50 max-h-[400px] overflow-y-auto"
|
|
445
|
+
style={{
|
|
446
|
+
backgroundColor: 'var(--color-bg)',
|
|
447
|
+
border: '1px solid var(--color-surface-700)',
|
|
448
|
+
boxShadow: '0 4px 12px 0 rgba(5, 5, 5, 0.12)',
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
<div className="p-2">
|
|
452
|
+
{accounts.map(account => {
|
|
453
|
+
const isSelected = currentAccount?.index === account.index;
|
|
454
|
+
const showSelection = accounts.length > 1 && isSelected;
|
|
455
|
+
return (
|
|
456
|
+
<button
|
|
457
|
+
key={account.index}
|
|
458
|
+
onClick={() => handleSwitchAccount(account.index)}
|
|
459
|
+
className="wallet-dropdown-item w-full flex items-center gap-2 p-2 rounded-tile border transition"
|
|
460
|
+
style={{
|
|
461
|
+
backgroundColor: showSelection ? 'var(--color-bg)' : 'transparent',
|
|
462
|
+
borderColor: showSelection ? 'var(--color-text-primary)' : 'transparent',
|
|
463
|
+
}}
|
|
464
|
+
onMouseEnter={e => {
|
|
465
|
+
if (!showSelection) {
|
|
466
|
+
e.currentTarget.style.backgroundColor = 'var(--color-surface-900)';
|
|
467
|
+
}
|
|
468
|
+
}}
|
|
469
|
+
onMouseLeave={e => {
|
|
470
|
+
if (!showSelection) {
|
|
471
|
+
e.currentTarget.style.backgroundColor = showSelection
|
|
472
|
+
? 'var(--color-bg)'
|
|
473
|
+
: 'transparent';
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<div
|
|
478
|
+
className="h-10 w-10 rounded-tile grid place-items-center"
|
|
479
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
480
|
+
>
|
|
481
|
+
<AccountIcon
|
|
482
|
+
styleId={account.iconStyleId}
|
|
483
|
+
color={account.iconColor}
|
|
484
|
+
className="h-6 w-6"
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
<div className="flex-1 text-left">
|
|
488
|
+
<div
|
|
489
|
+
className="text-[14px] leading-[18px] font-medium"
|
|
490
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
491
|
+
>
|
|
492
|
+
{account.name}
|
|
493
|
+
</div>
|
|
494
|
+
<div
|
|
495
|
+
className="text-[13px] leading-[18px] tracking-[0.26px]"
|
|
496
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
497
|
+
>
|
|
498
|
+
{truncateAddress(account.address)}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
<div
|
|
502
|
+
className="wallet-balance text-[14px] font-medium whitespace-nowrap"
|
|
503
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
504
|
+
>
|
|
505
|
+
{(wallet.accountBalances[account.address] ?? 0).toLocaleString('en-US', {
|
|
506
|
+
minimumFractionDigits: 2,
|
|
507
|
+
maximumFractionDigits: 2,
|
|
508
|
+
})}{' '}
|
|
509
|
+
NOCK
|
|
510
|
+
</div>
|
|
511
|
+
<div
|
|
512
|
+
className="wallet-settings-icon h-10 w-10 rounded-tile hidden items-center justify-center"
|
|
513
|
+
style={{ backgroundColor: 'var(--color-surface-700)' }}
|
|
514
|
+
onClick={e => {
|
|
515
|
+
e.stopPropagation();
|
|
516
|
+
setWalletDropdownOpen(false);
|
|
517
|
+
navigate('wallet-settings');
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
<img src={PencilEditIcon} alt="Edit wallet" className="h-5 w-5" />
|
|
521
|
+
</div>
|
|
522
|
+
</button>
|
|
523
|
+
);
|
|
524
|
+
})}
|
|
525
|
+
</div>
|
|
526
|
+
<div className="h-px" style={{ backgroundColor: 'var(--color-divider)' }} />
|
|
527
|
+
<div className="p-2">
|
|
528
|
+
<button
|
|
529
|
+
className="w-full h-12 font-medium rounded-lg"
|
|
530
|
+
style={{ backgroundColor: 'var(--color-text-primary)', color: 'var(--color-bg)' }}
|
|
531
|
+
onClick={handleAddAccount}
|
|
532
|
+
>
|
|
533
|
+
Add Wallet
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</>
|
|
538
|
+
)}
|
|
539
|
+
|
|
540
|
+
{/* Settings dropdown */}
|
|
541
|
+
{settingsDropdownOpen && (
|
|
542
|
+
<>
|
|
543
|
+
<div className="fixed inset-0 z-40" onClick={() => setSettingsDropdownOpen(false)} />
|
|
544
|
+
<div
|
|
545
|
+
className="fixed top-[64px] right-2 w-[245px] rounded-xl p-2 z-50 flex flex-col gap-1"
|
|
546
|
+
style={{
|
|
547
|
+
backgroundColor: 'var(--color-bg)',
|
|
548
|
+
border: '1px solid var(--color-surface-700)',
|
|
549
|
+
boxShadow: '0 4px 12px 0 rgba(5, 5, 5, 0.12)',
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
{hasV0 && (
|
|
553
|
+
<DropdownItem
|
|
554
|
+
icon={KeyIcon}
|
|
555
|
+
label="Upgrade v0 → v1"
|
|
556
|
+
onClick={() => {
|
|
557
|
+
setSettingsDropdownOpen(false);
|
|
558
|
+
navigate('onboarding-import-v0');
|
|
559
|
+
}}
|
|
560
|
+
/>
|
|
561
|
+
)}
|
|
562
|
+
<DropdownItem
|
|
563
|
+
icon={ExplorerIcon}
|
|
564
|
+
label="View on explorer"
|
|
565
|
+
onClick={() =>
|
|
566
|
+
window.open(`https://nockblocks.com/address/${currentAccount?.address}`, '_blank')
|
|
567
|
+
}
|
|
568
|
+
/>
|
|
569
|
+
<DropdownItem
|
|
570
|
+
icon={PermissionsIcon}
|
|
571
|
+
label="Wallet permissions"
|
|
572
|
+
onClick={() => {
|
|
573
|
+
setSettingsDropdownOpen(false);
|
|
574
|
+
navigate('wallet-permissions');
|
|
575
|
+
}}
|
|
576
|
+
/>
|
|
577
|
+
<DropdownItem
|
|
578
|
+
icon={SettingsIconAsset}
|
|
579
|
+
label="Settings"
|
|
580
|
+
onClick={() => {
|
|
581
|
+
setSettingsDropdownOpen(false);
|
|
582
|
+
navigate('settings');
|
|
583
|
+
}}
|
|
584
|
+
/>
|
|
585
|
+
<div className="h-px my-1" style={{ backgroundColor: 'var(--color-divider)' }} />
|
|
586
|
+
<DropdownItem
|
|
587
|
+
icon={FeedbackIcon}
|
|
588
|
+
label="Wallet feedback"
|
|
589
|
+
onClick={() => window.open('https://nockchain.net/feedback', '_blank')}
|
|
590
|
+
/>
|
|
591
|
+
</div>
|
|
592
|
+
</>
|
|
593
|
+
)}
|
|
594
|
+
|
|
595
|
+
{/* Sticky balance block (lower z) */}
|
|
596
|
+
<div
|
|
597
|
+
className="sticky top-[var(--header-h)] z-10 px-4 pt-1"
|
|
598
|
+
style={{ backgroundColor: 'var(--color-home-fill)' }}
|
|
599
|
+
>
|
|
600
|
+
<div className="mb-3">
|
|
601
|
+
<div className="flex items-baseline gap-[6px]">
|
|
602
|
+
{!isInitialized || (isBalanceFetching && wallet.balance === 0) ? (
|
|
603
|
+
<>
|
|
604
|
+
<div className="h-[40px] w-32 rounded skeleton-shimmer" />
|
|
605
|
+
<div className="h-[28px] w-16 rounded skeleton-shimmer" />
|
|
606
|
+
</>
|
|
607
|
+
) : (
|
|
608
|
+
<>
|
|
609
|
+
<div
|
|
610
|
+
className="font-display font-semibold text-[36px] leading-[40px] tracking-[-0.72px]"
|
|
611
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
612
|
+
>
|
|
613
|
+
{balanceHidden ? '••••••' : balance}
|
|
614
|
+
</div>
|
|
615
|
+
<div
|
|
616
|
+
className="font-display text-[24px] leading-[28px] tracking-[-0.48px]"
|
|
617
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
618
|
+
>
|
|
619
|
+
NOCK
|
|
620
|
+
</div>
|
|
621
|
+
</>
|
|
622
|
+
)}
|
|
623
|
+
<button
|
|
624
|
+
className="ml-1"
|
|
625
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
626
|
+
onClick={toggleBalanceHidden}
|
|
627
|
+
aria-label="Toggle balance visibility"
|
|
628
|
+
>
|
|
629
|
+
{balanceHidden ? (
|
|
630
|
+
<EyeOffIcon className="h-4 w-4" />
|
|
631
|
+
) : (
|
|
632
|
+
<EyeIcon className="h-4 w-4" />
|
|
633
|
+
)}
|
|
634
|
+
</button>
|
|
635
|
+
<button
|
|
636
|
+
className="ml-1"
|
|
637
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
638
|
+
onClick={handleRefreshBalance}
|
|
639
|
+
disabled={isRefreshing || isBalanceFetching}
|
|
640
|
+
aria-label="Refresh balance"
|
|
641
|
+
>
|
|
642
|
+
<img
|
|
643
|
+
src={RefreshIcon}
|
|
644
|
+
alt="Refresh"
|
|
645
|
+
className={`h-4 w-4 ${isRefreshing || isBalanceFetching ? 'animate-spin' : ''}`}
|
|
646
|
+
style={{
|
|
647
|
+
opacity: isRefreshing || isBalanceFetching ? 0.5 : 1,
|
|
648
|
+
}}
|
|
649
|
+
/>
|
|
650
|
+
</button>
|
|
651
|
+
</div>
|
|
652
|
+
<div className="mt-1 text-[13px] font-medium leading-[18px] flex items-center gap-1">
|
|
653
|
+
{isPriceFetching || (isBalanceFetching && wallet.balance === 0) ? (
|
|
654
|
+
<div className="h-[14px] w-36 rounded skeleton-shimmer" />
|
|
655
|
+
) : (
|
|
656
|
+
<>
|
|
657
|
+
<img
|
|
658
|
+
src={priceChange24h >= 0 ? TrendUpArrow : TrendDownArrow}
|
|
659
|
+
alt={priceChange24h >= 0 ? 'up' : 'down'}
|
|
660
|
+
className="h-4 w-4"
|
|
661
|
+
/>
|
|
662
|
+
<span
|
|
663
|
+
style={{
|
|
664
|
+
color: priceChange24h >= 0 ? 'var(--color-green)' : 'var(--color-red)',
|
|
665
|
+
}}
|
|
666
|
+
>
|
|
667
|
+
{balanceHidden
|
|
668
|
+
? '••••• •••••'
|
|
669
|
+
: `$${formattedUsdValue} (${priceChange24h >= 0 ? '+' : '-'}${formattedPercentChange}%)`}
|
|
670
|
+
</span>
|
|
671
|
+
</>
|
|
672
|
+
)}
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
{/* Actions */}
|
|
677
|
+
<div className="grid grid-cols-2 gap-2 mb-3">
|
|
678
|
+
<div className="relative">
|
|
679
|
+
<button
|
|
680
|
+
className="btn-primary w-full rounded-card shadow-card flex flex-col items-start justify-center gap-4 p-3 font-sans text-[14px] font-medium transition-all hover:opacity-90 active:scale-[0.98]"
|
|
681
|
+
style={{ color: '#000' }}
|
|
682
|
+
onClick={() => navigate('send')}
|
|
683
|
+
>
|
|
684
|
+
<SendPaperPlaneIcon className="h-5 w-5" />
|
|
685
|
+
Send
|
|
686
|
+
</button>
|
|
687
|
+
</div>
|
|
688
|
+
<button
|
|
689
|
+
className="btn-secondary rounded-card shadow-card flex flex-col items-start justify-center gap-4 p-3 font-sans text-[14px] font-medium transition-all hover:opacity-90 active:scale-[0.98]"
|
|
690
|
+
style={{
|
|
691
|
+
color: 'var(--color-text-primary)',
|
|
692
|
+
}}
|
|
693
|
+
onClick={() => navigate('receive')}
|
|
694
|
+
>
|
|
695
|
+
<ReceiveCircleIcon className="h-5 w-5" />
|
|
696
|
+
Receive
|
|
697
|
+
</button>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
<section
|
|
702
|
+
className={`relative z-20 shadow-card rounded-t-xl transition-all duration-300 flex-1 flex flex-col ${
|
|
703
|
+
isTransactionsStuck ? '' : 'mx-2 mt-4'
|
|
704
|
+
}`}
|
|
705
|
+
style={{
|
|
706
|
+
backgroundColor: 'var(--color-home-accent)',
|
|
707
|
+
border: '1px solid var(--color-divider)',
|
|
708
|
+
}}
|
|
709
|
+
>
|
|
710
|
+
{/* Sticky header inside the sheet (matches scroll state 2) */}
|
|
711
|
+
<div
|
|
712
|
+
className="sticky top-[var(--header-h)] z-10 px-4 py-3 rounded-t-xl"
|
|
713
|
+
style={{
|
|
714
|
+
backgroundColor: 'var(--color-home-accent)',
|
|
715
|
+
borderBottom: '1px solid var(--color-divider)',
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
<div className="flex items-center justify-between">
|
|
719
|
+
<h2
|
|
720
|
+
className="font-display text-[14px] font-medium"
|
|
721
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
722
|
+
>
|
|
723
|
+
Recent Transactions
|
|
724
|
+
</h2>
|
|
725
|
+
<a
|
|
726
|
+
href={`https://nockblocks.com/address/${currentAccount?.address}`}
|
|
727
|
+
target="_blank"
|
|
728
|
+
rel="noopener noreferrer"
|
|
729
|
+
className="text-[12px] font-medium rounded-full pl-[12px] pr-[16px] py-[3px] flex items-center gap-[4px] transition-opacity"
|
|
730
|
+
style={{
|
|
731
|
+
border: '1px solid var(--color-text-primary)',
|
|
732
|
+
color: 'var(--color-text-primary)',
|
|
733
|
+
}}
|
|
734
|
+
onMouseEnter={e => (e.currentTarget.style.opacity = '0.7')}
|
|
735
|
+
onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
|
|
736
|
+
>
|
|
737
|
+
<ArrowUpRightIcon className="h-[10px] w-[10px]" />
|
|
738
|
+
View all
|
|
739
|
+
</a>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
{/* Groups */}
|
|
744
|
+
<div className="px-4 pb-6 flex-1 flex flex-col">
|
|
745
|
+
{displayTransactions.length === 0 ? (
|
|
746
|
+
/* Empty state */
|
|
747
|
+
<div className="flex flex-col items-center justify-center gap-2 flex-1">
|
|
748
|
+
<div
|
|
749
|
+
className="h-10 w-10 rounded-full flex items-center justify-center"
|
|
750
|
+
style={{ backgroundColor: 'var(--color-tx-icon)' }}
|
|
751
|
+
>
|
|
752
|
+
<img src={ReceiptIcon} alt="" className="h-5 w-5" />
|
|
753
|
+
</div>
|
|
754
|
+
<div className="text-center">
|
|
755
|
+
<p
|
|
756
|
+
className="font-display font-medium text-[14px] leading-[18px] tracking-[0.14px] m-0"
|
|
757
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
758
|
+
>
|
|
759
|
+
{displayBalance === 0
|
|
760
|
+
? 'Your wallet is ready to receive NOCK.'
|
|
761
|
+
: 'Make your first NOCK transaction.'}
|
|
762
|
+
</p>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
) : (
|
|
766
|
+
transactions.map((group, idx) => (
|
|
767
|
+
<div
|
|
768
|
+
key={idx}
|
|
769
|
+
className={idx === 0 ? 'pt-4' : 'pt-4'}
|
|
770
|
+
style={idx !== 0 ? { borderTop: '1px solid var(--color-divider)' } : undefined}
|
|
771
|
+
>
|
|
772
|
+
<div
|
|
773
|
+
className="font-display font-medium text-[14px] leading-[18px] tracking-[0.14px] mb-3"
|
|
774
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
775
|
+
>
|
|
776
|
+
{group.date}
|
|
777
|
+
</div>
|
|
778
|
+
<div>
|
|
779
|
+
{group.items.map((t, i) => (
|
|
780
|
+
<button
|
|
781
|
+
key={i}
|
|
782
|
+
className="w-full flex items-start gap-3 py-3 rounded-lg px-0 -mx-0 overflow-hidden"
|
|
783
|
+
onClick={() => {
|
|
784
|
+
setSelectedTransaction(t.originalTx);
|
|
785
|
+
navigate('tx-details');
|
|
786
|
+
}}
|
|
787
|
+
>
|
|
788
|
+
<div
|
|
789
|
+
className="h-10 w-10 shrink-0 rounded-full grid place-items-center"
|
|
790
|
+
style={{ backgroundColor: 'var(--color-tx-icon)' }}
|
|
791
|
+
>
|
|
792
|
+
{t.type === 'received' ? (
|
|
793
|
+
<ReceiveArrowIcon
|
|
794
|
+
className="h-4 w-4"
|
|
795
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
796
|
+
/>
|
|
797
|
+
) : (
|
|
798
|
+
<SentArrowIcon
|
|
799
|
+
className="h-4 w-4"
|
|
800
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
801
|
+
/>
|
|
802
|
+
)}
|
|
803
|
+
</div>
|
|
804
|
+
<div className="flex-1 min-w-0 text-left">
|
|
805
|
+
<div
|
|
806
|
+
className="text-[14px] font-medium truncate"
|
|
807
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
808
|
+
>
|
|
809
|
+
{t.type === 'received' ? 'Received' : 'Sent'}
|
|
810
|
+
</div>
|
|
811
|
+
<div
|
|
812
|
+
className="text-[12px] flex items-center gap-1.5 truncate"
|
|
813
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
814
|
+
>
|
|
815
|
+
{t.status === 'pending' && (
|
|
816
|
+
<>
|
|
817
|
+
<span style={{ color: '#C88414' }}>Pending</span>
|
|
818
|
+
<span>·</span>
|
|
819
|
+
</>
|
|
820
|
+
)}
|
|
821
|
+
{t.status === 'failed' && (
|
|
822
|
+
<>
|
|
823
|
+
<span style={{ color: 'var(--color-red)' }}>Failed</span>
|
|
824
|
+
<span>·</span>
|
|
825
|
+
</>
|
|
826
|
+
)}
|
|
827
|
+
{t.status === 'expired' && (
|
|
828
|
+
<>
|
|
829
|
+
<span style={{ color: 'var(--color-text-muted)' }}>Expired</span>
|
|
830
|
+
<span>·</span>
|
|
831
|
+
</>
|
|
832
|
+
)}
|
|
833
|
+
<span className="truncate">{t.from}</span>
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
<div className="text-right shrink-0 pr-0">
|
|
837
|
+
<div
|
|
838
|
+
className="text-[14px] font-medium whitespace-nowrap"
|
|
839
|
+
style={{
|
|
840
|
+
color:
|
|
841
|
+
t.type === 'received'
|
|
842
|
+
? 'var(--color-green)'
|
|
843
|
+
: 'var(--color-text-primary)',
|
|
844
|
+
}}
|
|
845
|
+
>
|
|
846
|
+
{t.amount}
|
|
847
|
+
</div>
|
|
848
|
+
{t.usdValue && (
|
|
849
|
+
<div
|
|
850
|
+
className="text-[12px] whitespace-nowrap"
|
|
851
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
852
|
+
>
|
|
853
|
+
{t.usdValue}
|
|
854
|
+
</div>
|
|
855
|
+
)}
|
|
856
|
+
</div>
|
|
857
|
+
</button>
|
|
858
|
+
))}
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
))
|
|
862
|
+
)}
|
|
863
|
+
</div>
|
|
864
|
+
</section>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function DropdownItem({
|
|
871
|
+
icon,
|
|
872
|
+
label,
|
|
873
|
+
onClick,
|
|
874
|
+
}: {
|
|
875
|
+
icon: string;
|
|
876
|
+
label: string;
|
|
877
|
+
onClick: () => void;
|
|
878
|
+
}) {
|
|
879
|
+
return (
|
|
880
|
+
<button
|
|
881
|
+
onClick={onClick}
|
|
882
|
+
className="w-full flex items-center gap-2 p-2 rounded-lg text-left transition-colors"
|
|
883
|
+
style={{ backgroundColor: 'transparent' }}
|
|
884
|
+
onMouseEnter={e => {
|
|
885
|
+
e.currentTarget.style.backgroundColor = 'var(--color-surface-900)';
|
|
886
|
+
}}
|
|
887
|
+
onMouseLeave={e => {
|
|
888
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
889
|
+
}}
|
|
890
|
+
>
|
|
891
|
+
<div
|
|
892
|
+
className="h-8 w-8 rounded-tile grid place-items-center"
|
|
893
|
+
style={{ backgroundColor: 'var(--color-surface-800)' }}
|
|
894
|
+
>
|
|
895
|
+
<img src={icon} className="h-5 w-5" alt="" />
|
|
896
|
+
</div>
|
|
897
|
+
<span className="text-[14px] font-medium" style={{ color: 'var(--color-text-primary)' }}>
|
|
898
|
+
{label}
|
|
899
|
+
</span>
|
|
900
|
+
</button>
|
|
901
|
+
);
|
|
902
|
+
}
|