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
13 changes: 11 additions & 2 deletions src/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { ContextApp } from "../../context/ContextAppState";
import routes from "../../constants/routes.json";
import TorIndicator from "./TorIndicator";

import { SyncStatusScanRangePriorityEnum, SyncStatusScanRangeType, ValueTransferClass } from "../appstate";
import {
SyncStatusScanRangePriorityEnum,
SyncStatusScanRangeType,
ValueTransferClass,
ValueTransferStatusEnum,
} from "../appstate";
import ScrollPaneTop from "../scrollPane/ScrollPane";
import DetailLine from "../detailLine/DetailLine";
import { useNavigate } from "react-router-dom";
Expand Down Expand Up @@ -44,9 +49,12 @@ const Dashboard: React.FC<DashboardProps> = ({ navigateToHistory }) => {

useEffect(() => {
// set somePending as well here when I know there is something new in ValueTransfers
// avoid failed txs because always they have 0 confirmations.
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
setAnyPending(pending > 0);
}, [valueTransfers]);
Expand Down Expand Up @@ -340,6 +348,7 @@ const Dashboard: React.FC<DashboardProps> = ({ navigateToHistory }) => {
key={index}
label={Utils.VTTypeWithConfirmations(vt.type, vt.status, vt.confirmations)}
value={"ZEC " + Utils.maxPrecisionTrimmed(vt.amount)}
failed={vt.status === ValueTransferStatusEnum.failed}
/>
))}
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/components/detailLine/DetailLine.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import React from "react";
import { render, screen } from "../../test-utils";
import DetailLine from "./DetailLine";

// DetailLine pulls Utils → electronBridge, which reads window.electronAPI at
// module load. That global only exists in the Electron preload, so jsdom
// would throw at import time without this mock.
jest.mock("../../electronBridge");

