UNPKG

saepenatus

Version:

Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, mul

229 lines (190 loc) 6.57 kB
import BigNumber from 'bignumber.js' import { nanoid } from 'nanoid' import defaultCopy from './i18n/en.json' import type { Network } from 'bnc-sdk' import type { Notification, PreflightNotificationsOptions } from './types.js' import { addNotification, removeNotification } from './store/actions.js' import { state } from './store/index.js' import { eventToType } from './notify.js' import { networkToChainId } from './utils.js' import { validatePreflightNotifications } from './validation.js' let notificationsArr: Notification[] state.select('notifications').subscribe(notifications => { notificationsArr = notifications }) export async function preflightNotifications( options: PreflightNotificationsOptions ): Promise<string | void> { const invalid = validatePreflightNotifications(options) if (invalid) { throw invalid } const { sendTransaction, estimateGas, gasPrice, balance, txDetails, txApproveReminderTimeout } = options // Check for reminder timeout and confirm its greater than 3 seconds const reminderTimeout: number = txApproveReminderTimeout && txApproveReminderTimeout > 3000 ? txApproveReminderTimeout : 15000 // if `balance` or `estimateGas` or `gasPrice` is not provided, // then sufficient funds check is disabled // if `txDetails` is not provided, // then duplicate transaction check is disabled // if dev doesn't want notify to initiate the transaction // and `sendTransaction` is not provided, then transaction // rejected notification is disabled // to disable hints for `txAwaitingApproval`, `txConfirmReminder` // or any other notification, then return false from listener functions const [gas, price] = await gasEstimates(estimateGas, gasPrice) const id = createId(nanoid()) const value = new BigNumber((txDetails && txDetails.value) || 0) // check sufficient balance if required parameters are available if (balance && gas && price) { const transactionCost = gas.times(price).plus(value) // if transaction cost is greater than the current balance if (transactionCost.gt(new BigNumber(balance))) { const eventCode = 'nsfFail' addNotification(buildNotification(eventCode, id)) } } // check previous transactions awaiting approval const txRequested = notificationsArr.find(tx => tx.eventCode === 'txRequest') if (txRequested) { const eventCode = 'txAwaitingApproval' const newNotification = buildNotification(eventCode, txRequested.id) addNotification(newNotification) } // confirm reminder timeout defaults to 20 seconds setTimeout(() => { const awaitingApproval = notificationsArr.find( tx => tx.id === id && tx.eventCode === 'txRequest' ) if (awaitingApproval) { const eventCode = 'txConfirmReminder' const newNotification = buildNotification(eventCode, awaitingApproval.id) addNotification(newNotification) } }, reminderTimeout) const eventCode = 'txRequest' addNotification(buildNotification(eventCode, id)) // if not provided with sendTransaction function, // resolve with transaction hash(or void) so dev can initiate transaction if (!sendTransaction) { return id } // get result and handle errors let hash try { hash = await sendTransaction() } catch (error) { type CatchError = { message: string stack: string } const { eventCode, errorMsg } = extractMessageFromError(error as CatchError) addNotification(buildNotification(eventCode, id)) console.error(errorMsg) return } // Remove preflight notification if a resolves to hash // and let the SDK take over removeNotification(id) if (hash) { return hash } return } const buildNotification = (eventCode: string, id: string): Notification => { return { eventCode, type: eventToType(eventCode), id, key: createKey(id, eventCode), message: createMessageText(eventCode), startTime: Date.now(), network: Object.keys(networkToChainId).find( key => networkToChainId[key] === state.get().chains[0].id ) as Network, autoDismiss: 0 } } const createKey = (id: string, eventCode: string): string => { return `${id}-${eventCode}` } const createId = (id: string): string => { return `${id}-preflight` } const createMessageText = (eventCode: string): string => { const notificationDefaultMessages = defaultCopy.notify const notificationMessageType = notificationDefaultMessages.transaction return notificationDefaultMessages.transaction[ eventCode as keyof typeof notificationMessageType ] } export function extractMessageFromError(error: { message: string stack: string }): { eventCode: string; errorMsg: string } { if (!error.stack || !error.message) { return { eventCode: 'txError', errorMsg: 'An unknown error occured' } } const message = error.stack || error.message if (message.includes('User denied transaction signature')) { return { eventCode: 'txSendFail', errorMsg: 'User denied transaction signature' } } if (message.includes('transaction underpriced')) { return { eventCode: 'txUnderpriced', errorMsg: 'Transaction is under priced' } } return { eventCode: 'txError', errorMsg: message } } const gasEstimates = async ( gasFunc: () => Promise<string>, gasPriceFunc: () => Promise<string> ) => { if (!gasFunc || !gasPriceFunc) { return Promise.resolve([]) } const gasProm = gasFunc() if (!gasProm.then) { throw new Error('The `estimateGas` function must return a Promise') } const gasPriceProm = gasPriceFunc() if (!gasPriceProm.then) { throw new Error('The `gasPrice` function must return a Promise') } return Promise.all([gasProm, gasPriceProm]) .then(([gasResult, gasPriceResult]) => { if (typeof gasResult !== 'string') { throw new Error( `The Promise returned from calling 'estimateGas' must resolve with a value of type 'string'. Received a value of: ${gasResult} with a type: ${typeof gasResult}` ) } if (typeof gasPriceResult !== 'string') { throw new Error( `The Promise returned from calling 'gasPrice' must resolve with a value of type 'string'. Received a value of: ${gasPriceResult} with a type: ${typeof gasPriceResult}` ) } return [new BigNumber(gasResult), new BigNumber(gasPriceResult)] }) .catch(error => { throw new Error(`There was an error getting gas estimates: ${error}`) }) }