diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d7a3d2fe..5cd1c5a4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -14,23 +14,23 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: ['react-refresh', 'react-hooks'], rules: { - // 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - // indent: 'off', - // '@typescript-eslint/indent': 'off', - // 'multiline-ternary': 'off', - // 'no-unused-vars': 'off', - // '@typescript-eslint/no-unused-vars': 'off', - // '@typescript-eslint/explicit-function-return-type': 'off', - // '@typescript-eslint/prefer-reduce-type-parameter': 'off', - // '@typescript-eslint/strict-boolean-expressions': 'off', - // '@typescript-eslint/space-before-function-paren': 'off', - // '@typescript-eslint/prefer-nullish-coalescing': 'off', - // '@typescript-eslint/member-delimiter-style': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + indent: 'off', + '@typescript-eslint/indent': 'off', + 'multiline-ternary': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/prefer-reduce-type-parameter': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/space-before-function-paren': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/member-delimiter-style': 'off', '@typescript-eslint/no-explicit-any': 'off', - // 'generator-star-spacing': ['error', { before: false, after: true }], - // 'yield-star-spacing': ['error', { before: false, after: true }], + 'generator-star-spacing': ['error', { before: false, after: true }], + 'yield-star-spacing': ['error', { before: false, after: true }], 'react-hooks/exhaustive-deps': 'off', - // 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/rules-of-hooks': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/.storybook/decorators.jsx b/.storybook/decorators.jsx index 7ee29888..ba553cdb 100644 --- a/.storybook/decorators.jsx +++ b/.storybook/decorators.jsx @@ -1,8 +1,4 @@ import React from 'react' import { Provider } from 'react-redux' -// export const withMaterialStyles = storyFn => ( -// {storyFn()} -// ) - export const withStore = store => storyFn => {storyFn()} diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 6e1333f4..2116d026 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -73,7 +73,6 @@ const preview: Preview = { ] }, createBrowserRouter: { withRouter } - // actions: { argTypesRegex: '^on.*' } }, decorators: [ @@ -85,7 +84,6 @@ const preview: Preview = { Provider: ThemeProvider, GlobalStyles: CssBaseline }) - // withRouter ] } diff --git a/index.html b/index.html index bcecd014..56076cd3 100644 --- a/index.html +++ b/index.html @@ -69,9 +69,6 @@ - - diff --git a/src/static/svg/twitter-ic-footer.svg b/src/static/svg/twitter-ic-footer.svg deleted file mode 100644 index d4f2b087..00000000 --- a/src/static/svg/twitter-ic-footer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/static/svg/twitterCircle.svg b/src/static/svg/twitterCircle.svg deleted file mode 100644 index 8b083331..00000000 --- a/src/static/svg/twitterCircle.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/public/unknownToken.svg b/src/static/svg/unknownToken.svg similarity index 100% rename from public/unknownToken.svg rename to src/static/svg/unknownToken.svg diff --git a/src/static/svg/xCircle.svg b/src/static/svg/xCircle.svg new file mode 100644 index 00000000..30cb29db --- /dev/null +++ b/src/static/svg/xCircle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/theme/index.ts b/src/static/theme/index.ts index c524a747..37b289f1 100644 --- a/src/static/theme/index.ts +++ b/src/static/theme/index.ts @@ -1,17 +1,16 @@ -// theme.ts import { createTheme } from '@mui/material/styles' export const colors = { black: { full: '#000000', - background: '#1B1C2A', // v2.0 + background: '#1B1C2A', light: '#090B1B', kinda: '#1A1A1A', greyish: '#081323', - cinder: '#0E0C12', // v2.0 background color - controls: '#44424E', // v2.0 controls background color - header: '#1A1D28', // v2.0 header - card: '#28242E' // v2.0 card color + cinder: '#0E0C12', + controls: '#44424E', + header: '#1A1D28', + card: '#28242E' }, blue: { accent: '#072E5A', @@ -22,7 +21,7 @@ export const colors = { neon: '#08F7FE', astel: '#48ADF1', bastille: '#1E1A23', - charade: '#272735', // v2.0 component + charade: '#272735', deep: '#4B5983' }, green: { @@ -47,7 +46,6 @@ export const colors = { neon: '#F5D300' }, navy: { - // colors with suffix "2" on figma background: '#0C0D2C', dark: '#0E0E2A', component: '#1D1D49', @@ -158,12 +156,11 @@ export const typography = { } } -// Create a theme instance. export const theme = createTheme({ palette: { primary: { - main: colors.navy.button, // v2.0 - contrastText: colors.navy.veryLightGrey // v2.0 + main: colors.navy.button, + contrastText: colors.navy.veryLightGrey }, secondary: { main: colors.green.button, @@ -198,15 +195,4 @@ export const theme = createTheme({ xl: 1920 } } - // overrides: { - // MuiInputBase: { - // input: { - // MozAppearance: "textfield", - // "&::-webkit-clear-button, &::-webkit-outer-spin-button, &::-webkit-inner-spin-button": - // { - // display: "none", - // }, - // }, - // }, - // }, }) diff --git a/src/store/consts/static.ts b/src/store/consts/static.ts index 93c9f9fa..89a292c9 100644 --- a/src/store/consts/static.ts +++ b/src/store/consts/static.ts @@ -1,6 +1,7 @@ import { FEE_TIERS, Network, + Position, TESTNET_BTC_ADDRESS, TESTNET_ETH_ADDRESS, TESTNET_USDC_ADDRESS, @@ -8,7 +9,8 @@ import { } from '@invariant-labs/a0-sdk' import { Keyring } from '@polkadot/api' import { BestTier, FormatNumberThreshold, PrefixConfig, Token, TokenPriceData } from './types' -import { testnetBestTiersCreator } from './utils' +import { testnetBestTiersCreator } from '@utils/utils' +import { POSITIONS_ENTRIES_LIMIT } from '@invariant-labs/a0-sdk/target/consts' export enum AlephZeroNetworks { TEST = 'wss://ws.test.azero.dev', @@ -255,3 +257,24 @@ export const reversedAddressTickerMap = Object.fromEntries( export const LIQUIDITY_PLOT_DECIMAL = 12n export const DEFAULT_TOKEN_DECIMAL = 12n + +export const EMPTY_POSITION: Position = { + poolKey: { + tokenX: TESTNET_BTC_ADDRESS, + tokenY: TESTNET_ETH_ADDRESS, + feeTier: { fee: 0n, tickSpacing: 1n } + }, + liquidity: 0n, + lowerTickIndex: 0n, + upperTickIndex: 0n, + feeGrowthInsideX: 0n, + feeGrowthInsideY: 0n, + lastBlockNumber: 0n, + tokensOwedX: 0n, + tokensOwedY: 0n +} + +export const POSITIONS_PER_QUERY = + Number(POSITIONS_ENTRIES_LIMIT) - (Number(POSITIONS_ENTRIES_LIMIT) % POSITIONS_PER_PAGE) + +export const MINIMAL_POOL_INIT_PRICE = 0.00000001 diff --git a/src/store/consts/types.ts b/src/store/consts/types.ts index 5c58efc5..ef1f56bc 100644 --- a/src/store/consts/types.ts +++ b/src/store/consts/types.ts @@ -35,6 +35,7 @@ export type SimulateResult = { amountOut: bigint priceImpact: number targetSqrtPrice: bigint + fee: bigint errors: SwapError[] } diff --git a/src/store/reducers/connection.ts b/src/store/reducers/connection.ts index 920d8c53..5a8ea165 100644 --- a/src/store/reducers/connection.ts +++ b/src/store/reducers/connection.ts @@ -1,4 +1,4 @@ -import { Network, TESTNET_INVARIANT_ADDRESS } from '@invariant-labs/a0-sdk' +import { Network, TESTNET_INVARIANT_ADDRESS, TESTNET_WAZERO_ADDRESS } from '@invariant-labs/a0-sdk' import { PayloadAction, createSlice } from '@reduxjs/toolkit' import { AlephZeroNetworks } from '@store/consts/static' import { PayloadType } from '@store/consts/types' @@ -16,6 +16,7 @@ export interface IAlephZeroConnectionStore { blockNumber: number rpcAddress: string invariantAddress: string + wrappedAZEROAddress: string } export const defaultState: IAlephZeroConnectionStore = { @@ -24,7 +25,8 @@ export const defaultState: IAlephZeroConnectionStore = { networkType: Network.Testnet, blockNumber: 0, rpcAddress: AlephZeroNetworks.TEST, - invariantAddress: TESTNET_INVARIANT_ADDRESS + invariantAddress: TESTNET_INVARIANT_ADDRESS, + wrappedAZEROAddress: TESTNET_WAZERO_ADDRESS } export const connectionSliceName = 'connection' const connectionSlice = createSlice({ diff --git a/src/store/reducers/pools.ts b/src/store/reducers/pools.ts index 1de5e8e9..7c809dcd 100644 --- a/src/store/reducers/pools.ts +++ b/src/store/reducers/pools.ts @@ -13,7 +13,7 @@ import { import { PayloadAction, createSlice } from '@reduxjs/toolkit' import { AZERO, BTC, ETH, USDC } from '@store/consts/static' import { PayloadType, Token } from '@store/consts/types' -import { poolKeyToString } from '@store/consts/utils' +import { poolKeyToString } from '@utils/utils' import * as R from 'remeda' @@ -113,9 +113,6 @@ const poolsSlice = createSlice({ name: poolsSliceName, initialState: defaultState, reducers: { - initPool(state, _action: PayloadAction) { - return state - }, addTokens(state, action: PayloadAction>) { state.tokens = { ...state.tokens, @@ -149,11 +146,7 @@ const poolsSlice = createSlice({ const { poolKey } = action.payload const keyStringified = poolKeyToString(poolKey) - // Check if a pool with the same PoolKey already exists - // if (!state.pools[keyStringified]) { - // If the pool does not exist, add it to the pools object state.pools[keyStringified] = action.payload - // } } state.isLoadingLatestPoolsForTransaction = false diff --git a/src/store/reducers/positions.ts b/src/store/reducers/positions.ts index da5aef23..294fbfac 100644 --- a/src/store/reducers/positions.ts +++ b/src/store/reducers/positions.ts @@ -3,6 +3,8 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit' import { PayloadType } from '@store/consts/types' export interface PositionsListStore { + length: bigint + loadedPages: Record list: Position[] loading: boolean } @@ -13,6 +15,9 @@ export interface PlotTickData { } export type TickPlotPositionData = Omit + +export type InitMidPrice = TickPlotPositionData & { sqrtPrice: bigint } + export interface PlotTicks { data: PlotTickData[] loading: boolean @@ -87,6 +92,8 @@ export const defaultState: IPositionsStore = { loading: false }, positionsList: { + length: 0n, + loadedPages: {}, list: [], loading: true }, @@ -142,10 +149,46 @@ const positionsSlice = createSlice({ state.positionsList.loading = false return state }, - getPositionsList(state) { + getPositionsListPage(state, _action: PayloadAction<{ index: number; refresh: boolean }>) { + state.positionsList.loading = true + return state + }, + getRemainingPositions(state, _action: PayloadAction) { state.positionsList.loading = true return state }, + setPositionsListLength(state, action: PayloadAction) { + state.positionsList.length = action.payload + return state + }, + setPositionsListLoadedStatus( + state, + action: PayloadAction<{ indexes: number[]; isLoaded: boolean }> + ) { + const { indexes, isLoaded } = action.payload + + for (const index of indexes) { + state.positionsList.loadedPages[index] = isLoaded + } + + return state + }, + removePosition(state, action: PayloadAction) { + if (Number(action.payload) !== state.positionsList.list.length - 1) { + state.positionsList.list[Number(action.payload)] = + state.positionsList.list[state.positionsList.list.length - 1] + } + + state.positionsList.list.pop() + state.positionsList.length -= 1n + + return state + }, + addPosition(state, action: PayloadAction) { + state.positionsList.list.push(action.payload) + state.positionsList.length += 1n + return state + }, getSinglePosition(state, _action: PayloadAction) { return state }, diff --git a/src/store/reducers/snackbars.ts b/src/store/reducers/snackbars.ts index 2a8507ce..69cc47c5 100644 --- a/src/store/reducers/snackbars.ts +++ b/src/store/reducers/snackbars.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { PayloadType } from '../consts/types' -import { createLoaderKey } from '@store/consts/utils' +import { createLoaderKey } from '@utils/utils' import { SnackbarAction, VariantType } from 'notistack' export interface ISnackbar { diff --git a/src/store/reducers/swap.ts b/src/store/reducers/swap.ts index 5937e865..794af2ee 100644 --- a/src/store/reducers/swap.ts +++ b/src/store/reducers/swap.ts @@ -44,6 +44,7 @@ export const defaultState: ISwapStore = { amountOut: 0n, priceImpact: 0, targetSqrtPrice: 0n, + fee: 0n, errors: [] } } diff --git a/src/store/sagas/connection.ts b/src/store/sagas/connection.ts index 6a182cc7..6e1a8baa 100644 --- a/src/store/sagas/connection.ts +++ b/src/store/sagas/connection.ts @@ -1,22 +1,83 @@ import { all, call, put, SagaGenerator, select, takeLeading, spawn, delay } from 'typed-redux-saga' import { actions, Status, PayloadTypes } from '@store/reducers/connection' import { actions as snackbarsActions } from '@store/reducers/snackbars' -import { rpcAddress, networkType } from '@store/selectors/connection' +import { + rpcAddress, + networkType, + invariantAddress, + wrappedAZEROAddress +} from '@store/selectors/connection' import { PayloadAction } from '@reduxjs/toolkit' import { ApiPromise } from '@polkadot/api' import apiSingleton from '@store/services/apiSingleton' +import invariantSingleton from '@store/services/invariantSingleton' +import { Invariant, PSP22, WrappedAZERO } from '@invariant-labs/a0-sdk' +import SingletonPSP22 from '@store/services/psp22Singleton' +import SingletonWrappedAZERO from '@store/services/wrappedAZEROSingleton' -export function* getConnection(): SagaGenerator { - const rpc = yield* select(rpcAddress) - const network = yield* select(networkType) - const api = yield* call([apiSingleton, apiSingleton.loadInstance], network, rpc) +export function* getApi(): SagaGenerator { + let api = yield* call([apiSingleton, apiSingleton.getInstance]) + + if (!api) { + const network = yield* select(networkType) + const rpc = yield* select(rpcAddress) + api = yield* call([apiSingleton, apiSingleton.loadInstance], network, rpc) + } return api } +export function* getInvariant(): SagaGenerator { + let invariant = yield* call([invariantSingleton, invariantSingleton.getInstance]) + + if (!invariant) { + const api = yield* call(getApi) + const network = yield* select(networkType) + const invariantAddr = yield* select(invariantAddress) + invariant = yield* call( + [invariantSingleton, invariantSingleton.loadInstance], + api, + network, + invariantAddr + ) + } + + return invariant +} + +export function* getPSP22(): SagaGenerator { + let psp22 = yield* call([SingletonPSP22, SingletonPSP22.getInstance]) + + if (!psp22) { + const api = yield* call(getApi) + const network = yield* select(networkType) + psp22 = yield* call([SingletonPSP22, SingletonPSP22.loadInstance], api, network) + } + + return psp22 +} + +export function* getWrappedAZERO(): SagaGenerator { + let wrappedAZERO = yield* call([SingletonWrappedAZERO, SingletonWrappedAZERO.getInstance]) + + if (!wrappedAZERO) { + const api = yield* call(getApi) + const network = yield* select(networkType) + const wrappedAZEROAddr = yield* select(wrappedAZEROAddress) + wrappedAZERO = yield* call( + [SingletonWrappedAZERO, SingletonWrappedAZERO.loadInstance], + api, + network, + wrappedAZEROAddr + ) + } + + return wrappedAZERO +} + export function* initConnection(): Generator { try { - yield* call(getConnection) + yield* getApi() yield* put( snackbarsActions.add({ @@ -44,8 +105,7 @@ export function* initConnection(): Generator { export function* handleNetworkChange(action: PayloadAction): Generator { yield* delay(1000) - const { networkType, rpcAddress } = action.payload - yield* call([apiSingleton, apiSingleton.loadInstance], networkType, rpcAddress) + yield* getApi() yield* put( snackbarsActions.add({ diff --git a/src/store/sagas/pools.ts b/src/store/sagas/pools.ts index 13ddacbc..15ec55ff 100644 --- a/src/store/sagas/pools.ts +++ b/src/store/sagas/pools.ts @@ -1,13 +1,11 @@ -import { PoolKey, newPoolKey, sendTx, toSqrtPrice } from '@invariant-labs/a0-sdk' -import { Signer } from '@polkadot/api/types' +import { PoolKey, newPoolKey } from '@invariant-labs/a0-sdk' import { PayloadAction } from '@reduxjs/toolkit' import { - createLoaderKey, findPairs, getPoolsByPoolKeys, getTokenBalances, getTokenDataByAddresses -} from '@store/consts/utils' +} from '@utils/utils' import { FetchTicksAndTickMaps, ListPoolsRequest, @@ -15,30 +13,18 @@ import { PoolWithPoolKey, actions } from '@store/reducers/pools' -import { actions as snackbarsActions } from '@store/reducers/snackbars' import { actions as walletActions } from '@store/reducers/wallet' -import { invariantAddress, networkType } from '@store/selectors/connection' import { tokens } from '@store/selectors/pools' import { address } from '@store/selectors/wallet' -import invariantSingleton from '@store/services/invariantSingleton' -import { getAlephZeroWallet } from '@utils/web3/wallet' -import { closeSnackbar } from 'notistack' import { all, call, put, select, spawn, takeEvery, takeLatest } from 'typed-redux-saga' -import { getConnection } from './connection' import { MAX_POOL_KEYS_RETURNED } from '@invariant-labs/a0-sdk/target/consts' +import { getInvariant, getPSP22 } from './connection' export function* fetchPoolsDataForList(action: PayloadAction) { const walletAddress = yield* select(address) - const connection = yield* call(getConnection) - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const pools = yield* call( - getPoolsByPoolKeys, - invAddress, - action.payload.poolKeys, - connection, - network - ) + const invariant = yield* getInvariant() + const pools = yield* call(getPoolsByPoolKeys, invariant, action.payload.poolKeys) + const psp22 = yield* getPSP22() const allTokens = yield* select(tokens) const unknownTokens = new Set( @@ -55,17 +41,10 @@ export function* fetchPoolsDataForList(action: PayloadAction) const unknownTokensData = yield* call( getTokenDataByAddresses, [...unknownTokens], - connection, - network, - walletAddress - ) - const knownTokenBalances = yield* call( - getTokenBalances, - [...knownTokens], - connection, - network, + psp22, walletAddress ) + const knownTokenBalances = yield* call(getTokenBalances, [...knownTokens], psp22, walletAddress) yield* put(walletActions.getBalances(Object.keys(unknownTokensData))) yield* put(actions.addTokens(unknownTokensData)) @@ -76,91 +55,11 @@ export function* fetchPoolsDataForList(action: PayloadAction) yield* put(actions.addPoolsForList({ data: pools, listType: action.payload.listType })) } -export function* handleInitPool(action: PayloadAction): Generator { - const loaderKey = createLoaderKey() - const loaderSigningTx = createLoaderKey() - try { - yield put( - snackbarsActions.add({ - message: 'Creating new pool...', - variant: 'pending', - persist: true, - key: loaderKey - }) - ) - - const { tokenX, tokenY, feeTier } = action.payload - - const api = yield* getConnection() - const network = yield* select(networkType) - const walletAddress = yield* select(address) - const adapter = yield* call(getAlephZeroWallet) - const invAddress = yield* select(invariantAddress) - - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) - - const poolKey = newPoolKey(tokenX, tokenY, feeTier) - - const initSqrtPrice = toSqrtPrice(1n, 0n) - - const tx = yield* call([invariant, invariant.createPoolTx], poolKey, initSqrtPrice) - - yield put( - snackbarsActions.add({ - message: 'Signing transaction...', - variant: 'pending', - persist: true, - key: loaderSigningTx - }) - ) - - const signedTx = yield* call([tx, tx.signAsync], walletAddress, { - signer: adapter.signer as Signer - }) - - closeSnackbar(loaderSigningTx) - yield put(snackbarsActions.remove(loaderSigningTx)) - - const txResult = yield* call(sendTx, signedTx) - - yield put( - snackbarsActions.add({ - message: 'Pool successfully created', - variant: 'success', - persist: false, - txid: txResult.hash - }) - ) - - closeSnackbar(loaderKey) - yield put(snackbarsActions.remove(loaderKey)) - } catch (error) { - console.log(error) - closeSnackbar(loaderKey) - yield put(snackbarsActions.remove(loaderKey)) - closeSnackbar(loaderSigningTx) - yield put(snackbarsActions.remove(loaderSigningTx)) - } -} - export function* fetchPoolData(action: PayloadAction): Generator { - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) const { feeTier, tokenX, tokenY } = action.payload try { - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const pool = yield* call([invariant, invariant.getPool], tokenX, tokenY, feeTier) @@ -181,17 +80,8 @@ export function* fetchPoolData(action: PayloadAction): Generator { } export function* fetchAllPoolKeys(): Generator { - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - try { - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const [poolKeys, poolKeysCount] = yield* call( [invariant, invariant.getPoolKeys], @@ -216,15 +106,7 @@ export function* fetchAllPoolKeys(): Generator { } export function* fetchAllPoolsForPairData(action: PayloadAction) { - const api = yield* call(getConnection) - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const token0 = action.payload.first.toString() const token1 = action.payload.second.toString() @@ -240,16 +122,7 @@ export function* fetchTicksAndTickMaps(action: PayloadAction @@ -281,12 +154,37 @@ export function* fetchTicksAndTickMaps(action: PayloadAction + [tokenX, tokenY].filter(token => !allTokens[token]) + ) + ) + const knownTokens = new Set( + poolsWithPoolKeys.flatMap(({ poolKey: { tokenX, tokenY } }) => + [tokenX, tokenY].filter(token => allTokens[token]) + ) + ) + + const unknownTokensData = yield* call( + getTokenDataByAddresses, + [...unknownTokens], + psp22, + walletAddress + ) + const knownTokenBalances = yield* call(getTokenBalances, [...knownTokens], psp22, walletAddress) + + yield* put(walletActions.getBalances(Object.keys(unknownTokensData))) + yield* put(actions.addTokens(unknownTokensData)) + yield* put(actions.updateTokenBalances(knownTokenBalances)) } -export function* initPoolHandler(): Generator { - yield* takeLatest(actions.initPool, handleInitPool) +export function* getPoolsDataForListHandler(): Generator { + yield* takeEvery(actions.getPoolsDataForList, fetchPoolsDataForList) } export function* getPoolDataHandler(): Generator { @@ -308,7 +206,6 @@ export function* getTicksAndTickMapsHandler(): Generator { export function* poolsSaga(): Generator { yield all( [ - initPoolHandler, getPoolDataHandler, getPoolKeysHandler, getPoolsDataForListHandler, diff --git a/src/store/sagas/positions.ts b/src/store/sagas/positions.ts index 1f4d3c42..a6c4a592 100644 --- a/src/store/sagas/positions.ts +++ b/src/store/sagas/positions.ts @@ -1,13 +1,15 @@ -import { PoolKey, TESTNET_WAZERO_ADDRESS, sendTx } from '@invariant-labs/a0-sdk' +import { Pool, Position, TESTNET_WAZERO_ADDRESS, sendTx } from '@invariant-labs/a0-sdk' import { Signer } from '@polkadot/api/types' import { PayloadAction } from '@reduxjs/toolkit' import { + EMPTY_POSITION, ErrorMessage, INVARIANT_CLAIM_FEE_OPTIONS, INVARIANT_CREATE_POOL_OPTIONS, INVARIANT_CREATE_POSITION_OPTIONS, INVARIANT_REMOVE_POSITION_OPTIONS, INVARIANT_WITHDRAW_ALL_WAZERO, + POSITIONS_PER_QUERY, PSP22_APPROVE_OPTIONS, U128MAX, WAZERO_DEPOSIT_OPTIONS @@ -20,7 +22,7 @@ import { ensureError, isErrorMessage, poolKeyToString -} from '@store/consts/utils' +} from '@utils/utils' import { FetchTicksAndTickMaps, ListType, actions as poolsActions } from '@store/reducers/pools' import { ClosePositionData, @@ -32,20 +34,18 @@ import { } from '@store/reducers/positions' import { actions as snackbarsActions } from '@store/reducers/snackbars' import { actions as walletActions } from '@store/reducers/wallet' -import { invariantAddress, networkType } from '@store/selectors/connection' +import { invariantAddress } from '@store/selectors/connection' import { poolsArraySortedByFees, tickMaps, tokens } from '@store/selectors/pools' import { address, balance } from '@store/selectors/wallet' -import invariantSingleton from '@store/services/invariantSingleton' -import psp22Singleton from '@store/services/psp22Singleton' -import wrappedAZEROSingleton from '@store/services/wrappedAZEROSingleton' import { getAlephZeroWallet } from '@utils/web3/wallet' import { closeSnackbar } from 'notistack' import { all, call, fork, join, put, select, spawn, takeEvery, takeLatest } from 'typed-redux-saga' -import { getConnection } from './connection' -import { fetchTicksAndTickMaps } from './pools' +import { fetchTicksAndTickMaps, fetchTokens } from './pools' import { fetchBalances } from './wallet' import { SubmittableExtrinsic } from '@polkadot/api/promise/types' import { calculateTokenAmountsWithSlippage } from '@invariant-labs/a0-sdk/target/utils' +import { positionsList } from '@store/selectors/positions' +import { getApi, getInvariant, getPSP22, getWrappedAZERO } from './connection' function* handleInitPosition(action: PayloadAction): Generator { const { @@ -82,15 +82,14 @@ function* handleInitPosition(action: PayloadAction): Generator }) ) - const api = yield* getConnection() - const network = yield* select(networkType) + const api = yield* getApi() const walletAddress = yield* select(address) const adapter = yield* call(getAlephZeroWallet) const invAddress = yield* select(invariantAddress) const txs = [] - const psp22 = yield* call([psp22Singleton, psp22Singleton.loadInstance], api, network) + const psp22 = yield* getPSP22() const [xAmountWithSlippage, yAmountWithSlippage] = calculateTokenAmountsWithSlippage( feeTier.tickSpacing, @@ -108,12 +107,7 @@ function* handleInitPosition(action: PayloadAction): Generator const YTokenTx = psp22.approveTx(invAddress, yAmountWithSlippage, tokenY, PSP22_APPROVE_OPTIONS) txs.push(YTokenTx) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() if (initPool) { const createPoolTx = invariant.createPoolTx( @@ -174,7 +168,9 @@ function* handleInitPosition(action: PayloadAction): Generator }) ) - yield put(actions.getPositionsList()) + const { length } = yield* select(positionsList) + const position = yield* call([invariant, invariant.getPosition], walletAddress, length) + yield* put(actions.addPosition(position)) yield* call(fetchBalances, [tokenX, tokenY]) @@ -236,8 +232,7 @@ function* handleInitPositionWithAZERO(action: PayloadAction): }) ) - const api = yield* getConnection() - const network = yield* select(networkType) + const api = yield* getApi() const walletAddress = yield* select(address) const adapter = yield* call(getAlephZeroWallet) const invAddress = yield* select(invariantAddress) @@ -245,13 +240,9 @@ function* handleInitPositionWithAZERO(action: PayloadAction): const txs = [] - const wazero = yield* call( - [wrappedAZEROSingleton, wrappedAZEROSingleton.loadInstance], - api, - network - ) + const wazero = yield* getWrappedAZERO() - const psp22 = yield* call([psp22Singleton, psp22Singleton.loadInstance], api, network) + const psp22 = yield* getPSP22() const [xAmountWithSlippage, yAmountWithSlippage] = calculateTokenAmountsWithSlippage( feeTier.tickSpacing, @@ -278,12 +269,7 @@ function* handleInitPositionWithAZERO(action: PayloadAction): const YTokenTx = psp22.approveTx(invAddress, yAmountWithSlippage, tokenY, PSP22_APPROVE_OPTIONS) txs.push(YTokenTx) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() if (initPool) { const createPoolTx = invariant.createPoolTx( @@ -369,7 +355,9 @@ function* handleInitPositionWithAZERO(action: PayloadAction): yield put(walletActions.getBalances([tokenX, tokenY])) - yield put(actions.getPositionsList()) + const { length } = yield* select(positionsList) + const position = yield* call([invariant, invariant.getPosition], walletAddress, length) + yield* put(actions.addPosition(position)) yield* call(fetchBalances, [tokenX === TESTNET_WAZERO_ADDRESS ? tokenY : tokenX]) @@ -405,57 +393,10 @@ function* handleInitPositionWithAZERO(action: PayloadAction): } } -export function* handleGetPositionsList() { - try { - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) - const walletAddress = yield* select(address) - - const positions = yield* call([invariant, invariant.getAllPositions], walletAddress) - - const pools: PoolKey[] = [] - const poolSet: Set = new Set() - for (let i = 0; i < positions.length; i++) { - const poolKeyString = poolKeyToString(positions[i].poolKey) - - if (!poolSet.has(poolKeyString)) { - poolSet.add(poolKeyString) - pools.push(positions[i].poolKey) - } - } - - yield* put( - poolsActions.getPoolsDataForList({ - poolKeys: Array.from(pools), - listType: ListType.POSITIONS - }) - ) - - yield* put(actions.setPositionsList(positions)) - } catch (e) { - yield* put(actions.setPositionsList([])) - } -} - export function* handleGetCurrentPositionTicks(action: PayloadAction) { const { poolKey, lowerTickIndex, upperTickIndex } = action.payload - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + + const invariant = yield* getInvariant() const [lowerTick, upperTick] = yield* all([ call([invariant, invariant.getTick], poolKey, lowerTickIndex), @@ -472,9 +413,6 @@ export function* handleGetCurrentPositionTicks(action: PayloadAction): Generator { const { poolKey, isXtoY, fetchTicksAndTickmap } = action.payload - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) let allTickmaps = yield* select(tickMaps) const allTokens = yield* select(tokens) const allPools = yield* select(poolsArraySortedByFees) @@ -483,12 +421,7 @@ export function* handleGetCurrentPlotTicks(action: PayloadAction = { @@ -576,16 +509,8 @@ export function* handleClaimFee(action: PayloadAction) { }) ) const walletAddress = yield* select(address) - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const adapter = yield* call(getAlephZeroWallet) @@ -674,18 +599,12 @@ export function* handleClaimFeeWithAZERO(action: PayloadAction) ) const walletAddress = yield* select(address) - const api = yield* getConnection() - const network = yield* select(networkType) + const api = yield* getApi() const invAddress = yield* select(invariantAddress) const { index, addressTokenX, addressTokenY } = action.payload - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) - const psp22 = yield* call([psp22Singleton, psp22Singleton.loadInstance], api, network) + const invariant = yield* getInvariant() + const psp22 = yield* getPSP22() const adapter = yield* call(getAlephZeroWallet) const txs = [] @@ -785,15 +704,7 @@ export function* handleClaimFeeWithAZERO(action: PayloadAction) export function* handleGetSinglePosition(action: PayloadAction) { try { const walletAddress = yield* select(address) - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const position = yield* call([invariant, invariant.getPosition], walletAddress, action.payload) yield* put( actions.setSinglePosition({ @@ -841,18 +752,21 @@ export function* handleClosePosition(action: PayloadAction) { ) const walletAddress = yield* select(address) - const api = yield* getConnection() - const network = yield* select(networkType) - const invAddress = yield* select(invariantAddress) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const invariant = yield* getInvariant() const adapter = yield* call(getAlephZeroWallet) + const allPositions = yield* select(positionsList) + const getPositionsListPagePayload: PayloadAction<{ index: number; refresh: boolean }> = { + type: actions.getPositionsListPage.type, + payload: { + index: Math.floor(Number(allPositions.length) / POSITIONS_PER_QUERY), + refresh: false + } + } + const fetchTask = yield* fork(handleGetPositionsListPage, getPositionsListPagePayload) + yield* join(fetchTask) + const tx = invariant.removePositionTx(positionIndex, INVARIANT_REMOVE_POSITION_OPTIONS) yield put( @@ -889,7 +803,7 @@ export function* handleClosePosition(action: PayloadAction) { }) ) - yield* put(actions.getPositionsList()) + yield* put(actions.removePosition(positionIndex)) onSuccess() yield* call(fetchBalances, [addressTokenX, addressTokenY]) @@ -938,18 +852,23 @@ export function* handleClosePositionWithAZERO(action: PayloadAction = { + type: actions.getPositionsListPage.type, + payload: { + index: Math.floor(Number(allPositions.length) / POSITIONS_PER_QUERY), + refresh: false + } + } + const fetchTask = yield* fork(handleGetPositionsListPage, getPositionsListPagePayload) + yield* join(fetchTask) + const txs = [] const removePositionTx = invariant.removePositionTx(positionIndex) @@ -1000,7 +919,7 @@ export function* handleClosePositionWithAZERO(action: PayloadAction isLoaded) + .map(([index]) => Number(index)), + BigInt(POSITIONS_PER_QUERY) + ) + + const allList = [...list] + for (const { index, entries } of pages) { + for (let i = 0; i < entries.length; i++) { + allList[i + index * Number(POSITIONS_PER_QUERY)] = entries[i][0] + } + } + + yield* put(actions.setPositionsList(allList)) + yield* put( + actions.setPositionsListLoadedStatus({ + indexes: pages.map(({ index }: { index: number }) => index), + isLoaded: true + }) + ) } -export function* getPositionsListHandler(): Generator { - yield* takeLatest(actions.getPositionsList, handleGetPositionsList) +export function* handleGetPositionsListPage( + action: PayloadAction<{ index: number; refresh: boolean }> +) { + const { index, refresh } = action.payload + + const walletAddress = yield* select(address) + const { length, list, loadedPages } = yield* select(positionsList) + + const invariant = yield* getInvariant() + + let entries: [Position, Pool][] = [] + let positionsLength = 0n + + if (refresh) { + yield* put( + actions.setPositionsListLoadedStatus({ + indexes: Object.keys(loadedPages) + .map(key => Number(key)) + .filter(keyIndex => keyIndex !== index), + isLoaded: false + }) + ) + } + + if (!length || refresh) { + console.log('call', index) + const result = yield* call( + [invariant, invariant.getPositions], + walletAddress, + BigInt(POSITIONS_PER_QUERY), + BigInt(index * POSITIONS_PER_QUERY) + ) + entries = result[0] + positionsLength = result[1] + + const poolsWithPoolKeys = entries.map(entry => ({ + poolKey: entry[0].poolKey, + ...entry[1] + })) + + yield* put( + poolsActions.addPoolsForList({ data: poolsWithPoolKeys, listType: ListType.POSITIONS }) + ) + yield* call(fetchTokens, poolsWithPoolKeys) + + yield* put(actions.setPositionsListLength(positionsLength)) + } + + const allList = length ? [...list] : Array(Number(positionsLength)).fill(EMPTY_POSITION) + + const isPageLoaded = loadedPages[index] + + if (!isPageLoaded || refresh) { + if (length && !refresh) { + console.log('call', index) + const result = yield* call( + [invariant, invariant.getPositions], + walletAddress, + BigInt(POSITIONS_PER_QUERY), + BigInt(index * POSITIONS_PER_QUERY) + ) + entries = result[0] + positionsLength = result[1] + + const poolsWithPoolKeys = entries.map(entry => ({ + poolKey: entry[0].poolKey, + ...entry[1] + })) + + yield* put( + poolsActions.addPoolsForList({ data: poolsWithPoolKeys, listType: ListType.POSITIONS }) + ) + yield* call(fetchTokens, poolsWithPoolKeys) + } + + for (let i = 0; i < entries.length; i++) { + allList[i + index * POSITIONS_PER_QUERY] = entries[i][0] + } + } + + yield* put(actions.setPositionsList(allList)) + yield* put(actions.setPositionsListLoadedStatus({ indexes: [index], isLoaded: true })) +} + +export function* initPositionHandler(): Generator { + yield* takeEvery(actions.initPosition, handleInitPosition) } export function* getCurrentPositionTicksHandler(): Generator { @@ -1060,16 +1091,25 @@ export function* closePositionHandler(): Generator { yield* takeEvery(actions.closePosition, handleClosePosition) } +export function* getPositionsListPage(): Generator { + yield* takeLatest(actions.getPositionsListPage, handleGetPositionsListPage) +} + +export function* getRemainingPositions(): Generator { + yield* takeLatest(actions.getRemainingPositions, handleGetRemainingPositions) +} + export function* positionsSaga(): Generator { yield all( [ initPositionHandler, - getPositionsListHandler, getCurrentPositionTicksHandler, getCurrentPlotTicksHandler, claimFeeHandler, getSinglePositionHandler, - closePositionHandler + closePositionHandler, + getPositionsListPage, + getRemainingPositions ].map(spawn) ) } diff --git a/src/store/sagas/swap.ts b/src/store/sagas/swap.ts index 959cc354..e861985a 100644 --- a/src/store/sagas/swap.ts +++ b/src/store/sagas/swap.ts @@ -30,23 +30,20 @@ import { isErrorMessage, poolKeyToString, printBigint -} from '@store/consts/utils' +} from '@utils/utils' import { actions as poolActions } from '@store/reducers/pools' import { actions as snackbarsActions } from '@store/reducers/snackbars' import { Simulate, Swap, actions } from '@store/reducers/swap' -import { invariantAddress, networkType } from '@store/selectors/connection' +import { invariantAddress } from '@store/selectors/connection' import { poolTicks, pools, tickMaps, tokens } from '@store/selectors/pools' import { simulateResult } from '@store/selectors/swap' import { address, balance } from '@store/selectors/wallet' -import invariantSingleton from '@store/services/invariantSingleton' -import psp22Singleton from '@store/services/psp22Singleton' -import wrappedAZEROSingleton from '@store/services/wrappedAZEROSingleton' import { getAlephZeroWallet } from '@utils/web3/wallet' import { closeSnackbar } from 'notistack' import { all, call, put, select, spawn, takeEvery } from 'typed-redux-saga' -import { getConnection } from './connection' import { fetchBalances } from './wallet' import { SubmittableExtrinsic } from '@polkadot/api/promise/types' +import { getApi, getInvariant, getPSP22, getWrappedAZERO } from './connection' export function* handleSwap(action: PayloadAction>): Generator { const { @@ -83,8 +80,7 @@ export function* handleSwap(action: PayloadAction>): Generato }) ) - const api = yield* getConnection() - const network = yield* select(networkType) + const api = yield* getApi() const walletAddress = yield* select(address) const adapter = yield* call(getAlephZeroWallet) const invAddress = yield* select(invariantAddress) @@ -95,13 +91,8 @@ export function* handleSwap(action: PayloadAction>): Generato const txs = [] - const psp22 = yield* call([psp22Singleton, psp22Singleton.loadInstance], api, network) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const psp22 = yield* getPSP22() + const invariant = yield* getInvariant() const sqrtPriceLimit = calculateSqrtPriceAfterSlippage(estimatedPriceAfterSwap, slippage, !xToY) const calculatedAmountIn = slippage ? calculateAmountInWithSlippage(amountOut, sqrtPriceLimit, xToY, poolKey.feeTier.fee) @@ -252,8 +243,7 @@ export function* handleSwapWithAZERO(action: PayloadAction>): }) ) - const api = yield* getConnection() - const network = yield* select(networkType) + const api = yield* getApi() const walletAddress = yield* select(address) const adapter = yield* call(getAlephZeroWallet) const swapSimulateResult = yield* select(simulateResult) @@ -265,18 +255,9 @@ export function* handleSwapWithAZERO(action: PayloadAction>): const txs = [] - const wazero = yield* call( - [wrappedAZEROSingleton, wrappedAZEROSingleton.loadInstance], - api, - network - ) - const psp22 = yield* call([psp22Singleton, psp22Singleton.loadInstance], api, network) - const invariant = yield* call( - [invariantSingleton, invariantSingleton.loadInstance], - api, - network, - invAddress - ) + const wazero = yield* getWrappedAZERO() + const psp22 = yield* getPSP22() + const invariant = yield* getInvariant() const sqrtPriceLimit = calculateSqrtPriceAfterSlippage(estimatedPriceAfterSwap, slippage, !xToY) const calculatedAmountIn = slippage ? calculateAmountInWithSlippage(amountOut, sqrtPriceLimit, xToY, poolKey.feeTier.fee) @@ -454,6 +435,7 @@ export function* handleGetSimulateResult(action: PayloadAction) { amountOut: 0n, priceImpact: 0, targetSqrtPrice: 0n, + fee: 0n, errors: [SwapError.AmountIsZero] }) ) @@ -472,6 +454,7 @@ export function* handleGetSimulateResult(action: PayloadAction) { amountOut: 0n, priceImpact: 0, targetSqrtPrice: 0n, + fee: 0n, errors: [SwapError.NoRouteFound] }) ) @@ -483,6 +466,7 @@ export function* handleGetSimulateResult(action: PayloadAction) { let insufficientLiquidityAmountOut = byAmountIn ? 0n : U128MAX let priceImpact = 0 let targetSqrtPrice = 0n + let fee = 0n const errors = [] for (const pool of filteredPools) { @@ -507,9 +491,9 @@ export function* handleGetSimulateResult(action: PayloadAction) { : result.amountIn < insufficientLiquidityAmountOut ) { insufficientLiquidityAmountOut = byAmountIn ? result.amountOut : result.amountIn + fee = pool.poolKey.feeTier.fee + errors.push(SwapError.InsufficientLiquidity) } - - errors.push(SwapError.InsufficientLiquidity) continue } @@ -548,6 +532,7 @@ export function* handleGetSimulateResult(action: PayloadAction) { amountOut: amountOut ? amountOut : insufficientLiquidityAmountOut, priceImpact, targetSqrtPrice, + fee, errors }) ) diff --git a/src/store/sagas/wallet.ts b/src/store/sagas/wallet.ts index 4122d958..68c12994 100644 --- a/src/store/sagas/wallet.ts +++ b/src/store/sagas/wallet.ts @@ -1,15 +1,13 @@ -import { Network, sendTx } from '@invariant-labs/a0-sdk' +import { sendTx } from '@invariant-labs/a0-sdk' import { NightlyConnectAdapter } from '@nightlylabs/wallet-selector-polkadot' import { PayloadAction } from '@reduxjs/toolkit' import { FaucetTokenList, TokenAirdropAmount } from '@store/consts/static' -import { createLoaderKey, getTokenBalances } from '@store/consts/utils' +import { createLoaderKey, getTokenBalances } from '@utils/utils' import { actions as positionsActions } from '@store/reducers/positions' import { actions as snackbarsActions } from '@store/reducers/snackbars' import { Status, actions, actions as walletActions } from '@store/reducers/wallet' -import { networkType } from '@store/selectors/connection' import { tokens } from '@store/selectors/pools' import { address, status } from '@store/selectors/wallet' -import psp22Singleton from '@store/services/psp22Singleton' import { disconnectWallet, getAlephZeroWallet } from '@utils/web3/wallet' import { closeSnackbar } from 'notistack' import { @@ -22,8 +20,9 @@ import { takeLatest, takeLeading } from 'typed-redux-saga' -import { getConnection } from './connection' import { Signer } from '@polkadot/api/types' +import { positionsList } from '@store/selectors/positions' +import { getApi, getPSP22 } from './connection' export function* getWallet(): SagaGenerator { const wallet = yield* call(getAlephZeroWallet) @@ -43,7 +42,7 @@ type FrameSystemAccountInfo = { sufficients: number } export function* getBalance(walletAddress: string): SagaGenerator { - const connection = yield* call(getConnection) + const connection = yield* getApi() const accountInfoResponse = yield* call( [connection.query.system.account, connection.query.system.account], walletAddress @@ -80,14 +79,10 @@ export function* handleAirdrop(): Generator { }) ) - const connection = yield* getConnection() + const connection = yield* getApi() const adapter = yield* call(getAlephZeroWallet) - const psp22 = yield* call( - [psp22Singleton, psp22Singleton.loadInstance], - connection, - Network.Testnet - ) + const psp22 = yield* getPSP22() const txs = [] @@ -184,10 +179,20 @@ export function* handleConnect(): Generator { export function* handleDisconnect(): Generator { try { + const { loadedPages } = yield* select(positionsList) + yield* call(disconnectWallet) yield* put(actions.resetState()) yield* put(positionsActions.setPositionsList([])) + yield* put(positionsActions.setPositionsListLength(0n)) + yield* put( + positionsActions.setPositionsListLoadedStatus({ + indexes: Object.keys(loadedPages).map(key => Number(key)), + isLoaded: false + }) + ) + yield* put( positionsActions.setCurrentPositionTicks({ lowerTick: undefined, @@ -201,15 +206,14 @@ export function* handleDisconnect(): Generator { export function* fetchBalances(tokens: string[]): Generator { const walletAddress = yield* select(address) - const api = yield* getConnection() - const network = yield* select(networkType) + const psp22 = yield* getPSP22() yield* put(walletActions.setIsBalanceLoading(true)) const balance = yield* call(getBalance, walletAddress) yield* put(walletActions.setBalance(BigInt(balance))) - const tokenBalances = yield* call(getTokenBalances, tokens, api, network, walletAddress) + const tokenBalances = yield* call(getTokenBalances, tokens, psp22, walletAddress) yield* put( walletActions.addTokenBalances( tokenBalances.map(([address, balance]) => { diff --git a/src/store/selectors/connection.ts b/src/store/selectors/connection.ts index e9152a51..3f2d80da 100644 --- a/src/store/selectors/connection.ts +++ b/src/store/selectors/connection.ts @@ -3,17 +3,29 @@ import { AnyProps, keySelectors } from './helpers' const store = (s: AnyProps) => s[connectionSliceName] as IAlephZeroConnectionStore -export const { networkType, status, blockNumber, rpcAddress, invariantAddress } = keySelectors( - store, - ['networkType', 'status', 'blockNumber', 'rpcAddress', 'invariantAddress'] -) +export const { + networkType, + status, + blockNumber, + rpcAddress, + invariantAddress, + wrappedAZEROAddress +} = keySelectors(store, [ + 'networkType', + 'status', + 'blockNumber', + 'rpcAddress', + 'invariantAddress', + 'wrappedAZEROAddress' +]) export const alephZeroConnectionSelectors = { networkType, status, blockNumber, rpcAddress, - invariantAddress + invariantAddress, + wrappedAZEROAddress } export default alephZeroConnectionSelectors diff --git a/src/store/selectors/positions.ts b/src/store/selectors/positions.ts index 7122633a..0e392a06 100644 --- a/src/store/selectors/positions.ts +++ b/src/store/selectors/positions.ts @@ -1,5 +1,5 @@ import { Position } from '@invariant-labs/a0-sdk' -import { poolKeyToString } from '@store/consts/utils' +import { poolKeyToString } from '@utils/utils' import { PoolWithPoolKey } from '@store/reducers/pools' import { createSelector } from 'reselect' import { IPositionsStore, positionsSliceName } from '../reducers/positions' @@ -54,22 +54,30 @@ export const positionsWithPoolsData = createSelector( } }) - return list.map((position, index) => { + const result = [] + for (let index = 0; index < list.length; index++) { + const position = list[index] const tokenX = tokens.find(token => token.assetAddress === position.poolKey.tokenX) const tokenY = tokens.find(token => token.assetAddress === position.poolKey.tokenY) - if (!tokenX || !tokenY) { - throw new Error(`Token not found for position: ${position}`) + if (!tokenX) { + console.log(`Token ${position.poolKey.tokenX} not found for position`) + continue + } else if (!tokenY) { + console.log(`Token ${position.poolKey.tokenY} not found for position`) + continue } - return { + result.push({ ...position, poolData: poolsByKey[poolKeyToString(position.poolKey)], tokenX, tokenY, positionIndex: index - } - }) + }) + } + + return result } ) diff --git a/src/store/services/apiSingleton.ts b/src/store/services/apiSingleton.ts index 6785c8aa..e7c67ebf 100644 --- a/src/store/services/apiSingleton.ts +++ b/src/store/services/apiSingleton.ts @@ -1,33 +1,24 @@ import { Network, initPolkadotApi } from '@invariant-labs/a0-sdk' import { ApiPromise } from '@polkadot/api' -class SingletonAPI { - private static instance: SingletonAPI - private api: ApiPromise | null = null - private currentRpc: string | null = null - private currentNetwork: Network | null = null +class SingletonApi { + static api: ApiPromise | null = null + static rpc: string | null = null + static network: Network | null = null - private constructor() {} - - public static getInstance(): SingletonAPI { - if (!SingletonAPI.instance) { - SingletonAPI.instance = new SingletonAPI() - } - return SingletonAPI.instance + static getInstance(): ApiPromise | null { + return this.api } - public async loadInstance(network: Network, rpc: string): Promise { - if (!this.api || this.currentRpc !== rpc || this.currentNetwork !== network) { - const newApi = await initPolkadotApi(network, rpc) - this.currentRpc = rpc - this.currentNetwork = network - this.api = newApi - - return newApi + static async loadInstance(network: Network, rpc: string): Promise { + if (!this.api || network !== this.network || rpc !== this.rpc) { + this.api = await initPolkadotApi(network, rpc) + this.network = network + this.rpc = rpc } return this.api } } -export default SingletonAPI.getInstance() +export default SingletonApi diff --git a/src/store/services/invariantSingleton.ts b/src/store/services/invariantSingleton.ts index 19237a60..ede065f5 100644 --- a/src/store/services/invariantSingleton.ts +++ b/src/store/services/invariantSingleton.ts @@ -3,37 +3,32 @@ import { ApiPromise } from '@polkadot/api' import { DEFAULT_INVARIANT_OPTIONS } from '@store/consts/static' class SingletonInvariant { - private static instance: SingletonInvariant - private invariant: Invariant | null = null - private currentApi: ApiPromise | null = null - private currentNetwork: Network | null = null + static invariant: Invariant | null = null + static api: ApiPromise | null = null + static network: Network | null = null - private constructor() {} - - public static getInstance(): SingletonInvariant { - if (!SingletonInvariant.instance) { - SingletonInvariant.instance = new SingletonInvariant() - } - return SingletonInvariant.instance + static getInstance(): Invariant | null { + return this.invariant } - public async loadInstance( + static async loadInstance( api: ApiPromise, network: Network, address: string ): Promise { if ( !this.invariant || - this.currentApi !== api || - this.currentNetwork !== network || + api !== this.api || + network !== this.network || address !== this.invariant.contract.address.toString() ) { this.invariant = await Invariant.load(api, network, address, DEFAULT_INVARIANT_OPTIONS) - this.currentApi = api - this.currentNetwork = network + this.api = api + this.network = network } + return this.invariant } } -export default SingletonInvariant.getInstance() +export default SingletonInvariant diff --git a/src/store/services/psp22Singleton.ts b/src/store/services/psp22Singleton.ts index 986c0701..2520ca3c 100644 --- a/src/store/services/psp22Singleton.ts +++ b/src/store/services/psp22Singleton.ts @@ -3,28 +3,23 @@ import { ApiPromise } from '@polkadot/api' import { DEFAULT_PSP22_OPTIONS } from '@store/consts/static' class SingletonPSP22 { - private static instance: SingletonPSP22 - private psp22: PSP22 | null = null - private currentApi: ApiPromise | null = null - private currentNetwork: Network | null = null + static psp22: PSP22 | null = null + static api: ApiPromise | null = null + static network: Network | null = null - private constructor() {} - - public static getInstance(): SingletonPSP22 { - if (!SingletonPSP22.instance) { - SingletonPSP22.instance = new SingletonPSP22() - } - return SingletonPSP22.instance + static getInstance(): PSP22 | null { + return this.psp22 } - public async loadInstance(api: ApiPromise, network: Network): Promise { - if (!this.psp22 || this.currentApi !== api || this.currentNetwork !== network) { + static async loadInstance(api: ApiPromise, network: Network): Promise { + if (!this.psp22 || api !== this.api || network !== this.network) { this.psp22 = await PSP22.load(api, network, DEFAULT_PSP22_OPTIONS) - this.currentApi = api - this.currentNetwork = network + this.api = api + this.network = network } + return this.psp22 } } -export default SingletonPSP22.getInstance() +export default SingletonPSP22 diff --git a/src/store/services/wrappedAZEROSingleton.ts b/src/store/services/wrappedAZEROSingleton.ts index f2139296..9b9299c7 100644 --- a/src/store/services/wrappedAZEROSingleton.ts +++ b/src/store/services/wrappedAZEROSingleton.ts @@ -3,33 +3,37 @@ import { ApiPromise } from '@polkadot/api' import { DEFAULT_WAZERO_OPTIONS } from '@store/consts/static' class SingletonWrappedAZERO { - private static instance: SingletonWrappedAZERO - private wrappedAZERO: WrappedAZERO | null = null - private currentApi: ApiPromise | null = null - private currentNetwork: Network | null = null + static wrappedAZERO: WrappedAZERO | null = null + static api: ApiPromise | null = null + static network: Network | null = null - private constructor() {} - - public static getInstance(): SingletonWrappedAZERO { - if (!SingletonWrappedAZERO.instance) { - SingletonWrappedAZERO.instance = new SingletonWrappedAZERO() - } - return SingletonWrappedAZERO.instance + static getInstance(): WrappedAZERO | null { + return this.wrappedAZERO } - public async loadInstance(api: ApiPromise, network: Network): Promise { - if (!this.wrappedAZERO || this.currentApi !== api || this.currentNetwork !== network) { + static async loadInstance( + api: ApiPromise, + network: Network, + address: string + ): Promise { + if ( + !this.wrappedAZERO || + api !== this.api || + network !== this.network || + address !== this.wrappedAZERO.contract.address.toString() + ) { this.wrappedAZERO = await WrappedAZERO.load( api, network, TESTNET_WAZERO_ADDRESS, DEFAULT_WAZERO_OPTIONS ) - this.currentApi = api - this.currentNetwork = network + this.api = api + this.network = network } + return this.wrappedAZERO } } -export default SingletonWrappedAZERO.getInstance() +export default SingletonWrappedAZERO diff --git a/src/store/consts/utils.ts b/src/utils/utils.ts similarity index 85% rename from src/store/consts/utils.ts rename to src/utils/utils.ts index c9c777ed..bb7ac821 100644 --- a/src/store/consts/utils.ts +++ b/src/utils/utils.ts @@ -2,6 +2,7 @@ import { Invariant, LiquidityTick, Network, + PSP22, PoolKey, Tick, Tickmap, @@ -17,18 +18,19 @@ import { PERCENTAGE_DENOMINATOR, PERCENTAGE_SCALE, PRICE_SCALE, + SQRT_PRICE_SCALE, TESTNET_BTC_ADDRESS, TESTNET_ETH_ADDRESS, TESTNET_USDC_ADDRESS, TESTNET_WAZERO_ADDRESS } from '@invariant-labs/a0-sdk/target/consts' -import { calculateLiquidityBreakpoints } from '@invariant-labs/a0-sdk/target/utils' -import { ApiPromise, Keyring } from '@polkadot/api' +import { + calculateLiquidityBreakpoints, + priceToSqrtPrice +} from '@invariant-labs/a0-sdk/target/utils' +import { Keyring } from '@polkadot/api' import { PoolWithPoolKey } from '@store/reducers/pools' import { PlotTickData } from '@store/reducers/positions' -import apiSingleton from '@store/services/apiSingleton' -import invariantSingleton from '@store/services/invariantSingleton' -import psp22Singleton from '@store/services/psp22Singleton' import axios from 'axios' import { BTC, @@ -39,6 +41,8 @@ import { FAUCET_DEPLOYER_MNEMONIC, FormatConfig, LIQUIDITY_PLOT_DECIMAL, + POSITIONS_PER_PAGE, + POSITIONS_PER_QUERY, PositionTokenBlock, STABLECOIN_ADDRESSES, USDC, @@ -48,7 +52,7 @@ import { reversedAddressTickerMap, subNumbers, tokensPrices -} from './static' +} from '@store/consts/static' import { sleep } from '@store/sagas/wallet' import { BestTier, @@ -57,7 +61,8 @@ import { PrefixConfig, Token, TokenPriceData -} from './types' +} from '@store/consts/types' +import icons from '@static/icons' export const createLoaderKey = () => (new Date().getMilliseconds() + Math.random()).toString() @@ -180,7 +185,7 @@ export const toMaxNumericPlaces = (num: number, places: number): string => { return num.toFixed(places + Math.abs(log) - 1) } -export const calcPrice = ( +export const calcPriceByTickIndex = ( amountTickIndex: bigint, isXtoY: boolean, xDecimal: bigint, @@ -195,6 +200,16 @@ export const calcPrice = ( return price === 0 ? Number.MAX_SAFE_INTEGER : 1 / price } +export const calcPriceBySqrtPrice = ( + sqrtPrice: bigint, + isXtoY: boolean, + xDecimal: bigint, + yDecimal: bigint +): number => { + const price = calcYPerXPriceBySqrtPrice(sqrtPrice, xDecimal, yDecimal) ** (isXtoY ? 1 : -1) + + return price +} export const createPlaceholderLiquidityPlot = ( isXtoY: boolean, yValueToFill: number, @@ -207,7 +222,7 @@ export const createPlaceholderLiquidityPlot = ( const min = getMinTick(tickSpacing) const max = getMaxTick(tickSpacing) - const minPrice = calcPrice(min, isXtoY, tokenXDecimal, tokenYDecimal) + const minPrice = calcPriceByTickIndex(min, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: minPrice, @@ -215,7 +230,7 @@ export const createPlaceholderLiquidityPlot = ( index: min }) - const maxPrice = calcPrice(max, isXtoY, tokenXDecimal, tokenYDecimal) + const maxPrice = calcPriceByTickIndex(max, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: maxPrice, @@ -345,12 +360,9 @@ export const parseFeeToPathFee = (fee: bigint): string => { export const getTokenDataByAddresses = async ( tokens: string[], - api: ApiPromise, - network: Network, + psp22: PSP22, address: string ): Promise> => { - const psp22 = await psp22Singleton.loadInstance(api, network) - const promises = tokens.flatMap(token => { return [ psp22.tokenSymbol(token), @@ -370,7 +382,7 @@ export const getTokenDataByAddresses = async ( name: results[baseIndex + 1] ? (results[baseIndex + 1] as string) : '', decimals: results[baseIndex + 2] as bigint, balance: results[baseIndex + 3] as bigint, - logoURI: '/unknownToken.svg', + logoURI: icons.unknownToken, isUnknown: true } }) @@ -379,12 +391,9 @@ export const getTokenDataByAddresses = async ( export const getTokenBalances = async ( tokens: string[], - api: ApiPromise, - network: Network, + psp22: PSP22, address: string ): Promise<[string, bigint][]> => { - const psp22 = await psp22Singleton.loadInstance(api, network) - const promises: Promise[] = [] tokens.map(token => { promises.push(psp22.balanceOf(address, token)) @@ -399,13 +408,9 @@ export const getTokenBalances = async ( } export const getPoolsByPoolKeys = async ( - invariantAddress: string, - poolKeys: PoolKey[], - api: ApiPromise, - network: Network + invariant: Invariant, + poolKeys: PoolKey[] ): Promise => { - const invariant = await invariantSingleton.loadInstance(api, network, invariantAddress) - const promises = poolKeys.map(({ tokenX, tokenY, feeTier }) => invariant.getPool(tokenX, tokenY, feeTier) ) @@ -504,6 +509,52 @@ export const nearestSpacingMultiplicity = (centerTick: number, spacing: number) ) } +export const calculateSqrtPriceFromBalance = ( + price: number, + spacing: bigint, + isXtoY: boolean, + xDecimal: bigint, + yDecimal: bigint +) => { + const minTick = getMinTick(spacing) + const maxTick = getMaxTick(spacing) + + const basePrice = Math.min( + Math.max( + price, + Number(calcPriceByTickIndex(isXtoY ? minTick : maxTick, isXtoY, xDecimal, yDecimal)) + ), + Number(calcPriceByTickIndex(isXtoY ? maxTick : minTick, isXtoY, xDecimal, yDecimal)) + ) + + const primaryUnitsPrice = getPrimaryUnitsPrice( + basePrice, + isXtoY, + Number(xDecimal), + Number(yDecimal) + ) + + const parsedPrimaryUnits = + primaryUnitsPrice > 1 && Number.isInteger(primaryUnitsPrice) + ? primaryUnitsPrice.toString() + : primaryUnitsPrice.toFixed(24) + + const bigintPrice = convertBalanceToBigint(parsedPrimaryUnits, SQRT_PRICE_SCALE) + const sqrtPrice = priceToSqrtPrice(bigintPrice) + + const minSqrtPrice = calculateSqrtPrice(minTick) + const maxSqrtPrice = calculateSqrtPrice(maxTick) + + let validatedSqrtPrice = sqrtPrice + if (sqrtPrice < minSqrtPrice) { + validatedSqrtPrice = minSqrtPrice + } else if (sqrtPrice > maxSqrtPrice) { + validatedSqrtPrice = maxSqrtPrice + } + + return validatedSqrtPrice +} + export const calculateTickFromBalance = ( price: number, spacing: bigint, @@ -516,7 +567,7 @@ export const calculateTickFromBalance = ( const basePrice = Math.max( price, - Number(calcPrice(isXtoY ? minTick : maxTick, isXtoY, xDecimal, yDecimal)) + Number(calcPriceByTickIndex(isXtoY ? minTick : maxTick, isXtoY, xDecimal, yDecimal)) ) const primaryUnitsPrice = getPrimaryUnitsPrice( basePrice, @@ -727,7 +778,7 @@ export const createLiquidityPlot = ( const max = getMaxTick(tickSpacing) if (!ticks.length || ticks[0].index > min) { - const minPrice = calcPrice(min, isXtoY, tokenXDecimal, tokenYDecimal) + const minPrice = calcPriceByTickIndex(min, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: minPrice, @@ -738,14 +789,24 @@ export const createLiquidityPlot = ( ticks.forEach((tick, i) => { if (i === 0 && tick.index - tickSpacing > min) { - const price = calcPrice(tick.index - tickSpacing, isXtoY, tokenXDecimal, tokenYDecimal) + const price = calcPriceByTickIndex( + tick.index - tickSpacing, + isXtoY, + tokenXDecimal, + tokenYDecimal + ) ticksData.push({ x: price, y: 0, index: tick.index - tickSpacing }) } else if (i > 0 && tick.index - tickSpacing > ticks[i - 1].index) { - const price = calcPrice(tick.index - tickSpacing, isXtoY, tokenXDecimal, tokenYDecimal) + const price = calcPriceByTickIndex( + tick.index - tickSpacing, + isXtoY, + tokenXDecimal, + tokenYDecimal + ) ticksData.push({ x: price, y: +printBigint(ticks[i - 1].liqudity, LIQUIDITY_PLOT_DECIMAL), @@ -753,7 +814,7 @@ export const createLiquidityPlot = ( }) } - const price = calcPrice(tick.index, isXtoY, tokenXDecimal, tokenYDecimal) + const price = calcPriceByTickIndex(tick.index, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: price, y: +printBigint(ticks[i].liqudity, LIQUIDITY_PLOT_DECIMAL), @@ -762,7 +823,7 @@ export const createLiquidityPlot = ( }) const lastTick = ticks[ticks.length - 1].index if (!ticks.length) { - const maxPrice = calcPrice(max, isXtoY, tokenXDecimal, tokenYDecimal) + const maxPrice = calcPriceByTickIndex(max, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: maxPrice, @@ -771,7 +832,12 @@ export const createLiquidityPlot = ( }) } else if (lastTick < max) { if (max - lastTick > tickSpacing) { - const price = calcPrice(lastTick + tickSpacing, isXtoY, tokenXDecimal, tokenYDecimal) + const price = calcPriceByTickIndex( + lastTick + tickSpacing, + isXtoY, + tokenXDecimal, + tokenYDecimal + ) ticksData.push({ x: price, y: 0, @@ -779,7 +845,7 @@ export const createLiquidityPlot = ( }) } - const maxPrice = calcPrice(max, isXtoY, tokenXDecimal, tokenYDecimal) + const maxPrice = calcPriceByTickIndex(max, isXtoY, tokenXDecimal, tokenYDecimal) ticksData.push({ x: maxPrice, @@ -831,8 +897,12 @@ export const formatNumber = (number: number | bigint | string): string => { FormatConfig.DecimalsAfterDot ) + 'K' + } else if (afterDot && countLeadingZeros(afterDot) <= 3) { + const roundedNumber = numberAsNumber.toFixed(countLeadingZeros(afterDot) + 4).slice(0, -1) + formattedNumber = trimZeros(roundedNumber) } else { const leadingZeros = afterDot ? countLeadingZeros(afterDot) : 0 + const parsedAfterDot = String(parseInt(afterDot)).length > 3 ? String(parseInt(afterDot)).slice(0, 3) : afterDot formattedNumber = trimZeros( @@ -872,13 +942,10 @@ export const isErrorMessage = (message: string): boolean => { export const getNewTokenOrThrow = async ( address: string, - network: Network, - rpc: string, + psp22: PSP22, walletAddress: string ): Promise> => { - const api = await apiSingleton.loadInstance(network, rpc) - - const tokenData = await getTokenDataByAddresses([address], api, network, walletAddress) + const tokenData = await getTokenDataByAddresses([address], psp22, walletAddress) if (tokenData) { return tokenData @@ -1015,3 +1082,38 @@ export function testnetBestTiersCreator() { return bestTiers } + +export const positionListPageToQueryPage = (page: number): number => { + return Math.max(Math.ceil((page * POSITIONS_PER_PAGE) / POSITIONS_PER_QUERY) - 1, 0) +} + +export const validConcentrationMidPriceTick = ( + midPriceTick: bigint, + isXtoY: boolean, + tickSpacing: bigint +) => { + const minTick = getMinTick(tickSpacing) + const maxTick = getMaxTick(tickSpacing) + + const parsedTickSpacing = Number(tickSpacing) + const tickDelta = BigInt(calculateTickDelta(parsedTickSpacing, 2, 2)) + + const minTickLimit = minTick + (2n + tickDelta) * tickSpacing + const maxTickLimit = maxTick - (2n + tickDelta) * tickSpacing + + if (isXtoY) { + if (midPriceTick < minTickLimit) { + return minTickLimit + } else if (midPriceTick > maxTickLimit) { + return maxTickLimit + } + } else { + if (midPriceTick > maxTickLimit) { + return maxTickLimit + } else if (midPriceTick < minTickLimit) { + return minTickLimit + } + } + + return midPriceTick +}