Skip to content

Commit df692c5

Browse files
[core:2.7.0-alpha.2] [ledger:2.1.8-alpha.1] - [feature] - Replacement Transactions (#1195)
* Initial implementation of Gas module * Working prototype * Add docs * Modify API * Add override poll example and comment * Remove gas module from demo * Update usage in core * Update gas API * Revert changes to fix types * Revert changes to types that are in another pr * Fix docs * Revert in favor of PR * Fix Configuration types * Initial styles * Style fixes * Modifies results to be an array rather than a map * Increments versions * Increments core version * Update yarn.lock for joi dep * Fix ledger error handling * Update demo * Add missing txStuck msg * Handle replacement txs * Remove test gas fee data * Add replacement option to docs * Fix docs * Code organisation, refactor, error handling * Adds txReplaceError message * Fixes error notification * Add more detail in docs * Additional documentation * Update packages/core/README.md Co-authored-by: Adam Carpenter <adamcarpenter86@gmail.com> Co-authored-by: Adam Carpenter <adamcarpenter86@gmail.com>
1 parent 6e0d1a9 commit df692c5

File tree

17 files changed

+359
-38
lines changed

17 files changed

+359
-38
lines changed

packages/core/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,16 @@ The `transactionHandler` can react off any property of the Ethereum TransactionD
145145
- `Notification.link` - add link to the transaction hash. For instance, a link to the transaction on etherscan
146146
- `Notification.onClick()` - onClick handler for when user clicks the notification element
147147
148-
Notify can also be styled by using the CSS variables found below. These are setup to allow maximum customization with base styling variables setting the global theme (i.e. `--onboard-grey-600`) along with more precise component level styling variables available (`--notify-onboard-grey-600`) with the latter taking precedent if defined
148+
Notify can also be styled by using the CSS variables found below. These are setup to allow maximum customization with base styling variables setting the global theme (i.e. `--onboard-grey-600`) along with more precise component level styling variables available (`--notify-onboard-grey-600`) with the latter taking precedent if defined.
149+
150+
Under the following conditions, when a transaction notification is hovered, a dropdown will appear, allowing the user to replace (speedup or cancel) their transaction:
151+
152+
- The [gas](../gas/) module has been passed in to the Web3 Onboard initialization
153+
- The transaction was initiated on the networks that the gas module can get estimations for (currently Ethereum and Polygon mainnet)
154+
- The transaction has a pending status (eventCode: 'txPool')
155+
- The transaction was initiated by a hardware wallet (possibly other wallet types in future as they recognize the `nonce` field)
156+
157+
The replacement gas values can be customized from the defaults by using the `replacement` parameter in the Notify options.
149158
150159
If notifications are enabled the notifications can be handled through onboard app state as seen below.
151160
@@ -176,6 +185,14 @@ export type Notify = {
176185
event: EthereumTransactionData
177186
) => TransactionHandlerReturn
178187
position: CommonPositions
188+
replacement?: {
189+
gasPriceProbability?: {
190+
// define the gas price used for speedup based on the probability of getting in to the next block. Default 80
191+
speedup?: number
192+
// define the gas price used for cancel based on the probability of getting in to the next block. Default 95
193+
cancel?: number
194+
}
195+
}
179196
}
180197

181198
export type CommonPositions =

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@web3-onboard/core",
3-
"version": "2.7.0-alpha.1",
3+
"version": "2.7.0-alpha.2",
44
"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",

