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 { 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, gasPriceFunc: () => Promise ) => { 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}`) }) }