Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .storybook/decorators/withStealthKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ const baseValue: StealthKeysValue = {
clearStellar: noop,
clearSolana: noop,
clearCkb: noop,
getKeysForProfile: () => ({
evmKeys: null,
evmMetaAddress: null,
stellarKeys: null,
stellarMetaAddress: null,
solanaKeys: null,
solanaMetaAddress: null,
ckbKeys: null,
ckbMetaAddress: null,
}),
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Schedule from '@/pages/Schedule';
import StellarSplit from '@/pages/StellarSplit';
import Names from '@/pages/Names';
import Activity from '@/pages/Activity';
import Portfolio from '@/pages/Portfolio';
import Debug from '@/pages/Debug';

export function App() {
Expand All @@ -39,6 +40,7 @@ export function App() {
<Route path="/names" element={<Names />} />
<Route path="/activity" element={<Activity />} />
<Route path="/history" element={<Activity />} />
<Route path="/portfolio" element={<Portfolio />} />
<Route path="/debug" element={<Debug />} />
<Route path="*" element={<Navigate to="/send" replace />} />
</Routes>
Expand Down
66 changes: 48 additions & 18 deletions src/components/AutoSign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import type { HexString as CkbHexString } from '@wraith-protocol/sdk/chains/ckb'
import { useStealthKeys } from '@/context/StealthKeysContext';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { useChain } from '@/context/ChainContext';
import { useProfilesStore } from '@/store/profilesStore';
import { profileSigningMessage } from '@/lib/profileSigningMessage';

function HorizenAutoSign() {
const { isConnected, address, connector } = useAccount();
const { data: connectorClient } = useConnectorClient();
const { signMessageAsync } = useSignMessage();
const { evmKeys, setEvmKeys, setEvmMetaAddress, clearEvm } = useStealthKeys();
const activeProfileId = useProfilesStore((s) => s.activeProfileId);
const prompted = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const isLoading = useRef(false);
Expand All @@ -49,14 +52,16 @@ function HorizenAutoSign() {
if (!ready || !address) return;
if (evmKeys) return;
if (isLoading.current) return;
if (prompted.current === address) return;
const promptKey = `${address}:${activeProfileId}`;
if (prompted.current === promptKey) return;

prompted.current = address;
prompted.current = promptKey;
isLoading.current = true;

(async () => {
try {
const signature = await signMessageAsync({ message: STEALTH_SIGNING_MESSAGE });
const message = profileSigningMessage(STEALTH_SIGNING_MESSAGE, activeProfileId);
const signature = await signMessageAsync({ message });
const keys = deriveStealthKeys(signature as HexString);
const meta = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey);
setEvmKeys(keys);
Expand All @@ -67,7 +72,7 @@ function HorizenAutoSign() {
isLoading.current = false;
}
})();
}, [ready, address, evmKeys, signMessageAsync, setEvmKeys, setEvmMetaAddress]);
}, [ready, address, evmKeys, activeProfileId, signMessageAsync, setEvmKeys, setEvmMetaAddress]);

useEffect(() => {
if (!isConnected) {
Expand All @@ -83,6 +88,7 @@ function HorizenAutoSign() {
function StellarAutoSign() {
const { isConnected, address, signMessage } = useStellarWallet();
const { stellarKeys, setStellarKeys, setStellarMetaAddress, clearStellar } = useStealthKeys();
const activeProfileId = useProfilesStore((s) => s.activeProfileId);
const prompted = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const isLoading = useRef(false);
Expand All @@ -99,14 +105,16 @@ function StellarAutoSign() {
if (!ready || !address) return;
if (stellarKeys) return;
if (isLoading.current) return;
if (prompted.current === address) return;
const promptKey = `${address}:${activeProfileId}`;
if (prompted.current === promptKey) return;

prompted.current = address;
prompted.current = promptKey;
isLoading.current = true;

(async () => {
try {
const signature = await signMessage(STELLAR_SIGNING_MESSAGE);
const message = profileSigningMessage(STELLAR_SIGNING_MESSAGE, activeProfileId);
const signature = await signMessage(message);
const keys = deriveStellarKeys(signature);
const meta = encodeStellarMeta(keys.spendingPubKey, keys.viewingPubKey);
setStellarKeys(keys);
Expand All @@ -117,7 +125,15 @@ function StellarAutoSign() {
isLoading.current = false;
}
})();
}, [ready, address, stellarKeys, signMessage, setStellarKeys, setStellarMetaAddress]);
}, [
ready,
address,
stellarKeys,
activeProfileId,
signMessage,
setStellarKeys,
setStellarMetaAddress,
]);

useEffect(() => {
if (!isConnected) {
Expand All @@ -133,6 +149,7 @@ function StellarAutoSign() {
function SolanaAutoSign() {
const { connected, publicKey, signMessage } = useWallet();
const { solanaKeys, setSolanaKeys, setSolanaMetaAddress, clearSolana } = useStealthKeys();
const activeProfileId = useProfilesStore((s) => s.activeProfileId);
const prompted = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const isLoading = useRef(false);
Expand All @@ -150,14 +167,16 @@ function SolanaAutoSign() {
if (solanaKeys) return;
if (isLoading.current) return;
const addr = publicKey.toBase58();
if (prompted.current === addr) return;
const promptKey = `${addr}:${activeProfileId}`;
if (prompted.current === promptKey) return;

prompted.current = addr;
prompted.current = promptKey;
isLoading.current = true;

(async () => {
try {
const msgBytes = new TextEncoder().encode(SOLANA_SIGNING_MESSAGE);
const message = profileSigningMessage(SOLANA_SIGNING_MESSAGE, activeProfileId);
const msgBytes = new TextEncoder().encode(message);
const signature = await signMessage(msgBytes);
const keys = deriveSolanaKeys(signature);
const meta = encodeSolanaMeta(keys.spendingPubKey, keys.viewingPubKey);
Expand All @@ -169,7 +188,15 @@ function SolanaAutoSign() {
isLoading.current = false;
}
})();
}, [ready, publicKey, solanaKeys, signMessage, setSolanaKeys, setSolanaMetaAddress]);
}, [
ready,
publicKey,
solanaKeys,
activeProfileId,
signMessage,
setSolanaKeys,
setSolanaMetaAddress,
]);

useEffect(() => {
if (!connected) {
Expand All @@ -186,7 +213,8 @@ function CkbAutoSign() {
const { wallet } = ccc.useCcc();
const signer = ccc.useSigner();
const { ckbKeys, setCkbKeys, setCkbMetaAddress, clearCkb } = useStealthKeys();
const prompted = useRef(false);
const activeProfileId = useProfilesStore((s) => s.activeProfileId);
const prompted = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const isLoading = useRef(false);

Expand All @@ -202,14 +230,16 @@ function CkbAutoSign() {
if (!ready || !signer) return;
if (ckbKeys) return;
if (isLoading.current) return;
if (prompted.current) return;
const promptKey = activeProfileId;
if (prompted.current === promptKey) return;

prompted.current = true;
prompted.current = promptKey;
isLoading.current = true;

(async () => {
try {
const sig = await (signer as any).signMessageRaw(CKB_SIGNING_MESSAGE);
const message = profileSigningMessage(CKB_SIGNING_MESSAGE, activeProfileId);
const sig = await (signer as any).signMessageRaw(message);
const sigStr = typeof sig === 'string' ? sig : `0x${Buffer.from(sig).toString('hex')}`;
const sigHex = sigStr.startsWith('0x') ? sigStr : `0x${sigStr}`;
const derived = deriveCkbKeys(sigHex as CkbHexString);
Expand All @@ -222,11 +252,11 @@ function CkbAutoSign() {
isLoading.current = false;
}
})();
}, [ready, signer, ckbKeys, setCkbKeys, setCkbMetaAddress]);
}, [ready, signer, ckbKeys, activeProfileId, setCkbKeys, setCkbMetaAddress]);

useEffect(() => {
if (!wallet) {
prompted.current = false;
prompted.current = null;
setReady(false);
clearCkb();
}
Expand Down
8 changes: 6 additions & 2 deletions src/components/CkbReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useStealthKeys } from '@/context/StealthKeysContext';
import { EmptyState } from '@/components/EmptyState';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';
import { useProfilesStore } from '@/store/profilesStore';
import { profileSigningMessage } from '@/lib/profileSigningMessage';

function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
const { t } = useTranslation();
Expand Down Expand Up @@ -88,6 +90,7 @@ export function CkbReceive() {
const { wallet } = ccc.useCcc();
const signer = ccc.useSigner();
const { ckbKeys, ckbMetaAddress, setCkbKeys, setCkbMetaAddress } = useStealthKeys();
const activeProfileId = useProfilesStore((s) => s.activeProfileId);

const [isDerivingKeys, setIsDerivingKeys] = useState(false);
const [isScanning, setIsScanning] = useState(false);
Expand All @@ -103,7 +106,8 @@ export function CkbReceive() {
setIsDerivingKeys(true);
setError('');
try {
const sig = await (signer as any).signMessageRaw(STEALTH_SIGNING_MESSAGE);
const message = profileSigningMessage(STEALTH_SIGNING_MESSAGE, activeProfileId);
const sig = await (signer as any).signMessageRaw(message);
const sigStr = typeof sig === 'string' ? sig : `0x${Buffer.from(sig).toString('hex')}`;
const sigHex = sigStr.startsWith('0x') ? sigStr : `0x${sigStr}`;

Expand All @@ -122,7 +126,7 @@ export function CkbReceive() {
} finally {
setIsDerivingKeys(false);
}
}, [signer, setCkbKeys, setCkbMetaAddress, t]);
}, [signer, activeProfileId, setCkbKeys, setCkbMetaAddress, t]);

const scanPayments = useCallback(async () => {
if (!ckbKeys) return;
Expand Down
56 changes: 55 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ChainSwitcher } from './ChainSwitcher';
import { WalletConnect } from './WalletConnect';
import { LocaleSwitcher } from './LocaleSwitcher';
import { NetworkChip } from './NetworkChip';
import { ProfileSwitcher } from './ProfileSwitcher';
import { useTheme } from '@/context/ThemeContext';
import { useNotificationsStore } from '@/stores/notificationsStore';

const INSTALL_PROMPT_DISMISSED_KEY = 'wraith:pwa-install-dismissed';

interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
}

export function Header() {
const location = useLocation();
const { t } = useTranslation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const { theme, toggleTheme } = useTheme();
const unreadCount = useNotificationsStore((state) => state.unreadCount());

useEffect(() => {
const captureInstallPrompt = (event: Event) => {
event.preventDefault();
if (localStorage.getItem(INSTALL_PROMPT_DISMISSED_KEY) === 'true') return;
setInstallPrompt(event as BeforeInstallPromptEvent);
};
const hideInstallPrompt = () => setInstallPrompt(null);

window.addEventListener('beforeinstallprompt', captureInstallPrompt);
window.addEventListener('appinstalled', hideInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', captureInstallPrompt);
window.removeEventListener('appinstalled', hideInstallPrompt);
};
}, []);

const installApp = async () => {
if (!installPrompt) return;

const prompt = installPrompt;
setInstallPrompt(null);
await prompt.prompt();
const choice = await prompt.userChoice;
if (choice.outcome === 'dismissed') {
localStorage.setItem(INSTALL_PROMPT_DISMISSED_KEY, 'true');
}
};

const navLinks = [
{ to: '/send', label: t('nav.send') },
{ to: '/receive', label: t('nav.receive') },
{ to: '/schedule', label: t('nav.schedule') },
{ to: '/names', label: t('nav.names') },
{ to: '/activity', label: t('nav.activity') },
{ to: '/portfolio', label: t('nav.portfolio') },
];

return (
Expand Down Expand Up @@ -60,6 +98,15 @@ export function Header() {
</div>

<div className="flex items-center gap-2 sm:gap-3">
{installPrompt && (
<button
type="button"
onClick={installApp}
className="h-8 border border-primary px-3 font-heading text-[10px] font-semibold uppercase tracking-widest text-primary transition-colors hover:bg-primary hover:text-surface"
>
Install
</button>
)}
<LocaleSwitcher />
<button
onClick={toggleTheme}
Expand Down Expand Up @@ -92,6 +139,7 @@ export function Header() {
<div className="hidden sm:flex sm:items-center sm:gap-3">
<ChainSwitcher />
<NetworkChip />
<ProfileSwitcher />
<WalletConnect />
</div>
<button
Expand Down Expand Up @@ -153,6 +201,12 @@ export function Header() {
</span>
<ChainSwitcher />
</div>
<div className="flex items-center justify-between gap-3">
<span className="font-heading text-[10px] uppercase tracking-widest text-outline">
Profile
</span>
<ProfileSwitcher />
</div>
<div className="flex items-center justify-between gap-3">
<span className="font-heading text-[10px] uppercase tracking-widest text-outline">
{t('header.wallet')}
Expand Down
6 changes: 5 additions & 1 deletion src/components/HorizenReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { trackEvent } from '@/lib/telemetry';
import { useProfilesStore } from '@/store/profilesStore';
import { profileSigningMessage } from '@/lib/profileSigningMessage';
import { CopyButton } from '@/components/CopyButton';
import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer';
import { EmptyState } from '@/components/EmptyState';
Expand Down Expand Up @@ -228,6 +230,7 @@ export function HorizenReceive() {
const { isConnected, address } = useAccount();
const { signMessageAsync } = useSignMessage();
const { evmKeys, evmMetaAddress, setEvmKeys, setEvmMetaAddress } = useStealthKeys();
const activeProfileId = useProfilesStore((s) => s.activeProfileId);

const [isDerivingKeys, setIsDerivingKeys] = useState(false);
const [isScanning, setIsScanning] = useState(false);
Expand Down Expand Up @@ -268,7 +271,8 @@ export function HorizenReceive() {
setIsDerivingKeys(true);
setError('');
try {
const signature = await signMessageAsync({ message: STEALTH_SIGNING_MESSAGE });
const message = profileSigningMessage(STEALTH_SIGNING_MESSAGE, activeProfileId);
const signature = await signMessageAsync({ message });
const derived = deriveStealthKeys(signature as HexString);
setEvmKeys(derived);
const meta = encodeStealthMetaAddress(derived.spendingPubKey, derived.viewingPubKey);
Expand Down
Loading