Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added

### Changed
- [MAJOR] Updated to ethers v6
- [MINOR] Added model to abstract away ethers types

### Removed

Expand Down
17,527 changes: 4,682 additions & 12,845 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,27 @@
"url": "https://github.com/kibalabs/web3-react.git"
},
"bugs": "https://github.com/kibalabs/web3-react/issues",
"files": [
"dist/*"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"sideEffects": false,
"files": [
"./dist/**/*"
],
"//": "NOTE(krishan711): because of some webpack issue, ethers needs to be specified as a dependency by all lib users",
"dependencies": {
"@ethersproject/abstract-provider": "5.7.0",
"@kibalabs/core": "^0.5.10",
"@kibalabs/core-react": "^0.9.3",
"@metamask/detect-provider": "2.0.0",
"ethers": "5.7.2"
"@kibalabs/core": "^0.6.1",
"@kibalabs/core-react": "^0.9.4",
"ethers": "^6.4.0"
},
"devDependencies": {
"@kibalabs/build": "^0.12.0",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"@kibalabs/build": "0.13.0",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 ",
"react-dom": "^17.0.0 || ^18.0.0 "
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
}
67 changes: 44 additions & 23 deletions src/Web3AccountContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import React from 'react';

import { dateToString, LocalStorageClient } from '@kibalabs/core';
import { IMultiAnyChildProps, useInitialization } from '@kibalabs/core-react';
import detectEthereumProvider from '@metamask/detect-provider';
import { BigNumber, ethers } from 'ethers';
import { Eip1193Provider, BrowserProvider as EthersBrowserProvider, JsonRpcApiProvider as EthersJsonRpcApiProvider } from 'ethers';

import { Web3Provider, Web3Signer } from './model';

export type Web3Account = {
address: string;
signer: ethers.Signer;
signer: Web3Signer;
}

export type Web3LoginSignature = {
Expand All @@ -16,7 +17,7 @@ export type Web3LoginSignature = {
}

type Web3AccountControl = {
web3: ethers.providers.Web3Provider | undefined | null;
web3: Web3Provider | undefined | null;
web3ChainId: number | undefined | null;
web3Account: Web3Account | undefined | null;
web3LoginSignature: Web3LoginSignature | undefined | null;
Expand All @@ -32,19 +33,25 @@ interface IWeb3AccountControlProviderProps extends IMultiAnyChildProps {
}

export const Web3AccountControlProvider = (props: IWeb3AccountControlProviderProps): React.ReactElement => {
const [web3, setWeb3] = React.useState<ethers.providers.Web3Provider | null | undefined>(undefined);
const [eip1193Provider, setEip1193Provider] = React.useState<Eip1193Provider | null | undefined>(undefined);
const [web3, setWeb3] = React.useState<EthersBrowserProvider | null | undefined>(undefined);
const [web3ChainId, setWeb3ChainId] = React.useState<number | null | undefined>(undefined);
const [web3Account, setWeb3Account] = React.useState<Web3Account | undefined | null>(undefined);
const [loginCount, setLoginCount] = React.useState<number>(0);

const loadWeb3 = async (): Promise<void> => {
const provider = await detectEthereumProvider() as ethers.providers.ExternalProvider;
// NOTE(krishan711): keep an eye on how metamask provider does this: https://github.com/MetaMask/detect-provider/blob/main/src/index.ts
// NOTE(krishan711): could use ethers.getDefaultProvider() for non-wallet scenarios
// @ts-expect-error
const provider = window.ethereum != null ? new EthersBrowserProvider(window.ethereum) : null;
if (!provider) {
setWeb3Account(null);
setEip1193Provider(null);
return;
}
const web3Connection = new ethers.providers.Web3Provider(provider);
setWeb3(web3Connection);
setWeb3(provider);
// @ts-expect-error
setEip1193Provider(window.ethereum);
};

const onChainChanged = React.useCallback((): void => {
Expand All @@ -55,8 +62,15 @@ export const Web3AccountControlProvider = (props: IWeb3AccountControlProviderPro
if (!web3) {
return;
}
const linkedWeb3Accounts = web3AccountAddresses.map((web3AccountAddress: string): ethers.Signer => web3.getSigner(web3AccountAddress));
const potentialLinkedWeb3Accounts: (Web3Signer | null)[] = await Promise.all(web3AccountAddresses.map((web3AccountAddress: string): Promise<Web3Signer | null> => {
if (!(web3 instanceof EthersJsonRpcApiProvider)) {
return Promise.resolve(null);
}
return (web3 as EthersJsonRpcApiProvider).getSigner(web3AccountAddress);
}));
const linkedWeb3Accounts = potentialLinkedWeb3Accounts.filter((potentialSigner: Web3Signer | null): boolean => potentialSigner != null) as Web3Signer[];
if (linkedWeb3Accounts.length === 0) {
setWeb3Account(null);
return;
}
// NOTE(krishan711): metamask only deals with one web3Account at the moment but returns an array for future compatibility
Expand All @@ -69,35 +83,42 @@ export const Web3AccountControlProvider = (props: IWeb3AccountControlProviderPro
if (!web3) {
return;
}
// @ts-expect-error
onWeb3AccountsChanged(await web3.provider.request({ method: 'eth_accounts' }));
// @ts-expect-error
web3.provider.on('accountsChanged', onWeb3AccountsChanged);
// @ts-expect-error
const newChainId = await web3.provider.request({ method: 'eth_chainId' });
setWeb3ChainId(BigNumber.from(newChainId).toNumber());
// @ts-expect-error
web3.provider.on('chainChanged', onChainChanged);
}, [web3, onWeb3AccountsChanged, onChainChanged]);
const newWeb3AccountAddresses = await web3.send('eth_accounts', []);
onWeb3AccountsChanged(newWeb3AccountAddresses);
const newChainId = await web3.send('eth_chainId', []);
setWeb3ChainId(Number(newChainId));
}, [web3, onWeb3AccountsChanged]);

React.useEffect((): void => {
loadWeb3Accounts();
}, [loadWeb3Accounts]);

const monitorWeb3AccountChanges = React.useCallback(async (): Promise<void> => {
if (!eip1193Provider) {
return;
}
// @ts-expect-error
eip1193Provider.on('accountsChanged', onWeb3AccountsChanged);
// @ts-expect-error
eip1193Provider.on('chainChanged', onChainChanged);
}, [eip1193Provider, onWeb3AccountsChanged, onChainChanged]);

React.useEffect((): void => {
monitorWeb3AccountChanges();
}, [monitorWeb3AccountChanges]);

const onLinkWeb3AccountsClicked = async (): Promise<void> => {
if (!web3) {
return;
}
// @ts-expect-error
web3.provider.request({ method: 'eth_requestAccounts', params: [] }).then(async (): Promise<void> => {
web3.send('eth_requestAccounts').then(async (): Promise<void> => {
await loadWeb3();
}).catch((error: unknown): void => {
if ((error as Error).message?.includes('wallet_requestPermissions')) {
props.onError(new Error('WALLET_REQUEST_ALREADY_OPEN'));
// toastManager.showToast('error', 'You already have a MetaMask request window open, please find it!');
} else {
props.onError(new Error('WALLET_CONNECTION_FAILED'));
// toastManager.showToast('error', 'Something went wrong connecting to MetaMask. Please try refresh the page / your browser and try again');
}
});
};
Expand Down Expand Up @@ -150,7 +171,7 @@ Date: ${dateToString(new Date())}
);
};

export const useWeb3 = (): ethers.providers.Web3Provider | undefined | null => {
export const useWeb3 = (): Web3Provider | undefined | null => {
const web3AccountsControl = React.useContext(Web3AccountContext);
if (!web3AccountsControl) {
throw Error('web3AccountsControl has not been initialized correctly.');
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './model';
export * from './Web3AccountContext';
export * from './useWeb3Contract';
export * from './useWeb3Transaction';
8 changes: 8 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Contract as EthersContract, InterfaceAbi as EthersInterfaceAbi, Provider as EthersProvider, Signer as EthersSigner, TransactionReceipt as EthersTransactionReceipt, TransactionResponse as EthersTransactionResponse } from 'ethers';

export type Web3Signer = EthersSigner;
export type Web3Provider = EthersProvider;
export type Web3ContractInterface = EthersInterfaceAbi;
export type Web3Contract = EthersContract;
export type Web3TransactionResponse = EthersTransactionResponse;
export type Web3TransactionReceipt = EthersTransactionReceipt;
9 changes: 5 additions & 4 deletions src/useWeb3Contract.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';

import { ethers } from 'ethers';
import { Contract as EthersContract } from 'ethers';

import { Web3Contract, Web3ContractInterface } from './model';
import { useWeb3, useWeb3ChainId } from './Web3AccountContext';


export const useWeb3Contract = (contractChainIdAddressMap: Record<number, string>, abi: ethers.ContractInterface): ethers.Contract | null | undefined => {
export const useWeb3Contract = (contractChainIdAddressMap: Record<number, string>, abi: Web3ContractInterface): Web3Contract | null | undefined => {
const web3 = useWeb3();
const chainId = useWeb3ChainId();
const contract = React.useMemo((): ethers.Contract | null | undefined => {
const contract = React.useMemo((): Web3Contract | null | undefined => {
if (web3 === undefined || chainId === undefined) {
return undefined;
}
Expand All @@ -19,7 +20,7 @@ export const useWeb3Contract = (contractChainIdAddressMap: Record<number, string
if (!contractAddress) {
return null;
}
return new ethers.Contract(contractAddress, abi, web3);
return new EthersContract(contractAddress, abi, web3);
}, [web3, chainId, abi, contractChainIdAddressMap]);
return contract;
};
31 changes: 16 additions & 15 deletions src/useWeb3Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React from 'react';

import { ethers } from 'ethers';
import { Web3TransactionReceipt, Web3TransactionResponse } from './model';

export type TransactionPromise = Promise<ethers.ContractTransaction>;
export type TransactionPromise = Promise<Web3TransactionResponse>;

export interface Web3TransactionDetails {
transactionPromise: TransactionPromise | null;
transaction: ethers.ContractTransaction | null;
transaction: Web3TransactionResponse | null;
error: Error | null;
receipt: ethers.ContractReceipt | null;
receipt: Web3TransactionReceipt | null;
}

export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPromise: TransactionPromise | null) => void, () => void, () => void] => {
const [transactionPromise, setTransactionPromise] = React.useState<TransactionPromise | null>(null);
const [transaction, setTransaction] = React.useState<ethers.ContractTransaction | null>(null);
const [transaction, setTransaction] = React.useState<Web3TransactionResponse | null>(null);
const [error, setError] = React.useState<Error | null>(null);
const [receipt, setReceipt] = React.useState<ethers.ContractReceipt | null>(null);
const [receipt, setReceipt] = React.useState<Web3TransactionReceipt | null>(null);

const clearError = React.useCallback((): void => {
setError(null);
Expand Down Expand Up @@ -69,14 +69,15 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr
waitForTransaction();
}, [waitForTransaction]);

return [{
transactionPromise,
transaction,
error,
receipt,
},
setNewTransactionPromise,
clearError,
clearTransaction,
return [
{
transactionPromise,
transaction,
error,
receipt,
},
setNewTransactionPromise,
clearError,
clearTransaction,
];
};