Skip to content

Commit

Permalink
Feature: ERC-1155 Transfers (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunomenezes authored May 7, 2024
1 parent 3e27275 commit 6580e4d
Show file tree
Hide file tree
Showing 32 changed files with 4,963 additions and 135 deletions.
6 changes: 6 additions & 0 deletions apps/web/graphql/queries.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ query tokens($limit: Int, $where: TokenWhereInput) {
}
}

query multiTokens($limit: Int, $where: MultiTokenWhereInput) {
multiTokens(limit: $limit, where: $where) {
id
}
}

fragment ApplicationItem on Application {
id
owner
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ColorSchemeScript } from "@mantine/core";
import "@mantine/core/styles.css";
import { Notifications } from "@mantine/notifications";
import "@mantine/notifications/styles.css";
import { Analytics } from "@vercel/analytics/react";
import { Metadata } from "next";
Expand Down Expand Up @@ -34,7 +35,10 @@ const Layout: FC<LayoutProps> = ({ children }) => {
</head>
<body>
<Providers>
<Shell>{children}</Shell>
<>
<Notifications />
<Shell>{children}</Shell>
</>
</Providers>
<Analytics />
</body>
Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/components/BlockExplorerLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Anchor, Group, Text, rem } from "@mantine/core";
import { anyPass, equals } from "ramda";
import { FC } from "react";
import { TbExternalLink } from "react-icons/tb";
import { useConfig } from "wagmi";

interface BlockExplorerLinkProps {
value: string;
type: "tx" | "block" | "address";
}

const isTxOrAddress = anyPass([equals("tx"), equals("address")]);

/**
*
* Works in conjuction with Wagmi. It requires a Wagmi-Provider to work as expected.
* When running devnet it will not render a block-explorer link.
*
*/
export const BlockExplorerLink: FC<BlockExplorerLinkProps> = ({
value,
type,
}) => {
const config = useConfig();
const explorerUrl = config.chains[0].blockExplorers?.default.url;

if (!explorerUrl) return;

const shouldShorten = isTxOrAddress(type);

const text = shouldShorten
? `${value.slice(0, 8)}...${value.slice(-6)}`
: value;

const href = `${explorerUrl}/${type}/${value}`;

return (
<Anchor href={href} target="_blank">
<Group gap="xs">
<Text>{text}</Text>
<TbExternalLink style={{ width: rem(21), height: rem(21) }} />
</Group>
</Anchor>
);
};
76 changes: 73 additions & 3 deletions apps/web/src/components/sendTransaction.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,62 @@
"use client";
import {
DepositFormSuccessData,
ERC1155DepositForm,
ERC20DepositForm,
ERC721DepositForm,
EtherDepositForm,
RawInputForm,
} from "@cartesi/rollups-explorer-ui";
import { Select } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { FC, useCallback, useState } from "react";
import { useSearchApplications } from "../hooks/useSearchApplications";
import { useSearchMultiTokens } from "../hooks/useSearchMultiTokens";
import { useSearchTokens } from "../hooks/useSearchTokens";
import { notifications } from "@mantine/notifications";
import { BlockExplorerLink } from "./BlockExplorerLink";

export type DepositType =
| "ether"
| "erc20"
| "erc721"
| "erc1155"
| "erc1155Batch"
| "relay"
| "input";

interface DepositProps {
initialDepositType?: DepositType;
}

const DEBOUNCE_TIME = 400 as const;

