Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core-v2.4.0-alpha.8, react-v2.2.3-alpha.5, vue-v2.1.3-alpha.6] : Feature - Preflight notification handling #1138

Merged
merged 17 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,63 @@ setTimeout(
)
```

**`preflightNotifications`**
Notify can be used to deliver standard notifications along with preflight information by passing a `PreflightNotificationsOptions` object to the `preflightNotifications` action. This will return a a promise that resolves to the transaction hash (if `sendTransaction` resolves the transaction hash and is successful), the internal notification id (if no `sendTransaction` function is provided) or return nothing if an error occurs or `sendTransaction` is not provided or doesn't resolve to a string.

Preflight event types include
- `txRequest` : Alert user there is a transaction request awaiting confirmation by their wallet
- `txAwaitingApproval` : A previous transaction is awaiting confirmation
- `txConfirmReminder` : Reminder to confirm a transaction to continue - configurable with the `txApproveReminderTimeout` property; defaults to 15 seconds
- `nsfFail` : The user has insufficient funds for transaction (requires `gasPrice`, `estimateGas`, `balance`, `txDetails.value`)
- `txError` : General transaction error (requires `sendTransaction`)
- `txSendFail` : The user rejected the transaction (requires `sendTransaction`)
- `txUnderpriced` : The gas price for the transaction is too low (requires `sendTransaction`)

```typescript
interface PreflightNotificationsOptions {
sendTransaction?: () => Promise<string | void>
estimateGas?: () => Promise<string>
gasPrice?: () => Promise<string>
balance?: string | number
txDetails?: {
value: string | number
to?: string
from?: string
}
txApproveReminderTimeout?: number // defaults to 15 seconds if not specified
}
```

```typescript
const balanceValue = Object.values(balance)[0]
const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')

const signer = ethersProvider.getSigner()
const txDetails = {
to: toAddress,
value: 100000000000000
}

const sendTransaction = () => {
return signer.sendTransaction(txDetails).then(tx => tx.hash)
}

const gasPrice = () =>
ethersProvider.getGasPrice().then(res => res.toString())

const estimateGas = () => {
return ethersProvider.estimateGas(txDetails).then(res => res.toString())
}
const transactionHash = await onboard.state.actions.preflightNotifications({
sendTransaction,
gasPrice,
estimateGas,
balance: balanceValue,
txDetails: txDetails
})
console.log(transactionHash)
```

**`updateAccountCenter`**
If you need to update your Account Center configuration after initialization, you can call the `updateAccountCenter` function with the new configuration

Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@web3-onboard/core",
"version": "2.4.0-alpha.9",
"description": "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, multi-chain and multi-account support, reactive wallet state subscriptions and real-time transaction state change notifications.",
"version": "2.4.0-alpha.10",
"description": "Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardized spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, multi-chain and multi-account support, reactive wallet state subscriptions and real-time transaction state change notifications.",
"keywords": [
"Ethereum",
"Web3",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from './store/actions'

import updateBalances from './update-balances'
import { preflightNotifications } from './preflight-notifications'

const API = {
connectWallet,
Expand All @@ -39,6 +40,7 @@ const API = {
setLocale,
updateNotify,
customNotification,
preflightNotifications,
updateBalances,
updateAccountCenter,
setPrimaryWallet
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ export function eventToType(eventCode: string | undefined): NotificationType {
case 'txRepeat':
case 'txAwaitingApproval':
case 'txConfirmReminder':
case 'txStallPending':
case 'txStallConfirmed':
case 'txStuck':
return 'hint'
case 'txError':
Expand Down
233 changes: 233 additions & 0 deletions packages/core/src/preflight-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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'
import { addNotification, removeNotification } from './store/actions'
import { state } from './store'
import { eventToType } from './notify'
import { networkToChainId } from './utils'
import { validatePreflightNotifications } from './validation'

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'

const newNotification = buildNotification(eventCode, id)
addNotification(newNotification)
}
}

// 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'
const newNotification = buildNotification(eventCode, id)
addNotification(newNotification)

// 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)

const newNotification = buildNotification(eventCode, id)
addNotification(newNotification)
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}`)
})
}
15 changes: 14 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ export type NotificationPosition = CommonPositions
export type AccountCenter = {
enabled: boolean
position?: AccountCenterPosition
containerElement?: string
expanded?: boolean
minimal?: boolean
containerElement: string
}

export type AccountCenterOptions = {
Expand Down Expand Up @@ -208,6 +208,19 @@ export interface UpdateNotification {
}
}

export interface PreflightNotificationsOptions {
sendTransaction?: () => Promise<string | void>
estimateGas?: () => Promise<string>
gasPrice?: () => Promise<string>
balance?: string | number
txDetails?: {
value: string | number
to?: string
from?: string
}
txApproveReminderTimeout?: number
}

// ==== ACTIONS ==== //
export type Action =
| AddChainsAction
Expand Down
Loading