Skip to content
Closed
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
16,783 changes: 16,783 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@wraith-protocol/sdk": "^1.4.5",
"bs58": "^6.0.0",
"buffer": "^6.0.3",
"esbuild": "^0.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.6.0",
Expand Down
109 changes: 100 additions & 9 deletions src/components/CkbReceive.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { ccc } from '@ckb-ccc/connector-react';
import {
deriveStealthKeys,
Expand All @@ -11,8 +11,29 @@ import {
} from '@wraith-protocol/sdk/chains/ckb';
import { useStealthKeys } from '@/context/StealthKeysContext';
import { CopyButton } from '@/components/CopyButton';
import { useStealthLabels } from '@/hooks/useStealthLabels';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
import { StealthLabelEditor } from '@/components/StealthLabelEditor';
import { StealthLabelBar } from '@/components/StealthLabelBar';
import type { StealthLabel } from '@/lib/stealth-labels';

function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
function CkbStealthRow({
match,
label,
onSaveLabel,
onSaveTags,
onHide,
onUnhide,
onTagFilter,
}: {
match: MatchedStealthCell;
label: StealthLabel | undefined;
onSaveLabel: (text: string) => void;
onSaveTags: (tags: string[]) => void;
onHide: () => void;
onUnhide: () => void;
onTagFilter: (tag: string | null) => void;
}) {
const [showKey, setShowKey] = useState(false);
const keyHex = match.stealthPrivateKey.slice(2);
const capacityCkb = (Number(match.capacity) / 1e8).toFixed(4);
Expand Down Expand Up @@ -71,21 +92,66 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
</div>
)}
</div>

<StealthLabelEditor
label={label}
onSaveLabel={onSaveLabel}
onSaveTags={onSaveTags}
onHide={onHide}
onUnhide={onUnhide}
onTagFilter={onTagFilter}
/>
</div>
);
}

export function CkbReceive() {
const { wallet } = ccc.useCcc();
const signer = ccc.useSigner();
const [walletAddress, setWalletAddress] = useState<string | undefined>();

useEffect(() => {
(async () => {
if (signer) {
try {
const addr = await (signer as any).getRecommendedAddress();
setWalletAddress(addr);
} catch {
setWalletAddress(undefined);
}
} else {
setWalletAddress(undefined);
}
})();
}, [signer]);

const labelOps = useStealthLabels(walletAddress);
const { ckbKeys, ckbMetaAddress, setCkbKeys, setCkbMetaAddress } = useStealthKeys();
const { shouldDisable } = useNetworkStatus();

const [isDerivingKeys, setIsDerivingKeys] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [matched, setMatched] = useState<MatchedStealthCell[]>([]);
const [hasScanned, setHasScanned] = useState(false);
const [error, setError] = useState('');

const displayedMatches = useMemo(() => {
if (!labelOps.searchQuery && !labelOps.activeTag) return matched;
return matched.filter((m) => {
const l = labelOps.getLabel(m.stealthPubKeyHash);
if (labelOps.activeTag) return l?.tags.includes(labelOps.activeTag);
if (labelOps.searchQuery) {
const q = labelOps.searchQuery.toLowerCase();
return (
l?.label.toLowerCase().includes(q) ||
l?.tags.some((t) => t.toLowerCase().includes(q)) ||
m.stealthPubKeyHash.toLowerCase().includes(q)
);
}
return true;
});
}, [matched, labelOps.searchQuery, labelOps.activeTag, labelOps]);

const deriveKeys = useCallback(async () => {
if (!signer) {
setError('Connect your CKB wallet first');
Expand Down Expand Up @@ -170,7 +236,7 @@ export function CkbReceive() {
<div className="flex flex-col gap-4">
<button
onClick={deriveKeys}
disabled={isDerivingKeys}
disabled={isDerivingKeys || shouldDisable}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isDerivingKeys ? 'Sign in wallet...' : 'Derive Stealth Keys'}
Expand All @@ -196,7 +262,7 @@ export function CkbReceive() {
<div className="flex items-center justify-between">
<button
onClick={scanPayments}
disabled={isScanning}
disabled={isScanning || shouldDisable}
className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isScanning ? 'Scanning...' : 'Scan for Cells'}
Expand All @@ -211,11 +277,36 @@ export function CkbReceive() {
{error && <p className="text-sm text-error">{error}</p>}

{matched.length > 0 && (
<div className="flex flex-col gap-4">
{matched.map((m, i) => (
<CkbStealthRow key={i} match={m} />
))}
</div>
<>
<StealthLabelBar
searchQuery={labelOps.searchQuery}
onSearchChange={labelOps.setSearchQuery}
activeTag={labelOps.activeTag}
allTags={labelOps.allTags}
onTagSelect={labelOps.setActiveTag}
showHidden={labelOps.showHidden}
onToggleShowHidden={() => labelOps.setShowHidden(!labelOps.showHidden)}
showPrivacyWarning={labelOps.showPrivacyWarning}
onDismissPrivacyWarning={labelOps.dismissPrivacyWarning}
onExport={labelOps.export}
onImport={labelOps.import}
/>

<div className="flex flex-col gap-4">
{displayedMatches.map((m, i) => (
<CkbStealthRow
key={i}
match={m}
label={labelOps.getLabel(m.stealthPubKeyHash)}
onSaveLabel={(text) => labelOps.setLabel(m.stealthPubKeyHash, text)}
onSaveTags={(tags) => labelOps.setTags(m.stealthPubKeyHash, tags)}
onHide={() => labelOps.hide(m.stealthPubKeyHash)}
onUnhide={() => labelOps.unhide(m.stealthPubKeyHash)}
onTagFilter={labelOps.setActiveTag}
/>
))}
</div>
</>
)}

{hasScanned && matched.length === 0 && (
Expand Down
4 changes: 3 additions & 1 deletion src/components/CkbSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
getDeployment,
} from '@wraith-protocol/sdk/chains/ckb';
import { CopyButton } from '@/components/CopyButton';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';

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

export function CkbSend() {
const { wallet } = ccc.useCcc();
const signer = ccc.useSigner();
const { shouldDisable } = useNetworkStatus();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [error, setError] = useState('');
Expand Down Expand Up @@ -189,7 +191,7 @@ export function CkbSend() {

<button
onClick={handleSend}
disabled={!recipient || !amount || isPending}
disabled={!recipient || !amount || isPending || shouldDisable}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isPending ? 'Confirm in wallet...' : 'Send Privately'}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { ChainSwitcher } from './ChainSwitcher';
import { WalletConnect } from './WalletConnect';
import { NetworkBadge } from './NetworkBadge';

const navLinks = [
{ to: '/send', label: 'Send' },
Expand Down Expand Up @@ -45,6 +46,7 @@ export function Header() {

<div className="flex items-center gap-3">
<ChainSwitcher />
<NetworkBadge />
<WalletConnect />
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
Expand Down
93 changes: 83 additions & 10 deletions src/components/HorizenReceive.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import {
useAccount,
useSignMessage,
Expand All @@ -24,13 +24,30 @@ import { useStealthKeys } from '@/context/StealthKeysContext';
import { CopyButton } from '@/components/CopyButton';
import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer';
import { horizenTestnet } from '@/config';
import { useStealthLabels } from '@/hooks/useStealthLabels';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
import { StealthLabelEditor } from '@/components/StealthLabelEditor';
import { StealthLabelBar } from '@/components/StealthLabelBar';
import type { StealthLabel } from '@/lib/stealth-labels';

function StealthRow({
match,
onWithdrawn,
label,
onSaveLabel,
onSaveTags,
onHide,
onUnhide,
onTagFilter,
}: {
match: MatchedAnnouncement;
onWithdrawn: (hash: string) => void;
label: StealthLabel | undefined;
onSaveLabel: (text: string) => void;
onSaveTags: (tags: string[]) => void;
onHide: () => void;
onUnhide: () => void;
onTagFilter: (tag: string | null) => void;
}) {
const [balance, setBalance] = useState<string | null>(null);
const [loadingBal, setLoadingBal] = useState(true);
Expand All @@ -39,6 +56,7 @@ function StealthRow({
const [withdrawHash, setWithdrawHash] = useState<string | null>(null);
const [error, setError] = useState('');
const [showKey, setShowKey] = useState(false);
const { shouldDisable } = useNetworkStatus();

useEffect(() => {
(async () => {
Expand Down Expand Up @@ -143,7 +161,7 @@ function StealthRow({
/>
<button
onClick={handleWithdraw}
disabled={!dest || withdrawing}
disabled={!dest || withdrawing || shouldDisable}
className="h-10 bg-primary px-4 font-heading text-[10px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{withdrawing ? '...' : 'Withdraw'}
Expand Down Expand Up @@ -193,6 +211,15 @@ function StealthRow({
</div>
)}
</div>

<StealthLabelEditor
label={label}
onSaveLabel={onSaveLabel}
onSaveTags={onSaveTags}
onHide={onHide}
onUnhide={onUnhide}
onTagFilter={onTagFilter}
/>
</div>
);
}
Expand All @@ -202,12 +229,32 @@ export function HorizenReceive() {
const { signMessageAsync } = useSignMessage();
const { evmKeys, evmMetaAddress, setEvmKeys, setEvmMetaAddress } = useStealthKeys();

const labelOps = useStealthLabels(address);
const { shouldDisable } = useNetworkStatus();

const [isDerivingKeys, setIsDerivingKeys] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [matched, setMatched] = useState<MatchedAnnouncement[]>([]);
const [hasScanned, setHasScanned] = useState(false);
const [error, setError] = useState('');

const displayedMatches = useMemo(() => {
if (!labelOps.searchQuery && !labelOps.activeTag) return matched;
return matched.filter((m) => {
const l = labelOps.getLabel(m.stealthAddress);
if (labelOps.activeTag) return l?.tags.includes(labelOps.activeTag);
if (labelOps.searchQuery) {
const q = labelOps.searchQuery.toLowerCase();
return (
l?.label.toLowerCase().includes(q) ||
l?.tags.some((t) => t.toLowerCase().includes(q)) ||
m.stealthAddress.toLowerCase().includes(q)
);
}
return true;
});
}, [matched, labelOps.searchQuery, labelOps.activeTag, labelOps]);

const deployment = getDeployment('horizen');

// Check if already registered on-chain
Expand Down Expand Up @@ -319,7 +366,7 @@ export function HorizenReceive() {
<div className="flex flex-col gap-4">
<button
onClick={deriveKeys}
disabled={isDerivingKeys}
disabled={isDerivingKeys || shouldDisable}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isDerivingKeys ? 'Sign in wallet...' : 'Derive Keys'}
Expand Down Expand Up @@ -373,7 +420,7 @@ export function HorizenReceive() {
</p>
<button
onClick={registerOnChain}
disabled={isRegPending || isRegConfirming}
disabled={isRegPending || isRegConfirming || shouldDisable}
className="h-11 w-full border border-outline-variant font-heading text-[13px] font-semibold uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright disabled:opacity-30"
>
{isRegPending
Expand All @@ -389,7 +436,7 @@ export function HorizenReceive() {
<div className="flex items-center justify-between">
<button
onClick={scanPayments}
disabled={isScanning}
disabled={isScanning || shouldDisable}
className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isScanning ? 'Scanning...' : 'Scan for Payments'}
Expand All @@ -404,11 +451,37 @@ export function HorizenReceive() {
{error && <p className="text-sm text-error">{error}</p>}

{matched.length > 0 && (
<div className="flex flex-col gap-4">
{matched.map((m, i) => (
<StealthRow key={i} match={m} onWithdrawn={() => {}} />
))}
</div>
<>
<StealthLabelBar
searchQuery={labelOps.searchQuery}
onSearchChange={labelOps.setSearchQuery}
activeTag={labelOps.activeTag}
allTags={labelOps.allTags}
onTagSelect={labelOps.setActiveTag}
showHidden={labelOps.showHidden}
onToggleShowHidden={() => labelOps.setShowHidden(!labelOps.showHidden)}
showPrivacyWarning={labelOps.showPrivacyWarning}
onDismissPrivacyWarning={labelOps.dismissPrivacyWarning}
onExport={labelOps.export}
onImport={labelOps.import}
/>

<div className="flex flex-col gap-4">
{displayedMatches.map((m, i) => (
<StealthRow
key={i}
match={m}
onWithdrawn={() => {}}
label={labelOps.getLabel(m.stealthAddress)}
onSaveLabel={(text) => labelOps.setLabel(m.stealthAddress, text)}
onSaveTags={(tags) => labelOps.setTags(m.stealthAddress, tags)}
onHide={() => labelOps.hide(m.stealthAddress)}
onUnhide={() => labelOps.unhide(m.stealthAddress)}
onTagFilter={labelOps.setActiveTag}
/>
))}
</div>
</>
)}

{hasScanned && matched.length === 0 && (
Expand Down
Loading