const SendTransaction: FC<DepositProps> = ({
initialDepositType = "ether",
}) => {
const [depositType, setDepositType] =
useState<DepositType>(initialDepositType);
const [applicationId, setApplicationId] = useState<string>("");
const [multiTokenId, setMultiTokenId] = useState<string>("");
const [tokenId, setTokenId] = useState<string>("");
const [debouncedApplicationId] = useDebouncedValue(applicationId, 400);
const [debouncedTokenId] = useDebouncedValue(tokenId, 400);
const [debouncedApplicationId] = useDebouncedValue(
applicationId,
DEBOUNCE_TIME,
);
const [debouncedTokenId] = useDebouncedValue(tokenId, DEBOUNCE_TIME);
const [debouncedMultiTokenId] = useDebouncedValue(
multiTokenId,
DEBOUNCE_TIME,
);
const { applications, fetching } = useSearchApplications({
address: debouncedApplicationId,
});
const { tokens } = useSearchTokens({
address: debouncedTokenId,
});
const { multiTokens } = useSearchMultiTokens({
address: debouncedMultiTokenId,
});

const onDepositErc721 = useCallback(() => {
notifications.show({
Expand All @@ -48,6 +66,33 @@ const SendTransaction: FC<DepositProps> = ({
});
}, []);

const onSuccess = useCallback(
({ receipt, type }: DepositFormSuccessData) => {
const message = receipt?.transactionHash
? {
message: (
<BlockExplorerLink
value={receipt.transactionHash.toString()}
type="tx"
/>
),
title: `${type} transfer completed`,
}
: { message: `${type} transfer completed.` };

notifications.show({
withCloseButton: true,
autoClose: false,
color: "green",
...message,
});

setMultiTokenId("");
setApplicationId("");
},
[],
);

return (
<>
<Select
Expand All @@ -62,6 +107,11 @@ const SendTransaction: FC<DepositProps> = ({
{ value: "ether", label: "Ether Deposit" },
{ value: "erc20", label: "ERC-20 Deposit" },
{ value: "erc721", label: "ERC-721 Deposit" },
{ value: "erc1155", label: "ERC-1155 Deposit" },
{
value: "erc1155Batch",
label: "ERC-1155 Batch Deposit",
},
],
},
{
Expand Down Expand Up @@ -103,6 +153,26 @@ const SendTransaction: FC<DepositProps> = ({
isLoadingApplications={fetching}
onSearchApplications={setApplicationId}
/>
) : depositType === "erc1155" ? (
<ERC1155DepositForm
mode="single"
tokens={multiTokens}
applications={applications}
isLoadingApplications={fetching}
onSearchApplications={setApplicationId}
onSearchTokens={setMultiTokenId}
onSuccess={onSuccess}
/>
) : depositType === "erc1155Batch" ? (
<ERC1155DepositForm
mode="batch"
tokens={multiTokens}
applications={applications}
isLoadingApplications={fetching}
onSearchApplications={setApplicationId}
onSearchTokens={setMultiTokenId}
onSuccess={onSuccess}
/>
) : null}
</>
);
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/hooks/useSearchMultiTokens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Address } from "viem";
import { useMultiTokensQuery } from "../graphql/explorer/hooks/queries";

type SearchInput = { address?: string; limit?: number };
type SearchOutput = { fetching: boolean; multiTokens: Address[] };
type UseSearchMultiTokens = (args: SearchInput) => SearchOutput;

export const useSearchMultiTokens: UseSearchMultiTokens = ({
address,
limit,
}) => {
const [{ data, fetching }] = useMultiTokensQuery({
variables: {
limit: limit ?? 10,
where: {
id_containsInsensitive: address ?? "",
},
},
});

const multiTokens = (data?.multiTokens ?? []).map((t) => t.id as Address);
return { multiTokens, fetching };
};
2 changes: 0 additions & 2 deletions apps/web/src/providers/styleProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import type { FC, ReactNode } from "react";
import { theme } from "./theme";

Expand All @@ -10,7 +9,6 @@ export type StyleProviderProps = {
const StyleProvider: FC<StyleProviderProps> = (props) => {
return (
<MantineProvider defaultColorScheme="dark" theme={theme}>
<Notifications />
{props.children}
</MantineProvider>
);
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/providers/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
List,
MantineThemeOverride,
Modal,
Paper,
createTheme,
mergeMantineTheme,
} from "@mantine/core";
Expand Down Expand Up @@ -52,6 +53,11 @@ const themeOverride: MantineThemeOverride = createTheme({
withBorder: true,
},
}),
Paper: Paper.extend({
defaultProps: {
shadow: "xs",
},
}),
},
});

Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/providers/walletProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import { ReactNode } from "react";
import { useMantineColorScheme } from "@mantine/core";
import {
AvatarComponent,
Expand All @@ -10,14 +9,14 @@ import {
RainbowKitProvider,
} from "@rainbow-me/rainbowkit";
import { ThemeOptions } from "@rainbow-me/rainbowkit/dist/themes/baseTheme";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@rainbow-me/rainbowkit/styles.css";
import { ledgerWallet, trustWallet } from "@rainbow-me/rainbowkit/wallets";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Image from "next/image";
import { ReactNode } from "react";
import Jazzicon, { jsNumberForAddress } from "react-jazzicon";
import { createConfig, fallback, http, WagmiProvider } from "wagmi";
import { foundry, mainnet, sepolia } from "wagmi/chains";
import { Transport } from "viem";

// select chain based on env var
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || "31337");
Expand Down
71 changes: 71 additions & 0 deletions apps/web/test/components/BlockExplorerLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { cleanup, render, screen } from "@testing-library/react";
import { foundry, sepolia } from "viem/chains";
import { afterEach, beforeEach, describe, it } from "vitest";
import { useConfig } from "wagmi";
import { BlockExplorerLink } from "../../src/components/BlockExplorerLink";
import withMantineTheme from "../utils/WithMantineTheme";

