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

feat: update to read from new document with chainId field #520

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
30bf627
feat: add multi-chain support
superical Dec 8, 2021
658072a
chore: temporary oa-verify for trial run and testing; to be removed
superical Dec 22, 2021
0cd713c
fix: test env for headless test
superical Dec 22, 2021
e5cdbc7
refactor: make mainnet the default network when in production
superical Dec 23, 2021
a6db94b
docs: add comment for getSupportedChainInfo
superical Dec 23, 2021
6602d44
refactor: clean up index import in network-config.ts
superical Dec 23, 2021
b02d11b
refactor: clean up build commands for deployment
superical Dec 23, 2021
84c4306
refactor: clean up chain config files
superical Dec 23, 2021
e967a11
refactor: multiple ternaries in verify/index.tsx
superical Dec 30, 2021
a5bd4e2
Merge remote-tracking branch 'origin/master' into feat/multi-chain
superical Dec 30, 2021
c1e9f2d
chore: resolve conflicts with changes from master
superical Dec 30, 2021
7c950bb
chore: fix ts conflict with changes from master in config file
superical Dec 30, 2021
693fd24
Merge remote-tracking branch 'origin/master' into feat/multi-chain
superical Dec 31, 2021
3a8ffea
refactor: review feedback to change iife to normal function
superical Dec 31, 2021
7ed8ac9
Merge remote-tracking branch 'origin/master' into feat/multi-chain
superical Dec 31, 2021
4a155bc
fix: merge conflict issues from new changes in master
superical Jan 3, 2022
0ef71b3
Merge remote-tracking branch 'origin/master' into feat/multi-chain
superical Jan 5, 2022
508820b
chore: update oa-verify to v7.8.0
superical Jan 5, 2022
e9d0b4e
Merge remote-tracking branch 'origin/master' into feat/multi-chain
superical Jan 7, 2022
5230e72
feat: update to read from new document with chainId field
isaackps Jan 11, 2022
3969caf
feat: revert TT document and use OA document
isaackps Jan 26, 2022
5b34a56
fix: revert changes
isaackps Jan 26, 2022
632ae09
fix: revert chagnes
isaackps Jan 26, 2022
b5f3060
feat: update getChainId fn and oa package
isaackps Feb 9, 2022
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: 1 addition & 1 deletion .github/workflows/test_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Test
run: npm run test
- name: Build
run: npm run build
run: npm run build:test
- name: Integration - testcafe
run: npm run integration:headless
- name: Integration - dappeteer
Expand Down
370 changes: 249 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
"blockchain": "ganache-cli --mnemonic \"indicate swing place chair flight used hammer soon photo region volume shuffle\"",
"blockchain:contracts": "node --experimental-modules ./integration/local/setup-contracts.mjs",
"build": "run-s check-types build:css build:app",
"build:css": "cross-env NODE_ENV=production postcss src/tailwind.css -o src/index.css",
"build:app": "cross-env NODE_ENV=production webpack --progress --mode production",
"build:dev": "cross-env NODE_ENV=development npm run build",
"build:test": "cross-env NODE_ENV=test npm run build",
"build:prod": "cross-env NODE_ENV=production NET=mainnet npm run build",
"build:css": "postcss src/tailwind.css -o src/index.css",
"build:app": "webpack --progress --mode production",
"check-types": "tsc --sourceMap false --noEmit",
"dev": "run-p dev:*",
"dev:css": "postcss src/tailwind.css -o src/index.css --watch",
Expand Down Expand Up @@ -43,8 +46,8 @@
"@govtechsg/decentralized-renderer-react-components": "^3.6.2",
"@govtechsg/ethers-contract-hook": "^2.2.0",
"@govtechsg/oa-encryption": "^1.3.3",
"@govtechsg/oa-verify": "^7.7.0",
"@govtechsg/open-attestation": "^6.2.0",
"@govtechsg/oa-verify": "^7.8.0",
"@govtechsg/open-attestation": "^6.3.0",
"@govtechsg/open-attestation-cli": "^1.40.4",
"@govtechsg/token-registry": "2.5.1",
"@govtechsg/tradetrust-ui-components": "^2.12.1",
Expand Down
58 changes: 58 additions & 0 deletions public/static/demo/goerli.tt

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions public/static/demo/maticmum.tt

Large diffs are not rendered by default.

