@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -0,0 +1,266 @@
1
+ import { pick } from '@exodus/basic-utils'
2
+ import { createCurrency, makeSimulationAPICall } from '@exodus/web3-utils'
3
+
4
+ import { BLOWFISH_EVM_CHAINS, MAX_INT256_SOLIDITY } from './common.js'
5
+
6
+ const convertTransactionToApiPayload = (transactions, origin, blockNumber, recipientAddress) => {
7
+ const payload = {
8
+ txObjects: transactions.map((transaction) => ({
9
+ from: transaction.from,
10
+ to: transaction.to,
11
+ data: transaction.data,
12
+ value: transaction.value,
13
+ gas: transaction.gas,
14
+ gas_price: transaction.gasPrice,
15
+ })),
16
+ metadata: recipientAddress ? { origin, recipientAddress } : { origin },
17
+ userAccount: transactions[0].from,
18
+ }
19
+
20
+ if (blockNumber !== undefined) {
21
+ payload.blockNumber = blockNumber
22
+ }
23
+
24
+ return payload
25
+ }
26
+
27
+ const createBalanceAmount = (amount, { decimals, symbol }) => {
28
+ const beforeAmount = createCurrency({
29
+ amount: amount.before,
30
+ symbol,
31
+ denominator: decimals,
32
+ })
33
+ const afterAmount = createCurrency({
34
+ amount: amount.after,
35
+ symbol,
36
+ denominator: decimals,
37
+ })
38
+
39
+ return beforeAmount.sub(afterAmount)
40
+ }
41
+
42
+ const handleExpectedBalanceChange = (expectedBalanceChange, balanceChanges, { network }) => {
43
+ const { willApprove, willSend, willReceive } = balanceChanges
44
+ const { data, kind } = expectedBalanceChange.rawInfo
45
+
46
+ if (kind === 'FARCASTER_CHANGE_RECOVERY_ADDRESS' || kind === 'ERC20_PERMIT') {
47
+ return
48
+ }
49
+
50
+ const { asset: simulationAsset } = data
51
+
52
+ if (!simulationAsset) {
53
+ return
54
+ }
55
+
56
+ const isNftTransfer =
57
+ kind === 'ERC721_TRANSFER' || (kind === 'ERC1155_TRANSFER' && !simulationAsset.decimals)
58
+ const isNftTransferApproval =
59
+ kind === 'ERC721_APPROVAL' ||
60
+ (kind === 'ERC721_APPROVAL_FOR_ALL' && !simulationAsset.decimals) ||
61
+ (kind === 'ERC1155_APPROVAL_FOR_ALL' && !simulationAsset.decimals)
62
+ const decimals =
63
+ isNftTransfer || isNftTransferApproval || !Number.isFinite(simulationAsset.decimals)
64
+ ? 0
65
+ : simulationAsset.decimals
66
+ const simulationAssetInfo = pick(simulationAsset, [
67
+ 'address',
68
+ 'imageUrl',
69
+ 'name',
70
+ 'symbol',
71
+ 'verified',
72
+ ])
73
+
74
+ let symbol = simulationAsset.symbol || simulationAsset.name || 'NFT'
75
+ if (network === 'polygon' && simulationAsset.symbol === 'MATIC') {
76
+ symbol = 'POL'
77
+ }
78
+
79
+ if (isNftTransfer || isNftTransferApproval) {
80
+ simulationAssetInfo.imageUrl = data?.metadata?.rawImageUrl
81
+ }
82
+
83
+ if (
84
+ kind === 'ERC20_APPROVAL' ||
85
+ kind === 'ERC721_APPROVAL_FOR_ALL' ||
86
+ kind === 'ERC1155_APPROVAL_FOR_ALL' ||
87
+ kind === 'ERC721_APPROVAL'
88
+ ) {
89
+ const unitsToUse = symbol || simulationAsset.name || 'Units.'
90
+
91
+ const approvingBalance = createCurrency({
92
+ amount: data.amount.after,
93
+ symbol: unitsToUse,
94
+ denominator: decimals,
95
+ })
96
+
97
+ willApprove.push({
98
+ asset: simulationAssetInfo,
99
+ spender: data.spender.address,
100
+ unitName: unitsToUse,
101
+ balance: approvingBalance,
102
+ isMaxApproval: approvingBalance._number.gte(MAX_INT256_SOLIDITY),
103
+ })
104
+ return
105
+ }
106
+
107
+ const { amount } = data
108
+
109
+ const balance = createBalanceAmount(amount, { decimals, symbol })
110
+
111
+ if (balance.isZero) {
112
+ return
113
+ }
114
+
115
+ const balanceChange = {
116
+ asset: simulationAssetInfo,
117
+ balance,
118
+ }
119
+
120
+ if (isNftTransfer || isNftTransferApproval) {
121
+ const tokenId = data.tokenId
122
+ const compositeId = `${simulationAsset.address}/${tokenId}`
123
+ balanceChange.nft = {
124
+ id: `${network}:${compositeId}`,
125
+ compositeId,
126
+ title: simulationAsset.name,
127
+ }
128
+ }
129
+
130
+ if (balance.isNegative) {
131
+ balanceChange.balance = balanceChange.balance.negate()
132
+
133
+ willReceive.push(balanceChange)
134
+ } else {
135
+ willSend.push(balanceChange)
136
+ }
137
+ }
138
+
139
+ export const SimulationErrorMessages = {
140
+ APICallFailed: 0,
141
+ SimulationFailed: 1,
142
+ SimulationNotSupported: 2,
143
+ }
144
+
145
+ export const simulateTransactionsApi = async ({
146
+ transactions,
147
+ apiEndpoint,
148
+ origin,
149
+ blockNumber,
150
+ recipientAddress,
151
+ simulationResult,
152
+ headers,
153
+ asset,
154
+ }) => {
155
+ const successDefaultResult = {
156
+ kind: 'success',
157
+ value: undefined,
158
+ }
159
+ if (!BLOWFISH_EVM_CHAINS[asset.name]) {
160
+ return {
161
+ kind: 'error',
162
+ error: SimulationErrorMessages.SimulationNotSupported,
163
+ errorMessage: 'Simulation is not supported for this asset.',
164
+ }
165
+ }
166
+
167
+ const userAddress = transactions[0].from
168
+
169
+ const payload = convertTransactionToApiPayload(
170
+ transactions,
171
+ origin,
172
+ blockNumber,
173
+ recipientAddress
174
+ )
175
+
176
+ if (!Object.hasOwnProperty.call(BLOWFISH_EVM_CHAINS, asset.name)) {
177
+ return successDefaultResult
178
+ }
179
+
180
+ const { network, chain } = BLOWFISH_EVM_CHAINS[asset.name]
181
+
182
+ const response = await makeSimulationAPICall({
183
+ url: apiEndpoint,
184
+ chain,
185
+ network,
186
+ payload,
187
+ headers,
188
+ })
189
+ if (!response) {
190
+ return {
191
+ kind: 'error',
192
+ error: SimulationErrorMessages.APICallFailed,
193
+ errorMessage: 'Simulation API call failed.',
194
+ }
195
+ }
196
+
197
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
198
+ simulationResult.metadata.simulatedLocally = false
199
+
200
+ const { expectedStateChanges = {}, error } = response.simulationResults.aggregated
201
+
202
+ if (error) {
203
+ return {
204
+ kind: 'error',
205
+ error: SimulationErrorMessages.SimulationFailed,
206
+ errorMessage: error?.humanReadableError,
207
+ }
208
+ }
209
+
210
+ const { action } = response
211
+
212
+ if (action === 'BLOCK') {
213
+ simulationResult.warnings.push({
214
+ kind: 'MALICIOUS_ACTION',
215
+ severity: 'CRITICAL',
216
+ message: 'This dApp could be malicious. Do not proceed unless you are certain this is safe.',
217
+ })
218
+ } else if (action === 'WARN') {
219
+ simulationResult.warnings.push({
220
+ kind: 'SUSPICIOUS_ACTION',
221
+ severity: 'MEDIUM',
222
+ message: 'This transaction may be suspicious. Proceed with caution.',
223
+ })
224
+ }
225
+
226
+ const userAddressLower = userAddress.toLowerCase()
227
+ const recipientAddressLower =
228
+ recipientAddress && recipientAddress.toLowerCase() !== userAddressLower
229
+ ? recipientAddress.toLowerCase()
230
+ : undefined
231
+
232
+ const relevantExpectedChanges = Object.entries(expectedStateChanges).flatMap(
233
+ ([address, changes]) => {
234
+ const addressLower = address.toLowerCase()
235
+ const isUserOwned = addressLower === userAddressLower
236
+ if (!isUserOwned && addressLower !== recipientAddressLower) {
237
+ return []
238
+ }
239
+
240
+ return (changes ?? []).map((change) => ({ change, isUserOwned }))
241
+ }
242
+ )
243
+
244
+ const noExpectedStateChangesDetected = relevantExpectedChanges.length === 0
245
+
246
+ if (noExpectedStateChangesDetected) {
247
+ simulationResult.balanceChanges.willSend.push({
248
+ balance: createCurrency({
249
+ amount: 0,
250
+ symbol: asset.displayTicker,
251
+ denominator: asset.currency.defaultUnit.power,
252
+ }),
253
+ })
254
+
255
+ return successDefaultResult
256
+ }
257
+
258
+ relevantExpectedChanges.forEach(({ change, isUserOwned }) => {
259
+ const balanceChanges = isUserOwned
260
+ ? simulationResult.balanceChanges
261
+ : simulationResult.recipientBalanceChanges
262
+ handleExpectedBalanceChange(change, balanceChanges, { network })
263
+ })
264
+
265
+ return successDefaultResult
266
+ }
@@ -0,0 +1,16 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ import { createSimulateTransactions } from './create-simulate-transactions.js'
4
+
5
+ const simulateEVMTransactions = createSimulateTransactions({
6
+ apiEndpoint: 'https://simulation.a.exodus.io/simulate',
7
+ headers: {
8
+ 'X-Api-Version': '2023-06-05',
9
+ },
10
+ })
11
+
12
+ export function simulateTransactions({ asset, transactions, ...restParameters }) {
13
+ assert(asset, '"asset" should be passed.')
14
+
15
+ return simulateEVMTransactions({ asset, transactions, ...restParameters })
16
+ }
@@ -0,0 +1,52 @@
1
+ import SolidityContract from '@exodus/solidity-contract'
2
+
3
+ export function getDisplayDetails(willApprove) {
4
+ let title = 'Approve Transaction?'
5
+ const isApproveTransaction = willApprove.length
6
+
7
+ if (isApproveTransaction) {
8
+ const tokenName = willApprove[0].unitName
9
+ title = tokenName ? `Approve ${tokenName} Transactions?` : title
10
+ }
11
+
12
+ return { title }
13
+ }
14
+
15
+ export const isSimpleTransfer = (transaction) => {
16
+ return ['0x', null, undefined].includes(transaction.data)
17
+ }
18
+
19
+ export const getTxFeeDetails = (transaction) => {
20
+ const maxFeePerGas = transaction.maxFeePerGas
21
+ const gasPrice = transaction.gasPrice
22
+ const gasLimit = transaction.gas
23
+
24
+ return {
25
+ gasLimit,
26
+ maxFeePerGas,
27
+ gasPrice,
28
+ }
29
+ }
30
+
31
+ export function decodeRecipientAddresses(transaction) {
32
+ try {
33
+ if (!transaction.to) {
34
+ return []
35
+ }
36
+
37
+ if (isSimpleTransfer(transaction)) {
38
+ return [transaction.to]
39
+ }
40
+
41
+ const token = SolidityContract.erc20(transaction.to)
42
+ const { method, values } = token.decodeInput(transaction.data)
43
+
44
+ if (method === 'transfer') {
45
+ return [values[0]]
46
+ }
47
+
48
+ return []
49
+ } catch {
50
+ return []
51
+ }
52
+ }
@@ -0,0 +1,26 @@
1
+ import { estimateSimpleTransfer, NotSimpleTransferError } from './estimate-simple-transfer.js'
2
+
3
+ export const tryEstimatingChangesLocally = ({ asset, simulationResult, transaction }) => {
4
+ if (typeof transaction.from !== 'string' || typeof transaction.to !== 'string') {
5
+ return false
6
+ }
7
+
8
+ try {
9
+ const estimatedValue = estimateSimpleTransfer({ baseAsset: asset, transaction })
10
+ const actualTransferValue =
11
+ transaction.from.toLowerCase() === transaction.to.toLowerCase()
12
+ ? asset.currency.ZERO
13
+ : estimatedValue
14
+ simulationResult.balanceChanges.willSend.push({
15
+ balance: actualTransferValue,
16
+ })
17
+
18
+ return true
19
+ } catch (err) {
20
+ if (!(err instanceof NotSimpleTransferError)) {
21
+ // TODO: accept a logger dep and log the unexpected local simulation error.
22
+ }
23
+
24
+ return false
25
+ }
26
+ }
@@ -0,0 +1,43 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export const stakingApiFactory = ({
4
+ assetName,
5
+ currency,
6
+ assetClientInterface,
7
+ server, // coin node server
8
+ stakingConfiguration,
9
+ stakingDependencies,
10
+ createRpcRequestAccumulator,
11
+ }) => {
12
+ assert(assetName, '"assetName" is required')
13
+ assert(currency, '"currency" is required')
14
+ assert(assetClientInterface, '"assetClientInterface" is required')
15
+ assert(server, '"server" is required')
16
+ assert(stakingConfiguration, '"stakingConfiguration" is required')
17
+ assert(stakingDependencies, '"stakingDependencies" is required')
18
+
19
+ const { contracts, minAmount } = stakingConfiguration
20
+ const { stakingServerFactory, stakingServiceFactory } = stakingDependencies
21
+
22
+ const stakingServer = stakingServerFactory({
23
+ assetName,
24
+ currency,
25
+ server,
26
+ minAmount,
27
+ contracts,
28
+ createRpcRequestAccumulator,
29
+ })
30
+
31
+ const stakingService = stakingServiceFactory({
32
+ assetClientInterface,
33
+ server,
34
+ stakingServer,
35
+ })
36
+
37
+ return {
38
+ ...stakingService,
39
+ isStaking: async ({ isDelegating }) => isDelegating,
40
+ isUnstaking: async ({ isUndelegateInProgress }) => isUndelegateInProgress,
41
+ isUnstaked: async ({ canClaimUndelegateBalance }) => canClaimUndelegateBalance,
42
+ }
43
+ }
@@ -0,0 +1,33 @@
1
+ // Background-only caching. Callers outside the monitor always request fresh data.
2
+ // Keep this state in memory: restored accounts must be discovered again immediately.
3
+ export const createCachedStakingRead = ({
4
+ read,
5
+ interval = 15 * 60_000,
6
+ now = () => performance.now(),
7
+ }) => {
8
+ const entries = new Map()
9
+ return async (key, { useCache = false } = {}) => {
10
+ let entry = entries.get(key)
11
+ if (!entry) {
12
+ entry = { generation: 0 }
13
+ entries.set(key, entry)
14
+ }
15
+ if (useCache) {
16
+ if (entry.pending) return entry.pending
17
+ if (entry.deadline > now()) return entry.value
18
+ }
19
+ const generation = ++entry.generation
20
+ const pending = Promise.resolve().then(() => read(key))
21
+ entry.pending = pending
22
+ try {
23
+ const value = await pending
24
+ if (entry.generation === generation) {
25
+ entry.value = value
26
+ entry.deadline = now() + interval
27
+ }
28
+ return value
29
+ } finally {
30
+ if (entry.generation === generation) entry.pending = undefined
31
+ }
32
+ }
33
+ }