packages/core/src/constants.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ export const APP_INITIAL_STATE: AppState = {
1515
notify: {
1616
enabled: true,
1717
transactionHandler: () => {},
18-
position: 'topRight'
18+
position: 'topRight',
19+
replacement: {
20+
gasPriceProbability: {
21+
speedup: 80,
22+
cancel: 95
23+
}
24+
}
1925
},
2026
notifications: [],
2127
locale: '',

packages/core/src/i18n/en.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@
9191
"txCancel": "Your transaction is being canceled",
9292
"txFailed": "Your transaction has failed",
9393
"txConfirmed": "Your transaction has succeeded",
94-
"txError": "Oops something went wrong, please try again"
94+
"txError": "Oops something went wrong, please try again",
95+
"txReplaceError": "There was an error replacing your transaction, please try again"
9596
},
9697
"watched": {
9798
"txPool": "Your account is {verb} {formattedValue} {asset} {preposition} {counterpartyShortened}",
9899
"txSpeedUp": "Transaction for {formattedValue} {asset} {preposition} {counterpartyShortened} has been sped up",
99100
"txCancel": "Transaction for {formattedValue} {asset} {preposition} {counterpartyShortened} has been canceled",
100101
"txConfirmed": "Your account successfully {verb} {formattedValue} {asset} {preposition} {counterpartyShortened}",
101-
"txFailed": "Your account failed to {verb} {formattedValue} {asset} {preposition} {counterpartyShortened}"
102+
"txFailed": "Your account failed to {verb} {formattedValue} {asset} {preposition} {counterpartyShortened}",
103+
"txStuck": "Your transaction is stuck due to a nonce gap"
102104
},
103105
"time": {
104106
"minutes": "min",

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ function mountApp() {
311311
--border-radius-2: 20px;
312312
--border-radius-3: 16px;
313313
--border-radius-4: 12px;
314+
--border-radius-5: 8px;
314315
315316
/* SHADOWS */
316317
--shadow-0: none;

packages/core/src/notify.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { validateTransactionHandlerReturn } from './validation'
1414
import { state } from './store'
1515
import { addNotification } from './store/actions'
1616
import updateBalances from './update-balances'
17+
import { updateTransaction } from './streams'
1718

1819
export function handleTransactionUpdates(
1920
transaction: EthereumTransactionData
@@ -32,6 +33,7 @@ export function handleTransactionUpdates(
3233
const notification = transactionEventToNotification(transaction, customized)
3334

3435
addNotification(notification)
36+
updateTransaction(transaction)
3537
}
3638

3739
export function transactionEventToNotification(
@@ -164,4 +166,3 @@ export function typeToDismissTimeout(type: string): number {
164166
return 0
165167
}
166168
}
167-

packages/core/src/replacement.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { EthereumTransactionData, Network } from 'bnc-sdk'
2+
import { BigNumber } from 'ethers'
3+
import { configuration } from './configuration'
4+
import { state } from './store'
5+
import type { WalletState } from './types'
6+
import { gweiToWeiHex, networkToChainId, toHexString } from './utils'
7+
8+
const ACTIONABLE_EVENT_CODES: string[] = ['txPool']
9+
const VALID_GAS_NETWORKS: Network[] = ['main', 'matic-main']
10+
11+
const WALLETS_SUPPORT_REPLACEMENT: WalletState['label'][] = [
12+
'Ledger',
13+
'Trezor',
14+
'Keystone',
15+
'Keepkey'
16+
]
17+
18+
export const actionableEventCode = (eventCode: string): boolean =>
19+
ACTIONABLE_EVENT_CODES.includes(eventCode)
20+
21+
export const validGasNetwork = (network: Network): boolean =>
22+
VALID_GAS_NETWORKS.includes(network)
23+
24+
export const walletSupportsReplacement = (wallet: WalletState): boolean =>
25+
wallet && WALLETS_SUPPORT_REPLACEMENT.includes(wallet.label)
26+
27+
export async function replaceTransaction({
28+
type,
29+
wallet,
30+
transaction
31+
}: {
32+
type: 'speedup' | 'cancel'
33+
wallet: WalletState
34+
transaction: EthereumTransactionData
35+
}): Promise<unknown> {
36+
const { from, input, value, to, nonce, gas: gasLimit, network } = transaction
37+
38+
const chainId = networkToChainId[network]
39+
40+
const { gasPriceProbability } = state.get().notify.replacement
41+
const { gas, apiKey } = configuration
42+
43+
// get gas price
44+
const [gasResult] = await gas.get({
45+
chains: [networkToChainId[network]],
46+
endpoint: 'blockPrices',
47+
apiKey
48+
})
49+
50+
const { maxFeePerGas, maxPriorityFeePerGas } =
51+
gasResult.blockPrices[0].estimatedPrices.find(
52+
({ confidence }) =>
53+
confidence ===
54+
(type === 'speedup'
55+
? gasPriceProbability.speedup
56+
: gasPriceProbability.cancel)
57+
)
58+
59+
const maxFeePerGasWeiHex = gweiToWeiHex(maxFeePerGas)
60+
const maxPriorityFeePerGasWeiHex = gweiToWeiHex(maxPriorityFeePerGas)
61+
62+
// Some wallets do not like empty '0x' val
63+
const dataObj = input === '0x' ? {} : { data: input }
64+
65+
return wallet.provider.request({
66+
method: 'eth_sendTransaction',
67+
params: [
68+
{
69+
type: '0x2',
70+
from,
71+
to: type === 'cancel' ? from : to,
72+
chainId: parseInt(chainId),
73+
value: `${BigNumber.from(value).toHexString()}`,
74+
nonce: toHexString(nonce),
75+
gasLimit: toHexString(gasLimit),
76+
maxFeePerGas: maxFeePerGasWeiHex,
77+
maxPriorityFeePerGas: maxPriorityFeePerGasWeiHex,
78+
...dataObj
79+
}
80+
]
81+
})
82+
}

packages/core/src/streams.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { resetStore } from './store/actions'
1313
import { state } from './store'
1414

1515
import type { WalletState, ConnectOptions } from './types'
16+
import type { EthereumTransactionData } from 'bnc-sdk'
1617

1718
export const reset$ = new Subject<void>()
1819
export const disconnectWallet$ = new Subject<WalletState['label']>()
@@ -41,6 +42,30 @@ reset$.pipe(withLatestFrom(wallets$), pluck('1')).subscribe(wallets => {
4142
resetStore()
4243
})
4344

45+
// keep transactions for all notifications for replacement actions
46+
export const transactions$ = new BehaviorSubject<EthereumTransactionData[]>([])
47+
48+
export function updateTransaction(tx: EthereumTransactionData): void {
49+
const currentTransactions = transactions$.getValue()
50+
51+
const txIndex = currentTransactions.findIndex(({ hash }) => hash === tx.hash)
52+
53+
if (txIndex !== -1) {
54+
const updatedTransactions = currentTransactions.map((val, i) =>
55+
i === txIndex ? tx : val
56+
)
57+
58+
transactions$.next(updatedTransactions)
59+
} else {
60+
transactions$.next([...currentTransactions, tx])
61+
}
62+
}
63+
64+
export function removeTransaction(hash: string): void {
65+
const currentTransactions = transactions$.getValue()
66+
transactions$.next(currentTransactions.filter(tx => tx.hash !== hash))
67+
}
68+
4469
export const onMount$ = defer(() => {
4570
const subject = new Subject<void>()
4671
onMount(() => {

packages/core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ export type Notify = {
185185
* and notifications are enabled (enabled by default with API key)
186186
*/
187187
position?: NotificationPosition
188+
replacement?: {
189+
gasPriceProbability?: {
190+
speedup?: number
191+
cancel?: number
192+
}
193+
}
188194
}
189195

190196
export type NotifyOptions = {

packages/core/src/utils.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,19 @@ export async function copyWalletAddress(text: string): Promise<void> {
9393
}
9494
}
9595

96-
export const toHexString = (val: number | string): string => typeof val === 'number' ? `0x${val.toString(16)}` : val
96+
export const toHexString = (val: number | string): string =>
97+
typeof val === 'number' ? `0x${val.toString(16)}` : val
9798

98-
export function chainIdToHex(chains : Chain[] | ChainWithDecimalId[] ):
99-
Chain[] {
100-
return chains.map(({ id, ...rest }) => {
99+
export function chainIdToHex(chains: Chain[] | ChainWithDecimalId[]): Chain[] {
100+
return chains.map(({ id, ...rest }) => {
101101
const hexId = toHexString(id)
102102
return { id: hexId, ...rest }
103-
})
104-
}
103+
})
104+
}
105+
106+
export function gweiToWeiHex(gwei: number): string {
107+
return `0x${(gwei * 1e9).toString(16)}`
108+
}
105109

106110
export const chainIdToLabel: Record<string, string> = {
107111
'0x1': 'Ethereum',

0 commit comments

Comments
 (0)