Skip to content
Merged
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
5 changes: 4 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
content="Send and receive private payments on Horizen and Stellar using stealth addresses."
/>
<meta name="twitter:image" content="/og-image.png" />

<!-- Plausible Analytics -->
<script defer data-domain="demo.usewraith.xyz" src="https://plausible.io/js/script.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>

<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
Expand Down
8 changes: 7 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { AutoSign } from '@/components/AutoSign';
import { TelemetryBanner } from '@/components/TelemetryBanner';
import Send from '@/pages/Send';
import Receive from '@/pages/Receive';
import Privacy from '@/pages/Privacy';
import { HelpButton } from '@/components/HelpButton';
import Send from '@/pages/Send';
import Receive from '@/pages/Receive';
Expand All @@ -10,11 +14,13 @@ export function App() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<TelemetryBanner />
<AutoSign />
<main className="mx-auto w-full max-w-[720px] flex-1 px-4 pb-16 pt-8 sm:px-6 sm:pb-24 sm:pt-10">
<Routes>
<Route path="/send" element={<Send />} />
<Route path="/receive" element={<Receive />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/vault" element={<Vault />} />
<Route path="/pay" element={<Send />} />
<Route path="*" element={<Navigate to="/send" replace />} />
Expand All @@ -23,4 +29,4 @@ export function App() {
<HelpButton />
</div>
);
}
}
2 changes: 2 additions & 0 deletions src/components/CkbReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@wraith-protocol/sdk/chains/ckb';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';

function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
const [showKey, setShowKey] = useState(false);
Expand Down Expand Up @@ -129,6 +130,7 @@ export function CkbReceive() {
);
setMatched(results);
setHasScanned(true);
trackEvent('scan_triggered');
} catch (err) {
setError(err instanceof Error ? err.message : 'Scan failed');
} finally {
Expand Down
2 changes: 2 additions & 0 deletions src/components/CkbSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getDeployment,
} from '@wraith-protocol/sdk/chains/ckb';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';

const STEALTH_LOCK_CODE_HASH = getDeployment('ckb').contracts.stealthLockCodeHash;