Binary file added public/static/images/networks/ethereum.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/static/images/networks/polygon.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions src/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { Footer } from "./components/Layout/Footer";
import { NavigationBar, leftNavItems, rightNavItems } from "./components/Layout/NavigationBar";
import { NETWORK } from "./config";
import { Routes, routes } from "./routes";
import styled from "@emotion/styled";
import { useProviderContext } from "./common/contexts/provider";
import { getChainInfo } from "./common/utils/chain-utils";

const Main = styled.main`
background-image: url("/static/images/common/wave-lines.png");
Expand All @@ -15,16 +16,19 @@ const Main = styled.main`
const AppContainer = (): React.ReactElement => {
const location = useLocation();
const [toggleNavBar, setToggleNavBar] = useState(false);
const { currentChainId } = useProviderContext();

useEffect(() => {
setToggleNavBar(false);
window.scrollTo(0, 0);
}, [location]);

const networkName = currentChainId ? getChainInfo(currentChainId).label : "Unsupported";

return (
<div className="flex flex-col min-h-full" data-location={location.pathname}>
<NetworkBar network={NETWORK}>
You are currently on <span className="capitalize">{NETWORK}</span> network.
<NetworkBar network={networkName}>
You are currently on <span className="capitalize">{networkName}</span> network.
</NetworkBar>
<NavigationBar
toggleNavBar={toggleNavBar}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export const TokenInformationContextProvider: FunctionComponent<TokenInformation
}) => {
const [tokenId, setTokenId] = useState<string>();
const [tokenRegistryAddress, setTokenRegistryAddress] = useState<string>();
const { provider } = useProviderContext();
const { tokenRegistry } = useTokenRegistryContract(tokenRegistryAddress, provider);
const { titleEscrow, updateTitleEscrow, documentOwner } = useTitleEscrowContract(provider, tokenRegistry, tokenId);
const { getTransactor } = useProviderContext();
const transactor = getTransactor();
const { tokenRegistry } = useTokenRegistryContract(tokenRegistryAddress, transactor);
const { titleEscrow, updateTitleEscrow, documentOwner } = useTitleEscrowContract(transactor, tokenRegistry, tokenId);
const isSurrendered = documentOwner === tokenRegistryAddress;
const isTokenBurnt = documentOwner === "0x000000000000000000000000000000000000dEaD"; // check if the token belongs to burn address.

Expand All @@ -98,7 +99,7 @@ export const TokenInformationContextProvider: FunctionComponent<TokenInformation
reset: resetDestroyingTokenState,
} = useContractFunctionHook(tokenRegistry, "destroyToken");

const { restoreToken, state: restoreTokenState } = useRestoreToken(provider, tokenRegistry, tokenId);
const { restoreToken, state: restoreTokenState } = useRestoreToken(transactor, tokenRegistry, tokenId);

// Contract Write Functions (available only after provider has been upgraded)
const {
Expand Down Expand Up @@ -194,7 +195,7 @@ export const TokenInformationContextProvider: FunctionComponent<TokenInformation
}, [transferToNewEscrowState, updateTitleEscrow]);

// Reset states for all write functions when provider changes to allow methods to be called again without refreshing
useEffect(resetProviders, [resetProviders, provider]);
useEffect(resetProviders, [resetProviders, transactor]);

