diff --git a/apps/web/src/components/AccountCard/AccountBalance.test.tsx b/apps/web/src/components/AccountCard/AccountBalance.test.tsx index 3a0252f491..cce60fe3df 100644 --- a/apps/web/src/components/AccountCard/AccountBalance.test.tsx +++ b/apps/web/src/components/AccountCard/AccountBalance.test.tsx @@ -5,7 +5,9 @@ import { addTestAccount, assetsActions, makeStore, + networksActions, } from "@umami/state"; +import { GHOSTNET, MAINNET } from "@umami/tezos"; import { AccountBalance } from "./AccountBalance"; import { act, render, screen, userEvent, waitFor, within } from "../../testUtils"; @@ -20,7 +22,9 @@ beforeEach(() => { }); describe("", () => { - it("renders a buy tez link", () => { + it("renders a buy tez link for mainnet", () => { + store.dispatch(networksActions.setCurrent(MAINNET)); + render(, { store }); const link = screen.getByRole("link", { name: "Buy" }); @@ -31,6 +35,16 @@ describe("", () => { ); }); + it("renders a buy tez link for ghostnet", () => { + store.dispatch(networksActions.setCurrent(GHOSTNET)); + + render(, { store }); + + const link = screen.getByRole("link", { name: "Buy" }); + expect(link).toBeVisible(); + expect(link).toHaveAttribute("href", "https://faucet.ghostnet.teztnets.com/"); + }); + it("renders a receive button", () => { render(, { store }); diff --git a/apps/web/src/components/AccountCard/AccountBalance.tsx b/apps/web/src/components/AccountCard/AccountBalance.tsx index 25e4f5bef2..ed079eac9c 100644 --- a/apps/web/src/components/AccountCard/AccountBalance.tsx +++ b/apps/web/src/components/AccountCard/AccountBalance.tsx @@ -1,6 +1,11 @@ import { Box, Flex, Link, Text } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; -import { useCurrentAccount, useGetAccountBalance, useGetDollarBalance } from "@umami/state"; +import { + useBuyTezUrl, + useCurrentAccount, + useGetAccountBalance, + useGetDollarBalance, +} from "@umami/state"; import { TEZ, prettyTezAmount } from "@umami/tezos"; import { SendTezButton } from "./SendTezButton"; @@ -19,7 +24,7 @@ export const AccountBalance = () => { const usdBalance = useGetDollarBalance()(address); const isVerified = useIsAccountVerified(); - const buyTezUrl = `https://widget.wert.io/default/widget/?commodity=XTZ&address=${address}&network=tezos&commodity_id=xtz.simple.tezos`; + const buyTezUrl = useBuyTezUrl(address); const getUsdBalance = () => { if (balance === undefined) { diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx index c5186cd0ff..8ccc3389b0 100644 --- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx +++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx @@ -1,4 +1,5 @@ import { SigningType } from "@airgap/beacon-wallet"; +import { useToast } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core"; import { @@ -9,7 +10,7 @@ import { walletKit, } from "@umami/state"; import { WalletConnectError } from "@umami/utils"; -import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; +import { formatJsonRpcError, formatJsonRpcResult } from "@walletconnect/jsonrpc-utils"; import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types"; import { type SdkErrorKey, getSdkError } from "@walletconnect/utils"; @@ -21,7 +22,6 @@ import { type SignHeaderProps, type SignPayloadProps, } from "../SendFlow/utils"; - /** * @returns a function that handles a beacon message and opens a modal with the appropriate content * @@ -34,6 +34,7 @@ export const useHandleWcRequest = () => { const getAccount = useGetOwnedAccountSafe(); const getImplicitAccount = useGetImplicitAccount(); const findNetwork = useFindNetwork(); + const toast = useToast(); return async ( event: { @@ -57,11 +58,28 @@ export const useHandleWcRequest = () => { switch (request.method) { case "tezos_getAccounts": { - throw new WalletConnectError( - "Getting accounts is not supported yet", - "WC_METHOD_UNSUPPORTED", - session - ); + const wcPeers = walletKit.getActiveSessions(); + if (!(topic in wcPeers)) { + throw new WalletConnectError(`Unknown session ${topic}`, "UNAUTHORIZED_EVENT", null); + } + const session = wcPeers[topic]; + const accountPkh = session.namespaces.tezos.accounts[0].split(":")[2]; + const signer = getImplicitAccount(accountPkh); + const publicKey = signer.pk; + const response = formatJsonRpcResult(id, [ + { + algo: "ed25519", // the only supported curve + address: accountPkh, + pubkey: publicKey, + }, + ]); + await walletKit.respondSessionRequest({ topic, response }); + + toast({ + description: "Successfully provided the requested account data", + status: "success", + }); + return; } case "tezos_sign": { diff --git a/apps/web/src/views/Activity/Activity.test.tsx b/apps/web/src/views/Activity/Activity.test.tsx index 73fd07bd39..e0a544ad4c 100644 --- a/apps/web/src/views/Activity/Activity.test.tsx +++ b/apps/web/src/views/Activity/Activity.test.tsx @@ -6,7 +6,7 @@ import { makeStore, networksActions, } from "@umami/state"; -import { MAINNET } from "@umami/tezos"; +import { GHOSTNET, MAINNET } from "@umami/tezos"; import { type TzktCombinedOperation, getCombinedOperations, @@ -45,7 +45,7 @@ describe("", () => { jest.mocked(getRelatedTokenTransfers).mockResolvedValue([]); }); - it("displays an empty state ", async () => { + it("displays an empty state", async () => { render(, { store }); await waitFor(() => { @@ -57,6 +57,32 @@ describe("", () => { ).toBeVisible(); expect(screen.queryByTestId("view-all-operations-button")).not.toBeInTheDocument(); }); + + it("has correct mainnet Buy Tez link", async () => { + store.dispatch(networksActions.setCurrent(MAINNET)); + render(, { store }); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-message")).toBeVisible(); + }); + const link = screen.getByRole("link", { name: "Buy Tez Now" }); + expect(link).toHaveAttribute( + "href", + `https://widget.wert.io/default/widget/?commodity=XTZ&address=${account.address.pkh}&network=tezos&commodity_id=xtz.simple.tezos` + ); + }); + + it("has correct ghostnet Buy Tez link", async () => { + store.dispatch(networksActions.setCurrent(GHOSTNET)); + render(, { store }); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-message")).toBeVisible(); + }); + const link = screen.getByRole("link", { name: "Buy Tez Now" }); + expect(link).toBeVisible(); + expect(link).toHaveAttribute("href", "https://faucet.ghostnet.teztnets.com/"); + }); }); describe("with operations", () => { diff --git a/apps/web/src/views/Activity/Activity.tsx b/apps/web/src/views/Activity/Activity.tsx index ba6824010d..ccf4b39d88 100644 --- a/apps/web/src/views/Activity/Activity.tsx +++ b/apps/web/src/views/Activity/Activity.tsx @@ -1,7 +1,7 @@ import { Box, Center, Divider, Flex, Image, Spinner } from "@chakra-ui/react"; import { type Account } from "@umami/core"; import { useGetOperations } from "@umami/data-polling"; -import { useCurrentAccount } from "@umami/state"; +import { useBuyTezUrl, useCurrentAccount } from "@umami/state"; import loadingDots from "../../assets/loading-dots.gif"; import { EmptyMessage } from "../../components/EmptyMessage"; @@ -21,7 +21,7 @@ export const Activity = () => { isVerified ); - const buyTezUrl = `https://widget.wert.io/default/widget/?commodity=XTZ&address=${currentAccount?.address.pkh}&network=tezos&commodity_id=xtz.simple.tezos`; + const buyTezUrl = useBuyTezUrl(currentAccount?.address.pkh); const isEmpty = operations.length === 0 && !isLoading; diff --git a/apps/web/src/views/Earn/Earn.tsx b/apps/web/src/views/Earn/Earn.tsx index 4a47046f11..d4fff578ab 100644 --- a/apps/web/src/views/Earn/Earn.tsx +++ b/apps/web/src/views/Earn/Earn.tsx @@ -8,7 +8,7 @@ import { ViewOverlay } from "../../components/ViewOverlay/ViewOverlay"; export const Earn = () => { const isVerified = useIsAccountVerified(); - const buyTezUrl = "https://stake.tezos.com/"; + const stakeTezosUrl = "https://stake.tezos.com/"; return ( <> @@ -16,7 +16,7 @@ export const Earn = () => { {isVerified ? ( diff --git a/apps/web/src/views/Tokens/Token.tsx b/apps/web/src/views/Tokens/Token.tsx index f9ff8e6382..ab21853452 100644 --- a/apps/web/src/views/Tokens/Token.tsx +++ b/apps/web/src/views/Tokens/Token.tsx @@ -43,6 +43,7 @@ export const Token = ({ token }: TokenProps) => { _notLast={{ borderBottom: `1px solid ${color("100")}`, }} + data-testid="token-card" paddingY={{ base: "18px", md: "30px" }} > diff --git a/apps/web/src/views/Tokens/Tokens.test.tsx b/apps/web/src/views/Tokens/Tokens.test.tsx new file mode 100644 index 0000000000..adb1241d9c --- /dev/null +++ b/apps/web/src/views/Tokens/Tokens.test.tsx @@ -0,0 +1,72 @@ +import { mockImplicitAccount, mockMnemonicAccount } from "@umami/core"; +import { + type UmamiStore, + accountsActions, + addTestAccount, + makeStore, + networksActions, +} from "@umami/state"; +import { GHOSTNET, MAINNET } from "@umami/tezos"; + +import { Tokens } from "./Tokens"; +import { render, screen, waitFor } from "../../testUtils"; + +let store: UmamiStore; +const account = mockImplicitAccount(0); + +beforeEach(() => { + store = makeStore(); + addTestAccount(store, account); + store.dispatch(accountsActions.setCurrent(account.address.pkh)); +}); + +describe("", () => { + beforeEach(() => { + addTestAccount(store, mockMnemonicAccount(1)); + addTestAccount(store, mockMnemonicAccount(2)); + store.dispatch(networksActions.setCurrent(MAINNET)); + }); + + describe("without tokens", () => { + it("displays an empty state", async () => { + render(, { store }); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-message")).toBeVisible(); + }); + + expect(screen.getByText("Get Started with Tokens")).toBeVisible(); + expect( + screen.getByText("You need Tez to take part in any activity. Buy some to get started.") + ).toBeVisible(); + expect(screen.getByText("Buy Tez Now")).toBeVisible(); + expect(screen.queryByTestId("token-card")).not.toBeInTheDocument(); + }); + + it("has correct mainnet Buy Tez link", async () => { + store.dispatch(networksActions.setCurrent(MAINNET)); + render(, { store }); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-message")).toBeVisible(); + }); + const link = screen.getByRole("link", { name: "Buy Tez Now" }); + expect(link).toHaveAttribute( + "href", + `https://widget.wert.io/default/widget/?commodity=XTZ&address=${account.address.pkh}&network=tezos&commodity_id=xtz.simple.tezos` + ); + }); + + it("has correct ghostnet Buy Tez link", async () => { + store.dispatch(networksActions.setCurrent(GHOSTNET)); + render(, { store }); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-message")).toBeVisible(); + }); + const link = screen.getByRole("link", { name: "Buy Tez Now" }); + expect(link).toBeVisible(); + expect(link).toHaveAttribute("href", "https://faucet.ghostnet.teztnets.com/"); + }); + }); +}); diff --git a/apps/web/src/views/Tokens/Tokens.tsx b/apps/web/src/views/Tokens/Tokens.tsx index 3baa203218..96d0702bb5 100644 --- a/apps/web/src/views/Tokens/Tokens.tsx +++ b/apps/web/src/views/Tokens/Tokens.tsx @@ -1,6 +1,6 @@ import { Flex, VStack } from "@chakra-ui/react"; import { fullId } from "@umami/core"; -import { useCurrentAccount, useGetAccountAllTokens } from "@umami/state"; +import { useBuyTezUrl, useCurrentAccount, useGetAccountAllTokens } from "@umami/state"; import { Token } from "./Token"; import { EmptyMessage } from "../../components/EmptyMessage"; @@ -13,7 +13,7 @@ export const Tokens = () => { const currentAccount = useCurrentAccount()!; const availableTokens = useGetAccountAllTokens()(currentAccount.address.pkh); - const buyTezUrl = `https://widget.wert.io/default/widget/?commodity=XTZ&address=${currentAccount.address.pkh}&network=tezos&commodity_id=xtz.simple.tezos`; + const buyTezUrl = useBuyTezUrl(currentAccount.address.pkh); return ( <> diff --git a/packages/state/src/hooks/network.test.ts b/packages/state/src/hooks/network.test.ts index 1d9ecdb119..f905f2553e 100644 --- a/packages/state/src/hooks/network.test.ts +++ b/packages/state/src/hooks/network.test.ts @@ -3,6 +3,7 @@ import { GHOSTNET, MAINNET, mockImplicitAddress } from "@umami/tezos"; import { useAvailableNetworks, + useBuyTezUrl, useFindNetwork, useSelectNetwork, useSelectedNetwork, @@ -35,6 +36,28 @@ describe("networkHooks", () => { }); }); + describe("useBuyTezUrl", () => { + it("for mainnet - returns customized mainnet url", () => { + store.dispatch(networksActions.setCurrent(MAINNET)); + + const { + result: { current }, + } = renderHook(() => useBuyTezUrl("pkh123"), { store }); + expect(current).toEqual( + `${MAINNET.buyTezUrl}/default/widget/?commodity=XTZ&address=pkh123&network=tezos&commodity_id=xtz.simple.tezos` + ); + }); + + it("for others - returns url from network setting", () => { + store.dispatch(networksActions.setCurrent(GHOSTNET)); + + const { + result: { current }, + } = renderHook(() => useBuyTezUrl("pkh123"), { store }); + expect(current).toEqual(GHOSTNET.buyTezUrl); + }); + }); + describe("useAvailableNetworks", () => { it("returns default networks by default", () => { const { diff --git a/packages/state/src/hooks/network.ts b/packages/state/src/hooks/network.ts index 232542ea97..78070cac65 100644 --- a/packages/state/src/hooks/network.ts +++ b/packages/state/src/hooks/network.ts @@ -1,3 +1,4 @@ +import { MAINNET } from "@umami/tezos"; import { useDispatch } from "react-redux"; import { useAppSelector } from "./useAppSelector"; @@ -5,6 +6,17 @@ import { assetsActions, networksActions } from "../slices"; export const useSelectedNetwork = () => useAppSelector(s => s.networks.current); +export const useBuyTezUrl = (pkh?: string) => { + const network = useSelectedNetwork(); + let buyTezUrl = network.buyTezUrl; + + if (buyTezUrl && network === MAINNET) { + buyTezUrl += `/default/widget/?commodity=XTZ&address=${pkh}&network=tezos&commodity_id=xtz.simple.tezos`; + } + + return buyTezUrl; +}; + export const useAvailableNetworks = () => useAppSelector(s => s.networks.available); export const useFindNetwork = () => {