Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Garage: add multi chain support #879

Merged
merged 8 commits into from
Jul 5, 2023
4 changes: 2 additions & 2 deletions garage/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import { RigAttributeStatsContextProvider } from "./components/RigAttributeStats
import { NFTsContextProvider } from "./components/NFTsContext";
import { ActingAsAddressContextProvider } from "./components/ActingAsAddressContext";
import { routes } from "./routes";
import { chain } from "./env";
import { mainChain, secondaryChain } from "./env";

const { chains, publicClient } = configureChains(
[chain],
[mainChain, secondaryChain],
[
alchemyProvider({ apiKey: import.meta.env.VITE_ALCHEMY_ID }),
publicProvider(),
Expand Down
13 changes: 9 additions & 4 deletions garage/src/components/ChainAwareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React from "react";
import { Button } from "@chakra-ui/react";
import { useNetwork } from "wagmi";
import { Chain, useNetwork } from "wagmi";
import { useChainModal } from "@rainbow-me/rainbowkit";
import { chain as expectedChain } from "../env";
import { mainChain } from "../env";

export const ChainAwareButton = (
props: React.ComponentProps<typeof Button>
props: { expectedChain?: Chain } & React.ComponentProps<typeof Button>
) => {
const { chain } = useNetwork();
const { children: _children, onClick: _onClick, ...rest } = props;
const {
expectedChain = mainChain,
children: _children,
onClick: _onClick,
...rest
} = props;
const { openChainModal } = useChainModal();

let children = _children;
Expand Down
16 changes: 11 additions & 5 deletions garage/src/components/FlyParkModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ import {
import debounce from "lodash/debounce";
import { useActivePilotSessions } from "../hooks/useActivePilotSessions";
import { Rig, WalletAddress } from "../types";
import { ChainAwareButton } from "./ChainAwareButton";
import { TransactionStateAlert } from "./TransactionStateAlert";
import { RigDisplay } from "./RigDisplay";
import { deployment } from "../env";
import { mainChain, deployment } from "../env";
import { abi } from "../abis/TablelandRigs";
import { copySet, toggleInSet } from "../utils/set";
import { pluralize } from "../utils/fmt";
Expand All @@ -77,6 +78,7 @@ export const TrainRigsModal = ({
onTransactionSubmitted,
}: ModalProps) => {
const { config } = usePrepareContractWrite({
chainId: mainChain.id,
address: as0xString(contractAddress),
abi,
functionName: "trainRig",
Expand Down Expand Up @@ -125,13 +127,14 @@ export const TrainRigsModal = ({
<TransactionStateAlert {...contractWrite} />
</ModalBody>
<ModalFooter>
<Button
<ChainAwareButton
expectedChain={mainChain}
mr={3}
onClick={() => (write ? write() : undefined)}
isDisabled={isLoading || isSuccess}
>
Train {pluralize("rig", rigs)}
</Button>
</ChainAwareButton>
<Button
variant="ghost"
onClick={onClose}
Expand All @@ -152,6 +155,7 @@ export const ParkRigsModal = ({
onTransactionSubmitted,
}: ModalProps) => {
const { config } = usePrepareContractWrite({
chainId: mainChain.id,
address: as0xString(contractAddress),
abi,
functionName: "parkRig",
Expand Down Expand Up @@ -256,6 +260,7 @@ const PilotTransactionStep = ({
}: PilotTransactionProps) => {
// TODO support calling pilotRig(uint256, address, uint256) for a single rig?
const { config } = usePrepareContractWrite({
chainId: mainChain.id,
address: as0xString(contractAddress),
abi,
functionName: "pilotRig",
Expand Down Expand Up @@ -332,13 +337,14 @@ const PilotTransactionStep = ({
<TransactionStateAlert {...contractWrite} />
</ModalBody>
<ModalFooter>
<Button
<ChainAwareButton
expectedChain={mainChain}
mr={3}
onClick={() => (write ? write() : undefined)}
isDisabled={isLoading || isSuccess || !sessions}
>
Pilot {pluralize("rig", pairs)}
</Button>
</ChainAwareButton>
<Button
variant="ghost"
onClick={onClose}
Expand Down
9 changes: 6 additions & 3 deletions garage/src/components/TransferRigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import { Rig } from "../types";
import { isValidAddress, as0xString } from "../utils/types";
import { TransactionStateAlert } from "./TransactionStateAlert";
import { RigDisplay } from "./RigDisplay";
import { deployment } from "../env";
import { mainChain, deployment } from "../env";
import { abi } from "../abis/TablelandRigs";
import { ChainAwareButton } from "./ChainAwareButton";

const { contractAddress } = deployment;

Expand All @@ -53,6 +54,7 @@ export const TransferRigModal = ({
}, [toAddress]);

const { config } = usePrepareContractWrite({
chainId: mainChain.id,
address: as0xString(contractAddress),
abi,
functionName: rig.currentPilot
Expand Down Expand Up @@ -135,13 +137,14 @@ export const TransferRigModal = ({
<TransactionStateAlert {...contractWrite} />
</ModalBody>
<ModalFooter>
<Button
<ChainAwareButton
expectedChain={mainChain}
mr={3}
onClick={() => (write ? write() : undefined)}
isDisabled={!isValidToAddress || isLoading || isSuccess}
>
Transfer Rig
</Button>
</ChainAwareButton>
<Button
variant="ghost"
onClick={onClose}
Expand Down
20 changes: 15 additions & 5 deletions garage/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,31 @@ const parseEnv = (env?: string): DeploymentEnvironment => {

const environment = parseEnv(import.meta.env.VITE_APP_ENV);

const chainEnvMapping = {
const mainChainEnvMapping = {
[DeploymentEnvironment.DEVELOPMENT]: chains.polygonMumbai,
[DeploymentEnvironment.STAGING]: chains.polygonMumbai,
[DeploymentEnvironment.PRODUCTION]: chains.mainnet,
};

export const chain = chainEnvMapping[environment];
const secondaryChainEnvMapping = {
[DeploymentEnvironment.DEVELOPMENT]: chains.polygonMumbai,
[DeploymentEnvironment.STAGING]: chains.polygonMumbai,
[DeploymentEnvironment.PRODUCTION]: chains.filecoin,
};

// Main chain used by the rigs contract
export const mainChain = mainChainEnvMapping[environment];

// Chain used by secondary tables, like ft rewards and the voting mechanism
export const secondaryChain = secondaryChainEnvMapping[environment];

const blockExplorerChainMapping = {
[DeploymentEnvironment.DEVELOPMENT]:
chainEnvMapping[DeploymentEnvironment.STAGING].blockExplorers.etherscan.url,
mainChainEnvMapping[DeploymentEnvironment.STAGING].blockExplorers.etherscan.url,
[DeploymentEnvironment.STAGING]:
chainEnvMapping[DeploymentEnvironment.STAGING].blockExplorers.etherscan.url,
mainChainEnvMapping[DeploymentEnvironment.STAGING].blockExplorers.etherscan.url,
[DeploymentEnvironment.PRODUCTION]:
chainEnvMapping[DeploymentEnvironment.PRODUCTION].blockExplorers.etherscan
mainChainEnvMapping[DeploymentEnvironment.PRODUCTION].blockExplorers.etherscan
.url,
};

Expand Down
25 changes: 23 additions & 2 deletions garage/src/hooks/useAccount.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { useEffect, useMemo, useState } from "react";
import { DelegateCash } from "delegatecash";
import { providers } from "ethers";
import { useAccount as useWagmiAccount } from "wagmi";
import { deployment } from "../env";
import { mainChain, deployment } from "../env";
import { isPresent } from "../utils/types";
import { useActingAsAddress } from "../components/ActingAsAddressContext";

const dc = new DelegateCash();
const { id, network } = mainChain;

// NOTE(daniel):
// this is a hack to work around the fact that we always want to use the
// mainChain-chain to look up delegated wallets.
//
// delegate cash uses the default provider (window.ethereum) if none is provided
// and the default provider will switch network automatically when the
// connected wallet switches network. since we sometimes need the wallet
// to be connected to `mainChain` and sometimes to `secondaryChain`
// we expect the rest of the app to work regardless of which chain
// the user is connected to
//
// we also need to overwrite the `getSigner` method since the dc sdk
// always calls getSigner regardless of if it is supported or not
const provider = new providers.AlchemyProvider(
{ chainId: id, name: network },
import.meta.env.VITE_ALCHEMY_ID
);
provider.getSigner = () => null as any;
const dc = new DelegateCash(provider);

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

Expand Down
6 changes: 3 additions & 3 deletions garage/src/hooks/useNFTs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
NftFilters,
GetNftsForOwnerOptions,
} from "alchemy-sdk";
import { chain } from "../env";
import { mainChain } from "../env";
import { useQuery } from "@tanstack/react-query";

const wagmiChainToNetwork = (c: Chain): Network => {
Expand All @@ -24,13 +24,13 @@ const wagmiChainToNetwork = (c: Chain): Network => {
case chains.polygonMumbai:
return Network.MATIC_MUMBAI;
default:
throw new Error(`wagmiChainToNetwork unsupported chain, ${c}`);
throw new Error(`wagmiChainToNetwork unsupported mainChain, ${c}`);
}
};

const settings = {
apiKey: import.meta.env.VITE_ALCHEMY_ID,
network: wagmiChainToNetwork(chain),
network: wagmiChainToNetwork(mainChain),
};

export const alchemy = new Alchemy(settings);
Expand Down
3 changes: 2 additions & 1 deletion garage/src/hooks/useOwnedRigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useContractRead } from "wagmi";
import { useTablelandConnection } from "./useTablelandConnection";
import { selectRigs } from "../utils/queries";
import { isValidAddress, as0xString } from "../utils/types";
import { deployment } from "../env";
import { mainChain, deployment } from "../env";
import { abi } from "../abis/TablelandRigs";

const { contractAddress } = deployment;
Expand All @@ -13,6 +13,7 @@ export const useOwnedRigs = (address?: string) => {
const { db } = useTablelandConnection();

const { data } = useContractRead({
chainId: mainChain.id,
address: as0xString(contractAddress),
abi,
functionName: "tokensOfOwner",
Expand Down
2 changes: 1 addition & 1 deletion garage/src/hooks/useTablelandConnection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Database, Validator, helpers } from "@tableland/sdk";
import { chain } from "../env";
import { mainChain as chain } from "../env";

const db = new Database({ baseUrl: helpers.getBaseUrl(chain.id) });
const validator = new Validator(db.config);
Expand Down
49 changes: 27 additions & 22 deletions garage/src/pages/Admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import { Database } from "@tableland/sdk";
import { useSigner } from "../../hooks/useSigner";
import { TOPBAR_HEIGHT } from "../../Topbar";
import { Footer } from "../../components/Footer";
import { ChainAwareButton } from "../../components/ChainAwareButton";
import { isValidAddress } from "../../utils/types";
import { deployment } from "../../env";
import { secondaryChain, deployment } from "../../env";

const { ftRewardsTable } = deployment;

Expand All @@ -34,7 +35,7 @@ const MODULE_PROPS = {
const GiveFtRewardForm = (props: React.ComponentProps<typeof Box>) => {
const toast = useToast();

const signer = useSigner();
const signer = useSigner({ chainId: secondaryChain.id });

const db = useMemo(() => {
if (signer) return new Database({ signer });
Expand Down Expand Up @@ -83,28 +84,31 @@ const GiveFtRewardForm = (props: React.ComponentProps<typeof Box>) => {

setIsQuerying(true);

const { meta: insert } = await db
.prepare(
`INSERT INTO ${ftRewardsTable} (block_num, recipient, reason, amount) VALUES (BLOCK_NUM(), ?1, ?2, ?3)`
)
.bind(form.recipient, form.reason, form.amount)
.run();
try {
const { meta: insert } = await db
.prepare(
`INSERT INTO ${ftRewardsTable} (block_num, recipient, reason, amount) VALUES (BLOCK_NUM(), ?1, ?2, ?3)`
)
.bind(form.recipient, form.reason, form.amount)
.run();

insert.txn
?.wait()
.then((_) => {
insert.txn?.wait().then((_) => {
setIsQuerying(false);
toast({ title: "Success", status: "success", duration: 7_500 });
})
.catch((e) => {
setIsQuerying(false);
toast({
title: "Reward failed",
description: e.toString(),
status: "error",
duration: 7_500,
});
});
} catch (e) {
if (e instanceof Error) {
if (!/user rejected transaction/.test(e.message)) {
toast({
title: "Reward failed",
description: e.message,
status: "error",
duration: 7_500,
});
}
}
setIsQuerying(false);
}
}, [db, form]);

return (
Expand Down Expand Up @@ -139,13 +143,14 @@ const GiveFtRewardForm = (props: React.ComponentProps<typeof Box>) => {
</NumberInput>
</FormControl>
<Flex justify="flex-end" width="100%" mt={4}>
<Button
<ChainAwareButton
expectedChain={secondaryChain}
isDisabled={isQuerying || !isFormValid}
onClick={onSubmit}
isLoading={isQuerying}
>
Submit
</Button>
</ChainAwareButton>
</Flex>
</Box>
);
Expand Down
4 changes: 2 additions & 2 deletions garage/src/pages/Dashboard/modules/RigsInventory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { findNFT } from "../../../utils/nfts";
import { sleep } from "../../../utils/async";
import { prettyNumber } from "../../../utils/fmt";
import { firstSetValue, copySet, toggleInSet } from "../../../utils/set";
import { chain } from "../../../env";
import { mainChain } from "../../../env";

interface RigListItemProps {
rig: RigWithPilots;
Expand Down Expand Up @@ -201,7 +201,7 @@ export const RigsInventory = (props: React.ComponentProps<typeof Box>) => {
validator
.pollForReceiptByTransactionHash(
{
chainId: chain.id,
chainId: mainChain.id,
transactionHash: pendingTx,
},
{ interval: 2000, signal }
Expand Down
3 changes: 2 additions & 1 deletion garage/src/pages/PilotDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { useNFTs, NFT } from "../../hooks/useNFTs";
import { useRigImageUrls } from "../../hooks/useRigImageUrls";
import { TOPBAR_HEIGHT } from "../../Topbar";
import { prettyNumber, truncateWalletAddress } from "../../utils/fmt";
import { openseaBaseUrl } from "../../env";
import { mainChain, openseaBaseUrl } from "../../env";
import { PilotSessionWithRigId } from "../../types";
import { ReactComponent as OpenseaMark } from "../../assets/opensea-mark.svg";
import { selectPilotSessionsForPilot } from "../../utils/queries";
Expand Down Expand Up @@ -202,6 +202,7 @@ export const PilotDetails = () => {
const pilot = nfts?.length ? nfts[0] : null;

const { data: owner } = useContractRead({
chainId: mainChain.id,
address: as0xString(collection),
abi,
functionName: "ownerOf",
Expand Down
Loading
Loading