fix: wallet balance

- fiat balance display
- currency formatting
- added tests for WalletBalance
- added test coverage scripts
- bumped haveno-ts to v0.0.5

---

Reviewed-by: localredhead
This commit is contained in:
Subir 2022-05-17 16:01:28 +05:30
parent bc6ed842d5
commit 71bbc7e3f5
No known key found for this signature in database
GPG Key ID: 2D633D8047FD3FF0
28 changed files with 653 additions and 145 deletions

6
.gitignore vendored
View File

@ -55,4 +55,10 @@ thumbs.db
.idea/httpRequests .idea/httpRequests
/.idea/csv-plugin.xml /.idea/csv-plugin.xml
# Dev environment variables
.env .env
# Coverage reports
packages/main/coverage/
packages/preload/coverage/
packages/renderer/coverage/

View File

@ -23,6 +23,10 @@
"test:preload": "vitest run -r packages/preload --passWithNoTests", "test:preload": "vitest run -r packages/preload --passWithNoTests",
"test:renderer": "vitest run -r packages/renderer --passWithNoTests", "test:renderer": "vitest run -r packages/renderer --passWithNoTests",
"test:renderer:watch": "vitest -r packages/renderer", "test:renderer:watch": "vitest -r packages/renderer",
"coverage:main": "vitest run -r packages/main --coverage",
"coverage:preload": "vitest run -r packages/preload --coverage",
"coverage:renderer": "vitest run -r packages/renderer --coverage",
"coverage": "npm run coverage:main && npm run coverage:preload && npm run coverage:renderer",
"watch": "node scripts/watch.js", "watch": "node scripts/watch.js",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint .", "lint": "eslint .",
@ -84,7 +88,7 @@
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"electron-updater": "4.6.5", "electron-updater": "4.6.5",
"haveno-ts": "0.0.4", "haveno-ts": "0.0.5",
"joi": "^17.6.0", "joi": "^17.6.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -0,0 +1,32 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
/**
* Config for global end-to-end tests
* placed in project root tests folder
* @type {import('vite').UserConfig}
* @see https://vitest.dev/config/
*/
const config = {
test: {
include: ["./src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
reporter: ["html"],
},
},
};
export default config;

View File

@ -0,0 +1,32 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
/**
* Config for global end-to-end tests
* placed in project root tests folder
* @type {import('vite').UserConfig}
* @see https://vitest.dev/config/
*/
const config = {
test: {
include: ["./src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
reporter: ["html"],
},
},
};
export default config;

View File

