Skip to content

Commit 674078c

Browse files
authored
Add useReadOnlyContract and useReadOnlyProvider (#205)
* Add `useReadOnlyContract` and `useStaticProvider` * Add changeset * Rename `useStaticProvider` to `useReadOnlyProvider` * Fix `useReadOnlyProvider` type and update changeset message * Rename `staticProvider` to `readOnlyProvider`
1 parent ba55322 commit 674078c

File tree

9 files changed

+174
-10
lines changed

9 files changed

+174
-10
lines changed

.changeset/metal-guests-flash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web3-ui/hooks': minor
3+
---
4+
5+
The `hooks` package now has two new hooks: `useReadOnlyProvider` which gives you a read-only provider and `useReadOnlyContract` which lets you interact with a smart contract without needing a signer.

packages/hooks/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ The following hooks are available:
3333
- [useContract](#usecontract)
3434
- [useTransaction](#usetransaction)
3535
- [useTokenBalance](#usetokenbalance)
36+
- [useReadOnlyContract](#usereadonlycontract)
37+
- [useReadOnlyProvider](#usereadonlyprovider)
3638

3739
---
3840

@@ -69,7 +71,7 @@ const {
6971

7072
### useContract
7173

72-
The `useContract` hook takes the ABI and address of a contract and returns the contract instance.
74+
The `useContract` hook takes the ABI and address of a contract and returns the contract instance. This hook requires the user to have connected their wallet. If you don't want to force your users to connect their wallet in order to read from a contract, use [`useReadOnlyContract`](#usereadonlycontract) instead.
7375

7476
```tsx
7577
import { useContract } from '@web3-ui/hooks';
@@ -133,3 +135,38 @@ const {
133135
balanceInBigNumber
134136
} = useTokenBalance('TOKEN_CONTRACT_ADDRESS', 'ACCOUNT_ADDRESS');
135137
```
138+
139+
---
140+
141+
### useReadOnlyContract
142+
143+
The `useReadOnlyContract` hook takes in a contract address and an ABI and returns a read-only contract instance. This is especially useful when you want to read data from a function without asking the user to connect their wallet. eg. When you are only 'reading' from a contract and not interacting with it.
144+
145+
In order for this hook to work, you need to pass in a `rpcUrl` to the `<Provider />`. eg. `https://rinkeby.infura.io/v3/YOUR_INFURA_ID`
146+
147+
```tsx
148+
<Provider network={NETWORKS.rinkeby} rpcUrl='https://rinkeby.infura.io/v3/YOUR_INFURA_ID'>
149+
```
150+
151+
```tsx
152+
import { useReadOnlyContract } from '@web3-ui/hooks';
153+
154+
const [contract, isReady] = useReadOnlyContract(
155+
'CONTRACT_ADDRESS',
156+
'CONTRACT_ABI'
157+
);
158+
```
159+
160+
---
161+
162+
### useReadOnlyProvider
163+
164+
The `useReadOnlyProvider` takes in a RPC URL (think Infura, Alchemy, etc.) and returns a provider. This provider can be used to read data from the blockchain and from any contract.
165+
166+
```tsx
167+
import { useReadOnlyProvider } from '@web3-ui/hooks';
168+
169+
const provider = useReadOnlyProvider(
170+
'https://rinkeby.infura.io/v3/YOUR_INFURA_ID'
171+
);
172+
```

packages/hooks/src/Provider.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import WalletConnectProvider from '@walletconnect/web3-provider';
33
import { ethers } from 'ethers';
44
import React from 'react';
55
import Web3Modal, { IProviderOptions } from 'web3modal';
6+
import { StaticJsonRpcProvider } from '@ethersproject/providers';
7+
import { useReadOnlyProvider } from './hooks';
68

79
export interface Web3ContextType {
810
connectWallet?: () => void;
@@ -14,6 +16,7 @@ export interface Web3ContextType {
1416
provider?: ethers.providers.Web3Provider | null;
1517
correctNetwork: boolean;
1618
network: number;
19+
readOnlyProvider?: StaticJsonRpcProvider;
1720
}
1821

1922
export const Web3Context = React.createContext<Web3ContextType | undefined>(
@@ -44,6 +47,11 @@ export interface ProviderProps {
4447
]
4548
*/
4649
extraWalletProviders?: [IProviderOptions];
50+
/**
51+
* @dev The JSON RPC provider URL you want to use for read only operations. eg. https://mainnet.infura.io/v3/YOUR_INFURA_KEY
52+
* @type string
53+
*/
54+
rpcUrl?: string;
4755
}
4856

4957
/**
@@ -57,7 +65,8 @@ export const Provider: React.FC<ProviderProps> = ({
5765
children,
5866
network,
5967
infuraId,
60-
extraWalletProviders = []
68+
extraWalletProviders = [],
69+
rpcUrl = ''
6170
}) => {
6271
const [signer, setSigner] = React.useState<null | JsonRpcSigner>();
6372
const [
@@ -69,6 +78,7 @@ export const Provider: React.FC<ProviderProps> = ({
6978
const [chainId, setChainId] = React.useState<number | null>();
7079
const [connected, setConnected] = React.useState<boolean>(false);
7180
const [correctNetwork, setCorrectNetwork] = React.useState<boolean>(true);
81+
const readOnlyProvider = useReadOnlyProvider(rpcUrl);
7282

7383
const connectWallet = React.useCallback(async () => {
7484
const defaulProviderOptions = {
@@ -78,14 +88,14 @@ export const Provider: React.FC<ProviderProps> = ({
7888
bridge: 'https://polygon.bridge.walletconnect.org',
7989
infuraId,
8090
rpc: {
81-
1: `https://eth-mainnet.alchemyapi.io/v2/${infuraId}`, // mainnet // For more WalletConnect providers: https://docs.walletconnect.org/quick-start/dapps/web3-provider#required
91+
1: `https://mainnet.infura.io/v3/${infuraId}`, // mainnet // For more WalletConnect providers: https://docs.walletconnect.org/quick-start/dapps/web3-provider#required
92+
4: `https://rinkeby.infura.io/v3/${infuraId}`,
8293
42: `https://kovan.infura.io/v3/${infuraId}`,
8394
100: 'https://dai.poa.network' // xDai
8495
}
8596
}
8697
}
8798
};
88-
8999
const web3Modal = new Web3Modal({
90100
providerOptions: Object.assign(
91101
defaulProviderOptions,
@@ -169,7 +179,8 @@ export const Provider: React.FC<ProviderProps> = ({
169179
provider,
170180
network,
171181
chainId,
172-
correctNetwork
182+
correctNetwork,
183+
readOnlyProvider
173184
}),
174185
[
175186
connectWallet,
@@ -180,7 +191,8 @@ export const Provider: React.FC<ProviderProps> = ({
180191
provider,
181192
network,
182193
chainId,
183-
correctNetwork
194+
correctNetwork,
195+
readOnlyProvider
184196
]
185197
);
186198

packages/hooks/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export { useWallet } from './useWallet';
22
export { useContract } from './useContract';
33
export { useTransaction } from './useTransaction';
44
export { useTokenBalance } from './useTokenBalance';
5+
export { useReadOnlyProvider } from './useReadOnlyProvider';
6+
export { useReadOnlyContract } from './useReadOnlyContract';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Contract, ContractInterface } from 'ethers';
2+
import React, { useContext, useEffect } from 'react';
3+
import { Web3Context } from '..';
4+
5+
/*
6+
* @dev returns a read-only contract instance. You need to pass in a readOnlyProviderUrl to the Provider in order for this to work.
7+
Does not need a wallet connection / signer to function.
8+
* @param address - The contract address
9+
* @param abi - The contract ABI
10+
*/
11+
export function useReadOnlyContract(address: string, abi: ContractInterface) {
12+
const context = useContext(Web3Context);
13+
const readOnlyProvider = context?.readOnlyProvider;
14+
15+
const [contract, setContract] = React.useState<any>();
16+
const [isReady, setIsReady] = React.useState(false);
17+
18+
useEffect(() => {
19+
if (readOnlyProvider) {
20+
const contract = new Contract(address, abi, readOnlyProvider);
21+
const contractInterface = Object.values(
22+
contract.interface.functions
23+
).reduce((accumulator, funcFragment) => {
24+
return {
25+
...accumulator,
26+
[funcFragment.name]: contract[funcFragment.name]
27+
};
28+
}, {});
29+
setContract(contractInterface);
30+
setIsReady(true);
31+
}
32+
}, [address, abi, readOnlyProvider]);
33+
34+
return [contract, isReady];
35+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect, useState } from 'react';
2+
import { ethers } from 'ethers';
3+
import { StaticJsonRpcProvider } from '@ethersproject/providers';
4+
5+
const createProvider = async (url: string) => {
6+
const p = new ethers.providers.StaticJsonRpcProvider(url);
7+
await p.ready;
8+
return p;
9+
};
10+
11+
/*
12+
* @dev returns a static JSON RPC provider
13+
* @param url - The url of the JSON RPC provider
14+
* @credits scaffold-eth (https://github.com/scaffold-eth/scaffold-eth/blob/master/packages/react-app/src/hooks/useStaticJsonRPC.js)
15+
*/
16+
export function useReadOnlyProvider(url: string) {
17+
const [provider, setProvider] = useState<StaticJsonRpcProvider>();
18+
19+
useEffect(() => {
20+
async function createAndSetProvider() {
21+
if (url === '') {
22+
return;
23+
}
24+
if (!url) {
25+
console.error('Please pass in a valid RPC url');
26+
return;
27+
}
28+
try {
29+
const p = await createProvider(url);
30+
setProvider(p);
31+
} catch (error) {
32+
console.error(error);
33+
}
34+
}
35+
createAndSetProvider();
36+
}, [url]);
37+
38+
return provider;
39+
}

packages/hooks/src/hooks/useWallet.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export function useWallet() {
2222
connected,
2323
provider,
2424
correctNetwork,
25-
network
25+
network,
26+
readOnlyProvider
2627
} = context;
2728

2829
React.useEffect(() => {
@@ -84,6 +85,7 @@ export function useWallet() {
8485
connected,
8586
provider,
8687
correctNetwork,
87-
switchToCorrectNetwork
88+
switchToCorrectNetwork,
89+
readOnlyProvider
8890
};
8991
}

packages/hooks/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ export {
22
useWallet,
33
useContract,
44
useTokenBalance,
5-
useTransaction
5+
useTransaction,
6+
useReadOnlyProvider,
7+
useReadOnlyContract
68
} from './hooks';
79
export { Provider, Web3Context } from './Provider';
810
export { NETWORKS, CHAIN_ID_TO_NETWORK } from './constants';

packages/hooks/src/stories/UseContract.stories.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { storiesOf } from '@storybook/react';
2-
import React from 'react';
2+
import React, { useEffect } from 'react';
33
import { Provider, useWallet, useContract, NETWORKS } from '..';
44
import { Button, Input, Divider, VStack } from '@chakra-ui/react';
55
import { ethers } from 'ethers';
6+
import { useReadOnlyContract } from '../hooks/useReadOnlyContract';
67

78
const ADDRESS = '0x7e1D33FcF1C6b6fd301e0B7305dD40E543CF7135'; // Rinkeby
89
const ABI = [
@@ -133,3 +134,32 @@ storiesOf('Hooks/useContract', module).add('Default', () => (
133134
<Default />
134135
</Provider>
135136
));
137+
138+
const ReadContract = () => {
139+
const [contract, isReady] = useReadOnlyContract(ADDRESS, ABI);
140+
const [greeting, setGreeting] = React.useState('');
141+
142+
useEffect(() => {
143+
async function exec() {
144+
setGreeting(await contract.greet());
145+
}
146+
if (isReady) {
147+
exec();
148+
}
149+
}, [contract, isReady]);
150+
151+
return (
152+
<VStack>
153+
<h3>Greeting: {greeting}</h3>
154+
</VStack>
155+
);
156+
};
157+
158+
storiesOf('Hooks/useReadContract', module).add('Default', () => (
159+
<Provider
160+
network={NETWORKS.rinkeby}
161+
readOnlyProviderUrl="https://rinkeby.infura.io/v3/INFURA_ID"
162+
>
163+
<ReadContract />
164+
</Provider>
165+
));

0 commit comments

Comments
 (0)