Skip to content

Commit bdbd075

Browse files
authored
[core-v2.4.0-alpha.8, react-v2.2.3-alpha.5, vue-v2.1.3-alpha.6] : Feature - Preflight notification handling (#1138)
* Preflight file setup needs cleanup * Making progress * Added validation * Update name of preflightNotification(s) to be plural * Update return of sendTransaction to include void
1 parent 3058c0f commit bdbd075

File tree

12 files changed

+384
-13
lines changed

12 files changed

+384
-13
lines changed

packages/core/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,63 @@ setTimeout(
637637
)
638638
```
639639

640+
**`preflightNotifications`**
641+
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.
642+
643+
Preflight event types include
644+
- `txRequest` : Alert user there is a transaction request awaiting confirmation by their wallet
645+
- `txAwaitingApproval` : A previous transaction is awaiting confirmation
646+
- `txConfirmReminder` : Reminder to confirm a transaction to continue - configurable with the `txApproveReminderTimeout` property; defaults to 15 seconds
647+
- `nsfFail` : The user has insufficient funds for transaction (requires `gasPrice`, `estimateGas`, `balance`, `txDetails.value`)
648+
- `txError` : General transaction error (requires `sendTransaction`)
649+
- `txSendFail` : The user rejected the transaction (requires `sendTransaction`)
650+
- `txUnderpriced` : The gas price for the transaction is too low (requires `sendTransaction`)
651+
652+
```typescript
653+
interface PreflightNotificationsOptions {
654+
sendTransaction?: () => Promise<string | void>
655+
estimateGas?: () => Promise<string>
656+
gasPrice?: () => Promise<string>
657+
balance?: string | number
658+
txDetails?: {
659+
value: string | number
660+
to?: string
661+
from?: string
662+
}
663+
txApproveReminderTimeout?: number // defaults to 15 seconds if not specified
664+
}
665+
```
666+
667+
```typescript
668+
const balanceValue = Object.values(balance)[0]
669+
const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
670+
671+
const signer = ethersProvider.getSigner()
672+
const txDetails = {
673+
to: toAddress,
674+
value: 100000000000000
675+
}
676+
677+
const sendTransaction = () => {
678+
return signer.sendTransaction(txDetails).then(tx => tx.hash)
679+
}
680+
681+
const gasPrice = () =>
682+
ethersProvider.getGasPrice().then(res => res.toString())
683+
684+
const estimateGas = () => {
685+
return ethersProvider.estimateGas(txDetails).then(res => res.toString())
686+
}
687+
const transactionHash = await onboard.state.actions.preflightNotifications({
688+
sendTransaction,
689+
gasPrice,
690+
estimateGas,
691+
balance: balanceValue,
692+
txDetails: txDetails
693+
})
694+
console.log(transactionHash)
695+
```
696+
640697
**`updateAccountCenter`**
641698
If you need to update your Account Center configuration after initialization, you can call the `updateAccountCenter` function with the new configuration
642699

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@web3-onboard/core",
3-
"version": "2.4.0-alpha.9",
4-
"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.",
3+
"version": "2.4.0-alpha.10",
4+
"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.",
55
"keywords": [
66
"Ethereum",
77
"Web3",

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from './store/actions'
2727

2828
import updateBalances from './update-balances'
29+
import { preflightNotifications } from './preflight-notifications'
2930

3031
const API = {
3132
connectWallet,
@@ -39,6 +40,7 @@ const API = {
3940
setLocale,
4041
updateNotify,
4142
customNotification,
43+
preflightNotifications,
4244
updateBalances,
4345
updateAccountCenter,
4446
setPrimaryWallet

packages/core/src/notify.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ export function eventToType(eventCode: string | undefined): NotificationType {
139139
case 'txRepeat':
140140
case 'txAwaitingApproval':
141141
case 'txConfirmReminder':
142-
case 'txStallPending':
143-
case 'txStallConfirmed':
144142
case 'txStuck':
145143
return 'hint'
146144
case 'txError':
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import BigNumber from 'bignumber.js'
2+
import { nanoid } from 'nanoid'
3+
import defaultCopy from './i18n/en.json'
4+
import type { Network } from 'bnc-sdk'
5+
6+
import type { Notification, PreflightNotificationsOptions } from './types'
7+
import { addNotification, removeNotification } from './store/actions'
8+
import { state } from './store'
9+
import { eventToType } from './notify'
10+
import { networkToChainId } from './utils'
11+
import { validatePreflightNotifications } from './validation'
12+
13+
let notificationsArr: Notification[]
14+
state.select('notifications').subscribe(notifications => {
15+
notificationsArr = notifications
16+
})
17+
18+
export async function preflightNotifications(
19+
options: PreflightNotificationsOptions
20+
): Promise<string | void> {
21+
22+
23+
const invalid = validatePreflightNotifications(options)
24+
25+
if (invalid) {
26+
throw invalid
27+
}
28+
29+
const {
30+
sendTransaction,
31+
estimateGas,
32+
gasPrice,
33+
balance,
34+
txDetails,
35+
txApproveReminderTimeout
36+
} = options
37+
38+
// Check for reminder timeout and confirm its greater than 3 seconds
39+
const reminderTimeout: number =
40+
txApproveReminderTimeout && txApproveReminderTimeout > 3000
41+
? txApproveReminderTimeout
42+
: 15000
43+
44+
// if `balance` or `estimateGas` or `gasPrice` is not provided,
45+
// then sufficient funds check is disabled
46+
// if `txDetails` is not provided,
47+
// then duplicate transaction check is disabled
48+
// if dev doesn't want notify to initiate the transaction
49+
// and `sendTransaction` is not provided, then transaction
50+
// rejected notification is disabled
51+
// to disable hints for `txAwaitingApproval`, `txConfirmReminder`
52+
// or any other notification, then return false from listener functions
53+
54+
const [gas, price] = await gasEstimates(estimateGas, gasPrice)
55+
const id = createId(nanoid())
56+
const value = new BigNumber((txDetails && txDetails.value) || 0)
57+
58+
// check sufficient balance if required parameters are available
59+
if (balance && gas && price) {
60+
const transactionCost = gas.times(price).plus(value)
61+
62+
// if transaction cost is greater than the current balance
63+
if (transactionCost.gt(new BigNumber(balance))) {
64+
const eventCode = 'nsfFail'
65+
66+
const newNotification = buildNotification(eventCode, id)
67+
addNotification(newNotification)
68+
}
69+
}
70+
71+
// check previous transactions awaiting approval
72+
const txRequested = notificationsArr.find(tx => tx.eventCode === 'txRequest')
73+
74+
if (txRequested) {
75+
const eventCode = 'txAwaitingApproval'
76+
77+
const newNotification = buildNotification(eventCode, txRequested.id)
78+
addNotification(newNotification)
79+
}
80+
81+
// confirm reminder timeout defaults to 20 seconds
82+
setTimeout(() => {
83+
const awaitingApproval = notificationsArr.find(
84+
tx => tx.id === id && tx.eventCode === 'txRequest'
85+
)
86+
87+
if (awaitingApproval) {
88+
const eventCode = 'txConfirmReminder'
89+
90+
const newNotification = buildNotification(eventCode, awaitingApproval.id)
91+
addNotification(newNotification)
92+
}
93+
}, reminderTimeout)
94+
95+
const eventCode = 'txRequest'
96+
const newNotification = buildNotification(eventCode, id)
97+
addNotification(newNotification)
98+
99+
// if not provided with sendTransaction function,
100+
// resolve with transaction hash(or void) so dev can initiate transaction
101+
if (!sendTransaction) {
102+
return id
103+
}
104+
// get result and handle errors
105+
let hash
106+
try {
107+
hash = await sendTransaction()
108+
} catch (error) {
109+
type CatchError = {
110+
message: string
111+
stack: string
112+
}
113+
const { eventCode, errorMsg } = extractMessageFromError(error as CatchError)
114+
115+
const newNotification = buildNotification(eventCode, id)
116+
addNotification(newNotification)
117+
console.error(errorMsg)
118+
return
119+
}
120+
121+
// Remove preflight notification if a resolves to hash
122+
// and let the SDK take over
123+
removeNotification(id)
124+
if (hash) {
125+
return hash
126+
}
127+
return
128+
}
129+
130+
const buildNotification = (eventCode: string, id: string): Notification => {
131+
return {
132+
eventCode,
133+
type: eventToType(eventCode),
134+
id,
135+
key: createKey(id, eventCode),
136+
message: createMessageText(eventCode),
137+
startTime: Date.now(),
138+
network: Object.keys(networkToChainId).find(
139+
key => networkToChainId[key] === state.get().chains[0].id
140+
) as Network,
141+
autoDismiss: 0
142+
}
143+
}
144+
145+
const createKey = (id: string, eventCode: string): string => {
146+
return `${id}-${eventCode}`
147+
}
148+
149+
const createId = (id: string): string => {
150+
return `${id}-preflight`
151+
}
152+
153+
const createMessageText = (eventCode: string): string => {
154+
const notificationDefaultMessages = defaultCopy.notify
155+
156+
const notificationMessageType = notificationDefaultMessages.transaction
157+
158+
return notificationDefaultMessages.transaction[
159+
eventCode as keyof typeof notificationMessageType
160+
]
161+
}
162+
163+
export function extractMessageFromError(error: {
164+
message: string
165+
stack: string
166+
}): { eventCode: string; errorMsg: string } {
167+
if (!error.stack || !error.message) {
168+
return {
169+
eventCode: 'txError',
170+
errorMsg: 'An unknown error occured'
171+
}
172+
}
173+
174+
const message = error.stack || error.message
175+
176+
if (message.includes('User denied transaction signature')) {
177+
return {
178+
eventCode: 'txSendFail',
179+
errorMsg: 'User denied transaction signature'
180+
}
181+
}
182+
183+
if (message.includes('transaction underpriced')) {
184+
return {
185+
eventCode: 'txUnderpriced',
186+
errorMsg: 'Transaction is under priced'
187+
}
188+
}
189+
190+
return {
191+
eventCode: 'txError',
192+
errorMsg: message
193+
}
194+
}
195+
196+
const gasEstimates = async (
197+
gasFunc: () => Promise<string>,
198+
gasPriceFunc: () => Promise<string>
199+
) => {
200+
if (!gasFunc || !gasPriceFunc) {
201+
return Promise.resolve([])
202+
}
203+
204+
const gasProm = gasFunc()
205+
if (!gasProm.then) {
206+
throw new Error('The `estimateGas` function must return a Promise')
207+
}
208+
209+
const gasPriceProm = gasPriceFunc()
210+
if (!gasPriceProm.then) {
211+
throw new Error('The `gasPrice` function must return a Promise')
212+
}
213+
214+
return Promise.all([gasProm, gasPriceProm])
215+
.then(([gasResult, gasPriceResult]) => {
216+
if (typeof gasResult !== 'string') {
217+
throw new Error(
218+
`The Promise returned from calling 'estimateGas' must resolve with a value of type 'string'. Received a value of: ${gasResult} with a type: ${typeof gasResult}`
219+
)
220+
}
221+
222+
if (typeof gasPriceResult !== 'string') {
223+
throw new Error(
224+
`The Promise returned from calling 'gasPrice' must resolve with a value of type 'string'. Received a value of: ${gasPriceResult} with a type: ${typeof gasPriceResult}`
225+
)
226+
}
227+
228+
return [new BigNumber(gasResult), new BigNumber(gasPriceResult)]
229+
})
230+
.catch(error => {
231+
throw new Error(`There was an error getting gas estimates: ${error}`)
232+
})
233+
}

packages/core/src/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ export type NotificationPosition = CommonPositions
138138
export type AccountCenter = {
139139
enabled: boolean
140140
position?: AccountCenterPosition
141-
containerElement?: string
142141
expanded?: boolean
143142
minimal?: boolean
143+
containerElement: string
144144
}
145145

146146
export type AccountCenterOptions = {
@@ -208,6 +208,19 @@ export interface UpdateNotification {
208208
}
209209
}
210210

211+
export interface PreflightNotificationsOptions {
212+
sendTransaction?: () => Promise<string | void>
213+
estimateGas?: () => Promise<string>
214+
gasPrice?: () => Promise<string>
215+
balance?: string | number
216+
txDetails?: {
217+
value: string | number
218+
to?: string
219+
from?: string
220+
}
221+
txApproveReminderTimeout?: number
222+
}
223+
211224
// ==== ACTIONS ==== //
212225
export type Action =
213226
| AddChainsAction

0 commit comments

Comments
 (0)