Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

.idea/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useContract/useContract.ts
Original file line number Diff line number Diff line change
@@ -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<ethers.Contract | undefined>(
undefined
);

const metamaskState = useRecoilValue(metamaskState)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRecoilValue is a hook if you only need to READ state, whereas useRecoilState is a simile to useState, giving you [state, setState] equiv


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
};
28 changes: 28 additions & 0 deletions src/hooks/useMetamask/types.ts
Original file line number Diff line number Diff line change
@@ -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<MetamaskState>; // 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<MetamaskState | MetamaskIsConnected>({
key: 'metamaskState',
dangerouslyAllowMutability: true,
default: {
provider: undefined,
signer: undefined,
account: undefined,
},
});
40 changes: 40 additions & 0 deletions src/hooks/useMetamask/useMetamask.ts
Original file line number Diff line number Diff line change
@@ -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
};
};



5 changes: 5 additions & 0 deletions src/utilities/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


export type AllMandatory<T> = {
[Property in keyof T]-?: T[Property]
}
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down