From f1916e92f1de07b181d355d34c54e39c253840f1 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Mon, 14 Sep 2020 12:59:28 -0300 Subject: [PATCH] (Feature) Add unit tests (#1230) * Test for balances store * Types for addressBook.ts * Adds addressBookUtils.test.ts * Remove duplicated type * Tests for addressBookUtils * Adds fetchSafeTokens.test.ts * Adds getMockedSafeInstance and getMockedTxServiceModel in safeHelper.ts * Adds shouldExecuteTransaction tests * Fix types for TransactionProps And getNewTxNonce * Adds tests for getNewTxNonce and getLastTx * Moves utils.test.tsx to /actions folder * Placeholder for transactionHelpers tests * isInnerTransaction tests * calculateTransactionStatus tests * Adds calculateTransactionType tests * Adds buildTx test Adds generateSafeTxHash test * Absolute imports * Adds types for getRefundParams * Adds getRefundParams tests * Add mock example for isInnerTransaction * Adds isCancelTransaction tests Adds isModifySettingsTransaction tests Adds isMultiSendTransaction tests Adds isUpgradeTransaction tests Adds isOutgoingTransaction tests Adds isCustomTransaction tests * Adds types in mockNonPayableTransactionObject * Add TODOS * Fix shortVersionOf function * Add ethAddresses.test.ts Adds sameAddress test Adds shortVersionOf test Adds isUserAnOwner test * Adds isUserAnOwnerOfAnySafe Adds isValidEnsName * Fix isERC721Contract * Adds tokenHelpers.test.ts: - getEthAsToken - isTokenTransfer - getERC20DecimalsAndSymbol - isERC721Contract * Fix eslint errors * Remove unused files * Use selectors in safeBalance tests * Move file near his implementation * Replaces resultExpected with expectedResult * Update comment * Reword tests * Adds utility function description * Merge with dev Fix types * Fix types * Fix build types * Mock contract Co-authored-by: Daniel Sanchez Co-authored-by: Mikhail Mikheev --- src/logic/addressBook/model/addressBook.ts | 4 +- .../addressBook/store/reducer/addressBook.ts | 18 +- .../utils/__tests__/addressBookUtils.test.ts | 101 ++ src/logic/addressBook/utils/index.ts | 24 +- .../__tests__/fetchSafeTokens.test.ts | 53 ++ .../__tests__/transactionHelpers.test.ts | 875 ++++++++++++++++++ .../transactions/utils/transactionHelpers.ts | 12 +- .../safe/store/tests/safe.balances.test.ts | 59 ++ .../utils/__tests__}/formatAmount.test.ts | 36 +- .../utils/__tests__/tokenHelpers.test.ts | 174 ++++ src/logic/tokens/utils/tokenHelpers.ts | 2 +- src/logic/wallets/ethAddresses.ts | 2 +- src/logic/wallets/tests/ethAddresses.test.ts | 281 ++++++ .../CreateEditEntryModal/index.tsx | 4 +- .../EthAddressInput/index.tsx | 2 +- .../SendCustomTx/index.tsx | 2 +- .../screens/SendCollectible/index.tsx | 6 +- .../SendModal/screens/SendFunds/index.tsx | 26 +- .../Settings/ManageOwners/index.tsx | 4 +- .../transactions/__tests__/utils.test.ts | 170 ++++ src/test/safe.dom.balances.ts | 72 -- src/test/utils/safeHelper.ts | 162 ++++ src/utils/fetch.ts | 17 - 23 files changed, 1955 insertions(+), 151 deletions(-) create mode 100644 src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts create mode 100644 src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts create mode 100644 src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts create mode 100644 src/logic/safe/store/tests/safe.balances.test.ts rename src/{test/logic/token/utils => logic/tokens/utils/__tests__}/formatAmount.test.ts (87%) create mode 100644 src/logic/tokens/utils/__tests__/tokenHelpers.test.ts create mode 100644 src/logic/wallets/tests/ethAddresses.test.ts create mode 100644 src/routes/safe/store/actions/transactions/__tests__/utils.test.ts delete mode 100644 src/test/safe.dom.balances.ts create mode 100644 src/test/utils/safeHelper.ts delete mode 100644 src/utils/fetch.ts diff --git a/src/logic/addressBook/model/addressBook.ts b/src/logic/addressBook/model/addressBook.ts index 732027d0..34554330 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -6,12 +6,10 @@ export interface AddressBookEntryProps { isOwner: boolean } -export type AddressBookEntryRecord = RecordOf - export const makeAddressBookEntry = Record({ address: '', name: '', isOwner: false, }) -export type AddressBookEntry = RecordOf +export type AddressBookEntryRecord = RecordOf diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts index ea1a46d7..e1ce17d6 100644 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ b/src/logic/addressBook/store/reducer/addressBook.ts @@ -1,27 +1,31 @@ import { List, Map } from 'immutable' import { handleActions } from 'redux-actions' -import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { + AddressBookEntryRecord, + AddressBookEntryProps, + makeAddressBookEntry, +} from 'src/logic/addressBook/model/addressBook' import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook' import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook' import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils' +import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { checksumAddress } from 'src/utils/checksumAddress' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' -export type AddressBookCollection = List +export type AddressBookCollection = List export type AddressBookState = Map> -export const buildAddressBook = (storedAdbk) => { - let addressBookBuilt = Map([]) +export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map => { + let addressBookBuilt: Map = Map([]) Object.entries(storedAdbk).forEach((adbkProps: any) => { const safeAddress = checksumAddress(adbkProps[0]) - const adbkRecords = adbkProps[1].map(makeAddressBookEntry) + const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry) const adbkSafeEntries = List(adbkRecords) addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries) }) @@ -55,7 +59,7 @@ export default handleActions( const safeAddressBook = state.getIn(['addressBook', safeAddress]) if (safeAddressBook) { - const adbkAddressList = getAddressesListFromAdbk(safeAddressBook) + const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook) const found = adbkAddressList.includes(entry.address) if (!found) { const updatedSafeAdbkList = safeAddressBook.push(entry) diff --git a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts new file mode 100644 index 00000000..ce410367 --- /dev/null +++ b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts @@ -0,0 +1,101 @@ +import { Map, List } from 'immutable' +import { + getAddressBookFromStorage, + getAddressesListFromSafeAddressBook, + getNameFromSafeAddressBook, + getOwnersWithNameFromAddressBook, + saveAddressBook, +} from 'src/logic/addressBook/utils/index' +import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' +import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' + +const getMockAddressBookEntry = ( + address: string, + name: string = 'test', + isOwner: boolean = false, +): AddressBookEntryRecord => + makeAddressBookEntry({ + address, + name, + isOwner, + }) + +describe('getAddressesListFromAdbk', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + + it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => { + // given + const safeAddressBook = List([entry1, entry2, entry3]) + const expectedResult = [entry1.address, entry2.address, entry3.address] + + // when + const result = getAddressesListFromSafeAddressBook(safeAddressBook) + + // then + expect(result).toStrictEqual(expectedResult) + }) +}) + +describe('getNameFromSafeAddressBook', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should returns the user name given a safeAddressBook and an user account', () => { + // given + const safeAddressBook = List([entry1, entry2, entry3]) + const expectedResult = entry2.name + + // when + const result = getNameFromSafeAddressBook(safeAddressBook, entry2.address) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('getOwnersWithNameFromAddressBook', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => { + // given + const safeAddressBook = List([entry1, entry2, entry3]) + const ownerList = List([ + { address: entry1.address, name: '' }, + { address: entry2.address, name: '' }, + ]) + const expectedResult = List([ + { address: entry1.address, name: entry1.name }, + { address: entry2.address, name: entry2.name }, + ]) + + // when + const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList) + + // then + expect(result).toStrictEqual(expectedResult) + }) +}) + +describe('saveAddressBook', () => { + const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should save a given addressBook to the localStorage', async () => { + // given + const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) }) + + // when + // @ts-ignore + await saveAddressBook(addressBook) + const storedAdBk = await getAddressBookFromStorage() + let result = buildAddressBook(storedAdBk) + + // then + expect(result).toStrictEqual(addressBook) + }) +}) diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 22cd11e8..8b636892 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,27 +1,28 @@ import { List } from 'immutable' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { AddressBookEntryProps } from './../model/addressBook' +import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook' import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { AddressBookCollection } from '../store/reducer/addressBook' +import { AddressBookMap } from '../store/reducer/types/addressBook' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' export const getAddressBookFromStorage = async (): Promise | undefined> => { - const data = await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) - - return data + return await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) } -export const saveAddressBook = async (addressBook) => { +export const saveAddressBook = async (addressBook: AddressBookMap): Promise => { try { - await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON()) + await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS()) } catch (err) { console.error('Error storing addressBook in localstorage', err) } } -export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address) +export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] => + Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address) -export const getNameFromAdbk = (addressBook, userAddress) => { +export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => { const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress) if (entry) { return entry.name @@ -30,18 +31,17 @@ export const getNameFromAdbk = (addressBook, userAddress) => { } export const getOwnersWithNameFromAddressBook = ( - addressBook: AddressBookEntryProps, + addressBook: AddressBookCollection, ownerList: List, ): List | [] => { if (!ownerList) { return [] } - const ownersListWithAdbkNames = ownerList.map((owner) => { - const ownerName = getNameFromAdbk(addressBook, owner.address) + return ownerList.map((owner) => { + const ownerName = getNameFromSafeAddressBook(addressBook, owner.address) return { address: owner.address, name: ownerName || owner.name, } }) - return ownersListWithAdbkNames } diff --git a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts new file mode 100644 index 00000000..246a2664 --- /dev/null +++ b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts @@ -0,0 +1,53 @@ +import { aNewStore } from 'src/store' +import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' +import axios from 'axios' +import { getTxServiceHost } from 'src/config' + +jest.mock('axios') +describe('fetchTokenCurrenciesBalances', () => { + let store + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + beforeEach(async () => { + store = aNewStore() + }) + afterAll(() => { + jest.unmock('axios') + }) + + it('Given a safe address, calls the API and returns token balances', async () => { + // given + const expectedResult = [ + { + balance: '849890000000000000', + balanceUsd: '337.2449', + token: null, + tokenAddress: null, + usdConversion: '396.81', + }, + { + balance: '24698677800000000000', + balanceUsd: '29.3432', + token: { + name: 'Dai', + symbol: 'DAI', + decimals: 18, + logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', + }, + tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa', + usdConversion: '1.188', + }, + ] + const apiUrl = getTxServiceHost() + + // @ts-ignore + axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult)) + + // when + const result = await fetchTokenCurrenciesBalances(safeAddress) + + // then + expect(result).toStrictEqual(expectedResult) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/`, { params: { limit: 3000 } }) + }) +}) diff --git a/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts new file mode 100644 index 00000000..4076f002 --- /dev/null +++ b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts @@ -0,0 +1,875 @@ +import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper' + +import { makeTransaction } from 'src/logic/safe/store/models/transaction' +import { TransactionStatus, TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import makeSafe from 'src/logic/safe/store/models/safe' +import { List, Map, Record } from 'immutable' +import { makeToken, TokenProps } from 'src/logic/tokens/store/model/token' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { + buildTx, + calculateTransactionStatus, + calculateTransactionType, + generateSafeTxHash, + getRefundParams, + isCancelTransaction, + isCustomTransaction, + isInnerTransaction, + isModifySettingsTransaction, + isMultiSendTransaction, + isOutgoingTransaction, + isPendingTransaction, + isUpgradeTransaction, +} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' +import { getERC20DecimalsAndSymbol } from 'src/logic/tokens/utils/tokenHelpers' + +const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' +const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' +describe('isInnerTransaction', () => { + it('It should return true if the transaction recipient is our given safeAddress and the txValue is 0', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if the transaction recipient is our given safeAddress and the txValue is >0', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not our given safeAddress', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return true if the transaction recipient is the given safeAddress and the txValue is 0', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if the transaction recipient is the given safeAddress and the txValue is >0', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not the given safeAddress', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress2, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isCancelTransaction', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return false if given a inner transaction with empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isCancelTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a inner transaction without empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isCancelTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isPendingTransaction', () => { + it('It should return true if the transaction is on pending status', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.PENDING }) + const cancelTx = makeTransaction({ data: null }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(true) + }) + it('It should return true If the transaction is not pending status but the cancellation transaction is', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS }) + const cancelTx = makeTransaction({ status: TransactionStatus.PENDING }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(true) + }) + it('It should return true If the transaction and a cancellation transaction are not pending', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.CANCELLED }) + const cancelTx = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(false) + }) +}) + +describe('isModifySettingsTransaction', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return true if given an inner transaction without empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isModifySettingsTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if given an inner transaction with empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isModifySettingsTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isMultiSendTransaction', () => { + it('It should return true if given a transaction without value, the data has multisend data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a transaction without data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return true if given a transaction without value, the data has not multisend substring', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'thisiswrongdata' }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(false) + }) +}) + +describe('isUpgradeTransaction', () => { + it('If should return true if the transaction data is empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction data is multisend transaction but does not have upgradeTx function signature encoded in data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return true if the transaction data is multisend transaction and has upgradeTx enconded in function signature data', () => { + // given + const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000` + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: upgradeTxData }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(true) + }) +}) + +describe('isOutgoingTransaction', () => { + it('It should return true if the transaction recipient is not a safe address and data is not empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return true if the transaction has an address equal to the safe address', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not a safe address and data is empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +jest.mock('src/logic/tokens/utils/tokenHelpers') +describe('isCustomTransaction', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/utils/tokenHelpers') + }) + it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(true) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) + it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(true) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) + it('It should return false if is outgoing transaction, not SendERC20Transaction, isUpgradeTransaction and not isSendERC721Transaction', async () => { + // given + const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000` + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => true) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(false) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + }) + it('It should return false if is outgoing transaction, is not SendERC20Transaction, not isUpgradeTransaction and isSendERC721Transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => true) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(false) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) +}) + +describe('getRefundParams', () => { + it('It should return null if given a transaction with the gasPrice == 0', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice: '0' }) + + // when + const result = await getRefundParams(transaction, getERC20DecimalsAndSymbol) + + // then + expect(result).toBe(null) + }) + it('It should return 0.000000000000020000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 18 decimals', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 100 + const decimals = 18 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + const feeString = (Number(gasPrice) * (Number(baseGas) + Number(safeTxGas))).toString().padStart(decimals, '0') + const whole = feeString.slice(0, feeString.length - decimals) || '0' + const fraction = feeString.slice(feeString.length - decimals) + + const expectedResult = { + fee: `${whole}.${fraction}`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) + it('Given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 1 decimal, returns 2000.0', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 100 + const decimals = 1 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + + const expectedResult = { + fee: `2000.0`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) + it('It should return 0.50000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 400 and 5 decimals', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 400 + const decimals = 5 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + + const expectedResult = { + fee: `0.50000`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) +}) + +describe('getDecodedParams', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('isTransactionCancelled', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('calculateTransactionStatus', () => { + it('It should return SUCCESS if the tx is executed and successful', () => { + // given + const transaction = makeTransaction({ isExecuted: true, isSuccessful: true }) + const safe = makeSafe() + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.SUCCESS) + }) + it('It should return CANCELLED if the tx is cancelled and successful', () => { + // given + const transaction = makeTransaction({ cancelled: true }) + const safe = makeSafe() + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.CANCELLED) + }) + it('It should return AWAITING_EXECUTION if the tx has an amount of confirmations equal to the safe threshold', () => { + // given + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + const transaction = makeTransaction({ cancelled: true, confirmations: List([makeUser(), makeUser(), makeUser()]) }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.CANCELLED) + }) + it('It should return SUCCESS if the tx is the creation transaction', () => { + // given + const transaction = makeTransaction({ creationTx: true, confirmations: List() }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.SUCCESS) + }) + it('It should return PENDING if the tx is pending', () => { + // given + const transaction = makeTransaction({ confirmations: List(), isPending: true }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.PENDING) + }) + it('It should return PENDING if the tx has no confirmations', () => { + // given + const transaction = makeTransaction({ confirmations: List(), isPending: false }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.PENDING) + }) + it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ + threshold: 3, + owners: List([ + { name: '', address: userAddress }, + { name: '', address: userAddress2 }, + ]), + }) + const currentUser = userAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS) + }) + it('It should return AWAITING_YOUR_CONFIRMATION if the tx has confirmations bellow the threshold, the user is owner and not signed', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ + threshold: 3, + owners: List([ + { name: '', address: userAddress }, + { name: '', address: userAddress2 }, + ]), + }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_YOUR_CONFIRMATION) + }) + it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is not owner', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS) + }) + it('It should return FAILED if the tx is not successful', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ + confirmations: List([makeUser({ owner: userAddress })]), + isSuccessful: false, + }) + const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.FAILED) + }) +}) + +describe('calculateTransactionType', () => { + it('It should return TOKEN If the tx is a token transfer transaction', () => { + // given + const transaction = makeTransaction({ isTokenTransfer: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.TOKEN) + }) + it('It should return COLLECTIBLE If the tx is a collectible transfer transaction', () => { + // given + const transaction = makeTransaction({ isCollectibleTransfer: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.COLLECTIBLE) + }) + it('It should return SETTINGS If the tx is a modifySettings transaction', () => { + // given + const transaction = makeTransaction({ modifySettingsTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.SETTINGS) + }) + + it('It should return CANCELLATION If the tx is a cancellation transaction', () => { + // given + const transaction = makeTransaction({ isCancellationTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CANCELLATION) + }) + + it('It should return CUSTOM If the tx is a custom transaction', () => { + // given + const transaction = makeTransaction({ customTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CUSTOM) + }) + it('It should return CUSTOM If the tx is a creation transaction', () => { + // given + const transaction = makeTransaction({ creationTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CREATION) + }) + it('It should return UPGRADE If the tx is an upgrade transaction', () => { + // given + const transaction = makeTransaction({ upgradeTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.UPGRADE) + }) +}) + +describe('buildTx', () => { + it('Returns a valid transaction', async () => { + // given + const cancelTx1 = makeTransaction() + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' }) + const userAddress = 'address1' + const cancellationTxs = List([cancelTx1]) + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + const knownTokens = Map & Readonly>() + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + const outgoingTxs = List([cancelTx1]) + const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress }) + const expectedTx = makeTransaction({ + baseGas: 0, + blockNumber: 0, + cancelled: false, + confirmations: List([]), + creationTx: false, + customTx: false, + data: EMPTY_DATA, + dataDecoded: null, + decimals: 18, + decodedParams: null, + executionDate: '', + executionTxHash: '', + executor: '', + gasPrice: '', + gasToken: ZERO_ADDRESS, + isCancellationTx: false, + isCollectibleTransfer: false, + isExecuted: false, + isSuccessful: false, + isTokenTransfer: false, + modifySettingsTx: false, + multiSendTx: false, + nonce: 0, + operation: 0, + origin: '', + recipient: safeAddress2, + refundParams: null, + refundReceiver: ZERO_ADDRESS, + safeTxGas: 0, + safeTxHash: '', + setupData: '', + status: TransactionStatus.FAILED, + submissionDate: '', + symbol: 'ETH', + upgradeTx: false, + value: '0', + fee: '', + }) + + // when + const txResult = await buildTx({ + cancellationTxs, + currentUser: userAddress, + knownTokens, + outgoingTxs, + safe: safeInstance, + tx: transaction, + txCode: null, + }) + + // then + expect(txResult).toStrictEqual(expectedTx) + }) +}) + +describe('updateStoredTransactionsStatus', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('generateSafeTxHash', () => { + it('It should return a safe transaction hash', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const userAddress = 'address1' + const userAddress2 = 'address2' + const userAddress3 = 'address3' + const safeInstance = getMockedSafeInstance({}) + const txArgs = { + baseGas: 100, + data: '', + gasPrice: '1000', + gasToken: '', + nonce: 0, + operation: 0, + refundReceiver: userAddress, + safeInstance, + safeTxGas: 1000, + sender: userAddress2, + sigs: '', + to: userAddress3, + valueInWei: '5000', + } + + // when + const result = generateSafeTxHash(safeAddress, txArgs) + + // then + expect(result).toBe('0x21e6ebc992f959dd0a2a6ce6034c414043c598b7f446c274efb3527c30dec254') + }) +}) diff --git a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts index 71512cc0..761f978e 100644 --- a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts @@ -87,12 +87,12 @@ export const isCustomTransaction = async ( safeAddress: string, knownTokens: Map, ): Promise => { - return ( - isOutgoingTransaction(tx, safeAddress) && - !(await isSendERC20Transaction(tx, txCode, knownTokens)) && - !isUpgradeTransaction(tx) && - !isSendERC721Transaction(tx, txCode, knownTokens) - ) + const isOutgoing = isOutgoingTransaction(tx, safeAddress) + const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens) + const isUpgrade = isUpgradeTransaction(tx) + const isErc721 = isSendERC721Transaction(tx, txCode, knownTokens) + + return isOutgoing && !isErc20 && !isUpgrade && !isErc721 } export const getRefundParams = async ( diff --git a/src/logic/safe/store/tests/safe.balances.test.ts b/src/logic/safe/store/tests/safe.balances.test.ts new file mode 100644 index 00000000..18909a6e --- /dev/null +++ b/src/logic/safe/store/tests/safe.balances.test.ts @@ -0,0 +1,59 @@ +import { Set, Map } from 'immutable' +import { aNewStore } from 'src/store' +import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' +import '@testing-library/jest-dom/extend-expect' +import updateSafe from 'src/logic/safe/store/actions/updateSafe' +import { makeToken } from 'src/logic/tokens/store/model/token' +import { safesMapSelector } from 'src/logic/safe/store/selectors' + +describe('Feature > Balances', () => { + let store + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + beforeEach(async () => { + store = aNewStore() + }) + + it('It should return an updated balance when updates active tokens', async () => { + // given + const tokensAmount = '100' + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + const balances = Map({ + [token.address]: tokensAmount, + }) + const expectedResult = '100' + + // when + store.dispatch(updateActiveTokens(safeAddress, Set([token.address]))) + store.dispatch(updateSafe({ address: safeAddress, balances })) + + const safe = safesMapSelector(store.getState()).get(safeAddress) + const balanceResult = safe.get('balances').get(token.address) + const activeTokens = safe.get('activeTokens') + const tokenIsActive = activeTokens.has(token.address) + + // then + expect(balanceResult).toBe(expectedResult) + expect(tokenIsActive).toBe(true) + }) + + it('The store should have an updated ether balance after updating the value', async () => { + // given + const etherAmount = '1' + const expectedResult = '1' + + // when + store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount })) + const safe = safesMapSelector(store.getState()).get(safeAddress) + const balanceResult = safe.get('ethBalance') + + // then + expect(balanceResult).toBe(expectedResult) + }) +}) diff --git a/src/test/logic/token/utils/formatAmount.test.ts b/src/logic/tokens/utils/__tests__/formatAmount.test.ts similarity index 87% rename from src/test/logic/token/utils/formatAmount.test.ts rename to src/logic/tokens/utils/__tests__/formatAmount.test.ts index 94fc60c9..8dd5a0d6 100644 --- a/src/test/logic/token/utils/formatAmount.test.ts +++ b/src/logic/tokens/utils/__tests__/formatAmount.test.ts @@ -1,8 +1,7 @@ import { formatAmount, formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' - describe('formatAmount', () => { - it('Given 0 returns 0', () => { + it('Given 0 returns 0', () => { // given const input = '0' const expectedResult = '0' @@ -13,7 +12,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given 1 returns 1', () => { + it('Given 1 returns 1', () => { // given const input = '1' const expectedResult = '1' @@ -24,7 +23,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { // given const input = '19797.899' const expectedResult = '19,797.899' @@ -35,7 +34,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number > 0.001 && < 1000 returns the same number as string', () => { + it('Given number > 0.001 && < 1000 returns the same number as string', () => { // given const input = 999 const expectedResult = '999' @@ -45,7 +44,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => { + it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => { // given const input = 9999 const expectedResult = '9,999' @@ -55,7 +54,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => { + it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => { // given const input = 99999 const expectedResult = '99,999' @@ -65,7 +64,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => { + it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => { // given const input = 999999 const expectedResult = '999,999' @@ -75,7 +74,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => { + it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => { // given const input = 9999999 const expectedResult = '9,999,999' @@ -85,7 +84,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number < 0.001 returns < 0.001', () => { + it('Given number < 0.001 returns < 0.001', () => { // given const input = 0.000001 const expectedResult = '< 0.001' @@ -95,7 +94,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number > 10 ** 15 returns > 1000T', () => { + it('Given number > 10 ** 15 returns > 1000T', () => { // given const input = 10 ** 15 * 2 const expectedResult = '> 1000T' @@ -109,7 +108,7 @@ describe('formatAmount', () => { }) describe('FormatsAmountsInUsFormat', () => { - it('Given 0 returns 0.00', () => { + it('Given 0 returns 0.00', () => { // given const input = 0 const expectedResult = '0.00' @@ -120,7 +119,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given 1 returns 1.00', () => { + it('Given 1 returns 1.00', () => { // given const input = 1 const expectedResult = '1.00' @@ -131,9 +130,9 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => { + it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => { // given - const input = 311137.30 + const input = 311137.3 const expectedResult = '311,137.30' // when @@ -142,7 +141,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { // given const input = 19797.899 const expectedResult = '19,797.899' @@ -153,7 +152,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => { + it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => { // given const input = 19797899.479 const expectedResult = '19,797,899.479' @@ -164,7 +163,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => { + it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => { // given const input = 19797899479.999 const expectedResult = '19,797,899,479.999' @@ -176,4 +175,3 @@ describe('FormatsAmountsInUsFormat', () => { expect(result).toBe(expectedResult) }) }) - diff --git a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts new file mode 100644 index 00000000..7afee4ed --- /dev/null +++ b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts @@ -0,0 +1,174 @@ +import { makeToken } from 'src/logic/tokens/store/model/token' +import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers' +import { getMockedTxServiceModel } from 'src/test/utils/safeHelper' + +describe('isTokenTransfer', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return false if the transaction has no value but but "transfer" function signature is encoded in the data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9059cbb' }) + const expectedResult = true + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if the transaction has no value but and no "transfer" function signature encoded in data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9055cbb' }) + const expectedResult = false + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if the transaction has empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + const expectedResult = false + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) +}) + +jest.mock('src/logic/tokens/store/actions/fetchTokens') +jest.mock('src/logic/contracts/generateBatchRequests') +jest.mock('console') +describe('getERC20DecimalsAndSymbol', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/store/actions/fetchTokens') + jest.unmock('src/logic/contracts/generateBatchRequests') + jest.unmock('console') + }) + it('It should return DAI information from the store if given a DAI address', async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'DAI' + const token = makeToken({ + address: tokenAddress, + name: 'Dai', + symbol, + decimals, + logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', + balance: 0, + }) + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => token) + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spy).toHaveBeenCalled() + }) + it('It should return default value decimals: 18, symbol: UNKNOWN if given a token address and if there is an error fetching the data', async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'UNKNOWN' + + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => { + throw new Error() + }) + console.error = jest.fn() + const spyConsole = jest.spyOn(console, 'error').mockImplementation() + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spy).toHaveBeenCalled() + expect(spyConsole).toHaveBeenCalled() + }) + it("It should fetch token information from the blockchain if given a token address and if the token doesn't exist in redux store", async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'DAI' + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const generateBatchRequests = require('src/logic/contracts/generateBatchRequests') + const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null) + + const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [decimals, symbol]) + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spyTokenInfos).toHaveBeenCalled() + expect(spyGenerateBatchRequest).toHaveBeenCalled() + }) +}) + +describe('isERC721Contract', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/store/actions/fetchTokens') + }) + beforeEach(() => { + jest.mock('src/logic/tokens/store/actions/fetchTokens') + }) + it('It should return false if given non-erc721 contract address', async () => { + // given + const contractAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' // DAI Address + const expectedResult = false + + const ERC721Contract = { + at: () => { + throw new Error('Contract is not ERC721') + }, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract) + + // when + const result = await isERC721Contract(contractAddress) + + // then + expect(result).toEqual(expectedResult) + expect(standardContractSpy).toHaveBeenCalled + }) + it('It should return true if given a Erc721 contract address', async () => { + // given + const contractAddress = '0x014d5883274ab3a9708b0f1e4263df6e90160a30' // dummy ft Address + const ERC721Contract = { + at: (address) => address === contractAddress, + } + const expectedResult = true + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract) + + // when + const result = await isERC721Contract(contractAddress) + + // then + expect(result).toEqual(expectedResult) + expect(standardContractSpy).toHaveBeenCalled() + }) +}) diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index 493aa1e0..74550de2 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -118,8 +118,8 @@ export const isERC721Contract = async (contractAddress: string): Promise { } const final = value.length - cut - if (value.length < final) { + if (value.length <= cut) { return value } diff --git a/src/logic/wallets/tests/ethAddresses.test.ts b/src/logic/wallets/tests/ethAddresses.test.ts new file mode 100644 index 00000000..7a6081a8 --- /dev/null +++ b/src/logic/wallets/tests/ethAddresses.test.ts @@ -0,0 +1,281 @@ +import { + isUserAnOwner, + isUserAnOwnerOfAnySafe, + isValidEnsName, + sameAddress, + shortVersionOf, +} from 'src/logic/wallets/ethAddresses' +import makeSafe from 'src/logic/safe/store/models/safe' +import { makeOwner } from 'src/logic/safe/store/models/owner' +import { List } from 'immutable' + +describe('Utility function: sameAddress', () => { + it('It should return false if no address given', () => { + // given + const safeAddress = null + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) + it('It should return false if not second address given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = null + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) + it('It should return true if two equal addresses given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + + // when + const result = sameAddress(safeAddress, safeAddress) + + // then + expect(result).toBe(true) + }) + it('If should return false if two different addresses given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) +}) + +describe('Utility function: shortVersionOf', () => { + it('It should return Unknown if no address given', () => { + // given + const safeAddress = null + const cut = 5 + const expectedResult = 'Unknown' + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return 0x344...f0503 if given 0x344B941b1aAE2e4Be73987212FC4741687Bf0503 and a cut = 5', () => { + // given + const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const cut = 5 + const expectedResult = `0x344...f0503` + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) + it('If should return the same address if a cut value bigger than the address length given', () => { + // given + const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const cut = safeAddress.length + const expectedResult = safeAddress + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isUserAnOwner', () => { + it("Should return false if there's no Safe", () => { + // given + const userAddress = 'address1' + const safeInstance = null + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return false if there's no `userAccount`", () => { + // given + const userAddress = null + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it('Should return false if there are no owners for the Safe', () => { + // given + const userAddress = 'address1' + const owners = null + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return true if `userAccount` is not in the list of Safe's owners", () => { + // given + const userAddress = 'address1' + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = true + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return false if `userAccount` is not in the list of Safe's owners", () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress2) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isUserAnOwnerOfAnySafe', () => { + it('Should return true if given a list of safes, one of them has an owner equal to the userAccount', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const owners1 = List([makeOwner({ address: userAddress })]) + const owners2 = List([makeOwner({ address: userAddress2 })]) + const safeInstance = makeSafe({ owners: owners1 }) + const safeInstance2 = makeSafe({ owners: owners2 }) + const safesList = List([safeInstance, safeInstance2]) + const expectedResult = true + + // when + const result = isUserAnOwnerOfAnySafe(safesList, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false if given a list of safes, none of them has an owner equal to the userAccount', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const userAddress3 = 'address3' + const owners1 = List([makeOwner({ address: userAddress3 })]) + const owners2 = List([makeOwner({ address: userAddress2 })]) + const safeInstance = makeSafe({ owners: owners1 }) + const safeInstance2 = makeSafe({ owners: owners2 }) + const safesList = List([safeInstance, safeInstance2]) + const expectedResult = false + + // when + const result = isUserAnOwnerOfAnySafe(safesList, userAddress) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isValidEnsName', () => { + it('If should return false if given no ens name', () => { + // given + const ensName = null + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false for an ens without extension in format [value].[eth|test|xyz|luxe]', () => { + // given + const ensName = 'test' + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false for an ens without the format [value].[eth|test|xyz|luxe]', () => { + // given + const ensName = 'test.et12312' + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in format [value].eth', () => { + // given + const ensName = 'test.eth' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for ens in format [value].test', () => { + // given + const ensName = 'test.test' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in the format [value].xyz', () => { + // given + const ensName = 'test.xyz' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in format [value].luxe', () => { + // given + const ensName = 'test.luxe' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) +}) diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index 974be3e4..628a7af0 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -20,7 +20,7 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils' +import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils' export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name' export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address' @@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({ } const addressBook = useSelector(getAddressBook) - const addressBookAddressesList = getAddressesListFromAdbk(addressBook) + const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook) const entryDoesntExist = uniqueAddress(addressBookAddressesList) const formMutators = { diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx index c51b58d0..b09aff40 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx @@ -40,7 +40,7 @@ const EthAddressInput = ({ const { input: { value }, } = useField('contractAddress', { subscription: { value: true } }) - const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: value, name: '', }) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx index c481f080..d66d2c48 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx @@ -54,7 +54,7 @@ const SendCustomTx: React.FC = ({ initialValues, onClose, onNext, contrac const classes = useStyles() const { ethBalance } = useSelector(safeSelector) const [qrModalOpen, setQrModalOpen] = useState(false) - const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: contractAddress || initialValues.contractAddress, name: '', }) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx index 637085a7..d057e42e 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -21,7 +21,7 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils' import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -48,7 +48,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel const nftAssets = useSelector(safeActiveSelectorMap) const nftTokens = useSelector(nftTokensSelector) const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState({ + const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string | null }>({ address: recipientAddress || initialValues.recipientAddress, name: '', }) @@ -97,7 +97,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ name: scannedName, diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 8b78fc79..397bc6c5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -26,7 +26,7 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -48,7 +48,25 @@ const formMutators = { const useStyles = makeStyles(styles as any) -const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }): React.ReactElement => { +type SendFundsProps = { + initialValues: { + amount?: string + recipientAddress?: string + token?: string + } + onClose: () => void + onNext: (txInfo: unknown) => void + recipientAddress: string + selectedToken: string +} + +const SendFunds = ({ + initialValues, + onClose, + onNext, + recipientAddress, + selectedToken = '', +}: SendFundsProps): React.ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) const addressBook = useSelector(getAddressBook) @@ -100,10 +118,10 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ - name: scannedName, + name: scannedName || '', address: scannedAddress, }) closeQrModal() diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index 461ed4a8..61cbaeae 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -30,8 +30,8 @@ import Paragraph from 'src/components/layout/Paragraph/index' import Row from 'src/components/layout/Row' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -import { AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook' import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { AddressBookCollection } from 'src/logic/addressBook/store/reducer/addressBook' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' @@ -84,7 +84,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) - const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookEntryProps, owners) + const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookCollection, owners) const ownerData = getOwnerData(ownersAdbk) return ( diff --git a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts new file mode 100644 index 00000000..db0b015c --- /dev/null +++ b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts @@ -0,0 +1,170 @@ +import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' +import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper' +import axios from 'axios' +import { buildTxServiceUrl } from 'src/logic/safe/transactions' + +describe('shouldExecuteTransaction', () => { + it('It should return false if given a safe with a threshold > 1', async () => { + // given + const nonce = '0' + const threshold = '2' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({}) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(false) + }) + it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => { + // given + const nonce = '0' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({}) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(true) + }) + it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => { + // given + const nonce = '10' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({ isExecuted: true }) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a safe with a threshold === 1 and the previous transaction is not yet executed', async () => { + // given + const nonce = '10' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({ isExecuted: false }) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(false) + }) +}) + +describe('getNewTxNonce', () => { + it('It should return 2 if given the last transaction with nonce 1', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const lastTx = getMockedTxServiceModel({ nonce: 1 }) + const expectedResult = '2' + + // when + const result = await getNewTxNonce(undefined, lastTx, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return 0 if given a safe with nonce 0 and no transactions should use safe contract instance for retrieving nonce', async () => { + // given + const safeNonce = '0' + const safeInstance = getMockedSafeInstance({ nonce: safeNonce }) + const expectedResult = '0' + const mockFnCall = jest.fn().mockImplementation(() => safeNonce) + const mockFnNonce = jest.fn().mockImplementation(() => ({ call: mockFnCall })) + + safeInstance.methods.nonce = mockFnNonce + + // when + const result = await getNewTxNonce(undefined, null, safeInstance) + + // then + expect(result).toBe(expectedResult) + expect(mockFnNonce).toHaveBeenCalled() + expect(mockFnCall).toHaveBeenCalled() + mockFnNonce.mockRestore() + mockFnCall.mockRestore() + }) + it('Given a Safe and the last transaction, should return nonce of the last transaction + 1', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const expectedResult = '11' + const lastTx = getMockedTxServiceModel({ nonce: 10 }) + + // when + const result = await getNewTxNonce(undefined, lastTx, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a pre-calculated nonce number should return it', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const expectedResult = '114' + const nextNonce = '114' + + // when + const result = await getNewTxNonce(nextNonce, null, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) +}) + +jest.mock('axios') +jest.mock('console') +describe('getLastTx', () => { + afterAll(() => { + jest.unmock('axios') + jest.unmock('console') + }) + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return the last transaction for a given a safe address', async () => { + // given + const lastTx = getMockedTxServiceModel({ nonce: 1 }) + const url = buildTxServiceUrl(safeAddress) + + // when + // @ts-ignore + axios.get.mockImplementationOnce(() => { + return { + data: { + results: [lastTx], + }, + } + }) + + const result = await getLastTx(safeAddress) + + // then + expect(result).toStrictEqual(lastTx) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } }) + }) + it('If should return null If catches an error getting last transaction', async () => { + // given + const lastTx = null + const url = buildTxServiceUrl(safeAddress) + + // when + // @ts-ignore + axios.get.mockImplementationOnce(() => { + throw new Error() + }) + console.error = jest.fn() + const result = await getLastTx(safeAddress) + const spyConsole = jest.spyOn(console, 'error').mockImplementation() + + // then + expect(result).toStrictEqual(lastTx) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } }) + expect(spyConsole).toHaveBeenCalled() + }) +}) diff --git a/src/test/safe.dom.balances.ts b/src/test/safe.dom.balances.ts deleted file mode 100644 index 142228fc..00000000 --- a/src/test/safe.dom.balances.ts +++ /dev/null @@ -1,72 +0,0 @@ -// -import { waitForElement } from '@testing-library/react' -import { Set, Map } from 'immutable' -import { aNewStore } from 'src/store' -import { sleep } from 'src/utils/timer' -import { aMinedSafe } from 'src/test/builder/safe.redux.builder' -import { sendTokenTo, sendEtherTo } from 'src/test/utils/tokenMovements' -import { renderSafeView } from 'src/test/builder/safe.dom.utils' -import { dispatchAddTokenToList } from 'src/test/utils/transactions/moveTokens.helper' -// import { calculateBalanceOf } from 'src/routes/safe/store/actions/fetchTokenBalances' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' -import '@testing-library/jest-dom/extend-expect' -import updateSafe from 'src/logic/safe/store/actions/updateSafe' -import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' -import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3' - -describe('DOM > Feature > Balances', () => { - let store - let safeAddress - beforeEach(async () => { - store = aNewStore() - safeAddress = await aMinedSafe(store) - }) - - it('Updates token balances automatically', async () => { - const tokensAmount = '100' - const tokenAddress = await sendTokenTo(safeAddress, tokensAmount) - await dispatchAddTokenToList(store, tokenAddress) - - const SafeDom = await renderSafeView(store, safeAddress) - - // Activate token - const safeTokenBalance = undefined - // const safeTokenBalance = await calculateBalanceOf(tokenAddress, safeAddress, 18) - // expect(safeTokenBalance).toBe(tokensAmount) - - const balances = Map({ - [tokenAddress]: safeTokenBalance, - }) - store.dispatch(updateActiveTokens(safeAddress, Set([tokenAddress]))) - store.dispatch(updateSafe({ address: safeAddress, balances })) - await sleep(1000) - - const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(2) - - await waitForElement(() => SafeDom.getByText(`${tokensAmount} OMG`)) - - await sendTokenTo(safeAddress, tokensAmount) - - await waitForElement(() => SafeDom.getByText(`${parseInt(tokensAmount, 10) * 2} OMG`)) - }) - - it('Updates ether balance automatically', async () => { - const etherAmount = '1' - await sendEtherTo(safeAddress, etherAmount) - - const SafeDom = await renderSafeView(store, safeAddress) - - const safeEthBalance = await getBalanceInEtherOf(safeAddress) - expect(safeEthBalance).toBe(etherAmount) - - const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(1) - - await waitForElement(() => SafeDom.getByText(`${etherAmount} ETH`)) - - await sendEtherTo(safeAddress, etherAmount) - - await waitForElement(() => SafeDom.getByText(`${parseInt(etherAmount, 10) * 2} ETH`)) - }) -}) diff --git a/src/test/utils/safeHelper.ts b/src/test/utils/safeHelper.ts new file mode 100644 index 00000000..02c244f7 --- /dev/null +++ b/src/test/utils/safeHelper.ts @@ -0,0 +1,162 @@ +import { NonPayableTransactionObject } from 'src/types/contracts/types' +import { PromiEvent } from 'web3-core' +import { GnosisSafe } from 'src/types/contracts/GnosisSafe' +import { ContractOptions, ContractSendMethod, DeployOptions, EventData, PastEventOptions } from 'web3-eth-contract' +import { + ConfirmationServiceModel, + TxServiceModel, +} from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { DataDecoded } from 'src/routes/safe/store/models/types/transactions' +import { List, Map } from 'immutable' +import { PendingActionValues } from 'src/logic/safe/store/models/types/transaction' + +const mockNonPayableTransactionObject = (callResult?: string): NonPayableTransactionObject => { + return { + arguments: [], + call: (tx?) => new Promise((resolve) => resolve(callResult || '')), + encodeABI: (tx?) => '', + estimateGas: (tx?) => new Promise((resolve) => resolve(1000)), + send: () => { return {} as PromiEvent} + } +} + +type SafeMethodsProps = { + threshold?: string + nonce?: string + isOwnerUserAddress?: string, + name?: string, + version?: string +} + +export const getMockedSafeInstance = (safeProps: SafeMethodsProps): GnosisSafe => { + const { threshold = '1', nonce = '0', isOwnerUserAddress, name = 'safeName', version = '1.0.0' } = safeProps + return { + defaultAccount: undefined, + defaultBlock: undefined, + defaultChain: undefined, + defaultCommon: undefined, + defaultHardfork: undefined, + handleRevert: false, + options: undefined, + transactionBlockTimeout: 0, + transactionConfirmationBlocks: 0, + transactionPollingTimeout: 0, + clone(): GnosisSafe { + return undefined; + }, + constructor(jsonInterface: any[], address?: string, options?: ContractOptions): GnosisSafe { + return undefined; + }, + deploy(options: DeployOptions): ContractSendMethod { + return undefined; + }, + getPastEvents(event: string, options?: PastEventOptions | ((error: Error, event: EventData) => void), callback?: (error: Error, event: EventData) => void): Promise { + return undefined; + }, + once(event: "AddedOwner" | "ExecutionFromModuleSuccess" | "EnabledModule" | "ChangedMasterCopy" | "ExecutionFromModuleFailure" | "RemovedOwner" | "ApproveHash" | "DisabledModule" | "SignMsg" | "ExecutionSuccess" | "ChangedThreshold" | "ExecutionFailure", cb: any): void { + }, + events: { } as any, + methods: { + NAME: (): NonPayableTransactionObject => mockNonPayableTransactionObject(name) as NonPayableTransactionObject, + VERSION: (): NonPayableTransactionObject => mockNonPayableTransactionObject(version) as NonPayableTransactionObject, + addOwnerWithThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + approvedHashes: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + changeMasterCopy: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + changeThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + disableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + domainSeparator: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + enableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransactionFromModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransactionFromModuleReturnData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getModules: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject(threshold) as NonPayableTransactionObject, + isOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject(isOwnerUserAddress) as NonPayableTransactionObject, + nonce: (): NonPayableTransactionObject => mockNonPayableTransactionObject(nonce) as NonPayableTransactionObject, + removeOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + setFallbackHandler: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + signedMessages: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + swapOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + setup: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransaction: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + requiredTxGas: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + approveHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + signMessage: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + isValidSignature: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getMessageHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + encodeTransactionData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getTransactionHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + } as any + } +} + +type TransactionProps = { + baseGas?: number + blockNumber?: number | null + confirmations?: ConfirmationServiceModel[] + confirmationsRequired?: number + creationTx?: boolean | null + data?: string | null + dataDecoded?: DataDecoded + ethGasPrice?: string + executionDate?: string | null + executor?: string + fee?: string + gasPrice?: string + gasToken?: string + gasUsed?: number + isExecuted?: boolean + isSuccessful?: boolean + modified?: string + nonce?: number | null + operation?: number + origin?: string | null + ownersWithPendingActions?: Map>, + recipient?: string, + refundParams?: string, + refundReceiver?: string + safe?: string + safeTxGas?: number + safeTxHash?: string + signatures?: string + submissionDate?: string | null + to?: string + transactionHash?: string | null + value?: string +} + + +export const getMockedTxServiceModel = (txProps: TransactionProps): TxServiceModel => { + return { + baseGas: 0, + confirmations: [], + confirmationsRequired: 0, + creationTx: false, + data: null, + ethGasPrice: '', + executionDate: '', + executor: '', + fee: '', + gasPrice: '', + gasToken: '', + gasUsed: 0, + isExecuted: false, + isSuccessful: false, + modified: '', + nonce: 0, + operation: 0, + origin: '', + ownersWithPendingActions: Map(), + recipient: '', + refundParams: '', + refundReceiver: '', + safe: '', + safeTxGas: 0, + safeTxHash: '', + signatures: '', + submissionDate: '', + to: '', + transactionHash: '', + value: '', + ...txProps + } +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts deleted file mode 100644 index faf9f84e..00000000 --- a/src/utils/fetch.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const enhancedFetch = async (url, errMsg) => { - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) - - const sentData: any = { - mode: 'cors', - header, - } - - const response = await fetch(url, sentData) - if (!response.ok) { - return Promise.reject(new Error(errMsg)) - } - - return Promise.resolve(response.json()) -}