导出Web3合约常量时报错“Provider does not have a request or send method to use”
问题核心原因
web3Modal.connect()是异步方法,返回值为Promise<Provider>而非Provider实例本身。你在dai.js中直接将该Promise传入Web3构造函数,相当于给Web3绑定了一个没有send/request方法的Promise对象,自然会触发报错。- 模块导入逻辑会在代码加载阶段提前执行,dai.js中的实例化代码在用户触发钱包连接操作前就已经运行完成,此时还未获取到用户授权的有效Provider。
- dai.js中初始化的Web3实例和
loadWeb3Modal中初始化的Web3实例完全独立,后者后续拿到有效Provider后,前者绑定的仍然是初始化时的无效Provider,不会自动同步状态。
解决方案
方案1:导出合约工厂函数(最简方案)
不要提前实例化合约,改为导出工厂函数,传入已经完成Provider初始化的Web3实例动态生成合约对象,既可以统一管理合约ABI和地址,也能保证Provider有效。
调整dai.js代码
import daiAbi from '../../artifactsUse/dai.json' const DaiAddress = "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa" export const getDaiContract = (web3) => { return new web3.eth.Contract(daiAbi.abi, DaiAddress) }
调整业务逻辑调用代码
import {getDaiContract} from '../../abi/dai' const purchaseTokensWithDAI = async(price)=>{ if(!accounts) { await faucet() } // 传入已经初始化完成的web3实例生成合约 const dai = getDaiContract(web3) const crowdsale = new web3.eth.Contract(props.IcoContract.abi,IcoAddress) // 原有交易逻辑保持不变 await dai.methods.approve(IcoAddress,Web3.utils.toBN(price * 10 ** 18)).send({from:accounts[0]}).once("confirmation",async(confirmation)=>{ await crowdsale.methods.buyTokensWithDAI(price).send({from:accounts[0]}).once("confirmation",(confirmation)=>{ console.log(confirmation) }).catch("error",(error)=>{ console.log(error) }) }) }
方案2:全局状态统一管理(推荐工程化方案)
如果项目中多处用到合约实例,可通过全局状态管理(React Context、Vuex/Pinia等)统一管理Web3实例和合约实例,在用户完成钱包连接后统一初始化所有合约,全局共享使用,避免重复实例化。
示例(React Context实现)
// Web3Context.js import { createContext, useContext, useState } from 'react' import Web3 from 'web3' import {web3Modal} from './provider' import daiAbi from '../../artifactsUse/dai.json' import icoAbi from '../../artifactsUse/ico.json' const DaiAddress = "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa" const IcoAddress = "你的ICO合约地址" const Web3Context = createContext() export const Web3Provider = ({children}) => { const [web3, setWeb3] = useState(null) const [daiContract, setDaiContract] = useState(null) const [icoContract, setIcoContract] = useState(null) const [accounts, setAccounts] = useState([]) const loadWeb3Modal=async()=>{ const provider = await web3Modal.connect() const web3Instance = new Web3(provider) setWeb3(web3Instance) // 统一初始化所有合约实例 setDaiContract(new web3Instance.eth.Contract(daiAbi.abi, DaiAddress)) setIcoContract(new web3Instance.eth.Contract(icoAbi.abi, IcoAddress)) // provider事件监听逻辑保持不变 provider.on("accountsChanged", (accounts) => { setAccounts(accounts) }); provider.on("chainChanged", (chainId) => { console.log(chainId); }); provider.on("connect", (chainId) => { console.log(chainId); }); provider.on("disconnect", () => { provider.close(); web3Modal.clearCachedProvider(); setWeb3(null) setDaiContract(null) setIcoContract(null) setAccounts([]) }); } const faucet=async()=> { await loadWeb3Modal() const accs = await web3Instance.eth.getAccounts() setAccounts(accs) } return ( <Web3Context.Provider value={{web3, daiContract, icoContract, accounts, faucet}}> {children} </Web3Context.Provider> ) } export const useWeb3 = () => useContext(Web3Context)
业务组件调用
const {daiContract, icoContract, accounts, faucet} = useWeb3() const purchaseTokensWithDAI = async(price)=>{ if(!accounts.length) { await faucet() } // 直接使用全局初始化完成的合约实例 await daiContract.methods.approve(IcoAddress,Web3.utils.toBN(price * 10 ** 18)).send({from:accounts[0]}).once("confirmation",async(confirmation)=>{ await icoContract.methods.buyTokensWithDAI(price).send({from:accounts[0]}).once("confirmation",(confirmation)=>{ console.log(confirmation) }).catch("error",(error)=>{ console.log(error) }) }) }
注意事项
- 不要在模块顶层执行钱包连接相关的异步逻辑,模块加载阶段用户还未进行交互,提前触发连接会影响用户体验,也无法拿到有效Provider。
- 所有依赖Provider的Web3、合约实例,必须在用户触发钱包连接、拿到有效Provider之后再初始化。
内容的提问来源于stack exchange,提问作者Icarus23
相关产品推荐
相关产品推荐