describe("DetailLine", () => {
it("renders without crashing", () => {
render(<DetailLine label="Address" value="abc123" />);
Expand Down
8 changes: 6 additions & 2 deletions src/components/detailLine/DetailLine.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import React from "react";
import cstyles from "../common/Common.module.css";
import styles from "./DetailLine.module.css";
import Utils from "../../utils/utils";

type DetailLineProps = {
label: string;
// ReactNode so callers can interleave inline icons / badges with the text
// (e.g. a Tor onion next to the ZEC Price line). String values still work
// unchanged because string is assignable to ReactNode.
value: React.ReactNode;
failed?: boolean;
};

const DetailLine = ({ label, value }: DetailLineProps) => {
const DetailLine = ({ label, value, failed }: DetailLineProps) => {
return (
<div className={styles.detailline}>
<div className={cstyles.sublight}>{label} :</div>
<div className={cstyles.breakword}>{value}</div>
<div className={cstyles.breakword} style={failed ? { color: Utils.getCssVariable("--color-error") } : {}}>
{value}
</div>
</div>
);
};
Expand Down
6 changes: 4 additions & 2 deletions src/components/history/History.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import cstyles from "../common/Common.module.css";
import styles from "./History.module.css";
import { ValueTransferClass, AddressBookEntryClass } from "../appstate";
import { ValueTransferClass, AddressBookEntryClass, ValueTransferStatusEnum } from "../appstate";
import ScrollPaneTop from "../scrollPane/ScrollPane";
import VtItemBlock from "./components/VtItemBlock";
import VtModal from "./components/VtModal";
Expand Down Expand Up @@ -43,7 +43,9 @@ const History: React.FC<HistoryProps> = () => {
// set somePending as well here when I know there is something new in ValueTransfers
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
setAnyPending(pending > 0);
}, [valueTransfers]);
Expand Down
6 changes: 4 additions & 2 deletions src/components/messages/Messages.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from "react";
import cstyles from "../common/Common.module.css";
import styles from "./Messages.module.css";
import { ValueTransferClass, AddressBookEntryClass } from "../appstate";
import { ValueTransferClass, AddressBookEntryClass, ValueTransferStatusEnum } from "../appstate";
import ScrollPaneBottom from "../scrollPane/ScrollPane";
import MessagesItemBlock from "./components/MessagesItemBlock";
import { BalanceBlock, BalanceBlockHighlight } from "../balanceBlock";
Expand Down Expand Up @@ -44,7 +44,9 @@ const Messages: React.FC<MessagesProps> = () => {
// set somePending as well here when I know there is something new in ValueTransfers
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
setAnyPending(pending > 0);
}, [valueTransfers]);
Expand Down
5 changes: 4 additions & 1 deletion src/components/receive/Receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TransparentAddressClass,
UnifiedAddressClass,
ValueTransferClass,
ValueTransferStatusEnum,
} from "../appstate";
import ScrollPaneTop from "../scrollPane/ScrollPane";
import AddressBlock from "./components/AddressBlock";
Expand Down Expand Up @@ -50,7 +51,9 @@ const Receive: React.FC<ReceiveProps> = () => {
// set somePending as well here when I know there is something new in ValueTransfers
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
setAnyPending(pending > 0);
}, [valueTransfers]);
Expand Down
13 changes: 11 additions & 2 deletions src/components/receive/components/AddressBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import styles from "../Receive.module.css";
import cstyles from "../../common/Common.module.css";
import Utils from "../../../utils/utils";
import { ContextApp } from "../../../context/ContextAppState";
import { ServerChainNameEnum, TransparentAddressClass, UnifiedAddressClass, ValueTransferClass } from "../../appstate";
import {
ServerChainNameEnum,
TransparentAddressClass,
UnifiedAddressClass,
ValueTransferClass,
ValueTransferStatusEnum,
} from "../../appstate";
import RPC from "../../../rpc/rpc";

import { ipcRenderer, isSandboxed } from "../../../electronBridge";
Expand Down Expand Up @@ -61,7 +67,10 @@ const AddressBlock: React.FC<AddressBlockProps> = ({
}, []);

const anyPending: boolean = useMemo(
() => valueTransfers.some((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3),
() =>
valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.some((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3),
[valueTransfers],
);

Expand Down
16 changes: 13 additions & 3 deletions src/components/send/Send.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React, { useContext, useEffect, useState } from "react";
import styles from "./Send.module.css";
import cstyles from "../common/Common.module.css";
import { ToAddrClass, SendPageStateClass, ValueTransferClass, ServerChainNameEnum } from "../appstate";
import {
ToAddrClass,
SendPageStateClass,
ValueTransferClass,
ServerChainNameEnum,
ValueTransferStatusEnum,
} from "../appstate";
import Utils from "../../utils/utils";
import ScrollPaneTop from "../scrollPane/ScrollPane";
import { BalanceBlockHighlight } from "../balanceBlock";
Expand Down Expand Up @@ -50,7 +56,9 @@ const Send: React.FC<SendProps> = ({ sendTransaction, setSendPageState }) => {
// set somePending as well here when I know there is something new in ValueTransfers
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
setAnyPending(pending > 0);
}, [valueTransfers]);
Expand All @@ -76,7 +84,9 @@ const Send: React.FC<SendProps> = ({ sendTransaction, setSendPageState }) => {
// set somePending as well here when I know there is something new in ValueTransfers
const pending: number =
valueTransfers.length > 0
? valueTransfers.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
? valueTransfers
.filter((vt: ValueTransferClass) => vt.status !== ValueTransferStatusEnum.failed)
.filter((vt: ValueTransferClass) => vt.confirmations >= 0 && vt.confirmations < 3).length
: 0;
// If there are unverified funds, then show a tooltip
const unconfirmed: number =
Expand Down
58 changes: 47 additions & 11 deletions src/components/sideBar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import { ipcRenderer, native } from "../../electronBridge";
// in place when the user clicks to copy. Seed phrase is intentionally NOT
// copyable to the clipboard — see the note in the JSX.
type SeedUfvkModalContentProps = {
seedStr: string;
ufvkStr: string;
// null = the native call is still in flight; render a loading state instead
// of empty content so the user gets feedback while get_seed / get_ufvk run.
seedStr: string | null;
ufvkStr: string | null;
birthday: number | undefined;
};

Expand All @@ -34,6 +36,27 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
const { copied: birthdayCopied, copy: copyBirthday } = useCopy(1500);
const birthdayStr = String(birthday ?? "");

if (seedStr === null || ufvkStr === null) {
return (
<div className={cstyles.verticalflex} style={{ alignItems: "center", padding: 24 }}>
<div style={{ marginBottom: 12 }}>Retrieving seed phrase / viewing key&hellip;</div>
<i className={`${"fas"} ${"fa-sync"} ${"fa-spin"}`} />
</div>
);
}

// Each sensitive value (seed, UFVK, birthday) lives inside this card so it's
// visually distinct from the surrounding explanatory text. Thin accent border
// and a slightly smaller font keep the data legible without dominating.
const dataBoxStyle: React.CSSProperties = {
border: "1px solid var(--color-primary)",
borderRadius: 4,
padding: 12,
marginTop: 8,
marginBottom: 16,
fontSize: "0.85em",
};

return (
<div className={cstyles.verticalflex}>
{!!seedStr && (
Expand All @@ -47,9 +70,9 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
<div style={{ textAlign: "center", color: "#ff6b6b", fontWeight: "bolder", marginTop: 6 }}>
Write this seed phrase down by hand. Do not copy it to the clipboard.
</div>
<hr style={{ width: "100%" }} />
<div
style={{
...dataBoxStyle,
textAlign: "center",
wordBreak: "break-word",
fontFamily: "monospace, Roboto",
Expand All @@ -58,7 +81,6 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
>
{seedStr}
</div>
<hr style={{ width: "100%" }} />
</>
)}
{!!ufvkStr && (
Expand All @@ -68,14 +90,13 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
<br />
PLEASE KEEP IT SAFE!
</div>
<hr style={{ width: "100%" }} />
<div
style={{
display: "flex",
justifyContent: "center",
gap: 8,
alignItems: "baseline",
marginBottom: 10,
marginTop: 10,
}}
>
<span style={{ color: "white", fontWeight: "bolder" }}>Unified Full Viewing Key</span>
Expand All @@ -87,6 +108,7 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
tabIndex={0}
aria-label="Copy viewing key"
style={{
...dataBoxStyle,
cursor: "pointer",
textAlign: "center",
wordBreak: "break-word",
Expand All @@ -103,7 +125,6 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
>
{ufvkStr}
</div>
<hr style={{ width: "100%" }} />
</>
)}
<div
Expand All @@ -112,7 +133,6 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
justifyContent: "center",
gap: 8,
alignItems: "baseline",
marginBottom: 10,
}}
>
<span style={{ color: "white", fontWeight: "bolder" }}>Birthday</span>
Expand All @@ -123,6 +143,10 @@ const SeedUfvkModalContent: React.FC<SeedUfvkModalContentProps> = ({ seedStr, uf
tabIndex={0}
aria-label="Copy birthday"
style={{
...dataBoxStyle,
marginBottom: 0,
alignSelf: "center",
minWidth: 120,
cursor: "pointer",
textAlign: "center",
fontFamily: "monospace, Roboto",
Expand Down Expand Up @@ -351,14 +375,26 @@ const Sidebar: React.FC<SidebarProps> = ({ doRescan, navigateToLoadingScreenChan
if (!authResult.success) return;
}

// Open the modal with a loading state before the native fetches so the
// user gets immediate feedback. get_seed / get_ufvk can each take a
// noticeable moment, and previously the UI sat silent between the auth
// prompt closing and the modal appearing.
openErrorModal(
"Wallet Seed Phrase / Viewing Key",
<SeedUfvkModalContent seedStr={null} ufvkStr={null} birthday={birthdayRef.current} />,
);

// Always fetch the UFVK — for seed wallets it's derived from the seed, and
// showing it alongside the seed lets the user share view-only access without
// exposing spend authority.
const seedRaw: string = readOnlyRef.current ? "" : await native.get_seed();
const ufvkRaw: string = await native.get_ufvk();
// exposing spend authority. Run both native calls in parallel.
const [seedRaw, ufvkRaw] = await Promise.all([
readOnlyRef.current ? Promise.resolve("") : native.get_seed(),
native.get_ufvk(),
]);
const seedStr: string = seedRaw ? (JSON.parse(seedRaw).seed_phrase ?? "") : "";
const ufvkStr: string = ufvkRaw ? (JSON.parse(ufvkRaw).ufvk ?? "") : "";

if (!active) return;
openErrorModal(
"Wallet Seed Phrase / Viewing Key",
<SeedUfvkModalContent seedStr={seedStr} ufvkStr={ufvkStr} birthday={birthdayRef.current} />,
Expand Down
28 changes: 27 additions & 1 deletion src/rpc/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,16 @@ export default class RPC {
} else {
const syncStr: string = await native.run_sync();
if (!syncStr || syncStr.toLowerCase().startsWith("error")) {
console.error(`Error sync ${syncStr}`);
// "sync is already running" is the expected outcome of the
// defensive run_sync() that fetchSyncPoll() fires while a sync is
// mid-flight (see the "is not complete" branch above). It's not a
// failure — the existing sync just keeps going. Demote to log so it
// doesn't drown the console with red.
if (syncStr && syncStr.toLowerCase().includes("already running")) {
console.log(`Sync already running: ${syncStr}`);
} else {
console.error(`Error sync ${syncStr}`);
}
}
}
} catch (error) {
Expand Down Expand Up @@ -680,11 +689,19 @@ export default class RPC {

vt.blockheight = tx.blockheight;
vt.status = tx.status;

if (tx.status === ValueTransferStatusEnum.failed) {
console.log("[RPC] failed value transfer (raw):", tx);
}
vt.address = !tx.recipient_address ? undefined : tx.recipient_address;
vt.amount = (!tx.value ? 0 : tx.value) / 10 ** 8;
vt.memos = !tx.memos || tx.memos.length === 0 ? undefined : tx.memos;
vt.pool = !tx.pool_received ? undefined : tx.pool_received;

if (vt.status === ValueTransferStatusEnum.failed) {
console.log("[RPC] failed value transfer (transformed):", vt);
}

return vt;
});

Expand Down Expand Up @@ -776,6 +793,15 @@ export default class RPC {
}

async getZecPrice() {
// Skip the network call entirely on testnet / regtest: TAZ has no USD
// price and the UI doesn't render the value anyway (see BalanceBlock,
// which only shows USD when currencyName === "ZEC"). Avoids unnecessary
// HTTP / Tor traffic every 5s on a testnet wallet. Wallet switches
// already reset the cached price via setZecPrice(0) in Routes.tsx.
if (this.currentWallet && this.currentWallet.chain_name !== ServerChainNameEnum.mainChainName) {
return;
}

// Read the user's "fetch price via Tor" preference from persisted
// settings. Default off — if the key is missing or anything throws while
// loading settings we keep the conventional HTTP path. The Tor client is
Expand Down
Loading
Loading