@ -0,0 +1,41 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { BodyText } from "@atoms/Typography";
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Currency } from ".";
export default {
title: "atoms/Currency",
component: Currency,
} as ComponentMeta<typeof Currency>;
const Template: ComponentStory<typeof Currency> = (args) => {
return (
<Stack>
<BodyText heavy>
<Currency {...args} />
</BodyText>
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
currencyCode: "EUR",
value: 400000.12,
};

View File

@ -0,0 +1,49 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { Currency } from ".";
describe("atoms::Currency", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<Currency value={10} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
it("renders currency value", () => {
const { asFragment } = render(
<AppProviders>
<Currency currencyCode="USD" value={10} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
it("renders decimal value", () => {
const { asFragment } = render(
<AppProviders>
<Currency value={10.12345678} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,50 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMemo } from "react";
import { useIntl } from "react-intl";
interface CurrencyProps {
currencyCode?: string;
value: number;
}
export function Currency(props: CurrencyProps) {
const { currencyCode, value } = props;
const intl = useIntl();
const formattedNumber = useMemo(
() =>
intl
.formatNumber(value, {
...(currencyCode
? {
currency: currencyCode,
currencyDisplay: "code",
style: "currency",
}
: {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 12,
}),
})
.replace(/XXX\s/, ""),
[currencyCode, value]
);
return <>{formattedNumber}</>;
}

View File

@ -0,0 +1,19 @@
// Vitest Snapshot v1
exports[`atoms::Currency > renders currency value 1`] = `
<DocumentFragment>
USD 10.00
</DocumentFragment>
`;
exports[`atoms::Currency > renders decimal value 1`] = `
<DocumentFragment>
10.12345678
</DocumentFragment>
`;
exports[`atoms::Currency > renders without exploding 1`] = `
<DocumentFragment>
10.00
</DocumentFragment>
`;

View File

@ -0,0 +1,17 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export * from "./Currency";

View File

@ -14,14 +14,14 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import type { ReactText } from "react"; import type { ReactNode, ReactText } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import type { TextProps as MTextProps } from "@mantine/core"; import type { TextProps as MTextProps } from "@mantine/core";
import { Text as MText, createStyles } from "@mantine/core"; import { Text as MText, createStyles } from "@mantine/core";
import type { LangKeys } from "@constants/lang"; import type { LangKeys } from "@constants/lang";
type TextProps<TComponent> = MTextProps<TComponent> & { type TextProps<TComponent> = MTextProps<TComponent> & {
children: ReactText; children: ReactText | ReactNode;
stringId?: LangKeys; stringId?: LangKeys;
}; };
@ -44,7 +44,10 @@ export function BodyText<TComponent = "p">(props: BodyTextProps<TComponent>) {
size={size} size={size}
> >
{stringId ? ( {stringId ? (
<FormattedMessage id={stringId} defaultMessage={children.toString()} /> <FormattedMessage
id={stringId}
defaultMessage={children ? children.toString() : ""}
/>
) : ( ) : (
children children
)} )}
@ -59,7 +62,10 @@ export function InfoText<TComponent = "p">(props: TextProps<TComponent>) {
return ( return (
<MText {...rest} className={cx(className, classes.info)}> <MText {...rest} className={cx(className, classes.info)}>
{stringId ? ( {stringId ? (
<FormattedMessage id={stringId} defaultMessage={children.toString()} /> <FormattedMessage
id={stringId}
defaultMessage={children ? children.toString() : ""}
/>
) : ( ) : (
children children
)} )}
@ -74,7 +80,10 @@ export function LabelText(props: TextProps<"label">) {
return ( return (
<MText component="label" {...rest} className={cx(className, classes.label)}> <MText component="label" {...rest} className={cx(className, classes.label)}>
{stringId ? ( {stringId ? (
<FormattedMessage id={stringId} defaultMessage={children.toString()} /> <FormattedMessage
id={stringId}
defaultMessage={children ? children.toString() : ""}
/>
) : ( ) : (
children children
)} )}

View File

@ -14,13 +14,45 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { describe, expect, it } from "vitest"; import { beforeAll, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react"; import { render, waitForElementToBeRemoved } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders"; import { AppProviders } from "@atoms/AppProviders";
import { WalletBalance } from "."; import { WalletBalance } from ".";
describe("molecules::WalletBalance", () => { describe("molecules::WalletBalance", () => {
it("renders without exploding", () => { beforeAll(() => {
vi.mock("@hooks/haveno/useHavenoClient", () => ({
useHavenoClient: () => ({
getBalances: async () => {
return {
getLockedBalance: () => 12,
getReservedTradeBalance: () => 14,
getBalance: () => 15,
};
},
}),
}));
vi.mock("@hooks/storage/useAccountInfo", () => ({
useAccountInfo: () => ({
isLoading: false,
isSuccess: true,
data: {
primaryFiat: "USD",
},
}),
}));
vi.mock("@hooks/haveno/usePrice", () => ({
usePrice: () => ({
isLoading: false,
isSuccess: true,
data: 300,
}),
}));
});
it("renders loading state", () => {
const { asFragment } = render( const { asFragment } = render(
<AppProviders> <AppProviders>
<WalletBalance /> <WalletBalance />
@ -28,4 +60,16 @@ describe("molecules::WalletBalance", () => {
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("renders after loading data", async () => {
const { asFragment, queryByText } = render(
<AppProviders>
<WalletBalance />
</AppProviders>
);
if (queryByText("Loading...")) {
await waitForElementToBeRemoved(() => queryByText("Loading..."));
}
expect(asFragment()).toMatchSnapshot();
});
}); });

View File

@ -26,19 +26,37 @@ import {
import { ReactComponent as XMRLogo } from "@assets/xmr-logo-1.svg"; import { ReactComponent as XMRLogo } from "@assets/xmr-logo-1.svg";
import { ReactComponent as ArrowDown } from "@assets/arrow-down.svg"; import { ReactComponent as ArrowDown } from "@assets/arrow-down.svg";
import { useBalances } from "@hooks/haveno/useBalances"; import { useBalances } from "@hooks/haveno/useBalances";
import { useAccountInfo } from "@hooks/storage/useAccountInfo";
import { usePrice } from "@hooks/haveno/usePrice";
import { Currency } from "@atoms/Currency";
import { BodyText } from "@atoms/Typography";
export function WalletBalance() { export function WalletBalance() {
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const { classes } = useStyles({ isOpen }); const { classes } = useStyles({ isOpen });
const { data: availableBalances } = useBalances(); const { data: availableBalances, isLoading: isLoadingBalance } =
const Total = useMemo(() => { useBalances();
const { data: accountInfo, isLoading: isLoadingAccountInfo } =
useAccountInfo();
const { data: price } = usePrice(accountInfo?.primaryFiat);
const totalBalance = useMemo(() => {
return ( return (
Number(availableBalances?.getLockedBalance() || 0) + Number(availableBalances?.getLockedBalance() || 0) +
Number(availableBalances?.getReservedTradeBalance() || 0) Number(availableBalances?.getReservedTradeBalance() || 0)
).toString(); );
}, [availableBalances]); }, [availableBalances]);
const fiatBalance = useMemo(() => {
if (!totalBalance || !price || !accountInfo?.primaryFiat) {
return 0;
}
return totalBalance * price;
}, [totalBalance, price, accountInfo]);
return ( return (
<UnstyledButton <UnstyledButton
aria-label="Show Balance"
className={classes.btnToggle} className={classes.btnToggle}
onClick={() => setOpen(!isOpen)} onClick={() => setOpen(!isOpen)}
> >
@ -49,35 +67,60 @@ export function WalletBalance() {
Available Balance Available Balance
</Text> </Text>
</Group> </Group>
<Stack spacing={4}> {isLoadingAccountInfo || isLoadingBalance ? (
<Group>
<Text className={classes.xmr}>
{availableBalances?.getBalance() ?? 0}
</Text>
<ArrowDown className={classes.toggleIcon} />
</Group>
<Text className={classes.fiat}>(EUR 2441,02)</Text>
</Stack>
<Collapse in={isOpen}>
<Stack> <Stack>
<Stack spacing={4}> <BodyText size="xs">Loading...</BodyText>
<Text className={classes.balanceLabel}>Total</Text>
<Text className={classes.balanceValue}>{Total}</Text>
</Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Reserved</Text>
<Text className={classes.balanceValue}>
{availableBalances?.getReservedTradeBalance() ?? 0}
</Text>
</Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Locked</Text>
<Text className={classes.balanceValue}>
{availableBalances?.getLockedBalance() ?? 0}
</Text>
</Stack>
</Stack> </Stack>
</Collapse> ) : (
<>
<Stack spacing={4}>
<Group>
<Text className={classes.xmr}>
<Currency
value={Number(availableBalances?.getBalance() ?? 0)}
/>
</Text>
<ArrowDown className={classes.toggleIcon} />
</Group>
<Text className={classes.fiat}>
(
<Currency
currencyCode={accountInfo?.primaryFiat}
value={fiatBalance}
/>
)
</Text>
</Stack>
<Collapse in={isOpen}>
<Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Total</Text>
<Text className={classes.balanceValue}>
<Currency value={totalBalance} />
</Text>
</Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Reserved</Text>
<Text className={classes.balanceValue}>
<Currency
value={Number(
availableBalances?.getReservedTradeBalance() ?? 0
)}
/>
</Text>
</Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Locked</Text>
<Text className={classes.balanceValue}>
<Currency
value={Number(availableBalances?.getLockedBalance() ?? 0)}
/>
</Text>
</Stack>
</Stack>
</Collapse>
</>
)}
</Stack> </Stack>
</UnstyledButton> </UnstyledButton>
); );

View File

@ -1,8 +1,9 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`molecules::WalletBalance > renders without exploding 1`] = ` exports[`molecules::WalletBalance > renders after loading data 1`] = `
<DocumentFragment> <DocumentFragment>
<button <button
aria-label="Show Balance"
class="mantine-UnstyledButton-root mantine-3om9so" class="mantine-UnstyledButton-root mantine-3om9so"
type="button" type="button"
> >
@ -59,7 +60,7 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
<div <div
class="mantine-Text-root mantine-Group-child mantine-q5labh" class="mantine-Text-root mantine-Group-child mantine-q5labh"
> >
0 15.00
</div> </div>
<svg <svg
class="mantine-Group-child mantine-qg5oag" class="mantine-Group-child mantine-qg5oag"
@ -80,7 +81,7 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
<div <div
class="mantine-Text-root mantine-1pfxwhx" class="mantine-Text-root mantine-1pfxwhx"
> >
(EUR 2441,02) (USD 7,800.00)
</div> </div>
</div> </div>
<div <div
@ -105,7 +106,7 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
<div <div
class="mantine-Text-root mantine-14d5cdm" class="mantine-Text-root mantine-14d5cdm"
> >
0 26.00
</div> </div>
</div> </div>
<div <div
@ -119,7 +120,7 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
<div <div
class="mantine-Text-root mantine-14d5cdm" class="mantine-Text-root mantine-14d5cdm"
> >
0 14.00
</div> </div>
</div> </div>
<div <div
@ -133,7 +134,7 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
<div <div
class="mantine-Text-root mantine-14d5cdm" class="mantine-Text-root mantine-14d5cdm"
> >
0 12.00
</div> </div>
</div> </div>
</div> </div>
@ -143,3 +144,68 @@ exports[`molecules::WalletBalance > renders without exploding 1`] = `
</button> </button>
</DocumentFragment> </DocumentFragment>
`; `;
exports[`molecules::WalletBalance > renders loading state 1`] = `
<DocumentFragment>
<button
aria-label="Show Balance"
class="mantine-UnstyledButton-root mantine-3om9so"
type="button"
>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Group-root mantine-1lumg83"
>
<svg
class="mantine-Group-child mantine-oilh7i"
fill="none"
height="1em"
viewBox="0 0 20 20"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#clip0_3508_17165)"
>
<path
d="M10.0026 0C4.4815 0 -0.00311409 4.48323 0.0039145 9.9987C0.00529892 11.1022 0.18128 12.1636 0.51157 13.1565H3.5034V4.74512L10.0026 11.2436L16.5015 4.74512V13.1567H19.494C19.8247 12.1639 19.9998 11.1024 20.0018 9.99885C20.0112 4.47764 15.5243 0.00133103 10.0026 0.00133103V0Z"
fill="#111111"
/>
<path
d="M8.50535 12.7375L5.66905 9.90137V15.1943H3.50057L1.45435 15.1946C3.20952 18.0739 6.38154 20 9.99999 20C13.6184 20 16.7906 18.0735 18.5461 15.1942H14.3303V9.90137L11.4938 12.7375L9.99967 14.2315L8.50545 12.7375H8.50535Z"
fill="#111111"
/>
</g>
<defs>
<clippath
id="clip0_3508_17165"
>
<rect
fill="white"
height="20"
width="20"
/>
</clippath>
</defs>
</svg>
<div
class="mantine-Text-root mantine-Group-child mantine-1iw7oli"
>
Available Balance
</div>
</div>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Text-root mantine-1152338"
>
Loading...
</div>
</div>
</div>
</button>
</DocumentFragment>
`;

View File

@ -202,6 +202,7 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
class="mantine-865t33" class="mantine-865t33"
> >
<button <button
aria-label="Show Balance"
class="mantine-UnstyledButton-root mantine-3om9so" class="mantine-UnstyledButton-root mantine-3om9so"
type="button" type="button"
> >
@ -250,92 +251,12 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
</div> </div>
</div> </div>
<div <div
class="mantine-Stack-root mantine-1kb6t4k" class="mantine-Stack-root mantine-lfk3cq"
> >
<div <div
class="mantine-Group-root mantine-6y1794" class="mantine-Text-root mantine-1152338"
> >
<div Loading...
class="mantine-Text-root mantine-Group-child mantine-q5labh"
>
0
</div>
<svg
class="mantine-Group-child mantine-qg5oag"
fill="none"
height="1em"
viewBox="0 0 7 4"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M2.91916 3.69319C3.23963 4.04927 3.76166 4.04657 4.07971 3.69319L6.82329 0.644746C7.14377 0.288661 7.01636 0 6.53811 0H0.460749C-0.0172283 0 -0.14248 0.291357 0.17557 0.644746L2.91916 3.69319Z"
fill="#111111"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mantine-Text-root mantine-1pfxwhx"
>
(EUR 2441,02)
</div>
</div>
<div
aria-hidden="true"
class="mantine-1avyp1d"
style="box-sizing: border-box; display: none; height: 0px; overflow: hidden;"
>
<div
style="opacity: 0; transition: opacity 200ms ease;"
>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Total
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
0
</div>
</div>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Reserved
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
0
</div>
</div>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Locked
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
0
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,6 +19,8 @@ import { ReactComponent as EthLogo } from "@assets/eth.svg";
import { ReactComponent as EurLogo } from "@assets/eur.svg"; import { ReactComponent as EurLogo } from "@assets/eur.svg";
import { PaymentMethodIds } from "./payment-methods"; import { PaymentMethodIds } from "./payment-methods";
export type SupportedFiat = "USD" | "ETH" | "GBP";
export const SupportedCurrencies = [ export const SupportedCurrencies = [
{ {
id: "BTC", id: "BTC",

View File

@ -15,17 +15,21 @@
// ============================================================================= // =============================================================================
export enum QueryKeys { export enum QueryKeys {
HavenoVersion = "Haveno.Version", // Haveno
Balances = "Haveno.Balances", Balances = "Haveno.Balances",
PaymentAccounts = "Haveno.PaymentAccounts", HavenoVersion = "Haveno.Version",
MoneroNodeSettings = "Haveno.MoneroNodeSettings",
MoneroNodeIsRunning = "Haveno.MoneroNodeIsRunning", MoneroNodeIsRunning = "Haveno.MoneroNodeIsRunning",
MoneroNodeSettings = "Haveno.MoneroNodeSettings",
MoneroRemoteNodes = "Haveno.MoneroRemoteNodes", MoneroRemoteNodes = "Haveno.MoneroRemoteNodes",
PaymentAccounts = "Haveno.PaymentAccounts",
Prices = "Haveno.Prices",
PrimaryAddress = "Haveno.PrimaryAddress",
SyncStatus = "Haveno.SyncStatus", SyncStatus = "Haveno.SyncStatus",
// Storage
StorageAccountInfo = "Storage.AccountInfo", StorageAccountInfo = "Storage.AccountInfo",
StoragePreferences = "Storage.Preferences", StoragePreferences = "Storage.Preferences",
// Others
AuthSession = "AuthSession", AuthSession = "AuthSession",
} }

View File

@ -0,0 +1,26 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function useAddress() {
const client = useHavenoClient();
return useQuery(QueryKeys.PrimaryAddress, async () =>
client.getXmrPrimaryAddress()
);
}

View File

@ -15,12 +15,13 @@
// ============================================================================= // =============================================================================
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import type { XmrBalanceInfo } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useBalances() { export function useBalances() {
const client = useHavenoClient(); const client = useHavenoClient();
return useQuery(QueryKeys.Balances, async () => { return useQuery<XmrBalanceInfo, Error>(QueryKeys.Balances, async () =>
return client.getBalances(); client.getBalances()
}); );
} }

View File

@ -20,7 +20,5 @@ import { useHavenoClient } from "./useHavenoClient";
export function useHavenoVersion() { export function useHavenoVersion() {
const client = useHavenoClient(); const client = useHavenoClient();
return useQuery(QueryKeys.HavenoVersion, async () => { return useQuery(QueryKeys.HavenoVersion, async () => client.getVersion());
return client.getVersion();
});
} }

View File

@ -0,0 +1,37 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function usePaymentMethods() {
const client = useHavenoClient();
return useQuery(QueryKeys.PaymentMethods, async () => {
// TODO: replace with getSupportedAssets(): TradeCurrency[]
// const mns = await client.getMoneroNodeSettings();
const mns = await client._moneroNodeClient.getMoneroNodeSettings({}, {});
console.log("monero node settings: ", mns?.toObject());
if (mns) {
const mns2 = mns.setStartupFlagsList(["foo1"]);
mns;
}
const assetCodes = await client.getSupportedAssetCodes();
return await Promise.all(
assetCodes.map((assetCode) => client.getPaymentMethods(assetCode))
);
});
}

View File

@ -0,0 +1,46 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useEffect, useState } from "react";
import { usePrices } from "./usePrices";
export function usePrice(currencyCode?: string) {
const [data, setData] = useState(0);
const [error, setError] = useState("");
const { data: prices, isLoading, isSuccess, error: _error } = usePrices();
useEffect(() => {
if (!currencyCode) {
return;
}
if (!isSuccess) {
setError(_error?.message ?? "Something went wrong");
return;
}
const price = (prices ?? []).find(
(price) => price.currencyCode === currencyCode
);
if (!price) {
setError("Unknown currency code");
return;
}
setData(price.price);
setError("");
}, []);
return { data, isLoading, isSuccess, error, isError: Boolean(error) };
}

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { MarketPriceInfo } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function usePrices() {
const client = useHavenoClient();
return useQuery<Array<MarketPriceInfo.AsObject>, Error>(
QueryKeys.Prices,
async () => {
const prices = await client.getPrices();
return prices.map((price) => price.toObject());
}
);
}

View File

@ -21,7 +21,6 @@ import { AccountLayout } from "@templates/AccountLayout";
export function AccountPaymentAccounts() { export function AccountPaymentAccounts() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<AccountLayout> <AccountLayout>
<PaymentMethodList <PaymentMethodList

View File

@ -21,7 +21,7 @@ import { LangKeys } from "@constants/lang/LangKeys";
import { CenteredLayout } from "@templates/CenteredLayout"; import { CenteredLayout } from "@templates/CenteredLayout";
import { Heading } from "@atoms/Typography"; import { Heading } from "@atoms/Typography";
import Logo from "@assets/logo.svg"; import Logo from "@assets/logo.svg";
import { useAccountInfo } from "@src/hooks/storage/useGetAccountInfo"; import { useAccountInfo } from "@hooks/storage/useAccountInfo";
import { ROUTES } from "@constants/routes"; import { ROUTES } from "@constants/routes";
import { showNotification } from "@mantine/notifications"; import { showNotification } from "@mantine/notifications";

View File

@ -0,0 +1,32 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
/**
* Config for global end-to-end tests
* placed in project root tests folder
* @type {import('vite').UserConfig}
* @see https://vitest.dev/config/
*/
const config = {
test: {
include: ["./src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
reporter: ["html"],
},
},
};
export default config;

View File

@ -33,7 +33,6 @@ const config = {
*/ */
testTimeout: 30_000, testTimeout: 30_000,
hookTimeout: 30_000, hookTimeout: 30_000,
deps: { inline: ["haveno-ts"] },
}, },
}; };

View File

@ -7973,10 +7973,10 @@ hastscript@^6.0.0:
property-information "^5.0.0" property-information "^5.0.0"
space-separated-tokens "^1.0.0" space-separated-tokens "^1.0.0"
haveno-ts@0.0.4: haveno-ts@0.0.5:
version "0.0.4" version "0.0.5"
resolved "https://registry.yarnpkg.com/haveno-ts/-/haveno-ts-0.0.4.tgz#4d9fac6601887f384e5b380471988ee3e8b17783" resolved "https://registry.yarnpkg.com/haveno-ts/-/haveno-ts-0.0.5.tgz#996e290b4dd1659e58f86aff4e0cc0d3c7516ad3"
integrity sha512-8Yf1kyeuBCbfOr7T0uy87XcnNk8pfwS6uN4iuXgkAKLySByGl8Mxay7C0Fq1qPExz4hPJsJ8ql2qbaU1HJkbkg== integrity sha512-HzBuvMmsQLUybk99NqOe50lAcYmnwv2uv0/5IMAIuUpqFmJq2xvPW6vvZASXkRBVYPdRYjLLnUV5ygy8Rq7Yog==
dependencies: dependencies:
"@types/node" "^17.0.30" "@types/node" "^17.0.30"
console "^0.7.2" console "^0.7.2"