vi.mock("wagmi");
const useConfigMock = vi.mocked(useConfig, { partial: true });
const Component = withMantineTheme(BlockExplorerLink);
const txHash =
"0x4d6ce102c5aedd46aff879dbb42eef3465a2b7aa7fb39e64296194fb313efebf";
const address = "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa";
const blockNumber = 5298115;

describe("BlockExplorerLink component", () => {
beforeEach(() => {
useConfigMock.mockReturnValue({
chains: [sepolia],
});
});

afterEach(() => {
vi.clearAllMocks();
cleanup();
});

it("should render nothing when the block-explorer URL is not available", () => {
useConfigMock.mockReturnValue({
chains: [foundry],
});

render(<Component type="tx" value={txHash} />);
const link = screen.queryByText("0x4d6ce1...3efebf")?.closest("a");

expect(screen.queryByText("0x4d6ce1...3efebf")).not.toBeInTheDocument();
expect(link).not.toBeDefined();
});

it("should render the correct link to the block-explorer given a transaction hash", () => {
render(<Component type="tx" value={txHash} />);
const link = screen.getByText("0x4d6ce1...3efebf").closest("a");

expect(screen.getByText("0x4d6ce1...3efebf")).toBeInTheDocument();
expect(link?.getAttribute("href")).toEqual(
`https://sepolia.etherscan.io/tx/${txHash}`,
);
});

it("should render the correct link to the block-explorer given an address", () => {
render(<Component type="address" value={address} />);
const textEl = screen.getByText("0xedB538...B055Aa");
const link = textEl.closest("a");

expect(textEl).toBeInTheDocument();
expect(link?.getAttribute("href")).toEqual(
`https://sepolia.etherscan.io/address/0xedB53860A6B52bbb7561Ad596416ee9965B055Aa`,
);
});

it("should render the correct link to the block-explorer given an block-number", () => {
render(<Component type="block" value={blockNumber.toString()} />);
const textEl = screen.getByText("5298115");
const link = textEl.closest("a");

expect(textEl).toBeInTheDocument();
expect(link?.getAttribute("href")).toEqual(
`https://sepolia.etherscan.io/block/5298115`,
);
});
});
Loading

0 comments on commit 6580e4d

Please sign in to comment.