Skip to content

Commit

Permalink
chore(portfolio): base sync portfolio (#3195)
Browse files Browse the repository at this point in the history
Signed-off-by: Juliano Lazzarotto <[email protected]>
Co-authored-by: Michal S <[email protected]>
  • Loading branch information
stackchain and michaeljscript authored Apr 15, 2024
1 parent da57048 commit 749f8ea
Show file tree
Hide file tree
Showing 174 changed files with 8,165 additions and 308 deletions.
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
]
},
}
8 changes: 4 additions & 4 deletions apps/wallet-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -858,7 +858,7 @@ SPEC CHECKSUMS:
Sentry: 97161cac725da1ecbe77d1445bf8a61c1e5667f1
SentryPrivate: 9a76def09fb08f9501997b8df946e8097947b94f
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5

PODFILE CHECKSUM: 31f344d67f1a9c35e34eb202e3cdfeb4907367e8

Expand Down
4 changes: 3 additions & 1 deletion apps/wallet-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions apps/wallet-mobile/scripts/create-mocked-token-infos.js
Original file line number Diff line number Diff line change
@@ -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
}, {})

11 changes: 7 additions & 4 deletions apps/wallet-mobile/src/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -160,6 +161,8 @@ export const AppNavigator = () => {
<Stack.Screen name="developer" component={DeveloperScreen} options={{headerShown: false}} />

<Stack.Screen name="storybook" component={StorybookScreen} />

<Stack.Screen name="portfolio-dashboard" component={PortfolioScreen} />
</Stack.Group>
)}
</Stack.Navigator>
Expand Down Expand Up @@ -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'

Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<Portfolio.Token.Id>({path: `balance/${walletId}/`})
const primaryBreakdownStorageMounted = mountMMKVStorage<Portfolio.Token.Id>({
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: '',
})
Original file line number Diff line number Diff line change
@@ -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<Portfolio.Token.Id>({path: `${network}/token-discovery/`})
const tokenInfoStorageMounted = mountMMKVStorage<Portfolio.Token.Id>({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])
}
127 changes: 127 additions & 0 deletions apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SafeAreaView edges={['bottom', 'top', 'left', 'right']}>
<View style={{padding: 16}}>
<Text>Portfolio playground</Text>

<Spacer height={16} />

<Button title="sync" onPress={handleOnSync} shelleyTheme />

<Spacer height={16} />

<Button title="reset" onPress={handleOnReset} outlineShelley />

<Spacer height={16} />

<Text>w1 {wallet.id}</Text>

<Text>all: {balances.length}</Text>

<FlatList
data={balances}
keyExtractor={(item) => item.info.id}
renderItem={({item}) => (
<View style={{padding: 8, backgroundColor: 'lightgray', marginVertical: 4, opacity}}>
<Text>{item.info.name}</Text>

<Text>{item.balance.toString()}</Text>
</View>
)}
/>

<Text>wallet 2</Text>

<Text>all: {balances.length}</Text>

<FlatList
data={balancesW2}
keyExtractor={(item) => item.info.id}
renderItem={({item}) => (
<View style={{padding: 8, backgroundColor: 'lightgray', marginVertical: 4, opacity}}>
<Text>{item.info.name}</Text>

<Text>{item.balance.toString()}</Text>
</View>
)}
/>
</View>
</SafeAreaView>
)
}
2 changes: 2 additions & 0 deletions apps/wallet-mobile/src/legacy/DeveloperScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const DeveloperScreen = () => {
onPress={() => storageVersionMaker(rootStorage).remove()}
/>

<Button title="Portfolio" style={styles.button} onPress={() => navigation.navigate('portfolio-dashboard')} />

<Button
title="Logout"
style={styles.button}
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet-mobile/src/migrations/4_9_0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('migrateAuthSetting', () => {

// if the store is inconsistent we favor OS, so the user can disable on device and it will ask for a new pin
it('old store is pin + os (inconsistent), method = "os"', async () => {
await rootStorage.join('appSettings/').multiSet([
await rootStorage.join('appSettings/').multiSet<string | boolean>([
['customPinHash', 'encrypted-hash'],
[OLD_OS_AUTH_KEY, true],
])
Expand All @@ -41,7 +41,7 @@ describe('migrateAuthSetting', () => {
})

it('old store is pin, method = "pin"', async () => {
await rootStorage.join('appSettings/').multiSet([
await rootStorage.join('appSettings/').multiSet<string | boolean>([
['customPinHash', 'encrypted-hash'],
[OLD_OS_AUTH_KEY, false],
])
Expand Down
Loading

0 comments on commit 749f8ea

Please sign in to comment.