From ee11e85ac6405c6f84927d1320e3bd77547ecb90 Mon Sep 17 00:00:00 2001 From: Nikita Polyakov <53777036+Nikita-Polyakov@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:44:12 +0300 Subject: [PATCH] Add links to bridge transfer modal (#1362) * add links to modal, update BridgeTransactionMixin * fix sub tx recipientAmount in reducer * account links in BridgeTransactionView * refactoring transaction view links * add node connection allowance timeout * return app align center * remove rococo block explorer url * fix ts issue * refactoring sonar issues * remove isOutgoingType from mixin * remove unused code --- src/App.vue | 18 +- src/components/App/Settings/Node/NodeInfo.vue | 19 +- .../App/Settings/Node/SelectNode.vue | 9 +- .../App/Settings/Node/SelectNodeDialog.vue | 6 + src/components/mixins/BridgeHistoryMixin.ts | 6 +- .../mixins/BridgeTransactionMixin.ts | 143 +++++++--- .../mixins/NetworkFormatterMixin.ts | 143 ++++++++-- .../pages/Bridge/TransferNotification.vue | 49 +++- .../pages/Moonpay/MoonpayHistory.vue | 7 - src/consts/sub.ts | 4 +- src/store/assets/getters.ts | 6 +- src/utils/bridge/sub/classes/history.ts | 2 +- src/utils/bridge/sub/classes/reducers.ts | 6 +- src/utils/connection/index.ts | 27 +- src/utils/index.ts | 20 +- src/views/Bridge.vue | 2 +- src/views/BridgeTransaction.vue | 264 ++++++------------ src/views/BridgeTransactionsHistory.vue | 10 +- 18 files changed, 433 insertions(+), 308 deletions(-) diff --git a/src/App.vue b/src/App.vue index 121228001..607d49cac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -573,14 +573,16 @@ i.icon-divider { } @include desktop { - .app-main { - .app-menu { - &:not(.collapsed) { - position: relative; - } - &.collapsed { - & + .app-body { - margin-left: 74px; + .app-main--swap { + &.app-main { + .app-menu { + &:not(.collapsed) { + position: relative; + } + &.collapsed { + & + .app-body { + margin-left: 74px; + } } } } diff --git a/src/components/App/Settings/Node/NodeInfo.vue b/src/components/App/Settings/Node/NodeInfo.vue index 5aa06a62c..ea8ce8fa6 100644 --- a/src/components/App/Settings/Node/NodeInfo.vue +++ b/src/components/App/Settings/Node/NodeInfo.vue @@ -109,14 +109,15 @@ const stripEndingSlash = (str: string): string => (str.charAt(str.length - 1) == }, }) export default class NodeInfo extends Mixins(TranslationMixin) { - @Prop({ default: () => {}, type: Function }) handleBack!: FnWithoutArgs; - @Prop({ default: () => {}, type: Function }) handleNode!: (node: any, isNewNode: boolean) => void; - @Prop({ default: () => {}, type: Function }) removeNode!: (node: any) => void; - @Prop({ default: () => ({}), type: Object }) node!: Node; - @Prop({ default: false, type: Boolean }) existing!: boolean; - @Prop({ default: false, type: Boolean }) removable!: boolean; - @Prop({ default: false, type: Boolean }) connected!: boolean; - @Prop({ default: false, type: Boolean }) showTutorial!: boolean; + @Prop({ default: () => {}, type: Function }) readonly handleBack!: FnWithoutArgs; + @Prop({ default: () => {}, type: Function }) readonly handleNode!: (node: any, isNewNode: boolean) => void; + @Prop({ default: () => {}, type: Function }) readonly removeNode!: (node: any) => void; + @Prop({ default: () => ({}), type: Object }) readonly node!: Node; + @Prop({ default: false, type: Boolean }) readonly existing!: boolean; + @Prop({ default: false, type: Boolean }) readonly removable!: boolean; + @Prop({ default: false, type: Boolean }) readonly connected!: boolean; + @Prop({ default: false, type: Boolean }) readonly showTutorial!: boolean; + @Prop({ default: false, type: Boolean }) readonly disabled!: boolean; @Prop({ default: '', type: String }) readonly nodeAddressConnecting!: string; @Ref('nodeNameInput') private readonly nodeNameInput!: HTMLInputElement; @@ -172,7 +173,7 @@ export default class NodeInfo extends Mixins(TranslationMixin) { } get buttonDisabled(): boolean { - return this.connected && !this.nodeDataChanged; + return this.disabled || (this.connected && !this.nodeDataChanged); } get buttonType(): string { diff --git a/src/components/App/Settings/Node/SelectNode.vue b/src/components/App/Settings/Node/SelectNode.vue index 799abf554..6f481f620 100644 --- a/src/components/App/Settings/Node/SelectNode.vue +++ b/src/components/App/Settings/Node/SelectNode.vue @@ -7,7 +7,7 @@ :key="node.address" :label="node.address" :value="node.address" - :disabled="node.address === nodeAddressConnecting" + :disabled="disabled || isConnecting(node.address)" size="medium" class="select-node-list__item s-flex" > @@ -30,7 +30,7 @@ icon="arrows-swap-90-24" @click.stop="handleNode(node)" /> - + {}, type: Function }) readonly handleNode!: (node?: Node) => void; @Prop({ default: () => {}, type: Function }) readonly viewNode!: (node?: Node) => void; @Prop({ default: '', type: String }) readonly nodeAddressConnecting!: string; + @Prop({ default: false, type: Boolean }) readonly disabled!: boolean; @ModelSync('value', 'input', { type: String }) readonly currentAddressValue!: string; @@ -75,6 +76,10 @@ export default class SelectNode extends Mixins(TranslationMixin) { return `${location.name} ${flag}`; } + isConnecting(address: string) { + return address === this.nodeAddressConnecting; + } + getTitle(node: Node) { const { name, chain } = node; diff --git a/src/components/App/Settings/Node/SelectNodeDialog.vue b/src/components/App/Settings/Node/SelectNodeDialog.vue index 66c20c182..04977f804 100644 --- a/src/components/App/Settings/Node/SelectNodeDialog.vue +++ b/src/components/App/Settings/Node/SelectNodeDialog.vue @@ -11,6 +11,7 @@ :nodes="connection.nodeList" :handle-node="handleNode" :view-node="navigateToNodeInfo" + :disabled="!connectionAllowance" /> @@ -81,6 +83,10 @@ export default class SelectNodeDialog extends Mixins(NodeErrorMixin, mixins.Load return this.connection.nodeAddressConnecting; } + get connectionAllowance(): boolean { + return this.connection.connectionAllowance; + } + get connectedNodeAddress(): string { if (this.nodeAddressConnecting) return ''; diff --git a/src/components/mixins/BridgeHistoryMixin.ts b/src/components/mixins/BridgeHistoryMixin.ts index 3770ee3ab..37ad1a283 100644 --- a/src/components/mixins/BridgeHistoryMixin.ts +++ b/src/components/mixins/BridgeHistoryMixin.ts @@ -26,10 +26,6 @@ export default class BridgeHistoryMixin extends Mi @action.bridge.updateInternalHistory updateInternalHistory!: FnWithoutArgs; @action.bridge.updateExternalHistory updateExternalHistory!: (clearHistory?: boolean) => Promise; - isOutgoingType(type: Operation): boolean { - return isOutgoingTransaction({ type } as IBridgeTransaction); - } - async showHistory(id?: string): Promise { if (!id) { this.handleBack(); @@ -38,7 +34,7 @@ export default class BridgeHistoryMixin extends Mi const tx = this.history[id as string]; // to display actual fees in BridgeTransaction - this.setSoraToEvm(this.isOutgoingType(tx.type)); + this.setSoraToEvm(isOutgoingTransaction(tx)); await this.setAssetAddress(tx.assetAddress); this.setHistoryId(tx.id); diff --git a/src/components/mixins/BridgeTransactionMixin.ts b/src/components/mixins/BridgeTransactionMixin.ts index 6fa9ae569..4a225074f 100644 --- a/src/components/mixins/BridgeTransactionMixin.ts +++ b/src/components/mixins/BridgeTransactionMixin.ts @@ -1,53 +1,108 @@ -import { BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; +import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; import { WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; import { Component, Mixins } from 'vue-property-decorator'; -import TranslationMixin from '@/components/mixins/TranslationMixin'; -import { isOutgoingTransaction } from '@/utils/bridge/common/utils'; -import { isUnsignedToPart } from '@/utils/bridge/eth/utils'; +import NetworkFormatterMixin from '@/components/mixins/NetworkFormatterMixin'; +import { soraExplorerLinks } from '@/utils'; import type { IBridgeTransaction } from '@sora-substrate/util'; - -const { ETH_BRIDGE_STATES } = WALLET_CONSTS; +import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; @Component -export default class BridgeTransactionMixin extends Mixins(TranslationMixin) { - isFailedState(item: Nullable): boolean { - if (!(item && item.transactionState)) return false; - // ETH - if ( - [ETH_BRIDGE_STATES.EVM_REJECTED as string, ETH_BRIDGE_STATES.SORA_REJECTED as string].includes( - item.transactionState - ) - ) - return true; - // EVM - if (item.transactionState === BridgeTxStatus.Failed) return true; - // OTHER - return false; - } - - isSuccessState(item: Nullable): boolean { - if (!item) return false; - // ETH - if ( - item.transactionState === - (isOutgoingTransaction(item) ? ETH_BRIDGE_STATES.EVM_COMMITED : ETH_BRIDGE_STATES.SORA_COMMITED) - ) - return true; - // EVM - if (item.transactionState === BridgeTxStatus.Done) return true; - // OTHER - return false; - } - - isWaitingForAction(item: Nullable): boolean { - if (!item) return false; - // ETH - return item.transactionState === ETH_BRIDGE_STATES.EVM_REJECTED && isUnsignedToPart(item); - } - - formatDatetime(item: Nullable): string { - return this.formatDate(item?.startTime ?? Date.now()); +export default class BridgeTransactionMixin extends Mixins(NetworkFormatterMixin) { + get tx(): Nullable { + console.warn('[BridgeTransactionMixin] "tx" computed property is not implemented'); + return null; + } + + get isOutgoing(): boolean { + return this.isOutgoingTx(this.tx); + } + + get isEvmTxType(): boolean { + return ( + !!this.externalNetworkType && [BridgeNetworkType.Eth, BridgeNetworkType.Evm].includes(this.externalNetworkType) + ); + } + + get txSoraAccount(): string { + return this.tx?.from ?? ''; + } + + get txExternalAccount(): string { + return this.tx?.to ?? ''; + } + + get txSoraId(): string { + return this.tx?.txId ?? ''; + } + + get txSoraBlockId(): string { + return this.tx?.blockId ?? ''; + } + + get txSoraHash(): string { + return this.tx?.hash ?? ''; + } + + get txInternalHash(): string { + if (!this.isOutgoing) return this.txSoraHash; + + return this.txSoraHash || this.txSoraBlockId || this.txSoraId; + } + + get txExternalHash(): string { + return this.tx?.externalHash ?? this.txExternalBlockId; + } + + get txExternalBlockId(): string { + return this.tx?.externalBlockId ?? ''; + } + + get externalNetworkType(): Nullable { + return this.tx?.externalNetworkType; + } + + get externalNetworkId(): Nullable { + return this.tx?.externalNetwork; + } + + get soraExplorerLinks(): Array { + return soraExplorerLinks(this.soraNetwork, this.txSoraId, this.txSoraBlockId); + } + + get externalExplorerLinks(): Array { + if (!(this.externalNetworkType && this.externalNetworkId)) return []; + + return this.getNetworkExplorerLinks( + this.externalNetworkType, + this.externalNetworkId, + this.txExternalHash, + this.txExternalBlockId, + this.EvmLinkType.Transaction + ); + } + + get internalAccountLinks(): Array { + return soraExplorerLinks(this.soraNetwork, this.txSoraAccount, this.txSoraBlockId, true); + } + + get externalAccountLinks(): Array { + if (!(this.externalNetworkType && this.externalNetworkId)) return []; + + return this.getNetworkExplorerLinks( + this.externalNetworkType, + this.externalNetworkId, + this.txExternalAccount, + this.txExternalBlockId, + this.EvmLinkType.Account + ); + } + + getNetworkText(text: string, networkId?: Nullable, approximate = false): string { + const network = networkId ? this.getNetworkName(this.externalNetworkType, networkId) : this.TranslationConsts.Sora; + const approx = approximate ? this.TranslationConsts.Max : ''; + + return [approx, network, text].filter((item) => !!item).join(' '); } } diff --git a/src/components/mixins/NetworkFormatterMixin.ts b/src/components/mixins/NetworkFormatterMixin.ts index f3084674b..9f2a0de6f 100644 --- a/src/components/mixins/NetworkFormatterMixin.ts +++ b/src/components/mixins/NetworkFormatterMixin.ts @@ -1,4 +1,4 @@ -import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; +import { BridgeNetworkType, BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; import { EvmNetworkId } from '@sora-substrate/util/build/bridgeProxy/evm/consts'; import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; import { WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; @@ -9,11 +9,83 @@ import { SUB_NETWORKS } from '@/consts/sub'; import { state, getter } from '@/store/decorators'; import type { AvailableNetwork } from '@/store/web3/types'; import type { NetworkData } from '@/types/bridge'; +import { isOutgoingTransaction } from '@/utils/bridge/common/utils'; +import { isUnsignedToPart } from '@/utils/bridge/eth/utils'; import TranslationMixin from './TranslationMixin'; +import type { IBridgeTransaction } from '@sora-substrate/util'; import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; +const { ETH_BRIDGE_STATES } = WALLET_CONSTS; + +const getSubNetworkLinks = ( + networkData: NetworkData, + type: EvmLinkType, + value: string, + blockHash: string +): WALLET_CONSTS.ExplorerLink[] => { + const links: Array = []; + const explorerUrl = networkData.blockExplorerUrls[0]; + + switch (type) { + case EvmLinkType.Account: { + if (explorerUrl) { + links.push({ + type: WALLET_CONSTS.ExplorerType.Subscan, + value: `${explorerUrl}/account/${value}`, + }); + } + break; + } + case EvmLinkType.Transaction: { + if (explorerUrl) { + const path = value === blockHash ? 'block' : 'extrinsic'; + links.push({ + type: WALLET_CONSTS.ExplorerType.Subscan, + value: `${explorerUrl}/${path}/${value}`, + }); + } + + if (blockHash) { + const networkUrl = networkData.nodes?.[0].address; + const polkadotBaseLink = `https://polkadot.js.org/apps/?rpc=${networkUrl}#/explorer/query`; + const polkadotLink = { + type: WALLET_CONSTS.ExplorerType.Polkadot, + value: `${polkadotBaseLink}/${blockHash}`, + }; + + links.push(polkadotLink); + } + break; + } + } + + return links; +}; + +const getEvmNetworkLinks = ( + networkData: NetworkData, + type: EvmLinkType, + value: string, + _blockHash: string +): WALLET_CONSTS.ExplorerLink[] => { + const links: Array = []; + const explorerUrl = networkData.blockExplorerUrls[0]; + + if (explorerUrl) { + const path = type === EvmLinkType.Transaction ? 'tx' : 'address'; + const etherscanLink = { + type: 'etherscan' as WALLET_CONSTS.ExplorerType, + value: `${explorerUrl}/${path}/${value}`, + }; + + links.push(etherscanLink); + } + + return links; +}; + @Component export default class NetworkFormatterMixin extends Mixins(TranslationMixin) { @state.wallet.settings.soraNetwork soraNetwork!: Nullable; @@ -104,7 +176,7 @@ export default class NetworkFormatterMixin extends Mixins(TranslationMixin) { value: string, blockHash = '', type = EvmLinkType.Transaction - ) { + ): Array { if (!value) return []; const networkData = this.availableNetworks[networkType][networkId]?.data; @@ -114,36 +186,51 @@ export default class NetworkFormatterMixin extends Mixins(TranslationMixin) { return []; } - if (networkType === BridgeNetworkType.Sub) { - if (type === EvmLinkType.Account) return []; + return networkType === BridgeNetworkType.Sub + ? getSubNetworkLinks(networkData, type, value, blockHash) + : getEvmNetworkLinks(networkData, type, value, blockHash); + } - if (!blockHash) return []; + isOutgoingTx(item: Nullable): boolean { + return isOutgoingTransaction(item); + } - const explorerUrl = networkData.nodes?.[0].address; - const baseLink = `https://polkadot.js.org/apps/?rpc=${explorerUrl}#/explorer/query`; + isFailedState(item: Nullable): boolean { + if (!item?.transactionState) return false; + // ETH + if ( + [ETH_BRIDGE_STATES.EVM_REJECTED as string, ETH_BRIDGE_STATES.SORA_REJECTED as string].includes( + item.transactionState + ) + ) + return true; + // EVM + if (item.transactionState === BridgeTxStatus.Failed) return true; + // OTHER + return false; + } - return [ - { - type: WALLET_CONSTS.ExplorerType.Polkadot, - value: `${baseLink}/${blockHash}`, - }, - ]; - } else { - const explorerUrl = networkData.blockExplorerUrls[0]; - - if (!explorerUrl) { - console.error(`"blockExplorerUrls" is not provided for network id "${networkId}"`); - return []; - } + isSuccessState(item: Nullable): boolean { + if (!item) return false; + // ETH + if ( + item.transactionState === + (this.isOutgoingTx(item) ? ETH_BRIDGE_STATES.EVM_COMMITED : ETH_BRIDGE_STATES.SORA_COMMITED) + ) + return true; + // EVM + if (item.transactionState === BridgeTxStatus.Done) return true; + // OTHER + return false; + } - const path = type === EvmLinkType.Transaction ? 'tx' : 'address'; + isWaitingForAction(item: Nullable): boolean { + if (!item) return false; + // ETH + return item.transactionState === ETH_BRIDGE_STATES.EVM_REJECTED && isUnsignedToPart(item); + } - return [ - { - type: this.TranslationConsts.Etherscan as WALLET_CONSTS.ExplorerType, - value: `${explorerUrl}/${path}/${value}`, - }, - ]; - } + formatDatetime(item: Nullable): string { + return this.formatDate(item?.startTime ?? Date.now()); } } diff --git a/src/components/pages/Bridge/TransferNotification.vue b/src/components/pages/Bridge/TransferNotification.vue index 181189846..f3559f777 100644 --- a/src/components/pages/Bridge/TransferNotification.vue +++ b/src/components/pages/Bridge/TransferNotification.vue @@ -2,6 +2,11 @@ + + + + + {{ t('bridgeTransferNotification.addToken', { symbol: assetSymbol }) }}
@@ -15,32 +20,39 @@ diff --git a/src/views/BridgeTransactionsHistory.vue b/src/views/BridgeTransactionsHistory.vue index e2f49676e..60eb20d1c 100644 --- a/src/views/BridgeTransactionsHistory.vue +++ b/src/views/BridgeTransactionsHistory.vue @@ -38,14 +38,14 @@ /> {{ t('bridgeTransaction.for') }}
@@ -79,10 +79,9 @@ import { Component, Mixins } from 'vue-property-decorator'; import BridgeHistoryMixin from '@/components/mixins/BridgeHistoryMixin'; import BridgeMixin from '@/components/mixins/BridgeMixin'; -import BridgeTransactionMixin from '@/components/mixins/BridgeTransactionMixin'; import NetworkFormatterMixin from '@/components/mixins/NetworkFormatterMixin'; -import { Components, PageNames } from '@/consts'; -import router, { lazyComponent } from '@/router'; +import { Components } from '@/consts'; +import { lazyComponent } from '@/router'; import type { BridgeRegisteredAsset } from '@/store/assets/types'; import { state } from '@/store/decorators'; @@ -99,7 +98,6 @@ import type { IBridgeTransaction } from '@sora-substrate/util'; }) export default class BridgeTransactionsHistory extends Mixins( BridgeMixin, - BridgeTransactionMixin, BridgeHistoryMixin, NetworkFormatterMixin, mixins.PaginationSearchMixin,