diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6b2eddc6ac..4b8545f0a4 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,13 @@
{
"java.configuration.updateBuildConfiguration": "disabled",
- "typescript.tsdk": "node_modules/typescript/lib"
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "jestrunner.debugOptions": {
+ "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
+ "runtimeArgs": [
+ "test",
+ "${fileBasename}",
+ "--no-cache",
+ "--color"
+ ]
+ },
}
\ No newline at end of file
diff --git a/apps/wallet-mobile/ios/Podfile.lock b/apps/wallet-mobile/ios/Podfile.lock
index 5c9e32eaf9..b9cb45d978 100644
--- a/apps/wallet-mobile/ios/Podfile.lock
+++ b/apps/wallet-mobile/ios/Podfile.lock
@@ -495,10 +495,10 @@ PODS:
- SentryPrivate (= 8.9.3)
- SentryPrivate (8.9.3)
- Yoga (1.14.0)
- - ZXingObjC/Core (3.6.5)
- - ZXingObjC/OneD (3.6.5):
+ - ZXingObjC/Core (3.6.9)
+ - ZXingObjC/OneD (3.6.9):
- ZXingObjC/Core
- - ZXingObjC/PDF417 (3.6.5):
+ - ZXingObjC/PDF417 (3.6.9):
- ZXingObjC/Core
DEPENDENCIES:
@@ -858,7 +858,7 @@ SPEC CHECKSUMS:
Sentry: 97161cac725da1ecbe77d1445bf8a61c1e5667f1
SentryPrivate: 9a76def09fb08f9501997b8df946e8097947b94f
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9
- ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
+ ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 31f344d67f1a9c35e34eb202e3cdfeb4907367e8
diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json
index e204d6e44a..1137499aa3 100644
--- a/apps/wallet-mobile/package.json
+++ b/apps/wallet-mobile/package.json
@@ -126,12 +126,13 @@
"@yoroi/common": "1.5.2",
"@yoroi/exchange": "2.0.1",
"@yoroi/links": "1.5.4",
+ "@yoroi/portfolio": "1.0.0",
"@yoroi/resolver": "2.0.4",
+ "@yoroi/setup-wallet": "1.0.0",
"@yoroi/staking": "1.5.1",
"@yoroi/swap": "1.5.2",
"@yoroi/theme": "^1.0.0",
"@yoroi/transfer": "1.0.0",
- "@yoroi/setup-wallet": "1.0.0",
"add": "2.0.6",
"assert": "^2.0.0",
"axios": "^1.5.0",
@@ -190,6 +191,7 @@
"react-native-webview": "^11.25.0",
"react-query": "^3.39.3",
"reselect": "^4.0.0",
+ "rxjs": "^7.8.1",
"sentry-expo": "^7.0.1",
"stream-browserify": "3.0.0",
"tinycolor2": "1.4.2",
diff --git a/apps/wallet-mobile/scripts/create-mocked-token-infos.js b/apps/wallet-mobile/scripts/create-mocked-token-infos.js
new file mode 100644
index 0000000000..9bca7fa207
--- /dev/null
+++ b/apps/wallet-mobile/scripts/create-mocked-token-infos.js
@@ -0,0 +1,40 @@
+const nftCryptoKitty = {
+ decimals: 0,
+ ticker: 'CryptoKitty',
+ name: 'CryptoKitty #1234',
+ symbol: 'CK',
+ status: 'normal',
+ application: 'token',
+ tag: '',
+ reference: '0xabcdef1234567890.cryptokitty1234',
+ website: 'https://www.cryptokitties.co',
+ originalImage: 'https://cdn.example.com/ck-original1234.png',
+ id: '14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.43554259',
+ fingerprint: 'asset1s7nlt45cc82upqewvjtgu7g97l7eg483c6wu75',
+ nature: 'secondary',
+ type: 'nft',
+}
+
+function generateTokenInfos(baseToken, count) {
+ const tokens = []
+ for (let i = 0; i < count; i++) {
+ // Clone the base token object to create a new token
+ const id = `${baseToken.id.split('.')[0]}.${Buffer.from(String(i)).toString('hex')}`
+ const name = `${baseToken.name.split(' ')[0]} #${i}`
+ const reference = `${baseToken.reference.split('.')[0]}.${Buffer.from(String(i)).toString('hex')}`
+ const fingerprint = `asset${i}s7nlt45cc82upqewvjtgu7g97l7eg483c6wu${i}`
+
+ const newToken = {...baseToken, id, name, reference, fingerprint}
+
+ tokens.push(newToken)
+ }
+ return tokens
+}
+
+const generatedTokens = generateTokenInfos(nftCryptoKitty, 50)
+
+const apiResponseTokenInfos = generatedTokens.reduce((acc, token, index) => {
+ acc[token.id] = [200, token, `hash${index + 1}`, 3600]
+ return acc
+}, {})
+
diff --git a/apps/wallet-mobile/src/AppNavigator.tsx b/apps/wallet-mobile/src/AppNavigator.tsx
index cfdfb3a24d..d97f976e4b 100644
--- a/apps/wallet-mobile/src/AppNavigator.tsx
+++ b/apps/wallet-mobile/src/AppNavigator.tsx
@@ -19,6 +19,7 @@ import {ModalScreen} from './components/Modal/ModalScreen'
import {AgreementChangedNavigator, InitializationNavigator} from './features/Initialization'
import {LegalAgreement, useLegalAgreement} from './features/Initialization/common'
import {useDeepLinkWatcher} from './features/Links/common/useDeepLinkWatcher'
+import {PortfolioScreen} from './features/Portfolio/useCases/PortfolioScreen'
import {AddWalletNavigator} from './features/SetupWallet/SetupWalletNavigator'
import {CONFIG} from './legacy/config'
import {DeveloperScreen} from './legacy/DeveloperScreen'
@@ -160,6 +161,8 @@ export const AppNavigator = () => {
+
+
)}
@@ -248,9 +251,9 @@ type FirstAction = 'auth-with-pin' | 'auth-with-os' | 'request-new-pin' | 'first
const getFirstAction = (
isAuthOsSupported: boolean,
authSetting: AuthSetting,
- agreement: LegalAgreement | undefined,
+ legalAgreement: LegalAgreement | undefined | null,
): FirstAction => {
- const hasAccepted = agreement?.latestAcceptedAgreementsDate === CONFIG.AGREEMENT_DATE
+ const hasAccepted = legalAgreement?.latestAcceptedAgreementsDate === CONFIG.AGREEMENT_DATE
if (isString(authSetting) && !hasAccepted) return 'show-agreement-changed-notice'
@@ -264,7 +267,7 @@ const getFirstAction = (
const useFirstAction = () => {
const authSetting = useAuthSetting()
const isAuthOsSupported = useIsAuthOsSupported()
- const terms = useLegalAgreement()
+ const legalAgreement = useLegalAgreement()
- return getFirstAction(isAuthOsSupported, authSetting, terms)
+ return getFirstAction(isAuthOsSupported, authSetting, legalAgreement)
}
diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts
new file mode 100644
index 0000000000..faa177bea4
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts
@@ -0,0 +1,73 @@
+import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common'
+import {createPrimaryTokenInfo, portfolioBalanceManagerMaker, portfolioBalanceStorageMaker} from '@yoroi/portfolio'
+import {Portfolio} from '@yoroi/types'
+import * as React from 'react'
+
+import {YoroiWallet} from '../../../yoroi-wallets/cardano/types'
+
+export const usePortfolioBalanceManager = ({
+ tokenManager,
+ walletId,
+}: {
+ tokenManager: Portfolio.Manager.Token
+ walletId: YoroiWallet['id']
+}) => {
+ return React.useMemo(() => {
+ const balanceStorageMounted = mountMMKVStorage({path: `balance/${walletId}/`})
+ const primaryBreakdownStorageMounted = mountMMKVStorage({
+ path: `/primary-breakdown/${walletId}/`,
+ })
+
+ const balanceStorage = portfolioBalanceStorageMaker({
+ balanceStorage: observableStorageMaker(balanceStorageMounted),
+ primaryBreakdownStorage: observableStorageMaker(primaryBreakdownStorageMounted),
+ })
+
+ const balanceManager = portfolioBalanceManagerMaker({
+ tokenManager,
+ storage: balanceStorage,
+ primaryToken: {
+ info: primaryTokenInfo,
+ discovery: {
+ counters: {
+ items: 0,
+ supply: 0n,
+ totalItems: 0,
+ },
+ id: primaryTokenInfo.id,
+ originalMetadata: {
+ filteredMintMetadatum: null,
+ referenceDatum: null,
+ tokenRegistry: null,
+ },
+ properties: {},
+ source: {
+ decimals: Portfolio.Token.Source.Metadata,
+ name: Portfolio.Token.Source.Metadata,
+ ticker: Portfolio.Token.Source.Metadata,
+ symbol: Portfolio.Token.Source.Metadata,
+ image: Portfolio.Token.Source.Metadata,
+ },
+ },
+ },
+ sourceId: walletId,
+ })
+
+ balanceManager.hydrate()
+ return {
+ balanceManager,
+ balanceStorage,
+ }
+ }, [tokenManager, walletId])
+}
+
+const primaryTokenInfo = createPrimaryTokenInfo({
+ decimals: 6,
+ name: 'ADA',
+ ticker: 'ADA',
+ symbol: '$',
+ reference: '',
+ tag: '',
+ website: '',
+ originalImage: '',
+})
diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts
new file mode 100644
index 0000000000..6286ddfcaa
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts
@@ -0,0 +1,27 @@
+import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common'
+import {portfolioApiMaker, portfolioTokenManagerMaker, portfolioTokenStorageMaker} from '@yoroi/portfolio'
+import {Chain, Portfolio} from '@yoroi/types'
+import * as React from 'react'
+
+export const usePortfolioTokenManager = ({network}: {network: Chain.Network}) => {
+ return React.useMemo(() => {
+ const tokenDiscoveryStorageMounted = mountMMKVStorage({path: `${network}/token-discovery/`})
+ const tokenInfoStorageMounted = mountMMKVStorage({path: `${network}/token-info/`})
+
+ const tokenStorage = portfolioTokenStorageMaker({
+ tokenDiscoveryStorage: observableStorageMaker(tokenDiscoveryStorageMounted),
+ tokenInfoStorage: observableStorageMaker(tokenInfoStorageMounted),
+ })
+ const api = portfolioApiMaker({
+ network,
+ })
+
+ const tokenManager = portfolioTokenManagerMaker({
+ api,
+ storage: tokenStorage,
+ })
+
+ tokenManager.hydrate({sourceId: 'initial'})
+ return {tokenManager, tokenStorage}
+ }, [network])
+}
diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx
new file mode 100644
index 0000000000..b8d7b9a2ff
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx
@@ -0,0 +1,127 @@
+import {useNavigation} from '@react-navigation/native'
+import {useObserver} from '@yoroi/common'
+import {Chain} from '@yoroi/types'
+import * as React from 'react'
+import {Text, View} from 'react-native'
+import {FlatList} from 'react-native-gesture-handler'
+import {SafeAreaView} from 'react-native-safe-area-context'
+
+import {Button, Spacer} from '../../../components'
+import {useSelectedWallet} from '../../WalletManager/Context'
+import {usePortfolioBalanceManager} from '../common/usePortfolioBalanceManager'
+import {usePortfolioTokenManager} from '../common/usePortfolioTokenManager'
+
+export const PortfolioScreen = () => {
+ const navigation = useNavigation()
+ const wallet = useSelectedWallet()
+ const {tokenManager, tokenStorage} = usePortfolioTokenManager({network: Chain.Network.Main})
+ const {balanceManager: bmW1, balanceStorage: bs1} = usePortfolioBalanceManager({
+ tokenManager,
+ walletId: wallet.id,
+ })
+
+ // wallet 2 for testing
+ const {balanceManager: bmW2, balanceStorage: bs2} = usePortfolioBalanceManager({
+ tokenManager,
+ walletId: 'wallet-2',
+ })
+ const {data: balancesW2, isPending: isPendingW2} = useObserver({
+ observable: bmW2.observable,
+ executor: () => bmW2.getBalances().all,
+ })
+ // end of wallet 2
+
+ const {data: balances, isPending} = useObserver({
+ observable: bmW1.observable,
+ executor: () => bmW1.getBalances().all,
+ })
+ const opacity = isPending || isPendingW2 ? 0.5 : 1
+
+ const handleOnSync = () => {
+ bmW1.sync({
+ primaryBalance: {
+ balance: 1n,
+ lockedInBuiltTxs: 2n,
+ minRequiredByTokens: 0n,
+ records: [],
+ },
+ secondaryBalances: new Map([
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.34', {balance: 2n, lockedInBuiltTxs: 0n}],
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.35', {balance: 3n, lockedInBuiltTxs: 0n}],
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.36', {balance: 4n, lockedInBuiltTxs: 0n}],
+ ]),
+ })
+
+ bmW2.sync({
+ primaryBalance: {
+ balance: 2n,
+ lockedInBuiltTxs: 3n,
+ minRequiredByTokens: 0n,
+ records: [],
+ },
+ secondaryBalances: new Map([
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3130', {balance: 222n, lockedInBuiltTxs: 0n}],
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.35', {balance: 223n, lockedInBuiltTxs: 0n}],
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3131', {balance: 224n, lockedInBuiltTxs: 0n}],
+ ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3132', {balance: 224n, lockedInBuiltTxs: 0n}],
+ ]),
+ })
+ }
+
+ const handleOnReset = () => {
+ bs1.clear()
+ bs2.clear()
+ tokenStorage.clear()
+ navigation.goBack()
+ }
+
+ return (
+
+
+ Portfolio playground
+
+
+
+
+
+
+
+
+
+
+
+ w1 {wallet.id}
+
+ all: {balances.length}
+
+ item.info.id}
+ renderItem={({item}) => (
+
+ {item.info.name}
+
+ {item.balance.toString()}
+
+ )}
+ />
+
+ wallet 2
+
+ all: {balances.length}
+
+ item.info.id}
+ renderItem={({item}) => (
+
+ {item.info.name}
+
+ {item.balance.toString()}
+
+ )}
+ />
+
+
+ )
+}
diff --git a/apps/wallet-mobile/src/legacy/DeveloperScreen.tsx b/apps/wallet-mobile/src/legacy/DeveloperScreen.tsx
index a887c2b41d..981afa0eda 100644
--- a/apps/wallet-mobile/src/legacy/DeveloperScreen.tsx
+++ b/apps/wallet-mobile/src/legacy/DeveloperScreen.tsx
@@ -85,6 +85,8 @@ export const DeveloperScreen = () => {
onPress={() => storageVersionMaker(rootStorage).remove()}
/>
+