Summary
This is proposal for introducing multichain support in useDApp. This will allow to connect to several chains in read-only mode and one in write mode.
In the future multiple write chains might be available, without backward compatibility breaking.
TODO
Configuration
Example configuration with mulitchain:
const config = {
networks: {
[chainId: ChainId.Mainnet]: {
url: 'https://mainnet.infura.io/v3/93626a985d4508b2b7a24827551487d1'
},
[chainId: ChainId.Kovan]: {
url: 'https://kovan.infura.io/v3/93626a985d4508b2b7a24827551487d1'
},
[chainId: 777]: {
url: 'magicChain.url.com',
contracts: {
multicall: '0x123...456'
}
},
defaultNetwork: ChainId.Mainnet,
notifications: {
checkInterval: 15000,
expirationPeriod: 5000
}
}
New types:
export type Config {
networks: {
[chainId: number]: {
name:string,
url:string,
pollingInterval?: number,
contracts: {
multicall: string,
uniswapFactory: string
}
}
},
notifications: {
checkInterval: number
expirationPeriod: number
},
defaultNetwork: number
}
Flexible chainId
export enum KnownChainId {
Mainnet = 1,
Ropsten = 3,
...
}
type ChainId = KnownChainId | number
It's also possible to leave chainId as is and in config accept number. Enum type can be supplied to number parameter.
Legacy config support
To introduce backward compatibility we can rename current config type to LegacyConfig
and introduce new type Config
.
type BackwardsCompatibleConfig = LegacyConfig | Config
New hooks
NetworkConnectorProvider
Provider that will hold a NetworkConnector object.
Object will be updated when networks in config change.
Draft:
export const NetworkConnectorContext = createContext<NetworkConnector>(new NetworkConnector({urls:[]}))
export function useNetworkConnector() {
return useContext(NetworkConnectorContext)
}
export function NetworkConnectorProvider({ children }: {children:ReactNode}) {
const {networks} = useConfig()
const networkConnector = useMemo(() => (
new NetworkConnector({urls: networks})
),[networks])
return <NetworkConnectorContext.Provider value={{ networkConnector }} children={children} />
}
LibrariesProvider
Web3ReactProvider for read only libraries.
See docs
Draft:
export function LibrariesProvider({ children, pollingInterval }: EthersProviderProps) {
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider, 'any')
library.pollingInterval = pollingInterval || DEFAULT_POLLING_INTERVAL
return library
}
const Provider = createWeb3ReactRoot('libraries')
return <Provider getLibrary={getLibrary}>{children}</Provider>
}
useLibraries
Similar to use ethers will use NetworkConnector to change connected ID. Also will contain a hasBlockChanged
to tell whether the block changed on currently connected network from the last time this function was called.
const {provider, changeNetworkID, hasBlockChanged } = useLibraries()
Draft:
export function useEthers() {
const result = useWeb3React<Web3Provider>('libraries')
const networkConnector = useNetworkConnector()
const [blockNumbers, setBlockNumbers] = useState<{[chainId: number]:number }>({})
const changeNetworkID = useCallback(
async (chainId: ChainId | number) => {
networkConnector.changeChainId(chainId)
await result.activate(networkConnector)
},
[]
)
const hasBlockChanged = useCallback(async () => {
const chainId = result.chainId
const blockNumber = blockNumbers[chainId]
const newBlockNumber = await result.library?.getBlockNumber()
if(blockNumber != newBlockNumber){
setBlockNumbers(...blockNumbers, [chainId]:newBlockNumber)
return true
}
return false
},[])
return { ...result, changeNetworkID, hasBlockChanged }
}
useContracts
const {uniswapFactory} = useContracts(chainId?)
New models
CallOptions
export type CallOptions = {
chainId?: ChainId
}
Changes to old models
TransactionOptions
export type TransactionOptions {
signer?: Signer
transactionName?: string
chainId?: ChainId | number
}
Changes to old hooks
useConfig
Add support for both legacy and new config
Extract multicallAdressess and supportedChains from new Config
const {config, multicallAdressess, supportedChains} = useConfig()
useUpdateConfig
const {updateConfig, addNetwork, removeNetwork, updateNetwork } = useUpdateConfig()
ChainStateProvider
ChainStateProvider will have to hold calls and results that are separated between each chainId, also will hold special list of calls that follows the chainId of connected wallet.
Draft:
export type Calls = {
walletCalls: ChainCall[]
[chainId: ChainId | number]: ChainCall[]
}
export interface State {
walletState:
| {
blockNumber: number
state?: ChainState
error?: unknown
}
| undefined
,
[chainId: number]:
| {
blockNumber: number
state?: ChainState
error?: unknown
}
| undefined
}
export function ChainStateProvider({ children, multicallAddresses }: Props) {
...
const {library,changeNetworkID, hasBlockChanged} = useLibraries
cost {networks} = useConfig
useEffect(() => {
const update = setInterval(async () => {
networks.forEach((network) => {
await changeNetworkID(network.chainId)
if (await hasBlockChanged()){
const blockNumber = await library.blockNumber
multicall(library, network.contract.multicall,blockNumber, calls[network.chainId])
.then((state) => dispatchState({ type: 'FETCH_SUCCESS', blockNumber, network.chainId, state }))
.catch((error) => {
console.error(error)
dispatchState({ type: 'FETCH_ERROR', blockNumber, network.chainId, error })
}
) }
} ,libraryPollingInterval)
return () => update.cancel()
}
,[networks])
const provided = { value:state, multicallAddress, addCalls, removeCalls }
return <ChainStateContext.Provider value={provided} children={children} />
}
useChainCalls
export function useChainCalls(calls: (ChainCall | Falsy)[], chainId?: ChainId | number)
read only hooks
Add CallOptions parameters to functions that read state from blockchain
export function useContractCalls(calls: (ContractCall | Falsy)[], options?:CallOptions): (any[] | undefined)[]
export function useContractCall(call: ContractCall | Falsy, options?:CallOptions): any[] | undefined
export function useEtherBalance(address: string | Falsy, options?:CallOptions): BigNumber | undefined
export function useTokenAllowance(
tokenAddress: string | Falsy,
ownerAddress: string | Falsy,
spenderAddress: string | Falsy,
options?: CallOptions
): BigNumber | undefined
export function useTokenBalance(
tokenAddress: string | Falsy,
address: string | Falsy,
options?: CallOptions
): BigNumber | undefined
usePromiseTransaction
usePromiseTransaction will set exception state when options.chainId doesn't match wallet chainId
Proposed task list
- Add new backwards compatible config
- Add necessary providers (new providers shouldn't change DApp behaviour)
- Refactor ChainCallProvider calls state and return state (make calls array differentiate between calls to different chainIDs)
- Make ChainCallProvider call other chains
- Refactor useContractCall to be able to set chainID
- Refactor transaction sending sets exception state
- General refactor