feat: market offers

---

Reviewed-by: @schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-06-13 20:57:27 +02:00 committed by GitHub
parent 07a984bb52
commit 5ed0efd67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1191 additions and 39 deletions

View File

@ -54,9 +54,9 @@
"@testing-library/user-event": "^14.2.0",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.182",
"@types/qrcode": "^1.4.2",
"@types/react": "<18.0.0",
"@types/react-dom": "<18.0.0",
"@types/qrcode": "^1.4.2",
"@typescript-eslint/eslint-plugin": "5.12.1",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-react": "^1.3.0",
@ -95,7 +95,7 @@
"dayjs": "^1.11.0",
"electron-store": "^8.0.1",
"electron-updater": "4.6.5",
"haveno-ts": "0.0.5",
"haveno-ts": "0.0.6",
"joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",

View File

@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 0C3.36316 0 0 3.36316 0 7.5C0 11.6368 3.36316 15 7.5 15C11.6368 15 15 11.6368 15 7.5C15 3.36316 11.6368 0 7.5 0ZM11.1316 5.81053L6.82105 10.5632C6.71053 10.6895 6.50526 10.6895 6.37895 10.5789L3.69474 7.87895C3.56842 7.75263 3.56842 7.56316 3.69474 7.45263L4.48421 6.66316C4.61053 6.53684 4.8 6.53684 4.91053 6.66316L6.55263 8.32105L9.86842 4.65789C9.97895 4.53158 10.1684 4.53158 10.2947 4.64211L11.1158 5.38421C11.2421 5.49474 11.2421 5.68421 11.1316 5.81053Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -30,6 +30,7 @@ import {
PaymentMethods,
} from "@pages/Account";
import { MyWallet } from "@pages/MyWallet";
import { MarketsOffersPage } from "@pages/Markets";
export function AppRoutes() {
return (
@ -38,6 +39,7 @@ export function AppRoutes() {
<Route path={ROUTES.Login} element={<Login />} />
<Route path={ROUTES.Welcome} element={<Welcome />} />
<Route path={ROUTES.CreateAccount} element={<CreateAccount />} />
<Route path={ROUTES.Markets} element={<MarketsOffersPage />} />
<Route
path={ROUTES.MyWallet}
element={

View File

@ -38,4 +38,5 @@ export const Default = Template.bind({});
Default.args = {
currencyCode: "EUR",
value: 400000.12,
format: "symbol",
};

View File

@ -15,15 +15,19 @@
// =============================================================================
import { useMemo } from "react";
import type { FormatNumberOptions } from "react-intl";
import { useIntl } from "react-intl";
interface CurrencyProps {
type CurrencyFormatType = "symbol" | "code" | "name" | "narrowSymbol";
interface CurrencyProps extends FormatNumberOptions {
currencyCode?: string;
format?: CurrencyFormatType;
value: number;
}
export function Currency(props: CurrencyProps) {
const { currencyCode, value } = props;
const { currencyCode, format, value, ...formatNumberProps } = props;
const intl = useIntl();
const formattedNumber = useMemo(
@ -32,7 +36,7 @@ export function Currency(props: CurrencyProps) {
...(currencyCode
? {
currency: currencyCode,
currencyDisplay: "code",
currencyDisplay: format || "code",
style: "currency",
}
: {
@ -40,6 +44,7 @@ export function Currency(props: CurrencyProps) {
minimumFractionDigits: 2,
maximumFractionDigits: 12,
}),
...formatNumberProps,
}),
[currencyCode, value]
);

View File

@ -0,0 +1,39 @@
// =============================================================================
// 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 { createStyles } from "@mantine/core";
export const useStyles = createStyles((theme) => ({
primary: {
"thead tr": {
backgroundColor: theme.colors.gray[0],
th: {
borderBottomColor: theme.colors.gray[2],
color: theme.colors.gray[5],
fontSize: 10,
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
},
},
"tbody tr": {
td: {
borderBottomColor: theme.colors.gray[2],
},
},
},
}));

View File

@ -20,23 +20,32 @@ import {
getExpandedRowModel,
} from "@tanstack/react-table";
import { Table as MTable } from "@mantine/core";
import type { TableProps } from "./_types";
import { TableVariant } from "./_types";
import { TableProvider } from "./use-table-context";
import { TableHeader } from "./TableHeader";
import { TableBody } from "./TableBody";
import type { TableProps } from "./_types";
import { useStyles } from "./Table.style";
export function Table(props: TableProps) {
const { table, columns, data, tableWrap } = props;
const { classes, cx } = useStyles();
const { table, columns, data, tableWrap, variant, state } = props;
const tableInstance = useTableInstance(table, {
data,
columns,
state,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
});
return (
<MTable {...tableWrap}>
<MTable
{...tableWrap}
className={cx(tableWrap?.className, {
[classes.primary]: variant === TableVariant.Primary,
})}
>
<TableProvider value={{ table: tableInstance, props }}>
<TableHeader />
<TableBody />

View File

@ -38,6 +38,7 @@ export function TableBody() {
key={cell.id}
style={{
width: cell.column.getSize(),
textAlign: cell.column.columnDef?.meta?.textAlign,
}}
>
{cell.renderCell()}

View File

@ -36,6 +36,7 @@ export function TableHeader() {
colSpan={header.colSpan}
style={{
width: header.getSize(),
textAlign: header.column.columnDef?.meta?.textAlign,
}}
>
{header.isPlaceholder ? null : header.renderHeader()}

View File

@ -15,13 +15,14 @@
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ColumnDef, Row } from "@tanstack/react-table";
import type { ColumnDef, Row, TableState } from "@tanstack/react-table";
import type { TableProps as MTableProps } from "@mantine/core";
export interface TableProps {
columns: Array<ColumnDef<any>>;
table: any;
data: Array<any>;
state?: Partial<TableState>;
showHeader?: boolean;
showFooter?: boolean;
@ -29,4 +30,10 @@ export interface TableProps {
rowSubComponent?: ({ row }: { row: Row<any> }) => React.ReactNode;
tableWrap?: MTableProps;
variant?: TableVariant;
}
export enum TableVariant {
Default = "Default",
Primary = "Primary",
}

View File

@ -87,4 +87,4 @@ const data = [
height: 2482937,
fee: 0.000005096,
},
] as TWalletTransaction[];
] as Array<TWalletTransaction>;

View File

@ -0,0 +1,58 @@
// =============================================================================
// 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 { ComponentStory, ComponentMeta } from "@storybook/react";
import { MarketOffersTable } from "./MarketOffersTable";
export default {
title: "molecules/MarketOffersTable",
component: MarketOffersTable,
} as ComponentMeta<typeof MarketOffersTable>;
const Template: ComponentStory<typeof MarketOffersTable> = () => {
return <MarketOffersTable data={data} />;
};
export const Default = Template.bind({});
Default.args = {};
const data = [
{
price: 123,
priceCurrency: "EUR",
priceComparison: 0.12,
amount: 123123,
amountCurrency: "EUR",
cost: 123,
costCurrency: "USD",
paymentMethod: "Bitcoin",
accountAge: 12,
accountTrades: 1212,
},
{
price: 123,
priceCurrency: "EUR",
priceComparison: 0.12,
amount: 123123,
amountCurrency: "EUR",
cost: 123,
costCurrency: "USD",
paymentMethod: "Altcoins",
accountAge: 12,
accountTrades: 1212,
},
];

View File

@ -0,0 +1,116 @@
// =============================================================================
// 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, screen } from "@testing-library/react";
import { MarketOffersTable } from "./MarketOffersTable";
import { AppProviders } from "@atoms/AppProviders";
describe("molecules::MarketOffersTable", () => {
it("renders without exploding.", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all columns.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Price")).toBeInTheDocument();
expect(screen.queryByText("Amount")).toBeInTheDocument();
expect(screen.queryByText("Costs")).toBeInTheDocument();
expect(screen.queryByText("Payment Method")).toBeInTheDocument();
unmount();
});
it("renders formatted price value with currency sign.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("€5,000.96")).toBeInTheDocument();
expect(screen.queryByText("€9,637.41")).toBeInTheDocument();
unmount();
});
it("renders formatted amount value with currency code.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("XMR 7,564.94")).toBeInTheDocument();
expect(screen.queryByText("XMR 6,483.23")).toBeInTheDocument();
unmount();
});
it("renders offer formatted payment method.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Bitcoin")).toBeInTheDocument();
expect(screen.queryByText("Altcoins")).toBeInTheDocument();
unmount();
});
it("renders formatted price percentage.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("12%")).toBeInTheDocument();
expect(screen.queryByText("15%")).toBeInTheDocument();
unmount();
});
});
const data = [
{
price: 5000.956,
priceComparison: 0.12,
priceCurrency: "EUR",
amount: 7564.94,
amountCurrency: "XMR",
cost: 532.34,
costCurrency: "USD",
paymentMethod: "Bitcoin",
accountAge: 12,
accountTrades: 1212,
},
{
price: 9637.41,
priceComparison: 0.15,
priceCurrency: "EUR",
amount: 6483.23,
amountCurrency: "XMR",
cost: 983.32,
costCurrency: "USD",
paymentMethod: "Altcoins",
accountAge: 12,
accountTrades: 3412,
},
];

View File

@ -0,0 +1,150 @@
// =============================================================================
// 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 { createStyles } from "@mantine/core";
import { createTable } from "@tanstack/react-table";
import { useIntl } from "react-intl";
import type { MarketOffer } from "./_types";
import {
MarketOffersPriceCell,
MarketOffersAmountCell,
MarketOffersAccountTradesCell,
MarketOffersCostsCell,
MarketOffersAccountAgeCell,
MarketOffersPaymentCell,
} from "./MarketOffersTableCell";
import { Table } from "@molecules/Table";
import { LangKeys } from "@constants/lang";
import { TableVariant } from "@molecules/Table/_types";
const table = createTable().setRowType<MarketOffer>();
interface MarketOffersTableProps {
data: Array<MarketOffer>;
}
export function MarketOffersTable({ data }: MarketOffersTableProps) {
const { classes } = useStyles();
const columns = useMarketOffersColumns();
return (
<Table
table={table}
columns={columns}
data={data}
variant={TableVariant.Primary}
state={{
columnVisibility: {
accountAge: false,
accountTrades: false,
},
}}
tableWrap={{
verticalSpacing: "md",
className: classes.root,
}}
/>
);
}
const useStyles = createStyles((theme) => ({
root: {
"thead tr th, tbody tr td": {
"&:first-of-type": {
paddingLeft: 30,
},
"&:last-of-type": {
paddingRight: 30,
},
},
"tbody tr": {
td: {
background: theme.white,
paddingTop: 22,
paddingBottom: 22,
},
"&:last-of-type": {
td: {
borderBottom: `1px solid ${theme.colors.gray[2]}`,
},
},
},
},
}));
const useMarketOffersColumns = () => {
const { formatMessage } = useIntl();
return [
table.createDataColumn("price", {
id: "price",
header: formatMessage({
id: LangKeys.MarketsOffersColumnPrice,
defaultMessage: "Price",
}),
cell: ({ row }) => <MarketOffersPriceCell row={row.original} />,
size: 400,
}),
table.createDataColumn("amount", {
id: "amount",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAmount,
defaultMessage: "Amount",
}),
cell: ({ row }) => <MarketOffersAmountCell row={row.original} />,
size: 400,
}),
table.createDataColumn("cost", {
id: "costs",
header: formatMessage({
id: LangKeys.MarketsOffersColumnCost,
defaultMessage: "Costs",
}),
cell: ({ row }) => <MarketOffersCostsCell row={row.original} />,
size: 400,
}),
table.createDataColumn("paymentMethod", {
id: "paymentMethod",
header: formatMessage({
id: LangKeys.MarketsOffersColumnPaymentMethod,
defaultMessage: "Payment Method",
}),
cell: ({ row }) => <MarketOffersPaymentCell row={row.original} />,
size: 400,
}),
table.createDataColumn("accountAge", {
id: "accountAge",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAccountAge,
defaultMessage: "Account Age",
}),
cell: ({ row }) => <MarketOffersAccountAgeCell row={row.original} />,
size: 400,
}),
table.createDataColumn("accountTrades", {
id: "accountTrades",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAccountTrades,
defaultMessage: "Account Trades",
}),
cell: ({ row }) => <MarketOffersAccountTradesCell row={row.original} />,
size: 400,
meta: {
textAlign: "right",
},
}),
];
};