return (
<TokenInformationContext.Provider
Expand Down
234 changes: 199 additions & 35 deletions src/common/contexts/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
import { MessageTitle } from "@govtechsg/tradetrust-ui-components";
import { ethers, providers, Signer } from "ethers";
import React, { createContext, FunctionComponent, useContext, useEffect, useState } from "react";
import { INFURA_API_KEY, NETWORK_NAME } from "../../config";
import { utils } from "@govtechsg/oa-verify";
import React, { createContext, FunctionComponent, useCallback, useContext, useEffect, useState } from "react";
import { INFURA_API_KEY } from "../../config";
import { utils } from "@govtechsg/oa-verify/";
import { magic } from "./helpers";
import { ChainId, ChainInfoObject } from "../../constants/chain-info";
import { NoMetaMaskError, UnsupportedNetworkError } from "../errors";
import { getChainInfo } from "../utils/chain-utils";

export enum SIGNER_TYPE {
IDENTITY = "Identity",
METAMASK = "Metamask",
MAGIC = "Magic",
}

const getProvider =
NETWORK_NAME === "local"
// Utility function for use in non-react components that cannot get through hooks
let currentProvider: providers.Provider | undefined;
export const getCurrentProvider = (): providers.Provider | undefined => currentProvider;

const createProvider = (chainId: ChainId) =>
chainId === ChainId.Local
? new providers.JsonRpcProvider()
: utils.generateProvider({ network: NETWORK_NAME, providerType: "infura", apiKey: INFURA_API_KEY });
: utils.generateProvider({
network: getChainInfo(chainId).networkName,
providerType: "infura",
apiKey: INFURA_API_KEY,
});

interface ProviderContextProps {
providerType: SIGNER_TYPE;
provider: providers.Provider | Signer;
upgradeToMetaMaskSigner: () => Promise<void>;
upgradeToMagicSigner: () => Promise<void>;
account?: string;
changeNetwork: (chainId: ChainId) => void;
reloadNetwork: () => Promise<void>;
getTransactor: () => Signer | providers.Provider | undefined;
getSigner: () => Signer | undefined;
getProvider: () => providers.Provider | undefined;
supportedChainInfoObjects: ChainInfoObject[];
currentChainId: ChainId | undefined;
}

export const ProviderContext = createContext<ProviderContextProps>({
providerType: SIGNER_TYPE.IDENTITY,
provider: getProvider,
// eslint-disable-next-line @typescript-eslint/no-empty-function
upgradeToMetaMaskSigner: async () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
upgradeToMagicSigner: async () => {},
account: undefined,
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changeNetwork: async (_chainId: ChainId) => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
reloadNetwork: async () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
getTransactor: () => undefined,
// eslint-disable-next-line @typescript-eslint/no-empty-function
getProvider: () => undefined,
// eslint-disable-next-line @typescript-eslint/no-empty-function
getSigner: () => undefined,
supportedChainInfoObjects: [],
currentChainId: undefined,
});

interface Ethereum extends providers.ExternalProvider, providers.BaseProvider {
Expand All @@ -49,44 +74,118 @@ declare global {

interface ProviderContextProviderProps {
children: React.ReactNode;
networks: ChainInfoObject[];
defaultChainId: ChainId;
}

export const ProviderContextProvider: FunctionComponent<ProviderContextProviderProps> = ({ children }) => {
export const ProviderContextProvider: FunctionComponent<ProviderContextProviderProps> = ({
children,
networks: supportedChainInfoObjects,
defaultChainId,
}) => {
const [providerType, setProviderType] = useState<SIGNER_TYPE>(SIGNER_TYPE.IDENTITY);
const [provider, setProvider] = useState<providers.Provider | Signer>(getProvider);
const [account, setAccount] = useState<string>();
const [isConnected, setIsConnected] = useState<boolean>();
const [currentChainId, setCurrentChainId] = useState<ChainId | undefined>(defaultChainId);

const initializeMetaMaskSigner = async () => {
const { ethereum, web3 } = window;
const isSupportedNetwork = (chainId: ChainId) =>
supportedChainInfoObjects.findIndex((chainInfoObj) => chainInfoObj.chainId === chainId) > -1;

const defaultProvider = isSupportedNetwork(defaultChainId) ? createProvider(defaultChainId) : undefined;
const [providerOrSigner, setProviderOrSigner] = useState<providers.Provider | Signer | undefined>(defaultProvider);

const updateProviderOrSigner = async (newProviderOrSigner: typeof providerOrSigner) => {
try {
if (!Signer.isSigner(newProviderOrSigner)) {
setIsConnected(false);
} else {
await (newProviderOrSigner as Signer).getAddress();
setIsConnected(true);
}
} catch (e) {
setIsConnected(false);
}
setProviderOrSigner(newProviderOrSigner);
};

const changeNetwork = async (chainId: ChainId) => {
if (!isSupportedNetwork(chainId)) throw new UnsupportedNetworkError(chainId);

const chainInfo = getChainInfo(chainId);

try {
const web3provider = getWeb3Provider();
await requestSwitchChain(chainId);
await updateProviderOrSigner(web3provider.getSigner());
} catch (e: unknown) {
if (e instanceof NoMetaMaskError) {
console.warn(e.message);
await updateProviderOrSigner(createProvider(chainInfo.chainId));
} else {
throw e;
}
}
};

const getProvider = useCallback(() => {
if (providers.Provider.isProvider(providerOrSigner)) return providerOrSigner;
if (Signer.isSigner(providerOrSigner)) return providerOrSigner.provider;
return undefined;
}, [providerOrSigner]);

const getSigner = useCallback(
() => (isConnected ? (providerOrSigner as Signer) : undefined),
[isConnected, providerOrSigner]
);

const getTransactor = useCallback(() => getSigner() ?? getProvider(), [getProvider, getSigner]);

const getWeb3Provider = () => {
const { ethereum, web3 } = window;
const metamaskExtensionNotFound = typeof ethereum === "undefined" || typeof web3 === "undefined";
if (metamaskExtensionNotFound) throw new Error(MessageTitle.NO_METAMASK);
if (metamaskExtensionNotFound || !ethereum.request) throw new NoMetaMaskError();

await ethereum.enable();
const injectedWeb3 = ethereum || web3.currentProvider;
if (!injectedWeb3) throw new Error("No injected web3 provider found");
const web3provider = new ethers.providers.Web3Provider(injectedWeb3);
const signer = web3provider.getSigner();
const web3account = (await web3provider.listAccounts())[0];
return new ethers.providers.Web3Provider(injectedWeb3, "any");
};

const requestSwitchChain = async (chainId: ChainId) => {
const { ethereum } = window;
if (!ethereum || !ethereum.request) return;
try {
await ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${chainId.toString(16)}` }],
});
} catch (e: any) {
if (e.code === -32601) {
// Possibly on localhost which doesn't support the call
return console.error(e);
}
throw e;
}
};

const initializeMetaMaskSigner = async () => {
const web3Provider = getWeb3Provider();
const provider = getProvider();
const network = await (provider ? provider.getNetwork() : web3Provider.getNetwork());
await web3Provider.send("eth_requestAccounts", []);
await requestSwitchChain(network.chainId);

setProvider(signer);
await updateProviderOrSigner(web3Provider.getSigner());
setProviderType(SIGNER_TYPE.METAMASK);
setAccount(web3account);
};

const initialiseMagicSigner = async () => {
// needs to be cast as any before https://github.com/magiclabs/magic-js/issues/83 has been merged.
const magicProvider = new ethers.providers.Web3Provider(magic.rpcProvider as any);
const signer = magicProvider.getSigner();
const address = await signer.getAddress();

setProvider(signer);
await updateProviderOrSigner(magicProvider.getSigner());
setProviderType(SIGNER_TYPE.MAGIC);
setAccount(address);
};

const upgradeToMetaMaskSigner = async () => {
if (providerType === SIGNER_TYPE.METAMASK) return;
return initializeMetaMaskSigner();
};

Expand All @@ -95,17 +194,82 @@ export const ProviderContextProvider: FunctionComponent<ProviderContextProviderP
return initialiseMagicSigner();
};

const reloadNetwork = async () => {
const provider = getProvider();
if (!provider) throw new UnsupportedNetworkError();

const chainId = (await provider.getNetwork()).chainId;
await changeNetwork(chainId);
};

useEffect(() => {
currentProvider = getProvider();
const updateChainId = async () => {
const provider = getProvider();
if (!provider) {
setCurrentChainId(undefined);
} else {
const network = await provider.getNetwork();
setCurrentChainId(network.chainId);
}
};
updateChainId();
}, [getProvider]);

useEffect(() => {
// Do not listen before the provider is upgraded by the app
if (providerType !== SIGNER_TYPE.METAMASK) return;
window.ethereum.on("accountsChanged", () => {
return initializeMetaMaskSigner();
});
}, [providerType]);
if (!window.ethereum) return;

const chainChangedHandler = async (chainIdHex: string) => {
try {
await changeNetwork(parseInt(chainIdHex, 16));
} catch (e) {
// Clear provider/signer when user selects an unsupported network
await updateProviderOrSigner(undefined);
console.warn("An unsupported network has been selected.", e);
throw e;
}
};

window.ethereum.on("accountsChanged", reloadNetwork).on("chainChanged", chainChangedHandler);

const initialiseWallet = async () => {
try {
const web3Provider = getWeb3Provider();
const provider = getProvider();
if (!provider) return;
const [web3Network, appNetwork] = await Promise.all([web3Provider.getNetwork(), provider.getNetwork()]);
if (web3Network.chainId === appNetwork.chainId) setProviderOrSigner(web3Provider.getSigner().provider);
} catch (e) {
if (e instanceof NoMetaMaskError) {
console.warn(e.message);
} else {
throw e;
}
}
};
initialiseWallet();

return () => {
if (!window.ethereum) return;
window.ethereum.off("chainChanged").off("accountsChanged");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<ProviderContext.Provider
value={{ provider, providerType, upgradeToMetaMaskSigner, upgradeToMagicSigner, account }}
value={{
providerType,
upgradeToMetaMaskSigner,
upgradeToMagicSigner,
changeNetwork,
reloadNetwork,
getProvider,
getTransactor,
getSigner,
supportedChainInfoObjects,
currentChainId,
}}
>
{children}
</ProviderContext.Provider>
Expand Down
Loading