diff --git a/Jenkinsfile b/Jenkinsfile index 1e42ffb4f..da3bdcfe7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -34,7 +34,7 @@ def pipeline = new org.js.AppPipeline(steps: this, noIndex: true, sonarSrcPath: 'src', sonarTestsPath: 'tests', - dojoProductType: 'sora', + dojoProductType: 'polkaswap', movingFiles: [ "*":"./", ".well-known/":"./"] ) pipeline.runPipeline() diff --git a/env.json b/env.json index 82b24c258..df6b2593e 100644 --- a/env.json +++ b/env.json @@ -12,7 +12,8 @@ "moonpay": true, "x1ex": false, "charts": true, - "soraCard": false + "soraCard": false, + "orderBook": false }, "SUBQUERY_ENDPOINT": "https://api.subquery.network/sq/sora-xor/sora-prod-sub4", "SUBSQUID_ENDPOINT": "", diff --git a/package.json b/package.json index 6f69df475..01070ec9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polkaswap-exchange-web", - "version": "1.28.3", + "version": "1.29.0", "repository": { "type": "git", "url": "https://github.com/sora-xor/polkaswap-exchange-web.git" @@ -24,14 +24,14 @@ }, "dependencies": { "@metamask/detect-provider": "^2.0.0", - "@soramitsu/soraneo-wallet-web": "1.28.0", + "@soramitsu/soraneo-wallet-web": "1.29.4", "@walletconnect/ethereum-provider": "^2.11.0", "@walletconnect/modal": "^2.6.2", "core-js": "^3.33.2", "country-code-emoji": "^2.3.0", "country-flag-emoji-polyfill": "^0.1.4", "direct-vuex": "^0.12.1", - "echarts": "^5.4.1", + "echarts": "^5.4.3", "email-validator": "^2.0.4", "ethers": "6.8.0", "jwt-decode": "^3.1.2", @@ -40,10 +40,10 @@ "vue": "2.7.14", "vue-class-component": "^7.2.6", "vue-echarts": "^6.3.3", - "vue-i18n": "^8.11.2", + "vue-i18n": "^8.28.2", "vue-property-decorator": "^9.1.2", "vue-router": "^3.6.5", - "vuex": "^3.1.3" + "vuex": "^3.6.2" }, "devDependencies": { "@babel/runtime": "^7.23.2", @@ -65,29 +65,29 @@ "@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-standard": "^6.1.0", "@vue/eslint-config-typescript": "^8.0.0", - "@vue/test-utils": "^1.2.2", - "@vue/vue2-jest": "^27.0.0-alpha.2", + "@vue/test-utils": "^1.3.6", + "@vue/vue2-jest": "^27.0.0", "babel-plugin-require-context-hook": "^1.0.0", "css-unicode-loader": "^1.0.3", "electron": "^13.0.0", - "electron-devtools-installer": "^3.1.0", - "eslint": "^7.29.0", + "electron-devtools-installer": "^3.2.0", + "eslint": "^7.32.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^5.2.0", "eslint-plugin-standard": "^5.0.0", "eslint-plugin-vue": "^7.20.0", - "jest": "^27.2.2", - "jsdom": "^16.6.0", + "jest": "^27.5.1", + "jsdom": "^16.7.0", "lint-staged": "^9.5.0", "node-polyfill-webpack-plugin": "^2.0.1", - "prettier": "^2.2.1", + "prettier": "^2.8.8", "sass": "^1.66.1", "sass-loader": "^13.3.2", - "ts-jest": "^27.0.5", + "ts-jest": "^27.1.5", "typescript": "~5.2.2", - "vue-cli-plugin-electron-builder": "^3.0.0-alpha.3", + "vue-cli-plugin-electron-builder": "^3.0.0-alpha.4", "vue-cli-plugin-test-attrs": "^0.1.5", "vue-template-compiler": "2.7.14" }, diff --git a/public/env.json b/public/env.json index c5029c8dc..7386257fd 100644 --- a/public/env.json +++ b/public/env.json @@ -12,7 +12,8 @@ "moonpay": true, "x1ex": false, "charts": true, - "soraCard": true + "soraCard": false, + "orderBook": true }, "FAUCET_URL": "https://faucet.dev.sora2.tachi.soramitsu.co.jp/", "DEFAULT_NETWORKS": [ @@ -34,7 +35,7 @@ "address": "wss://ws.framenode-3.r0.dev.sora2.soramitsu.co.jp" } ], - "SUBQUERY_ENDPOINT": "https://subquery.sq1.dev.sora2.soramitsu.co.jp", + "SUBQUERY_ENDPOINT": "https://api.subquery.network/sq/sora-xor/sora-dev", "SUBSQUID_ENDPOINT": "https://squid.subsquid.io/sora-dev/v/v1/graphql", "NETWORK_TYPE": "Dev", "CHAIN_GENESIS_HASH": "", diff --git a/public/marketing.json b/public/marketing.json index 96351c738..95504f8fa 100644 --- a/public/marketing.json +++ b/public/marketing.json @@ -1,9 +1,4 @@ [ - { - "title": "ROADMAP 2024 SURVEY", - "img": "/marketing/survey.png", - "link": "https://form.typeform.com/to/Mb6p2Kpy" - }, { "title": "GET SORA CARD", "img": "/marketing/card.png", diff --git a/public/marketing/survey.png b/public/marketing/survey.png deleted file mode 100644 index 1c3784620..000000000 Binary files a/public/marketing/survey.png and /dev/null differ diff --git a/src/App.vue b/src/App.vue index a9e1dcfc9..493844371 100644 --- a/src/App.vue +++ b/src/App.vue @@ -66,8 +66,8 @@ import type { ConnectToNodeOptions, Node } from './types/nodes'; import type { History, HistoryItem } from '@sora-substrate/util'; import type { WhitelistArrayItem } from '@sora-substrate/util/build/assets/types'; import type { EvmNetwork } from '@sora-substrate/util/build/bridgeProxy/evm/types'; -import type DesignSystem from '@soramitsu/soramitsu-js-ui/lib/types/DesignSystem'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type DesignSystem from '@soramitsu-ui/ui-vue2/lib/types/DesignSystem'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component({ components: { @@ -392,6 +392,8 @@ html { font-size: var(--s-font-size-small); line-height: var(--s-line-height-base); letter-spacing: var(--s-letter-spacing-small); + background-color: var(--s-color-utility-body); + scrollbar-color: transparent transparent; } ul ul { @@ -550,6 +552,18 @@ i.icon-divider { @include icon-styles; } +.app-main--orderbook { + .app-menu { + position: absolute; + right: initial; + } + + .app-content { + display: flex; + justify-content: center; + } +} + @include desktop { .app-main { &.app-main--swap.app-main--has-charts { diff --git a/src/assets/fonts/polkaswap_icons.ttf b/src/assets/fonts/polkaswap_icons.ttf index 59aa1dc04..fe862b8c1 100644 Binary files a/src/assets/fonts/polkaswap_icons.ttf and b/src/assets/fonts/polkaswap_icons.ttf differ diff --git a/src/components/App/Footer/AppFooter.vue b/src/components/App/Footer/AppFooter.vue index ed1af754d..6f916ece8 100644 --- a/src/components/App/Footer/AppFooter.vue +++ b/src/components/App/Footer/AppFooter.vue @@ -65,8 +65,8 @@ @@ -204,6 +211,10 @@ export default class AppMenu extends Mixins(TranslationMixin) { } } + .collapse-button { + pointer-events: none; + } + &:hover, &:focus { box-shadow: 20px 20px 60px 0px #0000001a; @@ -213,6 +224,10 @@ export default class AppMenu extends Mixins(TranslationMixin) { display: initial; } } + + .collapse-button { + pointer-events: all; + } } } } @@ -286,9 +301,6 @@ export default class AppMenu extends Mixins(TranslationMixin) { &:focus { background-color: unset !important; } - i.el-icon-bank-card { - width: 28px; // to avoid issue with paddings - } } } @@ -346,6 +358,7 @@ export default class AppMenu extends Mixins(TranslationMixin) { @include large-mobile(true) { position: fixed; + right: 0; &.visible { visibility: visible; @@ -368,10 +381,6 @@ export default class AppMenu extends Mixins(TranslationMixin) { } } - @include large-mobile(true) { - right: 0; - } - @include large-mobile { visibility: visible; position: relative; @@ -409,6 +418,8 @@ export default class AppMenu extends Mixins(TranslationMixin) { flex: 1; flex-flow: column nowrap; justify-content: space-between; + max-width: $sidebar-max-width; + padding-right: $inner-spacing-mini; // for shadow } } } @@ -480,4 +491,3 @@ export default class AppMenu extends Mixins(TranslationMixin) { } } -@/modules/staking/demeter/consts diff --git a/src/components/mixins/ChartSpecMixin.ts b/src/components/mixins/ChartSpecMixin.ts index a673a0af0..7c9121aa8 100644 --- a/src/components/mixins/ChartSpecMixin.ts +++ b/src/components/mixins/ChartSpecMixin.ts @@ -18,7 +18,6 @@ const AXIS_LABEL_CSS = { export default class ChartSpecMixin extends Mixins(ThemePaletteMixin, TranslationMixin) { gridSpec(options: any = {}) { return merge({ - top: 20, left: 0, right: 0, bottom: 20 + AXIS_OFFSET, diff --git a/src/components/mixins/ExplorePageMixin.ts b/src/components/mixins/ExplorePageMixin.ts index 3ade24223..236874e65 100644 --- a/src/components/mixins/ExplorePageMixin.ts +++ b/src/components/mixins/ExplorePageMixin.ts @@ -1,23 +1,16 @@ -import SScrollbar from '@soramitsu/soramitsu-js-ui/lib/components/Scrollbar'; -import { SortDirection } from '@soramitsu/soramitsu-js-ui/lib/components/Table/consts'; -import { mixins, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; -import Vue from 'vue'; -import { Component, Mixins, Prop, Ref, Watch } from 'vue-property-decorator'; +import { KnownAssets } from '@sora-substrate/util/build/assets/consts'; +import { SortDirection } from '@soramitsu-ui/ui-vue2/lib/components/Table/consts'; +import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'; import { getter } from '@/store/decorators'; -import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; +import ScrollableTableMixin from './ScrollableTableMixin'; +import TranslationMixin from './TranslationMixin'; + +import type { Asset, RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; @Component -export default class ExplorePageMixin extends Mixins( - mixins.LoadingMixin, - mixins.PaginationSearchMixin, - mixins.FormattedAmountMixin -) { - readonly FontSizeRate = WALLET_CONSTS.FontSizeRate; - readonly FontWeightRate = WALLET_CONSTS.FontWeightRate; - - @Ref('table') readonly tableComponent!: any; +export default class ExplorePageMixin extends Mixins(ScrollableTableMixin, TranslationMixin) { @Prop({ default: '', type: String }) readonly exploreQuery!: string; @Prop({ default: false, type: Boolean }) readonly isAccountItemsOnly!: boolean; @Watch('exploreQuery') @@ -25,16 +18,25 @@ export default class ExplorePageMixin extends Mixins( this.currentPage = 1; } - @getter.wallet.account.isLoggedIn isLoggedIn!: boolean; + @getter.wallet.account.isLoggedIn public isLoggedIn!: boolean; @getter.assets.assetDataByAddress public getAsset!: (addr?: string) => Nullable; + @getter.assets.whitelistAssets public whitelistAssets!: Array; - order = ''; - property = ''; + order = SortDirection.DESC; + property = 'tvl'; get loadingState(): boolean { return this.parentLoading || this.loading; } + get allowedAssets(): Array { + // if whitelist is not available, use KnownAssets + if (!this.whitelistAssets.length) { + return [...KnownAssets]; + } + return this.whitelistAssets; + } + get pricesAvailable(): boolean { return Object.keys(this.fiatPriceObject).length > 0; } @@ -43,26 +45,23 @@ export default class ExplorePageMixin extends Mixins( return !(this.order && this.property); } - get preparedItems(): any[] { - console.warn('[ExplorePageMixin]: "preparedItems" computed property is not implemented'); + // items -> prefilteredItems -> filteredItems -> preparedItems + get prefilteredItems(): any[] { + console.warn('[ExplorePageMixin]: "prefilteredItems" computed property is not implemented'); return []; } - get total(): number { - return this.filteredItems.length; - } - get filteredItems() { const search = this.exploreQuery.toLowerCase().trim(); - if (!search) return this.preparedItems; + if (!search) return this.prefilteredItems; const filterAsset = (asset): boolean => asset?.name?.toLowerCase?.()?.includes?.(search) || asset?.symbol?.toLowerCase?.()?.includes?.(search) || asset?.address?.toLowerCase?.() === search; - return this.preparedItems.filter( + return this.prefilteredItems.filter( (item: any) => filterAsset(item) || filterAsset(item.poolAsset) || @@ -72,7 +71,7 @@ export default class ExplorePageMixin extends Mixins( ); } - get sortedItems() { + get preparedItems() { if (this.isDefaultSort) return this.filteredItems; const isAscending = this.order === SortDirection.ASC; @@ -87,19 +86,11 @@ export default class ExplorePageMixin extends Mixins( }); } - get tableItems() { - return this.getPageItems(this.sortedItems); - } - async mounted(): Promise { - await this.$nextTick(); - - this.initScrollbar(); - await this.updateExploreData(); } - changeSort({ order = '', property = '' } = {}): void { + changeSort({ order = SortDirection.DESC, property = '' } = {}): void { this.order = order; this.property = property; } @@ -111,54 +102,4 @@ export default class ExplorePageMixin extends Mixins( async updateExploreData(): Promise { console.warn('[ExplorePageMixin]: "updateExploreData" method is not implemented'); } - - handlePaginationClick(button: WALLET_CONSTS.PaginationButton): void { - let current = 1; - - switch (button) { - case WALLET_CONSTS.PaginationButton.Prev: - current = this.currentPage - 1; - break; - case WALLET_CONSTS.PaginationButton.Next: - current = this.currentPage + 1; - break; - case WALLET_CONSTS.PaginationButton.First: - current = 1; - break; - case WALLET_CONSTS.PaginationButton.Last: - current = this.lastPage; - } - - this.currentPage = current; - } - - private initScrollbar(): void { - if (!this.tableComponent) return; - - const Scrollbar = Vue.extend(SScrollbar); - const scrollbar = new Scrollbar(); - scrollbar.$mount(); - - const elTable = this.tableComponent.$refs.table; - const elTableBodyWrapper = elTable.$refs.bodyWrapper; - const elTableHeaderWrapper = elTable.$refs.headerWrapper; - const elTableNativeTable = elTableBodyWrapper.getElementsByTagName('table')[0]; - const scrollbarWrap = scrollbar.$el.getElementsByClassName('el-scrollbar__wrap')[0]; - const scrollbarView = scrollbar.$el.getElementsByClassName('el-scrollbar__view')[0]; - - elTableBodyWrapper.appendChild(scrollbar.$el); - scrollbarView.appendChild(elTableNativeTable); - - this.$watch( - () => (scrollbar.$children[0] as any).moveX, - () => { - const scrollLeft = scrollbarWrap.scrollLeft; - // to scroll table content - elTableBodyWrapper.scrollLeft = scrollLeft; - elTableHeaderWrapper.scrollLeft = scrollLeft; - // to render box shadow on fixed table - elTable.scrollPosition = scrollLeft === 0 ? 'left' : 'right'; - } - ); - } } diff --git a/src/components/mixins/ScrollableTableMixin.ts b/src/components/mixins/ScrollableTableMixin.ts new file mode 100644 index 000000000..8ca6e6aee --- /dev/null +++ b/src/components/mixins/ScrollableTableMixin.ts @@ -0,0 +1,89 @@ +import { mixins, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; +import SScrollbar from '@soramitsu-ui/ui-vue2/lib/components/Scrollbar'; +import Vue from 'vue'; +import { Component, Mixins, Ref } from 'vue-property-decorator'; + +@Component +export default class ScrollableTableMixin extends Mixins( + mixins.LoadingMixin, + mixins.PaginationSearchMixin, + mixins.FormattedAmountMixin +) { + readonly FontSizeRate = WALLET_CONSTS.FontSizeRate; + readonly FontWeightRate = WALLET_CONSTS.FontWeightRate; + + @Ref('table') readonly tableComponent!: any; + + get loadingState(): boolean { + return this.parentLoading || this.loading; + } + + // should be already filtered & sorted + get preparedItems(): any[] { + console.warn('[ScrollableTableMixin]: "preparedItems" computed property is not implemented'); + return []; + } + + get total(): number { + return this.preparedItems.length; + } + + get tableItems() { + return this.getPageItems(this.preparedItems); + } + + async mounted(): Promise { + await this.$nextTick(); + this.initScrollbar(); + } + + public handlePaginationClick(button: WALLET_CONSTS.PaginationButton): void { + let current = 1; // First by default (instead of case WALLET_CONSTS.PaginationButton.First) + + switch (button) { + case WALLET_CONSTS.PaginationButton.Prev: + current = this.currentPage - 1; + break; + case WALLET_CONSTS.PaginationButton.Next: + current = this.currentPage + 1; + break; + case WALLET_CONSTS.PaginationButton.Last: + current = this.lastPage; + break; + } + + this.currentPage = current; + } + + public initScrollbar(): void { + if (!this.tableComponent) return; + + const Scrollbar = Vue.extend(SScrollbar); + const scrollbar = new Scrollbar(); + scrollbar.$mount(); + + const elTable = this.tableComponent.$refs.table; + const elTableBodyWrapper = elTable.$refs.bodyWrapper; + const elTableHeaderWrapper = elTable.$refs.headerWrapper; + const elTableNativeTable = elTableBodyWrapper.getElementsByTagName('table')[0]; + const scrollbarContainer = scrollbar.$el; + const scrollbarWrap = scrollbar.$el.getElementsByClassName('el-scrollbar__wrap')[0]; + const scrollbarView = scrollbar.$el.getElementsByClassName('el-scrollbar__view')[0]; + + scrollbarContainer.classList.add('scrollable-table'); + elTableBodyWrapper.appendChild(scrollbar.$el); + scrollbarView.appendChild(elTableNativeTable); + + this.$watch( + () => (scrollbar.$children[0] as any).moveX, + () => { + const scrollLeft = scrollbarWrap.scrollLeft; + // to scroll table content + elTableBodyWrapper.scrollLeft = scrollLeft; + elTableHeaderWrapper.scrollLeft = scrollLeft; + // to render box shadow on fixed table + elTable.scrollPosition = scrollLeft === 0 ? 'left' : 'right'; + } + ); + } +} diff --git a/src/components/mixins/SelectedTokensRouteMixin.ts b/src/components/mixins/SelectedTokensRouteMixin.ts index 93c9b9bca..fd6fee74e 100644 --- a/src/components/mixins/SelectedTokensRouteMixin.ts +++ b/src/components/mixins/SelectedTokensRouteMixin.ts @@ -36,6 +36,10 @@ export default class SelectedTokenRouteMixin extends Vue { const bothArePresented = !!(firstAddress && secondAddress); switch (this.$route.name) { + case PageNames.OrderBook: + // Second asset address should be used as quote for Orderbook /trade/base/quote + // only XOR for now, like /trade/xst/xor + return bothArePresented && secondAddress === XOR.address; case PageNames.RemoveLiquidity: return bothArePresented && api.dex.baseAssetsIds.includes(firstAddress); case PageNames.AddLiquidity: { @@ -75,6 +79,7 @@ export default class SelectedTokenRouteMixin extends Vue { * (b) `false` - when assets are equal; * (c) SWAP: `true` when both parameters are parsed as asset ids; * (d) ADD/REMOVE LIQUIDITY: `true` when both parameters are parsed as asset ids and first is from baseAssetIds; + * (e) ORDERBOOK: `true` is second === correct quote asset */ get isValidRoute(): boolean { const { first, second } = this.$route.params; @@ -84,7 +89,7 @@ export default class SelectedTokenRouteMixin extends Vue { } /** - * Sould be used in Add liquidity & Swap during mount + * Should be used in Add liquidity & Swap during mount * * Returns is valid state for routing life cycle */ diff --git a/src/components/mixins/ThemePaletteMixin.ts b/src/components/mixins/ThemePaletteMixin.ts index 312b69b2d..58ca87f39 100644 --- a/src/components/mixins/ThemePaletteMixin.ts +++ b/src/components/mixins/ThemePaletteMixin.ts @@ -3,7 +3,7 @@ import { Component, Vue } from 'vue-property-decorator'; import { getter } from '@/store/decorators'; import { getCssVariableValue as css } from '@/utils'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component export default class ThemePaletteMixin extends Vue { diff --git a/src/components/pages/Moonpay/Confirmation.vue b/src/components/pages/Moonpay/Confirmation.vue index 917948bdb..d3d90e9d5 100644 --- a/src/components/pages/Moonpay/Confirmation.vue +++ b/src/components/pages/Moonpay/Confirmation.vue @@ -33,7 +33,7 @@ import { lazyComponent } from '@/router'; import { getter, state } from '@/store/decorators'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component({ components: { diff --git a/src/components/pages/Moonpay/Moonpay.vue b/src/components/pages/Moonpay/Moonpay.vue index b8d3baca0..cf30b63e5 100644 --- a/src/components/pages/Moonpay/Moonpay.vue +++ b/src/components/pages/Moonpay/Moonpay.vue @@ -21,8 +21,8 @@ import type { MoonpayTransaction } from '@/utils/moonpay'; import MoonpayBridgeInitMixin from './BridgeInitMixin'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; import type { WALLET_TYPES } from '@soramitsu/soraneo-wallet-web'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component({ components: { diff --git a/src/components/pages/Moonpay/MoonpayHistory.vue b/src/components/pages/Moonpay/MoonpayHistory.vue index 884f87094..e4b52ebeb 100644 --- a/src/components/pages/Moonpay/MoonpayHistory.vue +++ b/src/components/pages/Moonpay/MoonpayHistory.vue @@ -85,7 +85,7 @@ import { MoonpayTransactionStatus } from '../../../utils/moonpay'; import type { MoonpayTransaction, MoonpayCurrency, MoonpayCurrenciesById } from '../../../utils/moonpay'; import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; const HistoryView = 'history'; const DetailsView = 'details'; @@ -242,6 +242,7 @@ export default class MoonpayHistory extends Mixins(mixins.PaginationSearchMixin, case WALLET_CONSTS.PaginationButton.Last: current = this.lastPage; this.isLtrDirection = false; + break; } this.currentPage = current; diff --git a/src/components/pages/Moonpay/Notification.vue b/src/components/pages/Moonpay/Notification.vue index 9626861e7..a6251a7d6 100644 --- a/src/components/pages/Moonpay/Notification.vue +++ b/src/components/pages/Moonpay/Notification.vue @@ -20,7 +20,7 @@ import { mutation, state, getter } from '@/store/decorators'; import { MoonpayNotifications } from './consts'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component({ components: { diff --git a/src/components/pages/OrderBook/BookChartsWidget.vue b/src/components/pages/OrderBook/BookChartsWidget.vue new file mode 100644 index 000000000..bc801f017 --- /dev/null +++ b/src/components/pages/OrderBook/BookChartsWidget.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/pages/OrderBook/BookWidget.vue b/src/components/pages/OrderBook/BookWidget.vue new file mode 100644 index 000000000..9a193bdec --- /dev/null +++ b/src/components/pages/OrderBook/BookWidget.vue @@ -0,0 +1,598 @@ + + + + + diff --git a/src/components/pages/OrderBook/BuySell.vue b/src/components/pages/OrderBook/BuySell.vue new file mode 100644 index 000000000..a38617190 --- /dev/null +++ b/src/components/pages/OrderBook/BuySell.vue @@ -0,0 +1,1081 @@ + + + + + + + diff --git a/src/components/pages/OrderBook/Dialogs/CancelOrders.vue b/src/components/pages/OrderBook/Dialogs/CancelOrders.vue new file mode 100644 index 000000000..3e1f3f4cf --- /dev/null +++ b/src/components/pages/OrderBook/Dialogs/CancelOrders.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/pages/OrderBook/Dialogs/CustomisePage.vue b/src/components/pages/OrderBook/Dialogs/CustomisePage.vue new file mode 100644 index 000000000..fe5c23e29 --- /dev/null +++ b/src/components/pages/OrderBook/Dialogs/CustomisePage.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/pages/OrderBook/Dialogs/PlaceOrder.vue b/src/components/pages/OrderBook/Dialogs/PlaceOrder.vue new file mode 100644 index 000000000..31881280f --- /dev/null +++ b/src/components/pages/OrderBook/Dialogs/PlaceOrder.vue @@ -0,0 +1,183 @@ + + + + + + + diff --git a/src/components/pages/OrderBook/HistoryOrderWidget.vue b/src/components/pages/OrderBook/HistoryOrderWidget.vue new file mode 100644 index 000000000..93e12641e --- /dev/null +++ b/src/components/pages/OrderBook/HistoryOrderWidget.vue @@ -0,0 +1,309 @@ + + + + + + + diff --git a/src/components/pages/OrderBook/MarketTradesWidget.vue b/src/components/pages/OrderBook/MarketTradesWidget.vue new file mode 100644 index 000000000..6fa6688ba --- /dev/null +++ b/src/components/pages/OrderBook/MarketTradesWidget.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/components/pages/OrderBook/Popovers/PairListPopover.vue b/src/components/pages/OrderBook/Popovers/PairListPopover.vue new file mode 100644 index 000000000..3da34aa19 --- /dev/null +++ b/src/components/pages/OrderBook/Popovers/PairListPopover.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/src/components/pages/OrderBook/SetLimitOrderWidget.vue b/src/components/pages/OrderBook/SetLimitOrderWidget.vue new file mode 100644 index 000000000..77908995f --- /dev/null +++ b/src/components/pages/OrderBook/SetLimitOrderWidget.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/components/pages/OrderBook/Tables/AllOrders.vue b/src/components/pages/OrderBook/Tables/AllOrders.vue new file mode 100644 index 000000000..a53ea6f22 --- /dev/null +++ b/src/components/pages/OrderBook/Tables/AllOrders.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/components/pages/OrderBook/Tables/OpenOrders.vue b/src/components/pages/OrderBook/Tables/OpenOrders.vue new file mode 100644 index 000000000..f6690fadb --- /dev/null +++ b/src/components/pages/OrderBook/Tables/OpenOrders.vue @@ -0,0 +1,160 @@ + + + diff --git a/src/components/pages/OrderBook/Tables/OrderTable.vue b/src/components/pages/OrderBook/Tables/OrderTable.vue new file mode 100644 index 000000000..813f6b685 --- /dev/null +++ b/src/components/pages/OrderBook/Tables/OrderTable.vue @@ -0,0 +1,359 @@ + + + + + diff --git a/src/components/pages/OrderBook/TransactionDetails.vue b/src/components/pages/OrderBook/TransactionDetails.vue new file mode 100644 index 000000000..0419fc28e --- /dev/null +++ b/src/components/pages/OrderBook/TransactionDetails.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/components/pages/Rewards/AmountTable.vue b/src/components/pages/Rewards/AmountTable.vue index 57bce3c9a..30c7288b4 100644 --- a/src/components/pages/Rewards/AmountTable.vue +++ b/src/components/pages/Rewards/AmountTable.vue @@ -78,8 +78,8 @@ @@ -285,6 +333,39 @@ $el-input-class: '.el-input'; } } } + +.input-line--footer-with-slider { + @include input-slider; + width: 100%; + + .el-slider__button { + background-color: #fff; + border-radius: 4px; + transform: rotate(-45deg); + } + + .el-slider__stop { + height: 10px; + width: 10px; + border-radius: 2px; + top: -1.8px; + border: 1.3px solid var(--s-color-base-content-tertiary); + transform: translateX(-50%) rotate(-45deg); + } + + .asset-info { + display: flex; + width: 100%; + justify-content: space-between; + } + + .delimiter { + background-color: var(--s-color-base-border-secondary); + width: 100%; + height: 1px; + margin: 14px 0 4px 0; + } +} -@/modules/staking/demeter/types diff --git a/src/modules/staking/sora/components/ClaimRewardsDialog.vue b/src/modules/staking/sora/components/ClaimRewardsDialog.vue index 32754aaae..82dbea23c 100644 --- a/src/modules/staking/sora/components/ClaimRewardsDialog.vue +++ b/src/modules/staking/sora/components/ClaimRewardsDialog.vue @@ -38,7 +38,7 @@ @@ -82,7 +82,7 @@ import type { CodecString } from '@sora-substrate/util'; FormattedAmountWithFiatValue: components.FormattedAmountWithFiatValue, }, }) -export default class ClaimRewardsDialog extends Mixins(StakingMixin, mixins.DialogMixin, mixins.LoadingMixin) { +export default class ClaimRewardsDialog extends Mixins(StakingMixin, mixins.DialogMixin, mixins.TransactionMixin) { @Prop({ default: () => true, type: Boolean }) readonly isAdding!: boolean; rewardsDestination = ''; @@ -167,16 +167,18 @@ export default class ClaimRewardsDialog extends Mixins(StakingMixin, mixins.Dial } async handleConfirm(): Promise { - await this.payout({ - payouts: this.pendingRewards - ? this.pendingRewards.map((r) => ({ era: r.era, validators: r.validators.map((v) => v.address) })) - : [], - payee: this.rewardsDestination !== this.payeeAddress ? this.rewardsDestination : undefined, - }); + await this.withNotifications(async () => { + await this.payout({ + payouts: this.pendingRewards + ? this.pendingRewards.map((r) => ({ era: r.era, validators: r.validators.map((v) => v.address) })) + : [], + payee: this.rewardsDestination !== this.payeeAddress ? this.rewardsDestination : undefined, + }); - await this.getPendingRewards(); + await this.getPendingRewards(); - this.closeDialog(); + this.closeDialog(); + }); } checkPendingRewards(): void { diff --git a/src/modules/staking/sora/components/PendingRewardsDialog.vue b/src/modules/staking/sora/components/PendingRewardsDialog.vue index c7c575f1a..a1f968923 100644 --- a/src/modules/staking/sora/components/PendingRewardsDialog.vue +++ b/src/modules/staking/sora/components/PendingRewardsDialog.vue @@ -69,7 +69,7 @@ @@ -129,8 +129,8 @@ type Reward = { export default class PendingRewardsDialog extends Mixins( StakingMixin, ValidatorsMixin, - mixins.DialogMixin, - mixins.LoadingMixin + mixins.TransactionMixin, + mixins.DialogMixin ) { payoutNetworkFee: string | null = null; selectedRewards: Reward[] = []; @@ -235,13 +235,15 @@ export default class PendingRewardsDialog extends Mixins( } async handleConfirm(): Promise { - await this.payout({ - payouts: this.payouts, - }); + await this.withNotifications(async () => { + await this.payout({ + payouts: this.payouts, + }); - await this.getPendingRewards(); + await this.getPendingRewards(); - this.closeDialog(); + this.closeDialog(); + }); } } diff --git a/src/modules/staking/sora/components/StakeDialog.vue b/src/modules/staking/sora/components/StakeDialog.vue index ff567168f..941aafcea 100644 --- a/src/modules/staking/sora/components/StakeDialog.vue +++ b/src/modules/staking/sora/components/StakeDialog.vue @@ -43,7 +43,7 @@ @@ -68,7 +68,6 @@ import { Component, Mixins, Watch, Prop } from 'vue-property-decorator'; import { Components } from '@/consts'; import { lazyComponent } from '@/router'; -import { state } from '@/store/decorators'; import { StakeDialogMode } from '../consts'; import StakingMixin from '../mixins/StakingMixin'; @@ -83,11 +82,9 @@ import type { CodecString } from '@sora-substrate/util'; AccountCard: components.AccountCard, }, }) -export default class StakeDialog extends Mixins(StakingMixin, mixins.DialogMixin, mixins.LoadingMixin) { +export default class StakeDialog extends Mixins(StakingMixin, mixins.TransactionMixin, mixins.DialogMixin) { @Prop({ required: true, type: String }) readonly mode!: StakeDialogMode; - @state.wallet.settings.shouldBalanceBeHidden private shouldBalanceBeHidden!: boolean; - @Watch('visible') private resetValue() { if (this.visible) { @@ -202,13 +199,14 @@ export default class StakeDialog extends Mixins(StakingMixin, mixins.DialogMixin async handleConfirm(): Promise { this.setStakeAmount(this.value); + let extrinsic = this.unbond; if (this.mode === StakeDialogMode.NEW) { - await this.bondAndNominate(); + extrinsic = this.bondAndNominate; } else if (this.mode === StakeDialogMode.ADD) { - await this.bondExtra(); - } else { - await this.unbond(); + extrinsic = this.bondExtra; } + + await this.withNotifications(async () => await extrinsic()); this.$emit('confirm'); } } diff --git a/src/modules/staking/sora/components/ValidatorsDialog.vue b/src/modules/staking/sora/components/ValidatorsDialog.vue index 961de5f84..63474bb07 100644 --- a/src/modules/staking/sora/components/ValidatorsDialog.vue +++ b/src/modules/staking/sora/components/ValidatorsDialog.vue @@ -9,7 +9,13 @@
- + {{ confirmText }}
@@ -49,7 +55,7 @@ import type { MyStakingInfo } from '@sora-substrate/util/build/staking/types'; InfoLine: components.InfoLine, }, }) -export default class ValidatorsDialog extends Mixins(StakingMixin, mixins.DialogMixin, mixins.LoadingMixin) { +export default class ValidatorsDialog extends Mixins(StakingMixin, mixins.DialogMixin, mixins.TransactionMixin) { @mutation.staking.setStakingInfo setStakingInfo!: (stakingInfo: MyStakingInfo) => void; @action.staking.getStakingInfo getStakingInfo!: AsyncFnWithoutArgs; @@ -164,16 +170,18 @@ export default class ValidatorsDialog extends Mixins(StakingMixin, mixins.Dialog if (this.mode === ValidatorsListMode.USER) { this.isSelectingEditingMode = true; } else { - await this.nominate(); + await this.withNotifications(async () => { + await this.nominate(); - if (!this.stakingInfo) throw new Error('There is no staking info'); + if (!this.stakingInfo) throw new Error('There is no staking info'); - this.setStakingInfo({ - ...this.stakingInfo, - myValidators: this.selectedValidators.map((v) => v.address), - }); + this.setStakingInfo({ + ...this.stakingInfo, + myValidators: this.selectedValidators.map((v) => v.address), + }); - this.mode = ValidatorsListMode.USER; + this.mode = ValidatorsListMode.USER; + }); } } diff --git a/src/modules/staking/sora/mixins/StakingMixin.ts b/src/modules/staking/sora/mixins/StakingMixin.ts index 97814bd87..89475c38b 100644 --- a/src/modules/staking/sora/mixins/StakingMixin.ts +++ b/src/modules/staking/sora/mixins/StakingMixin.ts @@ -53,7 +53,6 @@ export default class StakingMixin extends Mixins(mixins.FormattedAmountMixin, Tr @mutation.staking.setShowValidatorsFilterDialog setShowValidatorsFilterDialog!: (value: boolean) => void; @mutation.staking.selectValidators selectValidators!: (validators: ValidatorInfoFull[]) => void; - @action.staking.bond bond!: AsyncFnWithoutArgs; @action.staking.nominate nominate!: AsyncFnWithoutArgs; @action.staking.bondAndNominate bondAndNominate!: AsyncFnWithoutArgs; @action.staking.getBondAndNominateNetworkFee getBondAndNominateNetworkFee!: () => Promise; diff --git a/src/modules/staking/sora/views/Overview.vue b/src/modules/staking/sora/views/Overview.vue index f9c80c5be..021637186 100644 --- a/src/modules/staking/sora/views/Overview.vue +++ b/src/modules/staking/sora/views/Overview.vue @@ -97,7 +97,7 @@ v-if="stakingInitialized" :label="t('soraStaking.info.redeemable')" :value="redeemableFundsFormatted" - :asset-symbol="rewardAsset?.symbol" + :asset-symbol="stakingAsset?.symbol" :fiat-value="redeemableFundsFiat" /> = [ name: PageNames.ExploreTokens, component: lazyView(PageNames.ExploreTokens), }, + { + path: 'books', + name: PageNames.ExploreBooks, + component: lazyView(PageNames.ExploreBooks), + }, ], }, { @@ -265,6 +270,11 @@ const routes: Array = [ name: PageNames.Stats, component: lazyView(PageNames.Stats), }, + { + path: '/trade/:first?/:second?', + name: PageNames.OrderBook, + component: lazyView(PageNames.OrderBook), + }, { path: '*', redirect: '/swap', diff --git a/src/store/bridge/actions.ts b/src/store/bridge/actions.ts index 341527005..02fcb9605 100644 --- a/src/store/bridge/actions.ts +++ b/src/store/bridge/actions.ts @@ -139,7 +139,7 @@ async function getEvmNetworkFee(context: ActionContext): Promise const bridgeRegisteredAsset = rootState.assets.registeredAssets[asset.address]; const decimals = state.isSoraToEvm ? asset.decimals : asset.externalDecimals; // using max balance to not overflow contract calculation - const value = FPNumber.fromCodecValue(state.assetSenderBalance!, decimals).toString(); + const value = FPNumber.fromCodecValue(state.assetSenderBalance ?? 0, decimals).toString(); fee = await getEthNetworkFee( asset, diff --git a/src/store/consts.ts b/src/store/consts.ts index c638b9a07..7bca9a655 100644 --- a/src/store/consts.ts +++ b/src/store/consts.ts @@ -19,6 +19,7 @@ export enum Module { Staking = 'staking', DemeterFarming = 'demeterFarming', SoraCard = 'soraCard', + OrderBook = 'orderBook', } export const Modules = [...Object.values(Module), ...WalletModules]; diff --git a/src/store/index.ts b/src/store/index.ts index f0f90bee6..d9ef15ee9 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,7 @@ import assets from './assets'; import bridge from './bridge'; import demeterFarming from './demeterFarming'; import moonpay from './moonpay'; +import orderBook from './orderBook'; import pool from './pool'; import referrals from './referrals'; import removeLiquidity from './removeLiquidity'; @@ -40,6 +41,7 @@ const modules = { staking, demeterFarming, soraCard, + orderBook, }; const { store, rootGetterContext, rootActionContext } = createDirectStore({ diff --git a/src/store/orderBook/actions.ts b/src/store/orderBook/actions.ts new file mode 100644 index 000000000..5fa5fdf14 --- /dev/null +++ b/src/store/orderBook/actions.ts @@ -0,0 +1,195 @@ +import { api } from '@soramitsu/soraneo-wallet-web'; +import { defineActions } from 'direct-vuex'; +import { combineLatest } from 'rxjs'; + +import { subscribeOnOrderBookUpdates, fetchOrderBooks } from '@/indexer/queries/orderBook'; + +import { orderBookActionContext } from '.'; + +import type { OrderBook } from '@sora-substrate/liquidity-proxy'; +import type { LimitOrder } from '@sora-substrate/util/build/orderBook/types'; +import type { Subscription } from 'rxjs'; + +const actions = defineActions({ + async getOrderBooksInfo(context): Promise { + const { commit, rootGetters } = orderBookActionContext(context); + const { whitelist } = rootGetters.wallet.account; + const orderBooks = await api.orderBook.getOrderBooks(); + + const orderBooksWhitelist = Object.entries(orderBooks).reduce>((buffer, [key, book]) => { + const { base, quote } = book.orderBookId; + if ([base, quote].every((address) => address in whitelist)) { + buffer[key] = book; + } + return buffer; + }, {}); + + commit.setOrderBooks(orderBooksWhitelist); + }, + + async updateOrderBooksStats(context): Promise { + const { commit } = orderBookActionContext(context); + + const orderBooksWithStats = await fetchOrderBooks(); + const orderBooksStats = (orderBooksWithStats ?? []).reduce((buffer, item) => { + const { + id: { base, quote }, + stats, + } = item; + + const key = api.orderBook.serializedKey(base, quote); + buffer[key] = stats; + return buffer; + }, {}); + + commit.setStats(orderBooksStats); + }, + + async subscribeToBidsAndAsks(context): Promise { + const { commit, dispatch, getters } = orderBookActionContext(context); + const { baseAsset, quoteAsset } = getters; + + dispatch.unsubscribeFromBidsAndAsks(); + + if (!(baseAsset && quoteAsset)) return; + + let asksSubscription!: Subscription; + let bidsSubscription!: Subscription; + + await Promise.all([ + new Promise((resolve) => { + asksSubscription = api.orderBook + .subscribeOnAggregatedAsks(baseAsset.address, quoteAsset.address) + .subscribe((asks) => { + commit.setAsks(asks.toReversed()); + resolve(); + }); + }), + new Promise((resolve) => { + bidsSubscription = api.orderBook + .subscribeOnAggregatedBids(baseAsset.address, quoteAsset.address) + .subscribe((bids) => { + commit.setBids(bids.toReversed()); + resolve(); + }); + }), + ]); + + commit.setOrderBookUpdates([asksSubscription, bidsSubscription]); + }, + + unsubscribeFromBidsAndAsks(context): void { + const { commit } = orderBookActionContext(context); + + commit.setAsks(); + commit.setBids(); + commit.resetOrderBookUpdates(); + }, + + async subscribeToOrderBookStats(context): Promise { + const { commit, dispatch, getters, state } = orderBookActionContext(context); + const { dexId } = state; + const { baseAsset, quoteAsset } = getters; + + dispatch.unsubscribeFromOrderBookStats(); + + if (!(baseAsset && quoteAsset)) return; + + const subscription = await subscribeOnOrderBookUpdates( + dexId, + baseAsset.address, + quoteAsset.address, + (data) => { + const { + id: { base, quote }, + stats, + deals, + } = data; + const key = api.orderBook.serializedKey(base, quote); + commit.setDeals(deals); + commit.setStats({ [key]: stats }); + }, + console.error + ); + + if (!subscription) return; + + commit.setOrderBookStatsUpdates(subscription); + }, + + unsubscribeFromOrderBookStats(context): void { + const { commit } = orderBookActionContext(context); + + commit.setDeals(); + commit.resetOrderBookStatsUpdates(); + }, + + async subscribeToUserLimitOrders(context): Promise { + const { commit, dispatch, getters } = orderBookActionContext(context); + const { baseAsset, quoteAsset, accountAddress } = getters; + + dispatch.unsubscribeFromUserLimitOrders(); + + if (!(accountAddress && baseAsset && quoteAsset)) return; + + let subscription!: Subscription; + + await new Promise((resolve) => { + subscription = api.orderBook + .subscribeOnUserLimitOrdersIds(baseAsset.address, quoteAsset.address, accountAddress) + .subscribe(async (ids) => { + const userLimitOrders = (await Promise.all( + ids.map((id) => api.orderBook.getLimitOrder(baseAsset.address, quoteAsset.address, id)) + )) as LimitOrder[]; + + const orders = userLimitOrders.map((el) => { + const amountStr = el.amount.toString(); + const originalAmountStr = el.originalAmount.toString(); + return { ...el, amountStr, originalAmountStr }; + }); + + commit.setUserLimitOrders(orders); + + resolve(); + }); + }); + + commit.setUserLimitOrderUpdates(subscription); + }, + + async subscribeOnLimitOrders(context, ids: number[]): Promise { + const { commit, getters, state } = orderBookActionContext(context); + const { baseAsset, quoteAsset, accountAddress } = getters; + + if (!(accountAddress && baseAsset && quoteAsset)) return; + + let subscription!: Subscription; + const observables = ids.map((id) => api.orderBook.subscribeOnLimitOrder(baseAsset.address, quoteAsset.address, id)); + + await new Promise((resolve) => { + subscription = combineLatest(observables).subscribe((updated) => { + const updatedOrders = updated.filter((item) => !!item) as LimitOrder[]; + if (updatedOrders.length) { + const userLimitOrders = state.userLimitOrders.map((order) => { + const found = updatedOrders.find((item) => item.id === order.id); + return found ?? order; + }); + commit.setUserLimitOrders(userLimitOrders); + resolve(); + } else { + resolve(); + } + }); + }); + + commit.setPagedUserLimitOrdersSubscription(subscription); + }, + + unsubscribeFromUserLimitOrders(context): void { + const { commit } = orderBookActionContext(context); + + commit.resetUserLimitOrderUpdates(); + }, +}); + +export default actions; diff --git a/src/store/orderBook/getters.ts b/src/store/orderBook/getters.ts new file mode 100644 index 000000000..0344e7b45 --- /dev/null +++ b/src/store/orderBook/getters.ts @@ -0,0 +1,63 @@ +import { OrderBook } from '@sora-substrate/liquidity-proxy'; +import { api } from '@soramitsu/soraneo-wallet-web'; +import { defineGetters } from 'direct-vuex'; + +import type { OrderBookStats, OrderBookDealData } from '@/types/orderBook'; +import { getBookDecimals } from '@/utils/orderBook'; + +import { OrderBookState } from './types'; + +import { orderBookGetterContext } from '.'; + +import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; + +const getters = defineGetters()({ + baseAsset(...args): Nullable { + const { state, rootGetters } = orderBookGetterContext(args); + if (!state.baseAssetAddress) return null; + return rootGetters.assets.assetDataByAddress(state.baseAssetAddress); + }, + quoteAsset(...args): Nullable { + const { state, rootGetters } = orderBookGetterContext(args); + if (!state.quoteAssetAddress) return null; + return rootGetters.assets.assetDataByAddress(state.quoteAssetAddress); + }, + orderBookId(...args): string { + const { getters } = orderBookGetterContext(args); + const { baseAsset, quoteAsset } = getters; + + if (!(baseAsset && quoteAsset)) return ''; + + return api.orderBook.serializedKey(baseAsset.address, quoteAsset.address); + }, + currentOrderBook(...args): Nullable { + const { getters, state } = orderBookGetterContext(args); + + if (!getters.orderBookId) return null; + + return state.orderBooks[getters.orderBookId]; + }, + orderBookStats(...args): Nullable { + const { getters, state } = orderBookGetterContext(args); + + if (!getters.orderBookId) return null; + + return state.orderBooksStats[getters.orderBookId]; + }, + orderBookDecimals(...args): number { + const { getters } = orderBookGetterContext(args); + + return getBookDecimals(getters.currentOrderBook); + }, + orderBookLastDeal(...args): Nullable { + const { state } = orderBookGetterContext(args); + + return state.deals[0] ?? null; + }, + accountAddress(...args): string { + const { rootState } = orderBookGetterContext(args); + return rootState.wallet.account.address; + }, +}); + +export default getters; diff --git a/src/store/orderBook/index.ts b/src/store/orderBook/index.ts new file mode 100644 index 000000000..745ae1971 --- /dev/null +++ b/src/store/orderBook/index.ts @@ -0,0 +1,23 @@ +import { defineModule } from 'direct-vuex'; + +import { localActionContext, localGetterContext } from '@/store'; +import { Module } from '@/store/consts'; + +import actions from './actions'; +import getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +const orderBook = defineModule({ + namespaced: true, + state, + getters, + mutations, + actions, +}); + +const orderBookGetterContext = (args: [any, any, any, any]) => localGetterContext(args, Module.OrderBook, orderBook); +const orderBookActionContext = (context: any) => localActionContext(context, Module.OrderBook, orderBook); + +export { orderBookActionContext, orderBookGetterContext }; +export default orderBook; diff --git a/src/store/orderBook/mutations.ts b/src/store/orderBook/mutations.ts new file mode 100644 index 000000000..b36673708 --- /dev/null +++ b/src/store/orderBook/mutations.ts @@ -0,0 +1,84 @@ +import { PriceVariant } from '@sora-substrate/liquidity-proxy'; +import { defineMutations } from 'direct-vuex'; + +import { LimitOrderType } from '@/consts'; +import type { OrderBookDealData, OrderBookStats } from '@/types/orderBook'; + +import type { OrderBookState } from './types'; +import type { OrderBookId, OrderBookPriceVolume, OrderBook } from '@sora-substrate/liquidity-proxy'; +import type { LimitOrder } from '@sora-substrate/util/build/orderBook/types'; +import type { Subscription } from 'rxjs'; + +const mutations = defineMutations()({ + setOrderBooks(state, orderBooks: Record): void { + state.orderBooks = orderBooks; + }, + setCurrentOrderBook(state, { dexId, base, quote }: OrderBookId): void { + state.dexId = dexId; + state.baseAssetAddress = base; + state.quoteAssetAddress = quote; + }, + setBaseValue(state, value: string): void { + state.baseValue = value; + }, + setQuoteValue(state, value: string): void { + state.quoteValue = value; + }, + setSide(state, side: PriceVariant): void { + state.side = side; + }, + setLimitOrderType(state, type: LimitOrderType): void { + state.limitOrderType = type; + }, + setAsks(state, asks: readonly OrderBookPriceVolume[] = []): void { + state.asks = Object.freeze([...asks]); + }, + setBids(state, bids: readonly OrderBookPriceVolume[] = []): void { + state.bids = Object.freeze([...bids]); + }, + setDeals(state, deals: readonly OrderBookDealData[] = []): void { + state.deals = Object.freeze([...deals]); + }, + setStats(state, stats: Record): void { + state.orderBooksStats = Object.freeze({ ...state.orderBooksStats, ...stats }); + }, + setUserLimitOrders(state, limitOrders: LimitOrder[] = []): void { + state.userLimitOrders = Object.freeze([...limitOrders]); + }, + setOrderBookUpdates(state, subscriptions: Array): void { + state.orderBookUpdates = subscriptions; + }, + resetOrderBookUpdates(state): void { + state.orderBookUpdates?.forEach((subscription) => subscription?.unsubscribe()); + state.orderBookUpdates = []; + }, + setOrderBookStatsUpdates(state, subscription: VoidFunction): void { + state.orderBookStatsUpdates = subscription; + }, + resetOrderBookStatsUpdates(state): void { + state.orderBookStatsUpdates?.(); + state.orderBookStatsUpdates = null; + }, + setUserLimitOrderUpdates(state, subscription: Subscription): void { + state.userLimitOrderUpdates = subscription; + }, + resetUserLimitOrderUpdates(state): void { + state.userLimitOrderUpdates?.unsubscribe(); + state.userLimitOrderUpdates = null; + }, + setPagedUserLimitOrdersSubscription(state, subscription: Subscription): void { + state.pagedUserLimitOrdersSubscription = subscription; + }, + resetPagedUserLimitOrdersSubscription(state): void { + state.pagedUserLimitOrdersSubscription?.unsubscribe(); + state.pagedUserLimitOrdersSubscription = null; + }, + setOrdersToBeCancelled(state, orders): void { + state.ordersToBeCancelled = orders; + }, + setAmountSliderValue(state, percent: number) { + state.amountSliderValue = percent; + }, +}); + +export default mutations; diff --git a/src/store/orderBook/state.ts b/src/store/orderBook/state.ts new file mode 100644 index 000000000..8cfea5703 --- /dev/null +++ b/src/store/orderBook/state.ts @@ -0,0 +1,34 @@ +import { PriceVariant } from '@sora-substrate/liquidity-proxy'; +import { DexId } from '@sora-substrate/util/build/dex/consts'; + +import { LimitOrderType } from '@/consts'; + +import type { OrderBookState } from './types'; + +function initialState(): OrderBookState { + return { + orderBooks: {}, + dexId: DexId.XOR, + baseAssetAddress: null, + quoteAssetAddress: null, + limitOrderType: LimitOrderType.limit, + baseValue: '', + quoteValue: '', + orderBooksStats: {}, + deals: [], + asks: [], + bids: [], + userLimitOrders: [], + side: PriceVariant.Buy, + orderBookUpdates: [], + orderBookStatsUpdates: null, + userLimitOrderUpdates: null, + pagedUserLimitOrdersSubscription: null, + ordersToBeCancelled: [], + amountSliderValue: 0, + }; +} + +const state = initialState(); + +export default state; diff --git a/src/store/orderBook/types.ts b/src/store/orderBook/types.ts new file mode 100644 index 000000000..765bfcb6d --- /dev/null +++ b/src/store/orderBook/types.ts @@ -0,0 +1,29 @@ +import type { LimitOrderType } from '@/consts'; +import type { OrderBookStats, OrderBookDealData } from '@/types/orderBook'; + +import type { PriceVariant, OrderBookPriceVolume, OrderBook } from '@sora-substrate/liquidity-proxy'; +import type { DexId } from '@sora-substrate/util/build/dex/consts'; +import type { LimitOrder } from '@sora-substrate/util/build/orderBook/types'; +import type { Subscription } from 'rxjs'; + +export type OrderBookState = { + orderBooks: Record; + dexId: DexId; + baseAssetAddress: Nullable; + quoteAssetAddress: Nullable; + limitOrderType: LimitOrderType; + orderBooksStats: Record; + deals: readonly OrderBookDealData[]; + asks: readonly OrderBookPriceVolume[]; + bids: readonly OrderBookPriceVolume[]; + userLimitOrders: readonly LimitOrder[]; + baseValue: string; + quoteValue: string; + side: PriceVariant; + orderBookUpdates: Array; + orderBookStatsUpdates: Nullable; + userLimitOrderUpdates: Nullable; + pagedUserLimitOrdersSubscription: Nullable; + ordersToBeCancelled: Array; + amountSliderValue: number; +}; diff --git a/src/store/settings/actions.ts b/src/store/settings/actions.ts index a5c9ecbdd..c7f41dfaf 100644 --- a/src/store/settings/actions.ts +++ b/src/store/settings/actions.ts @@ -204,7 +204,7 @@ const actions = defineActions({ updateDocumentTitle(); updateFpNumberLocale(locale); commit.setLanguage(locale); - commit.updateDisplayRegions(); // based on locale + commit.updateIntlUtils(); // based on locale }, async setBlockNumber(context): Promise { const { commit } = settingsActionContext(context); diff --git a/src/store/settings/getters.ts b/src/store/settings/getters.ts index 39ef38022..c8a4d8cf4 100644 --- a/src/store/settings/getters.ts +++ b/src/store/settings/getters.ts @@ -64,6 +64,10 @@ const getters = defineGetters()({ const { state } = settingsGetterContext(args); return state.featureFlags.soraCard; }, + orderBookEnabled(...args): Nullable { + const { state } = settingsGetterContext(args); + return state.featureFlags.orderBook; + }, notificationActivated(...args): boolean { const { state } = settingsGetterContext(args); return state.browserNotifsPermission === 'granted'; diff --git a/src/store/settings/mutations.ts b/src/store/settings/mutations.ts index 34e11dd0b..0968f470f 100644 --- a/src/store/settings/mutations.ts +++ b/src/store/settings/mutations.ts @@ -95,12 +95,14 @@ const mutations = defineMutations()({ state.userDisclaimerApprove = true; settingsStorage.set('disclaimerApprove', true); }, - updateDisplayRegions(state): void { + updateIntlUtils(state): void { try { state.displayRegions = new Intl.DisplayNames([state.language], { type: 'region' }); + state.percentFormat = new Intl.NumberFormat([state.language], { style: 'percent', maximumFractionDigits: 2 }); } catch (error) { - console.warn('Intl.DisplayNames issue', error); + console.warn('Intl is not supported!', error); state.displayRegions = null; + state.percentFormat = null; } }, setFeatureFlags(state, featureFlags: FeatureFlags = {}): void { @@ -109,6 +111,9 @@ const mutations = defineMutations()({ setBlockNumber(state, value: number): void { state.blockNumber = value || 0; }, + setMenuCollapsed(state, collapsed: boolean): void { + state.menuCollapsed = collapsed; + }, setBlockNumberUpdates(state, subscription: Subscription): void { state.blockNumberUpdates = subscription; }, diff --git a/src/store/settings/state.ts b/src/store/settings/state.ts index cc40494ac..73332c259 100644 --- a/src/store/settings/state.ts +++ b/src/store/settings/state.ts @@ -22,12 +22,14 @@ function initialState(): SettingsState { node: node ? JSON.parse(node) : {}, language: getLocale(), displayRegions: undefined, + percentFormat: undefined, defaultNodes: [], customNodes: customNodes ? JSON.parse(customNodes) : [], nodeAddressConnecting: '', nodeConnectionAllowance: true, chainGenesisHash: '', faucetUrl: '', + menuCollapsed: false, selectNodeDialogVisibility: false, selectIndexerDialogVisibility: false, selectLanguageDialogVisibility: false, diff --git a/src/store/settings/types.ts b/src/store/settings/types.ts index 79232fa14..0eace1fa6 100644 --- a/src/store/settings/types.ts +++ b/src/store/settings/types.ts @@ -15,6 +15,7 @@ export type FeatureFlags = { x1ex?: boolean; charts?: boolean; soraCard?: boolean; + orderBook?: boolean; }; export type SettingsState = { @@ -27,12 +28,14 @@ export type SettingsState = { node: Partial; language: string; displayRegions: Nullable; + percentFormat: Nullable; defaultNodes: Array; customNodes: Array; nodeAddressConnecting: string; nodeConnectionAllowance: boolean; chainGenesisHash: string; faucetUrl: string; + menuCollapsed: boolean; selectNodeDialogVisibility: boolean; selectIndexerDialogVisibility: boolean; selectLanguageDialogVisibility: boolean; diff --git a/src/store/staking/actions.ts b/src/store/staking/actions.ts index 87ad66e25..75213ab6d 100644 --- a/src/store/staking/actions.ts +++ b/src/store/staking/actions.ts @@ -7,16 +7,6 @@ import type { Payouts } from '@sora-substrate/util/build/staking/types'; import type { Subscription } from 'rxjs'; const actions = defineActions({ - async bond(context): Promise { - const { state, getters } = stakingActionContext(context); - - const controller = state.controller || getters.stash; - - if (!state.payee) throw new Error('Payee is not set'); - - await api.staking.bond({ controller, value: state.stakeAmount, payee: state.payee }); - }, - async nominate(context): Promise { const { state, dispatch } = stakingActionContext(context); @@ -106,10 +96,6 @@ const actions = defineActions({ }); }, - async chill(context): Promise { - await api.staking.chill(); - }, - async getStakingInfo(context): Promise { const { getters, commit } = stakingActionContext(context); @@ -153,7 +139,7 @@ const actions = defineActions({ async getUnbondPeriod(context): Promise { const { commit } = stakingActionContext(context); - const unbondPeriod = await api.staking.getUnbondPeriod(); + const unbondPeriod = api.staking.getUnbondPeriod(); commit.setUnbondPeriod(unbondPeriod); }, @@ -161,7 +147,7 @@ const actions = defineActions({ async getMaxNominations(context): Promise { const { commit } = stakingActionContext(context); - const maxNominations = await api.staking.getMaxNominations(); + const maxNominations = api.staking.getMaxNominations(); commit.setMaxNominations(maxNominations); }, @@ -169,7 +155,7 @@ const actions = defineActions({ async getHistoryDepth(context): Promise { const { commit } = stakingActionContext(context); - const historyDepth = await api.staking.getHistoryDepth(); + const historyDepth = api.staking.getHistoryDepth(); commit.setHistoryDepth(historyDepth); }, @@ -179,7 +165,7 @@ const actions = defineActions({ commit.resetActiveEraUpdates(); - const observable = await api.staking.getActiveEraObservable(); + const observable = api.staking.getActiveEraObservable(); if (!observable) return; @@ -200,7 +186,7 @@ const actions = defineActions({ commit.resetCurrentEraUpdates(); - const observable = await api.staking.getCurrentEraObservable(); + const observable = api.staking.getCurrentEraObservable(); if (!observable) return; @@ -223,7 +209,7 @@ const actions = defineActions({ if (!state.currentEra) throw new Error('Current era is not set'); - const observable = await api.staking.getEraTotalStakeObservable(state.currentEra); + const observable = api.staking.getEraTotalStakeObservable(state.currentEra); if (!observable) return; @@ -244,7 +230,7 @@ const actions = defineActions({ commit.resetControllerUpdates(); - const observable = await api.staking.getControllerObservable(getters.stash); + const observable = api.staking.getControllerObservable(getters.stash); if (!observable) return; @@ -265,7 +251,7 @@ const actions = defineActions({ commit.resetPayeeUpdates(); - const observable = await api.staking.getPayeeObservable(getters.stash); + const observable = api.staking.getPayeeObservable(getters.stash); if (!observable) return; @@ -286,7 +272,7 @@ const actions = defineActions({ commit.resetNominationsUpdates(); - const observable = await api.staking.getNominationsObservable(getters.stash); + const observable = api.staking.getNominationsObservable(getters.stash); if (!observable) return; @@ -307,7 +293,7 @@ const actions = defineActions({ commit.resetAccountLedgerUpdates(); - const observable = await api.staking.getAccountLedgerObservable(getters.stash); + const observable = api.staking.getAccountLedgerObservable(getters.stash); if (!observable) return; diff --git a/src/store/swap/mutations.ts b/src/store/swap/mutations.ts index bc8b8f679..d230de1fd 100644 --- a/src/store/swap/mutations.ts +++ b/src/store/swap/mutations.ts @@ -67,6 +67,9 @@ const mutations = defineMutations()({ state.isAvailable = isAvailable; state.liquiditySources = liquiditySources; }, + setLiquiditySource(state, liquiditySource): void { + state.liquiditySources = [liquiditySource]; + }, selectDexId(state, dexId: number) { state.selectedDexId = dexId; }, diff --git a/src/store/types.ts b/src/store/types.ts index 4af26079f..c0f16b432 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -3,7 +3,7 @@ import type store from '@/store'; import type { VUEX_TYPES } from '@soramitsu/soraneo-wallet-web'; import type { VueDecorator } from 'vue-class-component'; -type BaseModuleDecorator = { +type BaseModuleDecorator = { router: VUEX_TYPES.BaseDecorator; web3: VUEX_TYPES.BaseDecorator; assets: VUEX_TYPES.BaseDecorator; @@ -19,6 +19,7 @@ type BaseModuleDecorator; demeterFarming: VUEX_TYPES.BaseDecorator; soraCard: VUEX_TYPES.BaseDecorator; + orderBook: VUEX_TYPES.BaseDecorator; }; export type StateDecorators = BaseModuleDecorator< @@ -36,7 +37,8 @@ export type StateDecorators = BaseModuleDecorator< typeof store.state.rewards, typeof store.state.staking, typeof store.state.demeterFarming, - typeof store.state.soraCard + typeof store.state.soraCard, + typeof store.state.orderBook > & VUEX_TYPES.WalletStateDecorators; @@ -55,7 +57,8 @@ export type GettersDecorators = BaseModuleDecorator< typeof store.getters.rewards, typeof store.getters.staking, typeof store.getters.demeterFarming, - typeof store.getters.soraCard + typeof store.getters.soraCard, + typeof store.getters.orderBook > & VUEX_TYPES.WalletGettersDecorators & { libraryDesignSystem: VueDecorator; libraryTheme: VueDecorator }; @@ -74,7 +77,8 @@ export type CommitDecorators = BaseModuleDecorator< typeof store.commit.rewards, typeof store.commit.staking, typeof store.commit.demeterFarming, - typeof store.commit.soraCard + typeof store.commit.soraCard, + typeof store.commit.orderBook > & VUEX_TYPES.WalletCommitDecorators; @@ -93,6 +97,7 @@ export type DispatchDecorators = BaseModuleDecorator< typeof store.dispatch.rewards, typeof store.dispatch.staking, typeof store.dispatch.demeterFarming, - typeof store.dispatch.soraCard + typeof store.dispatch.soraCard, + typeof store.dispatch.orderBook > & VUEX_TYPES.WalletDispatchDecorators; diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss index b7bd75250..5c861279b 100644 --- a/src/styles/_layout.scss +++ b/src/styles/_layout.scss @@ -7,6 +7,7 @@ $basic-spacing-medium: $basic-spacing-mini * 4; $inner-window-width: 464px; $inner-window-height: 217px; +$sidebar-max-width: 190px; $inner-spacing-mini: 8px; $inner-spacing-tiny: math.div($inner-spacing-mini, 2); diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 0d9028d3a..2aab15861 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -265,6 +265,7 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, text-align: center; &.is-active { background: var(--s-color-base-background); + box-shadow: var(--s-shadow-element); } @if $withBottomMargin == true { &:last-child { @@ -286,10 +287,6 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, position: relative; } - &.is-active { - box-shadow: var(--s-shadow-element); - } - .el-collapse-item__header { height: auto; min-height: #{$collapse-icon-height + $inner-spacing-medium * 2}; @@ -311,16 +308,22 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, font-weight: 600; } -@mixin scrollbar($marginOffset: 0, $verticalRight: 2px, $isLeft: false) { +@mixin scrollbar( + $marginOffset: 0, + $verticalRight: 2px, + $isLeft: false, + $withHorizontalScroll: false, + $hideVerticalScroll: false +) { &.el-scrollbar { margin-left: $marginOffset; margin-right: $marginOffset; & > .el-scrollbar__wrap { - overflow-x: hidden; - margin-bottom: 0px !important; // to disable element-ui negative margin - } - & > .el-scrollbar__wrap { + @if $withHorizontalScroll == false { + overflow-x: hidden; + margin-bottom: 0px !important; // to disable element-ui negative margin + } &, & > .el-scrollbar__view { display: flex; @@ -328,13 +331,15 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, flex-flow: column nowrap; } } - & > .el-scrollbar__bar { - &.is-vertical { - right: $verticalRight; + & > .el-scrollbar__bar.is-vertical { + right: $verticalRight; - @if $isLeft == true { - left: 0; - } + @if $isLeft == true { + left: 0; + } + + @if $hideVerticalScroll == true { + width: 0; } } } @@ -615,12 +620,22 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, &-address { display: flex; font-size: var(--s-font-size-extra-mini); + .tokens-item-address__value { + &.token-address { + font-size: var(--s-font-size-extra-mini); + font-weight: 400; + color: var(--s-color-base-content-primary); + } + } } &-tokens { display: flex; flex-flow: column nowrap; align-items: flex-end; } + &-token { + font-size: var(--s-font-size-small); + } &-price { font-size: var(--s-font-size-medium); white-space: nowrap; @@ -628,15 +643,6 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, &-amount.formatted-amount--fiat-value { color: var(--s-color-base-content-primary) !important; } - &-address { - .tokens-item-address__value { - &.token-address { - font-size: var(--s-font-size-extra-mini); - font-weight: 400; - color: var(--s-color-base-content-primary); - } - } - } } } @@ -679,12 +685,8 @@ $button-custom-shadow: -1px -1px 5px rgba(0, 0, 0, 0.05), 1px 1px 5px rgba(0, 0, &#{$tabs-class} { #{$tabs-class}__header { width: 100%; - } - #{$tabs-class} { - &__header { - #{$tabs-class}__item { - font-weight: 600; - } + #{$tabs-class}__item { + font-weight: 600; } } } diff --git a/src/styles/soramitsu-variables.scss b/src/styles/soramitsu-variables.scss index b9702008d..b3d33c7e3 100644 --- a/src/styles/soramitsu-variables.scss +++ b/src/styles/soramitsu-variables.scss @@ -1,40 +1,40 @@ -$s-color-brand-day: #A19A9D; // 0d0248 +$s-color-brand-day: #a19a9d; // 0d0248 // Primary theme colors -$s-color-theme-accent: #F8087B; // NEU bright pink fluro -$s-color-theme-accent-hover: #F754A3; // NEU pale quartz pink +$s-color-theme-accent: #f8087b; // NEU bright pink fluro +$s-color-theme-accent-hover: #f754a3; // NEU pale quartz pink $s-color-theme-accent-pressed: #bf065f; $s-color-theme-accent-focused: #ab0555; // Secondary theme colors -$s-color-theme-secondary: #44E5B2; -$s-color-theme-secondary-hover: #24DAA0; -$s-color-theme-secondary-pressed: #24DAA0; -$s-color-theme-secondary-focused: #24DAA0; +$s-color-theme-secondary: #44e5b2; +$s-color-theme-secondary-hover: #24daa0; +$s-color-theme-secondary-pressed: #24daa0; +$s-color-theme-secondary-focused: #24daa0; // Base content colors -$s-color-base-content-primary: #2A171F; -$s-color-base-content-secondary: #A19A9D; -$s-color-base-content-tertiary: #D5CDD0; +$s-color-base-content-primary: #2a171f; +$s-color-base-content-secondary: #a19a9d; +$s-color-base-content-tertiary: #d5cdd0; $s-color-base-content-quaternary: #75787b; // Base misc colors -$s-color-base-background: #FAF4F8; -$s-color-base-border-primary: #F7F3F4; -$s-color-base-border-secondary: #EDE4E7; -$s-color-base-background-hover: #F7F3F4; -$s-color-base-disabled: #FDF7FB; -$s-color-base-on-disabled: #A19A9D; -$s-color-base-on-accent: #FFFFFF; +$s-color-base-background: #faf4f8; +$s-color-base-border-primary: #f7f3f4; +$s-color-base-border-secondary: #ede4e7; +$s-color-base-background-hover: #f7f3f4; +$s-color-base-disabled: #fdf7fb; +$s-color-base-on-disabled: #a19a9d; +$s-color-base-on-accent: #ffffff; // Utility colors -$s-color-utility-body: #F7F3F4; -$s-color-utility-surface: #FDF7FB; +$s-color-utility-body: #f7f3f4; +$s-color-utility-surface: #fdf7fb; $s-color-utility-overlay: rgba(42, 23, 31, 0.1); // Status colors -$s-color-status-success: #34AD87; -$s-color-status-warning: #EBA332; -$s-color-status-error: #F754A3; -$s-color-status-info: #479AEF; -$s-color-status-success-background: #B9EBDB; -$s-color-status-warning-background: #FCEEBD; -$s-color-status-error-background: #FFD8EB; -$s-color-status-info-background: #C6E2FF; +$s-color-status-success: #34ad87; +$s-color-status-warning: #eba332; +$s-color-status-error: #f754a3; +$s-color-status-info: #479aef; +$s-color-status-success-background: #b9ebdb; +$s-color-status-warning-background: #fceebd; +$s-color-status-error-background: #ffd8eb; +$s-color-status-info-background: #c6e2ff; // Fiat colors $s-color-fiat-value: rgba(71, 154, 239, 1) !default; $s-color-rewards: #c0e2ff !default; @@ -46,7 +46,7 @@ $s-size-big: 58px; // Shadows $s-shadow-surface: 1px 1px 5px var(--s-shadow-color-dark), inset 1px 1px 1px var(--s-shadow-color-dark); $s-shadow-tooltip: 0px 1px 4px rgba(13, 2, 72, 0.35); -$s-shadow-tab: 1px 1px 2px #FFFFFF, inset 1px 1px 2px rgba(0, 0, 0, 0.1); // 0px 1px 1px rgba(0, 0, 0, 0.1); +$s-shadow-tab: 1px 1px 2px #ffffff, inset 1px 1px 2px rgba(0, 0, 0, 0.1); // 0px 1px 1px rgba(0, 0, 0, 0.1); $s-shadow-dropdown: 0px 4px 8px rgba(19, 19, 19, 0.15); $s-shadow-mobile-tap-bar: 0px -4px 8px rgba(19, 19, 19, 0.15); $s-shadow-mobile-side-menu: -4px 4px 8px rgba(19, 19, 19, 0.15); @@ -57,8 +57,8 @@ $s-border-radius-small: $s-border-radius-base * 1.5; $s-border-radius-medium: $s-border-radius-base * 2; $s-border-radius-big: $s-border-radius-base * 3; // Fonts paths -$s-font-family-default-path: '~@soramitsu/soramitsu-js-ui/lib/assets/fonts/Sora-VariableFont_wght.ttf' !default; -$s-font-family-mono-path: '~@soramitsu/soramitsu-js-ui/lib/assets/fonts/JetBrainsMono-Regular.woff' !default; +$s-font-family-default-path: '~@soramitsu-ui/ui-vue2/lib/assets/fonts/Sora-VariableFont_wght.ttf' !default; +$s-font-family-mono-path: '~@soramitsu-ui/ui-vue2/lib/assets/fonts/JetBrainsMono-Regular.woff' !default; $s-font-family-icons-path: '~@/assets/fonts/polkaswap_icons.ttf' !default; // Font Feature Settings $s-font-feature-settings-common: normal; @@ -75,9 +75,9 @@ $s-transition-default: all 0.125s ease-in-out; $s-asset-item-height: 71px; $s-asset-item-height--fiat: 86px; // override dark theme on disabled -$s-color-base-on-disabled--dark: #C29AB7; +$s-color-base-on-disabled--dark: #c29ab7; -@import "../../node_modules/@soramitsu/soramitsu-js-ui/lib/styles/index"; +@import '../../node_modules/@soramitsu-ui/ui-vue2/lib/styles/index'; :root { --s-color-brand-day: #{$s-color-brand-day}; diff --git a/src/types/chart.ts b/src/types/chart.ts new file mode 100644 index 000000000..b0db3887a --- /dev/null +++ b/src/types/chart.ts @@ -0,0 +1,8 @@ +/** "open", "close", "low", "high" data */ +export type OCLH = [number, number, number, number]; + +export type SnapshotItem = { + timestamp: number; + price: OCLH; + volume: number; +}; diff --git a/src/types/orderBook.ts b/src/types/orderBook.ts new file mode 100644 index 000000000..d0d4c073a --- /dev/null +++ b/src/types/orderBook.ts @@ -0,0 +1,47 @@ +import { INDEXER_TYPES } from '@soramitsu/soraneo-wallet-web'; + +import type { OrderBookId, PriceVariant } from '@sora-substrate/liquidity-proxy'; +import type { FPNumber, CodecString } from '@sora-substrate/util'; +import type { LimitOrder } from '@sora-substrate/util/build/orderBook/types'; + +export enum Filter { + open = 'open', + all = 'all', + executed = 'executed', +} + +export enum Cancel { + multiple = 'multiple', + all = 'all', +} + +export const OrderStatus = INDEXER_TYPES.OrderStatus; + +export type OrderBookDealData = { + timestamp: number; + side: PriceVariant; + price: FPNumber; + amount: FPNumber; +}; + +export type OrderBookStats = { + baseAssetReserves?: CodecString; + quoteAssetReserves?: CodecString; + price: FPNumber; + priceChange: FPNumber; + volume: FPNumber; + status: string; +}; + +export type OrderBookWithStats = { + id: OrderBookId; + stats: OrderBookStats; +}; + +export type OrderBookUpdateData = OrderBookWithStats & { + deals: OrderBookDealData[]; +}; + +export type OrderData = LimitOrder & { + status: string; +}; diff --git a/src/types/tabs.ts b/src/types/tabs.ts index ce9b64459..fb92f0756 100644 --- a/src/types/tabs.ts +++ b/src/types/tabs.ts @@ -1,3 +1,8 @@ +export enum OrderBookTabs { + Limit = 'limit', + Market = 'market', +} + export enum AlertTypeTabs { Drop = 'drop', Raise = 'raise', diff --git a/src/utils/bridge/eth/utils.ts b/src/utils/bridge/eth/utils.ts index 3ba446d2c..1feed0caf 100644 --- a/src/utils/bridge/eth/utils.ts +++ b/src/utils/bridge/eth/utils.ts @@ -122,7 +122,7 @@ export async function getIncomingEvmTransactionData({ asset, value, recipient, g const amount = new FPNumber(value, asset.externalDecimals).toCodecString(); - const contractAddress = getContractAddress(KnownEthBridgeAsset.Other)!; + const contractAddress = getContractAddress(KnownEthBridgeAsset.Other) as string; const contractAbi = SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other].abi; const contract = new ethers.Contract(contractAddress, contractAbi, signer); @@ -159,7 +159,7 @@ export async function getOutgoingEvmTransactionData({ const symbol = asset.symbol as KnownEthBridgeAsset; const isValOrXor = [KnownEthBridgeAsset.XOR, KnownEthBridgeAsset.VAL].includes(symbol); const bridgeAsset: KnownEthBridgeAsset = isValOrXor ? symbol : KnownEthBridgeAsset.Other; - const contractAddress = getContractAddress(bridgeAsset)!; + const contractAddress = getContractAddress(bridgeAsset) as string; const contractAbi = SmartContracts[SmartContractType.EthBridge][bridgeAsset].abi; const contract = new ethers.Contract(contractAddress, contractAbi, signer); @@ -267,10 +267,10 @@ export async function getEthNetworkFee( try { const { contract, method, args } = await getIncomingEvmTransactionData(txParams); - const signer = contract.runner!; + const signer = contract.runner; const tx = await contract[method].populateTransaction(...args); - txGasLimit = await signer.estimateGas!(tx); + txGasLimit = (await signer?.estimateGas?.(tx)) ?? BigInt(0); } catch { txGasLimit = getEthBridgeIncomingGasLimit(asset.externalAddress); } diff --git a/src/utils/bridge/sub/classes/adapter.ts b/src/utils/bridge/sub/classes/adapter.ts index 01c6fe43a..3a9fb4393 100644 --- a/src/utils/bridge/sub/classes/adapter.ts +++ b/src/utils/bridge/sub/classes/adapter.ts @@ -269,6 +269,13 @@ export class SubNetworksConnector { protected getConnection( network: SubNetwork, connectorAdapter?: Adapter + ): SubNetworkConnection; + + protected getConnection(network: undefined, connectorAdapter?: Adapter): undefined; + + protected getConnection( + network?: SubNetwork, + connectorAdapter?: Adapter ): SubNetworkConnection | undefined { if (!network) return undefined; @@ -312,7 +319,7 @@ export class SubNetworksConnector { public async init(destination: SubNetwork, connector?: SubNetworksConnector): Promise { const [soraParachain, relaychain, parachain] = this.getChains(destination); // Create adapters - this.soraParachain = this.getConnection(soraParachain, connector?.soraParachain?.adapter)!; + this.soraParachain = this.getConnection(soraParachain, connector?.soraParachain?.adapter); this.relaychain = this.getConnection(relaychain, connector?.relaychain?.adapter); this.parachain = this.getConnection(parachain, connector?.parachain?.adapter); // link destination network diff --git a/src/utils/bridge/sub/classes/history.ts b/src/utils/bridge/sub/classes/history.ts index 0f88e4dcd..7799c01c2 100644 --- a/src/utils/bridge/sub/classes/history.ts +++ b/src/utils/bridge/sub/classes/history.ts @@ -25,7 +25,7 @@ import type { ActionContext } from 'vuex'; const hasFinishedState = (item: Nullable) => { if (!item) return false; - return [BridgeTxStatus.Done, BridgeTxStatus.Failed].includes(item.transactionState!); + return [BridgeTxStatus.Done, BridgeTxStatus.Failed].includes(item.transactionState as BridgeTxStatus); }; const getType = (isOutgoing: boolean) => { @@ -252,7 +252,7 @@ class SubBridgeHistory extends SubNetworksConnector { history.transactionState = BridgeTxStatus.Failed; } - history.externalNetwork = subBridgeApi.getSoraParachain(history.externalNetwork!); + history.externalNetwork = subBridgeApi.getSoraParachain(history.externalNetwork as SubNetwork); history.externalBlockId = history.parachainBlockId; history.to = formatSubAddress(history.to as string, this.parachainApi.registry.chainSS58 as number); history.parachainBlockId = undefined; // parachain is external network @@ -374,7 +374,7 @@ class SubBridgeHistory extends SubNetworksConnector { ); const signer = feeEvent.event.data[0].toString(); // signer is spent balance for fee - history.externalNetwork = subBridgeApi.getSoraParachain(history.externalNetwork!); + history.externalNetwork = subBridgeApi.getSoraParachain(history.externalNetwork as SubNetwork); history.externalNetworkFee = feeEvent.event.data[1].toString(); history.externalBlockId = parachainBlockId; history.to = formatSubAddress(signer, this.parachainApi.registry.chainSS58 as number); diff --git a/src/utils/index.ts b/src/utils/index.ts index b8ab25656..3ce3cb43b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,6 +2,7 @@ import { FPNumber, CodecString } from '@sora-substrate/util'; import { isNativeAsset } from '@sora-substrate/util/build/assets'; import { XOR } from '@sora-substrate/util/build/assets/consts'; import { api, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; +import scrollbarWidth from 'element-ui/src/utils/scrollbar-width'; import debounce from 'lodash/debounce'; import { app, ZeroStringValue } from '@/consts'; @@ -20,6 +21,12 @@ type AssetWithBalance = AccountAsset | RegisteredAccountAsset; type PoolAssets = { baseAsset: T; poolAsset: T }; +export async function waitUntil(condition: () => boolean): Promise { + if (condition()) return; + await delay(250); + await waitUntil(condition); +} + export async function waitForSoraNetworkFromEnv(): Promise { return new Promise((resolve) => { store.original.watch( @@ -374,3 +381,5 @@ export const sortPools = (a: PoolAssets, b: PoolAssets) = return byBaseAsset === 0 ? sortAssets(a.poolAsset, b.poolAsset) : byBaseAsset; }; + +export const calcElScrollGutter: () => number = scrollbarWidth; diff --git a/src/utils/orderBook.ts b/src/utils/orderBook.ts new file mode 100644 index 000000000..f64490168 --- /dev/null +++ b/src/utils/orderBook.ts @@ -0,0 +1,9 @@ +import type { OrderBook } from '@sora-substrate/liquidity-proxy'; + +export const MAX_ORDERS_PER_SIDE = 1024; +export const MAX_ORDERS_PER_USER = 1024; +export const MAX_ORDERS_PER_SINGLE_PRICE = 1024; + +export function getBookDecimals(orderBook: Nullable): number { + return orderBook?.stepLotSize?.toString().split('.')[1]?.length ?? 2; +} diff --git a/src/views/About.vue b/src/views/About.vue index c62dfec4b..142098875 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -158,7 +158,7 @@ import TranslationMixin from '@/components/mixins/TranslationMixin'; import Web3Logo from '@/components/shared/Logo/Web3.vue'; import { getter } from '@/store/decorators'; -import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; +import type Theme from '@soramitsu-ui/ui-vue2/lib/types/Theme'; @Component({ components: { diff --git a/src/views/Bridge.vue b/src/views/Bridge.vue index dbfc82c67..9f69c557a 100644 --- a/src/views/Bridge.vue +++ b/src/views/Bridge.vue @@ -78,7 +78,9 @@ v-if="changeSenderWalletEvm" class="connect-wallet-btn disconnect" @click="resetEvmProviderConnection" - >{{ t('disconnectWalletText') }} + > + {{ t('disconnectWalletText') }} +
{{ t('disconnectWalletText') }} + > + {{ t('disconnectWalletText') }} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/src/views/Explore/Container.vue b/src/views/Explore/Container.vue index 8503aebda..7c8747dec 100644 --- a/src/views/Explore/Container.vue +++ b/src/views/Explore/Container.vue @@ -69,12 +69,16 @@ export default class ExploreContainer extends Mixins(mixins.LoadingMixin, Transl } get tabs(): Array<{ name: string; label: string }> { - return [PageNames.ExploreFarming, PageNames.ExplorePools, PageNames.ExploreStaking, PageNames.ExploreTokens].map( - (name) => ({ - name, - label: this.t(`pageTitle.${name}`), - }) - ); + return [ + PageNames.ExploreFarming, + PageNames.ExplorePools, + PageNames.ExploreStaking, + PageNames.ExploreTokens, + PageNames.ExploreBooks, + ].map((name) => ({ + name, + label: this.t(`pageTitle.${name}`), + })); } get pageName(): string { @@ -87,7 +91,11 @@ export default class ExploreContainer extends Mixins(mixins.LoadingMixin, Transl /** Shown only for logged in users and for any tab on page except Tokens */ get switcherAvailable(): boolean { - return this.pageName !== PageNames.ExploreTokens && this.isLoggedIn; + if (!this.isLoggedIn) return false; + + return [PageNames.ExploreFarming, PageNames.ExplorePools, PageNames.ExploreStaking].includes( + this.pageName as PageNames + ); } handleTabChange(name: string): void { diff --git a/src/views/Explore/Demeter.vue b/src/views/Explore/Demeter.vue index f3cf31ad0..31b928ccc 100644 --- a/src/views/Explore/Demeter.vue +++ b/src/views/Explore/Demeter.vue @@ -109,7 +109,7 @@ value-can-be-hidden :font-size-rate="FontSizeRate.SMALL" :value="balance" - class="explore-table-item-price explore-table-item-amount" + class="explore-table-item-token" >