Skip to content

Commit

Permalink
refactor: track txs (#3185)
Browse files Browse the repository at this point in the history
* feat: track approval txs

* refactor: update transactions

* chore: add ms to deps

* test: rm stale test

* fix: comment usage of trade for optimized trade
  • Loading branch information
zzmp authored Jan 25, 2022
1 parent 1f89a46 commit c7633d9
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 231 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
"graphql-request": "^3.4.0",
"inter-ui": "^3.13.1",
"jest-styled-components": "^7.0.5",
"ms.macro": "^2.0.0",
"polyfill-object.fromentries": "^1.0.1",
"prettier": "^2.2.1",
"qs": "^6.9.4",
Expand Down Expand Up @@ -187,6 +186,7 @@
"jotai": "^1.3.7",
"jsbi": "^3.1.4",
"make-plural": "^7.0.0",
"ms.macro": "^2.0.0",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"node-vibrant": "^3.2.1-alpha.1",
Expand Down
64 changes: 2 additions & 62 deletions src/lib/components/Swap/Status.fixture.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,8 @@
import { tokens } from '@uniswap/default-token-list'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import JSBI from 'jsbi'
import { swapTransactionAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { useSelect } from 'react-cosmos/fixture'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant'

import { Modal } from '../Dialog'
import { StatusDialog } from './Status'

const ETH = nativeOnChain(SupportedChainId.MAINNET)
const UNI = (function () {
const token = tokens.find(({ symbol }) => symbol === 'UNI')
invariant(token)
return new WrappedTokenInfo(token)
})()

function Fixture() {
const setTransaction = useUpdateAtom(swapTransactionAtom)

const [state] = useSelect('state', {
options: ['PENDING', 'ERROR', 'SUCCESS'],
})
useEffect(() => {
setTransaction({
input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)),
output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)),
receipt: '',
timestamp: Date.now(),
})
}, [setTransaction])
useEffect(() => {
switch (state) {
case 'PENDING':
setTransaction({
input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)),
output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)),
receipt: '',
timestamp: Date.now(),
})
break
case 'ERROR':
setTransaction((tx) => {
invariant(tx)
tx.status = new Error(
'Swap failed: Unknown error: "Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent pulvinar, risus eu pretium condimentum, tellus dui fermentum turpis, id gravida metus justo ac lorem. Etiam vitae dapibus eros, nec elementum ipsum. Duis condimentum, felis vel tempor ultricies, eros diam tempus odio, at tempor urna odio id massa. Aliquam laoreet turpis justo, auctor accumsan est pellentesque at. Integer et dolor feugiat, sodales tortor non, cursus augue. Phasellus id suscipit justo, in ultricies tortor. Aenean libero nibh, egestas sit amet vehicula sit amet, tempor ac ligula. Cras at tempor lectus. Mauris sollicitudin est velit, nec consectetur lorem dapibus ut. Praesent magna ex, faucibus ac fermentum malesuada, molestie at ex. Phasellus bibendum lorem nec dolor dignissim eleifend. Nam dignissim varius velit, at volutpat justo pretium id."'
)
tx.elapsedMs = Date.now() - tx.timestamp
})
break
case 'SUCCESS':
setTransaction((tx) => {
invariant(tx)
tx.status = true
tx.elapsedMs = Date.now() - tx.timestamp
})
break
}
}, [setTransaction, state])
return <StatusDialog onClose={() => void 0} />
return null
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
}

