diff --git a/.gitignore b/.gitignore index 4d29575..eecc05b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.idea/ diff --git a/package.json b/package.json index 4452d8e..9a8ba9b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3", + "recoil": "^0.5.2", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, diff --git a/src/hooks/useContract/useContract.ts b/src/hooks/useContract/useContract.ts new file mode 100644 index 0000000..3beb5a3 --- /dev/null +++ b/src/hooks/useContract/useContract.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; +import {ContractInterface, ethers} from "ethers"; +import {useRecoilValue} from "recoil"; +import {isMetamaskConnectedTypeGuard, metamaskState} from "../useMetamask/types"; + +/** + * A hook to engage with any contract (can be used outright, or as the base to compose other contract-specific hooks) + * - returned value will be undefined until the user connects their wallet via metamask + * @param address the blockchain address of the contract + * @param abi the contract abi (can be partial) + * + */ +export const useContract = (address: string, abi: ContractInterface) => { + const [contract, setContract] = useState( + undefined + ); + + const metamaskState = useRecoilValue(metamaskState) + + useEffect(() => { + if (isMetamaskConnectedTypeGuard(metamaskState)) { + const {provider, signer} = metamaskState; + const c = new ethers.Contract(address, abi, provider); + const contractWithSigner = c.connect(signer!); + setContract(contractWithSigner); + } + }, [metamaskState]); + + return contract +}; diff --git a/src/hooks/useMetamask/types.ts b/src/hooks/useMetamask/types.ts new file mode 100644 index 0000000..3e7f095 --- /dev/null +++ b/src/hooks/useMetamask/types.ts @@ -0,0 +1,28 @@ +import {atom} from 'recoil'; +import {ethers} from 'ethers'; +import {AllMandatory} from "../../utilities/types"; + +export type MetamaskState = { + provider?: ethers.providers.Web3Provider; + signer?: ethers.providers.JsonRpcSigner; + account?: string; +}; + +type MetamaskIsConnected = AllMandatory; // the state when user is connected + +/** + * A Typeguard to allow using state properties without optional-chaining or multiple if/else + * @param state the whole state object from metamaskState + */ +export const isMetamaskConnectedTypeGuard = (state: MetamaskState | MetamaskIsConnected): state is MetamaskIsConnected => + !!state.provider && !!state.signer && !!state.account + +export const metamaskState = atom({ + key: 'metamaskState', + dangerouslyAllowMutability: true, + default: { + provider: undefined, + signer: undefined, + account: undefined, + }, +}); diff --git a/src/hooks/useMetamask/useMetamask.ts b/src/hooks/useMetamask/useMetamask.ts new file mode 100644 index 0000000..5f51231 --- /dev/null +++ b/src/hooks/useMetamask/useMetamask.ts @@ -0,0 +1,40 @@ +import { useRecoilState} from 'recoil'; +import {ethers, providers} from 'ethers'; +import {isMetamaskConnectedTypeGuard, metamaskState} from "./types"; + +// ethereum isn't on TS standard Window type: +declare const window: Window & { + ethereum: providers.ExternalProvider; +} + +/** + * A hook to provide Metamask's 'connect' function anywhere, and give global access to the + * returned values (user's account, provider, signer). + * - As this uses recoil state, consumers only concerned about the raw state values (and not connect) can simply use + * useRecoilValue(metamaskState) instead of this hook + */ +export const useMetamask = () => { + const [state, setState] = useRecoilState(metamaskState); + + const connect = async () => { + const provider = new ethers.providers.Web3Provider(window.ethereum, 'any'); + await provider.send('eth_requestAccounts', []); + + provider.on('network', (_, oldNetwork) => { + oldNetwork && window.location.reload(); // might want to change this to render UI + }); + + const signer = provider.getSigner(); + const account = await signer.getAddress(); + setState(() => ({provider, signer, account})); + }; + + return { + ...state, + connect, + isConnected: isMetamaskConnectedTypeGuard(state) // CAUTION: isConnected is not a type-guard + }; +}; + + + diff --git a/src/utilities/types.ts b/src/utilities/types.ts new file mode 100644 index 0000000..ab262c9 --- /dev/null +++ b/src/utilities/types.ts @@ -0,0 +1,5 @@ + + +export type AllMandatory = { + [Property in keyof T]-?: T[Property] +} diff --git a/yarn.lock b/yarn.lock index bfaadad..9d75d0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5760,6 +5760,11 @@ gzip-size@5.1.1: duplexer "^0.1.1" pify "^4.0.1" +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE= + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -9621,6 +9626,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +recoil@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.5.2.tgz#61fecce3b4dc91127a595586cf7369e2811ba024" + integrity sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw== + dependencies: + hamt_plus "1.0.2" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"