diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index bb70681e48..91a9f56902 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -36,10 +36,11 @@ expo-env.d.ts # Native *.orig.* *. +*.jks *.p8 *.p12 *.key -*. +*.mobileprovision # Metro .metro-health-check* diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 88dc2f152c..027e159c24 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -26,10 +26,10 @@ function RootLayout() { store.dispatch(apiSliceWithChainsConfig.endpoints.getChainsConfig.initiate()) return ( - - - - + + + + @@ -63,10 +63,10 @@ function RootLayout() { - - - - + + + + ) } diff --git a/apps/mobile/src/components/Badge/Badge.tsx b/apps/mobile/src/components/Badge/Badge.tsx index 4db8b7b31e..b42d9612b8 100644 --- a/apps/mobile/src/components/Badge/Badge.tsx +++ b/apps/mobile/src/components/Badge/Badge.tsx @@ -15,6 +15,7 @@ interface BadgeProps { circleProps?: Partial textContentProps?: Partial circular?: boolean + testID?: string } export const Badge = ({ @@ -25,6 +26,7 @@ export const Badge = ({ circular = true, circleProps, textContentProps, + testID, }: BadgeProps) => { let contentToRender = content if (typeof content === 'string') { @@ -38,7 +40,7 @@ export const Badge = ({ if (circular) { return ( - + {contentToRender} @@ -47,6 +49,7 @@ export const Badge = ({ return ( = { + title: 'ChainsDisplay', + component: ChainsDisplay, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + chains: mockedChains as unknown as Chain[], + max: 3, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const Truncated: Story = { + args: { + chains: mockedChains as unknown as Chain[], + max: 1, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const ActiveChain: Story = { + args: { + chains: mockedChains as unknown as Chain[], + activeChainId: mockedChains[1].chainId, + max: 1, + }, + parameters: { + layout: 'fullscreen', + }, +} diff --git a/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.test.tsx b/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.test.tsx new file mode 100644 index 0000000000..29c093dc0b --- /dev/null +++ b/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.test.tsx @@ -0,0 +1,30 @@ +import { mockedChains } from '@/src/store/constants' +import { ChainsDisplay } from './ChainsDisplay' +import { render } from '@testing-library/react-native' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' + +describe('ChainsDisplay', () => { + it('should render all chains next each other', () => { + const container = render() + + expect(container.getAllByTestId('chain-display')).toHaveLength(3) + }) + it('should truncate the chains when the provided chains length is greatter than the max', () => { + const container = render() + const moreChainsBadge = container.getByTestId('more-chains-badge') + + expect(container.getAllByTestId('chain-display')).toHaveLength(2) + expect(moreChainsBadge).toBeVisible() + expect(moreChainsBadge).toHaveTextContent('+1') + }) + + it('should always show the selected chain as the first column of the row', () => { + const container = render( + , + ) + + expect(container.getAllByTestId('chain-display')[0].children[0].props.accessibilityLabel).toBe( + mockedChains[2].chainName, + ) + }) +}) diff --git a/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.tsx b/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.tsx new file mode 100644 index 0000000000..d7a2731ec5 --- /dev/null +++ b/apps/mobile/src/components/ChainsDisplay/ChainsDisplay.tsx @@ -0,0 +1,34 @@ +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import React, { useMemo } from 'react' +import { View } from 'tamagui' +import { Logo } from '../Logo' +import { Badge } from '../Badge' + +interface ChainsDisplayProps { + chains: Chain[] + max?: number + activeChainId?: string +} + +export function ChainsDisplay({ chains, activeChainId, max }: ChainsDisplayProps) { + const orderedChains = useMemo( + () => [...chains].sort((a, b) => (a.chainId === activeChainId ? -1 : b.chainId === activeChainId ? 1 : 0)), + [chains], + ) + const slicedChains = max ? orderedChains.slice(0, max) : chains + const showBadge = max && chains.length > max + + return ( + + {slicedChains.map(({ chainLogoUri, chainName, chainId }, index) => ( + + + + ))} + + {showBadge && ( + + )} + + ) +} diff --git a/apps/mobile/src/components/ChainsDisplay/index.ts b/apps/mobile/src/components/ChainsDisplay/index.ts new file mode 100644 index 0000000000..5c0ef338de --- /dev/null +++ b/apps/mobile/src/components/ChainsDisplay/index.ts @@ -0,0 +1 @@ +export { ChainsDisplay } from './ChainsDisplay' diff --git a/apps/mobile/src/components/Dropdown/Dropdown.tsx b/apps/mobile/src/components/Dropdown/Dropdown.tsx index 45952f63f2..160673638a 100644 --- a/apps/mobile/src/components/Dropdown/Dropdown.tsx +++ b/apps/mobile/src/components/Dropdown/Dropdown.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useRef } from 'react' -import { H5, ScrollView, Text, View } from 'tamagui' +import { GetThemeValueForKey, H5, ScrollView, Text, View } from 'tamagui' import { SafeFontIcon } from '@/src/components/SafeFontIcon/SafeFontIcon' -import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet' +import { BottomSheetFooterProps, BottomSheetModal, BottomSheetModalProps, BottomSheetView } from '@gorhom/bottom-sheet' import { StyleSheet } from 'react-native' import { BackdropComponent, BackgroundComponent } from './sheetComponents' @@ -11,18 +11,32 @@ interface DropdownProps { children?: React.ReactNode dropdownTitle?: string items?: T[] + snapPoints?: BottomSheetModalProps['snapPoints'] + labelProps?: { + fontSize?: '$4' | '$5' | GetThemeValueForKey<'fontSize'> + fontWeight: 400 | 500 | 600 + } + footerComponent?: React.FC renderItem?: React.FC<{ item: T; onClose: () => void }> keyExtractor?: ({ item, index }: { item: T; index: number }) => string } +const defaultLabelProps = { + fontSize: '$4', + fontWeight: 400, +} as const + export function Dropdown({ label, leftNode, children, dropdownTitle, items, + snapPoints = [600, '90%'], keyExtractor, renderItem: Render, + labelProps = defaultLabelProps, + footerComponent, }: DropdownProps) { const bottomSheetModalRef = useRef(null) @@ -44,10 +58,11 @@ export function Dropdown({ onPress={handlePresentModalPress} flexDirection="row" marginBottom="$3" + columnGap="$2" > {leftNode} - + {label} @@ -56,19 +71,20 @@ export function Dropdown({ {dropdownTitle && ( -
+
{dropdownTitle}
)} @@ -95,6 +111,6 @@ export function Dropdown({ const styles = StyleSheet.create({ contentContainer: { paddingHorizontal: 20, - flex: 1, + justifyContent: 'space-around', }, }) diff --git a/apps/mobile/src/components/Logo/Logo.tsx b/apps/mobile/src/components/Logo/Logo.tsx index 9409ac759d..15bc1a4274 100644 --- a/apps/mobile/src/components/Logo/Logo.tsx +++ b/apps/mobile/src/components/Logo/Logo.tsx @@ -7,12 +7,19 @@ interface LogoProps { accessibilityLabel?: string fallbackIcon?: IconProps['name'] imageBackground?: string + size?: string } -export function Logo({ logoUri, accessibilityLabel, imageBackground = '$color', fallbackIcon = 'nft' }: LogoProps) { +export function Logo({ + logoUri, + accessibilityLabel, + size = '$10', + imageBackground = '$color', + fallbackIcon = 'nft', +}: LogoProps) { return ( - + {logoUri && ( = { + title: 'TransactionsList/AccountCard', + component: AccountCard, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + name: 'This is my account', + chains: mockedChains as unknown as Chain[], + owners: 5, + balance: mockedActiveSafeInfo.fiatTotal, + address: mockedActiveSafeInfo.address.value as Address, + threshold: 2, + }, + parameters: { + layout: 'fullscreen', + }, + render: ({ ...args }) => } />, +} + +export const TruncatedAccount: Story = { + args: { + name: 'This is my account with a very long text in one more test', + chains: mockedChains as unknown as Chain[], + owners: 5, + balance: mockedActiveSafeInfo.fiatTotal, + address: mockedActiveSafeInfo.address.value as Address, + threshold: 2, + }, + parameters: { + layout: 'fullscreen', + }, + render: ({ ...args }) => } />, +} diff --git a/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.test.tsx b/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.test.tsx new file mode 100644 index 0000000000..3a5243a62e --- /dev/null +++ b/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.test.tsx @@ -0,0 +1,45 @@ +import { render } from '@/src/tests/test-utils' +import { AccountCard } from './AccountCard' +import { mockedActiveSafeInfo, mockedChains } from '@/src/store/constants' +import { Address } from '@/src/types/address' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import { ellipsis } from '@/src/utils/formatters' + +describe('AccountCard', () => { + it('should render the account card with only one chain provided', () => { + const accountName = 'This is my account' + const container = render( + , + ) + expect(container.getByTestId('threshold-info-badge')).toBeVisible() + expect(container.getByText('2/5')).toBeDefined() + expect(container.getByText(`$${mockedActiveSafeInfo.fiatTotal}`)).toBeDefined() + expect(container.getByText(accountName)).toBeDefined() + }) + + it('should truncate the account information when they are very long', () => { + const longAccountName = 'This is my account with a very very long text' + const longBalance = '21312321312213213121221312321312312' + const container = render( + , + ) + expect(container.getByTestId('threshold-info-badge')).toBeVisible() + expect(container.getByText('2/5')).toBeDefined() + expect(container.getByText(`$${ellipsis(longBalance, 14)}`)).toBeDefined() + expect(container.getByText(ellipsis(longAccountName, 18))).toBeDefined() + }) +}) diff --git a/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.tsx b/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.tsx new file mode 100644 index 0000000000..7409501c45 --- /dev/null +++ b/apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { Text, View } from 'tamagui' +import { SafeListItem } from '@/src/components/SafeListItem' +import { ellipsis } from '@/src/utils/formatters' +import { IdenticonWithBadge } from '@/src/features/Settings/components/IdenticonWithBadge' +import { Address } from '@/src/types/address' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import { ChainsDisplay } from '@/src/components/ChainsDisplay' + +interface AccountCardProps { + name: string | Address + balance: string + address: Address + owners: number + threshold: number + rightNode?: string | React.ReactNode + chains: Chain[] +} + +export function AccountCard({ name, chains, owners, balance, address, threshold, rightNode }: AccountCardProps) { + return ( + + + {ellipsis(name, 18)} + + + ${ellipsis(balance, 14)} + +
+ } + leftNode={ + + + + } + rightNode={ + + + {rightNode} + + } + transparent + /> + ) +} diff --git a/apps/mobile/src/components/transactions-list/Card/AccountCard/index.ts b/apps/mobile/src/components/transactions-list/Card/AccountCard/index.ts new file mode 100644 index 0000000000..d692abb08d --- /dev/null +++ b/apps/mobile/src/components/transactions-list/Card/AccountCard/index.ts @@ -0,0 +1 @@ +export { AccountCard } from './AccountCard' diff --git a/apps/mobile/src/config/constants.ts b/apps/mobile/src/config/constants.ts index 47b9bee8ee..158774b6b7 100644 --- a/apps/mobile/src/config/constants.ts +++ b/apps/mobile/src/config/constants.ts @@ -1,7 +1,9 @@ import Constants from 'expo-constants' import { Platform } from 'react-native' -export const isProduction = process.env.NODE_ENV !== 'production' +// export const isProduction = process.env.NODE_ENV === 'production' +// TODO: put it to get from process.env.NODE_ENV once we remove the mocks for the user account. +export const isProduction = true export const isAndroid = Platform.OS === 'android' export const isTestingEnv = process.env.NODE_ENV === 'test' export const isStorybookEnv = Constants?.expoConfig?.extra?.storybookEnabled === 'true' @@ -10,4 +12,4 @@ export const POLLING_INTERVAL = 15_000 export const GATEWAY_URL_PRODUCTION = process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' export const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' -export const GATEWAY_URL = process.env.NODE_ENV !== 'production' ? GATEWAY_URL_STAGING : GATEWAY_URL_PRODUCTION +export const GATEWAY_URL = isProduction ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING diff --git a/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.stories.tsx b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.stories.tsx new file mode 100644 index 0000000000..26b24209d4 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.stories.tsx @@ -0,0 +1,64 @@ +import { AccountItem } from './AccountItem' +import { Meta, StoryObj } from '@storybook/react/*' +import { mockedActiveSafeInfo, mockedChains } from '@/src/store/constants' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import { action } from '@storybook/addon-actions' +import { Address } from '@/src/types/address' + +const meta: Meta = { + title: 'Assets/AccountItem', + component: AccountItem, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + account: mockedActiveSafeInfo, + chains: mockedChains as unknown as Chain[], + activeAccount: '0x123', + onSelect: action('onSelect'), + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const ActiveAccount: Story = { + args: { + account: mockedActiveSafeInfo, + chains: mockedChains as unknown as Chain[], + activeAccount: mockedActiveSafeInfo.address.value as Address, + onSelect: action('onSelect'), + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const TruncatedAccountChains: Story = { + args: { + account: mockedActiveSafeInfo, + chains: [...mockedChains, ...mockedChains, ...mockedChains] as unknown as Chain[], + activeAccount: '0x12312', + onSelect: action('onSelect'), + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const TruncatedActiveAccountChains: Story = { + args: { + account: mockedActiveSafeInfo, + chains: [...mockedChains, ...mockedChains, ...mockedChains] as unknown as Chain[], + activeAccount: mockedActiveSafeInfo.address.value as Address, + onSelect: action('onSelect'), + }, + parameters: { + layout: 'fullscreen', + }, +} diff --git a/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.test.tsx b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.test.tsx new file mode 100644 index 0000000000..9f3573e921 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.test.tsx @@ -0,0 +1,59 @@ +import { render, userEvent } from '@/src/tests/test-utils' +import AccountItem from './AccountItem' +import { mockedActiveSafeInfo, mockedChains } from '@/src/store/constants' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import { shortenAddress } from '@/src/utils/formatters' +import { Address } from '@/src/types/address' + +describe('AccountItem', () => { + it('should render a unselected AccountItem', () => { + const container = render( + , + ) + + expect(container.getByTestId('account-item-wrapper')).toHaveStyle({ backgroundColor: 'transparent' }) + expect(container.getByText(shortenAddress(mockedActiveSafeInfo.address.value))).toBeDefined() + expect(container.getByText(`${mockedActiveSafeInfo.threshold}/${mockedActiveSafeInfo.owners.length}`)).toBeDefined() + expect(container.getByText(`$${mockedActiveSafeInfo.fiatTotal}`)).toBeVisible() + expect(container.getAllByTestId('chain-display')).toHaveLength(mockedChains.length) + }) + + it('should render a selected AccountItem', () => { + const container = render( + , + ) + + expect(container.getByTestId('account-item-wrapper')).toHaveStyle({ backgroundColor: '#DCDEE0' }) + expect(container.getByText(shortenAddress(mockedActiveSafeInfo.address.value))).toBeDefined() + expect(container.getByText(`${mockedActiveSafeInfo.threshold}/${mockedActiveSafeInfo.owners.length}`)).toBeDefined() + expect(container.getByText(`$${mockedActiveSafeInfo.fiatTotal}`)).toBeVisible() + expect(container.getAllByTestId('chain-display')).toHaveLength(mockedChains.length) + }) + + it('should trigger an event when user clicks in the account item', async () => { + const spyFn = jest.fn() + const user = userEvent.setup() + const container = render( + , + ) + + await user.press(container.getByTestId('account-item-wrapper')) + + expect(spyFn).toHaveBeenNthCalledWith(1, mockedActiveSafeInfo.address.value) + }) +}) diff --git a/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.tsx b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.tsx new file mode 100644 index 0000000000..2d3c2d7cec --- /dev/null +++ b/apps/mobile/src/features/Assets/components/AccountItem/AccountItem.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { TouchableOpacity } from 'react-native' +import { View } from 'tamagui' +import { SafeFontIcon } from '@/src/components/SafeFontIcon' +import { AccountCard } from '@/src/components/transactions-list/Card/AccountCard' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' +import { Address } from '@/src/types/address' +import { SafeOverview } from '@safe-global/store/gateway/AUTO_GENERATED/safes' +import { shortenAddress } from '@/src/utils/formatters' + +interface AccountItemProps { + chains: Chain[] + account: SafeOverview + activeAccount: Address + onSelect: (accountAddress: string) => void +} + +// TODO: These props needs to come from the AccountItem.container component +// remove this comment once it is done +export function AccountItem({ account, chains, activeAccount, onSelect }: AccountItemProps) { + const isActive = activeAccount === account.address.value + + const handleChainSelect = () => { + onSelect(account.address.value) + } + + return ( + + + } + /> + + + ) +} + +export default AccountItem diff --git a/apps/mobile/src/features/Assets/components/AccountItem/index.ts b/apps/mobile/src/features/Assets/components/AccountItem/index.ts new file mode 100644 index 0000000000..1a0911a316 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/AccountItem/index.ts @@ -0,0 +1,2 @@ +import { AccountItem } from './AccountItem' +export { AccountItem } diff --git a/apps/mobile/src/features/Assets/components/Balance/Balance.container.tsx b/apps/mobile/src/features/Assets/components/Balance/Balance.container.tsx index f9997d588c..23c8b5cb9c 100644 --- a/apps/mobile/src/features/Assets/components/Balance/Balance.container.tsx +++ b/apps/mobile/src/features/Assets/components/Balance/Balance.container.tsx @@ -4,16 +4,20 @@ import { useSafesGetSafeOverviewV1Query } from '@safe-global/store/gateway/AUTO_ import { selectActiveSafe } from '@/src/store/activeSafeSlice' import { SafeOverviewResult } from '@safe-global/store/gateway/types' import { POLLING_INTERVAL } from '@/src/config/constants' -import { selectAllChains } from '@/src/store/chains' +import { getChainsByIds, selectAllChains } from '@/src/store/chains' import { Balance } from './Balance' - -const makeSafeId = (chainId: string, address: string) => `${chainId}:${address}` as `${number}:0x${string}` +import { makeSafeId } from '@/src/utils/formatters' +import { RootState } from '@/src/store' +import { selectActiveSafeInfo } from '@/src/store/safesSlice' export function BalanceContainer() { const activeChain = useSelector(selectActiveChain) const chains = useSelector(selectAllChains) const activeSafe = useSelector(selectActiveSafe) const dispatch = useDispatch() + const activeSafeInfo = useSelector((state: RootState) => selectActiveSafeInfo(state, activeSafe.address)) + const activeSafeChains = useSelector((state: RootState) => getChainsByIds(state, activeSafeInfo.chains)) + const { data, isLoading } = useSafesGetSafeOverviewV1Query( { safes: chains.map((chain) => makeSafeId(chain.chainId, activeSafe.address)).join(','), @@ -34,7 +38,7 @@ export function BalanceContainer() { return ( label={activeChain?.chainName} dropdownTitle="Select network:" - leftNode={ - activeChain?.chainLogoUri && ( - - ) - } + leftNode={} items={data} keyExtractor={({ item }) => item.chainId} renderItem={({ item, onClose }) => ( diff --git a/apps/mobile/src/features/Assets/components/Balance/ChainItems.tsx b/apps/mobile/src/features/Assets/components/Balance/ChainItems.tsx index 2bf3fd6514..14af89b06f 100644 --- a/apps/mobile/src/features/Assets/components/Balance/ChainItems.tsx +++ b/apps/mobile/src/features/Assets/components/Balance/ChainItems.tsx @@ -32,7 +32,7 @@ export function ChainItems({ chainId, chains, activeChain, fiatTotal, onSelect } name={chain.chainName} logoUri={chain.chainLogoUri} description={`${fiatTotal}`} - rightNode={isActive && } + rightNode={isActive && } />
diff --git a/apps/mobile/src/features/Assets/components/MyAccounts/MyAccounts.container.tsx b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccounts.container.tsx new file mode 100644 index 0000000000..aac4ed881d --- /dev/null +++ b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccounts.container.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { AccountItem } from '../AccountItem' +import { SafesSliceItem } from '@/src/store/safesSlice' +import { Address } from '@/src/types/address' +import { useDispatch, useSelector } from 'react-redux' +import { selectActiveSafe, setActiveSafe } from '@/src/store/activeSafeSlice' +import { getChainsByIds } from '@/src/store/chains' +import { RootState } from '@/src/store' +import { switchActiveChain } from '@/src/store/activeChainSlice' +import { useMyAccountsService } from './hooks/useMyAccountsService' + +interface MyAccountsContainerProps { + item: SafesSliceItem + onClose: () => void +} + +export function MyAccountsContainer({ item, onClose }: MyAccountsContainerProps) { + useMyAccountsService(item) + + const dispatch = useDispatch() + const activeSafe = useSelector(selectActiveSafe) + const filteredChains = useSelector((state: RootState) => getChainsByIds(state, item.chains)) + + const handleAccountSelected = () => { + const chainId = item.chains[0] + + dispatch( + setActiveSafe({ + address: item.SafeInfo.address.value as Address, + chainId, + }), + ) + dispatch(switchActiveChain({ id: chainId })) + + onClose() + } + + return ( + + ) +} diff --git a/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.test.tsx b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.test.tsx new file mode 100644 index 0000000000..76b422a0ff --- /dev/null +++ b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.test.tsx @@ -0,0 +1,12 @@ +import { render } from '@/src/tests/test-utils' +import { MyAccountsFooter } from './MyAccountsFooter' +import { SharedValue } from 'react-native-reanimated' + +describe('MyAccountsFooter', () => { + it('should render the defualt template', () => { + const container = render(} />) + + expect(container.getByText('Add Existing Account')).toBeDefined() + expect(container.getByText('Join New Account')).toBeDefined() + }) +}) diff --git a/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.tsx b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.tsx new file mode 100644 index 0000000000..7d930ce395 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.tsx @@ -0,0 +1,58 @@ +import { Badge } from '@/src/components/Badge' +import { SafeFontIcon } from '@/src/components/SafeFontIcon' +import { BottomSheetFooter, BottomSheetFooterProps } from '@gorhom/bottom-sheet' +import React from 'react' +import { TouchableOpacity } from 'react-native' +import { styled, Text, View } from 'tamagui' + +const MyAccountsFooterContainer = styled(View, { + borderTopWidth: 1, + borderTopColor: '$colorSecondary', + paddingVertical: '$7', + paddingHorizontal: '$5', + backgroundColor: '$backgroundPaper', +}) + +const MyAccountsButton = styled(View, { + columnGap: '$3', + alignItems: 'center', + flexDirection: 'row', + marginBottom: '$7', +}) + +interface CustomFooterProps extends BottomSheetFooterProps {} + +export function MyAccountsFooter({ animatedFooterPosition }: CustomFooterProps) { + const onAddAccountClick = () => null + const onJoinAccountClick = () => null + + return ( + + + + + } + /> + + + Add Existing Account + + + + + + + } /> + + + Join New Account + + + + + + ) +} diff --git a/apps/mobile/src/features/Assets/components/MyAccounts/hooks/useMyAccountsService.ts b/apps/mobile/src/features/Assets/components/MyAccounts/hooks/useMyAccountsService.ts new file mode 100644 index 0000000000..d590807f76 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/MyAccounts/hooks/useMyAccountsService.ts @@ -0,0 +1,45 @@ +import { useSafesGetSafeOverviewV1Query } from '@safe-global/store/gateway/AUTO_GENERATED/safes' +import { SafeOverviewResult } from '@safe-global/store/gateway/types' +import { useEffect, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { selectAllChainsIds } from '@/src/store/chains' +import { SafesSliceItem, updateSafeInfo } from '@/src/store/safesSlice' +import { Address } from '@/src/types/address' +import { makeSafeId } from '@/src/utils/formatters' + +export const useMyAccountsService = (item: SafesSliceItem) => { + const dispatch = useDispatch() + const chainIds = useSelector(selectAllChainsIds) + const safes = useMemo( + () => chainIds.map((chainId: string) => makeSafeId(chainId, item.SafeInfo.address.value)).join(','), + [chainIds, item.SafeInfo.address.value], + ) + const { data } = useSafesGetSafeOverviewV1Query({ + safes, + currency: 'usd', + trusted: true, + excludeSpam: true, + }) + + useEffect(() => { + if (!data) { + return + } + + const safe = data[0] + + dispatch( + updateSafeInfo({ + address: safe.address.value as Address, + item: { + chains: data.map((safeInfo) => safeInfo.chainId), + SafeInfo: { + ...safe, + fiatTotal: data.reduce((prev, { fiatTotal }) => parseFloat(fiatTotal) + prev, 0).toString(), + }, + }, + }), + ) + }, [data, dispatch]) +} diff --git a/apps/mobile/src/features/Assets/components/MyAccounts/index.ts b/apps/mobile/src/features/Assets/components/MyAccounts/index.ts new file mode 100644 index 0000000000..e1d3d5f3b1 --- /dev/null +++ b/apps/mobile/src/features/Assets/components/MyAccounts/index.ts @@ -0,0 +1,2 @@ +export { MyAccountsFooter } from './MyAccountsFooter' +export { MyAccountsContainer } from './MyAccounts.container' diff --git a/apps/mobile/src/features/Assets/components/NFTs/NFTs.container.tsx b/apps/mobile/src/features/Assets/components/NFTs/NFTs.container.tsx index 6ee3ca8e13..d201de1cfe 100644 --- a/apps/mobile/src/features/Assets/components/NFTs/NFTs.container.tsx +++ b/apps/mobile/src/features/Assets/components/NFTs/NFTs.container.tsx @@ -1,5 +1,5 @@ import { safelyDecodeURIComponent } from 'expo-router/build/fork/getStateFromPath-forks' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useSelector } from 'react-redux' import { SafeTab } from '@/src/components/SafeTab' @@ -13,15 +13,17 @@ import { import { Fallback } from '../Fallback' import { NFTItem } from './NFTItem' +import { selectActiveChain } from '@/src/store/activeChainSlice' +import { useInfiniteScroll } from '@/src/hooks/useInfiniteScroll' export function NFTsContainer() { + const activeChain = useSelector(selectActiveChain) const activeSafe = useSelector(selectActiveSafe) const [pageUrl, setPageUrl] = useState() - const [list, setList] = useState() - const { data, isLoading, error, refetch } = useCollectiblesGetCollectiblesV2Query( + const { data, isFetching, error, refetch } = useCollectiblesGetCollectiblesV2Query( { - chainId: activeSafe.chainId, + chainId: activeChain.chainId, safeAddress: activeSafe.address, cursor: pageUrl && safelyDecodeURIComponent(pageUrl?.split('cursor=')[1]), }, @@ -29,26 +31,14 @@ export function NFTsContainer() { pollingInterval: POLLING_INTERVAL, }, ) - - useEffect(() => { - if (!data?.results) { - return - } - - setList((prev) => (prev ? [...prev, ...data.results] : data.results)) - }, [data]) - - const onEndReached = () => { - if (!data?.next) { - return - } - - setPageUrl(data.next) - refetch() - } - - if (isLoading || !list?.length || error) { - return + const { list, onEndReached } = useInfiniteScroll({ + refetch, + setPageUrl, + data, + }) + + if (isFetching || !list?.length || error) { + return } return ( diff --git a/apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx b/apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx index 0700470f2e..f67f461720 100644 --- a/apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx +++ b/apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx @@ -1,31 +1,42 @@ import { useSelector } from 'react-redux' import { selectActiveSafe } from '@/src/store/activeSafeSlice' -import { Text, View } from 'tamagui' +import { View } from 'tamagui' import { BlurredIdenticonBackground } from '@/src/components/BlurredIdenticonBackground' import { SafeAreaView } from 'react-native-safe-area-context' import { Identicon } from '@/src/components/Identicon' import { shortenAddress } from '@/src/utils/formatters' import { SafeFontIcon } from '@/src/components/SafeFontIcon' import { StyleSheet, TouchableOpacity } from 'react-native' -import React from 'react' +import React, { useMemo } from 'react' import { Address } from '@/src/types/address' +import { Dropdown } from '@/src/components/Dropdown' +import { MyAccountsContainer, MyAccountsFooter } from '../MyAccounts' +import { SafesSliceItem, selectAllSafes } from '@/src/store/safesSlice' + +const dropdownLabelProps = { + fontSize: '$5', + fontWeight: 600, +} as const export const Navbar = () => { const activeSafe = useSelector(selectActiveSafe) + const safes = useSelector(selectAllSafes) + const memoizedSafes = useMemo(() => Object.values(safes), [safes]) + return ( - - - - - - {shortenAddress(activeSafe.address)} - - - - + + label={shortenAddress(activeSafe.address)} + labelProps={dropdownLabelProps} + dropdownTitle="My accounts" + leftNode={} + items={memoizedSafes} + keyExtractor={({ item }) => item.SafeInfo.address.value} + footerComponent={MyAccountsFooter} + renderItem={MyAccountsContainer} + /> diff --git a/apps/mobile/src/features/Assets/components/Tokens/Tokens.container.tsx b/apps/mobile/src/features/Assets/components/Tokens/Tokens.container.tsx index bba93e7497..29b5ca90c5 100644 --- a/apps/mobile/src/features/Assets/components/Tokens/Tokens.container.tsx +++ b/apps/mobile/src/features/Assets/components/Tokens/Tokens.container.tsx @@ -9,17 +9,17 @@ import { POLLING_INTERVAL } from '@/src/config/constants' import { selectActiveSafe } from '@/src/store/activeSafeSlice' import { Balance, useBalancesGetBalancesV1Query } from '@safe-global/store/gateway/AUTO_GENERATED/balances' import { formatValue } from '@/src/utils/formatters' -// import { selectActiveChain } from '@/src/store/activeChainSlice' +import { selectActiveChain } from '@/src/store/activeChainSlice' import { Fallback } from '../Fallback' export function TokensContainer() { const activeSafe = useSelector(selectActiveSafe) - // const activeChain = useSelector(selectActiveChain) + const activeChain = useSelector(selectActiveChain) - const { data, isLoading, error } = useBalancesGetBalancesV1Query( + const { data, isFetching, error } = useBalancesGetBalancesV1Query( { - chainId: activeSafe.chainId, + chainId: activeChain.chainId, fiatCode: 'USD', safeAddress: activeSafe.address, excludeSpam: false, @@ -45,8 +45,8 @@ export function TokensContainer() { ) }, []) - if (isLoading || !data?.items.length || error) { - return + if (isFetching || !data?.items.length || error) { + return } return ( diff --git a/apps/mobile/src/features/Settings/components/IdenticonWithBadge/IdenticonWithBadge.tsx b/apps/mobile/src/features/Settings/components/IdenticonWithBadge/IdenticonWithBadge.tsx index 43052e96d8..7e4ed2c009 100644 --- a/apps/mobile/src/features/Settings/components/IdenticonWithBadge/IdenticonWithBadge.tsx +++ b/apps/mobile/src/features/Settings/components/IdenticonWithBadge/IdenticonWithBadge.tsx @@ -6,15 +6,17 @@ import React from 'react' import { StyleSheet } from 'react-native' import { Address } from '@/src/types/address' -type Props = { +type IdenticonWithBadgeProps = { address: Address badgeContent?: string + size?: number + testID?: string } -export const IdenticonWithBadge = ({ address, badgeContent }: Props) => { +export const IdenticonWithBadge = ({ address, testID, badgeContent, size = 56 }: IdenticonWithBadgeProps) => { return ( - - + + {badgeContent && ( diff --git a/apps/mobile/src/hooks/useInfiniteScroll/index.ts b/apps/mobile/src/hooks/useInfiniteScroll/index.ts new file mode 100644 index 0000000000..02e13ff6f4 --- /dev/null +++ b/apps/mobile/src/hooks/useInfiniteScroll/index.ts @@ -0,0 +1 @@ +export { useInfiniteScroll } from './useInfiniteScroll' diff --git a/apps/mobile/src/hooks/useInfiniteScroll/useInfiniteScroll.ts b/apps/mobile/src/hooks/useInfiniteScroll/useInfiniteScroll.ts new file mode 100644 index 0000000000..fc2290b3e1 --- /dev/null +++ b/apps/mobile/src/hooks/useInfiniteScroll/useInfiniteScroll.ts @@ -0,0 +1,39 @@ +import { selectActiveSafe } from '@/src/store/activeSafeSlice' +import { useCallback, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +type TUseInfiniteScrollData = { results: J[]; next?: string | null } + +type TUseInfiniteScrollConfig = { + refetch: () => void + setPageUrl: (nextUrl?: string) => void + data: (T & TUseInfiniteScrollData) | undefined +} + +export const useInfiniteScroll = ({ refetch, setPageUrl, data }: TUseInfiniteScrollConfig) => { + const activeSafe = useSelector(selectActiveSafe) + const [list, setList] = useState([]) + + useEffect(() => { + setList([]) + }, [activeSafe]) + + useEffect(() => { + if (!data?.results) { + return + } + + setList((prev) => (prev ? [...prev, ...data.results] : data.results)) + }, [data]) + + const onEndReached = useCallback(() => { + if (!data?.next) { + return + } + + setPageUrl(data.next) + refetch() + }, [data, refetch, setPageUrl]) + + return { list, onEndReached } +} diff --git a/apps/mobile/src/hooks/usePendingTxs/index.ts b/apps/mobile/src/hooks/usePendingTxs/index.ts index ec803e2a06..16ac8c0d78 100644 --- a/apps/mobile/src/hooks/usePendingTxs/index.ts +++ b/apps/mobile/src/hooks/usePendingTxs/index.ts @@ -1,43 +1,39 @@ import { useGetPendingTxsQuery } from '@safe-global/store/gateway' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { useSelector } from 'react-redux' -import { QueuedItemPage } from '@safe-global/store/gateway/AUTO_GENERATED/transactions' +import { + ConflictHeaderQueuedItem, + LabelQueuedItem, + QueuedItemPage, + TransactionQueuedItem, +} from '@safe-global/store/gateway/AUTO_GENERATED/transactions' import { groupPendingTxs } from '@/src/features/PendingTx/utils' import { selectActiveSafe } from '@/src/store/activeSafeSlice' +import { safelyDecodeURIComponent } from 'expo-router/build/fork/getStateFromPath-forks' +import { useInfiniteScroll } from '../useInfiniteScroll' const usePendingTxs = () => { const activeSafe = useSelector(selectActiveSafe) - const [list, setList] = useState([]) const [pageUrl, setPageUrl] = useState() const { data, isLoading, isFetching, refetch, isUninitialized } = useGetPendingTxsQuery( { chainId: activeSafe.chainId, safeAddress: activeSafe.address, - cursor: pageUrl, + cursor: pageUrl && safelyDecodeURIComponent(pageUrl?.split('cursor=')[1]), }, { skip: !activeSafe.chainId, }, ) - - useEffect(() => { - if (!data?.results) { - return - } - - setList((prev) => [...prev, ...data.results]) - }, [data]) - - const fetchMoreTx = async () => { - if (!data?.next) { - return - } - - setPageUrl(data.next) - - refetch() - } + const { list, onEndReached: fetchMoreTx } = useInfiniteScroll< + QueuedItemPage, + ConflictHeaderQueuedItem | LabelQueuedItem | TransactionQueuedItem + >({ + refetch, + setPageUrl, + data, + }) const pendingTxs = useMemo(() => groupPendingTxs(list || []), [list]) diff --git a/apps/mobile/src/store/activeChainSlice.ts b/apps/mobile/src/store/activeChainSlice.ts index fc5dc964e6..db064e6a39 100644 --- a/apps/mobile/src/store/activeChainSlice.ts +++ b/apps/mobile/src/store/activeChainSlice.ts @@ -1,8 +1,9 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '.' import { selectChainById } from './chains' +import { mockedActiveAccount } from './constants' -const initialState = { id: '1' } +const initialState = { id: mockedActiveAccount.chainId } const activeChainSlice = createSlice({ name: 'activeChain', diff --git a/apps/mobile/src/store/activeSafeSlice.ts b/apps/mobile/src/store/activeSafeSlice.ts index f600545480..adaf98cb14 100644 --- a/apps/mobile/src/store/activeSafeSlice.ts +++ b/apps/mobile/src/store/activeSafeSlice.ts @@ -1,15 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { Address } from '@/src/types/address' import { RootState } from '.' - -interface SafeInfo { - address: Address - chainId: string -} +import { mockedActiveAccount } from './constants' +import { SafeInfo } from '../types/address' const initialState: SafeInfo = { - address: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', - chainId: '1', + address: mockedActiveAccount.address, + chainId: mockedActiveAccount.chainId, } const activeSafeSlice = createSlice({ diff --git a/apps/mobile/src/store/chains/index.ts b/apps/mobile/src/store/chains/index.ts index ed675e21b7..5872031662 100644 --- a/apps/mobile/src/store/chains/index.ts +++ b/apps/mobile/src/store/chains/index.ts @@ -1,6 +1,7 @@ import { apiSliceWithChainsConfig, chainsAdapter, initialState } from '@safe-global/store/gateway/chains' import { createSelector } from '@reduxjs/toolkit' import { RootState } from '..' +import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains' const selectChainsResult = apiSliceWithChainsConfig.endpoints.getChainsConfig.select() @@ -11,5 +12,18 @@ const selectChainsData = createSelector(selectChainsResult, (result) => { const { selectAll: selectAllChains, selectById } = chainsAdapter.getSelectors(selectChainsData) export const selectChainById = (state: RootState, chainId: string) => selectById(state, chainId) +export const selectAllChainsIds = createSelector([selectAllChains], (chains: Chain[]) => + chains.map((chain) => chain.chainId), +) + +export const getChainsByIds = createSelector( + [ + // Pass the root state and chainIds array as dependencies + (state: RootState) => state, + (_state: RootState, chainIds: string[]) => chainIds, + ], + (state, chainIds) => chainIds.map((chainId) => selectById(state, chainId)), +) + export const { useGetChainsConfigQuery } = apiSliceWithChainsConfig export { selectAllChains } diff --git a/apps/mobile/src/store/constants.ts b/apps/mobile/src/store/constants.ts new file mode 100644 index 0000000000..c71e0af9e3 --- /dev/null +++ b/apps/mobile/src/store/constants.ts @@ -0,0 +1,262 @@ +import { SafeOverview } from '@safe-global/store/gateway/AUTO_GENERATED/safes' +import { SafeInfo } from '../types/address' + +export const mockedActiveAccount: SafeInfo = { + address: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', + chainId: '1', +} + +export const mockedActiveSafeInfo: SafeOverview = { + address: { value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', name: null, logoUri: null }, + awaitingConfirmation: null, + chainId: mockedActiveAccount.chainId, + fiatTotal: '758.926', + owners: [{ value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', name: null, logoUri: null }], + queued: 1, + threshold: 1, +} + +export const mockedAccounts = [ + mockedActiveSafeInfo, + { + address: { value: '0xc7c2E116A3027D0BFd9817781c717A81a8bC5518', name: null, logoUri: null }, + awaitingConfirmation: null, + chainId: '42161', + fiatTotal: '0', + owners: [{ value: '0xc7c2E116A3027D0BFd9817781c717A81a8bC5518', name: null, logoUri: null }], + queued: 1, + threshold: 1, + }, +] + +export const mockedChains = [ + { + balancesProvider: { chainName: 'xdai', enabled: true }, + beaconChainExplorerUriTemplate: { publicKey: null }, + blockExplorerUriTemplate: { + address: 'https://gnosisscan.io/address/{{address}}', + api: 'https://api.gnosisscan.io/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}', + txHash: 'https://gnosisscan.io/tx/{{txHash}}/', + }, + chainId: '100', + chainLogoUri: 'https://safe-transaction-assets.safe.global/chains/100/chain_logo.png', + chainName: 'Gnosis Chain', + contractAddresses: { + createCallAddress: null, + fallbackHandlerAddress: null, + multiSendAddress: null, + multiSendCallOnlyAddress: null, + safeProxyFactoryAddress: null, + safeSingletonAddress: null, + safeWebAuthnSignerFactoryAddress: null, + signMessageLibAddress: null, + simulateTxAccessorAddress: null, + }, + description: '', + disabledWallets: [ + 'keystone', + 'ledger_v2', + 'NONE', + 'opera', + 'operaTouch', + 'pk', + 'safeMobile', + 'tally', + 'trust', + 'walletConnect', + ], + ensRegistryAddress: null, + features: [ + 'COUNTERFACTUAL', + 'DEFAULT_TOKENLIST', + 'DELETE_TX', + 'EIP1271', + 'EIP1559', + 'ERC721', + 'MULTI_CHAIN_SAFE_ADD_NETWORK', + 'MULTI_CHAIN_SAFE_CREATION', + 'NATIVE_SWAPS', + 'NATIVE_SWAPS_FEE_ENABLED', + 'NATIVE_WALLETCONNECT', + 'PROPOSERS', + 'PUSH_NOTIFICATIONS', + 'RECOVERY', + 'RELAYING', + 'RELAYING_MOBILE', + 'RISK_MITIGATION', + 'SAFE_141', + 'SAFE_APPS', + 'SPEED_UP_TX', + 'SPENDING_LIMIT', + 'TX_SIMULATION', + 'ZODIAC_ROLES', + ], + gasPrice: [], + isTestnet: false, + l2: true, + nativeCurrency: { + decimals: 18, + logoUri: 'https://safe-transaction-assets.safe.global/chains/100/currency_logo.png', + name: 'xDai', + symbol: 'XDAI', + }, + publicRpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://rpc.gnosischain.com/' }, + rpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://rpc.gnosischain.com/' }, + safeAppsRpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://rpc.gnosischain.com/' }, + shortName: 'gno', + theme: { backgroundColor: '#48A9A6', textColor: '#ffffff' }, + transactionService: 'https://safe-transaction-gnosis-chain.safe.global', + }, + { + balancesProvider: { chainName: 'polygon', enabled: true }, + beaconChainExplorerUriTemplate: { publicKey: null }, + blockExplorerUriTemplate: { + address: 'https://polygonscan.com/address/{{address}}', + api: 'https://api.polygonscan.com/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}', + txHash: 'https://polygonscan.com/tx/{{txHash}}', + }, + chainId: '137', + chainLogoUri: 'https://safe-transaction-assets.safe.global/chains/137/chain_logo.png', + chainName: 'Polygon', + contractAddresses: { + createCallAddress: null, + fallbackHandlerAddress: null, + multiSendAddress: null, + multiSendCallOnlyAddress: null, + safeProxyFactoryAddress: null, + safeSingletonAddress: null, + safeWebAuthnSignerFactoryAddress: null, + signMessageLibAddress: null, + simulateTxAccessorAddress: null, + }, + description: 'L2 chain', + disabledWallets: [ + 'keystone', + 'ledger_v2', + 'NONE', + 'opera', + 'operaTouch', + 'pk', + 'safeMobile', + 'socialSigner', + 'tally', + 'trezor', + 'trust', + 'walletConnect', + ], + ensRegistryAddress: null, + features: [ + 'COUNTERFACTUAL', + 'DEFAULT_TOKENLIST', + 'DELETE_TX', + 'EIP1271', + 'EIP1559', + 'ERC721', + 'MOONPAY_MOBILE', + 'MULTI_CHAIN_SAFE_ADD_NETWORK', + 'MULTI_CHAIN_SAFE_CREATION', + 'NATIVE_WALLETCONNECT', + 'PROPOSERS', + 'PUSH_NOTIFICATIONS', + 'RECOVERY', + 'RELAYING', + 'RISK_MITIGATION', + 'SAFE_141', + 'SAFE_APPS', + 'SPEED_UP_TX', + 'SPENDING_LIMIT', + 'TX_SIMULATION', + 'ZODIAC_ROLES', + ], + gasPrice: [], + isTestnet: false, + l2: true, + nativeCurrency: { + decimals: 18, + logoUri: 'https://safe-transaction-assets.safe.global/chains/137/currency_logo.png', + name: 'POL (ex-MATIC)', + symbol: 'POL', + }, + publicRpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://polygon-rpc.com' }, + rpcUri: { authentication: 'API_KEY_PATH', value: 'https://polygon-mainnet.infura.io/v3/' }, + safeAppsRpcUri: { authentication: 'API_KEY_PATH', value: 'https://polygon-mainnet.infura.io/v3/' }, + shortName: 'matic', + theme: { backgroundColor: '#8248E5', textColor: '#ffffff' }, + transactionService: 'https://safe-transaction-polygon.safe.global', + }, + { + balancesProvider: { chainName: 'arbitrum', enabled: true }, + beaconChainExplorerUriTemplate: { publicKey: null }, + blockExplorerUriTemplate: { + address: 'https://arbiscan.io/address/{{address}}', + api: 'https://api.arbiscan.io/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}', + txHash: 'https://arbiscan.io/tx/{{txHash}}', + }, + chainId: '42161', + chainLogoUri: 'https://safe-transaction-assets.safe.global/chains/42161/chain_logo.png', + chainName: 'Arbitrum', + contractAddresses: { + createCallAddress: null, + fallbackHandlerAddress: null, + multiSendAddress: null, + multiSendCallOnlyAddress: null, + safeProxyFactoryAddress: null, + safeSingletonAddress: null, + safeWebAuthnSignerFactoryAddress: null, + signMessageLibAddress: null, + simulateTxAccessorAddress: null, + }, + description: '', + disabledWallets: [ + 'keystone', + 'ledger_v2', + 'NONE', + 'opera', + 'operaTouch', + 'pk', + 'safeMobile', + 'socialSigner', + 'tally', + 'trust', + 'walletConnect', + ], + ensRegistryAddress: null, + features: [ + 'COUNTERFACTUAL', + 'DEFAULT_TOKENLIST', + 'DELETE_TX', + 'EIP1271', + 'ERC721', + 'MOONPAY_MOBILE', + 'MULTI_CHAIN_SAFE_ADD_NETWORK', + 'MULTI_CHAIN_SAFE_CREATION', + 'NATIVE_SWAPS', + 'NATIVE_SWAPS_FEE_ENABLED', + 'NATIVE_WALLETCONNECT', + 'PROPOSERS', + 'PUSH_NOTIFICATIONS', + 'RECOVERY', + 'RISK_MITIGATION', + 'SAFE_141', + 'SAFE_APPS', + 'SPEED_UP_TX', + 'TX_SIMULATION', + 'ZODIAC_ROLES', + ], + gasPrice: [], + isTestnet: false, + l2: true, + nativeCurrency: { + decimals: 18, + logoUri: 'https://safe-transaction-assets.safe.global/chains/42161/currency_logo.png', + name: 'AETH', + symbol: 'AETH', + }, + publicRpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://arb1.arbitrum.io/rpc' }, + rpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://arb1.arbitrum.io/rpc' }, + safeAppsRpcUri: { authentication: 'NO_AUTHENTICATION', value: 'https://arb1.arbitrum.io/rpc' }, + shortName: 'arb1', + theme: { backgroundColor: '#28A0F0', textColor: '#ffffff' }, + transactionService: 'https://safe-transaction-arbitrum.safe.global', + }, +] diff --git a/apps/mobile/src/store/index.ts b/apps/mobile/src/store/index.ts index 4f078cd2e3..7c580238b2 100644 --- a/apps/mobile/src/store/index.ts +++ b/apps/mobile/src/store/index.ts @@ -4,6 +4,7 @@ import { reduxStorage } from './storage' import txHistory from './txHistorySlice' import activeChain from './activeChainSlice' import activeSafe from './activeSafeSlice' +import safes from './safesSlice' import { cgwClient, setBaseUrl } from '@safe-global/store/gateway/cgwClient' import devToolsEnhancer from 'redux-devtools-expo-dev-plugin' import { GATEWAY_URL, isTestingEnv } from '../config/constants' @@ -17,6 +18,7 @@ const persistConfig = { } export const rootReducer = combineReducers({ txHistory, + safes, activeChain, activeSafe, [cgwClient.reducerPath]: cgwClient.reducer, diff --git a/apps/mobile/src/store/safesSlice.ts b/apps/mobile/src/store/safesSlice.ts new file mode 100644 index 0000000000..d78641779c --- /dev/null +++ b/apps/mobile/src/store/safesSlice.ts @@ -0,0 +1,44 @@ +import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit' +import { RootState } from '.' +import { mockedAccounts, mockedActiveAccount, mockedActiveSafeInfo } from './constants' +import { Address } from '@/src/types/address' +import { SafeOverview } from '@safe-global/store/gateway/AUTO_GENERATED/safes' + +export type SafesSliceItem = { + SafeInfo: SafeOverview + chains: string[] +} + +export type SafesSlice = Record + +const initialState: SafesSlice = { + [mockedActiveAccount.address]: { + SafeInfo: mockedActiveSafeInfo, + chains: [mockedActiveAccount.chainId], + }, + [mockedAccounts[1].address.value]: { + SafeInfo: mockedAccounts[1], + chains: [mockedAccounts[1].chainId], + }, +} + +const activeSafeSlice = createSlice({ + name: 'safes', + initialState, + reducers: { + updateSafeInfo: (state, action: PayloadAction<{ address: Address; item: SafesSliceItem }>) => { + state[action.payload.address] = action.payload.item + return state + }, + }, +}) + +export const { updateSafeInfo } = activeSafeSlice.actions + +export const selectAllSafes = (state: RootState) => state.safes +export const selectActiveSafeInfo = createSelector( + [selectAllSafes, (_state, activeSafeAddress: Address) => activeSafeAddress], + (safes: SafesSlice, activeSafeAddress: Address) => safes[activeSafeAddress], +) + +export default activeSafeSlice.reducer diff --git a/apps/mobile/src/tests/jest.setup.tsx b/apps/mobile/src/tests/jest.setup.tsx index 70c19757e4..f7eed09035 100644 --- a/apps/mobile/src/tests/jest.setup.tsx +++ b/apps/mobile/src/tests/jest.setup.tsx @@ -115,6 +115,8 @@ jest.mock('@gorhom/bottom-sheet', () => { return { __esModule: true, default: View, + BottomSheetFooter: View, + BottomSheetFooterContainer: View, BottomSheetModal: MockBottomSheetComponent, BottomSheetModalProvider: View, BottomSheetView: View, diff --git a/apps/mobile/src/types/address.ts b/apps/mobile/src/types/address.ts index 816b1b8638..2125eb77f6 100644 --- a/apps/mobile/src/types/address.ts +++ b/apps/mobile/src/types/address.ts @@ -1 +1,6 @@ +export interface SafeInfo { + address: Address + chainId: string +} + export type Address = `0x${string}` diff --git a/apps/mobile/src/utils/formatters.ts b/apps/mobile/src/utils/formatters.ts index 4ef155a16a..86db4980a9 100644 --- a/apps/mobile/src/utils/formatters.ts +++ b/apps/mobile/src/utils/formatters.ts @@ -2,6 +2,8 @@ export const ellipsis = (str: string, length: number): string => { return str.length > length ? `${str.slice(0, length)}...` : str } +export const makeSafeId = (chainId: string, address: string) => `${chainId}:${address}` as `${number}:0x${string}` + export const shortenAddress = (address: string, length = 4): string => { if (!address) { return '' diff --git a/packages/store/scripts/api-schema/schema.json b/packages/store/scripts/api-schema/schema.json index 2d6dcabaa0..291d62f0b6 100644 --- a/packages/store/scripts/api-schema/schema.json +++ b/packages/store/scripts/api-schema/schema.json @@ -17,7 +17,9 @@ } } }, - "tags": ["about"] + "tags": [ + "about" + ] } }, "/v1/accounts": { @@ -46,7 +48,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/data-types": { @@ -68,7 +72,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}/data-settings": { @@ -99,7 +105,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "put": { "operationId": "accountsUpsertAccountDataSettingsV1", @@ -138,7 +146,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}": { @@ -166,7 +176,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "delete": { "operationId": "accountsDeleteAccountV1", @@ -185,7 +197,9 @@ "description": "" } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}/address-books/{chainId}": { @@ -221,7 +235,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "post": { "operationId": "addressBooksCreateAddressBookItemV1", @@ -265,7 +281,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "delete": { "operationId": "addressBooksDeleteAddressBookV1", @@ -292,7 +310,9 @@ "description": "" } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}/address-books/{chainId}/{addressBookItemId}": { @@ -329,7 +349,9 @@ "description": "" } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}/counterfactual-safes/{chainId}/{predictedAddress}": { @@ -373,7 +395,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "delete": { "operationId": "counterfactualSafesDeleteCounterfactualSafeV1", @@ -408,7 +432,9 @@ "description": "" } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/accounts/{address}/counterfactual-safes": { @@ -439,7 +465,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "put": { "operationId": "counterfactualSafesCreateCounterfactualSafeV1", @@ -475,7 +503,9 @@ } } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] }, "delete": { "operationId": "counterfactualSafesDeleteCounterfactualSafesV1", @@ -494,7 +524,9 @@ "description": "" } }, - "tags": ["accounts"] + "tags": [ + "accounts" + ] } }, "/v1/auth/nonce": { @@ -513,7 +545,9 @@ } } }, - "tags": ["auth"] + "tags": [ + "auth" + ] } }, "/v1/auth/verify": { @@ -535,7 +569,9 @@ "description": "Empty response body. JWT token is set as response cookie." } }, - "tags": ["auth"] + "tags": [ + "auth" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/balances/{fiatCode}": { @@ -595,7 +631,9 @@ } } }, - "tags": ["balances"] + "tags": [ + "balances" + ] } }, "/v1/balances/supported-fiat-codes": { @@ -607,7 +645,9 @@ "description": "" } }, - "tags": ["balances"] + "tags": [ + "balances" + ] } }, "/v1/chains": { @@ -635,7 +675,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v1/chains/{chainId}": { @@ -663,7 +705,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v1/chains/{chainId}/about": { @@ -691,7 +735,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v1/chains/{chainId}/about/backbone": { @@ -719,7 +765,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v1/chains/{chainId}/about/master-copies": { @@ -750,7 +798,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v1/chains/{chainId}/about/indexing": { @@ -778,7 +828,9 @@ } } }, - "tags": ["chains"] + "tags": [ + "chains" + ] } }, "/v2/chains/{chainId}/safes/{safeAddress}/collectibles": { @@ -838,7 +890,9 @@ } } }, - "tags": ["collectibles"] + "tags": [ + "collectibles" + ] } }, "/v1/community/campaigns": { @@ -866,7 +920,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/campaigns/{resourceId}": { @@ -894,7 +950,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/campaigns/{resourceId}/activities": { @@ -931,7 +989,9 @@ "description": "" } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/campaigns/{resourceId}/leaderboard": { @@ -967,7 +1027,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/campaigns/{resourceId}/leaderboard/{safeAddress}": { @@ -1003,7 +1065,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/eligibility": { @@ -1032,7 +1096,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/locking/leaderboard": { @@ -1060,7 +1126,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/locking/{safeAddress}/rank": { @@ -1088,7 +1156,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/community/locking/{safeAddress}/history": { @@ -1124,7 +1194,9 @@ } } }, - "tags": ["community"] + "tags": [ + "community" + ] } }, "/v1/chains/{chainId}/contracts/{contractAddress}": { @@ -1160,7 +1232,9 @@ } } }, - "tags": ["contracts"] + "tags": [ + "contracts" + ] } }, "/v1/chains/{chainId}/data-decoder": { @@ -1198,7 +1272,9 @@ } } }, - "tags": ["data-decoded"] + "tags": [ + "data-decoded" + ] } }, "/v1/chains/{chainId}/delegates": { @@ -1268,7 +1344,9 @@ } }, "summary": "", - "tags": ["delegates"] + "tags": [ + "delegates" + ] }, "post": { "deprecated": true, @@ -1299,7 +1377,9 @@ } }, "summary": "", - "tags": ["delegates"] + "tags": [ + "delegates" + ] } }, "/v1/chains/{chainId}/delegates/{delegateAddress}": { @@ -1340,7 +1420,9 @@ } }, "summary": "", - "tags": ["delegates"] + "tags": [ + "delegates" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/delegates/{delegateAddress}": { @@ -1373,7 +1455,9 @@ } }, "summary": "", - "tags": ["delegates"] + "tags": [ + "delegates" + ] } }, "/v2/chains/{chainId}/delegates": { @@ -1441,7 +1525,9 @@ } } }, - "tags": ["delegates"] + "tags": [ + "delegates" + ] }, "post": { "operationId": "delegatesPostDelegateV2", @@ -1470,7 +1556,9 @@ "description": "" } }, - "tags": ["delegates"] + "tags": [ + "delegates" + ] } }, "/v2/chains/{chainId}/delegates/{delegateAddress}": { @@ -1509,7 +1597,89 @@ "description": "" } }, - "tags": ["delegates"] + "tags": [ + "delegates" + ] + } + }, + "/v1/chains/{chainId}/safes/{safeAddress}/recovery": { + "post": { + "operationId": "recoveryAddRecoveryModuleV1", + "parameters": [ + { + "name": "chainId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "safeAddress", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddRecoveryModuleDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "recovery" + ] + } + }, + "/v1/chains/{chainId}/safes/{safeAddress}/recovery/{moduleAddress}": { + "delete": { + "operationId": "recoveryDeleteRecoveryModuleV1", + "parameters": [ + { + "name": "chainId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "moduleAddress", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "safeAddress", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "tags": [ + "recovery" + ] } }, "/v2/chains/{chainId}/safes/{address}/multisig-transactions/estimations": { @@ -1555,7 +1725,140 @@ } } }, - "tags": ["estimations"] + "tags": [ + "estimations" + ] + } + }, + "/v2/register/notifications": { + "post": { + "operationId": "notificationsUpsertSubscriptionsV2", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertSubscriptionsDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "notifications" + ] + } + }, + "/v2/chains/{chainId}/notifications/devices/{deviceUuid}/safes/{safeAddress}": { + "get": { + "operationId": "notificationsGetSafeSubscriptionV2", + "parameters": [ + { + "name": "deviceUuid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "chainId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "safeAddress", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "notifications" + ] + }, + "delete": { + "operationId": "notificationsDeleteSubscriptionV2", + "parameters": [ + { + "name": "deviceUuid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "chainId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "safeAddress", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "notifications" + ] + } + }, + "/v2/chains/{chainId}/notifications/devices/{deviceUuid}": { + "delete": { + "operationId": "notificationsDeleteDeviceV2", + "parameters": [ + { + "name": "chainId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "deviceUuid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "notifications" + ] } }, "/v1/chains/{chainId}/messages/{messageHash}": { @@ -1591,7 +1894,9 @@ } } }, - "tags": ["messages"] + "tags": [ + "messages" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/messages": { @@ -1635,7 +1940,9 @@ } } }, - "tags": ["messages"] + "tags": [ + "messages" + ] }, "post": { "operationId": "messagesCreateMessageV1", @@ -1672,7 +1979,9 @@ "description": "" } }, - "tags": ["messages"] + "tags": [ + "messages" + ] } }, "/v1/chains/{chainId}/messages/{messageHash}/signatures": { @@ -1711,7 +2020,9 @@ "description": "" } }, - "tags": ["messages"] + "tags": [ + "messages" + ] } }, "/v1/register/notifications": { @@ -1733,7 +2044,9 @@ "description": "" } }, - "tags": ["notifications"] + "tags": [ + "notifications" + ] } }, "/v1/chains/{chainId}/notifications/devices/{uuid}": { @@ -1762,7 +2075,9 @@ "description": "" } }, - "tags": ["notifications"] + "tags": [ + "notifications" + ] } }, "/v1/chains/{chainId}/notifications/devices/{uuid}/safes/{safeAddress}": { @@ -1799,7 +2114,9 @@ "description": "" } }, - "tags": ["notifications"] + "tags": [ + "notifications" + ] } }, "/v1/chains/{chainId}/owners/{ownerAddress}/safes": { @@ -1835,7 +2152,9 @@ } } }, - "tags": ["owners"] + "tags": [ + "owners" + ] } }, "/v1/owners/{ownerAddress}/safes": { @@ -1863,7 +2182,9 @@ } } }, - "tags": ["owners"] + "tags": [ + "owners" + ] } }, "/v1/chains/{chainId}/relay": { @@ -1894,7 +2215,9 @@ "description": "" } }, - "tags": ["relay"] + "tags": [ + "relay" + ] } }, "/v1/chains/{chainId}/relay/{safeAddress}": { @@ -1923,7 +2246,9 @@ "description": "" } }, - "tags": ["relay"] + "tags": [ + "relay" + ] } }, "/v1/chains/{chainId}/safe-apps": { @@ -1970,7 +2295,9 @@ } } }, - "tags": ["safe-apps"] + "tags": [ + "safe-apps" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}": { @@ -2006,7 +2333,9 @@ } } }, - "tags": ["safes"] + "tags": [ + "safes" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/nonces": { @@ -2042,7 +2371,9 @@ } } }, - "tags": ["safes"] + "tags": [ + "safes" + ] } }, "/v1/safes": { @@ -2105,7 +2436,9 @@ } } }, - "tags": ["safes"] + "tags": [ + "safes" + ] } }, "/v1/targeted-messaging/outreaches/{outreachId}/chains/{chainId}/safes/{safeAddress}/signers/{signerAddress}/submissions": { @@ -2157,7 +2490,9 @@ } } }, - "tags": ["targeted-messaging"] + "tags": [ + "targeted-messaging" + ] }, "post": { "operationId": "targetedMessagingCreateSubmissionV1", @@ -2217,7 +2552,9 @@ } } }, - "tags": ["targeted-messaging"] + "tags": [ + "targeted-messaging" + ] } }, "/v1/chains/{chainId}/transactions/{id}": { @@ -2253,7 +2590,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/multisig-transactions": { @@ -2345,7 +2684,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/transactions/{safeTxHash}": { @@ -2384,7 +2725,9 @@ "description": "" } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/module-transactions": { @@ -2452,7 +2795,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/transactions/{safeTxHash}/confirmations": { @@ -2498,7 +2843,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/incoming-transfers": { @@ -2590,7 +2937,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/transactions/{safeAddress}/preview": { @@ -2636,7 +2985,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/transactions/queued": { @@ -2688,7 +3039,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/transactions/history": { @@ -2765,7 +3118,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/transactions/{safeAddress}/propose": { @@ -2811,7 +3166,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/transactions/creation": { @@ -2847,7 +3204,9 @@ } } }, - "tags": ["transactions"] + "tags": [ + "transactions" + ] } }, "/v1/chains/{chainId}/safes/{safeAddress}/views/transaction-confirmation": { @@ -2885,28 +3244,6 @@ }, "responses": { "200": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/BaselineConfirmationView" - }, - { - "$ref": "#/components/schemas/CowSwapConfirmationView" - }, - { - "$ref": "#/components/schemas/CowSwapTwapConfirmationView" - }, - { - "$ref": "#/components/schemas/NativeStakingDepositConfirmationView" - }, - { - "$ref": "#/components/schemas/NativeStakingValidatorsExitConfirmationView" - }, - { - "$ref": "#/components/schemas/NativeStakingWithdrawConfirmationView" - } - ] - }, "description": "", "content": { "application/json": { @@ -2937,7 +3274,9 @@ } }, "summary": "", - "tags": ["transactions"] + "tags": [ + "transactions" + ] } } }, @@ -2966,7 +3305,9 @@ "nullable": true } }, - "required": ["name"] + "required": [ + "name" + ] }, "CreateAccountDto": { "type": "object", @@ -2978,7 +3319,10 @@ "type": "string" } }, - "required": ["address", "name"] + "required": [ + "address", + "name" + ] }, "Account": { "type": "object", @@ -2997,7 +3341,11 @@ "type": "string" } }, - "required": ["id", "address", "name"] + "required": [ + "id", + "address", + "name" + ] }, "AccountDataType": { "type": "object", @@ -3016,7 +3364,11 @@ "type": "boolean" } }, - "required": ["id", "name", "isActive"] + "required": [ + "id", + "name", + "isActive" + ] }, "AccountDataSetting": { "type": "object", @@ -3028,7 +3380,10 @@ "type": "boolean" } }, - "required": ["dataTypeId", "enabled"] + "required": [ + "dataTypeId", + "enabled" + ] }, "UpsertAccountDataSettingDto": { "type": "object", @@ -3040,7 +3395,10 @@ "type": "boolean" } }, - "required": ["dataTypeId", "enabled"] + "required": [ + "dataTypeId", + "enabled" + ] }, "UpsertAccountDataSettingsDto": { "type": "object", @@ -3052,7 +3410,9 @@ } } }, - "required": ["accountDataSettings"] + "required": [ + "accountDataSettings" + ] }, "AddressBookItem": { "type": "object", @@ -3067,7 +3427,11 @@ "type": "string" } }, - "required": ["id", "name", "address"] + "required": [ + "id", + "name", + "address" + ] }, "AddressBook": { "type": "object", @@ -3088,7 +3452,12 @@ } } }, - "required": ["id", "accountId", "chainId", "data"] + "required": [ + "id", + "accountId", + "chainId", + "data" + ] }, "CreateAddressBookItemDto": { "type": "object", @@ -3100,7 +3469,10 @@ "type": "string" } }, - "required": ["name", "address"] + "required": [ + "name", + "address" + ] }, "CounterfactualSafe": { "type": "object", @@ -3189,7 +3561,9 @@ "type": "string" } }, - "required": ["nonce"] + "required": [ + "nonce" + ] }, "SiweDto": { "type": "object", @@ -3201,7 +3575,10 @@ "type": "string" } }, - "required": ["message", "signature"] + "required": [ + "message", + "signature" + ] }, "Token": { "type": "object", @@ -3224,10 +3601,21 @@ }, "type": { "type": "string", - "enum": ["ERC721", "ERC20", "NATIVE_TOKEN", "UNKNOWN"] + "enum": [ + "ERC721", + "ERC20", + "NATIVE_TOKEN", + "UNKNOWN" + ] } }, - "required": ["address", "logoUri", "name", "symbol", "type"] + "required": [ + "address", + "logoUri", + "name", + "symbol", + "type" + ] }, "Balance": { "type": "object", @@ -3245,7 +3633,12 @@ "$ref": "#/components/schemas/Token" } }, - "required": ["balance", "fiatBalance", "fiatConversion", "tokenInfo"] + "required": [ + "balance", + "fiatBalance", + "fiatConversion", + "tokenInfo" + ] }, "Balances": { "type": "object", @@ -3264,7 +3657,10 @@ } } }, - "required": ["fiatTotal", "items"] + "required": [ + "fiatTotal", + "items" + ] }, "GasPriceOracle": { "type": "object", @@ -3282,7 +3678,12 @@ "type": "string" } }, - "required": ["type", "gasParameter", "gweiFactor", "uri"] + "required": [ + "type", + "gasParameter", + "gweiFactor", + "uri" + ] }, "GasPriceFixed": { "type": "object", @@ -3294,7 +3695,10 @@ "type": "string" } }, - "required": ["type", "weiValue"] + "required": [ + "type", + "weiValue" + ] }, "GasPriceFixedEIP1559": { "type": "object", @@ -3309,7 +3713,11 @@ "type": "string" } }, - "required": ["type", "maxFeePerGas", "maxPriorityFeePerGas"] + "required": [ + "type", + "maxFeePerGas", + "maxPriorityFeePerGas" + ] }, "NativeCurrency": { "type": "object", @@ -3327,7 +3735,12 @@ "type": "string" } }, - "required": ["decimals", "logoUri", "name", "symbol"] + "required": [ + "decimals", + "logoUri", + "name", + "symbol" + ] }, "BlockExplorerUriTemplate": { "type": "object", @@ -3342,7 +3755,11 @@ "type": "string" } }, - "required": ["address", "api", "txHash"] + "required": [ + "address", + "api", + "txHash" + ] }, "BalancesProvider": { "type": "object", @@ -3355,7 +3772,9 @@ "type": "boolean" } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "ContractAddresses": { "type": "object", @@ -3403,13 +3822,20 @@ "properties": { "authentication": { "type": "string", - "enum": ["API_KEY_PATH", "NO_AUTHENTICATION", "UNKNOWN"] + "enum": [ + "API_KEY_PATH", + "NO_AUTHENTICATION", + "UNKNOWN" + ] }, "value": { "type": "string" } }, - "required": ["authentication", "value"] + "required": [ + "authentication", + "value" + ] }, "Theme": { "type": "object", @@ -3421,7 +3847,10 @@ "type": "string" } }, - "required": ["backgroundColor", "textColor"] + "required": [ + "backgroundColor", + "textColor" + ] }, "Chain": { "type": "object", @@ -3509,6 +3938,10 @@ }, "theme": { "$ref": "#/components/schemas/Theme" + }, + "recommendedMasterCopyVersion": { + "type": "string", + "nullable": true } }, "required": [ @@ -3555,7 +3988,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "AboutChain": { "type": "object", @@ -3573,7 +4008,12 @@ "type": "string" } }, - "required": ["transactionServiceBaseUri", "name", "version", "buildNumber"] + "required": [ + "transactionServiceBaseUri", + "name", + "version", + "buildNumber" + ] }, "Backbone": { "type": "object", @@ -3602,7 +4042,14 @@ "type": "string" } }, - "required": ["api_version", "host", "name", "secure", "settings", "version"] + "required": [ + "api_version", + "host", + "name", + "secure", + "settings", + "version" + ] }, "MasterCopy": { "type": "object", @@ -3614,7 +4061,10 @@ "type": "string" } }, - "required": ["address", "version"] + "required": [ + "address", + "version" + ] }, "IndexingStatus": { "type": "object", @@ -3626,7 +4076,10 @@ "type": "boolean" } }, - "required": ["lastSync", "synced"] + "required": [ + "lastSync", + "synced" + ] }, "Collectible": { "type": "object", @@ -3667,7 +4120,13 @@ "nullable": true } }, - "required": ["address", "tokenName", "tokenSymbol", "logoUri", "id"] + "required": [ + "address", + "tokenName", + "tokenSymbol", + "logoUri", + "id" + ] }, "CollectiblePage": { "type": "object", @@ -3691,7 +4150,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "ActivityMetadata": { "type": "object", @@ -3706,7 +4167,11 @@ "type": "number" } }, - "required": ["name", "description", "maxPoints"] + "required": [ + "name", + "description", + "maxPoints" + ] }, "Campaign": { "type": "object", @@ -3761,7 +4226,14 @@ "type": "boolean" } }, - "required": ["resourceId", "name", "description", "startDate", "endDate", "isPromoted"] + "required": [ + "resourceId", + "name", + "description", + "startDate", + "endDate", + "isPromoted" + ] }, "CampaignPage": { "type": "object", @@ -3785,7 +4257,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "CampaignRank": { "type": "object", @@ -3806,7 +4280,13 @@ "type": "number" } }, - "required": ["holder", "position", "boost", "totalPoints", "totalBoostedPoints"] + "required": [ + "holder", + "position", + "boost", + "totalPoints", + "totalBoostedPoints" + ] }, "CampaignRankPage": { "type": "object", @@ -3830,7 +4310,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "EligibilityRequest": { "type": "object", @@ -3842,7 +4324,10 @@ "type": "string" } }, - "required": ["requestId", "sealedData"] + "required": [ + "requestId", + "sealedData" + ] }, "Eligibility": { "type": "object", @@ -3857,7 +4342,11 @@ "type": "boolean" } }, - "required": ["requestId", "isAllowed", "isVpn"] + "required": [ + "requestId", + "isAllowed", + "isVpn" + ] }, "LockingRank": { "type": "object", @@ -3878,7 +4367,13 @@ "type": "string" } }, - "required": ["holder", "position", "lockedAmount", "unlockedAmount", "withdrawnAmount"] + "required": [ + "holder", + "position", + "lockedAmount", + "unlockedAmount", + "withdrawnAmount" + ] }, "LockingRankPage": { "type": "object", @@ -3902,14 +4397,18 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "LockEventItem": { "type": "object", "properties": { "eventType": { "type": "string", - "enum": ["LOCKED"] + "enum": [ + "LOCKED" + ] }, "executionDate": { "type": "string" @@ -3927,14 +4426,23 @@ "type": "string" } }, - "required": ["eventType", "executionDate", "transactionHash", "holder", "amount", "logIndex"] + "required": [ + "eventType", + "executionDate", + "transactionHash", + "holder", + "amount", + "logIndex" + ] }, "UnlockEventItem": { "type": "object", "properties": { "eventType": { "type": "string", - "enum": ["UNLOCKED"] + "enum": [ + "UNLOCKED" + ] }, "executionDate": { "type": "string" @@ -3955,14 +4463,24 @@ "type": "string" } }, - "required": ["eventType", "executionDate", "transactionHash", "holder", "amount", "logIndex", "unlockIndex"] + "required": [ + "eventType", + "executionDate", + "transactionHash", + "holder", + "amount", + "logIndex", + "unlockIndex" + ] }, "WithdrawEventItem": { "type": "object", "properties": { "eventType": { "type": "string", - "enum": ["WITHDRAWN"] + "enum": [ + "WITHDRAWN" + ] }, "executionDate": { "type": "string" @@ -3983,7 +4501,15 @@ "type": "string" } }, - "required": ["eventType", "executionDate", "transactionHash", "holder", "amount", "logIndex", "unlockIndex"] + "required": [ + "eventType", + "executionDate", + "transactionHash", + "holder", + "amount", + "logIndex", + "unlockIndex" + ] }, "LockingEventPage": { "type": "object", @@ -4017,7 +4543,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "Contract": { "type": "object", @@ -4042,7 +4570,13 @@ "type": "boolean" } }, - "required": ["address", "name", "displayName", "logoUri", "trustedForDelegateCall"] + "required": [ + "address", + "name", + "displayName", + "logoUri", + "trustedForDelegateCall" + ] }, "TransactionDataDto": { "type": "object", @@ -4060,7 +4594,9 @@ "description": "The wei amount being sent to a payable function" } }, - "required": ["data"] + "required": [ + "data" + ] }, "DataDecodedParameter": { "type": "object", @@ -4089,7 +4625,11 @@ "nullable": true } }, - "required": ["name", "type", "value"] + "required": [ + "name", + "type", + "value" + ] }, "DataDecoded": { "type": "object", @@ -4105,7 +4645,9 @@ } } }, - "required": ["method"] + "required": [ + "method" + ] }, "Delegate": { "type": "object", @@ -4124,7 +4666,11 @@ "type": "string" } }, - "required": ["delegate", "delegator", "label"] + "required": [ + "delegate", + "delegator", + "label" + ] }, "DelegatePage": { "type": "object", @@ -4148,7 +4694,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "CreateDelegateDto": { "type": "object", @@ -4170,7 +4718,12 @@ "type": "string" } }, - "required": ["delegate", "delegator", "signature", "label"] + "required": [ + "delegate", + "delegator", + "signature", + "label" + ] }, "DeleteDelegateDto": { "type": "object", @@ -4185,7 +4738,11 @@ "type": "string" } }, - "required": ["delegate", "delegator", "signature"] + "required": [ + "delegate", + "delegator", + "signature" + ] }, "DeleteSafeDelegateDto": { "type": "object", @@ -4200,7 +4757,11 @@ "type": "string" } }, - "required": ["delegate", "safe", "signature"] + "required": [ + "delegate", + "safe", + "signature" + ] }, "DeleteDelegateV2Dto": { "type": "object", @@ -4217,7 +4778,20 @@ "type": "string" } }, - "required": ["signature"] + "required": [ + "signature" + ] + }, + "AddRecoveryModuleDto": { + "type": "object", + "properties": { + "moduleAddress": { + "type": "string" + } + }, + "required": [ + "moduleAddress" + ] }, "GetEstimationDto": { "type": "object", @@ -4236,7 +4810,11 @@ "type": "number" } }, - "required": ["to", "value", "operation"] + "required": [ + "to", + "value", + "operation" + ] }, "EstimationResponse": { "type": "object", @@ -4251,7 +4829,83 @@ "type": "string" } }, - "required": ["currentNonce", "recommendedNonce", "safeTxGas"] + "required": [ + "currentNonce", + "recommendedNonce", + "safeTxGas" + ] + }, + "NotificationType": { + "type": "string", + "enum": [ + "CONFIRMATION_REQUEST", + "DELETED_MULTISIG_TRANSACTION", + "EXECUTED_MULTISIG_TRANSACTION", + "INCOMING_ETHER", + "INCOMING_TOKEN", + "MESSAGE_CONFIRMATION_REQUEST", + "MODULE_TRANSACTION" + ] + }, + "UpsertSubscriptionsSafesDto": { + "type": "object", + "properties": { + "chainId": { + "type": "string" + }, + "address": { + "type": "string" + }, + "notificationTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationType" + } + } + }, + "required": [ + "chainId", + "address", + "notificationTypes" + ] + }, + "DeviceType": { + "type": "string", + "enum": [ + "ANDROID", + "IOS", + "WEB" + ] + }, + "UpsertSubscriptionsDto": { + "type": "object", + "properties": { + "cloudMessagingToken": { + "type": "string" + }, + "safes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpsertSubscriptionsSafesDto" + } + }, + "deviceType": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceType" + } + ] + }, + "deviceUuid": { + "type": "string", + "nullable": true + } + }, + "required": [ + "cloudMessagingToken", + "safes", + "deviceType" + ] }, "AddressInfo": { "type": "object", @@ -4268,7 +4922,9 @@ "nullable": true } }, - "required": ["value"] + "required": [ + "value" + ] }, "Message": { "type": "object", @@ -4403,13 +5059,18 @@ "properties": { "type": { "type": "string", - "enum": ["DATE_LABEL"] + "enum": [ + "DATE_LABEL" + ] }, "timestamp": { "type": "number" } }, - "required": ["type", "timestamp"] + "required": [ + "type", + "timestamp" + ] }, "MessagePage": { "type": "object", @@ -4440,7 +5101,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "CreateMessageDto": { "type": "object", @@ -4461,7 +5124,10 @@ "nullable": true } }, - "required": ["message", "signature"] + "required": [ + "message", + "signature" + ] }, "UpdateMessageSignatureDto": { "type": "object", @@ -4470,7 +5136,9 @@ "type": "string" } }, - "required": ["signature"] + "required": [ + "signature" + ] }, "SafeRegistration": { "type": "object", @@ -4491,7 +5159,11 @@ } } }, - "required": ["chainId", "safes", "signatures"] + "required": [ + "chainId", + "safes", + "signatures" + ] }, "RegisterDeviceDto": { "type": "object", @@ -4526,7 +5198,14 @@ } } }, - "required": ["cloudMessagingToken", "buildNumber", "bundle", "deviceType", "version", "safeRegistrations"] + "required": [ + "cloudMessagingToken", + "buildNumber", + "bundle", + "deviceType", + "version", + "safeRegistrations" + ] }, "SafeList": { "type": "object", @@ -4538,7 +5217,9 @@ } } }, - "required": ["safes"] + "required": [ + "safes" + ] }, "RelayDto": { "type": "object", @@ -4558,7 +5239,11 @@ "description": "If specified, a gas buffer of 150k will be added on top of the expected gas usage for the transaction.\n This is for the \n Gelato Relay execution overhead, reducing the chance of the task cancelling before it is executed on-chain." } }, - "required": ["version", "to", "data"] + "required": [ + "version", + "to", + "data" + ] }, "SafeAppProvider": { "type": "object", @@ -4570,7 +5255,10 @@ "type": "string" } }, - "required": ["url", "name"] + "required": [ + "url", + "name" + ] }, "SafeAppAccessControl": { "type": "object", @@ -4586,20 +5274,30 @@ } } }, - "required": ["type"] + "required": [ + "type" + ] }, "SafeAppSocialProfile": { "type": "object", "properties": { "platform": { "type": "string", - "enum": ["DISCORD", "GITHUB", "TWITTER", "UNKNOWN"] + "enum": [ + "DISCORD", + "GITHUB", + "TWITTER", + "UNKNOWN" + ] }, "url": { "type": "string" } }, - "required": ["platform", "url"] + "required": [ + "platform", + "url" + ] }, "SafeApp": { "type": "object", @@ -4692,9 +5390,10 @@ "type": "number" }, "owners": { + "nullable": false, "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/AddressInfo" } }, "implementation": { @@ -4729,7 +5428,11 @@ }, "implementationVersionState": { "type": "string", - "enum": ["UP_TO_DATE", "OUTDATED", "UNKNOWN"] + "enum": [ + "UP_TO_DATE", + "OUTDATED", + "UNKNOWN" + ] }, "collectiblesTag": { "type": "string", @@ -4768,7 +5471,10 @@ "type": "number" } }, - "required": ["currentNonce", "recommendedNonce"] + "required": [ + "currentNonce", + "recommendedNonce" + ] }, "SafeOverview": { "type": "object", @@ -4783,9 +5489,10 @@ "type": "number" }, "owners": { + "nullable": false, "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/AddressInfo" } }, "fiatTotal": { @@ -4799,7 +5506,14 @@ "nullable": true } }, - "required": ["address", "chainId", "threshold", "owners", "fiatTotal", "queued"] + "required": [ + "address", + "chainId", + "threshold", + "owners", + "fiatTotal", + "queued" + ] }, "Submission": { "type": "object", @@ -4819,7 +5533,11 @@ "nullable": true } }, - "required": ["outreachId", "targetedSafeId", "signerAddress"] + "required": [ + "outreachId", + "targetedSafeId", + "signerAddress" + ] }, "CreateSubmissionDto": { "type": "object", @@ -4828,7 +5546,9 @@ "type": "boolean" } }, - "required": ["completed"] + "required": [ + "completed" + ] }, "TransactionInfo": { "type": "object", @@ -4853,7 +5573,9 @@ "nullable": true } }, - "required": ["type"] + "required": [ + "type" + ] }, "TransactionData": { "type": "object", @@ -4889,14 +5611,19 @@ "nullable": true } }, - "required": ["to", "operation"] + "required": [ + "to", + "operation" + ] }, "MultisigExecutionDetails": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["MULTISIG"] + "enum": [ + "MULTISIG" + ] }, "submittedAt": { "type": "number" @@ -5001,13 +5728,18 @@ "properties": { "type": { "type": "string", - "enum": ["MODULE"] + "enum": [ + "MODULE" + ] }, "address": { "$ref": "#/components/schemas/AddressInfo" } }, - "required": ["type", "address"] + "required": [ + "type", + "address" + ] }, "SafeAppInfo": { "type": "object", @@ -5023,7 +5755,10 @@ "nullable": true } }, - "required": ["name", "url"] + "required": [ + "name", + "url" + ] }, "TransactionDetails": { "type": "object", @@ -5040,7 +5775,13 @@ }, "txStatus": { "type": "string", - "enum": ["SUCCESS", "FAILED", "CANCELLED", "AWAITING_CONFIRMATIONS", "AWAITING_EXECUTION"] + "enum": [ + "SUCCESS", + "FAILED", + "CANCELLED", + "AWAITING_CONFIRMATIONS", + "AWAITING_EXECUTION" + ] }, "txInfo": { "nullable": true, @@ -5082,14 +5823,21 @@ ] } }, - "required": ["safeAddress", "txId", "txStatus", "txInfo"] + "required": [ + "safeAddress", + "txId", + "txStatus", + "txInfo" + ] }, "CreationTransactionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["Creation"] + "enum": [ + "Creation" + ] }, "humanDescription": { "type": "string", @@ -5117,14 +5865,20 @@ "nullable": true } }, - "required": ["type", "creator", "transactionHash"] + "required": [ + "type", + "creator", + "transactionHash" + ] }, "CustomTransactionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["Custom"] + "enum": [ + "Custom" + ] }, "humanDescription": { "type": "string", @@ -5152,7 +5906,12 @@ "nullable": true } }, - "required": ["type", "to", "dataSize", "isCancellation"] + "required": [ + "type", + "to", + "dataSize", + "isCancellation" + ] }, "SettingsChange": { "type": "object", @@ -5173,14 +5932,18 @@ ] } }, - "required": ["type"] + "required": [ + "type" + ] }, "SettingsChangeTransaction": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["SettingsChange"] + "enum": [ + "SettingsChange" + ] }, "humanDescription": { "type": "string", @@ -5198,14 +5961,19 @@ ] } }, - "required": ["type", "dataDecoded"] + "required": [ + "type", + "dataDecoded" + ] }, "Erc20Transfer": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["ERC20"] + "enum": [ + "ERC20" + ] }, "tokenAddress": { "type": "string" @@ -5237,14 +6005,21 @@ "type": "boolean" } }, - "required": ["type", "tokenAddress", "value", "imitation"] + "required": [ + "type", + "tokenAddress", + "value", + "imitation" + ] }, "Erc721Transfer": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["ERC721"] + "enum": [ + "ERC721" + ] }, "tokenAddress": { "type": "string" @@ -5269,38 +6044,54 @@ "nullable": true } }, - "required": ["type", "tokenAddress", "tokenId"] + "required": [ + "type", + "tokenAddress", + "tokenId" + ] }, "NativeCoinTransfer": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["NATIVE_COIN"] + "enum": [ + "NATIVE_COIN" + ] }, "value": { "type": "string", "nullable": true } }, - "required": ["type"] + "required": [ + "type" + ] }, "Transfer": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["NATIVE_COIN", "ERC20", "ERC721"] + "enum": [ + "NATIVE_COIN", + "ERC20", + "ERC721" + ] } }, - "required": ["type"] + "required": [ + "type" + ] }, "TransferTransactionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["Transfer"] + "enum": [ + "Transfer" + ] }, "humanDescription": { "type": "string", @@ -5314,7 +6105,11 @@ }, "direction": { "type": "string", - "enum": ["INCOMING", "OUTGOING", "UNKNOWN"] + "enum": [ + "INCOMING", + "OUTGOING", + "UNKNOWN" + ] }, "transferInfo": { "oneOf": [ @@ -5335,27 +6130,40 @@ ] } }, - "required": ["type", "sender", "recipient", "direction", "transferInfo"] + "required": [ + "type", + "sender", + "recipient", + "direction", + "transferInfo" + ] }, "ModuleExecutionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["MODULE"] + "enum": [ + "MODULE" + ] }, "address": { "$ref": "#/components/schemas/AddressInfo" } }, - "required": ["type", "address"] + "required": [ + "type", + "address" + ] }, "MultisigExecutionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["MULTISIG"] + "enum": [ + "MULTISIG" + ] }, "nonce": { "type": "number" @@ -5374,7 +6182,12 @@ } } }, - "required": ["type", "nonce", "confirmationsRequired", "confirmationsSubmitted"] + "required": [ + "type", + "nonce", + "confirmationsRequired", + "confirmationsSubmitted" + ] }, "TokenInfo": { "type": "object", @@ -5405,14 +6218,22 @@ "description": "The token trusted status" } }, - "required": ["address", "decimals", "name", "symbol", "trusted"] + "required": [ + "address", + "decimals", + "name", + "symbol", + "trusted" + ] }, "SwapOrderTransactionInfo": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["SwapOrder"] + "enum": [ + "SwapOrder" + ] }, "humanDescription": { "type": "string", @@ -5424,15 +6245,31 @@ }, "status": { "type": "string", - "enum": ["presignaturePending", "open", "fulfilled", "cancelled", "expired", "unknown"] + "enum": [ + "presignaturePending", + "open", + "fulfilled", + "cancelled", + "expired", + "unknown" + ] }, "kind": { "type": "string", - "enum": ["buy", "sell", "unknown"] + "enum": [ + "buy", + "sell", + "unknown" + ] }, "orderClass": { "type": "string", - "enum": ["market", "limit", "liquidity", "unknown"] + "enum": [ + "market", + "limit", + "liquidity", + "unknown" + ] }, "validUntil": { "type": "number", @@ -5515,7 +6352,9 @@ "properties": { "type": { "type": "string", - "enum": ["SwapTransfer"] + "enum": [ + "SwapTransfer" + ] }, "humanDescription": { "type": "string", @@ -5554,15 +6393,31 @@ }, "status": { "type": "string", - "enum": ["presignaturePending", "open", "fulfilled", "cancelled", "expired", "unknown"] + "enum": [ + "presignaturePending", + "open", + "fulfilled", + "cancelled", + "expired", + "unknown" + ] }, "kind": { "type": "string", - "enum": ["buy", "sell", "unknown"] + "enum": [ + "buy", + "sell", + "unknown" + ] }, "orderClass": { "type": "string", - "enum": ["market", "limit", "liquidity", "unknown"] + "enum": [ + "market", + "limit", + "liquidity", + "unknown" + ] }, "validUntil": { "type": "number", @@ -5649,7 +6504,9 @@ "properties": { "type": { "type": "string", - "enum": ["TwapOrder"] + "enum": [ + "TwapOrder" + ] }, "humanDescription": { "type": "string", @@ -5658,15 +6515,31 @@ "status": { "type": "string", "description": "The TWAP status", - "enum": ["presignaturePending", "open", "fulfilled", "cancelled", "expired", "unknown"] + "enum": [ + "presignaturePending", + "open", + "fulfilled", + "cancelled", + "expired", + "unknown" + ] }, "kind": { "type": "string", - "enum": ["buy", "sell", "unknown"] + "enum": [ + "buy", + "sell", + "unknown" + ] }, "class": { "type": "string", - "enum": ["market", "limit", "liquidity", "unknown"] + "enum": [ + "market", + "limit", + "liquidity", + "unknown" + ] }, "activeOrderUid": { "type": "string", @@ -5777,7 +6650,9 @@ "properties": { "type": { "type": "string", - "enum": ["NativeStakingDeposit"] + "enum": [ + "NativeStakingDeposit" + ] }, "humanDescription": { "type": "string", @@ -5867,7 +6742,9 @@ "properties": { "type": { "type": "string", - "enum": ["NativeStakingValidatorsExit"] + "enum": [ + "NativeStakingValidatorsExit" + ] }, "humanDescription": { "type": "string", @@ -5924,7 +6801,9 @@ "properties": { "type": { "type": "string", - "enum": ["NativeStakingWithdraw"] + "enum": [ + "NativeStakingWithdraw" + ] }, "humanDescription": { "type": "string", @@ -5943,7 +6822,12 @@ } } }, - "required": ["type", "value", "tokenInfo", "validators"] + "required": [ + "type", + "value", + "tokenInfo", + "validators" + ] }, "Transaction": { "type": "object", @@ -5960,7 +6844,13 @@ }, "txStatus": { "type": "string", - "enum": ["SUCCESS", "FAILED", "CANCELLED", "AWAITING_CONFIRMATIONS", "AWAITING_EXECUTION"] + "enum": [ + "SUCCESS", + "FAILED", + "CANCELLED", + "AWAITING_CONFIRMATIONS", + "AWAITING_EXECUTION" + ] }, "txInfo": { "oneOf": [ @@ -6021,24 +6911,39 @@ ] } }, - "required": ["id", "timestamp", "txStatus", "txInfo"] + "required": [ + "id", + "timestamp", + "txStatus", + "txInfo" + ] }, "MultisigTransaction": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["TRANSACTION"] + "enum": [ + "TRANSACTION" + ] }, "transaction": { "$ref": "#/components/schemas/Transaction" }, "conflictType": { "type": "string", - "enum": ["None", "HasNext", "End"] + "enum": [ + "None", + "HasNext", + "End" + ] } }, - "required": ["type", "transaction", "conflictType"] + "required": [ + "type", + "transaction", + "conflictType" + ] }, "MultisigTransactionPage": { "type": "object", @@ -6062,7 +6967,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "DeleteTransactionDto": { "type": "object", @@ -6071,24 +6978,34 @@ "type": "string" } }, - "required": ["signature"] + "required": [ + "signature" + ] }, "ModuleTransaction": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["TRANSACTION"] + "enum": [ + "TRANSACTION" + ] }, "transaction": { "$ref": "#/components/schemas/Transaction" }, "conflictType": { "type": "string", - "enum": ["None"] + "enum": [ + "None" + ] } }, - "required": ["type", "transaction", "conflictType"] + "required": [ + "type", + "transaction", + "conflictType" + ] }, "ModuleTransactionPage": { "type": "object", @@ -6112,7 +7029,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "AddConfirmationDto": { "type": "object", @@ -6121,24 +7040,34 @@ "type": "string" } }, - "required": ["signedSafeTxHash"] + "required": [ + "signedSafeTxHash" + ] }, "IncomingTransfer": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["TRANSACTION"] + "enum": [ + "TRANSACTION" + ] }, "transaction": { "$ref": "#/components/schemas/Transaction" }, "conflictType": { "type": "string", - "enum": ["None"] + "enum": [ + "None" + ] } }, - "required": ["type", "transaction", "conflictType"] + "required": [ + "type", + "transaction", + "conflictType" + ] }, "IncomingTransferPage": { "type": "object", @@ -6162,7 +7091,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "PreviewTransactionDto": { "type": "object", @@ -6181,7 +7112,11 @@ "type": "number" } }, - "required": ["to", "value", "operation"] + "required": [ + "to", + "value", + "operation" + ] }, "TransactionPreview": { "type": "object", @@ -6226,50 +7161,73 @@ "$ref": "#/components/schemas/TransactionData" } }, - "required": ["txInfo", "txData"] + "required": [ + "txInfo", + "txData" + ] }, "ConflictHeaderQueuedItem": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["CONFLICT_HEADER"] + "enum": [ + "CONFLICT_HEADER" + ] }, "nonce": { "type": "number" } }, - "required": ["type", "nonce"] + "required": [ + "type", + "nonce" + ] }, "LabelQueuedItem": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["LABEL"] + "enum": [ + "LABEL" + ] }, "label": { "type": "string" } }, - "required": ["type", "label"] + "required": [ + "type", + "label" + ] }, "TransactionQueuedItem": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["TRANSACTION"] + "enum": [ + "TRANSACTION" + ] }, "transaction": { "$ref": "#/components/schemas/Transaction" }, "conflictType": { "type": "string", - "enum": ["None", "HasNext", "End"] + "enum": [ + "None", + "HasNext", + "End" + ] } }, - "required": ["type", "transaction", "conflictType"] + "required": [ + "type", + "transaction", + "conflictType" + ] }, "QueuedItemPage": { "type": "object", @@ -6303,24 +7261,34 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "TransactionItem": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["TRANSACTION"] + "enum": [ + "TRANSACTION" + ] }, "transaction": { "$ref": "#/components/schemas/Transaction" }, "conflictType": { "type": "string", - "enum": ["None"] + "enum": [ + "None" + ] } }, - "required": ["type", "transaction", "conflictType"] + "required": [ + "type", + "transaction", + "conflictType" + ] }, "TransactionItemPage": { "type": "object", @@ -6351,7 +7319,9 @@ } } }, - "required": ["results"] + "required": [ + "results" + ] }, "ProposeTransactionDto": { "type": "object", @@ -6453,14 +7423,21 @@ ] } }, - "required": ["created", "creator", "transactionHash", "factoryAddress"] + "required": [ + "created", + "creator", + "transactionHash", + "factoryAddress" + ] }, "BaselineConfirmationView": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["GENERIC"] + "enum": [ + "GENERIC" + ] }, "method": { "type": "string" @@ -6473,14 +7450,19 @@ } } }, - "required": ["type", "method"] + "required": [ + "type", + "method" + ] }, "CowSwapConfirmationView": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["COW_SWAP_ORDER"] + "enum": [ + "COW_SWAP_ORDER" + ] }, "method": { "type": "string" @@ -6498,15 +7480,31 @@ }, "status": { "type": "string", - "enum": ["presignaturePending", "open", "fulfilled", "cancelled", "expired", "unknown"] + "enum": [ + "presignaturePending", + "open", + "fulfilled", + "cancelled", + "expired", + "unknown" + ] }, "kind": { "type": "string", - "enum": ["buy", "sell", "unknown"] + "enum": [ + "buy", + "sell", + "unknown" + ] }, "orderClass": { "type": "string", - "enum": ["market", "limit", "liquidity", "unknown"] + "enum": [ + "market", + "limit", + "liquidity", + "unknown" + ] }, "validUntil": { "type": "number", @@ -6590,7 +7588,9 @@ "properties": { "type": { "type": "string", - "enum": ["COW_SWAP_TWAP_ORDER"] + "enum": [ + "COW_SWAP_TWAP_ORDER" + ] }, "method": { "type": "string" @@ -6604,16 +7604,32 @@ }, "status": { "type": "string", - "enum": ["presignaturePending", "open", "fulfilled", "cancelled", "expired", "unknown"], + "enum": [ + "presignaturePending", + "open", + "fulfilled", + "cancelled", + "expired", + "unknown" + ], "description": "The TWAP status" }, "kind": { "type": "string", - "enum": ["buy", "sell", "unknown"] + "enum": [ + "buy", + "sell", + "unknown" + ] }, "class": { "type": "string", - "enum": ["market", "limit", "liquidity", "unknown"] + "enum": [ + "market", + "limit", + "liquidity", + "unknown" + ] }, "activeOrderUid": { "type": "null", @@ -6726,7 +7742,9 @@ "properties": { "type": { "type": "string", - "enum": ["KILN_NATIVE_STAKING_DEPOSIT"] + "enum": [ + "KILN_NATIVE_STAKING_DEPOSIT" + ] }, "status": { "type": "string", @@ -6815,7 +7833,9 @@ "properties": { "type": { "type": "string", - "enum": ["KILN_NATIVE_STAKING_VALIDATORS_EXIT"] + "enum": [ + "KILN_NATIVE_STAKING_VALIDATORS_EXIT" + ] }, "status": { "type": "string", @@ -6879,7 +7899,9 @@ "properties": { "type": { "type": "string", - "enum": ["KILN_NATIVE_STAKING_WITHDRAW"] + "enum": [ + "KILN_NATIVE_STAKING_WITHDRAW" + ] }, "method": { "type": "string" @@ -6904,7 +7926,13 @@ } } }, - "required": ["type", "method", "value", "tokenInfo", "validators"] + "required": [ + "type", + "method", + "value", + "tokenInfo", + "validators" + ] } } } diff --git a/packages/store/src/gateway/AUTO_GENERATED/chains.ts b/packages/store/src/gateway/AUTO_GENERATED/chains.ts index f1d9929dee..e01d846347 100644 --- a/packages/store/src/gateway/AUTO_GENERATED/chains.ts +++ b/packages/store/src/gateway/AUTO_GENERATED/chains.ts @@ -134,6 +134,7 @@ export type Chain = { safeAppsRpcUri: RpcUri shortName: string theme: Theme + recommendedMasterCopyVersion?: string | null } export type ChainPage = { count?: number | null diff --git a/packages/store/src/gateway/AUTO_GENERATED/notifications.ts b/packages/store/src/gateway/AUTO_GENERATED/notifications.ts index fb66aa5205..f93f99582d 100644 --- a/packages/store/src/gateway/AUTO_GENERATED/notifications.ts +++ b/packages/store/src/gateway/AUTO_GENERATED/notifications.ts @@ -6,6 +6,46 @@ const injectedRtkApi = api }) .injectEndpoints({ endpoints: (build) => ({ + notificationsUpsertSubscriptionsV2: build.mutation< + NotificationsUpsertSubscriptionsV2ApiResponse, + NotificationsUpsertSubscriptionsV2ApiArg + >({ + query: (queryArg) => ({ + url: `/v2/register/notifications`, + method: 'POST', + body: queryArg.upsertSubscriptionsDto, + }), + invalidatesTags: ['notifications'], + }), + notificationsGetSafeSubscriptionV2: build.query< + NotificationsGetSafeSubscriptionV2ApiResponse, + NotificationsGetSafeSubscriptionV2ApiArg + >({ + query: (queryArg) => ({ + url: `/v2/chains/${queryArg.chainId}/notifications/devices/${queryArg.deviceUuid}/safes/${queryArg.safeAddress}`, + }), + providesTags: ['notifications'], + }), + notificationsDeleteSubscriptionV2: build.mutation< + NotificationsDeleteSubscriptionV2ApiResponse, + NotificationsDeleteSubscriptionV2ApiArg + >({ + query: (queryArg) => ({ + url: `/v2/chains/${queryArg.chainId}/notifications/devices/${queryArg.deviceUuid}/safes/${queryArg.safeAddress}`, + method: 'DELETE', + }), + invalidatesTags: ['notifications'], + }), + notificationsDeleteDeviceV2: build.mutation< + NotificationsDeleteDeviceV2ApiResponse, + NotificationsDeleteDeviceV2ApiArg + >({ + query: (queryArg) => ({ + url: `/v2/chains/${queryArg.chainId}/notifications/devices/${queryArg.deviceUuid}`, + method: 'DELETE', + }), + invalidatesTags: ['notifications'], + }), notificationsRegisterDeviceV1: build.mutation< NotificationsRegisterDeviceV1ApiResponse, NotificationsRegisterDeviceV1ApiArg @@ -37,6 +77,27 @@ const injectedRtkApi = api overrideExisting: false, }) export { injectedRtkApi as cgwApi } +export type NotificationsUpsertSubscriptionsV2ApiResponse = unknown +export type NotificationsUpsertSubscriptionsV2ApiArg = { + upsertSubscriptionsDto: UpsertSubscriptionsDto +} +export type NotificationsGetSafeSubscriptionV2ApiResponse = unknown +export type NotificationsGetSafeSubscriptionV2ApiArg = { + deviceUuid: string + chainId: string + safeAddress: string +} +export type NotificationsDeleteSubscriptionV2ApiResponse = unknown +export type NotificationsDeleteSubscriptionV2ApiArg = { + deviceUuid: string + chainId: string + safeAddress: string +} +export type NotificationsDeleteDeviceV2ApiResponse = unknown +export type NotificationsDeleteDeviceV2ApiArg = { + chainId: string + deviceUuid: string +} export type NotificationsRegisterDeviceV1ApiResponse = unknown export type NotificationsRegisterDeviceV1ApiArg = { registerDeviceDto: RegisterDeviceDto @@ -52,6 +113,26 @@ export type NotificationsUnregisterSafeV1ApiArg = { uuid: string safeAddress: string } +export type NotificationType = + | 'CONFIRMATION_REQUEST' + | 'DELETED_MULTISIG_TRANSACTION' + | 'EXECUTED_MULTISIG_TRANSACTION' + | 'INCOMING_ETHER' + | 'INCOMING_TOKEN' + | 'MESSAGE_CONFIRMATION_REQUEST' + | 'MODULE_TRANSACTION' +export type UpsertSubscriptionsSafesDto = { + chainId: string + address: string + notificationTypes: NotificationType[] +} +export type DeviceType = 'ANDROID' | 'IOS' | 'WEB' +export type UpsertSubscriptionsDto = { + cloudMessagingToken: string + safes: UpsertSubscriptionsSafesDto[] + deviceType: DeviceType + deviceUuid?: string | null +} export type SafeRegistration = { chainId: string safes: string[] @@ -68,6 +149,10 @@ export type RegisterDeviceDto = { safeRegistrations: SafeRegistration[] } export const { + useNotificationsUpsertSubscriptionsV2Mutation, + useNotificationsGetSafeSubscriptionV2Query, + useNotificationsDeleteSubscriptionV2Mutation, + useNotificationsDeleteDeviceV2Mutation, useNotificationsRegisterDeviceV1Mutation, useNotificationsUnregisterDeviceV1Mutation, useNotificationsUnregisterSafeV1Mutation, diff --git a/packages/store/src/gateway/AUTO_GENERATED/safes.ts b/packages/store/src/gateway/AUTO_GENERATED/safes.ts index ff3cf1b5f7..059e249054 100644 --- a/packages/store/src/gateway/AUTO_GENERATED/safes.ts +++ b/packages/store/src/gateway/AUTO_GENERATED/safes.ts @@ -59,7 +59,7 @@ export type SafeState = { chainId: string nonce: number threshold: number - owners: string[] + owners: AddressInfo[] implementation: AddressInfo modules?: AddressInfo[] | null fallbackHandler?: AddressInfo | null @@ -79,7 +79,7 @@ export type SafeOverview = { address: AddressInfo chainId: string threshold: number - owners: string[] + owners: AddressInfo[] fiatTotal: string queued: number awaitingConfirmation?: number | null diff --git a/packages/store/src/gateway/AUTO_GENERATED/transactions.ts b/packages/store/src/gateway/AUTO_GENERATED/transactions.ts index d17095c601..1f45d6454a 100644 --- a/packages/store/src/gateway/AUTO_GENERATED/transactions.ts +++ b/packages/store/src/gateway/AUTO_GENERATED/transactions.ts @@ -245,8 +245,7 @@ export type TransactionsGetCreationTransactionV1ApiArg = { chainId: string safeAddress: string } -export type TransactionsViewGetTransactionConfirmationViewV1ApiResponse = - /** status 200 */ +export type TransactionsViewGetTransactionConfirmationViewV1ApiResponse = /** status 200 */ | BaselineConfirmationView | CowSwapConfirmationView | CowSwapTwapConfirmationView diff --git a/packages/store/src/gateway/chains/index.ts b/packages/store/src/gateway/chains/index.ts index 2316641354..d47946706b 100644 --- a/packages/store/src/gateway/chains/index.ts +++ b/packages/store/src/gateway/chains/index.ts @@ -1,5 +1,5 @@ import { type Chain as ChainInfo } from '../AUTO_GENERATED/chains' -import { createEntityAdapter, createSelector, EntityState } from '@reduxjs/toolkit' +import { createEntityAdapter, EntityState } from '@reduxjs/toolkit' import { cgwClient, getBaseUrl } from '../cgwClient' import { QueryReturnValue, FetchBaseQueryError, FetchBaseQueryMeta } from '@reduxjs/toolkit/dist/query'