Expand Down Expand Up @@ -71,6 +72,7 @@ export function CkbSend() {
await tx.completeFeeBy(signer, 1000);

const hash = await signer.sendTransaction(tx);
trackEvent('send_submitted');
setTxHash(hash);
} catch (err) {
setError(err instanceof Error ? err.message : 'Transaction failed');
Expand Down
3 changes: 3 additions & 0 deletions src/components/HorizenReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@wraith-protocol/sdk/chains/evm';
import type { HexString, MatchedAnnouncement } from '@wraith-protocol/sdk/chains/evm';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { trackEvent } from '@/lib/telemetry';
import { CopyButton } from '@/components/CopyButton';
import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer';
import { horizenTestnet } from '@/config';
Expand Down Expand Up @@ -87,6 +88,7 @@ function StealthRow({
});

setWithdrawHash(hash);
trackEvent('withdraw');
onWithdrawn(hash);
} catch (err) {
setError(err instanceof Error ? err.message : 'Withdraw failed');
Expand Down Expand Up @@ -278,6 +280,7 @@ export function HorizenReceive() {
);
setMatched(results);
setHasScanned(true);
trackEvent('scan_triggered');
} catch (err) {
setError(err instanceof Error ? err.message : 'Scan failed');
} finally {
Expand Down
2 changes: 2 additions & 0 deletions src/components/HorizenSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { HexString, BuildSendStealthResult } from '@wraith-protocol/sdk/cha
import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer';
import { horizenTestnet } from '@/config';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';

export function HorizenSend() {
const { isConnected, address } = useAccount();
Expand Down Expand Up @@ -61,6 +62,7 @@ export function HorizenSend() {
});

setStealthResult(result);
trackEvent('send_submitted');

sendTransaction({
to: result.transaction.to as `0x${string}`,
Expand Down
3 changes: 3 additions & 0 deletions src/components/SolanaReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import type { MatchedAnnouncement } from '@wraith-protocol/sdk/chains/solana';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';
import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer';
import { SOLANA_NETWORK } from '@/config';

Expand Down Expand Up @@ -92,6 +93,7 @@ function SolanaStealthRow({
await connection.confirmTransaction(txId, 'confirmed');

setWithdrawHash(txId);
trackEvent('withdraw');
onWithdrawn();
} catch (err) {
setError(err instanceof Error ? err.message : 'Withdraw failed');
Expand Down Expand Up @@ -245,6 +247,7 @@ export function SolanaReceive() {
);
setMatched(results);
setHasScanned(true);
trackEvent('scan_triggered');
} catch (err) {
setError(err instanceof Error ? err.message : 'Scan failed');
} finally {
Expand Down
2 changes: 2 additions & 0 deletions src/components/SolanaSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { buildSendSol, bytesToHex } from '@wraith-protocol/sdk/chains/solana';
import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer';
import { SOLANA_NETWORK } from '@/config';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';

export function SolanaSend() {
const { publicKey, connected, sendTransaction } = useWallet();
Expand Down Expand Up @@ -48,6 +49,7 @@ export function SolanaSend() {
amount: lamports,
senderPubkey: publicKey.toBase58(),
});
trackEvent('send_submitted');
setStealthResult({
stealthAddress: result.stealthAddress,
ephemeralPubKey: result.ephemeralPubKey,
Expand Down
50 changes: 7 additions & 43 deletions src/components/StellarReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ import type {
} from '@wraith-protocol/sdk/chains/stellar';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { StellarMatchCard } from '@/components/StellarMatchCard';
import { StellarReceiveView } from '@/components/StellarReceiveView';
import { QRCodeModal } from '@/components/QRCodeModal';
import { useStealthLabels } from '@/hooks/useStealthLabels';
import { useStellarNotifications } from '@/hooks/useStellarNotifications';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';
import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer';
import { STELLAR_NETWORK } from '@/config';
import { fetchWithRetry, withRetry, RetryExhaustedError } from '@/lib/stellar/retry';
import { useActivityStore } from '@/stores/activityStore';
Expand Down Expand Up @@ -318,7 +316,7 @@ function StellarMatchCardContainer({
}

setWithdrawHash(submitData.hash);
updateActivity(txHashHex, 'confirmed');
trackEvent('withdraw');
onWithdrawn();
} catch (err) {
setRetryStatus('');
Expand Down Expand Up @@ -1026,43 +1024,9 @@ export function StellarReceive() {
new URL('../workers/stellar-scanner.worker.ts', import.meta.url),
{ type: 'module' },
);
workerRef.current.onmessage = (e) => {
if (e.data.type === 'SUCCESS') {
const results = e.data.results;
setMatched(results);

results.forEach((m: MatchedAnnouncement) => {
addActivity({
id: m.stealthAddress, // use address as unique id for receives
chain: 'stellar',
wallet: address || '',
kind: 'stealth-receive',
direction: 'in',
status: 'confirmed', // immediately confirmed since it's discovered
timestamp: Date.now(),
});
});

setHasScanned(true);
setIsScanning(false);
} else if (e.data.type === 'ERROR') {
setError(e.data.error);
setIsScanning(false);
}
};

workerRef.current.onerror = () => {
setError('Worker crashed');
setIsScanning(false);
};

workerRef.current.postMessage({
rpcUrl: STELLAR_NETWORK.rpcUrl,
announcerContract: ANNOUNCER_CONTRACT,
viewingKey: stellarKeys.viewingKey,
spendingPubKey: stellarKeys.spendingPubKey,
spendingScalar: stellarKeys.spendingScalar,
});
setMatched(results);
setHasScanned(true);
trackEvent('scan_triggered');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start worker');
setIsScanning(false);
Expand Down
12 changes: 3 additions & 9 deletions src/components/StellarSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,8 @@ import {
} from '@wraith-protocol/sdk/chains/stellar';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { STELLAR_NETWORK } from '@/config';
import { StellarSendView } from '@/components/StellarSendView';
import { useActivityStore } from '@/stores/activityStore';
import { QrReader } from 'react-qr-reader';
import {
emptyStellarSendSimulation,
simulateStellarSendAnnouncement,
type StellarSendSimulationState,
} from '@/lib/stellarSimulation';
import { fetchWithRetry, withRetry, RetryExhaustedError } from '@/lib/stellar/retry';
import { CopyButton } from '@/components/CopyButton';
import { trackEvent } from '@/lib/telemetry';

const ANNOUNCER_CONTRACT = 'CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL';
const STELLAR_BASE_FEE_XLM = 0.00001;
Expand Down Expand Up @@ -403,6 +396,7 @@ export function StellarSend() {
const decoded = decodeStealthMetaAddress(metaAddress);
const result = generateStealthAddress(decoded.spendingPubKey, decoded.viewingPubKey);
setStealthResult(result);
trackEvent('send_submitted');

const horizonUrl = STELLAR_NETWORK.horizonUrl;
const networkPassphrase = STELLAR_NETWORK.networkPassphrase;
Expand Down
51 changes: 51 additions & 0 deletions src/components/TelemetryBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { getConsent, setConsent, type ConsentState } from '@/lib/telemetry';

export function TelemetryBanner() {
const [consent, setConsentState] = useState<ConsentState>(null);

useEffect(() => {
setConsentState(getConsent());
}, []);

if (consent !== null) return null;

function handleAccept() {
setConsent('accepted');
setConsentState('accepted');
}

function handleDecline() {
setConsent('declined');
setConsentState('declined');
}

return (
<div className="border-b border-outline-variant bg-surface-container px-4 py-3 sm:px-6">
<div className="mx-auto flex max-w-[720px] flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-on-surface-variant">
We use cookieless analytics to improve this demo.{' '}
<Link to="/privacy" className="text-primary underline underline-offset-2">
Privacy page
</Link>
. No wallet addresses or amounts are ever collected.
</p>
<div className="flex shrink-0 gap-2">
<button
onClick={handleDecline}
className="border border-outline-variant px-3 py-1 text-xs text-on-surface-variant hover:border-outline hover:text-on-surface"
>
Decline
</button>
<button
onClick={handleAccept}
className="border border-primary px-3 py-1 text-xs text-primary hover:bg-primary hover:text-surface"
>
Accept
</button>
</div>
</div>
</div>
);
}
18 changes: 14 additions & 4 deletions src/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { useWallet } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { ccc } from '@ckb-ccc/connector-react';
import { useChain } from '@/context/ChainContext';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { trackEvent } from '@/lib/telemetry';

const btnBase =
'bg-transparent border border-outline-variant px-3 py-1.5 font-heading text-[10px] uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright disabled:opacity-50 sm:px-4 sm:py-2 sm:text-xs h-8 sm:h-9';
const btnConnected =
'bg-transparent border border-outline-variant px-3 py-1.5 font-mono text-[10px] text-primary transition-colors hover:bg-surface-bright sm:px-4 sm:py-2 sm:text-xs h-8 sm:h-9';
import { useStellarWallet as useStellarWalletContext } from '@/context/StellarWalletContext';
import { useStellarWallet as useStellarWalletHook } from '@/hooks/useStellarWallet';
import { StellarWalletPicker } from '@/components/StellarWalletPicker';
Expand All @@ -26,7 +33,7 @@ function HorizenButton() {
})}
>
{!connected ? (
<button onClick={openConnectModal} className={btnBase}>
<button onClick={() => { openConnectModal(); trackEvent('connect_wallet'); }} className={btnBase}>
Connect Wallet
</button>
) : (
Expand Down Expand Up @@ -74,6 +81,9 @@ function FreighterButton() {
: 'disconnected';

return (
<button onClick={() => { connect(); trackEvent('connect_wallet'); }} className={btnBase}>
Connect Freighter
</button>
<div className="flex flex-col items-end gap-1">
<button
onClick={async () => {
Expand Down Expand Up @@ -110,8 +120,8 @@ function SolanaButton() {
</button>
);
}

return <WalletMultiButton className={btnBase} />;
return <WalletMultiButton className={btnBase} onClick={() => trackEvent('connect_wallet')} />;

}

function CkbButton() {
Expand All @@ -136,7 +146,7 @@ function CkbButton() {
}

return (
<button onClick={open} className={btnBase}>
<button onClick={() => { open(); trackEvent('connect_wallet'); }} className={btnBase}>
Connect Wallet
</button>
);
Expand Down
35 changes: 35 additions & 0 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const STORAGE_KEY = 'wraith-telemetry-consent';

export type ConsentState = 'accepted' | 'declined' | null;

export function getConsent(): ConsentState {
const val = localStorage.getItem(STORAGE_KEY);
if (val === 'accepted' || val === 'declined') return val;
return null;
}

export function setConsent(state: 'accepted' | 'declined'): void {
localStorage.setItem(STORAGE_KEY, state);
}

function isEnabled(): boolean {
return getConsent() === 'accepted';
}

export function trackPageView(path: string): void {
if (!isEnabled()) return;
if (typeof window.plausible === 'undefined') return;
window.plausible('pageview', { u: window.location.origin + path });
}

export function trackEvent(name: string): void {
if (!isEnabled()) return;
if (typeof window.plausible === 'undefined') return;
window.plausible(name);
}

declare global {
interface Window {
plausible?: (event: string, options?: { u?: string }) => void;
}
}
Loading
Loading