diff --git a/app/common/utils/chains.ts b/app/common/utils/chains.ts index 58dbdcb9..42a2b9d4 100644 --- a/app/common/utils/chains.ts +++ b/app/common/utils/chains.ts @@ -1,6 +1,6 @@ -export const DEFAULT_CONNECTED_CHAINS: Record = { - '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3': true, // POLKADOT - '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe': true, // KUSAMA - '0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f': true, // DOT_ASSET_HUB - '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e': true, // WESTEND +export const DEFAULT_CONNECTED_CHAINS: Record = { + '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3': [0], // POLKADOT + '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe': [0], // KUSAMA + '0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f': [1], // DOT_ASSET_HUB + '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e': [0], // WESTEND }; diff --git a/app/models/balances/balances-model.test.ts b/app/models/balances/balances-model.test.ts index afbe7597..aa7a64c9 100644 --- a/app/models/balances/balances-model.test.ts +++ b/app/models/balances/balances-model.test.ts @@ -25,11 +25,7 @@ describe('models/balances/balances-model', () => { }; const scope = fork({ - values: new Map().set(balancesModel._internal.$balances, { - '0x002': { - 0: { ...defaultBalance, assetId: 0 }, - }, - }), + values: [[balancesModel._internal.$balances, { '0x002': { 0: { ...defaultBalance, assetId: 0 } } }]], }); await allSettled(balancesModel._internal.balanceUpdated, { @@ -47,7 +43,7 @@ describe('models/balances/balances-model', () => { test('should update $activeAssets on assetToUnsubSet', async () => { const scope = fork({ - values: new Map().set(balancesModel._internal.$activeAssets, { '0x001': { 0: true } }), + values: [[balancesModel._internal.$activeAssets, { '0x001': { 0: true } }]], }); await allSettled(balancesModel.input.assetToUnsubSet, { @@ -62,7 +58,7 @@ describe('models/balances/balances-model', () => { const mockedSubscriptions = { '0x001': { 0: Promise.resolve(noop), 1: Promise.resolve(noop) } }; const scope = fork({ - values: new Map().set(balancesModel._internal.$subscriptions, mockedSubscriptions), + values: [[balancesModel._internal.$subscriptions, mockedSubscriptions]], }); await allSettled(balancesModel.input.assetToUnsubSet, { scope, params: { chainId: '0x001', assetId: 1 } }); @@ -72,7 +68,7 @@ describe('models/balances/balances-model', () => { test('should remove asset from $activeAssets on networkModel.output.assetChanged', async () => { const scope = fork({ - values: new Map().set(balancesModel._internal.$activeAssets, { '0x001': { 0: true } }), + values: [[balancesModel._internal.$activeAssets, { '0x001': { 0: true } }]], }); await allSettled(networkModel.output.assetChanged, { @@ -85,7 +81,7 @@ describe('models/balances/balances-model', () => { test('should update $activeAssets on assetToSubSet', async () => { const scope = fork({ - values: new Map().set(balancesModel._internal.$activeAssets, { '0x001': { 0: true } }), + values: [[balancesModel._internal.$activeAssets, { '0x001': { 0: true } }]], }); await allSettled(balancesModel.input.assetToSubSet, { @@ -100,7 +96,7 @@ describe('models/balances/balances-model', () => { test('should add asset to $activeAssets on networkModel.output.assetChanged', async () => { const scope = fork({ - values: new Map().set(balancesModel._internal.$activeAssets, { '0x001': { 0: true } }), + values: [[balancesModel._internal.$activeAssets, { '0x001': { 0: true } }]], }); await allSettled(networkModel.output.assetChanged, { @@ -118,8 +114,8 @@ describe('models/balances/balances-model', () => { const fakeUnsubscribeFx = vi.fn().mockReturnValue({ '0x001': undefined }); const scope = fork({ - values: new Map().set(balancesModel._internal.$subscriptions, mockedSubscriptions), - handlers: new Map().set(balancesModel._internal.unsubscribeChainAssetsFx, fakeUnsubscribeFx), + values: [[balancesModel._internal.$subscriptions, mockedSubscriptions]], + handlers: [[balancesModel._internal.unsubscribeChainAssetsFx, fakeUnsubscribeFx]], }); await allSettled(balancesModel._internal.unsubscribeChainAssetsFx, { @@ -135,8 +131,8 @@ describe('models/balances/balances-model', () => { const fakeSubscribeFx = vi.fn().mockReturnValue({ '0x001': { 1: Promise.resolve(noop) } }); const scope = fork({ - values: new Map().set(balancesModel._internal.$subscriptions, mockedSubscriptions), - handlers: new Map().set(balancesModel._internal.subscribeChainsAssetsFx, fakeSubscribeFx), + values: [[balancesModel._internal.$subscriptions, mockedSubscriptions]], + handlers: [[balancesModel._internal.subscribeChainsAssetsFx, fakeSubscribeFx]], }); await allSettled(balancesModel._internal.subscribeChainsAssetsFx, { @@ -151,9 +147,10 @@ describe('models/balances/balances-model', () => { const spyUnsub = vi.fn(); const scope = fork({ - values: new Map() - .set(balancesModel._internal.$activeAssets, { '0x001': { 0: true } }) - .set(balancesModel._internal.$subscriptions, { '0x001': { 0: Promise.resolve(spyUnsub) } }), + values: [ + [balancesModel._internal.$activeAssets, { '0x001': { 0: true } }], + [balancesModel._internal.$subscriptions, { '0x001': { 0: Promise.resolve(spyUnsub) } }], + ], }); await allSettled(balancesModel.input.assetToUnsubSet, { @@ -170,12 +167,13 @@ describe('models/balances/balances-model', () => { vi.spyOn(balancesApi, 'subscribeBalance').mockReturnValue(unsubPromise); const scope = fork({ - values: new Map() - .set(balancesModel._internal.$activeAssets, { '0x003': { 0: true } }) - .set(balancesModel._internal.$subscriptions, { '0x003': { 0: unsubPromise } }) - .set(walletModel._internal.$account, '0x999') - .set(networkModel._internal.$chains, mockedChains) - .set(networkModel._internal.$connections, { '0x003': { api: {}, status: 'connected' } }), + values: [ + [balancesModel._internal.$activeAssets, { '0x003': { 0: true } }], + [balancesModel._internal.$subscriptions, { '0x003': { 0: unsubPromise } }], + [walletModel._internal.$account, '0x999'], + [networkModel._internal.$chains, mockedChains], + [networkModel._internal.$connections, { '0x003': { api: {}, status: 'connected' } }], + ], }); await allSettled(balancesModel.input.assetToSubSet, { @@ -193,14 +191,18 @@ describe('models/balances/balances-model', () => { vi.spyOn(balancesApi, 'subscribeBalance').mockReturnValue(unsubPromise); const scope = fork({ - values: new Map() - .set(balancesModel._internal.$activeAssets, { '0x001': { 1: true }, '0x003': { 0: true, 1: true } }) - .set(walletModel._internal.$account, '0x999') - .set(networkModel._internal.$chains, mockedChains) - .set(networkModel._internal.$connections, { - '0x001': { status: 'disconnected' }, - '0x003': { api: {}, status: 'connected' }, - }), + values: [ + [balancesModel._internal.$activeAssets, { '0x001': { 1: true }, '0x003': { 0: true, 1: true } }], + [walletModel._internal.$account, '0x999'], + [networkModel._internal.$chains, mockedChains], + [ + networkModel._internal.$connections, + { + '0x001': { status: 'disconnected' }, + '0x003': { api: {}, status: 'connected' }, + }, + ], + ], }); await allSettled(networkModel.output.connectionChanged, { @@ -218,14 +220,18 @@ describe('models/balances/balances-model', () => { vi.spyOn(balancesApi, 'subscribeBalance').mockReturnValue(Promise.resolve(spyUnsub)); const scope = fork({ - values: new Map() - .set(balancesModel._internal.$activeAssets, { '0x001': { 1: true }, '0x003': { 0: true, 1: true } }) - .set(balancesModel._internal.$subscriptions, { - '0x001': { 1: Promise.resolve(spyUnsub) }, - '0x003': { 0: Promise.resolve(spyUnsub), 1: Promise.resolve(spyUnsub) }, - }) - .set(walletModel._internal.$account, '0x999') - .set(networkModel._internal.$chains, mockedChains), + values: [ + [balancesModel._internal.$activeAssets, { '0x001': { 1: true }, '0x003': { 0: true, 1: true } }], + [walletModel._internal.$account, '0x999'], + [networkModel._internal.$chains, mockedChains], + [ + balancesModel._internal.$subscriptions, + { + '0x001': { 1: Promise.resolve(spyUnsub) }, + '0x003': { 0: Promise.resolve(spyUnsub), 1: Promise.resolve(spyUnsub) }, + }, + ], + ], }); await allSettled(networkModel.output.connectionChanged, { @@ -245,13 +251,17 @@ describe('models/balances/balances-model', () => { vi.spyOn(balancesApi, 'subscribeBalance').mockReturnValue(unsubPromise); const scope = fork({ - values: new Map() - .set(balancesModel._internal.$activeAssets, { '0x001': { 0: true }, '0x003': { 0: true, 1: true } }) - .set(networkModel._internal.$chains, mockedChains) - .set(networkModel._internal.$connections, { - '0x001': { api: {}, status: 'connected' }, - '0x003': { api: {}, status: 'connected' }, - }), + values: [ + [balancesModel._internal.$activeAssets, { '0x001': { 0: true }, '0x003': { 0: true, 1: true } }], + [networkModel._internal.$chains, mockedChains], + [ + networkModel._internal.$connections, + { + '0x001': { api: {}, status: 'connected' }, + '0x003': { api: {}, status: 'connected' }, + }, + ], + ], }); await allSettled(walletModel._internal.$account, { scope, params: '0x999' }); diff --git a/app/models/network/network-model.test.ts b/app/models/network/network-model.test.ts index ad70b00a..aaf6f92d 100644 --- a/app/models/network/network-model.test.ts +++ b/app/models/network/network-model.test.ts @@ -1,10 +1,11 @@ import { allSettled, fork } from 'effector'; -import { keyBy, noop } from 'lodash-es'; +import { keyBy } from 'lodash-es'; import { describe, expect, test, vi } from 'vitest'; import { type ApiPromise } from '@polkadot/api'; -import { type ProviderWithMetadata } from '@/shared/api/network/provider-api'; +import { CONNECTIONS_STORE } from '@/common/utils'; +import { chainsApi } from '@/shared/api'; import { type Chain, type ChainMetadata } from '@/types/substrate'; import { networkModel } from './network-model'; @@ -26,79 +27,88 @@ const mockedChains = [ }, { name: 'Karura', - chainIndex: '3', + chainIndex: '2', chainId: '0xbaf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b', assets: [{ assetId: 0 }, { assetId: 1 }, { assetId: 2 }], nodes: [], }, + { + name: 'Polkadot Asset Hub', + chainIndex: '3', + chainId: '0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f', + assets: [{ assetId: 1 }], + nodes: [], + }, + { + name: 'Westend', + chainIndex: '4', + chainId: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', + assets: [{ assetId: 0 }], + nodes: [], + }, ] as unknown as Chain[]; -describe('@/common/network/network-model', () => { - const mockedChainsMap = keyBy(mockedChains, 'chainId'); - - const mockProvider = () => { - networkModel._internal.getConnectedAssetsFx.use(() => Promise.resolve({})); - networkModel._internal.createProviderFx.use(({ chainId }) => { - const mockedProvider = {} as ProviderWithMetadata; - const mockedApi = { genesisHash: { toHex: () => chainId } } as ApiPromise; +const mockedChainsMap = keyBy(mockedChains, 'chainId'); - return Promise.resolve({ provider: mockedProvider, api: mockedApi }); - }); +describe('@/common/network/network-model', () => { + const effectMocks = { + createProviderFx: { + fx: networkModel._internal.createProviderFx, + mock: ({ chainId }: any) => ({ + provider: {}, + api: { genesisHash: { toHex: () => chainId } }, + }), + }, + getConnectedAssetsFx: { + fx: networkModel._internal.getConnectedAssetsFx, + mock: vi.fn().mockResolvedValue({}), + }, }; test('should populate $chains on networkStarted event', async () => { - const fakePopulateFx = vi.fn().mockResolvedValue(mockedChainsMap); - const scope = fork(); - - networkModel._internal.populateChainsFx.use(fakePopulateFx); + const fakeRequestFx = vi.fn().mockResolvedValue(mockedChainsMap); + const scope = fork({ + handlers: [ + [networkModel._internal.requestChainsFx, fakeRequestFx], + [effectMocks.createProviderFx.fx, effectMocks.createProviderFx.mock], + ], + }); await allSettled(networkModel.input.networkStarted, { scope, params: 'chains_dev' }); - expect(fakePopulateFx).toHaveBeenCalledOnce(); + expect(fakeRequestFx).toHaveBeenCalledOnce(); expect(scope.getState(networkModel._internal.$chains)).toEqual(mockedChainsMap); }); - test('should add metadata subscription after createProviderFx effect', async () => { - const chain = mockedChains[0]; - const scope = fork(); - - mockProvider(); - networkModel._internal.populateChainsFx.use(() => { - return Promise.resolve({ [chain.chainId]: chain }); - }); - networkModel._internal.subscribeMetadataFx.use(() => { - return Promise.resolve({ chainId: chain.chainId, unsubscribe: noop }); - }); - - await allSettled(networkModel.input.networkStarted, { scope, params: mockedChains[0].chainId }); - - expect(scope.getState(networkModel._internal.$metadataSubscriptions)).toEqual({ [chain.chainId]: noop }); - }); - test('should update $metadata after requestMetadataFx effect', async () => { const chain = mockedChains[0]; const mockMetadata: ChainMetadata = { chainId: chain.chainId, version: 1, metadata: '0x0000' }; const scope = fork({ - values: new Map().set(networkModel._internal.$chains, chain), + values: [[networkModel._internal.$chains, chain]], + handlers: [[networkModel._internal.requestMetadataFx, () => mockMetadata]], }); - networkModel._internal.requestMetadataFx.use(() => Promise.resolve(mockMetadata)); - await allSettled(networkModel._internal.requestMetadataFx, { scope, params: {} as ApiPromise }); expect(scope.getState(networkModel._internal.$metadata)).toEqual([mockMetadata]); }); test('should connect to default_chains on networkStarted event', async () => { - const scope = fork(); + vi.spyOn(chainsApi, 'getChainsData').mockResolvedValue(mockedChains); - mockProvider(); - networkModel._internal.populateChainsFx.use(() => mockedChainsMap); + const scope = fork({ + handlers: [ + [effectMocks.getConnectedAssetsFx.fx, effectMocks.getConnectedAssetsFx.mock], + [effectMocks.createProviderFx.fx, effectMocks.createProviderFx.mock], + ], + }); await allSettled(networkModel.input.networkStarted, { scope, params: 'chains_dev' }); expect(scope.getState(networkModel.$assets)).toEqual({ [mockedChains[0].chainId]: { 0: { assetId: 0 } }, [mockedChains[1].chainId]: { 0: { assetId: 0 } }, + [mockedChains[3].chainId]: { 1: { assetId: 1 } }, + [mockedChains[4].chainId]: { 0: { assetId: 0 } }, }); const connection = { provider: expect.any(Object), api: expect.any(Object) }; @@ -106,22 +116,31 @@ describe('@/common/network/network-model', () => { [mockedChains[0].chainId]: { ...connection, status: 'connected' }, // Polkadot [mockedChains[1].chainId]: { ...connection, status: 'connected' }, // Kusama [mockedChains[2].chainId]: { status: 'disconnected' }, // Karura + [mockedChains[3].chainId]: { ...connection, status: 'connected' }, // Polkadot Asset Hub + [mockedChains[4].chainId]: { ...connection, status: 'connected' }, // Westend }); }); test('should connect to default_chains + Karura from CloudStorage on networkStarted event', async () => { - const scope = fork(); + vi.spyOn(chainsApi, 'getChainsData').mockResolvedValue(mockedChains); + + // Karura (index 2) and chain (index 19) that's missing in chains.json + const getItem = (_: string, cb: (_: null, result: string) => void) => cb(null, '2_0,2;19_0,2,3;'); + + window.Telegram = { WebApp: { CloudStorage: { getItem } } } as any; - mockProvider(); - networkModel._internal.getConnectedAssetsFx.use(() => Promise.resolve({ 3: [1, 2] })); - networkModel._internal.populateChainsFx.use(() => mockedChainsMap); + const scope = fork({ + handlers: [[effectMocks.createProviderFx.fx, effectMocks.createProviderFx.mock]], + }); await allSettled(networkModel.input.networkStarted, { scope, params: 'chains_dev' }); expect(scope.getState(networkModel.$assets)).toEqual({ [mockedChains[0].chainId]: { 0: { assetId: 0 } }, [mockedChains[1].chainId]: { 0: { assetId: 0 } }, - [mockedChains[2].chainId]: { 1: { assetId: 1 }, 2: { assetId: 2 } }, + [mockedChains[2].chainId]: { 0: { assetId: 0 }, 2: { assetId: 2 } }, + [mockedChains[3].chainId]: { 1: { assetId: 1 } }, + [mockedChains[4].chainId]: { 0: { assetId: 0 } }, }); const connection = { provider: expect.any(Object), api: expect.any(Object) }; @@ -129,18 +148,23 @@ describe('@/common/network/network-model', () => { [mockedChains[0].chainId]: { ...connection, status: 'connected' }, // Polkadot [mockedChains[1].chainId]: { ...connection, status: 'connected' }, // Kusama [mockedChains[2].chainId]: { ...connection, status: 'connected' }, // Karura + [mockedChains[3].chainId]: { ...connection, status: 'connected' }, // Polkadot Asset Hub + [mockedChains[4].chainId]: { ...connection, status: 'connected' }, // Westend }); }); test('should connect to Karura on assetConnected event', async () => { const scope = fork({ - values: new Map().set(networkModel._internal.$chains, mockedChainsMap).set(networkModel._internal.$connections, { - [mockedChains[2].chainId]: { status: 'disconnected' }, // Karura - }), + values: [ + [networkModel._internal.$chains, mockedChainsMap], + [networkModel._internal.$connections, { [mockedChains[2].chainId]: { status: 'disconnected' } }], // Karura + ], + handlers: [ + [effectMocks.getConnectedAssetsFx.fx, effectMocks.getConnectedAssetsFx.mock], + [effectMocks.createProviderFx.fx, effectMocks.createProviderFx.mock], + ], }); - mockProvider(); - await allSettled(networkModel.input.assetConnected, { scope, params: { chainId: mockedChains[2].chainId, assetId: 0 }, @@ -154,16 +178,17 @@ describe('@/common/network/network-model', () => { test('should disconnect from Polkadot on assetDisconnected event', async () => { const scope = fork({ - values: new Map() - .set(networkModel._internal.$chains, mockedChainsMap) - .set(networkModel._internal.$assets, { [mockedChains[0].chainId]: { 0: true } }) - .set(networkModel._internal.$connections, { - [mockedChains[0].chainId]: { provider: {}, api: {}, status: 'connected' }, // Polkadot - }), + values: [ + [networkModel._internal.$chains, mockedChainsMap], + [networkModel._internal.$assets, { [mockedChains[0].chainId]: { 0: true } }], + [ + networkModel._internal.$connections, + { [mockedChains[0].chainId]: { provider: {}, api: {}, status: 'connected' } }, // Polkadot + ], + ], + handlers: [[networkModel._internal.disconnectFx, () => mockedChains[0].chainId]], }); - networkModel._internal.disconnectFx.use(() => Promise.resolve(mockedChains[0].chainId)); - await allSettled(networkModel.input.assetDisconnected, { scope, params: { chainId: mockedChains[0].chainId, assetId: 0 }, @@ -178,16 +203,14 @@ describe('@/common/network/network-model', () => { const connection = { provider: {}, api: {}, status: 'connected' }; const scope = fork({ - values: new Map() - .set(networkModel._internal.$chains, mockedChainsMap) - .set(networkModel._internal.$assets, { [mockedChains[2].chainId]: { 0: true, 1: true } }) - .set(networkModel._internal.$connections, { - [mockedChains[2].chainId]: connection, // Karura - }), + values: [ + [networkModel._internal.$chains, mockedChainsMap], + [networkModel._internal.$assets, { [mockedChains[2].chainId]: { 0: true, 1: true } }], + [networkModel._internal.$connections, { [mockedChains[2].chainId]: connection }], // Karura + ], + handlers: [[networkModel._internal.disconnectFx, () => mockedChains[2].chainId]], }); - networkModel._internal.disconnectFx.use(() => Promise.resolve(mockedChains[2].chainId)); - await allSettled(networkModel.input.assetDisconnected, { scope, params: { chainId: mockedChains[2].chainId, assetId: 1 }, @@ -197,4 +220,31 @@ describe('@/common/network/network-model', () => { [mockedChains[2].chainId]: connection, // Karura }); }); + + test('should update CloudStorage on assetConnected event', async () => { + const setItemSpy = vi.fn(); + window.Telegram = { WebApp: { CloudStorage: { setItem: setItemSpy } } } as any; + + const scope = fork({ + values: [ + [networkModel._internal.$chains, mockedChainsMap], + [networkModel._internal.$assets, { [mockedChains[0].chainId]: { 0: true, 1: true } }], + [ + networkModel._internal.$connections, + { + [mockedChains[0].chainId]: { status: 'connected' }, + [mockedChains[2].chainId]: { status: 'disconnected' }, + }, + ], + ], + handlers: [[effectMocks.createProviderFx.fx, effectMocks.createProviderFx.mock]], + }); + + await allSettled(networkModel.input.assetConnected, { + scope, + params: { chainId: mockedChains[2].chainId, assetId: 1 }, + }); + + expect(setItemSpy).toHaveBeenCalledWith(CONNECTIONS_STORE, '0_0,1;2_1;'); + }); }); diff --git a/app/models/network/network-model.ts b/app/models/network/network-model.ts index 3139a53c..1201f62f 100644 --- a/app/models/network/network-model.ts +++ b/app/models/network/network-model.ts @@ -4,6 +4,7 @@ import { combineEvents, readonly, spread } from 'patronum'; import { type ApiPromise } from '@polkadot/api'; +import { CONNECTIONS_STORE } from '@/common/utils'; import { DEFAULT_CONNECTED_CHAINS } from '@/common/utils/chains.ts'; import { chainsApi, metadataApi } from '@/shared/api'; import { type ProviderWithMetadata, providerApi } from '@/shared/api/network/provider-api'; @@ -33,39 +34,51 @@ const $connections = createStore>({}); const $metadata = createStore([]); const $metadataSubscriptions = createStore>({}); -const populateChainsFx = createEffect(async (file: 'chains_dev' | 'chains_prod'): Promise> => { +const initConnectionsFx = createEffect((chainsMap: Record) => { + Object.entries(chainsMap).forEach(([chainId, assetIds]) => { + assetIds.forEach(assetId => assetConnected({ chainId: chainId as ChainId, assetId })); + }); +}); + +const requestChainsFx = createEffect(async (file: 'chains_dev' | 'chains_prod'): Promise> => { const chains = await chainsApi.getChainsData({ file, sort: true }); return keyBy(chains, 'chainId'); }); const getConnectedAssetsFx = createEffect((): Promise> => { - const cloud = '3_0,2,3;19_0,2,3;'; // Karura & unknown chain + const webApp = window.Telegram?.WebApp; + + if (!webApp) return Promise.resolve({}); + + return new Promise(resolve => { + // connections format is chainIndex_assetId1,...,assetIdn; + webApp.CloudStorage.getItem(CONNECTIONS_STORE, (error, connections) => { + if (error || !connections) resolve({}); - const result: Record = {}; - const entries = cloud.split(';').filter(item => item.trim() !== ''); + const result: Record = {}; + const entries = connections!.split(';').filter(item => item.trim() !== ''); - entries.forEach(entry => { - const [key, values] = entry.split('_'); - result[key] = values.split(',').map(Number); + entries.forEach(entry => { + const [key, values] = entry.split('_'); + result[key] = values.split(',').map(Number); + }); + + resolve(result); + }); }); +}); + +const updateAssetsInCloudFx = createEffect((assets: Record) => { + const webApp = window.Telegram?.WebApp; + + if (!webApp) return; + + const joinedAssets = Object.entries(assets).reduce((acc, [chainIndex, assetIds]) => { + return acc + `${chainIndex}_${assetIds.join(',')};`; + }, ''); - return Promise.resolve(result); - - // TODO: uncomment during task - https://app.clickup.com/t/869502m30 - // const webApp = window.Telegram?.WebApp; - // - // if (!webApp) return Promise.resolve([]); - // - // return new Promise(resolve => { - // // connections format is chainIndex_assetId1,...,assetIdn; - // webApp.CloudStorage.getItem(CONNECTIONS_STORE, (error, connections) => { - // if (error || !connections) resolve([]); - // - // const chainIndices = (connections as string).match(/(\d)_/g)?.map(match => match[0]); - // resolve(chainIndices || []); - // }); - // }); + webApp.CloudStorage.setItem(CONNECTIONS_STORE, joinedAssets); }); type MetadataSubResult = { @@ -94,12 +107,6 @@ const updateProviderMetadataFx = createEffect(({ provider, metadata }: ProviderM provider.updateMetadata(metadata); }); -const initConnectionsFx = createEffect((chainsMap: Record) => { - Object.entries(chainsMap).forEach(([chainId, values]) => { - values.forEach(assetId => assetConnected({ chainId: chainId as ChainId, assetId })); - }); -}); - type CreateProviderParams = { name: string; chainId: ChainId; @@ -112,6 +119,8 @@ const createProviderFx = createEffect( const boundDisconnected = scopeBind(disconnected, { safe: true }); const boundFailed = scopeBind(failed, { safe: true }); + console.info('⚫️ Connecting to ==> ', params.name); + return providerApi.createConnector( { nodes: params.nodes, metadata: params.metadata?.metadata }, { @@ -150,16 +159,16 @@ const disconnectFx = createEffect(async ({ provider, api }: DisconnectParams): P sample({ clock: networkStarted, - target: [populateChainsFx, getConnectedAssetsFx], + target: [requestChainsFx, getConnectedAssetsFx], }); sample({ - clock: populateChainsFx.doneData, + clock: requestChainsFx.doneData, target: $chains, }); sample({ - clock: populateChainsFx.doneData, + clock: requestChainsFx.doneData, fn: chains => { const connections: Record = {}; @@ -173,16 +182,30 @@ sample({ }); sample({ - clock: combineEvents([populateChainsFx.doneData, getConnectedAssetsFx.doneData]), + clock: requestChainsFx.doneData, + fn: chains => { + const chainsMap: Record = {}; + + for (const chain of Object.values(chains)) { + if (!DEFAULT_CONNECTED_CHAINS[chain.chainId]) continue; + + chainsMap[chain.chainId] = DEFAULT_CONNECTED_CHAINS[chain.chainId]; + } + + return chainsMap; + }, + target: initConnectionsFx, +}); + +sample({ + clock: combineEvents([requestChainsFx.doneData, getConnectedAssetsFx.doneData]), fn: ([chains, assetsMap]) => { const chainsMap: Record = {}; for (const chain of Object.values(chains)) { - if (DEFAULT_CONNECTED_CHAINS[chain.chainId]) { - chainsMap[chain.chainId] = [chain.assets[0].assetId]; - } else if (assetsMap[chain.chainIndex]) { - chainsMap[chain.chainId] = assetsMap[chain.chainIndex]; - } + if (!assetsMap[chain.chainIndex]) continue; + + chainsMap[chain.chainId] = assetsMap[chain.chainIndex]; } return chainsMap; @@ -198,7 +221,7 @@ sample({ connections: $connections, }, filter: ({ chains }, { chainId, assetId }) => { - return chains[chainId].assets.some(a => a.assetId === assetId); + return chains[chainId]?.assets.some(a => a.assetId === assetId); }, fn: ({ chains, connections, assets }, { chainId, assetId }) => { const isDisconnected = connections[chainId].status === 'disconnected'; @@ -206,7 +229,7 @@ sample({ const asset = chains[chainId].assets.find(a => a.assetId === assetId); const newAssets = { ...assets, [chainId]: { ...assets[chainId], [assetId]: asset! } }; - // If chain is disconnected then connect, notify with new asset, assets is updated anyway + // If chain is disconnected then connect, notify with new asset, $assets is updated anyway return { assets: newAssets, notify: { chainId, assetId, status: 'on' as const }, @@ -220,6 +243,29 @@ sample({ }), }); +sample({ + clock: [assetConnected, assetDisconnected], + source: { + chains: $chains, + assets: $assets, + }, + fn: ({ chains, assets }) => { + const result: Record = {}; + + for (const chainId of Object.keys(assets)) { + const typedChainId = chainId as ChainId; + const chain = chains[typedChainId]; + + if (!chain || !assets[typedChainId]) continue; + + result[chain.chainIndex] = Object.keys(assets[typedChainId]).map(Number); + } + + return result; + }, + target: updateAssetsInCloudFx, +}); + sample({ clock: assetDisconnected, source: { @@ -258,7 +304,6 @@ sample({ target: $connections, }); -// TODO: save new connection in CloudStorage - https://app.clickup.com/t/869502m30 sample({ clock: chainConnected, source: { @@ -268,13 +313,13 @@ sample({ fn: ({ chains, metadata }, chainId) => { const name = chains[chainId].name; const nodes = chains[chainId].nodes.map(node => node.url); - const newMetadata = metadata.reduce>((acc, data) => { - if (data.version >= (acc[data.chainId]?.version || -1)) { - acc[data.chainId] = data; - } - return acc; - }, {}); + const newMetadata: Record = {}; + for (const data of metadata) { + if (data.version < (newMetadata[data.chainId]?.version || -1)) continue; + + newMetadata[data.chainId] = data; + } return { name, chainId, nodes, metadata: newMetadata[chainId] }; }, @@ -461,7 +506,7 @@ export const networkModel = { $metadata, $metadataSubscriptions, - populateChainsFx, + requestChainsFx, getConnectedAssetsFx, createProviderFx, disconnectFx, diff --git a/app/routes/exchange/select.tsx b/app/routes/exchange/select.tsx index 66933ff9..9082bf5c 100644 --- a/app/routes/exchange/select.tsx +++ b/app/routes/exchange/select.tsx @@ -59,7 +59,7 @@ const Page = () => { {exchangeAssets.flatMap(([chainId, assets]) => { return assets.map(asset => (