From 585d3066e37e7a865fc3601a15eca2f64f0eebf0 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:34:36 +0100 Subject: [PATCH 01/75] chore(types): added SupportedNetworks --- packages/types/src/chain/network.ts | 5 +++++ packages/types/src/index.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/types/src/chain/network.ts b/packages/types/src/chain/network.ts index c5e71642e3..31b7ab2b24 100644 --- a/packages/types/src/chain/network.ts +++ b/packages/types/src/chain/network.ts @@ -5,3 +5,8 @@ export enum ChainNetwork { Preprod = 'preprod', Sancho = 'sancho', } + +export type ChainSupportedNetworks = + | ChainNetwork.Main + | ChainNetwork.Preprod + | ChainNetwork.Sancho diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8fcaa19dd8..9ef5c8f3be 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -165,7 +165,7 @@ import { PortfolioTokenBalance, } from './portfolio/balance' import {PortfolioTokenPrice} from './portfolio/price' -import {ChainNetwork} from './chain/network' +import {ChainNetwork, ChainSupportedNetworks} from './chain/network' import {NumbersErrorInvalidAtomicValue} from './numbers/errors' import {NumbersAtomicValue} from './numbers/atomic-value' import {AppErrorInvalidState} from './app/errors' @@ -490,6 +490,7 @@ export namespace Portfolio { export namespace Chain { export type Network = ChainNetwork export const Network = ChainNetwork + export type SupportedNetworks = ChainSupportedNetworks export namespace Cardano { export type UnsignedTx = CardanoUnsignedTx From 50054431e61ea7faab5918b228f092c336a5cba8 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:49:09 +0100 Subject: [PATCH 02/75] refactor(portfolio): refactor portfolio balance for pt --- .../features/Portfolio/common/transformers.ts | 49 +++++ .../common/usePortfolioBalanceManager.ts | 119 ++++++---- .../common/usePortfolioTokenManager.ts | 68 ++++-- .../Portfolio/useCases/PortfolioScreen.tsx | 2 + .../src/wallet-manager/walletManager.ts | 40 ++-- .../cardano/shelley/ShelleyWallet.ts | 41 +++- .../transactionManager/transactionManager.ts | 2 + .../src/yoroi-wallets/cardano/types.ts | 2 +- .../balance-storage-maker.test.ts | 34 +-- .../mmkv-storage/balance-storage-maker.ts | 18 +- .../mmkv-storage/token-storage-maker.ts | 2 +- .../src/adapters/token-balance.mocks.ts | 20 +- .../portfolio/src/balance-manager.test.ts | 208 +++++++++++++++--- packages/portfolio/src/balance-manager.ts | 188 +++++++++++----- packages/portfolio/src/index.ts | 2 +- packages/portfolio/src/token-manager.ts | 2 +- .../src/transformers/deserializers.ts | 12 +- .../primary-balance-breakdown.test.ts | 50 ----- .../validators/primary-balance-breakdown.ts | 26 --- .../src/validators/primary-breakdown.test.ts | 45 ++++ .../src/validators/primary-breakdown.ts | 19 ++ .../src/validators/token-balance.test.ts | 8 +- .../portfolio/src/validators/token-balance.ts | 1 - packages/types/src/index.ts | 6 +- packages/types/src/portfolio/balance.ts | 28 +-- packages/types/src/portfolio/discovery.ts | 2 +- packages/types/src/portfolio/event.ts | 1 + packages/types/src/portfolio/manager.ts | 52 +++-- packages/types/src/portfolio/storage.ts | 11 +- 29 files changed, 701 insertions(+), 357 deletions(-) create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers.ts delete mode 100644 packages/portfolio/src/validators/primary-balance-breakdown.test.ts delete mode 100644 packages/portfolio/src/validators/primary-balance-breakdown.ts create mode 100644 packages/portfolio/src/validators/primary-breakdown.test.ts create mode 100644 packages/portfolio/src/validators/primary-breakdown.ts diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts new file mode 100644 index 0000000000..39df6515a0 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts @@ -0,0 +1,49 @@ +import {parseTokenId} from '@yoroi/portfolio' +import {Chain, Portfolio} from '@yoroi/types' +import {freeze} from 'immer' + +import {NetworkId, RawUtxo, RemoteAsset} from '../../../yoroi-wallets/types' + +export function toChainSupportedNetwork(networkId: NetworkId): Chain.SupportedNetworks { + switch (networkId) { + case 0: + case 1: + return Chain.Network.Main + case 450: + return Chain.Network.Sancho + default: + return Chain.Network.Preprod + } +} + +export function toBalanceManagerArgs(rawUtxos: RawUtxo[]) { + let ptBalance = 0n + const secondary = new Map>() + for (const utxo of rawUtxos) { + ptBalance += BigInt(utxo.amount) + for (const record of utxo.assets) { + const tokenId = toTokenId(record.assetId) + if (!tokenId) continue // skip invalid token ids + secondary.set(tokenId, { + balance: secondary.get(tokenId)?.balance ?? 0n + BigInt(record.amount), + }) + } + } + + return freeze( + { + primaryBalance: { + balance: ptBalance, + minRequiredByTokens: 0n, + lockedInBuiltTxs: 0n, + records: [], + }, + secondaryBalances: new Map(secondary), + }, + true, + ) +} + +const toTokenId = (assetId: RemoteAsset['assetId']) => { + return parseTokenId(`${assetId.slice(0, 56)}.${assetId.slice(56)}`) +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts index faa177bea4..01c2b6e452 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts +++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts @@ -1,6 +1,6 @@ import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common' import {createPrimaryTokenInfo, portfolioBalanceManagerMaker, portfolioBalanceStorageMaker} from '@yoroi/portfolio' -import {Portfolio} from '@yoroi/types' +import {Chain, Portfolio} from '@yoroi/types' import * as React from 'react' import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' @@ -8,66 +8,93 @@ import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' export const usePortfolioBalanceManager = ({ tokenManager, walletId, + network, }: { tokenManager: Portfolio.Manager.Token walletId: YoroiWallet['id'] + network: Chain.Network }) => { - return React.useMemo(() => { - const balanceStorageMounted = mountMMKVStorage({path: `balance/${walletId}/`}) - const primaryBreakdownStorageMounted = mountMMKVStorage({ - path: `/primary-breakdown/${walletId}/`, - }) + return React.useMemo( + () => buildPortfolioBalanceManager({tokenManager, walletId, network}), + [network, tokenManager, walletId], + ) +} + +export const buildPortfolioBalanceManager = ({ + tokenManager, + walletId, + network, +}: { + tokenManager: Portfolio.Manager.Token + walletId: YoroiWallet['id'] + network: Chain.Network +}) => { + const primaryTokenInfo = network === Chain.Network.Main ? primaryTokenInfoMainnet : primaryTokenInfoAnyTestnet + const balanceStorageMounted = mountMMKVStorage({path: `/balance/${walletId}/`}) + const primaryBreakdownStorageMounted = mountMMKVStorage({ + path: `/primary-breakdown/${walletId}/`, + }) - const balanceStorage = portfolioBalanceStorageMaker({ - balanceStorage: observableStorageMaker(balanceStorageMounted), - primaryBreakdownStorage: observableStorageMaker(primaryBreakdownStorageMounted), - }) + 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, - }, + const balanceManager = portfolioBalanceManagerMaker({ + tokenManager, + storage: balanceStorage, + primaryToken: { + info: primaryTokenInfo, + discovery: { + counters: { + items: 0, + supply: 45n * BigInt(1e9), + 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, - }) + }, + sourceId: walletId, + }) - balanceManager.hydrate() - return { - balanceManager, - balanceStorage, - } - }, [tokenManager, walletId]) + balanceManager.hydrate() + return { + balanceManager, + balanceStorage, + } } -const primaryTokenInfo = createPrimaryTokenInfo({ +const primaryTokenInfoMainnet = createPrimaryTokenInfo({ decimals: 6, name: 'ADA', ticker: 'ADA', - symbol: '$', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', +}) + +const primaryTokenInfoAnyTestnet = createPrimaryTokenInfo({ + decimals: 6, + name: 'TADA', + ticker: 'TADA', + symbol: '₳', reference: '', tag: '', - website: '', + website: 'https://www.cardano.org/', originalImage: '', }) diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts index 6286ddfcaa..106ac31fea 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts +++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts @@ -1,27 +1,55 @@ import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common' import {portfolioApiMaker, portfolioTokenManagerMaker, portfolioTokenStorageMaker} from '@yoroi/portfolio' import {Chain, Portfolio} from '@yoroi/types' +import {freeze} from 'immer' 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]) + return React.useMemo(() => buildPortfolioTokenManager({network}), [network]) +} + +export const buildPortfolioTokenManager = ({network}: {network: Chain.Network}) => { + 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} +} + +export const buildPortfolioTokenManagers = () => { + const mainnetPortfolioTokenManager = buildPortfolioTokenManager({network: Chain.Network.Main}) + const preprodPortfolioTokenManager = buildPortfolioTokenManager({network: Chain.Network.Preprod}) + const sanchoPortfolioTokenManager = buildPortfolioTokenManager({network: Chain.Network.Sancho}) + + mainnetPortfolioTokenManager.tokenManager.hydrate({sourceId: 'initial'}) + preprodPortfolioTokenManager.tokenManager.hydrate({sourceId: 'initial'}) + sanchoPortfolioTokenManager.tokenManager.hydrate({sourceId: 'initial'}) + + const tokenManagers: Readonly<{ + [Chain.Network.Main]: Portfolio.Manager.Token + [Chain.Network.Preprod]: Portfolio.Manager.Token + [Chain.Network.Sancho]: Portfolio.Manager.Token + }> = freeze( + { + [Chain.Network.Main]: mainnetPortfolioTokenManager.tokenManager, + [Chain.Network.Preprod]: preprodPortfolioTokenManager.tokenManager, + [Chain.Network.Sancho]: sanchoPortfolioTokenManager.tokenManager, + }, + true, + ) + + return tokenManagers } diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx index 685481c8b2..db0037880d 100644 --- a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx @@ -56,12 +56,14 @@ export const PortfolioScreen = () => { const {balanceManager: bmW1, balanceStorage: bs1} = usePortfolioBalanceManager({ tokenManager, walletId: wallet.id, + network: Chain.Network.Main, }) // wallet 2 for testing const {balanceManager: bmW2, balanceStorage: bs2} = usePortfolioBalanceManager({ tokenManager, walletId: 'wallet-2', + network: Chain.Network.Main, }) const {data: balancesW2, isPending: isPendingW2} = useObserver({ observable: bmW2.observable, diff --git a/apps/wallet-mobile/src/wallet-manager/walletManager.ts b/apps/wallet-mobile/src/wallet-manager/walletManager.ts index c89939a897..c564bc7faa 100644 --- a/apps/wallet-mobile/src/wallet-manager/walletManager.ts +++ b/apps/wallet-mobile/src/wallet-manager/walletManager.ts @@ -1,8 +1,9 @@ import {parseSafe} from '@yoroi/common' -import {App} from '@yoroi/types' -import {catchError, concatMap, finalize, from, interval, of, Subject} from 'rxjs' +import {App, Chain} from '@yoroi/types' +import {catchError, concatMap, finalize, from, interval, of, startWith, Subject} from 'rxjs' import uuid from 'uuid' +import {buildPortfolioTokenManagers} from '../features/Portfolio/common/usePortfolioTokenManager' import {getCardanoWalletFactory} from '../yoroi-wallets/cardano/getWallet' import {isYoroiWallet, YoroiWallet} from '../yoroi-wallets/cardano/types' import {HWDeviceInfo} from '../yoroi-wallets/hw' @@ -16,18 +17,25 @@ import {isWalletMeta, parseWalletMeta} from './validators' const thirtyFiveSeconds = 35 * 1e3 export class WalletManager { - readonly #walletsRootStorage: App.Storage - readonly #rootStorage: App.Storage + static #instance: WalletManager + readonly #walletsRootStorage: App.Storage = rootStorage.join('wallet/') + readonly #rootStorage: App.Storage = rootStorage readonly #openedWallets: Map = new Map() public readonly walletInfos$ = new Subject() readonly #walletInfos: WalletInfos = new Map() + readonly #tokenManagersByNetwork = buildPortfolioTokenManagers() #selectedWalletId: YoroiWallet['id'] | null = null #subscriptions: Array = [] #isSyncing = false constructor() { - this.#walletsRootStorage = rootStorage.join('wallet/') - this.#rootStorage = rootStorage + if (WalletManager.#instance) return WalletManager.#instance + WalletManager.#instance = this + } + + static instance() { + if (!WalletManager.#instance) return new WalletManager() + return WalletManager.#instance } setSelectedWalletId(id: YoroiWallet['id']) { @@ -35,6 +43,10 @@ export class WalletManager { this._notify({type: 'selected-wallet-id', id}) } + getTokenManager(network: Chain.SupportedNetworks) { + return this.#tokenManagersByNetwork[network] + } + get selectedWalledId() { return this.#selectedWalletId } @@ -79,7 +91,7 @@ export class WalletManager { .subscribe() } - const subscription = interval(thirtyFiveSeconds).subscribe(syncWallets) + const subscription = interval(thirtyFiveSeconds).pipe(startWith(0)).subscribe(syncWallets) return { destroy: () => { @@ -185,8 +197,8 @@ export class WalletManager { walletImplementationId: WalletImplementationId, addressMode: AddressMode, ) { - await wallet.save() - if (!wallet.checksum) throw new Error('invalid wallet') + if (!isYoroiWallet(wallet)) throw new Error('invalid wallet') + const walletMeta: WalletMeta = { id, name, @@ -198,14 +210,10 @@ export class WalletManager { isEasyConfirmationEnabled: false, } + await wallet.save() await this.#walletsRootStorage.setItem(id, walletMeta) - if (isYoroiWallet(wallet)) { - this.#openedWallets.set(id, wallet) - return wallet - } - - throw new Error('invalid wallet') + return wallet } async openWallet(walletMeta: WalletMeta): Promise { @@ -291,7 +299,7 @@ export class WalletManager { } } -export const walletManager = new WalletManager() +export const walletManager = WalletManager.instance() walletManager.startSyncingAllWallets() export default walletManager diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/shelley/ShelleyWallet.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/shelley/ShelleyWallet.ts index 3d706f61a8..9caa385d20 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/shelley/ShelleyWallet.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/shelley/ShelleyWallet.ts @@ -4,7 +4,7 @@ import {createSignedLedgerTxFromCbor, signRawTransaction} from '@emurgo/yoroi-li import {Datum} from '@emurgo/yoroi-lib/dist/internals/models' import {AppApi, CardanoApi} from '@yoroi/api' import {isNonNullable, parseSafe} from '@yoroi/common' -import {Api, App, Balance} from '@yoroi/types' +import {Api, App, Balance, Portfolio} from '@yoroi/types' import assert from 'assert' import {BigNumber} from 'bignumber.js' import ExtendableError from 'es6-error' @@ -12,8 +12,10 @@ import _ from 'lodash' import DeviceInfo from 'react-native-device-info' import {defaultMemoize} from 'reselect' +import {buildPortfolioBalanceManager} from '../../../features/Portfolio/common/usePortfolioBalanceManager' import LocalizableError from '../../../i18n/LocalizableError' import {WalletMeta} from '../../../wallet-manager/types' +import walletManager from '../../../wallet-manager/walletManager' import {HWDeviceInfo} from '../../hw' import {Logger} from '../../logging' import {makeMemosManager, MemosManager} from '../../memos' @@ -53,6 +55,7 @@ import {processTxHistoryData} from '../processTransactions' import {filterAddressesByStakingKey, getDelegationStatus} from '../shelley/delegationUtils' import {yoroiSignedTx} from '../signedTx' import {TransactionManager} from '../transactionManager' +import {toChainSupportedNetwork} from '../../../features/Portfolio/common/transformers' import { CardanoTypes, isYoroiWallet, @@ -176,6 +179,7 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t private readonly memosManager: MemosManager private _collateralId = '' private readonly cardanoApi: Api.Cardano.Actions + private readonly balanceManager: Portfolio.Manager.Balance // =================== create =================== // @@ -291,6 +295,14 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t : 'preprod', }) + const chainNetwork = toChainSupportedNetwork(NETWORK_ID) + const {balanceManager} = buildPortfolioBalanceManager({ + tokenManager: walletManager.getTokenManager(chainNetwork), + walletId: id, + network: chainNetwork, + }) + balanceManager.refresh() + const wallet = new ShelleyWallet({ storage, id, @@ -306,6 +318,7 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t transactionManager, memosManager, cardanoApi, + balanceManager, }) await wallet.discoverAddresses() @@ -333,6 +346,7 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t transactionManager, memosManager, cardanoApi, + balanceManager, }: { storage: App.Storage id: string @@ -348,6 +362,7 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t transactionManager: TransactionManager memosManager: MemosManager cardanoApi: Api.Cardano.Actions + balanceManager: Portfolio.Manager.Balance }) { this.id = id this.storage = storage @@ -372,6 +387,7 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t this.isEasyConfirmationEnabled = isEasyConfirmationEnabled this.state = {lastGeneratedAddressIndex} this.cardanoApi = cardanoApi + this.balanceManager = balanceManager } get receiveAddresses(): Addresses { @@ -399,13 +415,20 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t return Promise.resolve() } + const addressesBeforeRequest = this.internalChain.addresses.length + this.externalChain.addresses.length await this.discoverAddresses() + const addressesAfterRequest = this.internalChain.addresses.length + this.externalChain.addresses.length + const hasAddedNewAddress = addressesAfterRequest !== addressesBeforeRequest - await Promise.all([this.syncUtxos(), this.transactionManager.doSync(this.getAddressesInBlocks(), BACKEND)]) + const [hasUpdatedUtxos, hasUpdateTxs] = await Promise.all([ + this.syncUtxos(), + this.transactionManager.doSync(this.getAddressesInBlocks(), BACKEND), + ]) - this.updateLastGeneratedAddressIndex() + const shouldPersist = + this.updateLastGeneratedAddressIndex() || hasAddedNewAddress || hasUpdateTxs || hasUpdatedUtxos - await this.save() + if (shouldPersist) await this.save() } async resync() { @@ -1011,14 +1034,18 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t const addresses = [...this.internalAddresses, ...this.externalAddresses] await this.utxoManager.sync(addresses) - const newUtxos = await this.utxoManager.getCachedUtxos() if (this.didUtxosUpdate(this._utxos, newUtxos)) { this._utxos = newUtxos - + // this.balanceManager.sync({ + // primaryBalance: {}, + // secondaryBalances: {}, + // }) this.notify({type: 'utxos', utxos: this.utxos}) + return true } + return false } get utxos() { @@ -1265,7 +1292,9 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t const lastUsedIndex = this.getLastUsedIndex(this.externalChain) if (lastUsedIndex > this.state.lastGeneratedAddressIndex) { this.state.lastGeneratedAddressIndex = lastUsedIndex + return true } + return false } // ========== UI state ============= // diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/transactionManager/transactionManager.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/transactionManager/transactionManager.ts index 294a8fa6d4..db5029b0b5 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/transactionManager/transactionManager.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/transactionManager/transactionManager.ts @@ -122,7 +122,9 @@ export class TransactionManager { // @deprecated perAddressSyncMetadata: this.#state.perAddressSyncMetadata, }) + return true } + return false } } diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts index e21702c522..f44eadc44c 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts @@ -11,7 +11,7 @@ import { TxMetadata as TxMetadataType, UnsignedTx as UnsignedTxType, } from '@emurgo/yoroi-lib' -import {Api, App, Balance} from '@yoroi/types' +import {Api, App, Balance, Portfolio} from '@yoroi/types' import {BigNumber} from 'bignumber.js' import {HWDeviceInfo} from '../hw' diff --git a/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.test.ts b/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.test.ts index 076b991169..e80028ef01 100644 --- a/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.test.ts +++ b/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.test.ts @@ -5,7 +5,6 @@ import {portfolioBalanceStorageMaker} from './balance-storage-maker' import {tokenMocks} from '../token.mocks' import {deserializers} from '../../transformers/deserializers' import {tokenBalanceMocks} from '../token-balance.mocks' -import {tokenInfoMocks} from '../token-info.mocks' describe('portfolioBalanceStorageMaker', () => { let balanceStorage: App.ObservableStorage @@ -31,6 +30,7 @@ describe('portfolioBalanceStorageMaker', () => { const storage = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, }) expect(storage).toBeDefined() @@ -41,6 +41,7 @@ describe('portfolioBalanceStorageMaker', () => { const {balances} = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, }) balances.save(tokenBalanceMocks.storage.entries1) @@ -58,6 +59,7 @@ describe('portfolioBalanceStorageMaker', () => { const {balances} = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, }) balances.save(tokenBalanceMocks.storage.entries1) @@ -76,19 +78,17 @@ describe('portfolioBalanceStorageMaker', () => { }) it('should clear all portfolio records', () => { - const {clear, balances, primaryBalanceBreakdown} = - portfolioBalanceStorageMaker({ - balanceStorage, - primaryBreakdownStorage, - }) + const {clear, balances, primaryBreakdown} = portfolioBalanceStorageMaker({ + balanceStorage, + primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, + }) balances.save(tokenBalanceMocks.storage.entries1) - primaryBalanceBreakdown.save(tokenBalanceMocks.primaryETHBreakdown) + primaryBreakdown.save(tokenBalanceMocks.primaryETHBreakdown) let balanceResult = balances.all() - let primaryBreakdownResult = primaryBalanceBreakdown.read( - tokenInfoMocks.primaryETH.id, - ) + let primaryBreakdownResult = primaryBreakdown.read() expect(balanceResult).toEqual(tokenBalanceMocks.storage.entries1) expect(primaryBreakdownResult).toEqual( @@ -98,9 +98,7 @@ describe('portfolioBalanceStorageMaker', () => { clear() balanceResult = balances.all() - primaryBreakdownResult = primaryBalanceBreakdown.read( - tokenInfoMocks.primaryETH.id, - ) + primaryBreakdownResult = primaryBreakdown.read() // keys are gone expect(balanceResult).toEqual([]) @@ -111,13 +109,14 @@ describe('portfolioBalanceStorageMaker', () => { }) it('should return all keys', () => { - const {balances, primaryBalanceBreakdown} = portfolioBalanceStorageMaker({ + const {balances, primaryBreakdown} = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, }) balances.save(tokenBalanceMocks.storage.entries1) - primaryBalanceBreakdown.save(tokenBalanceMocks.primaryETHBreakdown) + primaryBreakdown.save(tokenBalanceMocks.primaryETHBreakdown) let balanceResult = balances.keys() @@ -127,13 +126,14 @@ describe('portfolioBalanceStorageMaker', () => { }) it('should call clear', () => { - const {balances, primaryBalanceBreakdown} = portfolioBalanceStorageMaker({ + const {balances, primaryBreakdown} = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId: tokenMocks.primaryETH.info.id, }) balances.clear() - primaryBalanceBreakdown.clear() + primaryBreakdown.clear() expect(balanceStorage.clear).toHaveBeenCalled() expect(primaryBreakdownStorage.clear).toHaveBeenCalled() diff --git a/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.ts b/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.ts index 52a3834285..a209a79e4c 100644 --- a/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.ts +++ b/packages/portfolio/src/adapters/mmkv-storage/balance-storage-maker.ts @@ -6,10 +6,12 @@ import {deserializers} from '../../transformers/deserializers' export const portfolioBalanceStorageMaker = ({ balanceStorage, primaryBreakdownStorage, + primaryTokenId, }: { balanceStorage: App.ObservableStorage primaryBreakdownStorage: App.ObservableStorage -}) => { + primaryTokenId: Portfolio.Token.Id +}): Portfolio.Storage.Balance => { const balances = { save: ( entries: ReadonlyArray<[Portfolio.Token.Id, Portfolio.Token.Balance]>, @@ -32,16 +34,16 @@ export const portfolioBalanceStorageMaker = ({ clear: () => balanceStorage.clear(), } - const primaryBalanceBreakdown = { - save: (breakdown: Readonly) => + const primaryBreakdown = { + save: (breakdown: Readonly) => primaryBreakdownStorage.setItem( - breakdown.info.id, + primaryTokenId, breakdown, storageSerializer, ), - read: (key: Portfolio.Token.Id) => - primaryBreakdownStorage.getItem( - key, + read: () => + primaryBreakdownStorage.getItem( + primaryTokenId, deserializers.primaryBreakdown, ), clear: () => primaryBreakdownStorage.clear(), @@ -55,7 +57,7 @@ export const portfolioBalanceStorageMaker = ({ return freeze( { balances, - primaryBalanceBreakdown, + primaryBreakdown, clear, }, true, diff --git a/packages/portfolio/src/adapters/mmkv-storage/token-storage-maker.ts b/packages/portfolio/src/adapters/mmkv-storage/token-storage-maker.ts index dbfdc7ae38..ccb817950e 100644 --- a/packages/portfolio/src/adapters/mmkv-storage/token-storage-maker.ts +++ b/packages/portfolio/src/adapters/mmkv-storage/token-storage-maker.ts @@ -10,7 +10,7 @@ export const portfolioTokenStorageMaker = ({ }: { tokenInfoStorage: App.ObservableStorage tokenDiscoveryStorage: App.ObservableStorage -}) => { +}): Portfolio.Storage.Token => { const infos = { save: ( entries: ReadonlyArray< diff --git a/packages/portfolio/src/adapters/token-balance.mocks.ts b/packages/portfolio/src/adapters/token-balance.mocks.ts index d1a709b602..55d1ae3342 100644 --- a/packages/portfolio/src/adapters/token-balance.mocks.ts +++ b/packages/portfolio/src/adapters/token-balance.mocks.ts @@ -6,49 +6,37 @@ import {tokenInfoMocks} from './token-info.mocks' const primaryETH: Portfolio.Token.Balance = { info: tokenInfoMocks.primaryETH, balance: BigInt(1_000_000), - lockedInBuiltTxs: BigInt(0), } -const primaryETHBreakdown: Portfolio.BalancePrimaryBreakdown = { - ...primaryETH, - minRequiredByTokens: BigInt(500_000), - records: [ - { - quantity: BigInt(1_000_000), - redeemableAfter: new Date().getTime(), - source: 'rewards', - }, - ], +const primaryETHBreakdown: Portfolio.PrimaryBreakdown = { + availableRewards: BigInt(1_000_001), + totalFromTxs: BigInt(1_000_002), + lockedAsStorageCost: BigInt(1_000_003), } const missingToken: Portfolio.Token.Balance = { info: {...tokenInfoMocks.ftNameless, id: 'dead.fee'}, balance: BigInt(1), - lockedInBuiltTxs: BigInt(0), } const nftCryptoKitty: Portfolio.Token.Balance = { info: tokenInfoMocks.nftCryptoKitty, balance: BigInt(1_000_001), - lockedInBuiltTxs: BigInt(100_001), } const rnftWhatever: Portfolio.Token.Balance = { info: tokenInfoMocks.rnftWhatever, balance: BigInt(2_000_002), - lockedInBuiltTxs: BigInt(200_002), } const ftNoTicker: Portfolio.Token.Balance = { info: tokenInfoMocks.ftNoTicker, balance: BigInt(3_000_003), - lockedInBuiltTxs: BigInt(300_003), } const ftNameless: Portfolio.Token.Balance = { info: tokenInfoMocks.ftNameless, balance: BigInt(4_000_004), - lockedInBuiltTxs: BigInt(400_004), } const storage: { diff --git a/packages/portfolio/src/balance-manager.test.ts b/packages/portfolio/src/balance-manager.test.ts index 9c8d87cab1..902161bc01 100644 --- a/packages/portfolio/src/balance-manager.test.ts +++ b/packages/portfolio/src/balance-manager.test.ts @@ -16,6 +16,7 @@ const tokenInfoStorage = observableStorageMaker( path: `/test/token-info/`, }), ) +const primaryTokenId = tokenMocks.primaryETH.info.id const sourceId = 'sourceId' describe('portfolioBalanceManagerMaker', () => { @@ -34,13 +35,14 @@ describe('portfolioBalanceManagerMaker', () => { const storage: Portfolio.Storage.Balance = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId, }) const tokenManager: Portfolio.Manager.Token = { destroy: jest.fn(), hydrate: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), - observable: new BehaviorSubject({} as any).asObservable(), + observable$: new BehaviorSubject({} as any).asObservable(), sync: jest.fn().mockResolvedValue(new Map()), } @@ -72,13 +74,14 @@ describe('hydrate', () => { const storage: Portfolio.Storage.Balance = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId, }) const tokenManager: Portfolio.Manager.Token = { destroy: jest.fn(), hydrate: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), - observable: new BehaviorSubject({} as any).asObservable(), + observable$: new BehaviorSubject({} as any).asObservable(), sync: jest.fn().mockResolvedValue(new Map()), } @@ -87,15 +90,13 @@ describe('hydrate', () => { tokenInfoStorage.clear() }) - const primaryBalance: Readonly = { - info: tokenMocks.managerMaker.primaryToken.info, - balance: BigInt(1000000), - lockedInBuiltTxs: BigInt(0), - minRequiredByTokens: BigInt(0), - records: [], + const primaryStated: Readonly = { + availableRewards: 1000000n, + lockedAsStorageCost: 0n, + totalFromTxs: 0n, } - storage.primaryBalanceBreakdown.save(primaryBalance) + storage.primaryBreakdown.save(primaryStated) storage.balances.save(tokenBalanceMocks.storage.entries1) it('should hydrate data', async () => { @@ -122,9 +123,14 @@ describe('hydrate', () => { manager.hydrate() - expect(manager.getPrimaryBreakdown()).toEqual(primaryBalance) + expect(manager.getPrimaryBreakdown()).toEqual(primaryStated) expect(manager.getBalances()).toEqual(sortedBalances) + expect(manager.getPrimaryBalance()).toEqual({ + info: tokenMocks.managerMaker.primaryToken.info, + balance: primaryStated.totalFromTxs + primaryStated.availableRewards, + }) + expect(subscriber).toHaveBeenCalledTimes(1) }) }) @@ -145,6 +151,7 @@ describe('destroy', () => { const storage: Portfolio.Storage.Balance = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId, }) const tokenManagerObservable = new BehaviorSubject({} as any).asObservable() const tokenManager: jest.Mocked = { @@ -152,7 +159,7 @@ describe('destroy', () => { hydrate: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), - observable: tokenManagerObservable, + observable$: tokenManagerObservable, sync: jest.fn().mockResolvedValue(new Map()), } const queueDestroy = jest.fn() @@ -203,6 +210,156 @@ describe('destroy', () => { }) }) +describe('primary updates', () => { + const balanceStorage = observableStorageMaker( + mountMMKVStorage({ + path: '/tmp/balance/', + id: 'balance', + }), + ) + const primaryBreakdownStorage = observableStorageMaker( + mountMMKVStorage({ + path: '/tmp/primary-balance-breakdown/', + id: 'primary-balance-breakdown', + }), + ) + const storage: Portfolio.Storage.Balance = portfolioBalanceStorageMaker({ + balanceStorage, + primaryBreakdownStorage, + primaryTokenId, + }) + const tokenManagerObservable = new BehaviorSubject({} as any) + const tokenManager: jest.Mocked = { + destroy: jest.fn(), + hydrate: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + observable$: tokenManagerObservable.asObservable(), + sync: jest.fn().mockResolvedValue(new Map()), + } + + afterEach(() => { + storage.clear() + tokenInfoStorage.clear() + }) + + it('should update primary stated', () => { + const tasks: any = [] + const enqueueMock = jest.fn((task) => tasks.push(task)) + const mockedNotify = jest.fn() + + const manager = portfolioBalanceManagerMaker( + { + tokenManager, + storage, + primaryToken: tokenMocks.managerMaker.primaryToken, + sourceId, + }, + { + observer: { + destroy: jest.fn(), + notify: mockedNotify, + subscribe: jest.fn(), + unsubscribe: jest.fn(), + observable: new BehaviorSubject({} as any).asObservable(), + }, + queue: { + enqueue: enqueueMock, + destroy: jest.fn(), + observable: new BehaviorSubject({}).asObservable(), + } as any, + }, + ) + + const subscriber = jest.fn() + manager.subscribe(subscriber) + manager.hydrate() + + manager.updatePrimaryStated({ + totalFromTxs: 1000001n, + lockedAsStorageCost: 10n, + }) + + expect(manager.getPrimaryBreakdown()).toEqual({ + totalFromTxs: 1000001n, + availableRewards: 0n, + lockedAsStorageCost: 10n, + }) + expect(manager.getPrimaryBalance()).toEqual({ + info: tokenMocks.managerMaker.primaryToken.info, + balance: 1000001n, + }) + + expect(mockedNotify).toHaveBeenCalledTimes(2) + expect(mockedNotify).toHaveBeenCalledWith({ + on: Portfolio.Event.ManagerOn.Sync, + sourceId, + mode: 'primary-stated', + }) + }) + + it('should update primary derived', () => { + const tasks: any = [] + const enqueueMock = jest.fn((task) => tasks.push(task)) + const mockedNotify = jest.fn() + + const manager = portfolioBalanceManagerMaker( + { + tokenManager, + storage, + primaryToken: tokenMocks.managerMaker.primaryToken, + sourceId, + }, + { + observer: { + destroy: jest.fn(), + notify: mockedNotify, + subscribe: jest.fn(), + unsubscribe: jest.fn(), + observable: new BehaviorSubject({} as any).asObservable(), + }, + queue: { + enqueue: enqueueMock, + destroy: jest.fn(), + observable: new BehaviorSubject({}).asObservable(), + } as any, + }, + ) + + const primaryStated: Readonly = { + availableRewards: 0n, + lockedAsStorageCost: 0n, + totalFromTxs: 1000001n, + } + storage.primaryBreakdown.save(primaryStated) + + const subscriber = jest.fn() + manager.subscribe(subscriber) + manager.hydrate() + + manager.updatePrimaryDerived({ + availableRewards: 1000001n, + }) + + expect(manager.getPrimaryBreakdown()).toEqual({ + totalFromTxs: 1000001n, + availableRewards: 1000001n, + lockedAsStorageCost: 0n, + }) + expect(manager.getPrimaryBalance()).toEqual({ + info: tokenMocks.managerMaker.primaryToken.info, + balance: 2000002n, + }) + + expect(mockedNotify).toHaveBeenCalledTimes(2) + expect(mockedNotify).toHaveBeenCalledWith({ + on: Portfolio.Event.ManagerOn.Sync, + sourceId, + mode: 'primary-derived', + }) + }) +}) + describe('sync & refresh', () => { const balanceStorage = observableStorageMaker( mountMMKVStorage({ @@ -219,6 +376,7 @@ describe('sync & refresh', () => { const storage: Portfolio.Storage.Balance = portfolioBalanceStorageMaker({ balanceStorage, primaryBreakdownStorage, + primaryTokenId, }) const tokenManagerObservable = new BehaviorSubject({} as any) const tokenManager: jest.Mocked = { @@ -226,7 +384,7 @@ describe('sync & refresh', () => { hydrate: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), - observable: tokenManagerObservable.asObservable(), + observable$: tokenManagerObservable.asObservable(), sync: jest.fn().mockResolvedValue(new Map()), } @@ -235,12 +393,10 @@ describe('sync & refresh', () => { tokenInfoStorage.clear() }) - const primaryBalance: Readonly = { - info: tokenMocks.managerMaker.primaryToken.info, - balance: BigInt(1000000), - lockedInBuiltTxs: BigInt(0), - minRequiredByTokens: BigInt(0), - records: [], + const primaryStated: Readonly = { + totalFromTxs: BigInt(1000000), + availableRewards: BigInt(0), + lockedAsStorageCost: BigInt(0), } it('should sync (+hydrate)', async () => { @@ -282,8 +438,8 @@ describe('sync & refresh', () => { tokenBalanceMocks.storage.entries1.slice(0, -1), ) - manager.sync({ - primaryBalance, + manager.syncBalances({ + primaryStated, secondaryBalances, }) @@ -358,8 +514,8 @@ describe('sync & refresh', () => { tokenBalanceMocks.storage.entries1.slice(0, -1), ) - manager.sync({ - primaryBalance, + manager.syncBalances({ + primaryStated, secondaryBalances, }) @@ -421,8 +577,8 @@ describe('sync & refresh', () => { manager.hydrate() - manager.sync({ - primaryBalance, + manager.syncBalances({ + primaryStated, secondaryBalances, }) @@ -473,8 +629,8 @@ describe('sync & refresh', () => { Map > = new Map(tokenBalanceMocks.storage.entries1) - manager.sync({ - primaryBalance, + manager.syncBalances({ + primaryStated, secondaryBalances, }) diff --git a/packages/portfolio/src/balance-manager.ts b/packages/portfolio/src/balance-manager.ts index cdae599144..c1f5529300 100644 --- a/packages/portfolio/src/balance-manager.ts +++ b/packages/portfolio/src/balance-manager.ts @@ -45,26 +45,31 @@ export const portfolioBalanceManagerMaker = ( nfts: [], fts: [], }) - let primaryBreakdown: Readonly = freeze( + let primaryBreakdown: Readonly = freeze( + { + lockedAsStorageCost: 0n, + availableRewards: 0n, + totalFromTxs: 0n, + }, + true, + ) + let primaryBalance: Readonly = freeze( { - info: primaryToken.info, balance: 0n, - lockedInBuiltTxs: 0n, - minRequiredByTokens: 0n, - records: [], + info: primaryToken.info, }, true, ) - const sort = sortTokenBalancesWrapper({ + const sortBalances = balancesSorter({ primaryTokenInfo: primaryToken.info, }) - const updatePrimary = primaryTokenUpdaterWrapper({ + const updatePrimary = primaryUpdater({ primaryTokenInfo: primaryToken.info, - storagePrimaryBalanceBreakdown: storage.primaryBalanceBreakdown, + storagePrimaryBreakdown: storage.primaryBreakdown, }) - const subscription = tokenManager.observable + const subscription = tokenManager.observable$ .pipe( filter(() => isHydrated), filter((dtoEvent) => isNotTriggeredBySelf(sourceId)(dtoEvent)), @@ -93,17 +98,23 @@ export const portfolioBalanceManagerMaker = ( }) const hydrate = () => { - const cachedPrimaryBreakdown = freeze( - storage.primaryBalanceBreakdown.read(primaryToken.info.id), - true, - ) - if (cachedPrimaryBreakdown) { - primaryBreakdown = cachedPrimaryBreakdown - } - const primaryTokenBalance: Readonly = freeze( + const cachedPrimaryBreakdown = freeze(storage.primaryBreakdown.read(), true) + const lockedAsStorageCost = + cachedPrimaryBreakdown?.lockedAsStorageCost ?? 0n + const totalFromTxs = cachedPrimaryBreakdown?.totalFromTxs ?? 0n + const availableRewards = cachedPrimaryBreakdown?.availableRewards ?? 0n + + const balance = totalFromTxs + availableRewards + + const newPrimaryBreakdown: Readonly = freeze({ + lockedAsStorageCost, + availableRewards, + totalFromTxs, + }) + + const newPrimaryBalance: Readonly = freeze( { - balance: primaryBreakdown.balance, - lockedInBuiltTxs: primaryBreakdown.lockedInBuiltTxs, + balance, info: primaryToken.info, }, true, @@ -112,26 +123,78 @@ export const portfolioBalanceManagerMaker = ( new Map(storage.balances.all().filter(hasEntryValue)), true, ) - const sorted = sort({ - balances: [...secondaries.values(), primaryTokenBalance], + const sorted = sortBalances({ + balances: [...secondaries.values(), newPrimaryBalance], }) sortedBalances = splitByType(sorted) + primaryBreakdown = newPrimaryBreakdown + primaryBalance = newPrimaryBalance isHydrated = true observer.notify({on: Portfolio.Event.ManagerOn.Hydrate, sourceId}) } - const refresh = async () => - sync({primaryBalance: primaryBreakdown, secondaryBalances: secondaries}) + const refresh = () => + syncBalances({ + primaryStated: primaryBreakdown, + secondaryBalances: secondaries, + }) + + const updatePrimaryStated = ({ + lockedAsStorageCost, + totalFromTxs, + }: Pick< + Portfolio.PrimaryBreakdown, + 'totalFromTxs' | 'lockedAsStorageCost' + >) => { + // state + const {availableRewards} = primaryBreakdown + const newPrimaryBreakdown: Readonly = { + availableRewards, + // args + lockedAsStorageCost, + totalFromTxs, + } + const newPrimaryBalance = updatePrimary(newPrimaryBreakdown) + primaryBreakdown = newPrimaryBreakdown + primaryBalance = newPrimaryBalance + observer.notify({ + on: Portfolio.Event.ManagerOn.Sync, + sourceId, + mode: 'primary-stated', + }) + } + + const updatePrimaryDerived = ({ + availableRewards, + }: Pick) => { + // state + const {totalFromTxs, lockedAsStorageCost} = primaryBreakdown + const newPrimaryBreakdown: Readonly = { + lockedAsStorageCost, + totalFromTxs, + // args + availableRewards, + } + const newPrimaryBalance = updatePrimary(newPrimaryBreakdown) + primaryBreakdown = newPrimaryBreakdown + primaryBalance = newPrimaryBalance + observer.notify({ + on: Portfolio.Event.ManagerOn.Sync, + sourceId, + mode: 'primary-derived', + }) + } - const sync = ({ - primaryBalance, + const syncBalances = ({ + primaryStated, secondaryBalances, }: { - primaryBalance: Readonly> + primaryStated: Readonly< + Pick + > secondaryBalances: Readonly< - | Map> - | Map + Map> > }) => { if (!isHydrated) hydrate() @@ -150,7 +213,7 @@ export const portfolioBalanceManagerMaker = ( Portfolio.Token.Id, Portfolio.Token.Balance > = new Map() - secondaryBalances.forEach(({balance, lockedInBuiltTxs}, id) => { + secondaryBalances.forEach(({balance}, id) => { const cachedTokenInfo = tokenInfos.get(id) if (!cachedTokenInfo) return reject( @@ -159,30 +222,38 @@ export const portfolioBalanceManagerMaker = ( ), ) - const tokenBalance: Portfolio.Token.Balance = { + const secondaryBalance: Portfolio.Token.Balance = { info: cachedTokenInfo.record, balance, - lockedInBuiltTxs, } - newBalances.set(id, tokenBalance) + newBalances.set(id, secondaryBalance) }) + const {availableRewards} = primaryBreakdown + const {totalFromTxs, lockedAsStorageCost} = primaryStated + const newPrimaryBreakdown: Readonly = { + totalFromTxs, + lockedAsStorageCost, + availableRewards, + } + // persist storage.balances.clear() storage.balances.save([...newBalances.entries()]) - - const {newPrimaryBreakdown, newPrimaryTokenBalance} = - updatePrimary(primaryBalance) + const newPrimaryBalance = updatePrimary(newPrimaryBreakdown) // update state secondaries = freeze(newBalances, true) - const sorted = sort({ - balances: [...secondaries.values(), newPrimaryTokenBalance], + const sorted = sortBalances({ + balances: [...secondaries.values(), newPrimaryBalance], }) sortedBalances = splitByType(sorted) - primaryBreakdown = newPrimaryBreakdown - observer.notify({on: Portfolio.Event.ManagerOn.Sync, sourceId}) + observer.notify({ + on: Portfolio.Event.ManagerOn.Sync, + sourceId, + mode: 'all', + }) resolve() } asyncExecutor() @@ -194,6 +265,10 @@ export const portfolioBalanceManagerMaker = ( return primaryBreakdown } + function getPrimaryBalance() { + return primaryBalance + } + function getBalances() { return sortedBalances } @@ -208,13 +283,17 @@ export const portfolioBalanceManagerMaker = ( { hydrate, refresh, - sync, + + syncBalances, + updatePrimaryDerived, + updatePrimaryStated, subscribe: observer.subscribe, unsubscribe: observer.unsubscribe, - observable: observer.observable, + observable$: observer.observable, getPrimaryBreakdown, + getPrimaryBalance, getBalances, destroy, }, @@ -222,7 +301,7 @@ export const portfolioBalanceManagerMaker = ( ) } -const sortTokenBalancesWrapper = +const balancesSorter = ({primaryTokenInfo}: {primaryTokenInfo: Portfolio.Token.Info}) => ({ balances, @@ -290,32 +369,27 @@ const splitByType = ( ) } -const primaryTokenUpdaterWrapper = +const primaryUpdater = ({ - storagePrimaryBalanceBreakdown, + storagePrimaryBreakdown, primaryTokenInfo, }: Readonly<{ - storagePrimaryBalanceBreakdown: Portfolio.Storage.Balance['primaryBalanceBreakdown'] + storagePrimaryBreakdown: Portfolio.Storage.Balance['primaryBreakdown'] primaryTokenInfo: Portfolio.Token.Info }>) => ( - primaryBalance: Readonly>, - ) => { - const newPrimaryBreakdown: Readonly = { - ...primaryBalance, - info: primaryTokenInfo, - } - const newPrimaryTokenBalance: Readonly = freeze( + newPrimaryBreakdown: Readonly, + ): Readonly => { + storagePrimaryBreakdown.clear() + storagePrimaryBreakdown.save(newPrimaryBreakdown) + const {availableRewards, totalFromTxs} = newPrimaryBreakdown + return freeze( { - balance: primaryBalance.balance, - lockedInBuiltTxs: primaryBalance.lockedInBuiltTxs, info: primaryTokenInfo, + balance: availableRewards + totalFromTxs, }, true, ) - storagePrimaryBalanceBreakdown.clear() - storagePrimaryBalanceBreakdown.save(newPrimaryBreakdown) - return freeze({newPrimaryBreakdown, newPrimaryTokenBalance}, true) } // TODO list diff --git a/packages/portfolio/src/index.ts b/packages/portfolio/src/index.ts index 99a11f14d7..697ec41eb0 100644 --- a/packages/portfolio/src/index.ts +++ b/packages/portfolio/src/index.ts @@ -14,7 +14,7 @@ export * from './adapters/token.mocks' export * from './transformers/deserializers' export * from './validators/amount' -export * from './validators/primary-balance-breakdown' +export * from './validators/primary-breakdown' export * from './validators/response-record-with-cache-schema-maker' export * from './validators/token-application' export * from './validators/token-balance' diff --git a/packages/portfolio/src/token-manager.ts b/packages/portfolio/src/token-manager.ts index 9a1981f631..4474304539 100644 --- a/packages/portfolio/src/token-manager.ts +++ b/packages/portfolio/src/token-manager.ts @@ -89,7 +89,7 @@ export const portfolioTokenManagerMaker = ( subscribe: observer.subscribe, unsubscribe: observer.unsubscribe, - observable: observer.observable, + observable$: observer.observable, destroy, }, diff --git a/packages/portfolio/src/transformers/deserializers.ts b/packages/portfolio/src/transformers/deserializers.ts index 8c4f9078b4..7a74b2ca38 100644 --- a/packages/portfolio/src/transformers/deserializers.ts +++ b/packages/portfolio/src/transformers/deserializers.ts @@ -7,7 +7,7 @@ import {freeze} from 'immer' import {parseTokenBalance} from '../validators/token-balance' import {parseTokenDiscoveryWithCacheRecord} from '../validators/token-discovery' -import {parsePrimaryBalanceBreakdown} from '../validators/primary-balance-breakdown' +import {parsePrimaryBreakdown} from '../validators/primary-breakdown' export const tokenDiscoveryReviverMapping: StorageReviverMapping = { supply: StorageReviverType.AsBigInt, @@ -15,14 +15,12 @@ export const tokenDiscoveryReviverMapping: StorageReviverMapping = { export const tokenBalanceReviverMapping: StorageReviverMapping = { balance: StorageReviverType.AsBigInt, - lockedInBuiltTxs: StorageReviverType.AsBigInt, } export const primaryBalanceBreakdownReviverMapping: StorageReviverMapping = { - balance: StorageReviverType.AsBigInt, - lockedInBuiltTxs: StorageReviverType.AsBigInt, - minRequiredByTokens: StorageReviverType.AsBigInt, - quantity: StorageReviverType.AsBigInt, + availableRewards: StorageReviverType.AsBigInt, + totalFromTxs: StorageReviverType.AsBigInt, + lockedAsStorageCost: StorageReviverType.AsBigInt, } const tokenBalanceDeserializer = (jsonString: string | null) => { @@ -48,7 +46,7 @@ const primaryBalanceBreakdownDeserializer = (jsonString: string | null) => { const record = storageDeserializerMaker( primaryBalanceBreakdownReviverMapping, )(jsonString) - const parsed = parsePrimaryBalanceBreakdown(record) + const parsed = parsePrimaryBreakdown(record) return parsed ?? null } diff --git a/packages/portfolio/src/validators/primary-balance-breakdown.test.ts b/packages/portfolio/src/validators/primary-balance-breakdown.test.ts deleted file mode 100644 index 095bc002bf..0000000000 --- a/packages/portfolio/src/validators/primary-balance-breakdown.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - isPrimaryBalanceBreakdown, - parsePrimaryBalanceBreakdown, -} from './primary-balance-breakdown' -import {tokenBalanceMocks} from '../adapters/token-balance.mocks' -import {Portfolio} from '@yoroi/types' - -describe('isPrimaryBalanceBreakdown', () => { - it('should return true for a valid primary balance breakdown', () => { - const validPrimaryBalanceBreakdown: Portfolio.BalancePrimaryBreakdown = - tokenBalanceMocks.primaryETHBreakdown - - const result = isPrimaryBalanceBreakdown(validPrimaryBalanceBreakdown) - - expect(result).toBe(true) - }) - - it('should return false for an invalid primary balance breakdown', () => { - const invalidPrimaryBalanceBreakdown = { - ...tokenBalanceMocks.primaryETHBreakdown, - minRequiredByTokens: 'invalid', - } - - const result = isPrimaryBalanceBreakdown(invalidPrimaryBalanceBreakdown) - - expect(result).toBe(false) - }) -}) - -describe('parsePrimaryBalanceBreakdown', () => { - it('should return a valid primary balance breakdown', () => { - const validPrimaryBalanceBreakdown: Portfolio.BalancePrimaryBreakdown = - tokenBalanceMocks.primaryETHBreakdown - - const result = parsePrimaryBalanceBreakdown(validPrimaryBalanceBreakdown) - - expect(result).toEqual(validPrimaryBalanceBreakdown) - }) - - it('should return undefined for an invalid primary balance breakdown', () => { - const invalidPrimaryBalanceBreakdown = { - ...tokenBalanceMocks.primaryETHBreakdown, - minRequiredByTokens: 'invalid', - } - - const result = parsePrimaryBalanceBreakdown(invalidPrimaryBalanceBreakdown) - - expect(result).toBeUndefined() - }) -}) diff --git a/packages/portfolio/src/validators/primary-balance-breakdown.ts b/packages/portfolio/src/validators/primary-balance-breakdown.ts deleted file mode 100644 index f86ddb3aee..0000000000 --- a/packages/portfolio/src/validators/primary-balance-breakdown.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {z} from 'zod' -import {Portfolio} from '@yoroi/types' - -import {TokenBalanceSchema} from './token-balance' - -export const PrimaryBalanceBreakdownSchema = TokenBalanceSchema.extend({ - minRequiredByTokens: z.bigint(), - records: z.array( - z.object({ - quantity: z.bigint(), - redeemableAfter: z.number(), - source: z.string(), - }), - ), -}) - -export const isPrimaryBalanceBreakdown = ( - data: unknown, -): data is Portfolio.BalancePrimaryBreakdown => - PrimaryBalanceBreakdownSchema.safeParse(data).success - -export const parsePrimaryBalanceBreakdown = ( - data: unknown, -): Portfolio.BalancePrimaryBreakdown | undefined => { - return isPrimaryBalanceBreakdown(data) ? data : undefined -} diff --git a/packages/portfolio/src/validators/primary-breakdown.test.ts b/packages/portfolio/src/validators/primary-breakdown.test.ts new file mode 100644 index 0000000000..928f9f1d1f --- /dev/null +++ b/packages/portfolio/src/validators/primary-breakdown.test.ts @@ -0,0 +1,45 @@ +import {isPrimaryBreakdown, parsePrimaryBreakdown} from './primary-breakdown' +import {tokenBalanceMocks} from '../adapters/token-balance.mocks' +import {Portfolio} from '@yoroi/types' + +describe('isPrimaryBreakdown', () => { + it('should return true for a valid primary balance breakdown', () => { + const validPrimaryBalanceBreakdown: Portfolio.PrimaryBreakdown = + tokenBalanceMocks.primaryETHBreakdown + + const result = isPrimaryBreakdown(validPrimaryBalanceBreakdown) + + expect(result).toBe(true) + }) + + it('should return false for an invalid primary balance breakdown', () => { + const invalidPrimaryBalanceBreakdown = { + x: 'invalid', + } + + const result = isPrimaryBreakdown(invalidPrimaryBalanceBreakdown) + + expect(result).toBe(false) + }) +}) + +describe('parsePrimaryBreakdown', () => { + it('should return a valid primary balance breakdown', () => { + const validPrimaryBalanceBreakdown: Portfolio.PrimaryBreakdown = + tokenBalanceMocks.primaryETHBreakdown + + const result = parsePrimaryBreakdown(validPrimaryBalanceBreakdown) + + expect(result).toEqual(validPrimaryBalanceBreakdown) + }) + + it('should return undefined for an invalid primary balance breakdown', () => { + const invalidPrimaryBalanceBreakdown = { + x: 'invalid', + } + + const result = parsePrimaryBreakdown(invalidPrimaryBalanceBreakdown) + + expect(result).toBeUndefined() + }) +}) diff --git a/packages/portfolio/src/validators/primary-breakdown.ts b/packages/portfolio/src/validators/primary-breakdown.ts new file mode 100644 index 0000000000..9a42d29710 --- /dev/null +++ b/packages/portfolio/src/validators/primary-breakdown.ts @@ -0,0 +1,19 @@ +import {z} from 'zod' +import {Portfolio} from '@yoroi/types' + +export const PrimaryBreakdownSchema = z.object({ + availableRewards: z.bigint(), + totalFromTxs: z.bigint(), + lockedAsStorageCost: z.bigint(), +}) + +export const isPrimaryBreakdown = ( + data: unknown, +): data is Portfolio.PrimaryBreakdown => + PrimaryBreakdownSchema.safeParse(data).success + +export const parsePrimaryBreakdown = ( + data: unknown, +): Portfolio.PrimaryBreakdown | undefined => { + return isPrimaryBreakdown(data) ? data : undefined +} diff --git a/packages/portfolio/src/validators/token-balance.test.ts b/packages/portfolio/src/validators/token-balance.test.ts index 5fe6c80443..8d2110ae52 100644 --- a/packages/portfolio/src/validators/token-balance.test.ts +++ b/packages/portfolio/src/validators/token-balance.test.ts @@ -9,10 +9,9 @@ import {tokenInfoMocks} from '../adapters/token-info.mocks' describe('TokenBalanceSchema', () => { it('should validate a valid token balance', () => { - const validTokenBalance = { + const validTokenBalance: Portfolio.Token.Balance = { info: tokenInfoMocks.ftNoTicker, balance: BigInt(10000000000), - lockedInBuiltTxs: BigInt(0), } const result = TokenBalanceSchema.safeParse(validTokenBalance) @@ -24,7 +23,6 @@ describe('TokenBalanceSchema', () => { const invalidTokenBalance = { info: tokenInfoMocks.ftNoTicker, balance: '10000000000', - lockedInBuiltTxs: BigInt(0), } const result = TokenBalanceSchema.safeParse(invalidTokenBalance) @@ -38,7 +36,6 @@ describe('isTokenBalance', () => { const validTokenBalance: Portfolio.Token.Balance = { info: tokenInfoMocks.ftNoTicker, balance: BigInt(10000000000), - lockedInBuiltTxs: BigInt(0), } const result = isTokenBalance(validTokenBalance) @@ -50,7 +47,6 @@ describe('isTokenBalance', () => { const invalidTokenBalance = { info: tokenInfoMocks.ftNoTicker, balance: '10000000000', - lockedInBuiltTxs: BigInt(0), } const result = isTokenBalance(invalidTokenBalance) @@ -64,7 +60,6 @@ describe('parseTokenBalance', () => { const validTokenBalance: Portfolio.Token.Balance = { info: tokenInfoMocks.ftNoTicker, balance: BigInt(10000000000), - lockedInBuiltTxs: BigInt(0), } const result = parseTokenBalance(validTokenBalance) @@ -76,7 +71,6 @@ describe('parseTokenBalance', () => { const invalidTokenBalance = { info: tokenInfoMocks.ftNoTicker, balance: '10000000000', - lockedInBuiltTxs: BigInt(0), } const result = parseTokenBalance(invalidTokenBalance) diff --git a/packages/portfolio/src/validators/token-balance.ts b/packages/portfolio/src/validators/token-balance.ts index 36036ece92..708f91b5a3 100644 --- a/packages/portfolio/src/validators/token-balance.ts +++ b/packages/portfolio/src/validators/token-balance.ts @@ -6,7 +6,6 @@ import {TokenInfoSchema} from './token-info' export const TokenBalanceSchema = z.object({ info: TokenInfoSchema, balance: z.bigint(), - lockedInBuiltTxs: z.bigint(), }) export const isTokenBalance = ( diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9ef5c8f3be..d7bdc7301f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -160,8 +160,7 @@ import {PortfolioTokenInfo} from './portfolio/info' import { PortfolioAmount, PortfolioAmounts, - PortfolioBalancePrimaryBreakdown, - PortfolioBalancePrimaryRecord, + PortfolioPrimaryBreakdown, PortfolioTokenBalance, } from './portfolio/balance' import {PortfolioTokenPrice} from './portfolio/price' @@ -424,8 +423,7 @@ export namespace Transfer { export namespace Portfolio { export type Amount = PortfolioAmount export type Amounts = PortfolioAmounts - export type BalancePrimaryRecord = PortfolioBalancePrimaryRecord - export type BalancePrimaryBreakdown = PortfolioBalancePrimaryBreakdown + export type PrimaryBreakdown = PortfolioPrimaryBreakdown export namespace Event { export type SourceId = PortfolioEventSourceId diff --git a/packages/types/src/portfolio/balance.ts b/packages/types/src/portfolio/balance.ts index c807d6e1a7..df730a61d1 100644 --- a/packages/types/src/portfolio/balance.ts +++ b/packages/types/src/portfolio/balance.ts @@ -2,31 +2,27 @@ import {PortfolioTokenInfo} from './info' import {PortfolioTokenId} from './token' export type PortfolioAmounts = { - [key: PortfolioTokenId]: BigInt + [key: PortfolioTokenId]: bigint } export type PortfolioAmount = { id: PortfolioTokenId - quantity: BigInt + quantity: bigint } export type PortfolioTokenBalance = { info: PortfolioTokenInfo - // remote - balance: BigInt - // local - dynamic per transactions - lockedInBuiltTxs: BigInt // built txs - not submitted/confirmed + balance: bigint } -export type PortfolioBalancePrimaryRecord = { - source: 'rewards' | 'deposit' - quantity: BigInt - redeemableAfter: number // 0 means anytime with withdrawal, epoch or infinity means anytime with reverse tx -} +export type PortfolioPrimaryBreakdown = { + // TX + // stated + totalFromTxs: bigint -export type PortfolioBalancePrimaryBreakdown = PortfolioTokenBalance & { - // local - dynamic per epoch - minRequiredByTokens: BigInt // required on utxos to keep tokens (storage cost) - // remote - dynamic per epoch - records: ReadonlyArray + // EPOCH/TX + // derived + availableRewards: bigint + // inferred + lockedAsStorageCost: bigint // required on utxos to keep tokens (storage cost) } diff --git a/packages/types/src/portfolio/discovery.ts b/packages/types/src/portfolio/discovery.ts index 91dc1e02de..8beaf52888 100644 --- a/packages/types/src/portfolio/discovery.ts +++ b/packages/types/src/portfolio/discovery.ts @@ -22,7 +22,7 @@ export type PortfolioTokenDiscovery = { } counters: { - supply: BigInt + supply: bigint items: number totalItems: number } diff --git a/packages/types/src/portfolio/event.ts b/packages/types/src/portfolio/event.ts index 5a4bdabb2f..c614694fe5 100644 --- a/packages/types/src/portfolio/event.ts +++ b/packages/types/src/portfolio/event.ts @@ -33,6 +33,7 @@ export type PortfolioEventBalanceManager = export type PortfolioEventBalanceManagerSync = PortfolioEventSourceId & Readonly<{ on: PortfolioEventManagerOn.Sync + mode: 'all' | 'primary-derived' | 'primary-stated' }> export type PortfolioEventBalanceManagerHydrate = PortfolioEventSourceId & diff --git a/packages/types/src/portfolio/manager.ts b/packages/types/src/portfolio/manager.ts index 1a3a8f1924..10af4bb1c8 100644 --- a/packages/types/src/portfolio/manager.ts +++ b/packages/types/src/portfolio/manager.ts @@ -2,10 +2,7 @@ import {Observable, Subscription} from 'rxjs' import {AppCacheRecord} from '../app/cache' import {AppObserverSubscribe} from '../app/observer-manager' -import { - PortfolioBalancePrimaryBreakdown, - PortfolioTokenBalance, -} from './balance' +import {PortfolioPrimaryBreakdown, PortfolioTokenBalance} from './balance' import { PortfolioEventBalanceManager, PortfolioEventSourceId, @@ -15,43 +12,54 @@ import {PortfolioTokenInfo} from './info' import {PortfolioTokenId} from './token' export type PortfolioManagerToken = Readonly<{ - hydrate: (params: PortfolioEventSourceId) => void - sync: ( + hydrate(params: PortfolioEventSourceId): void + sync( params: Readonly<{ secondaryTokenIds: ReadonlyArray }> & PortfolioEventSourceId, - ) => Promise | null>> + ): Promise | null>> subscribe: AppObserverSubscribe - unsubscribe: (sub: Subscription) => void - observable: Observable + unsubscribe(subscription: Subscription): void + observable$: Observable - destroy: () => void + destroy(): void }> export type PortfolioManagerBalance = Readonly<{ - hydrate: () => void - refresh: () => void - sync: ( + hydrate(): void + refresh(): void + + updatePrimaryStated( + params: Readonly< + Pick + >, + ): void + updatePrimaryDerived( + params: Readonly>, + ): void + syncBalances( params: Readonly<{ - primaryBalance: Readonly> + primaryStated: Readonly< + Pick + > secondaryBalances: Readonly< - | Map> - | Map + Map> > }>, - ) => void + ): void subscribe: AppObserverSubscribe - unsubscribe: (sub: Subscription) => void - observable: Observable + unsubscribe(subscription: Subscription): void + observable$: Observable - getPrimaryBreakdown: () => Readonly - getBalances: () => Readonly<{ + getPrimaryBreakdown(): Readonly + getPrimaryBalance(): Readonly + getBalances(): Readonly<{ all: ReadonlyArray fts: ReadonlyArray nfts: ReadonlyArray }> - destroy: () => void + destroy(): void }> diff --git a/packages/types/src/portfolio/storage.ts b/packages/types/src/portfolio/storage.ts index 1142f20d33..fb365c9008 100644 --- a/packages/types/src/portfolio/storage.ts +++ b/packages/types/src/portfolio/storage.ts @@ -1,16 +1,13 @@ import {AppCacheRecord} from '../app/cache' -import { - PortfolioBalancePrimaryBreakdown, - PortfolioTokenBalance, -} from './balance' +import {PortfolioPrimaryBreakdown, PortfolioTokenBalance} from './balance' import {PortfolioTokenDiscovery} from './discovery' import {PortfolioTokenInfo} from './info' import {PortfolioTokenId} from './token' export type PortfolioStorageBalance = Readonly<{ - primaryBalanceBreakdown: { - save: (entry: PortfolioBalancePrimaryBreakdown) => void - read: (key: PortfolioTokenId) => PortfolioBalancePrimaryBreakdown | null + primaryBreakdown: { + save: (breakdown: Readonly) => void + read: () => Readonly | null clear: () => void } balances: { From 216535f98d426b7dd6beadfe0732205193bf2002 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:44:02 +0100 Subject: [PATCH 03/75] refactor(portfolio): disabling auto hydrate to avoid confunsion --- packages/portfolio/src/balance-manager.test.ts | 7 ++++--- packages/portfolio/src/balance-manager.ts | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/portfolio/src/balance-manager.test.ts b/packages/portfolio/src/balance-manager.test.ts index 902161bc01..8c262056e3 100644 --- a/packages/portfolio/src/balance-manager.test.ts +++ b/packages/portfolio/src/balance-manager.test.ts @@ -399,7 +399,7 @@ describe('sync & refresh', () => { lockedAsStorageCost: BigInt(0), } - it('should sync (+hydrate)', async () => { + it('should sync and respond to token event', async () => { tokenManager.sync.mockResolvedValue( new Map(tokenInfoMocks.storage.entries1), ) @@ -433,6 +433,7 @@ describe('sync & refresh', () => { const subscriber = jest.fn() manager.subscribe(subscriber) + manager.hydrate() const secondaryBalances = new Map( tokenBalanceMocks.storage.entries1.slice(0, -1), @@ -455,7 +456,7 @@ describe('sync & refresh', () => { expect(manager.getPrimaryBreakdown()).toEqual(expect.anything()) expect(manager.getBalances()).toEqual(expect.anything()) - expect(mockedNotify).toHaveBeenCalledTimes(2) // Hydrate + Sync + expect(mockedNotify).toHaveBeenCalledTimes(2) tokenManager.sync.mockResolvedValue( new Map(tokenInfoMocks.storage.entries1.slice(0, 1)), @@ -533,7 +534,7 @@ describe('sync & refresh', () => { expect(manager.getPrimaryBreakdown()).toEqual(expect.anything()) expect(manager.getBalances()).toEqual(expect.anything()) - expect(mockedNotify).toHaveBeenCalledTimes(3) // Hydrate + Sync + expect(mockedNotify).toHaveBeenCalledTimes(2) }) it('should sync', async () => { diff --git a/packages/portfolio/src/balance-manager.ts b/packages/portfolio/src/balance-manager.ts index c1f5529300..277ed08610 100644 --- a/packages/portfolio/src/balance-manager.ts +++ b/packages/portfolio/src/balance-manager.ts @@ -197,8 +197,6 @@ export const portfolioBalanceManagerMaker = ( Map> > }) => { - if (!isHydrated) hydrate() - queue.enqueue( () => new Promise((resolve, reject) => { From 3efe8d725d67faca15b819cd08ca05e28c510f3f Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:53:50 +0100 Subject: [PATCH 04/75] reafactor(wallet-mobile): calc locked - to resolve without api --- .../cardano/assetUtils/assetUtils.ts | 18 ++++++++++++++---- .../src/yoroi-wallets/hooks/index.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/assetUtils/assetUtils.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/assetUtils/assetUtils.ts index 6a36cce437..a7b423b6c5 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/assetUtils/assetUtils.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/assetUtils/assetUtils.ts @@ -1,15 +1,25 @@ import BigNumber from 'bignumber.js' -import {Address} from '../../types' import {RawUtxo} from '../../types/other' import {CardanoMobile} from '../../wallets' import {COINS_PER_UTXO_BYTE} from '../constants/common' import {cardanoValueFromRemoteFormat, normalizeToAddress} from '../utils' -export async function calcLockedDeposit(utxos: RawUtxo[], address: Address, coinsPerUtxoByteStr: string) { - const utxosWithAssets = utxos.filter((u) => u.assets.length > 0) +const addressPlaceholder = + 'addr1qx8nuj8a7gy8kes4pedpfdscrlxr6p8gkzyzmhdmsf4209xssydveuc8xyx4zh27fwcmr62mraeezjwf24hzkyejwfmqmpfpy5' - const coinsPerUtxoByte = await CardanoMobile.BigNum.fromStr(coinsPerUtxoByteStr ?? COINS_PER_UTXO_BYTE) +export async function calcLockedDeposit({ + rawUtxos, + address = addressPlaceholder, + coinsPerUtxoByteStr = COINS_PER_UTXO_BYTE, +}: { + rawUtxos: RawUtxo[] + address?: string + coinsPerUtxoByteStr?: string +}) { + const utxosWithAssets = rawUtxos.filter((u) => u.assets.length > 0) + + const coinsPerUtxoByte = await CardanoMobile.BigNum.fromStr(coinsPerUtxoByteStr) const dataCost = await CardanoMobile.DataCost.newCoinsPerByte(coinsPerUtxoByte) const normalizedAddress = await normalizeToAddress(address) if (normalizedAddress === undefined) throw new Error('calcLockedDeposit::Error not a valid address') diff --git a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts index d7d2a85369..187f541342 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts @@ -175,16 +175,18 @@ export const useLockedAmount = ( options?: UseQueryOptions, ) => { const {protocolParams} = useProtocolParams(wallet, {suspense: true}) - const coinsPerUtxoByte = protocolParams?.coinsPerUtxoByte ?? '' + const coinsPerUtxoByte = protocolParams?.coinsPerUtxoByte const query = useQuery({ ...options, suspense: true, - queryKey: [wallet.id, coinsPerUtxoByte, 'lockedAmount'], + queryKey: [wallet.id, coinsPerUtxoByte ?? '', 'lockedAmount'], queryFn: () => - calcLockedDeposit(wallet.utxos, wallet.receiveAddresses[0], coinsPerUtxoByte).then( - (amount) => amount.toString() as Balance.Quantity, - ), + calcLockedDeposit({ + rawUtxos: wallet.utxos, + address: wallet.receiveAddresses[0], + coinsPerUtxoByteStr: coinsPerUtxoByte, + }).then((amount) => amount.toString() as Balance.Quantity), }) React.useEffect(() => { From 3bc4d1283febff6c11c1ed56659cb467531ef673 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:09:07 +0100 Subject: [PATCH 05/75] chore(wallet-mobile): wip wiring up portoflio --- apps/wallet-mobile/package.json | 2 +- .../src/TxHistory/BalanceBanner.tsx | 6 +- .../src/TxHistory/LockedDeposit.tsx | 24 +-- .../TxHistory/TxHistoryList/TxHistoryList.tsx | 2 +- .../common/helpers/filterBySyncEvent.test.ts | 31 ++++ .../common/helpers/filterBySyncEvent.ts | 5 + .../Portfolio/common/hooks/useBalances.ts | 16 ++ .../{ => hooks}/usePortfolioBalanceManager.ts | 3 +- .../{ => hooks}/usePortfolioTokenManager.ts | 0 .../common/hooks/usePrimaryBalance.ts | 16 ++ .../common/hooks/usePrimaryBreakdown.ts | 16 ++ .../features/Portfolio/common/transformers.ts | 49 ----- .../toBalanceManagerSyncArgs.test.ts | 41 +++++ .../transformers/toBalanceManagerSyncArgs.ts | 26 +++ .../toChainSupportedNetwork.test.ts | 21 +++ .../transformers/toChainSupportedNetwork.ts | 15 ++ .../Portfolio/useCases/PortfolioScreen.tsx | 167 +++++------------- .../Send/common/useTokenQuantities.tsx | 6 +- .../ManageCollateralScreen.tsx | 6 +- .../SelectWalletFromListScreen.tsx | 43 ++--- .../wallet-manager/WalletManagerContext.tsx | 4 + .../src/wallet-manager/walletManager.test.ts | 3 + .../src/wallet-manager/walletManager.ts | 31 ++-- .../cardano/assetUtils/assetUtils.test.ts | 8 +- .../cardano/byron/ByronWallet.ts | 74 ++++++-- .../cardano/shelley/ShelleyWallet.ts | 50 ++++-- .../src/yoroi-wallets/cardano/types.ts | 14 +- .../src/yoroi-wallets/hooks/index.ts | 51 +----- .../src/yoroi-wallets/mocks/wallet.ts | 32 +++- packages/common/package.json | 1 + packages/common/src/index.ts | 3 +- .../common/src/numbers/split-bigint.test.ts | 33 ++++ packages/common/src/numbers/split-bigint.ts | 17 ++ .../reactjs/hooks/useObservableValue.test.ts | 28 +++ .../reactjs/hooks/useObservableValue.ts | 23 +++ .../reactjs/hooks/useObserver.test.ts | 34 ---- .../translators/reactjs/hooks/useObserver.ts | 30 ---- 37 files changed, 545 insertions(+), 386 deletions(-) create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.test.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/hooks/useBalances.ts rename apps/wallet-mobile/src/features/Portfolio/common/{ => hooks}/usePortfolioBalanceManager.ts (96%) rename apps/wallet-mobile/src/features/Portfolio/common/{ => hooks}/usePortfolioTokenManager.ts (100%) create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBalance.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBreakdown.ts delete mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.test.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.test.ts create mode 100644 apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.ts create mode 100644 packages/common/src/numbers/split-bigint.test.ts create mode 100644 packages/common/src/numbers/split-bigint.ts create mode 100644 packages/common/src/translators/reactjs/hooks/useObservableValue.test.ts create mode 100644 packages/common/src/translators/reactjs/hooks/useObservableValue.ts delete mode 100644 packages/common/src/translators/reactjs/hooks/useObserver.test.ts delete mode 100644 packages/common/src/translators/reactjs/hooks/useObserver.ts diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index d77cdf0867..c584da3c73 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -39,7 +39,7 @@ "prestart": "sb-rn-get-stories", "start": "yarn react-native start --reset-cache --verbose", "storybook:watch": "sb-rn-watcher", - "test": "jest src --ci --bail --silent", + "test": "jest src --ci --bail --silent --maxWorkers=1", "tsc": "tsc --noEmit -p tsconfig.json", "tsc:cover": "yarn typescript-coverage-report -i '**/*.test.*' -i '**/*.stories*' -t html --threshold=100" }, diff --git a/apps/wallet-mobile/src/TxHistory/BalanceBanner.tsx b/apps/wallet-mobile/src/TxHistory/BalanceBanner.tsx index b18d80255f..540dbdd5d8 100644 --- a/apps/wallet-mobile/src/TxHistory/BalanceBanner.tsx +++ b/apps/wallet-mobile/src/TxHistory/BalanceBanner.tsx @@ -5,17 +5,16 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Boundary, ResetErrorRef, Spacer} from '../components' import {Icon} from '../components/Icon' import {PairedBalance} from '../components/PairedBalance/PairedBalance' +import { usePrimaryBalance } from '../features/Portfolio/common/hooks/usePrimaryBalance' import {usePrivacyMode} from '../features/Settings/PrivacyMode/PrivacyMode' import {useSelectedWallet} from '../features/WalletManager/Context' import {formatTokenWithText, formatTokenWithTextWhenHidden} from '../legacy/format' -import {useBalances} from '../yoroi-wallets/hooks' import {Amounts} from '../yoroi-wallets/utils' export const BalanceBanner = React.forwardRef((_, ref) => { const wallet = useSelectedWallet() const styles = useStyles() - const balances = useBalances(wallet) - const primaryAmount = Amounts.getAmount(balances, wallet.primaryTokenInfo.id) + const primary = usePrimaryBalance({wallet}) const {isPrivacyOff, togglePrivacyMode} = usePrivacyMode() return ( @@ -47,7 +46,6 @@ const hiddenBalance = '*.******' const Balance = ({isPrivacyOff}: {isPrivacyOff: boolean}) => { const styles = useStyles() const wallet = useSelectedWallet() - const balances = useBalances(wallet) const balance = isPrivacyOff ? formatTokenWithTextWhenHidden(hiddenBalance, wallet.primaryToken) diff --git a/apps/wallet-mobile/src/TxHistory/LockedDeposit.tsx b/apps/wallet-mobile/src/TxHistory/LockedDeposit.tsx index f229ebd90b..efdf7d934c 100644 --- a/apps/wallet-mobile/src/TxHistory/LockedDeposit.tsx +++ b/apps/wallet-mobile/src/TxHistory/LockedDeposit.tsx @@ -3,37 +3,23 @@ import React from 'react' import {useIntl} from 'react-intl' import {StyleSheet, View} from 'react-native' -import {Boundary, Spacer, Text} from '../components' +import {Spacer, Text} from '../components' +import {usePrimaryBreakdown} from '../features/Portfolio/common/hooks/usePrimaryBreakdown' import {usePrivacyMode} from '../features/Settings/PrivacyMode/PrivacyMode' import {useSelectedWallet} from '../features/WalletManager/Context' import globalMessages from '../i18n/global-messages' import {formatTokenWithText, formatTokenWithTextWhenHidden} from '../legacy/format' -import {useLockedAmount} from '../yoroi-wallets/hooks' +import {asQuantity} from '../yoroi-wallets/utils' export const LockedDeposit = () => { const {isPrivacyOff} = usePrivacyMode() const wallet = useSelectedWallet() - const loadingAmount = formatTokenWithTextWhenHidden('...', wallet.primaryToken) const hiddenAmount = formatTokenWithTextWhenHidden('*.******', wallet.primaryToken) + const {lockedAsStorageCost} = usePrimaryBreakdown({wallet}) + const amount = formatTokenWithText(asQuantity(lockedAsStorageCost.toString()), wallet.primaryToken) if (isPrivacyOff) return - return ( - , - }} - error={{size: 'inline'}} - > - - - ) -} - -const LockedAmount = () => { - const wallet = useSelectedWallet() - const lockedAmount = useLockedAmount({wallet}) - const amount = formatTokenWithText(lockedAmount, wallet.primaryToken) return } diff --git a/apps/wallet-mobile/src/TxHistory/TxHistoryList/TxHistoryList.tsx b/apps/wallet-mobile/src/TxHistory/TxHistoryList/TxHistoryList.tsx index 7ba218103e..d4f2be42ac 100644 --- a/apps/wallet-mobile/src/TxHistory/TxHistoryList/TxHistoryList.tsx +++ b/apps/wallet-mobile/src/TxHistory/TxHistoryList/TxHistoryList.tsx @@ -33,7 +33,7 @@ export const TxHistoryList = (props: Props) => { ) const transactionsInfo = useTransactionInfos(wallet) - const groupedTransactions = getTransactionsByDate(transactionsInfo) + const groupedTransactions = React.useMemo(() => getTransactionsByDate(transactionsInfo), [transactionsInfo]) return ( diff --git a/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.test.ts b/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.test.ts new file mode 100644 index 0000000000..41df231d51 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.test.ts @@ -0,0 +1,31 @@ +import {Portfolio} from '@yoroi/types' + +import {filterBySyncEvent} from './filterBySyncEvent' + +describe('filterBySyncEvent', () => { + it('should return true if the event is a sync event', () => { + const event: Portfolio.Event.BalanceManager = { + on: Portfolio.Event.ManagerOn.Sync, + mode: 'all', + sourceId: '1', + } + expect(filterBySyncEvent(event)).toBe(true) + }) + + it('should return false if the event is not a sync event', () => { + const event: Portfolio.Event.BalanceManager = { + on: Portfolio.Event.ManagerOn.Hydrate, + sourceId: '1', + } + expect(filterBySyncEvent(event)).toBe(false) + }) + + it('should work with TokenManager events as well', () => { + const event: Portfolio.Event.TokenManager = { + on: Portfolio.Event.ManagerOn.Sync, + ids: [], + sourceId: '1', + } + expect(filterBySyncEvent(event)).toBe(true) + }) +}) diff --git a/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.ts b/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.ts new file mode 100644 index 0000000000..0bfb88b3c5 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/helpers/filterBySyncEvent.ts @@ -0,0 +1,5 @@ +import {Portfolio} from '@yoroi/types' + +export function filterBySyncEvent({on}: Portfolio.Event.BalanceManager | Portfolio.Event.TokenManager) { + return on === Portfolio.Event.ManagerOn.Sync +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/hooks/useBalances.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/useBalances.ts new file mode 100644 index 0000000000..131a5aaee1 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/hooks/useBalances.ts @@ -0,0 +1,16 @@ +import {useObservableValue} from '@yoroi/common' +import * as React from 'react' +import {filter} from 'rxjs' + +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' +import {filterBySyncEvent as isSyncEvent} from '../helpers/filterBySyncEvent' + +export const useBalances = ({wallet}: {wallet: YoroiWallet}) => { + const observable$ = React.useMemo(() => wallet.balance$.pipe(filter(isSyncEvent)), [wallet]) + const executor = React.useCallback(() => wallet.balances, [wallet]) + + return useObservableValue({ + observable$, + executor, + }) +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePortfolioBalanceManager.ts similarity index 96% rename from apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts rename to apps/wallet-mobile/src/features/Portfolio/common/hooks/usePortfolioBalanceManager.ts index 01c2b6e452..4cbac672f4 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts +++ b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePortfolioBalanceManager.ts @@ -3,7 +3,7 @@ import {createPrimaryTokenInfo, portfolioBalanceManagerMaker, portfolioBalanceSt import {Chain, Portfolio} from '@yoroi/types' import * as React from 'react' -import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' export const usePortfolioBalanceManager = ({ tokenManager, @@ -38,6 +38,7 @@ export const buildPortfolioBalanceManager = ({ const balanceStorage = portfolioBalanceStorageMaker({ balanceStorage: observableStorageMaker(balanceStorageMounted), primaryBreakdownStorage: observableStorageMaker(primaryBreakdownStorageMounted), + primaryTokenId: primaryTokenInfo.id, }) const balanceManager = portfolioBalanceManagerMaker({ diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePortfolioTokenManager.ts similarity index 100% rename from apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts rename to apps/wallet-mobile/src/features/Portfolio/common/hooks/usePortfolioTokenManager.ts diff --git a/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBalance.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBalance.ts new file mode 100644 index 0000000000..590cf4abae --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBalance.ts @@ -0,0 +1,16 @@ +import {useObservableValue} from '@yoroi/common' +import * as React from 'react' +import {filter} from 'rxjs' + +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' +import {filterBySyncEvent as isSyncEvent} from '../helpers/filterBySyncEvent' + +export const usePrimaryBalance = ({wallet}: {wallet: YoroiWallet}) => { + const observable$ = React.useMemo(() => wallet.balance$.pipe(filter(isSyncEvent)), [wallet]) + const executor = React.useCallback(() => wallet.primaryBalance, [wallet]) + + return useObservableValue({ + observable$, + executor, + }) +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBreakdown.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBreakdown.ts new file mode 100644 index 0000000000..404d2e933d --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/hooks/usePrimaryBreakdown.ts @@ -0,0 +1,16 @@ +import {useObservableValue} from '@yoroi/common' +import * as React from 'react' +import {filter} from 'rxjs' + +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' +import {filterBySyncEvent as isSyncEvent} from '../helpers/filterBySyncEvent' + +export const usePrimaryBreakdown = ({wallet}: {wallet: YoroiWallet}) => { + const observable$ = React.useMemo(() => wallet.balance$.pipe(filter(isSyncEvent)), [wallet]) + const executor = React.useCallback(() => wallet.primaryBreakdown, [wallet]) + + return useObservableValue({ + observable$, + executor, + }) +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts deleted file mode 100644 index 39df6515a0..0000000000 --- a/apps/wallet-mobile/src/features/Portfolio/common/transformers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {parseTokenId} from '@yoroi/portfolio' -import {Chain, Portfolio} from '@yoroi/types' -import {freeze} from 'immer' - -import {NetworkId, RawUtxo, RemoteAsset} from '../../../yoroi-wallets/types' - -export function toChainSupportedNetwork(networkId: NetworkId): Chain.SupportedNetworks { - switch (networkId) { - case 0: - case 1: - return Chain.Network.Main - case 450: - return Chain.Network.Sancho - default: - return Chain.Network.Preprod - } -} - -export function toBalanceManagerArgs(rawUtxos: RawUtxo[]) { - let ptBalance = 0n - const secondary = new Map>() - for (const utxo of rawUtxos) { - ptBalance += BigInt(utxo.amount) - for (const record of utxo.assets) { - const tokenId = toTokenId(record.assetId) - if (!tokenId) continue // skip invalid token ids - secondary.set(tokenId, { - balance: secondary.get(tokenId)?.balance ?? 0n + BigInt(record.amount), - }) - } - } - - return freeze( - { - primaryBalance: { - balance: ptBalance, - minRequiredByTokens: 0n, - lockedInBuiltTxs: 0n, - records: [], - }, - secondaryBalances: new Map(secondary), - }, - true, - ) -} - -const toTokenId = (assetId: RemoteAsset['assetId']) => { - return parseTokenId(`${assetId.slice(0, 56)}.${assetId.slice(56)}`) -} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.test.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.test.ts new file mode 100644 index 0000000000..ba4c26039a --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.test.ts @@ -0,0 +1,41 @@ +import {RawUtxo} from '../../../../yoroi-wallets/types' +import {toBalanceManagerSyncArgs} from './toBalanceManagerSyncArgs' + +describe('toBalanceManagerSyncArgs', () => { + const policyId = new Array(56).fill('1').join('') + it('should calculate primaryStated and secondaryBalances correctly', () => { + const rawUtxos: RawUtxo[] = [ + { + amount: '100', + receiver: '', + tx_hash: '', + tx_index: 0, + utxo_id: '', + assets: [ + {assetId: `${policyId}.DEAD`, amount: '50', policyId, name: 'DEAD'}, + {assetId: `${policyId}.DEADFEED`, amount: '30', policyId, name: 'DEADFEED'}, + ], + }, + { + receiver: '', + tx_hash: '', + tx_index: 0, + utxo_id: '', + amount: '200', + assets: [ + {assetId: `${policyId}.DEAD`, amount: '70', policyId, name: 'DEAD'}, + {assetId: `${policyId}.3031`, amount: '80', policyId, name: '3031'}, + ], + }, + ] + const lockedAsStorageCost = 10n + + const result = toBalanceManagerSyncArgs(rawUtxos, lockedAsStorageCost) + + expect(result.primaryStated.totalFromTxs).toBe(300n) + expect(result.primaryStated.lockedAsStorageCost).toBe(10n) + expect(result.secondaryBalances.get(`${policyId}.DEAD`)?.balance).toBe(120n) + expect(result.secondaryBalances.get(`${policyId}.DEADFEED`)?.balance).toBe(30n) + expect(result.secondaryBalances.get(`${policyId}.3031`)?.balance).toBe(80n) + }) +}) diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.ts new file mode 100644 index 0000000000..3407b3a95c --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toBalanceManagerSyncArgs.ts @@ -0,0 +1,26 @@ +import {Portfolio} from '@yoroi/types' + +import {RawUtxo} from '../../../../yoroi-wallets/types' + +export function toBalanceManagerSyncArgs(rawUtxos: RawUtxo[], lockedAsStorageCost: bigint) { + let primaryTokenBalance = 0n + const secondaries = new Map>() + for (const utxo of rawUtxos) { + primaryTokenBalance += BigInt(utxo.amount) + for (const record of utxo.assets) { + const tokenId: Portfolio.Token.Id = `${record.policyId}.${record.name}` + const balance = (secondaries.get(tokenId)?.balance ?? 0n) + BigInt(record.amount) + secondaries.set(tokenId, { + balance, + }) + } + } + + return { + primaryStated: { + totalFromTxs: primaryTokenBalance, + lockedAsStorageCost, + }, + secondaryBalances: new Map(secondaries), + } +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.test.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.test.ts new file mode 100644 index 0000000000..88d4842f31 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.test.ts @@ -0,0 +1,21 @@ +import {Chain} from '@yoroi/types' + +import {toChainSupportedNetwork} from './toChainSupportedNetwork' + +describe('toChainSupportedNetwork', () => { + it('should return Chain.Network.Main for networkId 0', () => { + expect(toChainSupportedNetwork(0)).toBe(Chain.Network.Main) + }) + + it('should return Chain.Network.Main for networkId 1', () => { + expect(toChainSupportedNetwork(1)).toBe(Chain.Network.Main) + }) + + it('should return Chain.Network.Sancho for networkId 450', () => { + expect(toChainSupportedNetwork(450)).toBe(Chain.Network.Sancho) + }) + + it('should return Chain.Network.Preprod for unknown networkId', () => { + expect(toChainSupportedNetwork(999 as never)).toBe(Chain.Network.Preprod) + }) +}) diff --git a/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.ts b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.ts new file mode 100644 index 0000000000..20350ef347 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/transformers/toChainSupportedNetwork.ts @@ -0,0 +1,15 @@ +import {Chain} from '@yoroi/types' + +import {NetworkId} from '../../../../yoroi-wallets/types' + +export function toChainSupportedNetwork(networkId: NetworkId): Chain.SupportedNetworks { + switch (networkId) { + case 0: + case 1: + return Chain.Network.Main + case 450: + return Chain.Network.Sancho + default: + return Chain.Network.Preprod + } +} diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx index db0037880d..9cd33d42c8 100644 --- a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx @@ -1,108 +1,22 @@ 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 {FlatList, Text, View} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' import {Button, Spacer} from '../../../components' -import {WalletMeta} from '../../../wallet-manager/types' import {useWalletManager} from '../../../wallet-manager/WalletManagerContext' import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' -import {useSelectedWallet} from '../../WalletManager/Context' -import {usePortfolioBalanceManager} from '../common/usePortfolioBalanceManager' -import {usePortfolioTokenManager} from '../common/usePortfolioTokenManager' +import {NetworkId} from '../../../yoroi-wallets/types' +import {toChainSupportedNetwork} from '../common/transformers/toChainSupportedNetwork' -type OpenWallets = Record>> export const PortfolioScreen = () => { const navigation = useNavigation() const manager = useWalletManager() - const wallet = useSelectedWallet() - const [openedWalletsByNetwork, setOpenedWalletsByNetwork] = React.useState({}) - - React.useEffect(() => { - manager.listWallets().then(async (walletMetas) => { - const walletsPerNetwork: OpenWallets = {} - - for (const meta of walletMetas) { - const wallet = await manager.openWallet(meta) - if (walletsPerNetwork[wallet.networkId] == null) walletsPerNetwork[wallet.networkId] = [] - walletsPerNetwork[wallet.networkId].push({ - id: wallet.id, - name: meta.name, - utxos: wallet.utxos, - }) - } - - setOpenedWalletsByNetwork(walletsPerNetwork) - - for (const networkId in walletsPerNetwork) { - const wallets = walletsPerNetwork[networkId] - for (const wallet of wallets) { - console.log(wallet.name) - if (wallet.utxos == null) continue - for (const utxo of wallet.utxos) { - for (const record of utxo.assets) { - console.log(record.assetId) - } - } - } - } - }) - }, [manager, openedWalletsByNetwork]) - - const {tokenManager, tokenStorage} = usePortfolioTokenManager({network: Chain.Network.Main}) - const {balanceManager: bmW1, balanceStorage: bs1} = usePortfolioBalanceManager({ - tokenManager, - walletId: wallet.id, - network: Chain.Network.Main, - }) - - // wallet 2 for testing - const {balanceManager: bmW2, balanceStorage: bs2} = usePortfolioBalanceManager({ - tokenManager, - walletId: 'wallet-2', - network: Chain.Network.Main, - }) - 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([]), - }) - - bmW2.sync({ - primaryBalance: { - balance: 2n, - lockedInBuiltTxs: 3n, - minRequiredByTokens: 0n, - records: [], - }, - secondaryBalances: new Map([]), - }) - } + const [openedWalletsByNetwork] = React.useState>>( + manager.getOpenedWalletsByNetwork(), + ) const handleOnReset = () => { - bs1.clear() - bs2.clear() - tokenStorage.clear() navigation.goBack() } @@ -113,7 +27,7 @@ export const PortfolioScreen = () => { -