Skip to content

Commit

Permalink
Merge pull request #522 from ensdomains/fix/safe-support
Browse files Browse the repository at this point in the history
fix: safe tx support
  • Loading branch information
TateB authored Jun 20, 2023
2 parents f69dfd8 + 9e5b303 commit 3b412c7
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useAccountSafely } from '@app/hooks/useAccountSafely'
import { useChainName } from '@app/hooks/useChainName'
import { GenericTransaction } from '@app/transaction-flow/types'
import { useEns } from '@app/utils/EnsProvider'
import { checkIsSafeApp } from '@app/utils/safe'

import { TransactionStageModal, handleBackToInput } from './TransactionStageModal'

Expand All @@ -18,6 +19,7 @@ jest.mock('@app/hooks/useChainName')
jest.mock('@app/hooks/transactions/useAddRecentTransaction')
jest.mock('@app/hooks/transactions/useRecentTransactions')
jest.mock('@app/utils/EnsProvider')
jest.mock('@app/utils/safe')

const mockPopulatedTransaction = {
data: '0x1896f70a516f53deb2dac3f055f1db1fbd64c12640aa29059477103c3ef28806f15929250000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41',
Expand Down Expand Up @@ -58,6 +60,7 @@ const mockUseAccountSafely = mockFunction(useAccountSafely)
const mockUseChainName = mockFunction(useChainName)
const mockUseSigner = mockFunction(useSigner)
const mockUseSendTransaction = mockFunction(useSendTransaction)
const mockCheckIsSafeApp = checkIsSafeApp as jest.MockedFunctionDeep<typeof checkIsSafeApp>

const mockEstimateGas = jest.fn()
const mockOnDismiss = jest.fn()
Expand Down Expand Up @@ -261,12 +264,36 @@ describe('TransactionStageModal', () => {
it('should add to recent transactions and run dispatch from success callback', async () => {
const mockAddTransaction = jest.fn()
mockUseAddRecentTransaction.mockReturnValue(mockAddTransaction)
mockCheckIsSafeApp.mockResolvedValue(false)
await renderHelper({ transaction: mockTransaction })
await waitFor(() => expect(mockUseSendTransaction.mock.lastCall[0].onSuccess).toBeDefined())
;(mockUseSendTransaction.mock.lastCall[0] as any).onSuccess({ hash: '0x123' })
await (mockUseSendTransaction.mock.lastCall[0] as any).onSuccess({
hash: '0x123',
})
expect(mockAddTransaction).toBeCalledWith({
hash: '0x123',
action: 'test',
isSafeTx: false,
key: 'test',
})
expect(mockDispatch).toBeCalledWith({
name: 'setTransactionHash',
payload: '0x123',
})
})
it('should add to recent transactions and run dispatch from success callback when isSafeTx', async () => {
const mockAddTransaction = jest.fn()
mockUseAddRecentTransaction.mockReturnValue(mockAddTransaction)
mockCheckIsSafeApp.mockResolvedValue('iframe')
await renderHelper({ transaction: mockTransaction })
await waitFor(() => expect(mockUseSendTransaction.mock.lastCall[0].onSuccess).toBeDefined())
await (mockUseSendTransaction.mock.lastCall[0] as any).onSuccess({
hash: '0x123',
})
expect(mockAddTransaction).toBeCalledWith({
hash: '0x123',
action: 'test',
isSafeTx: true,
key: 'test',
})
expect(mockDispatch).toBeCalledWith({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toUtf8String } from '@ethersproject/strings'
import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import { useProvider, useQuery, useSendTransaction, useSigner } from 'wagmi'
import { useAccount, useProvider, useQuery, useSendTransaction, useSigner } from 'wagmi'

import { Button, CrossCircleSVG, Dialog, Helper, Spinner, Typography } from '@ensdomains/thorin'

Expand All @@ -24,6 +24,7 @@ import {
} from '@app/transaction-flow/types'
import { useEns } from '@app/utils/EnsProvider'
import { useQueryKeys } from '@app/utils/cacheKeyFactory'
import { checkIsSafeApp } from '@app/utils/safe'
import { makeEtherscanLink } from '@app/utils/utils'

import { DisplayItems } from '../DisplayItems'
Expand Down Expand Up @@ -283,6 +284,9 @@ export const TransactionStageModal = ({
[recentTransactions, transaction.hash],
)

const { connector } = useAccount()
const provider = useProvider()

const uniqueTxIdentifiers = useMemo(
() =>
uniqueTransactionIdentifierGenerator(
Expand Down Expand Up @@ -359,11 +363,14 @@ export const TransactionStageModal = ({
} = useSendTransaction({
mode: 'prepared',
request,
onSuccess: (tx) => {
onSuccess: async (tx) => {
const isSafeApp = await checkIsSafeApp(connector)

addRecentTransaction({
hash: tx.hash,
action: actionName,
key: txKey!,
isSafeTx: !!isSafeApp,
})
dispatch({ name: 'setTransactionHash', payload: tx.hash })
},
Expand Down Expand Up @@ -481,8 +488,6 @@ export const TransactionStageModal = ({
return 'inProgress'
}, [stage])

const provider = useProvider()

const { data: upperError } = useQuery(
useQueryKeys().transactionStageModal.transactionError(transaction.hash),
async () => {
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/transactions/transactionStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { PartialMockedFunction } from '@app/test-utils'

import { waitFor } from '@testing-library/react'
import { waitForTransaction } from '@wagmi/core'

import { createTransactionStore } from './transactionStore'
import { waitForTransaction } from './waitForTransaction'

jest.mock('@wagmi/core', () => ({
jest.mock('./waitForTransaction', () => ({
waitForTransaction: jest.fn(),
}))

Expand Down Expand Up @@ -62,6 +62,7 @@ describe('transactionStore', () => {
expect(transactions[0]).toStrictEqual({
...transaction,
hash: newHash,
isSafeTx: false,
status: 'confirmed',
minedData: { status: 1, blockHash: 'blockHash', timestamp: 1000 },
})
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/transactions/transactionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
// this is taken from rainbowkit
import type { BaseProvider, Block, TransactionReceipt } from '@ethersproject/providers'
import { waitForTransaction } from '@wagmi/core'

import { MinedData } from '@app/types'

import { waitForTransaction } from './waitForTransaction'

const storageKey = 'transaction-data'

type TransactionStatus = 'pending' | 'confirmed' | 'failed' | 'repriced'
Expand All @@ -16,6 +17,7 @@ interface BaseTransaction {
action: string
key?: string
description?: string
isSafeTx?: boolean
status: TransactionStatus
minedData?: MinedData
newHash?: string
Expand Down Expand Up @@ -175,15 +177,16 @@ export function createTransactionStore({ provider: initialProvider }: { provider
setTransactionStatus(account, chainId, hash, 'repriced', speedUpTransaction.hash)
addTransaction(account, chainId, {
...transaction,
isSafeTx: false,
hash: speedUpTransaction.hash,
})

transactionRequestCache.set(speedUpTransaction.hash, requestPromise)
transactionRequestCache.delete(hash)
},
isSafeTx: transaction.isSafeTx,
})
.catch((err) => {
console.error('transaction error:', err)
if (err.cancelled) {
const replacement = err.replacement as TransactionReceipt
return { ...replacement, status: 0 }
Expand Down
170 changes: 170 additions & 0 deletions src/hooks/transactions/waitForTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { PartialMockedFunction } from '@app/test-utils'

import { getProvider } from '@wagmi/core'
import { BigNumber } from 'ethers'

import { fetchTxFromSafeTxHash } from '@app/utils/safe'

import { SafeTxExecutedError, waitForTransaction } from './waitForTransaction'

jest.mock('@app/utils/safe')

jest.mock('@wagmi/core', () => ({
getProvider: jest.fn(),
fetchBlockNumber: jest.fn(),
fetchTransaction: jest.fn(),
}))

const mockGetProvider = getProvider as unknown as jest.MockedFunction<
PartialMockedFunction<typeof getProvider>
>

const mockFetchTxFromSafeTxHash = fetchTxFromSafeTxHash as unknown as jest.MockedFunctionDeep<
typeof fetchTxFromSafeTxHash
>

const mockWaitForTransaction = jest.fn()
const mockCall = jest.fn()
const mockGetTransactionReceipt = jest.fn()
const mockGetBlockNumber = jest.fn()

mockGetProvider.mockReturnValue({
network: {
chainId: 1,
},
_waitForTransaction: mockWaitForTransaction,
call: mockCall,
getTransactionReceipt: mockGetTransactionReceipt,
getBlockNumber: mockGetBlockNumber,
})

describe('waitForTransaction', () => {
it('should wait for standard transaction', async () => {
mockWaitForTransaction.mockResolvedValueOnce({
status: 1,
transactionHash: 'test',
})

const result = await waitForTransaction({
hash: '0xtest',
})

expect(result).toStrictEqual({
status: 1,
transactionHash: 'test',
})
})
it('should allow a repriced transaction', async () => {
mockWaitForTransaction.mockImplementationOnce(async () => {
mockWaitForTransaction.mockImplementationOnce(async (hash: string) => {
return {
status: 1,
transactionHash: hash,
}
})
// equivalent to ethers repriced error
throw new SafeTxExecutedError('repriced', { hash: 'newHash' })
})

const result = await waitForTransaction({
hash: '0xtest',
})

expect(result).toStrictEqual({
status: 1,
transactionHash: 'newHash',
})
})
it('should call onSpeedUp if transaction repriced', async () => {
mockWaitForTransaction.mockImplementationOnce(async () => {
mockWaitForTransaction.mockImplementationOnce(async (hash: string) => {
return {
status: 1,
transactionHash: hash,
}
})
// equivalent to ethers repriced error
throw new SafeTxExecutedError('repriced', { hash: 'newHash' })
})

const onSpeedUp = jest.fn()

await waitForTransaction({
hash: '0xtest',
onSpeedUp,
})

expect(onSpeedUp).toHaveBeenCalledWith({ hash: 'newHash' })
})
it('should throw other errors', async () => {
mockWaitForTransaction.mockImplementationOnce(async () => {
throw new Error('cancelled')
})

await expect(
waitForTransaction({
hash: '0xtest',
}),
).rejects.toThrowError('cancelled')
})
it('should allow a SAFE transaction', async () => {
const SAFE_TX_HASH = '0xf4b645e849800a5f8cf8437d02acfc2ef5d57d10b186b1495c6a38c80c4ebeea'
const REAL_TX_HASH = '0x0d641a3dcbf8d7311f727a231be69c88d2f2f8478b718fd5692a0ec8df2b31cd'

mockFetchTxFromSafeTxHash.mockResolvedValue({
transactionHash: REAL_TX_HASH,
})

mockGetBlockNumber.mockResolvedValue(1)

mockGetTransactionReceipt.mockResolvedValue({
to: '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
from: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef',
contractAddress: null,
transactionIndex: 1,
gasUsed: BigNumber.from('21000'),
logsBloom: '0x00',
blockHash: '0x5c3d86723f063c1e5fb6319244a9a9d5a5fb6865a28f2a9df2ccee50a1a5f82a',
transactionHash: REAL_TX_HASH,
logs: [],
blockNumber: BigNumber.from('1'),
confirmations: 1,
cumulativeGasUsed: BigNumber.from('21000'),
byzantium: true,
status: 1,
type: 0,
rawLogs: [],
})

mockWaitForTransaction.mockImplementation(async () => {
return {
status: 1,
transactionHash: REAL_TX_HASH,
}
})

const onSpeedUp = jest.fn()

const result = await waitForTransaction({
hash: SAFE_TX_HASH,
isSafeTx: true,
onSpeedUp,
})

expect(mockFetchTxFromSafeTxHash).toHaveBeenCalledWith({
chainId: 1,
safeTxHash: SAFE_TX_HASH,
})

expect(mockGetTransactionReceipt).toHaveBeenCalledWith(REAL_TX_HASH)

expect(onSpeedUp).toHaveBeenCalledWith({ hash: REAL_TX_HASH })

expect(mockWaitForTransaction).toHaveBeenCalledWith(REAL_TX_HASH, 1, 0, null)

expect(result).toStrictEqual({
status: 1,
transactionHash: REAL_TX_HASH,
})
})
})
Loading

0 comments on commit 3b412c7

Please sign in to comment.