/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import Web3 from 'web3'; import { AbiItem } from 'web3-utils'; import { Observable, Observer } from 'rxjs'; import { TransactionReceipt } from 'web3-core'; import { AbiHelper } from '@white-matrix/web3-solidity-wallet'; import { ethereum } from './type'; import { EEventType, EInteractEventType, IEvent } from './type/IInternal'; import Web3Modal from 'web3modal'; import WalletConnect from '@walletconnect/web3-provider'; const DEFAULT_INTERVAL = 500; // 0.5s, 轮询查询间隔 const DEFAULT_BLOCKS_TO_WAIT = 5; // 确认次数 const DEFAULT_MAX_QUERY_WAIT_TIME = 500; // 操作后,等待确认的轮询查询次数限制 const CHAIN_NAME_REGEX = /\s*\/s\/(.*)\//; export default class Web3Wallet { static instance: Web3Wallet; web3: Web3; abiHelper: AbiHelper; Web3ModalProvider: Web3Modal; walletProvider: any; walletId = 'Web3Wallet'; interval: number; blocksToWait: number; maxQueryWaitTimes: number; constructor() { this.Web3ModalProvider = null; this.walletProvider = null; this.web3 = new (window as any).Web3(this.walletProvider); this.abiHelper = new AbiHelper(this.web3); this.interval = DEFAULT_INTERVAL; this.blocksToWait = DEFAULT_BLOCKS_TO_WAIT; this.maxQueryWaitTimes = DEFAULT_MAX_QUERY_WAIT_TIME; } static getInstance() { if (this.instance instanceof this === false) { this.instance = new this(); } return this.instance; } static destroyInstance() { this.instance = null; } setInterval = (interval: number) => { this.interval = interval; }; setBlocksToWait = (blocksToWait: number) => { this.blocksToWait = blocksToWait; }; setMaxTimes = (maxTimes: number) => { this.maxQueryWaitTimes = maxTimes; }; /** * [init 钱包初始化] * @method init * @params {onChainChange: listen并获取网络, onAccountChange: listen并获取账户 } * @return {[Promise]} */ init = async () => { const matchChainName = CHAIN_NAME_REGEX.exec(window.location.href); this.Web3ModalProvider = new Web3Modal({ network: 'rinkeby', theme: { background: '#1a1c1e', main: 'rgb(199, 199, 199)', secondary: 'rgb(136, 136, 136)', border: 'rgba(195, 195, 195, 0.14)', hover: 'rgb(16, 26, 32)' }, cacheProvider: true, providerOptions: { binancechainwallet: { package: matchChainName && matchChainName[1] === 'bnbchain' } } }); // await web3Modal.toggleModal(); this.walletProvider = await this.Web3ModalProvider.connect(); if (!this.walletProvider) { return new Error( matchChainName && matchChainName[1] === 'bnbchain' ? 'No Binance Chain Wallet Found' : 'No MetaMask Found' ); } await this.walletProvider.enable(); this.web3 = new Web3(this.walletProvider); return this.walletProvider; }; clearProvider = () => { if (this.Web3ModalProvider) { this.Web3ModalProvider.clearCachedProvider(); } }; createAccount = () => { const matchChainName = CHAIN_NAME_REGEX.exec(window.location.href); return new Observable((observer: Observer) => { observer.error( matchChainName && matchChainName[1] === 'bnbchain' ? '请在币安钱包中新增账户' : '请在metamask 插件中新增账户' ); observer.complete(); }); }; fetchNetWork = async () => { return new Promise((resolve, reject) => { this.walletProvider .request({ method: 'eth_requestAccounts' }) .then(() => { resolve(this.walletProvider.chainId); }) .catch((error: Error) => { reject(error); }); }); }; fetchAccount = (): Promise => { return new Promise((resolve, reject) => { this.walletProvider .request({ method: 'eth_accounts' }) .then(async (accounts: string[]) => { if (accounts?.[0]) { const balance = await this.walletProvider.request({ method: 'eth_getBalance', params: [accounts?.[0], 'latest'] }); const account = [ { address: accounts?.[0], balance: balance } ]; resolve(account); } resolve([]); }) .catch((error: unknown) => { reject(error); }); }); }; deployContract = ( actionData: ethereum.IDeployContractActionData ): Observable> => { return new Observable((observer: Observer>) => { const { abi } = actionData.solcCompiledOutput; const constructorFunc = abi.find((o) => o.type === 'constructor'); let params = ''; try { params = this.abiHelper.encodeParameters( constructorFunc?.inputs || [], actionData.constructorArgs ); const bytecode = `0x${actionData.solcCompiledOutput.evm.bytecode.object}`; let transactionHash: string; this.web3.eth .sendTransaction({ from: actionData.account.address, value: actionData.deployOptions.payPrice, data: bytecode + params.slice(2) }) .on('transactionHash', (hash: string) => { transactionHash = hash; observer.next({ type: EEventType.TRANSACTION_HASH, data: { hash } }); }) .on('receipt', (receipt: TransactionReceipt) => { this.web3.eth.getTransaction(transactionHash).then((tx) => { observer.next({ type: EEventType.RECEIPT, data: { receipt: { ...receipt, ...tx } } }); observer.complete(); }); }) // .on( // 'confirmation', // (confNumber: number, receipt: TransactionReceipt) => { // observer.next({ // type: EEventType.CONFIRMATION, // data: { receipt, confirmationNumber: confNumber } // }); // } // ) .on('error', (error: string) => { if (transactionHash) { observer.next({ type: EEventType.TRANSACTION_ERROR, data: { transactionHash } }); } observer.error(error); observer.complete(); }) .catch((e) => { if (transactionHash) { observer.next({ type: EEventType.TRANSACTION_ERROR, data: { transactionHash } }); } observer.error(e); observer.complete(); }); } catch (err) { observer.error(err); } }); }; interactContract = ( interactContractActionData: ethereum.IInteractContractActionData ): Observable> => { return new Observable((observer: Observer>) => { const { abi, abiEntryIdx, accountAddress, interactArgs, contractAddress, payableValue } = interactContractActionData; try { const targetAbiEntry = abi[abiEntryIdx]; const contract = new this.web3.eth.Contract( abi as AbiItem[], contractAddress ); if ( targetAbiEntry.stateMutability === 'pure' || targetAbiEntry.stateMutability === 'view' ) { // use call const functionSignature = this.web3.eth.abi.encodeFunctionSignature( targetAbiEntry as AbiItem ); const functionArguments = this.abiHelper.validAndParseArgs( targetAbiEntry.inputs, interactArgs ); const contractFunction = contract.methods[functionSignature]( ...functionArguments ); contractFunction .call({ from: accountAddress }) .then((result: any) => { const callResult = this.abiHelper.decodeFunctionResult({ abiOutputs: targetAbiEntry.outputs || [], originResult: result }); observer.next({ type: EInteractEventType.CALL_RESULT, data: callResult }); }) .catch((e: Error) => { if (e?.message?.includes('Internal JSON-RPC error')) { let message = e.message.split('"message":')?.[1]; message = message ? message.split('"data"')[0] : ''; message = `Internal JSON-RPC error --${message}`; e = new Error(message); } else if (e?.message?.includes('execution reverted')) { e = new Error(e.message.split('\n')[0]); } observer.error(e); observer.complete(); }); } else { let transactionHash: string; const functionData = this.abiHelper.encodeFunctionCall( targetAbiEntry as AbiItem, interactArgs ); this.web3.eth .sendTransaction({ from: accountAddress, to: contractAddress, value: payableValue || '0x00', data: functionData }) .on('transactionHash', (hash: string) => { transactionHash = hash; observer.next({ type: EInteractEventType.SEND_TRANSACTION_HASH, data: { hash } }); }) .on('receipt', (receipt: TransactionReceipt) => { this.web3.eth.getTransaction(transactionHash).then((tx) => { observer.next({ type: EInteractEventType.SEND_RECEIPT, data: { receipt: { ...receipt, ...tx } } }); observer.complete(); }); }) // .on( // 'confirmation', // (confNumber: number, receipt: TransactionReceipt) => { // observer.next({ // type: EInteractEventType.SEND_CONFIRMATION, // data: { receipt, confirmationNumber: confNumber } // }); // } // ) .on('error', (error: string) => { if (transactionHash) { observer.next({ type: EInteractEventType.GET_TRANSACTION_ERROR, data: { transactionHash } }); } observer.error(error); observer.complete(); }); } } catch (err) { observer.error(err); } }); }; getAccountBalance = async (address: string) => { const balance = await this.walletProvider.request({ method: 'eth_getBalance', params: [address, 'latest'] }); return { address: address, balance: balance }; }; /** * contract low level interaction * 1. receive和fallback都不存在则无法calldata * 2. receive存在只可transfer,不可发送数据 * 3. fallback存在可transfer和发送数据 */ calldataContract = (calldataActionData: ethereum.ICalldataActionData) => { return new Observable((observer: Observer>) => { const { calldata, payableValue, contractAddress, accountAddress } = calldataActionData; const transactionParameter = { from: accountAddress, to: contractAddress, value: payableValue, data: calldata || '' }; let transactionHash: string; this.web3.eth .sendTransaction(transactionParameter) .on('transactionHash', (hash: string) => { transactionHash = hash; observer.next({ type: EInteractEventType.SEND_TRANSACTION_HASH, data: { hash } }); }) .on('receipt', (receipt: TransactionReceipt) => { this.web3.eth.getTransaction(transactionHash).then((tx) => { observer.next({ type: EInteractEventType.SEND_RECEIPT, data: { receipt: { ...receipt, ...tx } } }); observer.complete(); }); }) // .on( // 'confirmation', // (confNumber: number, receipt: TransactionReceipt) => { // if (confNumber <= this.blocksToWait) { // observer.next({ // type: EInteractEventType.SEND_CONFIRMATION, // data: { receipt, confirmationNumber: confNumber } // }); // } else { // observer.complete(); // } // } // ) .on('error', (error: string) => { if (transactionHash) { observer.next({ type: EInteractEventType.GET_TRANSACTION_ERROR, data: { transactionHash } }); } observer.error(error); observer.complete(); }); }); }; }