手撸一个对标web3-react的库
目标:
动态监听walletAddress,chain, isconnect(可设置需要支持的chian,判断是否已连接钱包,自动保持登录,连接钱包,可构造合约交互,支持非钱包的只读模式。
为什么要自己写一个,这个原因是这样好方便自定义功能,以及方便移植到其他非eth项目的,互相保留多种链。
好了接下来实现,先写一个基本方法,这个要纯粹,不能和框架绑定,到时候好集成vue或者其他框架。
实现web3方法
实现
import Web3 from 'web3';
import detectProvider, { MetaMaskEthereumProvider } from './detectProvider';
const getRpc = (chain: number) =>
chain === 1
? 'https://mainnet.infura.io/v3/'
: 'https://rinkeby.infura.io/v3/';
const defaultChain = process.env.REACT_APP_BASE_ENV === 'test' ? 4 : 1;
let web3: Web3;
let provider: MetaMaskEthereumProvider;
const ethWeb3 = () => {
// @ts-ignore
const ethereum = window?.ethereum;
return {
isConnect(): boolean {
return ethereum?.isConnected?.() ?? false;
},
connect() {
ethereum?.enable();
},
notAccountWeb3(chain = defaultChain) {
var web3 = new Web3();
web3.setProvider(new Web3.providers.HttpProvider(getRpc(chain)));
return web3;
},
async web3(): Promise<Web3 | undefined> {
const _provider = await detectProvider();
if (_provider) {
provider = _provider;
web3 = new Web3(provider as any);
return web3;
}
},
getProvider: () => provider,
};
};
export { web3, ethWeb3, provider };
好了,可以用了,但是登录等操作,数据都是死的,我们需要做一个hooks。
在刚刚的eth web3的文件旁边加一个hook函数,然后新建一个叫useEth的函数。
import { useEffect, useState } from 'react';
import { ethWeb3, web3 } from '..';
import { BounceERC20 as erc20Abi } from 'modules/web3/contracts';
const supportChains = [1, 4];
export const useEth = () => {
const [_web3, setWeb3] = useState<typeof web3>();
const [rpcWeb3, setRpcWeb3] = useState<typeof web3>();
const { notAccountWeb3, ...eth } = ethWeb3();
const [walletAddress, setWalletAddress] = useState<string>();
const [chain, setChain] = useState<number>();
const [isConnect, setIsConnect] = useState<Boolean>(false);
const fetchSetAccount = () => {
if (_web3 ?? web3) {
(_web3 ?? web3).eth.getAccounts().then(res => {
setWalletAddress(res[0]);
});
fetchSetChain();
}
};
const fetchSetChain = async () => {
const res = await web3?.eth.getChainId();
setChain(res);
};
const getTokenBalance = async ({
tokenAddress,
walletAddress,
web3: _web3,
}: {
tokenAddress: string;
walletAddress: string;
web3?: typeof web3;
}) => {
const c_web3 = _web3 ?? web3;
if (!c_web3) return console.log('not web3');
try {
var c = new c_web3.eth.Contract(erc20Abi, tokenAddress);
const balance = await c.methods.balanceOf(walletAddress).call();
return c_web3.utils.fromWei(balance);
} catch (error) {
console.log(
'getTokenBalance error-->',
error,
tokenAddress,
walletAddress,
);
}
};
useEffect(() => {
setIsConnect(!!walletAddress && supportChains.includes(chain ?? -1));
}, [walletAddress, chain]);
useEffect(() => {
setRpcWeb3(notAccountWeb3(chain));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chain]);
useEffect(() => {
eth.web3().then(res => {
setWeb3(res);
eth.getProvider().on('connect', () => {
console.log('connect');
fetchSetAccount();
});
eth.getProvider().on('chainChanged', () => {
console.log('chainChanged');
fetchSetChain();
});
// run accountsChanged
eth.getProvider().on('disconnect', () => {
console.log('disconnect');
fetchSetAccount();
});
eth.getProvider().on('accountsChanged', () => {
console.log('accountsChanged');
fetchSetAccount();
});
});
// eslint-disable-next-line
}, []);
useEffect(() => {
fetchSetAccount();
// eslint-disable-next-line
}, [_web3]);
return {
walletAddress,
eth,
address: walletAddress,
chain,
connect: eth.connect,
web3: _web3,
isConnect,
getTokenBalance,
rpcWeb3,
};
};
可以啦。
对于上面一个导入大家可能比较好奇detectProvider是什么,这个是官方推荐的获取provider方法,我把内容直接下载了。
内容如下
export interface MetaMaskEthereumProvider {
isMetaMask?: boolean;
once(eventName: string | symbol, listener: (...args: any[]) => void): this;
on(eventName: string | symbol, listener: (...args: any[]) => void): this;
off(eventName: string | symbol, listener: (...args: any[]) => void): this;
addListener(
eventName: string | symbol,
listener: (...args: any[]) => void,
): this;
removeListener(
eventName: string | symbol,
listener: (...args: any[]) => void,
): this;
removeAllListeners(event?: string | symbol): this;
}
interface Window {
ethereum?: MetaMaskEthereumProvider;
}
export default detectEthereumProvider;
/**
* Returns a Promise that resolves to the value of window.ethereum if it is
* set within the given timeout, or null.
* The Promise will not reject, but an error will be thrown if invalid options
* are provided.
*
* @param options - Options bag.
* @param options.mustBeMetaMask - Whether to only look for MetaMask providers.
* Default: false
* @param options.silent - Whether to silence console errors. Does not affect
* thrown errors. Default: false
* @param options.timeout - Milliseconds to wait for 'ethereum#initialized' to
* be dispatched. Default: 3000
* @returns A Promise that resolves with the Provider if it is detected within
* given timeout, otherwise null.
*/
function detectEthereumProvider<T = MetaMaskEthereumProvider>({
mustBeMetaMask = false,
silent = false,
timeout = 3000,
} = {}): Promise<T | null> {
_validateInputs();
let handled = false;
return new Promise(resolve => {
if ((window as Window).ethereum) {
handleEthereum();
} else {
window.addEventListener('ethereum#initialized', handleEthereum, {
once: true,
});
setTimeout(() => {
handleEthereum();
}, timeout);
}
function handleEthereum() {
if (handled) {
return;
}
handled = true;
window.removeEventListener('ethereum#initialized', handleEthereum);
const { ethereum } = window as Window;
if (ethereum && (!mustBeMetaMask || ethereum.isMetaMask)) {
resolve(ethereum as unknown as T);
} else {
const message =
mustBeMetaMask && ethereum
? 'Non-MetaMask window.ethereum detected.'
: 'Unable to detect window.ethereum.';
!silent && console.error('@metamask/detect-provider:', message);
resolve(null);
}
}
});
function _validateInputs() {
if (typeof mustBeMetaMask !== 'boolean') {
throw new Error(
`@metamask/detect-provider: Expected option 'mustBeMetaMask' to be a boolean.`,
);
}
if (typeof silent !== 'boolean') {
throw new Error(
`@metamask/detect-provider: Expected option 'silent' to be a boolean.`,
);
}
if (typeof timeout !== 'number') {
throw new Error(
`@metamask/detect-provider: Expected option 'timeout' to be a number.`,
);
}
}
}
如何使用
这样即可(需要多少方法自己暴露多少)
const { isConnect, walletAddress, connect } = useEth();
其他
签名
这里我只针对metamask。另外也不会依赖上面的代码,我觉得这样比较纯粹。没有任何依赖~~
export const signMetaMask = (message: string) => {
return new Promise((ok, reject) => {
// @ts-ignore
const ethereum = window.ethereum
if (!ethereum) {
return window.open('https://metamask.io/download/')
}
ethereum
.request({ method: "eth_requestAccounts" })
.then(async (accounts: string[]) => {
console.log('accounts--->', accounts);
try {
const sigature = await ethereum.request({
method: "personal_sign",
params: [
message,
ethereum.selectedAddress
]
})
ok(sigature)
} catch (error) {
reject(error)
}
}).catch(() => {
reject('uninstall')
})
})
}
使用
try {
const sigature = await signMetaMask("massage")
console.log('sigature---->', sigature)
// your code
} catch (error) {
console.log('sigature fail---->', error)
}
--完--