View File

@ -0,0 +1,87 @@
// =============================================================================
// 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 { Group, useMantineTheme } from "@mantine/core";
import type { MarketOffer } from "./_types";
import { Currency } from "@atoms/Currency";
import { BodyText } from "@atoms/Typography";
import { fractionToPercent } from "@src/utils/math";
import { ReactComponent as CheckCircle } from "@assets/check-circle.svg";
export function MarketOffersAccountAgeCell({ row }: { row?: MarketOffer }) {
const theme = useMantineTheme();
return (
<Group spacing="sm">
<CheckCircle color={theme.colors.blue[6]} width={15} height={15} />
<BodyText heavy>{row?.accountAge} Days</BodyText>
</Group>
);
}
export function MarketOffersPriceCell({ row }: { row?: MarketOffer }) {
return (
<Group spacing="sm">
<BodyText heavy>
<Currency
currencyCode={row?.priceCurrency}
value={row?.price || 0}
format="symbol"
/>
</BodyText>
<BodyText color="gray">
<Currency
value={fractionToPercent(row?.priceComparison || 0)}
minimumFractionDigits={0}
/>
%
</BodyText>
</Group>
);
}
export function MarketOffersAmountCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency currencyCode={row?.amountCurrency} value={row?.amount || 0} />
</BodyText>
);
}
export function MarketOffersCostsCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency
currencyCode={row?.costCurrency}
value={row?.cost || 0}
format="symbol"
/>
</BodyText>
);
}
export function MarketOffersAccountTradesCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency value={row?.accountTrades || 0} minimumFractionDigits={0} />
</BodyText>
);
}
export function MarketOffersPaymentCell({ row }: { row?: MarketOffer }) {
return <BodyText heavy>{row?.paymentMethod}</BodyText>;
}

