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

Advanced config: Multichain support, useContracts and hooks overrides #132

Closed
3 tasks
marekkirejczyk opened this issue Apr 4, 2021 · 3 comments
Closed
3 tasks

Comments

@marekkirejczyk
Copy link
Contributor

marekkirejczyk commented Apr 4, 2021

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

  • Define precise configuration types (Config, what about non-network items)
  • Figure out ChainId
  • Update hooks (useChainCall, useContractFunction, defaults, etc)

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
@marekkirejczyk marekkirejczyk changed the title Multichain support Advanced config: Multichain support, useContracts and hooks overrides Apr 4, 2021
@tt-marek
Copy link
Contributor

This ticket needs rethinking, will create a new one soon

@SmitV
Copy link

SmitV commented Jan 3, 2022

Hi! I was wondering if there was any progress being made regarding this idea. Would love to help facilitate this

@tt-marek
Copy link
Contributor

tt-marek commented Jan 3, 2022

Hi @SmitV,
We will probably be able to move forward with it in February.
We will need to solve #409 first

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants