From 152918bacdc197fdec8b614bb7d2d4597385e26f Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Mon, 23 Dec 2024 11:09:28 +0100 Subject: [PATCH 1/5] Update docs (#53) * docs: move contributing to code-style The contributing doc is actually a code-style doc. Once we move everything to monorepo we will adopt the safe-wallet-web CONTRIBUTING.md file. * docs: add release procedure docs (cherry picked from commit 01345f008492e9a5d33fc5e198966e6be4d5452c) --- docs/code-style.md | 69 +++++++++++++++++++++++++++++++++++++++ docs/release-procedure.md | 15 +++++++++ 2 files changed, 84 insertions(+) create mode 100644 docs/code-style.md create mode 100644 docs/release-procedure.md diff --git a/docs/code-style.md b/docs/code-style.md new file mode 100644 index 0000000000..0c2e50a391 --- /dev/null +++ b/docs/code-style.md @@ -0,0 +1,69 @@ +# Code Style Guidelines + +## Code Structure + +### General Components + +- Components that are used across multiple features should reside in the `src/components/` folder. +- Each component should have its own folder, structured as follows: + ``` + Alert/ + - Alert.tsx + - Alert.test.tsx + - Alert.stories.tsx + - index.tsx + ``` +- The main component implementation should be in a named file (e.g., `Alert.tsx`), and `index.tsx` should only be used for exporting the component. +- **Reason**: Using `index.tsx` allows for cleaner imports, e.g., + ``` + import { Alert } from 'src/components/Alert'; + ``` + instead of: + ``` + import { Alert } from 'src/components/Alert/Alert'; + ``` + +### Exporting Components + +- **Always prefer named exports over default exports.** + - Named exports make it easier to refactor and identify exports in a codebase. + +### Features and Screens + +- Feature-specific components and screens should be implemented inside the `src/features/` folder. + +#### Example: Feature File Structure + +For a feature called **Assets**, the file structure might look like this: + +``` +// src/features/Assets +- Assets.container.tsx +- index.tsx +``` + +- `index.tsx` should only export the **Assets** component/container. + +#### Subcomponents for Features + +- If a feature depends on multiple subcomponents unique to that feature, place them in a `components` subfolder. For example: + +``` +// src/features/Assets/components/AssetHeader +- AssetHeader.tsx +- AssetHeader.container.tsx +- index.tsx +``` + +### Presentation vs. Container Components + +- **Presentation Components**: + + - Responsible only for rendering the UI. + - Receive data and callbacks via props. + - Avoid direct manipulation of business logic. + - Simple business logic can be included but should generally be extracted into hooks. + +- **Container Components**: + - Handle business logic (e.g., state management, API calls, etc.). + - Pass necessary data and callbacks to the corresponding Presentation component. diff --git a/docs/release-procedure.md b/docs/release-procedure.md new file mode 100644 index 0000000000..6868b7c00b --- /dev/null +++ b/docs/release-procedure.md @@ -0,0 +1,15 @@ +# Releasing to Production + +The code is being actively developed on the `main` branch. Pull requests are made against this branch. + +When we want to make a release, we create a new branch from `main` called `mobile-release/vX.Y.Z` where `X.Y.Z` is the +version number of the release. + +This will trigger a new build on the CI/CD pipeline, which will build the app and submit it to the internal distribution +lanes in App Store and Google Play Store. + +The release has to be tested by QA and once approved can be promoted to the production lane. + +## Triggering Maestro E2E tests + +On the release PR add the github label `eas-build-ios:build-and-maestro-test` to trigger the e2e tests in Expo CI. From 10689196baf3225437b2f2d6ed9775bb84732c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=B3vis=20Neto?= Date: Mon, 23 Dec 2024 11:23:22 +0100 Subject: [PATCH 2/5] Feat: Add create/get private key hook (#59) * feat: add necessary libraries * feat: create useSign hook * feat: cover sign hook with unit tests * Update apps/mobile/src/hooks/useSign/useSign.ts Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> * fix: adjust type Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> * feat: add react-native-quick-crypto back * fix: lint uintGenericArray problem * fix: adjust export style * fix: remove duplicated lines from gitignore * chore: add reference link * fix: typo on useSign unit tests * chore: store iv in the keychain * Update apps/mobile/src/hooks/useSign/useSign.ts Co-authored-by: Usame Algan <5880855+usame-algan@users.noreply.github.com> * fix: typo in unit tests --------- Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> Co-authored-by: Usame Algan <5880855+usame-algan@users.noreply.github.com> (cherry picked from commit 45e82f7ee28fd349669e70e5a3e259e88675743f) --- apps/mobile/.gitignore | 59 +++++++++----- apps/mobile/app.config.js | 3 + apps/mobile/app/_layout.tsx | 3 + apps/mobile/index.js | 8 ++ apps/mobile/metro.config.js | 5 ++ apps/mobile/package.json | 6 ++ apps/mobile/src/config/ethers.ts | 42 ++++++++++ apps/mobile/src/hooks/useSign/index.ts | 1 + apps/mobile/src/hooks/useSign/useSign.test.ts | 65 ++++++++++++++++ apps/mobile/src/hooks/useSign/useSign.ts | 76 +++++++++++++++++++ apps/mobile/src/tests/jest.setup.tsx | 37 +++++++++ .../src/types/react-native-device-info.d.ts | 1 + 12 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 apps/mobile/index.js create mode 100644 apps/mobile/src/config/ethers.ts create mode 100644 apps/mobile/src/hooks/useSign/index.ts create mode 100644 apps/mobile/src/hooks/useSign/useSign.test.ts create mode 100644 apps/mobile/src/hooks/useSign/useSign.ts create mode 100644 apps/mobile/src/types/react-native-device-info.d.ts diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 4453806e56..bb70681e48 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -1,15 +1,3 @@ -node_modules/ -.expo/ -dist/ -npm-debug.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision -*.orig.* -web-build/ - # Auto generated storybook file .storybook/storybook.requires.ts @@ -20,11 +8,6 @@ coverage # macOS .DS_Store -# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb -# The following patterns were generated by expo-cli - -expo-env.d.ts -# @end expo-cli /.idea # Tamagui UI generates a lot of cache files .tamagui @@ -35,3 +18,45 @@ expo-env.d.ts # Android and iOS build files /android/* /ios/* + +# @generated expo-cli sync-8d4afeec25ea8a192358fae2f8e2fc766bdce4ec +# The following patterns were generated by expo-cli + +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*. +*.p8 +*.p12 +*.key +*. + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# @end expo-cli \ No newline at end of file diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js index 99c83043b9..c560fbe19a 100644 --- a/apps/mobile/app.config.js +++ b/apps/mobile/app.config.js @@ -22,6 +22,9 @@ export default { config: { usesNonExemptEncryption: false, }, + infoPlist: { + NSFaceIDUsageDescription: 'Enabling Face ID allows you to create/access secure keys.', + }, supportsTablet: true, appleTeamId: 'MXRS32BBL4', bundleIdentifier: IS_DEV ? 'global.safe.mobileapp.dev' : 'global.safe.mobileapp', diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 356e3fc515..88dc2f152c 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -13,6 +13,9 @@ import { PortalProvider } from '@tamagui/portal' import { SafeToastProvider } from '@/src/theme/provider/toastProvider' import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated' import { OnboardingHeader } from '@/src/features/Onboarding/components/OnboardingHeader' +import { install } from 'react-native-quick-crypto' + +install() configureReanimatedLogger({ level: ReanimatedLogLevel.warn, diff --git a/apps/mobile/index.js b/apps/mobile/index.js new file mode 100644 index 0000000000..1d6e981ef6 --- /dev/null +++ b/apps/mobile/index.js @@ -0,0 +1,8 @@ +import { registerRootComponent } from 'expo'; + +import App from './App'; + +// registerRootComponent calls AppRegistry.registerComponent('main', () => App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index b63e80a53d..f3980d049d 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -16,6 +16,11 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { } } + if (moduleName === 'crypto') { + // when importing crypto, resolve to react-native-quick-crypto + return context.resolveRequest(context, 'react-native-quick-crypto', platform) + } + return defaultResolveResult } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index a248a53490..d6c4ad1f5c 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@cowprotocol/app-data": "^2.3.0", + "@ethersproject/shims": "^5.7.0", "@expo/config-plugins": "^9.0.10", "@expo/vector-icons": "^14.0.2", "@react-native-clipboard/clipboard": "^1.15.0", @@ -60,6 +61,7 @@ "burnt": "^0.12.2", "date-fns": "^4.1.0", "deepmerge": "^4.3.1", + "ethers": "^6.13.4", "expo": "~52.0.14", "expo-blur": "~14.0.1", "expo-constants": "~17.0.2", @@ -79,9 +81,13 @@ "react-dom": "^18.3.1", "react-native": "0.76.3", "react-native-collapsible-tab-view": "^8.0.0", + "react-native-device-crypto": "^0.1.7", + "react-native-device-info": "^14.0.1", "react-native-gesture-handler": "~2.20.2", + "react-native-keychain": "^9.2.2", "react-native-mmkv": "^3.1.0", "react-native-pager-view": "6.5.1", + "react-native-quick-crypto": "^0.7.10", "react-native-reanimated": "^3.16.2", "react-native-safe-area-context": "4.12.0", "react-native-screens": "^4.0.0", diff --git a/apps/mobile/src/config/ethers.ts b/apps/mobile/src/config/ethers.ts new file mode 100644 index 0000000000..52a030eebf --- /dev/null +++ b/apps/mobile/src/config/ethers.ts @@ -0,0 +1,42 @@ +//TODO: the interface of ethersjs register is not compatible +// with the interface of suggested crypto functions +// that is why we do casting. +// reference: https://docs.ethers.org/v6/cookbook/react-native/ + +import { BytesLike, ethers } from 'ethers' + +import crypto from 'react-native-quick-crypto' + +ethers.randomBytes.register((length) => { + return new Uint8Array(crypto.randomBytes(length)) +}) + +ethers.computeHmac.register((algo, key, data) => { + return crypto.createHmac(algo, key).update(data).digest() +}) + +ethers.pbkdf2.register( + ( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen: number, + algo: 'sha256' | 'sha512', + ): BytesLike => { + return crypto.pbkdf2Sync(password, salt, iterations, keylen, algo) as unknown as BytesLike + }, +) + +ethers.sha256.register((data) => { + return crypto + .createHash('sha256') + .update(data as unknown as string) + .digest() +}) + +ethers.sha512.register((data) => { + return crypto + .createHash('sha512') + .update(data as unknown as string) + .digest() +}) diff --git a/apps/mobile/src/hooks/useSign/index.ts b/apps/mobile/src/hooks/useSign/index.ts new file mode 100644 index 0000000000..26d610020f --- /dev/null +++ b/apps/mobile/src/hooks/useSign/index.ts @@ -0,0 +1 @@ +export { useSign } from './useSign' diff --git a/apps/mobile/src/hooks/useSign/useSign.test.ts b/apps/mobile/src/hooks/useSign/useSign.test.ts new file mode 100644 index 0000000000..342f5bea81 --- /dev/null +++ b/apps/mobile/src/hooks/useSign/useSign.test.ts @@ -0,0 +1,65 @@ +import { act, renderHook } from '@/src/tests/test-utils' +import { asymmetricKey, keychainGenericPassword, useSign } from './useSign' +import { HDNodeWallet, Wallet } from 'ethers' +import * as Keychain from 'react-native-keychain' +import DeviceCrypto from 'react-native-device-crypto' + +describe('useSign', () => { + it('should store the private key given a private key', async () => { + const { result } = renderHook(() => useSign()) + const { privateKey } = Wallet.createRandom() + const spy = jest.spyOn(Keychain, 'setGenericPassword') + const asymmetricKeySpy = jest.spyOn(DeviceCrypto, 'getOrCreateAsymmetricKey') + const encryptSpy = jest.spyOn(DeviceCrypto, 'encrypt') + + await act(async () => { + await result.current.storePrivateKey(privateKey) + }) + + expect(asymmetricKeySpy).toHaveBeenCalledWith(asymmetricKey, { accessLevel: 2, invalidateOnNewBiometry: true }) + expect(encryptSpy).toHaveBeenCalledWith(asymmetricKey, privateKey, { + biometryTitle: 'Authenticate', + biometrySubTitle: 'Saving key', + biometryDescription: 'Please authenticate yourself', + }) + expect(spy).toHaveBeenCalledWith( + keychainGenericPassword, + JSON.stringify({ encryptyedPassword: 'encryptedText', iv: `${privateKey}000` }), + ) + }) + + it('should decrypt and get the stored private key after it is encrypted', async () => { + const { result } = renderHook(() => useSign()) + const { privateKey } = Wallet.createRandom() + const spy = jest.spyOn(Keychain, 'setGenericPassword') + let returnedKey = null + + // To generate the iv and wait till the hook re-renders + await act(async () => { + await result.current.storePrivateKey(privateKey) + }) + + await act(async () => { + returnedKey = await result.current.getPrivateKey() + }) + + expect(spy).toHaveBeenCalledWith( + 'safeuser', + JSON.stringify({ encryptyedPassword: 'encryptedText', iv: `${privateKey}000` }), + ) + expect(returnedKey).toBe(privateKey) + }) + + it('should import a wallet when given a mnemonic phrase', async () => { + const { result } = renderHook(() => useSign()) + const { mnemonic, privateKey } = Wallet.createRandom() + + // To generate the iv and wait till the hook re-renders + await act(async () => { + const wallet = await result.current.createMnemonicAccount(mnemonic?.phrase as string) + + expect(wallet).toBeInstanceOf(HDNodeWallet) + expect(wallet?.privateKey).toBe(privateKey) + }) + }) +}) diff --git a/apps/mobile/src/hooks/useSign/useSign.ts b/apps/mobile/src/hooks/useSign/useSign.ts new file mode 100644 index 0000000000..a6030bf6d9 --- /dev/null +++ b/apps/mobile/src/hooks/useSign/useSign.ts @@ -0,0 +1,76 @@ +import DeviceCrypto from 'react-native-device-crypto' +import * as Keychain from 'react-native-keychain' +import DeviceInfo from 'react-native-device-info' +import { Wallet } from 'ethers' + +export const asymmetricKey = 'safe' +export const keychainGenericPassword = 'safeuser' + +export function useSign() { + // TODO: move it to a global context or reduce + const storePrivateKey = async (privateKey: string) => { + try { + const isEmulator = await DeviceInfo.isEmulator() + + await DeviceCrypto.getOrCreateAsymmetricKey(asymmetricKey, { + accessLevel: isEmulator ? 1 : 2, + invalidateOnNewBiometry: true, + }) + + const encryptyedPrivateKey = await DeviceCrypto.encrypt(asymmetricKey, privateKey, { + biometryTitle: 'Authenticate', + biometrySubTitle: 'Saving key', + biometryDescription: 'Please authenticate yourself', + }) + + await Keychain.setGenericPassword( + keychainGenericPassword, + JSON.stringify({ + encryptyedPassword: encryptyedPrivateKey.encryptedText, + iv: encryptyedPrivateKey.iv, + }), + ) + } catch (err) { + console.log(err) + } + } + + const getPrivateKey = async () => { + try { + const user = await Keychain.getGenericPassword() + + if (!user) { + throw 'user password not found' + } + + const { encryptyedPassword, iv } = JSON.parse(user.password) + const decryptedKey = await DeviceCrypto.decrypt(asymmetricKey, encryptyedPassword, iv, { + biometryTitle: 'Authenticate', + biometrySubTitle: 'Signing', + biometryDescription: 'Authenticate yourself to sign the text', + }) + + return decryptedKey + } catch (err) { + console.log(err) + } + } + + const createMnemonicAccount = async (mnemonic: string) => { + try { + if (!mnemonic) { + return + } + + return Wallet.fromPhrase(mnemonic) + } catch (err) { + console.log(err) + } + } + + return { + storePrivateKey, + getPrivateKey, + createMnemonicAccount, + } +} diff --git a/apps/mobile/src/tests/jest.setup.tsx b/apps/mobile/src/tests/jest.setup.tsx index 8643b5a8d1..70c19757e4 100644 --- a/apps/mobile/src/tests/jest.setup.tsx +++ b/apps/mobile/src/tests/jest.setup.tsx @@ -1,4 +1,7 @@ +import React from 'react' + import '@testing-library/react-native/extend-expect' +import mockRNDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock' jest.useFakeTimers() @@ -22,6 +25,40 @@ jest.mock('react-native-mmkv', () => ({ }, })) +jest.mock('react-native-device-info', () => mockRNDeviceInfo) +jest.mock('react-native-device-crypto', () => { + return { + getOrCreateAsymmetricKey: jest.fn(), + encrypt: jest.fn((_asymmetricKey: string, privateKey: string) => { + return Promise.resolve({ + encryptedText: 'encryptedText', + iv: privateKey + '000', + }) + }), + decrypt: jest.fn((_name, _password, iv) => Promise.resolve(iv.slice(0, -3))), + } +}) + +jest.mock('react-native-keychain', () => { + let password: string | null = null + return { + setGenericPassword: jest.fn((_user, newPassword: string) => { + password = newPassword + + return Promise.resolve(password) + }), + getGenericPassword: jest.fn(() => + Promise.resolve({ + password, + }), + ), + resetGenericPassword: jest.fn(() => { + password = null + Promise.resolve(null) + }), + } +}) + jest.mock('expo-splash-screen', () => ({ preventAutoHideAsync: jest.fn(), setOptions: jest.fn(), diff --git a/apps/mobile/src/types/react-native-device-info.d.ts b/apps/mobile/src/types/react-native-device-info.d.ts new file mode 100644 index 0000000000..f474fd9a63 --- /dev/null +++ b/apps/mobile/src/types/react-native-device-info.d.ts @@ -0,0 +1 @@ +declare module 'react-native-device-info/jest/react-native-device-info-mock' From 576bd4728f97154ac237e8e511872eaf13984910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=B3vis=20Neto?= Date: Tue, 24 Dec 2024 10:30:53 +0100 Subject: [PATCH 3/5] feat: create account management functionality (#60) * feat: create useSign hook * feat: add react-native-quick-crypto back * feat: create chainsDisplay component to show the grouped chains images * chore: move redux provider to the top of the react three to make the bottomSheet provider able to read values from redux selectors * feat: create badge variant for using the badge component in the chainsDisplay component according to figma * feat: add testIDs in the chainsDisplay component * feat: allow user to provider a footer to the dropdown component * chore: make the logo size dynamically * feat: create AccountCard component * feat: create AccountItem component to handle the dropdown events and specific layout * feat: replace the image component by the chainsDisplay component in the Balances component * feat: change the color of the active chain in the chain selection dropdown * feat: create a footer for MyAccounts dropdown * feat: remove unnecessary tests * feat: add accounts management feature inside the navbar dropdown * feat: create useInfiniteScroll hook to handle infinite scroll functionality * feat: use activeChain information into the tokens container * feat: make the Identicon component to be more extensible * feat: add mocked constants inside the store folder * feat: create safes slice to store all safes added into the app * feat: add possibility to get all supported chains ids and get them also by id * chore: auto generated types * chore: remove unused types * feat: create MyAccounts container * feat: use isFetching instead isLoading to avoid cached result while query is being revalidated * chore: memoize the chains manipulation in the chainsDisplay component * chore: create an useMyAccountsService hook to handle pos-fetch logic outside the container (cherry picked from commit be1a137b30984b8fcc866fd6767824f5f67ec659) --- apps/mobile/.gitignore | 3 +- apps/mobile/app/_layout.tsx | 16 +- apps/mobile/src/components/Badge/Badge.tsx | 5 +- apps/mobile/src/components/Badge/theme.ts | 8 + .../ChainsDisplay/ChainsDisplay.stories.tsx | 45 + .../ChainsDisplay/ChainsDisplay.test.tsx | 30 + .../ChainsDisplay/ChainsDisplay.tsx | 34 + .../src/components/ChainsDisplay/index.ts | 1 + .../src/components/Dropdown/Dropdown.tsx | 28 +- apps/mobile/src/components/Logo/Logo.tsx | 11 +- .../Card/AccountCard/AccountCard.stories.tsx | 46 + .../Card/AccountCard/AccountCard.test.tsx | 45 + .../Card/AccountCard/AccountCard.tsx | 52 + .../Card/AccountCard/index.ts | 1 + apps/mobile/src/config/constants.ts | 6 +- .../AccountItem/AccountItem.stories.tsx | 64 + .../AccountItem/AccountItem.test.tsx | 59 + .../components/AccountItem/AccountItem.tsx | 48 + .../Assets/components/AccountItem/index.ts | 2 + .../components/Balance/Balance.container.tsx | 12 +- .../Assets/components/Balance/Balance.tsx | 9 +- .../Assets/components/Balance/ChainItems.tsx | 2 +- .../MyAccounts/MyAccounts.container.tsx | 46 + .../MyAccounts/MyAccountsFooter.test.tsx | 12 + .../MyAccounts/MyAccountsFooter.tsx | 58 + .../MyAccounts/hooks/useMyAccountsService.ts | 45 + .../Assets/components/MyAccounts/index.ts | 2 + .../Assets/components/NFTs/NFTs.container.tsx | 38 +- .../Assets/components/Navbar/Navbar.tsx | 35 +- .../components/Tokens/Tokens.container.tsx | 12 +- .../IdenticonWithBadge/IdenticonWithBadge.tsx | 10 +- .../src/hooks/useInfiniteScroll/index.ts | 1 + .../useInfiniteScroll/useInfiniteScroll.ts | 39 + apps/mobile/src/hooks/usePendingTxs/index.ts | 40 +- apps/mobile/src/store/activeChainSlice.ts | 3 +- apps/mobile/src/store/activeSafeSlice.ts | 12 +- apps/mobile/src/store/chains/index.ts | 14 + apps/mobile/src/store/constants.ts | 262 +++ apps/mobile/src/store/index.ts | 2 + apps/mobile/src/store/safesSlice.ts | 44 + apps/mobile/src/tests/jest.setup.tsx | 2 + apps/mobile/src/types/address.ts | 5 + apps/mobile/src/utils/formatters.ts | 2 + packages/store/scripts/api-schema/schema.json | 1568 ++++++++++++++--- .../src/gateway/AUTO_GENERATED/chains.ts | 1 + .../gateway/AUTO_GENERATED/notifications.ts | 85 + .../store/src/gateway/AUTO_GENERATED/safes.ts | 4 +- .../gateway/AUTO_GENERATED/transactions.ts | 3 +- packages/store/src/gateway/chains/index.ts | 2 +- 49 files changed, 2491 insertions(+), 383 deletions(-) create mode 100644 apps/mobile/src/components/ChainsDisplay/ChainsDisplay.stories.tsx create mode 100644 apps/mobile/src/components/ChainsDisplay/ChainsDisplay.test.tsx create mode 100644 apps/mobile/src/components/ChainsDisplay/ChainsDisplay.tsx create mode 100644 apps/mobile/src/components/ChainsDisplay/index.ts create mode 100644 apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.stories.tsx create mode 100644 apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.test.tsx create mode 100644 apps/mobile/src/components/transactions-list/Card/AccountCard/AccountCard.tsx create mode 100644 apps/mobile/src/components/transactions-list/Card/AccountCard/index.ts create mode 100644 apps/mobile/src/features/Assets/components/AccountItem/AccountItem.stories.tsx create mode 100644 apps/mobile/src/features/Assets/components/AccountItem/AccountItem.test.tsx create mode 100644 apps/mobile/src/features/Assets/components/AccountItem/AccountItem.tsx create mode 100644 apps/mobile/src/features/Assets/components/AccountItem/index.ts create mode 100644 apps/mobile/src/features/Assets/components/MyAccounts/MyAccounts.container.tsx create mode 100644 apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.test.tsx create mode 100644 apps/mobile/src/features/Assets/components/MyAccounts/MyAccountsFooter.tsx create mode 100644 apps/mobile/src/features/Assets/components/MyAccounts/hooks/useMyAccountsService.ts create mode 100644 apps/mobile/src/features/Assets/components/MyAccounts/index.ts create mode 100644 apps/mobile/src/hooks/useInfiniteScroll/index.ts create mode 100644 apps/mobile/src/hooks/useInfiniteScroll/useInfiniteScroll.ts create mode 100644 apps/mobile/src/store/constants.ts create mode 100644 apps/mobile/src/store/safesSlice.ts 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' From e4f7fa7f8e0c2dd22358463cfb12bcf2418f8548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Clo=CC=81vis=20Neto?= Date: Tue, 24 Dec 2024 10:49:19 +0100 Subject: [PATCH 4/5] chore: update yarn.lock --- yarn.lock | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/yarn.lock b/yarn.lock index 6e3855812c..39c1caeac6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1772,6 +1772,16 @@ __metadata: languageName: node linkType: hard +"@craftzdog/react-native-buffer@npm:^6.0.5": + version: 6.0.5 + resolution: "@craftzdog/react-native-buffer@npm:6.0.5" + dependencies: + ieee754: "npm:^1.2.1" + react-native-quick-base64: "npm:^2.0.5" + checksum: 10/9963b430d0cd2af030605a7e81b3982dfec36603406d4f14d520e28936d24a79d69ca4e8a3b650734f6e45b12d87f1bd1ce86f0db11379103e7b1c024a2510d5 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -3133,6 +3143,13 @@ __metadata: languageName: node linkType: hard +"@ethersproject/shims@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/shims@npm:5.7.0" + checksum: 10/50307fbf6b76f4335b4ae324f4ce83ea88200f95589a4051685a8894a450a114680cc92531bf82252dba1e07986338895fb5ab8bcd24dd7162ab49ffa862cfc3 + languageName: node + linkType: hard + "@ethersproject/signing-key@npm:5.5.0": version: 5.5.0 resolution: "@ethersproject/signing-key@npm:5.5.0" @@ -7198,6 +7215,7 @@ __metadata: "@babel/preset-react": "npm:^7.26.3" "@cowprotocol/app-data": "npm:^2.3.0" "@eslint/js": "npm:^9.12.0" + "@ethersproject/shims": "npm:^5.7.0" "@expo/config-plugins": "npm:^9.0.10" "@expo/vector-icons": "npm:^14.0.2" "@gorhom/bottom-sheet": "npm:^5.0.6" @@ -7247,6 +7265,7 @@ __metadata: eslint-config-prettier: "npm:^9.1.0" eslint-plugin-prettier: "npm:^5.2.1" eslint-plugin-react: "npm:^7.37.1" + ethers: "npm:^6.13.4" expo: "npm:~52.0.14" expo-blur: "npm:~14.0.1" expo-constants: "npm:~17.0.2" @@ -7269,9 +7288,13 @@ __metadata: react-dom: "npm:^18.3.1" react-native: "npm:0.76.3" react-native-collapsible-tab-view: "npm:^8.0.0" + react-native-device-crypto: "npm:^0.1.7" + react-native-device-info: "npm:^14.0.1" react-native-gesture-handler: "npm:~2.20.2" + react-native-keychain: "npm:^9.2.2" react-native-mmkv: "npm:^3.1.0" react-native-pager-view: "npm:6.5.1" + react-native-quick-crypto: "npm:^0.7.10" react-native-reanimated: "npm:^3.16.2" react-native-safe-area-context: "npm:4.12.0" react-native-screens: "npm:^4.0.0" @@ -27535,6 +27558,25 @@ __metadata: languageName: node linkType: hard +"react-native-device-crypto@npm:^0.1.7": + version: 0.1.7 + resolution: "react-native-device-crypto@npm:0.1.7" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/0c4f9e8b8a10659511d74e4ec33cc3353a1cbe15038e5f4bae4012d679f2733e00fac4568a3c332ce11f984c2324f9a51373858ccf6ec24a2e6755273b5e595f + languageName: node + linkType: hard + +"react-native-device-info@npm:^14.0.1": + version: 14.0.2 + resolution: "react-native-device-info@npm:14.0.2" + peerDependencies: + react-native: "*" + checksum: 10/7d08a2bc77e397dd88fbcb9e2a983dab94d2e076ba3a2f2c2b65c9721b0c1a8496b25fcc0c5c58d54c31e980c3395126c1d9cd361acd69a2358700cfaf11ca6d + languageName: node + linkType: hard + "react-native-gesture-handler@npm:~2.20.2": version: 2.20.2 resolution: "react-native-gesture-handler@npm:2.20.2" @@ -27573,6 +27615,13 @@ __metadata: languageName: node linkType: hard +"react-native-keychain@npm:^9.2.2": + version: 9.2.2 + resolution: "react-native-keychain@npm:9.2.2" + checksum: 10/7af3cc896f8c91fbd6b834841bf160b0e2149e832936cc32bf8bd3f38f6b813a91783c01cf5c24d592a6e9def930da7684384ca9beb1f2ea529935ae0372dd25 + languageName: node + linkType: hard + "react-native-mmkv@npm:^3.1.0": version: 3.1.0 resolution: "react-native-mmkv@npm:3.1.0" @@ -27614,6 +27663,34 @@ __metadata: languageName: node linkType: hard +"react-native-quick-base64@npm:^2.0.5": + version: 2.1.2 + resolution: "react-native-quick-base64@npm:2.1.2" + dependencies: + base64-js: "npm:^1.5.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/20b205612417a6c39452ad3ebd205d74fcfaa244832ec9e717b87ee075bf28076bf2445e8f690aad8cf0be6f3da89b4148eb8379d0b71d1c953bd06f66cc7c56 + languageName: node + linkType: hard + +"react-native-quick-crypto@npm:^0.7.10": + version: 0.7.10 + resolution: "react-native-quick-crypto@npm:0.7.10" + dependencies: + "@craftzdog/react-native-buffer": "npm:^6.0.5" + events: "npm:^3.3.0" + readable-stream: "npm:^4.5.2" + string_decoder: "npm:^1.3.0" + util: "npm:^0.12.5" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/58ef0f25908386473fc981ecb7cd274ee08c7872d690c9d992abb3dbd371f0e2cf3a283f9aa2ca1b1cce339773a2b7efcfc50166001b66bc7b344d7a9bd16fa1 + languageName: node + linkType: hard + "react-native-reanimated@npm:^3.16.2": version: 3.16.3 resolution: "react-native-reanimated@npm:3.16.3" @@ -27998,6 +28075,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.5.2": + version: 4.6.0 + resolution: "readable-stream@npm:4.6.0" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10/ae6faa02c2513b0314711cf4901105fc9cc9c4d8324b39fda23c9055392f83916ce747bdf3b98e67e34b23040d048f7c3e1180dd4f7ffb5e9d6223170f496954 + languageName: node + linkType: hard + "readable-stream@npm:~1.0.17, readable-stream@npm:~1.0.27-1": version: 1.0.34 resolution: "readable-stream@npm:1.0.34" From 169c024447d1155d07379711c95bc614e36fedc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Clo=CC=81vis=20Neto?= Date: Tue, 24 Dec 2024 10:57:43 +0100 Subject: [PATCH 5/5] chore: link code style file with CONTRIBUTING.md --- CONTRIBUTING.md | 4 +++ apps/mobile/CONTRIBUTING.md | 69 ------------------------------------- 2 files changed, 4 insertions(+), 69 deletions(-) delete mode 100644 apps/mobile/CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b105d7c22b..5c3ea94838 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ When contributing to this repository, please first discuss the change you wish t Please note we have a Code of Conduct (see below), please follow it in all your interactions with the project. +## Code Style + +More information [here](./docs/code-style.md) + ## CLA It is a requirement for all contributors to sign the [Contributor License Agreement (CLA)](https://safe.global/cla) in order to proceed with their contribution. diff --git a/apps/mobile/CONTRIBUTING.md b/apps/mobile/CONTRIBUTING.md deleted file mode 100644 index 691c29345b..0000000000 --- a/apps/mobile/CONTRIBUTING.md +++ /dev/null @@ -1,69 +0,0 @@ -# React Native Code Guidelines - -## Code Structure - -### General Components - -- Components that are used across multiple features should reside in the `src/components/` folder. -- Each component should have its own folder, structured as follows: - ``` - Alert/ - - Alert.tsx - - Alert.test.tsx - - Alert.stories.tsx - - index.tsx - ``` -- The main component implementation should be in a named file (e.g., `Alert.tsx`), and `index.tsx` should only be used for exporting the component. -- **Reason**: Using `index.tsx` allows for cleaner imports, e.g., - ``` - import { Alert } from 'src/components/Alert'; - ``` - instead of: - ``` - import { Alert } from 'src/components/Alert/Alert'; - ``` - -### Exporting Components - -- **Always prefer named exports over default exports.** - - Named exports make it easier to refactor and identify exports in a codebase. - -### Features and Screens - -- Feature-specific components and screens should be implemented inside the `src/features/` folder. - -#### Example: Feature File Structure - -For a feature called **Assets**, the file structure might look like this: - -``` -// src/features/Assets -- Assets.container.tsx -- index.tsx -``` - -- `index.tsx` should only export the **Assets** component/container. - -#### Subcomponents for Features - -- If a feature depends on multiple subcomponents unique to that feature, place them in a `components` subfolder. For example: - -``` -// src/features/Assets/components/AssetHeader -- AssetHeader.tsx -- AssetHeader.container.tsx -- index.tsx -``` - -### Presentation vs. Container Components - -- **Presentation Components**: - - - Responsible only for rendering the UI. - - Receive data and callbacks via props. - - Avoid direct manipulation of business logic. - - Simple business logic can be included but should generally be extracted into hooks. - -- **Container Components**: - - Handle business logic (e.g., state management, API calls, etc.). - - Pass necessary data and callbacks to the corresponding Presentation component.