View File

@ -0,0 +1,134 @@
// Vitest Snapshot v1
exports[`molecules::MarketOffersTable > renders without exploding. 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root mantine-1ei5v84"
>
<thead>
<tr>
<th
colspan="1"
style="width: 400px;"
>
Price
</th>
<th
colspan="1"
style="width: 400px;"
>
Amount
</th>
<th
colspan="1"
style="width: 400px;"
>
Costs
</th>
<th
colspan="1"
style="width: 400px;"
>
Payment Method
</th>
</tr>
</thead>
<tbody>
<tr>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-1lumg83"
>
<div
class="mantine-Text-root mantine-Group-child mantine-1h17kkk"
>
€5,000.96
</div>
<div
class="mantine-Text-root mantine-Group-child mantine-1qscdi2"
>
12%
</div>
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
XMR 7,564.94
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
$532.34
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
Bitcoin
</div>
</td>
</tr>
<tr>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-1lumg83"
>
<div
class="mantine-Text-root mantine-Group-child mantine-1h17kkk"
>
€9,637.41
</div>
<div
class="mantine-Text-root mantine-Group-child mantine-1qscdi2"
>
15%
</div>
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
XMR 6,483.23
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
$983.32
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
Altcoins
</div>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -0,0 +1,28 @@
// =============================================================================
// 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 interface MarketOffer {
price: number;
priceComparison: number;
priceCurrency: string;
amount: number;
amountCurrency: string;
cost: number;
costCurrency: string;
paymentMethod: string;
accountAge: number;
accountTrades: number;
}

View File

@ -0,0 +1,18 @@
// =============================================================================
// 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 "./MarketOffersTable";
export * from "./_types";

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 { ComponentStory, ComponentMeta } from "@storybook/react";
import { MarketsOffers } from "./MarketsOffers";
export default {
title: "organisms/MarketsOffers",
component: MarketsOffers,
} as ComponentMeta<typeof MarketsOffers>;
const Template: ComponentStory<typeof MarketsOffers> = () => {
return <MarketsOffers />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,57 @@
// =============================================================================
// 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 { FC } from "react";
import { useMemo } from "react";
import { Group, Loader } from "@mantine/core";
import { transformToMarketsOffers } from "./_utils";
import { useMarketsOffers } from "@hooks/haveno/useMarketsOffers";
import { MarketOffersTable } from "@organisms/MarketOffersTable";
export function MarketsOffersLoaded() {
const { data } = useMarketsOffers({
assetCode: "ETH",
direction: "sell",
});
const tableData = useMemo(() => transformToMarketsOffers(data || []), [data]);
if (!data) {
return null;
}
return <MarketOffersTable data={tableData} />;
}
const MarketsOffersBoot: FC = ({ children }) => {
const { isLoading: isOffersLoading } = useMarketsOffers({
assetCode: "ETH",
direction: "buy",
});
return isOffersLoading ? (
<Group>
<Loader color="gray" style={{ margin: "auto" }} />
</Group>
) : (
<>{children}</>
);
};
export function MarketsOffers() {
return (
<MarketsOffersBoot>
<MarketsOffersLoaded />
</MarketsOffersBoot>
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { MarketOfferData } from "@hooks/haveno/useMarketsOffers";
import type { MarketOffer } from "@organisms/MarketOffersTable";
export const transformToMarketsOffers = (
offers: Array<MarketOfferData>
): Array<MarketOffer> => {
return offers.map((offer) => ({
price: offer.price,
priceCurrency: offer.counterCurrencyCode,
amount: offer.amount,
amountCurrency: offer.baseCurrencyCode,
costCurrency: offer.baseCurrencyCode,
paymentMethod: offer.paymentMethodShortName,
cost: offer.txFee,
priceComparison: 0.1,
accountAge: 1,
accountTrades: 1,
}));
};

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 "./MarketsOffers";

View File

@ -16,17 +16,20 @@
import { UnstyledButton, Group, Text, createStyles } from "@mantine/core";
import type { ReactNode } from "react";
import { Link } from "react-router-dom";
interface NavLinkProps {
icon: ReactNode;
isActive?: boolean;
label: string;
link: string;
}
export function NavLink({ icon, isActive = false, label }: NavLinkProps) {
export function NavLink({ icon, isActive = false, label, link }: NavLinkProps) {
const { classes } = useStyles({ isActive });
return (
<UnstyledButton className={classes.navLink}>
<UnstyledButton component={Link} className={classes.navLink} to={link}>
<Group>
{icon}
<Text

View File

@ -32,9 +32,9 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/markets"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -58,14 +58,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Markets
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -93,14 +93,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
My Offers
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -132,14 +132,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
My Trades
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -163,14 +163,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Notifications
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -193,7 +193,7 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Account
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"

View File

@ -14,6 +14,7 @@
// limitations under the License.
// =============================================================================
import type { ReactNode } from "react";
import { ReactComponent as MarketsIcon } from "@assets/markets.svg";
import { ReactComponent as OffersIcon } from "@assets/offers.svg";
import { ReactComponent as TradesIcon } from "@assets/trades.svg";
@ -22,25 +23,36 @@ import { ReactComponent as AccountIcon } from "@assets/account.svg";
export const WIDTH = 210;
interface NavigationLink {
icon: ReactNode;
label: string;
link: string;
}
export const NAV_LINKS = [
{
icon: <MarketsIcon />,
label: "Markets",
link: "/markets",
},
{
icon: <OffersIcon />,
label: "My Offers",
link: "/",
},
{
icon: <TradesIcon />,
label: "My Trades",
link: "/",
},
{
icon: <NotificationsIcon />,
label: "Notifications",
link: "/",
},
{
icon: <AccountIcon />,
label: "Account",
link: "/",
},
];
] as Array<NavigationLink>;

View File

@ -19,7 +19,7 @@ import { NavbarLayout } from "@templates/NavbarLayout";
import { AccountSidebar } from "@organisms/AccountSidebar";
interface AccountContentProps {
children: JSX.Element | JSX.Element[];
children: JSX.Element | Array<JSX.Element>;
}
function AccountContent({ children }: AccountContentProps) {
@ -34,7 +34,7 @@ function AccountContent({ children }: AccountContentProps) {
}
interface AccountLayoutProps {
children: JSX.Element | JSX.Element[];
children: JSX.Element | Array<JSX.Element>;
}
export function AccountLayout({ children }: AccountLayoutProps) {

View File

@ -14,15 +14,24 @@
// limitations under the License.
// =============================================================================
import { Box, createStyles, Group } from "@mantine/core";
import type { FC } from "react";
import type { DefaultProps } from "@mantine/core";
import { Box, createStyles, Group } from "@mantine/core";
import { Sidebar } from "@organisms/Sidebar";
export const NavbarLayout: FC = (props) => {
const { children } = props;
const { classes } = useStyles();
export const NavbarLayout: FC<DefaultProps> = ({
children,
classNames,
className,
...rest
}) => {
const { classes, cx } = useStyles(undefined, {
name: "NavbarLayout",
classNames,
});
return (
<Group className={classes.container} spacing={0}>
<Group className={cx(classes.container, className)} spacing={0} {...rest}>
<Sidebar />
<Box className={classes.contentArea}>{children}</Box>
</Group>

View File

@ -99,4 +99,11 @@ export enum LangKeys {
AccountBackupRestoreBtn = "account.backup.restore.btn",
AccountBackupDownloadSuccessNotif = "account.backup.download.successNotification",
AccountBackupRestoreSuccessNotif = "account.backup.restore.successNotification",
MarketsOffersColumnPrice = "marketsOffers.columnPrice",
MarketsOffersColumnAmount = "marketsOffers.columnAmount",
MarketsOffersColumnCost = "marketsOffers.columnCost",
MarketsOffersColumnPaymentMethod = "marketsOffers.columnPaymentMethod",
MarketsOffersColumnAccountAge = "marketsOffers.columnAccountAge",
MarketsOffersColumnAccountTrades = "marketsOffers.columnAccountTypes",
}

View File

@ -116,6 +116,12 @@ const LangPackEN: { [key in LangKeys]: string } = {
"The backup has been downloaded successfully.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"The backup has been restored successfully.",
[LangKeys.MarketsOffersColumnPrice]: "Price",
[LangKeys.MarketsOffersColumnAmount]: "Amount",
[LangKeys.MarketsOffersColumnCost]: "Costs",
[LangKeys.MarketsOffersColumnAccountAge]: "Account Age",
[LangKeys.MarketsOffersColumnAccountTrades]: "Account Trades",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Payment Method",
};
export default LangPackEN;

View File

@ -119,6 +119,12 @@ const LangPackES: { [key in LangKeys]: string } = {
"La copia de seguridad se ha descargado correctamente.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"La copia de seguridad se ha restaurado correctamente.",
[LangKeys.MarketsOffersColumnPrice]: "Precio",
[LangKeys.MarketsOffersColumnAmount]: "Monto",
[LangKeys.MarketsOffersColumnCost]: "Costos",
[LangKeys.MarketsOffersColumnAccountAge]: "Edad de la cuenta",
[LangKeys.MarketsOffersColumnAccountTrades]: "Operaciones de cuenta",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Método de pago",
};
export default LangPackES;

View File

@ -29,6 +29,8 @@ export enum QueryKeys {
XmrSeed = "Haveno.XmrSeed",
XmrPrimaryAddress = "Haveno.XmrPrimaryAddress",
XmrTxs = "Haveno.XmrTransactions",
MarketsOffers = "Haveno.MarketsOffers",
MyOffers = "Haveno.MyOffers",
// Storage
StorageAccountInfo = "Storage.AccountInfo",

View File

@ -22,6 +22,7 @@ export const ROUTES = {
CreateAccount: "/onboarding/create-account",
RestoreBackup: "/onboarding/restore-backup",
MyWallet: "/my-wallet",
Markets: "/markets",
// Account routes
PaymentAccounts: "/account/payment-accounts",

View File

@ -0,0 +1,82 @@
// =============================================================================
// 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 { useQuery } from "react-query";
import type { OfferInfo } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface MarketsOfferesQuery {
assetCode: string;
direction?: "buy" | "sell";
}
export function useMarketsOffers(query: MarketsOfferesQuery) {
const client = useHavenoClient();
return useQuery<Array<MarketOfferData>, Error>(
[QueryKeys.MarketsOffers, query],
async () => {
const offers = await client.getMyOffers(query.assetCode);
return transformData(offers);
}
);
}
const transformData = (offers: Array<OfferInfo>) => {
return offers.map((offerObj: OfferInfo): MarketOfferData => {
const offer = offerObj.toObject();
return {
...offer,
price: parseFloat(offer.price),
volume: parseFloat(offer.volume),
minVolume: parseFloat(offer.minVolume),
triggerPrice: parseFloat(offer.triggerPrice),
};
});
};
export interface MarketOfferData {
id: string;
direction: string;
price: number;
useMarketBasedPrice: boolean;
marketPriceMarginPct: number;
amount: number;
minAmount: number;
volume: number;
minVolume: number;
buyerSecurityDeposit: number;
triggerPrice: number;
paymentAccountId: string;
paymentMethodId: string;
paymentMethodShortName: string;
baseCurrencyCode: string;
counterCurrencyCode: string;
date: number;
state: string;
sellerSecurityDeposit: number;
offerFeePaymentTxId: string;
txFee: number;
makerFee: number;
isActivated: boolean;
isMyOffer: boolean;
ownerNodeAddress: string;
pubKeyRing: string;
versionNr: string;
protocolVersion: number;
}

View File

@ -0,0 +1,33 @@
// =============================================================================
// 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 { useQuery } from "react-query";
import type { OfferInfo } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface MyOfferesQuery {
assetCode: string;
direction?: "buy" | "sell";
}
export function useMarketsOffers(query: MyOfferesQuery) {
const client = useHavenoClient();
return useQuery<Array<OfferInfo>, Error>([QueryKeys.MyOffers, query], () =>
client.getMyOffers(query.assetCode, query.direction)
);
}

View File

@ -0,0 +1,54 @@
// =============================================================================
// 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 { useMutation, useQueryClient } from "react-query";
import { showNotification } from "@mantine/notifications";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface SetCryptoPaymentAccount {
accountName: string;
assetCode: string;
address: string;
}
export function useSetCryptoPaymentAccount() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation(
async (variables: SetCryptoPaymentAccount) => {
return client.createCryptoPaymentAccount(
variables.accountName,
variables.assetCode,
variables.address
);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.PaymentAccounts);
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message || "",
title: "Something went wrong",
});
},
}
);
}

View File

@ -0,0 +1,67 @@
// =============================================================================
// 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 { useMutation, useQueryClient } from "react-query";
import { showNotification } from "@mantine/notifications";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface SetOfferVariables {
direction: string;
amount: bigint;
assetCode: string;
paymentAccountId: string;
buyerSecurityDeposit: number;
price?: number;
marketPriceMargin?: number;
triggerPrice?: number;
minAmount?: bigint;
}
export function useSetOffer() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation(
async (variables: SetOfferVariables) => {
return client.postOffer(
variables.direction,
variables.amount,
variables.assetCode,
variables.paymentAccountId,
variables.buyerSecurityDeposit,
variables.price,
variables.marketPriceMargin,
variables.triggerPrice,
variables.minAmount
);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MarketsOffers);
queryClient.invalidateQueries(QueryKeys.MyOffers);
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message || "",
title: "Something went wrong",
});
},
}
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { createStyles } from "@mantine/core";
import { NavbarLayout } from "@templates/NavbarLayout";
import { MarketsOffers } from "@organisms/MarketsOffers";
export function MarketsOffersPage() {
const { classes } = useStyles();
return (
<NavbarLayout classNames={{ contentArea: classes.contentArea }}>
<MarketsOffers />
</NavbarLayout>
);
}
const useStyles = createStyles(() => ({
contentArea: {
padding: 0,
},
}));

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 "./MarketsOffers";

View File

@ -0,0 +1,19 @@
// =============================================================================
// 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 const fractionToPercent = (value: number) => {
return value * 100;
};

View File

@ -8187,10 +8187,10 @@ hastscript@^6.0.0:
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
haveno-ts@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/haveno-ts/-/haveno-ts-0.0.5.tgz#996e290b4dd1659e58f86aff4e0cc0d3c7516ad3"
integrity sha512-HzBuvMmsQLUybk99NqOe50lAcYmnwv2uv0/5IMAIuUpqFmJq2xvPW6vvZASXkRBVYPdRYjLLnUV5ygy8Rq7Yog==
haveno-ts@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/haveno-ts/-/haveno-ts-0.0.6.tgz#ba83986fb741744d3a4441ee323992deff838d0d"
integrity sha512-9OYfr2tGD8nTSSBzgltVeoZdntddwt6WrYj4SLN+9BArqSEdZ9jF+6O2xtUGaVaJDNVli2Eg6pINEAxsUN5MhA==
dependencies:
"@types/node" "^17.0.30"
console "^0.7.2"