Skip to content

Commit

Permalink
feat(explorer): anvil connector, connect external wallets (#3164)
Browse files Browse the repository at this point in the history
Co-authored-by: karooolis <[email protected]>
  • Loading branch information
holic and karooolis authored Sep 13, 2024
1 parent 2f935cf commit e6147b2
Show file tree
Hide file tree
Showing 17 changed files with 268 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-bats-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

World Explorer now supports connecting external wallets.
14 changes: 7 additions & 7 deletions packages/explorer/src/app/(explorer)/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { ReactNode } from "react";
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
import "@rainbow-me/rainbowkit/styles.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { anvil } from "@wagmi/core/chains";
import { AppStoreProvider } from "../../store";
import { getChain } from "../../common";
import { defaultAnvilConnectors } from "../../connectors/anvil";

const queryClient = new QueryClient();

const chain = getChain();
export const wagmiConfig = createConfig({
chains: [anvil],
chains: [chain],
connectors: [
injected(),
metaMask({
Expand All @@ -21,9 +22,10 @@ export const wagmiConfig = createConfig({
},
}),
safe(),
...defaultAnvilConnectors,
],
transports: {
[anvil.id]: http(),
[chain.id]: http(),
},
ssr: true,
});
Expand All @@ -32,9 +34,7 @@ export function Providers({ children }: { children: ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider theme={darkTheme()}>
<AppStoreProvider>{children}</AppStoreProvider>
</RainbowKitProvider>
<RainbowKitProvider theme={darkTheme()}>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import { Loader } from "lucide-react";
import { useParams } from "next/navigation";
import { toast } from "sonner";
import { Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { useChainId } from "wagmi";
import { useAccount } from "wagmi";
import { ChangeEvent, useState } from "react";
import { encodeField, getFieldIndex } from "@latticexyz/protocol-parser/internal";
import { SchemaAbiType } from "@latticexyz/schema-type/internal";
import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { waitForTransactionReceipt, writeContract } from "@wagmi/core";
import { getChain } from "../../../../../common";
import { Checkbox } from "../../../../../components/ui/Checkbox";
import { ACCOUNT_PRIVATE_KEYS } from "../../../../../consts";
import { camelCase, cn } from "../../../../../lib/utils";
import { useAppStore } from "../../../../../store";
import { TableConfig } from "../../../../api/table/route";
import { wagmiConfig } from "../../../Providers";

Expand All @@ -24,11 +22,13 @@ type Props = {
config: TableConfig;
};

const chain = getChain();
const chainId = chain.id;

export function EditableTableCell({ name, config, keyTuple, value: defaultValue }: Props) {
const queryClient = useQueryClient();
const chainId = useChainId();
const { account } = useAppStore();
const { worldAddress } = useParams();
const account = useAccount();

const [value, setValue] = useState<unknown>(defaultValue);

Expand All @@ -40,11 +40,11 @@ export function EditableTableCell({ name, config, keyTuple, value: defaultValue
const fieldIndex = getFieldIndex(config?.value_schema, camelCase(name));
const encodedField = encodeField(fieldType, newValue);
const txHash = await writeContract(wagmiConfig, {
account: privateKeyToAccount(ACCOUNT_PRIVATE_KEYS[account]),
abi: IBaseWorldAbi,
address: worldAddress as Hex,
functionName: "setField",
args: [tableId, keyTuple, fieldIndex, encodedField],
chainId,
});

const receipt = await waitForTransactionReceipt(wagmiConfig, {
Expand All @@ -67,7 +67,7 @@ export function EditableTableCell({ name, config, keyTuple, value: defaultValue
queryKey: [
"balance",
{
address: account,
address: account.address,
chainId,
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useParams } from "next/navigation";
import { toast } from "sonner";
import { Abi, AbiFunction, Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { useChainId } from "wagmi";
import { useAccount } from "wagmi";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { readContract, waitForTransactionReceipt, writeContract } from "@wagmi/core";
import { ACCOUNT_PRIVATE_KEYS } from "../../../../../consts";
import { useAppStore } from "../../../../../store";
import { getChain } from "../../../../../common";
import { wagmiConfig } from "../../../Providers";
import { FunctionType } from "./FunctionField";

Expand All @@ -15,11 +13,13 @@ type UseContractMutationProps = {
operationType: FunctionType;
};

const chain = getChain();
const chainId = chain.id;

export function useContractMutation({ abi, operationType }: UseContractMutationProps) {
const queryClient = useQueryClient();
const chainId = useChainId();
const { account } = useAppStore();
const { worldAddress } = useParams();
const account = useAccount();

return useMutation({
mutationFn: async ({ inputs, value }: { inputs: unknown[]; value?: string }) => {
Expand All @@ -29,17 +29,18 @@ export function useContractMutation({ abi, operationType }: UseContractMutationP
address: worldAddress as Hex,
functionName: abi.name,
args: inputs,
chainId,
});

return { result };
} else {
const txHash = await writeContract(wagmiConfig, {
account: privateKeyToAccount(ACCOUNT_PRIVATE_KEYS[account]),
abi: [abi] as Abi,
address: worldAddress as Hex,
functionName: abi.name,
args: inputs,
...(value && { value: BigInt(value) }),
chainId,
});

const receipt = await waitForTransactionReceipt(wagmiConfig, {
Expand Down
7 changes: 3 additions & 4 deletions packages/explorer/src/app/api/world/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { AbiFunction, Address, Hex, createWalletClient, http, parseAbi } from "viem";
import { getBlockNumber, getLogs } from "viem/actions";
import { getRpcUrl } from "@latticexyz/common/foundry";
import { helloStoreEvent } from "@latticexyz/store";
import { helloWorldEvent } from "@latticexyz/world";
import { getWorldAbi } from "@latticexyz/world/internal";
import { getChain } from "../../../common";

export const dynamic = "force-dynamic";

async function getClient() {
const profile = process.env.FOUNDRY_PROFILE;
const rpc = await getRpcUrl(profile);
const client = createWalletClient({
transport: http(rpc),
chain: getChain(),
transport: http(),
});

return client;
Expand Down
8 changes: 8 additions & 0 deletions packages/explorer/src/bin/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import process from "process";
import { fileURLToPath } from "url";
import yargs from "yargs";
import { ChildProcess, spawn } from "child_process";
import { chains } from "../common";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down Expand Up @@ -55,6 +56,12 @@ const argv = yargs(process.argv.slice(2))
default: process.env.WORLD_ADDRESS,
},
})
.check((argv) => {
if (!chains[Number(argv.chainId)]) {
throw new Error(`Invalid chain ID. Supported chains are: ${Object.keys(chains).join(", ")}.`);
}
return true;
})
.parseSync();

const { port, hostname, chainId, indexerDatabase, worldsFile, dev } = argv;
Expand All @@ -64,6 +71,7 @@ let explorerProcess: ChildProcess;
async function startExplorer() {
const env = {
...process.env,
NEXT_PUBLIC_CHAIN_ID: chainId.toString(),
WORLD_ADDRESS: worldAddress?.toString(),
INDEXER_DATABASE: path.join(process.cwd(), indexerDatabase),
};
Expand Down
22 changes: 22 additions & 0 deletions packages/explorer/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Chain, anvil, garnet, redstone } from "viem/chains";

export const chains: Partial<Record<number, Chain>> = {
[anvil.id]: anvil,
[redstone.id]: redstone,
[garnet.id]: garnet,
};

export function getChain() {
const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || anvil.id);
const chain = chains[chainId];
if (!chain) {
throw new Error(`Chain ID ${chainId} not supported. Supported chains are: ${Object.keys(chains).join(", ")}.`);
}

return chain;
}

export function isAnvil() {
const chain = getChain();
return chain.id === anvil.id;
}
81 changes: 61 additions & 20 deletions packages/explorer/src/components/AccountSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,82 @@
import { Address } from "viem";
import { useBalance } from "wagmi";
import { ACCOUNTS } from "../consts";
import { CirclePlusIcon, PlugIcon } from "lucide-react";
import { useAccount, useBalance, useConnect, useConnectors } from "wagmi";
import { useState } from "react";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { AnvilConnector, isAnvilConnector } from "../connectors/anvil";
import { formatBalance } from "../lib/utils";
import { useAppStore } from "../store";
import { Button } from "./ui/Button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/Select";
import { TruncatedHex } from "./ui/TruncatedHex";

function AccountSelectItem({ address, name }: { address: Address; name: string }) {
const balance = useBalance({
function AccountSelectItem({ connector }: { connector: AnvilConnector }) {
const address = connector.accounts[0].address;
const { data: balance } = useBalance({
address,
query: {
refetchInterval: 15000,
select: (data) => {
return data?.value;
},
enabled: !!address,
},
});
const balanceValue = balance.data?.value;

return (
<SelectItem key={address} value={address} className="font-mono">
{name}
{balanceValue !== undefined && ` (${formatBalance(balanceValue)} ETH)`}{" "}
<span className="opacity-70">
(<TruncatedHex hex={address} />)
</span>
<SelectItem key={address} value={connector.id} className="font-mono">
{connector.name}
{balance !== undefined && ` (${formatBalance(balance)} ETH)`}{" "}
{address && (
<span className="opacity-70">
(<TruncatedHex hex={address} />)
</span>
)}
</SelectItem>
);
}

export function AccountSelect() {
const { account, setAccount } = useAppStore();
const [open, setOpen] = useState(false);
const { connector } = useAccount();
const { connect } = useConnect();
const { openConnectModal } = useConnectModal();
const configuredConnectors = useConnectors();
const connectors = [...configuredConnectors.filter(isAnvilConnector)];

return (
<Select value={account} onValueChange={setAccount}>
<SelectTrigger className="w-[300px] text-left">
<SelectValue placeholder="Account" />
</SelectTrigger>
<Select
open={open}
onOpenChange={setOpen}
value={connector?.id}
onValueChange={(connectorId: string) => {
const connector = connectors.find((connector) => connector.id === connectorId);
if (connector) {
connect({ connector });
}
}}
>
<Button size="sm" asChild>
<SelectTrigger>
<PlugIcon className="mr-2 inline-block h-4 w-4" />
<SelectValue placeholder="Connect" />
</SelectTrigger>
</Button>

<SelectContent>
{ACCOUNTS.map((address, index) => {
return <AccountSelectItem key={address} address={address} name={`Account ${index + 1}`} />;
{connectors.map((connector) => {
return <AccountSelectItem key={connector.id} connector={connector} />;
})}

<Button
size="sm"
className="mt-2 w-full font-mono"
onClick={() => {
setOpen(false);
openConnectModal?.();
}}
>
<CirclePlusIcon className="mr-2 inline-block h-4 w-4" />
Add wallet
</Button>
</SelectContent>
</Select>
);
Expand Down
59 changes: 59 additions & 0 deletions packages/explorer/src/components/ConnectButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { PlugIcon, ZapIcon } from "lucide-react";
import { ConnectButton as RainbowConnectButton } from "@rainbow-me/rainbowkit";
import { isAnvil } from "../common";
import { cn } from "../lib/utils";
import { AccountSelect } from "./AccountSelect";
import { Button } from "./ui/Button";

export function ConnectButton() {
return (
<RainbowConnectButton.Custom>
{({ account, chain, openAccountModal, openChainModal, openConnectModal, mounted }) => {
const connected = mounted && account && chain;

return (
<div
className={cn({
"pointer-events-none select-none opacity-0": !mounted,
})}
>
{(() => {
if (!connected) {
if (isAnvil()) {
return <AccountSelect />;
}

return (
<Button type="button" size="sm" onClick={openConnectModal}>
<PlugIcon className="mr-2 inline-block h-4 w-4" /> Connect
</Button>
);
}

if (chain.unsupported) {
return (
<Button type="button" size="sm" onClick={openChainModal}>
<ZapIcon className="mr-2 inline-block h-4 w-4" />
Wrong network
</Button>
);
}

return (
<div className="flex-wrap gap-2">
<Button type="button" size="sm" onClick={openAccountModal} variant="secondary">
<PlugIcon className="mr-2 inline-block h-4 w-4" />
{account.displayName}
<span className="ml-2 font-normal opacity-70">
{account.displayBalance ? ` (${account.displayBalance})` : ""}
</span>
</Button>
</div>
);
})()}
</div>
);
}}
</RainbowConnectButton.Custom>
);
}
Loading

0 comments on commit e6147b2

Please sign in to comment.