export default (
Expand Down
52 changes: 26 additions & 26 deletions src/lib/components/Swap/Status/StatusDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { SwapTransaction, swapTransactionAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react'

import ActionButton from '../../ActionButton'
import Column from '../../Column'
import Row from '../../Row'
import Summary from '../Summary'

const errorMessage = (
<Trans>
Expand All @@ -24,17 +22,17 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`

function ElapsedTime({ tx }: { tx: SwapTransaction | null }) {
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(
() => {
if (tx?.elapsedMs) {
setElapsedMs(tx.elapsedMs)
} else if (tx?.timestamp) {
setElapsedMs(Date.now() - tx.timestamp)
if (tx.info.response.timestamp) {
setElapsedMs(tx.info.response.timestamp - tx.addedTime)
} else {
setElapsedMs(Date.now() - tx.addedTime)
}
},
elapsedMs === tx?.elapsedMs ? null : 1000
elapsedMs === tx.info.response.timestamp ? null : 1000
)
const toElapsedTime = useCallback((ms: number) => {
let sec = Math.floor(ms / 1000)
Expand Down Expand Up @@ -63,22 +61,25 @@ const EtherscanA = styled.a`
text-decoration: none;
`

interface TransactionStatusProps extends StatusProps {
tx: SwapTransaction | null
interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
onClose: () => void
}

function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
const Icon = useMemo(() => {
return tx?.status ? CheckCircle : Spinner
}, [tx?.status])
return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status])
const heading = useMemo(() => {
return tx?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx?.status])
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx?.status && 'success'}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
{tx ? <Summary input={tx.input} output={tx.output} /> : <div style={{ height: '1.25em' }} />}
{/* TODO(zzmp): Display actual transaction.
<Summary input={tx.info.inputCurrency} output={tx.info.outputCurrency} />
*/}
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
Expand All @@ -95,15 +96,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
)
}

interface StatusProps {
onClose: () => void
}

export default function TransactionStatusDialog({ onClose }: StatusProps) {
const tx = useAtomValue(swapTransactionAtom)

return tx?.status instanceof Error ? (
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status === 0 ? (
<ErrorDialog
header={errorMessage}
error={new Error('TODO(zzmp)')}
action={<Trans>Dismiss</Trans>}
onAction={onClose}
/>
) : (
<TransactionStatus tx={tx} onClose={onClose} />
)
Expand Down
27 changes: 19 additions & 8 deletions src/lib/components/Swap/SwapButton.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Trans } from '@lingui/macro'
import { useSwapInfo } from 'lib/hooks/swap'
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { useAddTransaction } from 'lib/hooks/transactions'
import { useIsPendingApproval } from 'lib/hooks/transactions'
import { Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useCallback, useEffect, useMemo, useState } from 'react'

import ActionButton from '../ActionButton'
import Dialog from '../Dialog'
import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary'

interface SwapButtonProps {
Expand All @@ -26,17 +28,26 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])

// TODO(zzmp): Track pending approval
const useIsPendingApproval = () => false

// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval)
const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)

const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
getApproval().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, getApproval])

const actionProps = useMemo(() => {
if (disabled) return { disabled: true }

if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
// TODO(zzmp): Update UI for pending approvals.
if (approval === ApprovalState.PENDING) {
return { disabled: true }
} else if (approval === ApprovalState.NOT_APPROVED) {
Expand All @@ -62,7 +73,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<ActionButton
color="interactive"
onClick={() => setActiveTrade(trade.trade)}
onUpdate={getApproval}
onUpdate={addApprovalTransaction}
{...actionProps}
>
<Trans>Review swap</Trans>
Expand All @@ -72,11 +83,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog>
)}
{false && (
{/* TODO(zzmp): Pass the completed tx, possibly at a different level of the DOM.
<Dialog color="dialog">
<StatusDialog onClose={() => void 0} />
</Dialog>
)}
*/}
</>
)
}
2 changes: 2 additions & 0 deletions src/lib/components/Widget.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n'
Expand Down Expand Up @@ -73,6 +74,7 @@ function Updaters() {
<>
<BlockUpdater />
<MulticallUpdater />
<TransactionsUpdater />
</>
)
}
Expand Down
92 changes: 92 additions & 0 deletions src/lib/hooks/transactions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Token } from '@uniswap/sdk-core'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { Transaction, TransactionInfo, transactionsAtom, TransactionType } from 'lib/state/transactions'
import ms from 'ms.macro'
import { useCallback } from 'react'
import invariant from 'tiny-invariant'

import useBlockNumber from '../useBlockNumber'
import Updater from './updater'

function isTransactionRecent(transaction: Transaction) {
return Date.now() - transaction.addedTime < ms`1d`
}

export function usePendingTransactions() {
const { chainId } = useActiveWeb3React()
const txs = useAtomValue(transactionsAtom)
return (chainId ? txs[chainId] : null) ?? {}
}

export function useAddTransaction() {
const { chainId } = useActiveWeb3React()
const blockNumber = useBlockNumber()
const updateTxs = useUpdateAtom(transactionsAtom)

return useCallback(
(info: TransactionInfo) => {
invariant(chainId)
const txChainId = chainId
const { hash } = info.response

updateTxs((chainTxs) => {
const txs = chainTxs[txChainId] || {}
txs[hash] = { addedTime: new Date().getTime(), lastCheckedBlockNumber: blockNumber, info }
chainTxs[chainId] = txs
})
},
[blockNumber, chainId, updateTxs]
)
}

export function useIsPendingApproval(token?: Token, spender?: string) {
const { chainId } = useActiveWeb3React()
const txs = useAtomValue(transactionsAtom)
if (!chainId || !token || !spender) return false

const chainTxs = txs[chainId]
if (!chainTxs) return false

return Object.values(chainTxs).some(
(tx) =>
tx &&
tx.receipt === undefined &&
tx.info.type === TransactionType.APPROVAL &&
tx.info.tokenAddress === token.address &&
tx.info.spenderAddress === spender &&
isTransactionRecent(tx)
)
}

export function TransactionsUpdater() {
const pendingTransactions = usePendingTransactions()

const updateTxs = useUpdateAtom(transactionsAtom)
const onCheck = useCallback(
({ chainId, hash, blockNumber }) => {
updateTxs((txs) => {
const tx = txs[chainId]?.[hash]
if (tx) {
tx.lastCheckedBlockNumber = tx.lastCheckedBlockNumber
? Math.max(tx.lastCheckedBlockNumber, blockNumber)
: blockNumber
}
})
},
[updateTxs]
)
const onReceipt = useCallback(
({ chainId, hash, receipt }) => {
updateTxs((txs) => {
const tx = txs[chainId]?.[hash]
if (tx) {
tx.receipt = receipt
}
})
},
[updateTxs]
)

return <Updater pendingTransactions={pendingTransactions} onCheck={onCheck} onReceipt={onReceipt} />
}
File renamed without changes.
Loading

0 comments on commit c7633d9

Please sign in to comment.