diff --git a/package.json b/package.json index c4a94cd06..d7f115660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polkaswap-exchange-web", - "version": "1.21.1", + "version": "1.22.0", "repository": { "type": "git", "url": "https://github.com/sora-xor/polkaswap-exchange-web.git" @@ -24,7 +24,7 @@ }, "dependencies": { "@metamask/detect-provider": "^2.0.0", - "@soramitsu/soraneo-wallet-web": "1.21.12", + "@soramitsu/soraneo-wallet-web": "1.22.2", "@walletconnect/web3-provider": "^1.8.0", "core-js": "^3.26.0", "country-code-emoji": "^2.3.0", diff --git a/src/components/App/Settings/Language/SelectLanguageDialog.vue b/src/components/App/Settings/Language/SelectLanguageDialog.vue index 476d61e7f..57d7e59d3 100644 --- a/src/components/App/Settings/Language/SelectLanguageDialog.vue +++ b/src/components/App/Settings/Language/SelectLanguageDialog.vue @@ -10,7 +10,7 @@ size="medium" class="select-language-list__item s-flex" > -
+
{{ lang.value }}
@@ -26,7 +26,7 @@ + + diff --git a/src/components/pages/Bridge/SelectAccount.vue b/src/components/pages/Bridge/SelectAccount.vue index 50e2443be..d37e90536 100644 --- a/src/components/pages/Bridge/SelectAccount.vue +++ b/src/components/pages/Bridge/SelectAccount.vue @@ -20,7 +20,7 @@ import { api, mixins, components } from '@soramitsu/soraneo-wallet-web'; import { Component, Mixins, Watch } from 'vue-property-decorator'; import TranslationMixin from '@/components/mixins/TranslationMixin'; -import { action, state, mutation } from '@/store/decorators'; +import { state, mutation } from '@/store/decorators'; @Component({ components: { @@ -32,7 +32,7 @@ export default class BridgeSelectAccount extends Mixins(mixins.LoadingMixin, Tra @state.web3.subAddress private subAddress!: string; @state.web3.selectAccountDialogVisibility private selectAccountDialogVisibility!: boolean; @mutation.web3.setSelectAccountDialogVisibility private setSelectAccountDialogVisibility!: (flag: boolean) => void; - @action.web3.connectSubAccount private connectSubAccount!: (address: string) => Promise; + @mutation.web3.setSubAddress private setSubAddress!: (address: string) => Promise; address = ''; @@ -50,11 +50,17 @@ export default class BridgeSelectAccount extends Mixins(mixins.LoadingMixin, Tra } get validAddress(): boolean { - return !!this.address && api.validateAddress(this.address); + if (!(this.address && api.validateAddress(this.address))) return false; + try { + api.formatAddress(this.address); + return true; // if it can be formatted -> it's correct + } catch { + return false; // EVM account address + } } handleSelectAddress(): void { - this.connectSubAccount(this.address); + this.setSubAddress(this.address); this.visibility = false; } } diff --git a/src/components/pages/Bridge/SelectAsset.vue b/src/components/pages/Bridge/SelectAsset.vue index 29e781aab..7d2baf83a 100644 --- a/src/components/pages/Bridge/SelectAsset.vue +++ b/src/components/pages/Bridge/SelectAsset.vue @@ -65,8 +65,10 @@ export default class BridgeSelectAsset extends Mixins(TranslationMixin, SelectAs get assetsList(): Array { const assetsAddresses = Object.keys(this.registeredAssets); const excludeAddress = this.asset?.address; + const list = this.getAssetsWithBalances(assetsAddresses, excludeAddress); + const orderedList = [...list].sort(this.sortByBalance); - return this.getAssetsWithBalances(assetsAddresses, excludeAddress).sort(this.sortByBalance); + return orderedList; } get filteredAssets(): Array { diff --git a/src/components/pages/Bridge/SelectNetwork.vue b/src/components/pages/Bridge/SelectNetwork.vue index 96b3548ab..ec1214899 100644 --- a/src/components/pages/Bridge/SelectNetwork.vue +++ b/src/components/pages/Bridge/SelectNetwork.vue @@ -25,6 +25,7 @@ import NetworkFormatterMixin from '@/components/mixins/NetworkFormatterMixin'; import { action, mutation, state } from '@/store/decorators'; import type { AvailableNetwork } from '@/store/web3/types'; +import type { SubNetwork } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; type NetworkItem = { @@ -47,10 +48,12 @@ export default class BridgeSelectNetwork extends Mixins(NetworkFormatterMixin) { @state.web3.networkSelected private networkSelected!: Nullable; @state.web3.selectNetworkDialogVisibility private selectNetworkDialogVisibility!: boolean; - @mutation.web3.setNetworkType private setNetworkType!: (networkType: BridgeNetworkType) => void; @mutation.web3.setSelectNetworkDialogVisibility private setSelectNetworkDialogVisibility!: (flag: boolean) => void; - @action.web3.selectExternalNetwork selectExternalNetwork!: (networkId: BridgeNetworkId) => void; + @action.web3.selectExternalNetwork selectExternalNetwork!: (network: { + id: BridgeNetworkId; + type: BridgeNetworkType; + }) => void; get visibility(): boolean { return this.selectNetworkDialogVisibility; @@ -66,7 +69,7 @@ export default class BridgeSelectNetwork extends Mixins(NetworkFormatterMixin) { const networks = Object.values(record) as AvailableNetwork[]; return networks.reduce((buffer, { available, disabled, data: { id, name } }) => { - const networkName = type === BridgeNetworkType.EvmLegacy ? `${name} (${this.t('hashiBridgeText')})` : name; + const networkName = type === BridgeNetworkType.Eth ? `${name} (${this.t('hashiBridgeText')})` : name; if (available) { buffer.push({ @@ -92,10 +95,12 @@ export default class BridgeSelectNetwork extends Mixins(NetworkFormatterMixin) { set selectedNetworkTuple(value: string) { const [networkType, networkSelected] = value.split(DELIMETER); - const networkFormatted = - networkType === BridgeNetworkType.Sub ? (networkSelected as BridgeNetworkId) : Number(networkSelected); - this.setNetworkType(networkType as BridgeNetworkType); - this.selectExternalNetwork(networkFormatted); + + const type = networkType as BridgeNetworkType; + const id = type === BridgeNetworkType.Sub ? (networkSelected as SubNetwork) : Number(networkSelected); + + this.selectExternalNetwork({ id, type }); + this.visibility = false; } } diff --git a/src/components/pages/Bridge/TransactionDetails.vue b/src/components/pages/Bridge/TransactionDetails.vue index 8de39204a..500e208a5 100644 --- a/src/components/pages/Bridge/TransactionDetails.vue +++ b/src/components/pages/Bridge/TransactionDetails.vue @@ -17,9 +17,18 @@ is-formatted > + @@ -29,7 +38,7 @@ import { components, mixins } from '@soramitsu/soraneo-wallet-web'; import { Component, Mixins, Prop } from 'vue-property-decorator'; import TranslationMixin from '@/components/mixins/TranslationMixin'; -import { Components, ZeroStringValue } from '@/consts'; +import { Components, ZeroStringValue, ApproximateSign } from '@/consts'; import { lazyComponent } from '@/router'; import type { CodecString } from '@sora-substrate/util'; @@ -43,12 +52,19 @@ import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/t }) export default class BridgeTransactionDetails extends Mixins(mixins.FormattedAmountMixin, TranslationMixin) { readonly XOR = XOR; + readonly ApproximateSign = ApproximateSign; + @Prop({ default: () => null, type: Object }) readonly asset!: Nullable; @Prop({ default: () => null, type: Object }) readonly nativeToken!: Nullable; + @Prop({ default: ZeroStringValue, type: String }) readonly externalTransferFee!: CodecString; @Prop({ default: ZeroStringValue, type: String }) readonly externalNetworkFee!: CodecString; @Prop({ default: ZeroStringValue, type: String }) readonly soraNetworkFee!: CodecString; @Prop({ default: '', type: String }) readonly networkName!: string; + get assetSymbol(): string { + return this.asset?.symbol ?? ''; + } + get nativeTokenSymbol(): string { return this.nativeToken?.symbol ?? ''; } @@ -60,5 +76,9 @@ export default class BridgeTransactionDetails extends Mixins(mixins.FormattedAmo get isExternalFeeNotZero(): boolean { return this.externalNetworkFee !== ZeroStringValue; } + + get isExternalTransferFeeNotZero(): boolean { + return this.externalTransferFee !== ZeroStringValue; + } } diff --git a/src/components/pages/Moonpay/BridgeInitMixin.ts b/src/components/pages/Moonpay/BridgeInitMixin.ts index e696c31cd..0e40dd99a 100644 --- a/src/components/pages/Moonpay/BridgeInitMixin.ts +++ b/src/components/pages/Moonpay/BridgeInitMixin.ts @@ -13,9 +13,11 @@ import ethersUtil from '@/utils/ethers-util'; import type { MoonpayTransaction } from '@/utils/moonpay'; import { MoonpayEVMTransferAssetData, MoonpayApi } from '@/utils/moonpay'; -import type { CodecString, BridgeHistory } from '@sora-substrate/util'; +import type { CodecString } from '@sora-substrate/util'; import type { Asset, AccountBalance } from '@sora-substrate/util/build/assets/types'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; import type { EvmNetwork } from '@sora-substrate/util/build/bridgeProxy/evm/types'; +import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; import type { WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; const createError = (text: string, notification: MoonpayNotifications) => { @@ -27,7 +29,7 @@ const createError = (text: string, notification: MoonpayNotifications) => { @Component export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, WalletConnectMixin) { @state.moonpay.api moonpayApi!: MoonpayApi; - @state.moonpay.bridgeTransactionData bridgeTransactionData!: Nullable; + @state.moonpay.bridgeTransactionData bridgeTransactionData!: Nullable; @state.web3.ethBridgeEvmNetwork ethBridgeEvmNetwork!: EvmNetwork; @state.wallet.settings.soraNetwork soraNetwork!: Nullable; @state.assets.registeredAssets private registeredAssets!: Record; @@ -40,12 +42,20 @@ export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, W @mutation.moonpay.setNotificationKey setNotificationKey!: (key: string) => void; @mutation.moonpay.setBridgeTxData setBridgeTxData!: (options?: BridgeTxData) => void; + @action.web3.selectExternalNetwork selectExternalNetwork!: (network: { + id: BridgeNetworkId; + type: BridgeNetworkType; + }) => Promise; + @action.moonpay.getTransactionTranserData private getTransactionTranserData!: ( hash: string ) => Promise>; async prepareEvmNetwork(): Promise { - await this.selectExternalNetwork(this.ethBridgeEvmNetwork); // WalletConnectMixin + await this.selectExternalNetwork({ + id: this.ethBridgeEvmNetwork, + type: BridgeNetworkType.Eth, + }); // WalletConnectMixin } initMoonpayApi(): void { @@ -64,7 +74,7 @@ export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, W } } - async getBridgeMoonpayTransaction(): Promise { + async getBridgeMoonpayTransaction(): Promise { if (!this.bridgeTransactionData) { throw new Error('bridgeTransactionData is empty'); } @@ -76,18 +86,18 @@ export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, W if (!tx) { const historyItem = await this.generateHistoryItem(this.bridgeTransactionData); - return historyItem as BridgeHistory; + return historyItem as EthHistory; } return tx; } - getBridgeHistoryItemByMoonpayId(moonpayId: string): Nullable { + getBridgeHistoryItemByMoonpayId(moonpayId: string): Nullable { const externalHash = this.moonpayApi.accountRecords?.[moonpayId]; if (!externalHash) return null; - return Object.values(this.history).find((item) => item.externalHash === externalHash) as Nullable; + return Object.values(this.history).find((item) => item.externalHash === externalHash) as Nullable; } async startBridgeForMoonpayTransaction(): Promise { @@ -100,7 +110,7 @@ export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, W * @param transaction moonpay transaction data * @returns string - bridge history item id */ - async prepareBridgeHistoryItemData(transaction: MoonpayTransaction): Promise { + async prepareBridgeHistoryItemData(transaction: MoonpayTransaction): Promise { return await this.withLoading(async () => { // this is not really good, but we should change evm network before fetching transaction data await this.prepareEvmNetwork(); @@ -173,7 +183,7 @@ export default class MoonpayBridgeInitMixin extends Mixins(BridgeHistoryMixin, W soraNetworkFee: this.networkFees[Operation.EthBridgeIncoming], externalNetworkFee: evmNetworkFee, externalNetwork: this.ethBridgeEvmNetwork, - externalNetworkType: BridgeNetworkType.EvmLegacy, + externalNetworkType: BridgeNetworkType.Eth, to: ethTransferData.to, payload: { moonpayId: transaction.id, diff --git a/src/components/pages/Moonpay/MoonpayHistory.vue b/src/components/pages/Moonpay/MoonpayHistory.vue index d0dbbdfeb..fd4d4b662 100644 --- a/src/components/pages/Moonpay/MoonpayHistory.vue +++ b/src/components/pages/Moonpay/MoonpayHistory.vue @@ -84,7 +84,7 @@ import ethersUtil from '../../../utils/ethers-util'; import { MoonpayTransactionStatus } from '../../../utils/moonpay'; import type { MoonpayTransaction, MoonpayCurrency, MoonpayCurrenciesById } from '../../../utils/moonpay'; -import type { BridgeHistory } from '@sora-substrate/util'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; import type Theme from '@soramitsu/soramitsu-js-ui/lib/types/Theme'; const HistoryView = 'history'; @@ -135,7 +135,7 @@ export default class MoonpayHistory extends Mixins(mixins.PaginationSearchMixin, } }, onNetworkChange: (networkHex: string) => { - this.connectExternalNetwork(networkHex); + this.updateProvidedEvmNetwork(networkHex); }, onDisconnect: () => { this.resetProvidedEvmNetwork(); @@ -209,7 +209,7 @@ export default class MoonpayHistory extends Mixins(mixins.PaginationSearchMixin, return `${this.selectedItem.returnUrl}?${query}`; } - get bridgeTxToSora(): Nullable { + get bridgeTxToSora(): Nullable { if (!this.selectedItem.id) return undefined; return this.getBridgeHistoryItemByMoonpayId(this.selectedItem.id); @@ -291,7 +291,7 @@ export default class MoonpayHistory extends Mixins(mixins.PaginationSearchMixin, if (!this.selectedItem.id) return; if (!this.isValidNetwork) { - this.updateNetworkProvided(); + this.changeEvmNetworkProvided(); } else if (this.bridgeTxToSora?.id) { await this.prepareEvmNetwork(); // MoonpayBridgeInitMixin await this.showHistory(this.bridgeTxToSora.id); // MoonpayBridgeInitMixin diff --git a/src/components/pages/Stats/SupplyChart.vue b/src/components/pages/Stats/SupplyChart.vue index 27b1edf6e..985e488e8 100644 --- a/src/components/pages/Stats/SupplyChart.vue +++ b/src/components/pages/Stats/SupplyChart.vue @@ -12,16 +12,16 @@ + @@ -119,7 +120,8 @@ const parse = (node: AssetSnapshotEntity): ChartData => { PriceChange: lazyComponent(Components.PriceChange), StatsCard: lazyComponent(Components.StatsCard), StatsFilter: lazyComponent(Components.StatsFilter), - TokenSelectDropdown: lazyComponent(Components.TokenSelectDropdown), + TokenSelectButton: lazyComponent(Components.TokenSelectButton), + SelectToken: lazyComponent(Components.SelectToken), FormattedAmount: components.FormattedAmount, }, }) @@ -131,11 +133,24 @@ export default class StatsSupplyChart extends Mixins(mixins.LoadingMixin, ChartS filter: SnapshotFilter = ASSET_SUPPLY_LINE_FILTERS[0]; token = XOR; + showSelectTokenDialog = false; data: readonly ChartData[] = []; isFetchingError = false; + get areActionsDisabled(): boolean { + return this.parentLoading || this.loading; + } + + get selectTokenIcon(): Nullable { + return !this.areActionsDisabled ? 'chevron-down-rounded-16' : undefined; + } + + get tokenTabIndex(): number { + return !this.areActionsDisabled ? 0 : -1; + } + get firstValue(): FPNumber { return new FPNumber(first(this.data)?.value ?? 0); } @@ -275,6 +290,10 @@ export default class StatsSupplyChart extends Mixins(mixins.LoadingMixin, ChartS }; } + handleSelectToken(): void { + this.showSelectTokenDialog = true; + } + changeFilter(filter: SnapshotFilter): void { this.filter = filter; this.updateData(); diff --git a/src/components/shared/Dialog/ConfirmBridgeTransaction.vue b/src/components/shared/Dialog/ConfirmBridgeTransaction.vue index f49064bbf..4c0b0af45 100644 --- a/src/components/shared/Dialog/ConfirmBridgeTransaction.vue +++ b/src/components/shared/Dialog/ConfirmBridgeTransaction.vue @@ -25,7 +25,9 @@
null, type: Object }) readonly asset!: Nullable; @Prop({ default: () => null, type: Object }) readonly nativeToken!: Nullable; + @Prop({ default: ZeroStringValue, type: String }) readonly externalTransferFee!: CodecString; @Prop({ default: ZeroStringValue, type: String }) readonly externalNetworkFee!: CodecString; @Prop({ default: ZeroStringValue, type: String }) readonly soraNetworkFee!: CodecString; @Prop({ default: true, type: Boolean }) readonly isSoraToEvm!: boolean; diff --git a/src/components/shared/Input/TokenSelectDropdown.vue b/src/components/shared/Input/TokenSelectDropdown.vue deleted file mode 100644 index 9dc8e9680..000000000 --- a/src/components/shared/Input/TokenSelectDropdown.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/src/components/shared/SelectAsset/SelectToken.vue b/src/components/shared/SelectAsset/SelectToken.vue index 5fe13a5c0..d37e92b86 100644 --- a/src/components/shared/SelectAsset/SelectToken.vue +++ b/src/components/shared/SelectAsset/SelectToken.vue @@ -155,8 +155,10 @@ export default class SelectToken extends Mixins(TranslationMixin, SelectAssetMix const assetsAddresses = whiteList.map((asset) => asset.address); const excludeAddress = this.asset?.address; + const list = this.getAssetsWithBalances(assetsAddresses, excludeAddress); + const orderedList = [...list].sort(this.sortByBalance); - return this.getAssetsWithBalances(assetsAddresses, excludeAddress).sort(this.sortByBalance); + return orderedList; } get filteredWhitelistTokens(): Array { diff --git a/src/consts/index.ts b/src/consts/index.ts index a083e9566..e121656ff 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -90,6 +90,8 @@ export const ObjectInit = () => null; export const ZeroStringValue = '0'; +export const ApproximateSign = '~'; + export const MetamaskCancellationCode = 4001; export const DefaultSlippageTolerance = '0.5'; @@ -188,6 +190,7 @@ export enum Components { BridgeSelectNetwork = 'pages/Bridge/SelectNetwork', BridgeSelectAccount = 'pages/Bridge/SelectAccount', BridgeLinksDropdown = 'pages/Bridge/LinksDropdown', + BridgeLimitCard = 'pages/Bridge/LimitCard', // Moonpay Page Moonpay = 'pages/Moonpay/Moonpay', MoonpayNotification = 'pages/Moonpay/Notification', @@ -229,7 +232,6 @@ export enum Components { // Shared Input TokenInput = 'shared/Input/TokenInput', TokenSelectButton = 'shared/Input/TokenSelectButton', - TokenSelectDropdown = 'shared/Input/TokenSelectDropdown', // Shared Dialogs ConfirmBridgeTransactionDialog = 'shared/Dialog/ConfirmBridgeTransaction', NetworkFeeWarningDialog = 'shared/Dialog/NetworkFeeWarning', diff --git a/src/consts/sub.ts b/src/consts/sub.ts index ef0321d5c..f1c3e1e3d 100644 --- a/src/consts/sub.ts +++ b/src/consts/sub.ts @@ -74,4 +74,10 @@ export const SUB_TRANSFER_FEES: SubNetworksFees = { [BridgeTxDirection.Incoming]: '0', }, }, + [SubNetwork.Kusama]: { + KSM: { + [BridgeTxDirection.Outgoing]: '10124190', // [TODO] check in real transfer + [BridgeTxDirection.Incoming]: '0', + }, + }, }; diff --git a/src/lang/cs.json b/src/lang/cs.json index 4fc69664b..c30fb7bd0 100644 --- a/src/lang/cs.json +++ b/src/lang/cs.json @@ -545,7 +545,9 @@ "copy": "Zkopírujte síťovou adresu", "soraAddress": "{Sora} adresa", "ethereumAddress": "{Ethereum} adresa", - "limitMessage": "V současné době existuje {type} {amount} {symbol} pro přemostění, aby byla zajištěna stabilita a bezpečnost sítě {Sora}. Děkujeme za pochopení." + "limitMessage": "V současné době existuje {type} {amount} {symbol} pro přemostění, aby byla zajištěna stabilita a bezpečnost sítě {Sora}. Děkujeme za pochopení.", + "externalTransferFee": "{network} poplatek {XCM}", + "externalTransferFeeTooltip": "Když odešlete překlenovací transakci do sítě {network} , z částky, kterou převádíte, se strhne menší poplatek" }, "selectRegisteredAsset": { "title": "Vyberte token", diff --git a/src/lang/de.json b/src/lang/de.json index c1af17242..2454e2aa2 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -545,7 +545,9 @@ "copy": "Netzwerkadresse kopieren", "soraAddress": "{Sora} Adresse", "ethereumAddress": "{Ethereum} Adresse", - "limitMessage": "Derzeit gibt es ein {type} {amount} {symbol} für Bridging, um die Stabilität und Sicherheit des {Sora}-Netzwerks zu gewährleisten. Vielen Dank für Ihr Verständnis." + "limitMessage": "Derzeit gibt es ein {type} {amount} {symbol} für Bridging, um die Stabilität und Sicherheit des {Sora}-Netzwerks zu gewährleisten. Vielen Dank für Ihr Verständnis.", + "externalTransferFee": "{network} {XCM} -Gebühr", + "externalTransferFeeTooltip": "Wenn Sie eine Bridge-Transaktion an das Netzwerk {network} senden, wird eine geringe Gebühr von dem Betrag erhoben, den Sie überweisen" }, "selectRegisteredAsset": { "title": "Wähle ein Token", diff --git a/src/lang/en.json b/src/lang/en.json index 318ca66fc..86d733f63 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -545,7 +545,9 @@ "copy": "Copy network address", "soraAddress": "{Sora} address", "ethereumAddress": "{Ethereum} address", - "limitMessage": "Currently, there's a {type} {amount} {symbol} for bridging to ensure the stability and security of the {Sora} Network. We appreciate your understanding." + "limitMessage": "Currently, there's a {type} {amount} {symbol} for bridging to ensure the stability and security of the {Sora} Network. We appreciate your understanding.", + "externalTransferFee": "{network} {XCM} fee", + "externalTransferFeeTooltip": "When you send a bridge transaction to the {network} network, a minor fee is taken from the amount you are transferring" }, "selectRegisteredAsset": { "title": "Select a token", diff --git a/src/lang/es.json b/src/lang/es.json index 1c77b7886..f8f64d029 100644 --- a/src/lang/es.json +++ b/src/lang/es.json @@ -545,7 +545,9 @@ "copy": "Copiar dirección de red", "soraAddress": "Dirección de {Sora}", "ethereumAddress": "dirección {Ethereum}", - "limitMessage": "Actualmente, existe un {type} {amount} {symbol} para establecer puentes para garantizar la estabilidad y seguridad de la red {Sora} . apreciamos su comprensión." + "limitMessage": "Actualmente, existe un {type} {amount} {symbol} para establecer puentes para garantizar la estabilidad y seguridad de la red {Sora} . apreciamos su comprensión.", + "externalTransferFee": "tarifa de {network} {XCM}", + "externalTransferFeeTooltip": "Cuando envía una transacción puente a la red {network} , se deduce una tarifa menor del monto que está transfiriendo" }, "selectRegisteredAsset": { "title": "Seleccione un Token", diff --git a/src/lang/fr.json b/src/lang/fr.json index bf47f81e5..7be0fea00 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -545,7 +545,9 @@ "copy": "Copier l'adresse réseau", "soraAddress": "{Sora} adresse", "ethereumAddress": "{Ethereum} adresse", - "limitMessage": "Actuellement, il existe un {type} {amount} {symbol} pour le pontage afin de garantir la stabilité et la sécurité du réseau {Sora}. Nous apprécions votre compréhension." + "limitMessage": "Actuellement, il existe un {type} {amount} {symbol} pour le pontage afin de garantir la stabilité et la sécurité du réseau {Sora}. Nous apprécions votre compréhension.", + "externalTransferFee": "Frais {network} {XCM}", + "externalTransferFeeTooltip": "Lorsque vous envoyez une transaction relais vers le réseau {network} , des frais mineurs sont prélevés sur le montant que vous transférez" }, "selectRegisteredAsset": { "title": "Sélectionnez un jeton", diff --git a/src/lang/hr.json b/src/lang/hr.json index 8bbe88bbd..9c11328a8 100644 --- a/src/lang/hr.json +++ b/src/lang/hr.json @@ -545,7 +545,9 @@ "copy": "Kopiraj mrežnu adresu", "soraAddress": "{Sora} adresa", "ethereumAddress": "{Ethereum} adresa", - "limitMessage": "Trenutno postoji {type} {amount} {symbol} za premošćivanje kako bi se osigurala stabilnost i sigurnost {Sora} mreže. Cijenimo vaše razumijevanje." + "limitMessage": "Trenutno postoji {type} {amount} {symbol} za premošćivanje kako bi se osigurala stabilnost i sigurnost {Sora} mreže. Cijenimo vaše razumijevanje.", + "externalTransferFee": "{network} {XCM} naknada", + "externalTransferFeeTooltip": "Kada pošaljete premosnu transakciju na mrežu {network} , od iznosa koji prenosite uzima se manja naknada" }, "selectRegisteredAsset": { "title": "Odaberite token", diff --git a/src/lang/hu.json b/src/lang/hu.json index 7b76f6d38..16db88753 100644 --- a/src/lang/hu.json +++ b/src/lang/hu.json @@ -545,7 +545,9 @@ "copy": "Hálózati cím másolása", "soraAddress": "{Sora} cím", "ethereumAddress": "{Ethereum} cím", - "limitMessage": "Jelenleg létezik egy {type} {amount} {symbol} áthidalás, amely biztosítja a {Sora} hálózat stabilitását és biztonságát. Köszönjük megértését." + "limitMessage": "Jelenleg létezik egy {type} {amount} {symbol} áthidalás, amely biztosítja a {Sora} hálózat stabilitását és biztonságát. Köszönjük megértését.", + "externalTransferFee": "{network} {XCM} díj", + "externalTransferFeeTooltip": "Ha hídtranzakciót küld a {network} hálózatra, az átutalt összegből egy kisebb díjat számítanak fel" }, "selectRegisteredAsset": { "title": "Token kiválasztása", diff --git a/src/lang/hy.json b/src/lang/hy.json index 4f8341ead..b15adcd4f 100644 --- a/src/lang/hy.json +++ b/src/lang/hy.json @@ -545,7 +545,9 @@ "copy": "Պատճենել ցանցի հասցեն", "soraAddress": "{Sora} հասցե", "ethereumAddress": "{Ethereum} հասցե", - "limitMessage": "Ներկայումս {Sora} ցանցի կայունությունն ու անվտանգությունն ապահովելու համար կամրջելու համար կա {type} {amount} {symbol} : Մենք գնահատում ենք ձեր ըմբռնումը:" + "limitMessage": "Ներկայումս {Sora} ցանցի կայունությունն ու անվտանգությունն ապահովելու համար կամրջելու համար կա {type} {amount} {symbol} : Մենք գնահատում ենք ձեր ըմբռնումը:", + "externalTransferFee": "{network} {XCM} վճար", + "externalTransferFeeTooltip": "Երբ դուք կամուրջ գործարք եք ուղարկում {network} ցանցին, ձեր փոխանցվող գումարից փոքր վճար է վերցվում" }, "selectRegisteredAsset": { "title": "Ընտրեք տոկեն", diff --git a/src/lang/id.json b/src/lang/id.json index 5511e3948..ab1a53813 100644 --- a/src/lang/id.json +++ b/src/lang/id.json @@ -545,7 +545,9 @@ "copy": "Salin alamat jaringan", "soraAddress": "{Sora} alamat", "ethereumAddress": "{Ethereum} alamat", - "limitMessage": "Saat ini, ada {type} {amount} {symbol} untuk menjembatani guna memastikan stabilitas dan keamanan Jaringan {Sora} . Kami menghargai pengertian Anda." + "limitMessage": "Saat ini, ada {type} {amount} {symbol} untuk menjembatani guna memastikan stabilitas dan keamanan Jaringan {Sora} . Kami menghargai pengertian Anda.", + "externalTransferFee": "biaya {network} {XCM}", + "externalTransferFeeTooltip": "Saat Anda mengirim transaksi jembatan ke jaringan {network} , sedikit biaya akan diambil dari jumlah yang Anda transfer" }, "selectRegisteredAsset": { "title": "Pilih token", diff --git a/src/lang/it.json b/src/lang/it.json index 74b499b35..ba8981cd3 100644 --- a/src/lang/it.json +++ b/src/lang/it.json @@ -545,7 +545,9 @@ "copy": "Copia l'indirizzo di rete", "soraAddress": "{Sora} indirizzo", "ethereumAddress": "{Ethereum} indirizzo", - "limitMessage": "Attualmente, esiste un {type} {amount} {symbol} per il bridging per garantire la stabilità e la sicurezza della rete {Sora} . Apprezziamo la tua comprensione." + "limitMessage": "Attualmente, esiste un {type} {amount} {symbol} per il bridging per garantire la stabilità e la sicurezza della rete {Sora} . Apprezziamo la tua comprensione.", + "externalTransferFee": "Tariffa {network} {XCM}", + "externalTransferFeeTooltip": "Quando invii una transazione bridge alla rete {network} , viene trattenuta una commissione minore dall'importo che stai trasferendo" }, "selectRegisteredAsset": { "title": "Seleziona un token", diff --git a/src/lang/messages.ts b/src/lang/messages.ts index 5127c869e..1d709fcb0 100644 --- a/src/lang/messages.ts +++ b/src/lang/messages.ts @@ -395,6 +395,9 @@ export default { connectWallets: 'Connect wallets to view respective transaction history.', soraNetworkFee: '{Sora} Network Fee', ethereumNetworkFee: '{Ethereum} Network Fee', + externalTransferFee: '{network} {XCM} fee', + externalTransferFeeTooltip: + 'When you send a bridge transaction to the {network} network, a minor fee is taken from the amount you are trasferring', tooltipValue: '@:comingSoonText', total: 'Total', viewHistory: 'View transactions history', @@ -406,6 +409,8 @@ export default { copy: 'Copy network address', soraAddress: '{Sora} address', ethereumAddress: '{Ethereum} address', + limitMessage: + "Currently, there's a {type} {amount} {symbol} for bridging to ensure the stability and security of the {Sora} Network. We appreciate your understanding.", }, selectRegisteredAsset: { title: 'Select a token', @@ -858,4 +863,7 @@ export default { btnGoToSettings: 'Go to settings', btnAllow: 'Allow access', }, + minAmountText: 'min. amount', + maxAmountText: 'max. amount', + exceededAmountText: '{amount} exceeded', }; diff --git a/src/lang/nl.json b/src/lang/nl.json index 772292e37..a2aef3bbd 100644 --- a/src/lang/nl.json +++ b/src/lang/nl.json @@ -545,7 +545,9 @@ "copy": "Netwerkadres kopiëren", "soraAddress": "{Sora} adres", "ethereumAddress": "{Ethereum} adres", - "limitMessage": "Momenteel is er een {type} {amount} {symbol} voor overbrugging om de stabiliteit en veiligheid van het {Sora}-netwerk te garanderen. We waarderen je begrip." + "limitMessage": "Momenteel is er een {type} {amount} {symbol} voor overbrugging om de stabiliteit en veiligheid van het {Sora}-netwerk te garanderen. We waarderen je begrip.", + "externalTransferFee": "{network} {XCM} kosten", + "externalTransferFeeTooltip": "Wanneer u een overbruggingstransactie naar het {network} -netwerk verzendt, wordt er een kleine vergoeding in rekening gebracht op het bedrag dat u overdraagt" }, "selectRegisteredAsset": { "title": "Kies een token", diff --git a/src/lang/no.json b/src/lang/no.json index a72ae903a..4666b15e4 100644 --- a/src/lang/no.json +++ b/src/lang/no.json @@ -545,7 +545,9 @@ "copy": "Kopier nettverksadressen", "soraAddress": "{Sora} adresse", "ethereumAddress": "{Ethereum} adresse", - "limitMessage": "For øyeblikket er det en {type} {amount} {symbol} for brobygging for å sikre stabiliteten og sikkerheten til {Sora} -nettverket. Vi setter pris på din forståelse." + "limitMessage": "For øyeblikket er det en {type} {amount} {symbol} for brobygging for å sikre stabiliteten og sikkerheten til {Sora} -nettverket. Vi setter pris på din forståelse.", + "externalTransferFee": "{network} {XCM} gebyr", + "externalTransferFeeTooltip": "Når du sender en brotransaksjon til {network} -nettverket, tas et mindre gebyr fra beløpet du overfører" }, "selectRegisteredAsset": { "title": "Velg token", diff --git a/src/lang/pl.json b/src/lang/pl.json index dc957e1bd..0e39cb773 100644 --- a/src/lang/pl.json +++ b/src/lang/pl.json @@ -545,7 +545,9 @@ "copy": "Skopiuj adres sieciowy", "soraAddress": "{Sora} adres", "ethereumAddress": "{Ethereum} adres", - "limitMessage": "Obecnie istnieje {type} {amount} {symbol} do mostkowania, aby zapewnić stabilność i bezpieczeństwo sieci {Sora} . Doceniamy twoje zrozumienie." + "limitMessage": "Obecnie istnieje {type} {amount} {symbol} do mostkowania, aby zapewnić stabilność i bezpieczeństwo sieci {Sora} . Doceniamy twoje zrozumienie.", + "externalTransferFee": "Opłata {network} {XCM}", + "externalTransferFeeTooltip": "Kiedy wysyłasz transakcję pomostową do sieci {network} , od kwoty przelewu pobierana jest niewielka opłata" }, "selectRegisteredAsset": { "title": "Wybierz token", diff --git a/src/lang/ru.json b/src/lang/ru.json index c4ff16173..52a3b4d15 100644 --- a/src/lang/ru.json +++ b/src/lang/ru.json @@ -545,7 +545,9 @@ "copy": "Скопировать адрес сети", "soraAddress": "{Sora} адрес", "ethereumAddress": "{Ethereum} адрес", - "limitMessage": "В настоящее время существует {type} {amount} {symbol} для моста, обеспечивающая стабильность и безопасность сети {Sora}. Мы ценим ваше понимание." + "limitMessage": "В настоящее время существует {type} {amount} {symbol} для моста, обеспечивающая стабильность и безопасность сети {Sora}. Мы ценим ваше понимание.", + "externalTransferFee": "{network} комиссия {XCM}", + "externalTransferFeeTooltip": "Когда вы отправляете транзакцию моста в сеть {network}, из суммы перевода взимается небольшая комиссия." }, "selectRegisteredAsset": { "title": "Выбрать токен", diff --git a/src/lang/sk.json b/src/lang/sk.json index aa646f7bd..bbe108b56 100644 --- a/src/lang/sk.json +++ b/src/lang/sk.json @@ -545,7 +545,9 @@ "copy": "Skopírujte sieťovú adresu", "soraAddress": "{Sora} adresa", "ethereumAddress": "{Ethereum} adresa", - "limitMessage": "V súčasnosti existuje {type} {amount} {symbol} na premostenie na zaistenie stability a bezpečnosti siete {Sora}. Ďakujeme za pochopenie." + "limitMessage": "V súčasnosti existuje {type} {amount} {symbol} na premostenie na zaistenie stability a bezpečnosti siete {Sora}. Ďakujeme za pochopenie.", + "externalTransferFee": "{network} poplatok {XCM}", + "externalTransferFeeTooltip": "Keď odošlete preklenovaciu transakciu do siete {network} , zo sumy, ktorú prenášate, sa účtuje malý poplatok" }, "selectRegisteredAsset": { "title": "Vybrať token", diff --git a/src/lang/sr.json b/src/lang/sr.json index 2d920aefe..42924cc2f 100644 --- a/src/lang/sr.json +++ b/src/lang/sr.json @@ -545,7 +545,9 @@ "copy": "Копирајте мрежну адресу", "soraAddress": "{Sora} adresa", "ethereumAddress": "{Ethereum} adresa", - "limitMessage": "Тренутно постоји {type} {amount} {symbol} за премошћавање да би се обезбедила стабилност и безбедност мреже {Sora} . Ценимо ваше разумевање." + "limitMessage": "Тренутно постоји {type} {amount} {symbol} за премошћавање да би се обезбедила стабилност и безбедност мреже {Sora} . Ценимо ваше разумевање.", + "externalTransferFee": "{network} {XCM} накнада", + "externalTransferFeeTooltip": "Када пошаљете бридге трансакцију на мрежу {network} , од износа који преносите узима се мања накнада" }, "selectRegisteredAsset": { "title": "Изаберите токен", diff --git a/src/lang/sv.json b/src/lang/sv.json index dfbc8b891..4d731d32e 100644 --- a/src/lang/sv.json +++ b/src/lang/sv.json @@ -545,7 +545,9 @@ "copy": "Kopiera nätverksadress", "soraAddress": "{Sora} adress", "ethereumAddress": "{Ethereum} adress", - "limitMessage": "För närvarande finns det en {type} {amount} {symbol} för överbryggning för att säkerställa stabiliteten och säkerheten i {Sora} -nätverket. Vi uppskattar din förståelse." + "limitMessage": "För närvarande finns det en {type} {amount} {symbol} för överbryggning för att säkerställa stabiliteten och säkerheten i {Sora} -nätverket. Vi uppskattar din förståelse.", + "externalTransferFee": "{network} {XCM} avgift", + "externalTransferFeeTooltip": "När du skickar en bryggtransaktion till {network} -nätverket tas en mindre avgift från beloppet du överför" }, "selectRegisteredAsset": { "title": "Välj en token", diff --git a/src/lang/vi.json b/src/lang/vi.json index 76d8f225d..03f8497c0 100644 --- a/src/lang/vi.json +++ b/src/lang/vi.json @@ -545,7 +545,9 @@ "copy": "Sao chép địa chỉ mạng", "soraAddress": "địa chỉ {Sora}", "ethereumAddress": "địa chỉ {Ethereum}", - "limitMessage": "Hiện tại, có một {type} {amount} {symbol} để kết nối nhằm đảm bảo tính ổn định và bảo mật của Mạng {Sora} . Chúng tôi đánh giá cao sự hiểu biết của bạn." + "limitMessage": "Hiện tại, có một {type} {amount} {symbol} để kết nối nhằm đảm bảo tính ổn định và bảo mật của Mạng {Sora} . Chúng tôi đánh giá cao sự hiểu biết của bạn.", + "externalTransferFee": "Phí {network} {XCM}", + "externalTransferFeeTooltip": "Khi bạn gửi giao dịch bắc cầu tới mạng {network} , số tiền bạn chuyển sẽ bị tính một khoản phí nhỏ" }, "selectRegisteredAsset": { "title": "Chọn token", diff --git a/src/lang/yo.json b/src/lang/yo.json index 8d1575299..8fbb054de 100644 --- a/src/lang/yo.json +++ b/src/lang/yo.json @@ -545,7 +545,9 @@ "copy": "Daakọ adirẹsi nẹtiwọki", "soraAddress": "{Sora} adirẹsi", "ethereumAddress": "{Ethereum} adirẹsi", - "limitMessage": "Lọwọlọwọ, {type} {amount} {symbol} wa fun sisọpọ lati rii daju iduroṣinṣin ati aabo ti Nẹtiwọọki {Sora} . A dupe oye rẹ." + "limitMessage": "Lọwọlọwọ, {type} {amount} {symbol} wa fun sisọpọ lati rii daju iduroṣinṣin ati aabo ti Nẹtiwọọki {Sora} . A dupe oye rẹ.", + "externalTransferFee": "{network} {XCM} ọya", + "externalTransferFeeTooltip": "Nigbati o ba fi iṣowo afara ranṣẹ si netiwọki {network} , owo kekere kan yoo gba lati iye ti o n gbe" }, "selectRegisteredAsset": { "title": "Yan Tokini kan", diff --git a/src/lang/zh_CN.json b/src/lang/zh_CN.json index f895c757a..0feb699fd 100644 --- a/src/lang/zh_CN.json +++ b/src/lang/zh_CN.json @@ -545,7 +545,9 @@ "copy": "复制网络地址", "soraAddress": "{Sora} 地址", "ethereumAddress": "{Ethereum} 地址", - "limitMessage": "目前,有一个{type} {amount} {symbol}用于桥接,以确保 {Sora} 网络的稳定性和安全性。我们感谢您的理解。" + "limitMessage": "目前,有一个{type} {amount} {symbol}用于桥接,以确保 {Sora} 网络的稳定性和安全性。我们感谢您的理解。", + "externalTransferFee": "{network} {XCM}费用", + "externalTransferFeeTooltip": "当您向{network}网络发送桥接交易时,将从您传输的金额中收取少量费用" }, "selectRegisteredAsset": { "title": "选择一个代币", diff --git a/src/modules/demeterFarming/components/CalculatorDialog.vue b/src/modules/demeterFarming/components/CalculatorDialog.vue index 2b1512017..76672a3b8 100644 --- a/src/modules/demeterFarming/components/CalculatorDialog.vue +++ b/src/modules/demeterFarming/components/CalculatorDialog.vue @@ -160,7 +160,7 @@ export default class CalculatorDialog extends Mixins(PoolCardMixin, mixins.Dialo } get calculatedRewardsFormatted(): string { - return '~' + this.calculatedRewards.toLocaleString(); + return this.calculatedRewards.toLocaleString(); } get calculatedRewardsFiat(): Nullable { diff --git a/src/router/index.ts b/src/router/index.ts index d05c20a78..43cf17e84 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -270,7 +270,7 @@ router.beforeEach((to, from, next) => { } } if (to.matched.some((record) => record.meta.requiresAuth)) { - if (BridgeChildPages.includes(current) && isLoggedIn && !store.getters.web3.externalAccount) { + if (BridgeChildPages.includes(current) && isLoggedIn && !store.getters.bridge.externalAccount) { setRoute(PageNames.Bridge); return; } diff --git a/src/store/assets/actions.ts b/src/store/assets/actions.ts index 3199ac301..0e48d8523 100644 --- a/src/store/assets/actions.ts +++ b/src/store/assets/actions.ts @@ -80,7 +80,7 @@ async function getRegisteredAssets(context: ActionContext): Promise => { + const accountBalance = await getAssetBalance(api.api, accountAddress, asset.address, asset.decimals); + return accountBalance.transferable; +}; + +const getExternalBalance = async ( + accountAddress: string, + asset: RegisteredAccountAsset, + isSub: boolean +): Promise => { + return isSub + ? await subBridgeConnector.networkAdapter.getTokenBalance(accountAddress, asset?.externalAddress) + : await ethersUtil.getAccountAssetBalance(accountAddress, asset?.externalAddress); +}; + +const getAccountBridgeBalance = async ( + accountAddress: string, + asset: Nullable, + isSora: boolean, + isSub: boolean +): Promise => { + if (!(asset?.address && accountAddress)) return ZeroStringValue; + + try { + return isSora + ? await getSoraBalance(accountAddress, asset) + : await getExternalBalance(accountAddress, asset, isSub); + } catch { + return ZeroStringValue; + } +}; + function getBridgeApi(context: ActionContext) { const { getters } = bridgeActionContext(context); @@ -47,43 +86,6 @@ function checkEvmNetwork(context: ActionContext): void { } } -async function evmTxDataToHistory( - assetDataByAddress: (address: string) => Nullable, - tx: BridgeTransactionData -): Promise { - const id = tx.soraHash; - const asset = assetDataByAddress(tx.soraAssetAddress); - const transactionState = tx.status; - const isOutgoing = tx.direction === BridgeTxDirection.Outgoing; - const blockHeight = isOutgoing ? tx.startBlock : tx.endBlock; - const externalBlockHeight = isOutgoing ? tx.endBlock : tx.startBlock; - const blockId = await api.system.getBlockHash(blockHeight); - const startTime = await api.system.getBlockTimestamp(blockId); - - return { - id, - txId: id, - blockId, - blockHeight, - type: isOutgoing ? Operation.EvmOutgoing : Operation.EvmIncoming, - hash: tx.soraHash, - transactionState, - externalBlockHeight, - externalNetwork: tx.externalNetwork as EvmNetwork, - externalNetworkType: BridgeNetworkType.Evm, - // for now we don't know it - externalHash: '', - amount: FPNumber.fromCodecValue(tx.amount, asset?.decimals).toString(), - assetAddress: asset?.address, - symbol: asset?.symbol, - from: tx.soraAccount, - to: tx.externalAccount, - // for now we only know sora block, assume what this is start & end times - startTime: startTime, - endTime: startTime, - }; -} - function bridgeDataToHistoryItem( context: ActionContext, { date = Date.now(), payload = {}, ...params } = {} @@ -93,7 +95,7 @@ function bridgeDataToHistoryItem( const transactionState = isEthBridge ? WALLET_CONSTS.ETH_BRIDGE_STATES.INITIAL : BridgeTxStatus.Pending; const externalNetwork = rootState.web3.networkSelected as BridgeNetworkId as any; const externalNetworkType = isEthBridge - ? BridgeNetworkType.EvmLegacy + ? BridgeNetworkType.Eth : isEvmBridge ? BridgeNetworkType.Evm : BridgeNetworkType.Sub; @@ -106,46 +108,46 @@ function bridgeDataToHistoryItem( assetAddress: (params as any).assetAddress ?? getters.asset?.address, startTime: date, endTime: date, - status: '', - hash: '', transactionState, - soraNetworkFee: (params as any).soraNetworkFee ?? getters.soraNetworkFee, + soraNetworkFee: (params as any).soraNetworkFee ?? state.soraNetworkFee, externalNetworkFee: (params as any).externalNetworkFee, externalNetwork, externalNetworkType, - to: (params as any).to ?? rootGetters.web3.externalAccount, + to: (params as any).to ?? getters.externalAccountFormatted, payload, }; } async function getEvmNetworkFee(context: ActionContext): Promise { - const { commit, state, rootState } = bridgeActionContext(context); + const { commit, getters, state, rootState } = bridgeActionContext(context); let fee = ZeroStringValue; - const address = state.assetAddress; - const registeredAsset = address ? rootState.assets.registeredAssets[address] : null; + if (getters.asset && getters.isRegisteredAsset) { + const bridgeRegisteredAsset = rootState.assets.registeredAssets[getters.asset.address]; - if (registeredAsset) { - fee = await ethersUtil.getEvmNetworkFee(registeredAsset.address, registeredAsset.kind, state.isSoraToEvm); + fee = await ethersUtil.getEvmNetworkFee( + bridgeRegisteredAsset.address, + bridgeRegisteredAsset.kind, + state.isSoraToEvm + ); } commit.setExternalNetworkFee(fee); } async function getSubNetworkFee(context: ActionContext): Promise { - const { commit } = bridgeActionContext(context); - + const { commit, getters } = bridgeActionContext(context); let fee = ZeroStringValue; - if (subConnector.networkAdapter) { - fee = await subConnector.networkAdapter.getNetworkFee(); + if (getters.asset && getters.isRegisteredAsset) { + fee = await subBridgeConnector.networkAdapter.getNetworkFee(getters.asset); } commit.setExternalNetworkFee(fee); } -async function getExternalNetworkFee(context: ActionContext): Promise { +async function updateExternalNetworkFee(context: ActionContext): Promise { const { getters } = bridgeActionContext(context); if (getters.isSubBridge) { @@ -155,39 +157,26 @@ async function getExternalNetworkFee(context: ActionContext): Promise< } } +async function updateExternalLockedBalance(context: ActionContext): Promise { + const { getters } = bridgeActionContext(context); + + if (getters.isEthBridge) { + await updateEthLockedBalance(context); + } else { + await updateBridgeProxyLockedBalance(context); + } +} + async function updateEvmBalances(context: ActionContext): Promise { const { commit, getters, state } = bridgeActionContext(context); - const { sender, recipient, asset } = getters; + const { sender, recipient, asset, nativeToken } = getters; const { isSoraToEvm } = state; - const spender = isSoraToEvm ? recipient : sender; - const getSenderBalance = async () => { - if (!(asset?.address && sender)) return ZeroStringValue; - - return isSoraToEvm - ? (await getAssetBalance(api.api, sender, asset.address, asset.decimals)).transferable - : await ethersUtil.getAccountAssetBalance(sender, asset?.externalAddress); - }; - - const getRecipientBalance = async () => { - if (!(asset?.address && recipient)) return ZeroStringValue; - - return isSoraToEvm - ? await ethersUtil.getAccountAssetBalance(recipient, asset?.externalAddress) - : (await getAssetBalance(api.api, recipient, asset.address, asset.decimals)).transferable; - }; - - const getSpenderBalance = async () => { - if (!(asset?.address && spender)) return ZeroStringValue; - - return await ethersUtil.getAccountBalance(spender); - }; - const [senderBalance, recipientBalance, nativeBalance] = await Promise.all([ - getSenderBalance(), - getRecipientBalance(), - getSpenderBalance(), + getAccountBridgeBalance(sender, asset, isSoraToEvm, false), + getAccountBridgeBalance(recipient, asset, !isSoraToEvm, false), + getAccountBridgeBalance(spender, nativeToken, false, false), ]); commit.setAssetSenderBalance(senderBalance); @@ -197,35 +186,14 @@ async function updateEvmBalances(context: ActionContext): Promise): Promise { const { commit, getters, state } = bridgeActionContext(context); - const { sender, recipient, asset } = getters; + const { sender, recipient, asset, nativeToken } = getters; const { isSoraToEvm } = state; - - const getSenderBalance = async () => { - if (!(asset?.address && sender)) return ZeroStringValue; - - return isSoraToEvm - ? (await getAssetBalance(api.api, sender, asset.address, asset.decimals)).transferable - : await subConnector.networkAdapter.getTokenBalance(sender, asset?.externalAddress); - }; - - const getRecipientBalance = async () => { - if (!(asset?.address && recipient)) return ZeroStringValue; - - return isSoraToEvm - ? await subConnector.networkAdapter.getTokenBalance(recipient, asset?.externalAddress) - : (await getAssetBalance(api.api, recipient, asset.address, asset.decimals)).transferable; - }; - - const getSpenderBalance = async () => { - if (!(asset?.address && sender)) return ZeroStringValue; - - return await subConnector.networkAdapter.getTokenBalance(sender); - }; + const spender = sender; const [senderBalance, recipientBalance, nativeBalance] = await Promise.all([ - getSenderBalance(), - getRecipientBalance(), - getSpenderBalance(), + getAccountBridgeBalance(sender, asset, isSoraToEvm, true), + getAccountBridgeBalance(recipient, asset, !isSoraToEvm, true), + getAccountBridgeBalance(spender, nativeToken, false, true), ]); commit.setAssetSenderBalance(senderBalance); @@ -247,46 +215,30 @@ async function updateEthHistory(context: ActionContext, clearHistory = await updateHistoryFn(clearHistory, dispatch.updateInternalHistory); } -async function updateEvmHistory(context: ActionContext): Promise { - const { commit, dispatch, state, rootState, rootGetters } = bridgeActionContext(context); - const externalNetwork = rootState.web3.networkSelected; - - if (!externalNetwork) return; - - const accountAddress = rootState.wallet.account.address; - const transactions = await evmBridgeApi.getUserTransactions(accountAddress, externalNetwork as EvmNetwork); - const externalHistory = {}; - - // [TODO]: update later - for (const txData of transactions) { - const tx = await evmTxDataToHistory(rootGetters.assets.assetDataByAddress, txData); - - if (tx.id) { - const inProgress = state.inProgressIds[tx.id]; - - if (!inProgress) { - externalHistory[tx.id] = tx; - - await dispatch.removeHistory({ tx, force: false }); - } - } - } - - commit.setExternalHistory(externalHistory); -} - async function updateEthLockedBalance(context: ActionContext): Promise { const { commit, getters, rootGetters, rootState } = bridgeActionContext(context); - const { address, externalAddress } = getters.asset || {}; + const { address, decimals, externalAddress } = getters.asset ?? {}; + const { networkSelected } = rootState.web3; const bridgeContractAddress = rootGetters.web3.contractAddress(KnownEthBridgeAsset.Other); - if (address && externalAddress && bridgeContractAddress) { - const assetKind = rootState.assets.registeredAssets[address]?.kind; - - if (assetKind === BridgeRequestAssetKind.Sidechain) { - const value = await ethersUtil.getAccountAssetBalance(bridgeContractAddress, externalAddress); - commit.setAssetLockedBalance(value); - return; + if (address && networkSelected && externalAddress && bridgeContractAddress) { + const registeredAsset = rootState.assets.registeredAssets[address]; + + if (registeredAsset) { + const { kind, decimals: externalDecimals } = registeredAsset; + + if (kind === EthAssetKind.Sidechain) { + const [lockedValue, bridgeValue] = await Promise.all([ + ethBridgeApi.getLockedAssets(networkSelected as number, address), + ethersUtil.getAccountAssetBalance(bridgeContractAddress, externalAddress), + ]); + const balance = FPNumber.min( + FPNumber.fromCodecValue(lockedValue, decimals), + FPNumber.fromCodecValue(bridgeValue, externalDecimals) + ); + commit.setAssetLockedBalance(balance); + return; + } } } @@ -295,20 +247,21 @@ async function updateEthLockedBalance(context: ActionContext): Promise async function updateBridgeProxyLockedBalance(context: ActionContext): Promise { const { commit, getters, rootState } = bridgeActionContext(context); - const { address } = getters.asset || {}; - const { networkSelected, networkType } = rootState.web3; + const { address, decimals } = getters.asset ?? {}; + const { networkSelected } = rootState.web3; - if (address && networkSelected && networkType) { - const bridgeApi = getBridgeApi(context) as typeof evmBridgeApi | typeof subBridgeApi; - const data = await bridgeApi.getLockedAssets(networkSelected as never, address); - const balance = data.toString(); + if (address && networkSelected) { + const bridgeApi = getBridgeApi(context); + const value = await bridgeApi.getLockedAssets(networkSelected as never, address); + const balance = FPNumber.fromCodecValue(value, decimals); commit.setAssetLockedBalance(balance); + return; } commit.setAssetLockedBalance(); } -async function getExternalTransferFee(context: ActionContext): Promise { +async function updateExternalTransferFee(context: ActionContext): Promise { const { commit, getters, state, rootState } = bridgeActionContext(context); let fee = ZeroStringValue; @@ -324,6 +277,96 @@ async function getExternalTransferFee(context: ActionContext): Promise commit.setExternalTransferFee(fee); } +function calculateMaxLimit( + limitAsset: string, + referenceAsset: string, + usdLimit: CodecString, + quote: SwapQuote +): FPNumber { + const outgoingLimitUSD = FPNumber.fromCodecValue(usdLimit); + + if (outgoingLimitUSD.isZero() || limitAsset === referenceAsset) return outgoingLimitUSD; + + try { + const { + result: { amount }, + } = quote(limitAsset, referenceAsset, 1, false, [], false); + + const assetPriceUSD = FPNumber.fromCodecValue(amount); + + if (!assetPriceUSD.isFinity() || assetPriceUSD.isZero()) return FPNumber.ZERO; + + return outgoingLimitUSD.div(assetPriceUSD); + } catch (error) { + console.error(error); + return FPNumber.ZERO; + } +} + +async function updateExternalBlockNumber(context: ActionContext): Promise { + const { getters, commit } = bridgeActionContext(context); + try { + const blockNumber = getters.isSubBridge + ? await subBridgeConnector.networkAdapter.getBlockNumber() + : await (await ethersUtil.getEthersInstance()).getBlockNumber(); + + commit.setExternalBlockNumber(blockNumber); + } catch (error) { + console.error(error); + commit.setExternalBlockNumber(0); + } +} + +async function updateFeesAndLockedFunds(context: ActionContext, withSora = false): Promise { + const { commit } = bridgeActionContext(context); + + commit.setFeesAndLockedFundsFetching(true); + + const promises = [ + updateExternalLockedBalance(context), + updateExternalNetworkFee(context), + updateExternalTransferFee(context), + ]; + + if (withSora) { + promises.push(updateSoraNetworkFee(context)); + } + + await Promise.allSettled(promises); + + commit.setFeesAndLockedFundsFetching(false); +} + +async function updateSoraNetworkFee(context: ActionContext): Promise { + const { commit, state, getters, rootState } = bridgeActionContext(context); + const { asset, operation } = getters; + const { + web3: { networkSelected }, + wallet: { + settings: { networkFees }, + }, + } = rootState; + + let fee = ZeroStringValue; + + if (networkSelected && asset && state.isSoraToEvm) { + if (getters.isEthBridge) { + fee = networkFees[operation]; + } else { + const bridgeApi = getBridgeApi(context) as typeof subBridgeApi | typeof evmBridgeApi; + fee = await bridgeApi.getNetworkFee(asset, networkSelected as never); + } + } + + commit.setSoraNetworkFee(fee); +} + +async function updateBalancesAndFees(context: ActionContext, withSora = false): Promise { + const { dispatch } = bridgeActionContext(context); + + await Promise.allSettled([dispatch.updateExternalBalance(), updateFeesAndLockedFunds(context, withSora)]); +} + const actions = defineActions({ setSendedAmount(context, value?: string) { const { commit, state, getters } = bridgeActionContext(context); @@ -361,14 +404,10 @@ const actions = defineActions({ } }, - async updateBalancesAndFees(context): Promise { + async resetBridgeForm(context): Promise { const { dispatch } = bridgeActionContext(context); - await Promise.all([ - dispatch.updateExternalBalance(), - dispatch.updateExternalLockedBalance(), - dispatch.updateExternalFees(), - ]); + await Promise.allSettled([dispatch.setAssetAddress(), dispatch.setSendedAmount()]); }, async switchDirection(context): Promise { @@ -378,7 +417,7 @@ const actions = defineActions({ commit.setAssetSenderBalance(); commit.setAssetRecipientBalance(); - await dispatch.updateBalancesAndFees(); + await updateBalancesAndFees(context, true); if (state.focusedField === FocusedField.Received) { await dispatch.setSendedAmount(state.amountReceived); @@ -394,23 +433,17 @@ const actions = defineActions({ commit.setAssetSenderBalance(); commit.setAssetRecipientBalance(); - await dispatch.updateBalancesAndFees(); - }, - - async updateExternalFees(context): Promise { - const { commit } = bridgeActionContext(context); - - commit.setExternalNetworkFeeFetching(true); - - await Promise.all([getExternalNetworkFee(context), getExternalTransferFee(context)]); - - commit.setExternalNetworkFeeFetching(false); + await Promise.allSettled([ + dispatch.updateOutgoingMaxLimit(), + dispatch.updateIncomingMinLimit(), + updateBalancesAndFees(context, true), + ]); }, async updateExternalBalance(context): Promise { const { commit, getters } = bridgeActionContext(context); - commit.setExternalBalancesFetching(true); + commit.setBalancesFetching(true); if (getters.isSubBridge) { await updateSubBalances(context); @@ -418,42 +451,78 @@ const actions = defineActions({ await updateEvmBalances(context); } - commit.setExternalBalancesFetching(false); + commit.setBalancesFetching(false); }, - async updateExternalLockedBalance(context): Promise { + async updateIncomingMinLimit(context): Promise { const { commit, getters } = bridgeActionContext(context); - commit.setAssetLockedBalanceFetching(true); - - if (getters.isEthBridge) { - await updateEthLockedBalance(context); - } else { - await updateBridgeProxyLockedBalance(context); + let minLimit = FPNumber.ZERO; + + if (getters.isSubBridge && getters.asset && getters.isRegisteredAsset) { + try { + const value = await subBridgeApi.soraParachainApi.getAssetMinimumAmount( + getters.asset.address, + subBridgeConnector.parachainAdapter.api + ); + minLimit = FPNumber.fromCodecValue(value, getters.asset.externalDecimals); + } catch (error) { + console.error(error); + } } - commit.setAssetLockedBalanceFetching(false); + commit.setIncomingMinLimit(minLimit); }, - async updateExternalBlockNumber(context): Promise { - const { getters, commit } = bridgeActionContext(context); + async updateOutgoingMaxLimit(context): Promise { + const { state, commit } = bridgeActionContext(context); - try { - const blockNumber = getters.isSubBridge - ? await subConnector.networkAdapter.getBlockNumber() - : await (await ethersUtil.getEthersInstance()).getBlockNumber(); + const limitAsset = state.assetAddress; - commit.setExternalBlockNumber(blockNumber); - } catch { - commit.setExternalBlockNumber(0); - } + commit.resetOutgoingMaxLimitSubscription(); + + if (!limitAsset) return; + + const hasOutgoingLimit = await api.bridgeProxy.isAssetTransferLimited(limitAsset); + + if (!hasOutgoingLimit) return; + + const referenceAsset = DAI.address; + const sources = [LiquiditySourceTypes.XYKPool, LiquiditySourceTypes.XSTPool]; + const limitObservable = api.bridgeProxy.getCurrentTransferLimitObservable(); + const quoteObservable = await api.swap.getSwapQuoteObservable(referenceAsset, limitAsset, sources, DexId.XOR); + + let subscription!: Subscription; + + await new Promise((resolve) => { + subscription = combineLatest([limitObservable, quoteObservable]).subscribe(([usdLimit, { quote }]) => { + const outgoingMaxLimit = calculateMaxLimit(limitAsset, referenceAsset, usdLimit, quote); + commit.setOutgoingMaxLimit(outgoingMaxLimit); + resolve(); + }); + }); + + commit.setOutgoingMaxLimitSubscription(subscription); + }, + + async subscribeOnBlockUpdates(context): Promise { + const { commit } = bridgeActionContext(context); + + commit.resetBlockUpdatesSubscription(); + + const subscription = api.system.updated.subscribe(() => { + updateExternalBlockNumber(context); + updateBalancesAndFees(context); + }); + + commit.setBlockUpdatesSubscription(subscription); }, async generateHistoryItem(context, playground): Promise { const { dispatch } = bridgeActionContext(context); const historyData = bridgeDataToHistoryItem(context, playground); const bridgeApi = getBridgeApi(context); - const historyItem = bridgeApi.generateHistoryItem(historyData as any); + const historyItem = bridgeApi.generateHistoryItem(historyData as never); if (!historyItem) { throw new Error('[Bridge]: "generateHistoryItem" failed'); @@ -481,18 +550,18 @@ const actions = defineActions({ if (getters.isEthBridge) { await updateEthHistory(context, clearHistory); } - if (getters.isEvmBridge) { - await updateEvmHistory(context); - } if (getters.isSubBridge) { await updateSubHistory(context, clearHistory); } + if (getters.isEvmBridge) { + console.info('Evm history not implemented'); + } commit.setHistoryLoading(false); }, removeHistory(context, { tx, force = false }: { tx: Partial; force: boolean }): void { - const { commit, dispatch, getters, state, rootState } = bridgeActionContext(context); + const { commit, dispatch, state, rootState } = bridgeActionContext(context); const { id, hash } = tx; @@ -555,7 +624,7 @@ const actions = defineActions({ async signEthBridgeOutgoingEvm(context, id: string): Promise { const { rootState, rootGetters } = bridgeActionContext(context); - const tx = ethBridgeApi.getHistory(id) as Nullable; + const tx = ethBridgeApi.getHistory(id) as Nullable; if (!tx?.hash) throw new Error('TX ID cannot be empty!'); if (!tx.amount) throw new Error('TX amount cannot be empty!'); @@ -582,11 +651,11 @@ const actions = defineActions({ const contractInstance = new ethers.Contract(contractAddress, contract.abi, signer); const method = isValOrXor ? 'mintTokensByPeers' - : request.currencyType === BridgeCurrencyType.TokenAddress + : request.currencyType === EthCurrencyType.TokenAddress ? 'receiveByEthereumAssetAddress' : 'receiveBySidechainAssetId'; const methodArgs: Array = [ - isValOrXor || request.currencyType === BridgeCurrencyType.TokenAddress + isValOrXor || request.currencyType === EthCurrencyType.TokenAddress ? asset.externalAddress // address tokenAddress OR : asset.address, // bytes32 assetId new FPNumber(tx.amount, asset.externalDecimals).toCodecString(), // uint256 amount diff --git a/src/store/bridge/getters.ts b/src/store/bridge/getters.ts index 7d04cb598..4bf8c4fab 100644 --- a/src/store/bridge/getters.ts +++ b/src/store/bridge/getters.ts @@ -4,6 +4,7 @@ import { defineGetters } from 'direct-vuex'; import { ZeroStringValue } from '@/consts'; import { bridgeGetterContext } from '@/store/bridge'; +import { formatSubAddress } from '@/utils/bridge/sub/utils'; import type { BridgeState } from './types'; import type { IBridgeTransaction, CodecString } from '@sora-substrate/util'; @@ -59,26 +60,56 @@ const getters = defineGetters()({ return !!asset?.externalAddress; }, + externalAccount(...args): string { + const { getters, rootState } = bridgeGetterContext(args); + const { evmAddress, subAddress } = rootState.web3; + + if (getters.isSubBridge) { + return subAddress; + } else { + return evmAddress; + } + }, + + externalAccountFormatted(...args): string { + const { getters, state, rootState } = bridgeGetterContext(args); + const { subSS58 } = rootState.web3; + + if (getters.isSubBridge && state.isSoraToEvm && getters.externalAccount) { + return formatSubAddress(getters.externalAccount, subSS58); + } else { + return getters.externalAccount; + } + }, + sender(...args): string { const { state, rootState, getters } = bridgeGetterContext(args); + const { address: soraAddress } = rootState.wallet.account; + const { evmAddress, subSS58 } = rootState.web3; - if (getters.isSubBridge) return rootState.wallet.account.address; + if (getters.isSubBridge) { + return !state.isSoraToEvm && soraAddress ? formatSubAddress(soraAddress, subSS58) : soraAddress; + } - return state.isSoraToEvm ? rootState.wallet.account.address : rootState.web3.evmAddress; + return state.isSoraToEvm ? soraAddress : evmAddress; }, recipient(...args): string { const { state, rootState, getters } = bridgeGetterContext(args); + const { address: soraAddress } = rootState.wallet.account; + const { evmAddress, subAddress, subSS58 } = rootState.web3; - if (getters.isSubBridge) return rootState.web3.subAddress; + if (getters.isSubBridge) { + return state.isSoraToEvm && subAddress ? formatSubAddress(subAddress, subSS58) : subAddress; + } - return state.isSoraToEvm ? rootState.web3.evmAddress : rootState.wallet.account.address; + return state.isSoraToEvm ? evmAddress : soraAddress; }, isEthBridge(...args): boolean { const { rootState } = bridgeGetterContext(args); - return rootState.web3.networkType === BridgeNetworkType.EvmLegacy; + return rootState.web3.networkType === BridgeNetworkType.Eth; }, isEvmBridge(...args): boolean { const { rootState } = bridgeGetterContext(args); @@ -100,10 +131,6 @@ const getters = defineGetters()({ return state.isSoraToEvm ? Operation.SubstrateOutgoing : Operation.SubstrateIncoming; } }, - soraNetworkFee(...args): CodecString { - const { getters, rootState } = bridgeGetterContext(args); - return rootState.wallet.settings.networkFees[getters.operation] ?? ZeroStringValue; - }, externalNetworkFee(...args): CodecString { const { state, getters } = bridgeGetterContext(args); diff --git a/src/store/bridge/mutations.ts b/src/store/bridge/mutations.ts index 7458d9bf7..8ef9f2a9e 100644 --- a/src/store/bridge/mutations.ts +++ b/src/store/bridge/mutations.ts @@ -2,7 +2,8 @@ import { defineMutations } from 'direct-vuex'; import omit from 'lodash/fp/omit'; import type { BridgeState, FocusedField } from './types'; -import type { IBridgeTransaction, CodecString } from '@sora-substrate/util'; +import type { FPNumber, IBridgeTransaction, CodecString } from '@sora-substrate/util'; +import type { Subscription } from 'rxjs'; const mutations = defineMutations()({ setSoraToEvm(state, isSoraToEvm: boolean): void { @@ -21,18 +22,39 @@ const mutations = defineMutations()({ state.assetRecipientBalance = balance; }, - setAssetLockedBalance(state, balance: Nullable = null): void { + setAssetLockedBalance(state, balance: Nullable = null): void { state.assetLockedBalance = balance; }, - setAssetLockedBalanceFetching(state, flag: boolean): void { - state.assetLockedBalanceFetching = flag; - }, setExternalBalance(state, balance: Nullable = null): void { state.externalNativeBalance = balance; }, - setExternalBalancesFetching(state, flag: boolean): void { - state.externalBalancesFetching = flag; + + setIncomingMinLimit(state, amount: FPNumber): void { + state.incomingMinLimit = amount; + }, + + setOutgoingMaxLimit(state, amount: Nullable): void { + state.outgoingMaxLimit = amount; + }, + + setOutgoingMaxLimitSubscription(state, subscription: Subscription): void { + state.outgoingMaxLimitSubscription = subscription; + }, + + resetOutgoingMaxLimitSubscription(state): void { + state.outgoingMaxLimitSubscription?.unsubscribe(); + state.outgoingMaxLimitSubscription = null; + state.outgoingMaxLimit = null; + }, + + setBlockUpdatesSubscription(state, subscription: Subscription): void { + state.blockUpdatesSubscription = subscription; + }, + + resetBlockUpdatesSubscription(state): void { + state.blockUpdatesSubscription?.unsubscribe(); + state.blockUpdatesSubscription = null; }, setAmountSend(state, value?: string): void { @@ -47,8 +69,12 @@ const mutations = defineMutations()({ state.focusedField = field; }, - setExternalNetworkFeeFetching(state, flag: boolean): void { - state.externalNetworkFeeFetching = flag; + setBalancesFetching(state, flag: boolean): void { + state.balancesFetching = flag; + }, + + setFeesAndLockedFundsFetching(state, flag: boolean): void { + state.feesAndLockedFundsFetching = flag; }, setExternalNetworkFee(state, fee: CodecString): void { @@ -59,6 +85,10 @@ const mutations = defineMutations()({ state.externalTransferFee = fee; }, + setSoraNetworkFee(state, fee: CodecString) { + state.soraNetworkFee = fee; + }, + /** * Set bridge transactions from localstorage (ethBridgeApi or evmBridgeApi) */ diff --git a/src/store/bridge/state.ts b/src/store/bridge/state.ts index f6c0074f4..6bf0fbf09 100644 --- a/src/store/bridge/state.ts +++ b/src/store/bridge/state.ts @@ -1,3 +1,5 @@ +import { FPNumber } from '@sora-substrate/util'; + import { ZeroStringValue } from '@/consts'; import type { BridgeState } from './types'; @@ -9,15 +11,19 @@ function initialState(): BridgeState { assetSenderBalance: null, // balance for sora assetRecipientBalance: null, // balance for bridge network assetLockedBalance: null, // asset balance locked on bridge - assetLockedBalanceFetching: false, + incomingMinLimit: FPNumber.ZERO, // incoming min limit in asset amount + outgoingMaxLimit: null, // outgoing max limit in asset amount + outgoingMaxLimitSubscription: null, + blockUpdatesSubscription: null, amountSend: '', amountReceived: '', focusedField: null, + soraNetworkFee: ZeroStringValue, externalTransferFee: ZeroStringValue, // fee for transfer between networks (xcm message fee for substrate network) externalNetworkFee: ZeroStringValue, // fee for transaction execution - externalNetworkFeeFetching: false, + balancesFetching: false, + feesAndLockedFundsFetching: false, externalNativeBalance: ZeroStringValue, // balance for external native token (like ETH) - externalBalancesFetching: false, externalBlockNumber: 0, // history sources historyInternal: {}, // localstorage history diff --git a/src/store/bridge/types.ts b/src/store/bridge/types.ts index 2ba335fde..1a6ce8c83 100644 --- a/src/store/bridge/types.ts +++ b/src/store/bridge/types.ts @@ -1,4 +1,5 @@ -import type { CodecString, IBridgeTransaction } from '@sora-substrate/util'; +import type { FPNumber, CodecString, IBridgeTransaction } from '@sora-substrate/util'; +import type { Subscription } from 'rxjs'; export enum FocusedField { Sended = 'Sended', @@ -10,16 +11,20 @@ export type BridgeState = { assetAddress: string; assetSenderBalance: Nullable; assetRecipientBalance: Nullable; - assetLockedBalance: Nullable; - assetLockedBalanceFetching: boolean; + assetLockedBalance: Nullable; + incomingMinLimit: FPNumber; + outgoingMaxLimit: Nullable; + outgoingMaxLimitSubscription: Nullable; + blockUpdatesSubscription: Nullable; amountSend: string; amountReceived: string; focusedField: Nullable; + soraNetworkFee: CodecString; externalTransferFee: CodecString; externalNetworkFee: CodecString; - externalNetworkFeeFetching: boolean; + balancesFetching: boolean; + feesAndLockedFundsFetching: boolean; externalNativeBalance: Nullable; - externalBalancesFetching: boolean; externalBlockNumber: number; // history sources (unsynced localstorage & network) historyInternal: Record; diff --git a/src/store/moonpay/actions.ts b/src/store/moonpay/actions.ts index 899c27973..c99b4b9a1 100644 --- a/src/store/moonpay/actions.ts +++ b/src/store/moonpay/actions.ts @@ -93,10 +93,8 @@ const actions = defineActions({ if (!decodedInput) throw new Error(`Unable to parse transaction data: "${tx.data}"`); const address = tx.to ?? ''; // asset address - const { - value, // BigNumber - to = '', // ethereum address - } = decodedInput.args; + const value = decodedInput.args.getValue('value'); // BigNumber + const to = decodedInput.args.getValue('to'); // ethereum address const amount = new FPNumber(value).toString(); // transferred amount return { diff --git a/src/store/moonpay/types.ts b/src/store/moonpay/types.ts index 6c28fa6ac..e1d615ff1 100644 --- a/src/store/moonpay/types.ts +++ b/src/store/moonpay/types.ts @@ -1,7 +1,7 @@ import type { MoonpayNotifications } from '@/components/pages/Moonpay/consts'; import type { MoonpayApi, MoonpayCurrency, MoonpayTransaction } from '@/utils/moonpay'; -import type { BridgeHistory } from '@sora-substrate/util'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; export type MoonpayState = { api: MoonpayApi; @@ -12,12 +12,12 @@ export type MoonpayState = { pollingTimestamp: number; transactions: Array; transactionsFetching: boolean; - bridgeTransactionData: Nullable; + bridgeTransactionData: Nullable; startBridgeButtonVisibility: boolean; currencies: Array; }; export type BridgeTxData = Partial<{ - data: Nullable; + data: Nullable; startBridgeButtonVisibility: boolean; }>; diff --git a/src/store/web3/actions.ts b/src/store/web3/actions.ts index e905bed96..483881886 100644 --- a/src/store/web3/actions.ts +++ b/src/store/web3/actions.ts @@ -6,83 +6,60 @@ import { ethers } from 'ethers'; import { KnownEthBridgeAsset, SmartContracts, SmartContractType } from '@/consts/evm'; import { web3ActionContext } from '@/store/web3'; -import { subConnector } from '@/utils/bridge/sub/classes/adapter'; +import { SubNetworksConnector, subBridgeConnector } from '@/utils/bridge/sub/classes/adapter'; import ethersUtil from '@/utils/ethers-util'; -import type { Provider } from '@/utils/ethers-util'; import type { SubNetworkApps } from './types'; import type { SubNetwork } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; import type { ActionContext } from 'vuex'; -async function connectEvmNetwork(context: ActionContext, networkHex?: string): Promise { - const { commit } = web3ActionContext(context); - const evmNetwork = networkHex ? ethersUtil.hexToNumber(networkHex) : await ethersUtil.getEvmNetworkId(); - commit.setProvidedEvmNetwork(evmNetwork); -} - async function connectSubNetwork(context: ActionContext): Promise { - const { getters } = web3ActionContext(context); + const { getters, commit } = web3ActionContext(context); const subNetwork = getters.selectedNetwork; if (!subNetwork) return; - await subConnector.open(subNetwork.id as SubNetwork); + await subBridgeConnector.open(subNetwork.id as SubNetwork); + + const ss58 = subBridgeConnector.networkAdapter.api.registry.chainSS58; + + if (ss58) commit.setSubSS58(ss58); } const actions = defineActions({ - async connectEvmAccount(context, provider: Provider): Promise { + async updateProvidedEvmNetwork(context, networkHex?: string): Promise { const { commit } = web3ActionContext(context); - const address = await ethersUtil.onConnect({ provider }); - commit.setEvmAddress(address); + const evmNetwork = networkHex ? ethersUtil.hexToNumber(networkHex) : await ethersUtil.getEvmNetworkId(); + commit.setProvidedEvmNetwork(evmNetwork); }, - async connectSubAccount(context, address: string): Promise { - const { commit, rootDispatch } = web3ActionContext(context); - commit.setSubAddress(address); - - await rootDispatch.bridge.updateExternalBalance(); + async disconnectExternalNetwork(context): Promise { + await subBridgeConnector.stop(); }, - async connectExternalNetwork(context, network?: string): Promise { - const { dispatch, state, rootDispatch } = web3ActionContext(context); + async selectExternalNetwork(context, { id, type }: { id: BridgeNetworkId; type: BridgeNetworkType }): Promise { + const { commit, dispatch, rootDispatch } = web3ActionContext(context); - await dispatch.disconnectExternalNetwork(); + dispatch.disconnectExternalNetwork(); - if (state.networkType === BridgeNetworkType.Sub) { - await connectSubNetwork(context); - } else { - await connectEvmNetwork(context, network); - } + commit.setNetworkType(type); + commit.setSelectedNetwork(id); - await Promise.all([rootDispatch.assets.updateRegisteredAssets(), rootDispatch.bridge.updateBalancesAndFees()]); - }, + rootDispatch.assets.updateRegisteredAssets(); - async disconnectExternalNetwork(context): Promise { - const { commit } = web3ActionContext(context); - - await subConnector.stop(); - - commit.resetProvidedEvmNetwork(); - }, - - async selectExternalNetwork(context, network: BridgeNetworkId): Promise { - const { commit, dispatch } = web3ActionContext(context); - commit.setSelectedNetwork(network); - await dispatch.updateNetworkProvided(); + if (type === BridgeNetworkType.Sub) { + await connectSubNetwork(context); + } }, - async updateNetworkProvided(context): Promise { - const { dispatch, getters, state } = web3ActionContext(context); + async changeEvmNetworkProvided(context): Promise { + const { getters, state } = web3ActionContext(context); const { selectedNetwork } = getters; const { networkType } = state; - if (!selectedNetwork) return; - - if (networkType !== BridgeNetworkType.Sub) { + if (selectedNetwork && networkType !== BridgeNetworkType.Sub) { await ethersUtil.switchOrAddChain(selectedNetwork); } - - await dispatch.connectExternalNetwork(); }, async getSupportedApps(context): Promise { @@ -95,15 +72,15 @@ const actions = defineActions({ const { commit } = web3ActionContext(context); // update apps in store commit.setSubNetworkApps(apps); - // update endpoints in subConnector - subConnector.endpoints = apps; + // update endpoints in SubNetworksConnector class + SubNetworksConnector.endpoints = apps; }, /** - * Restore selected by user network & network type (EVMLegacy, EVM, Sub) + * Restore selected by user network & network type (Eth, EVM, Sub) */ async restoreSelectedNetwork(context): Promise { - const { commit, state, getters } = web3ActionContext(context); + const { dispatch, state, getters } = web3ActionContext(context); const [type, id] = [ethersUtil.getSelectedBridgeType(), ethersUtil.getSelectedNetwork()]; @@ -111,14 +88,15 @@ const actions = defineActions({ const networkData = getters.availableNetworks[type]?.[id]; if (!!networkData && !networkData.disabled) { - commit.setNetworkType(type); - commit.setSelectedNetwork(id); + await dispatch.selectExternalNetwork({ id, type }); return; } } - commit.setNetworkType(BridgeNetworkType.EvmLegacy); - commit.setSelectedNetwork(state.ethBridgeEvmNetwork); + await dispatch.selectExternalNetwork({ + id: state.ethBridgeEvmNetwork, + type: BridgeNetworkType.Eth, + }); }, async getEvmTokenAddressByAssetId(context, soraAssetId: string): Promise { diff --git a/src/store/web3/getters.ts b/src/store/web3/getters.ts index b57d16250..2061ad99b 100644 --- a/src/store/web3/getters.ts +++ b/src/store/web3/getters.ts @@ -13,16 +13,6 @@ import type { Web3State, AvailableNetwork } from './types'; import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; const getters = defineGetters()({ - externalAccount(...args): string { - const { state } = web3GetterContext(args); - - if (state.networkType === BridgeNetworkType.Sub) { - return state.subAddress; - } else { - return state.evmAddress; - } - }, - availableNetworks(...args): Record>> { const { state } = web3GetterContext(args); @@ -75,7 +65,7 @@ const getters = defineGetters()({ }, {}); return { - [BridgeNetworkType.EvmLegacy]: hashi, + [BridgeNetworkType.Eth]: hashi, [BridgeNetworkType.Evm]: evm, [BridgeNetworkType.Sub]: sub, }; diff --git a/src/store/web3/mutations.ts b/src/store/web3/mutations.ts index 660838a75..30b7113f0 100644 --- a/src/store/web3/mutations.ts +++ b/src/store/web3/mutations.ts @@ -22,6 +22,10 @@ const mutations = defineMutations()({ state.subAddress = address; }, + setSubSS58(state, prefix: number) { + state.subSS58 = prefix; + }, + setEvmNetworksApp(state, networksIds: EvmNetwork[]): void { state.evmNetworkApps = networksIds; }, diff --git a/src/store/web3/state.ts b/src/store/web3/state.ts index febd3e895..7ff579007 100644 --- a/src/store/web3/state.ts +++ b/src/store/web3/state.ts @@ -1,15 +1,13 @@ import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; import { EvmNetworkId } from '@sora-substrate/util/build/bridgeProxy/evm/consts'; -import { ZeroStringValue } from '@/consts'; -import ethersUtil from '@/utils/ethers-util'; - import type { Web3State } from './types'; export function initialState(): Web3State { return { evmAddress: '', // external evm address subAddress: '', // external sub address + subSS58: 69, // external sub network ss58 prefix (sora by default) networkType: null, // network type for selected network networkSelected: null, // network selected by user @@ -19,7 +17,7 @@ export function initialState(): Web3State { subNetworkApps: {}, // sub netowrks from app config supportedApps: { - [BridgeNetworkType.EvmLegacy]: {}, + [BridgeNetworkType.Eth]: {}, [BridgeNetworkType.Evm]: {}, [BridgeNetworkType.Sub]: [], }, // supported apps from chain diff --git a/src/store/web3/types.ts b/src/store/web3/types.ts index 1d3681a06..d43f74076 100644 --- a/src/store/web3/types.ts +++ b/src/store/web3/types.ts @@ -29,6 +29,7 @@ export type AvailableNetwork = { export type Web3State = { evmAddress: string; subAddress: string; + subSS58: number; networkType: Nullable; networkSelected: Nullable; diff --git a/src/utils/bridge/common/utils.ts b/src/utils/bridge/common/utils.ts index 7351fa98d..3e073f486 100644 --- a/src/utils/bridge/common/utils.ts +++ b/src/utils/bridge/common/utils.ts @@ -1,4 +1,4 @@ -import { Operation, isBridgeOperation, isEvmOperation, isSubstrateOperation } from '@sora-substrate/util'; +import { Operation, isEthOperation, isEvmOperation, isSubstrateOperation } from '@sora-substrate/util'; import { api as soraApi } from '@soramitsu/soraneo-wallet-web'; import { ethers } from 'ethers'; @@ -8,7 +8,8 @@ import { isUnsignedTx as isUnsignedSubTx } from '@/utils/bridge/sub/utils'; import ethersUtil from '@/utils/ethers-util'; import type { ApiPromise } from '@polkadot/api'; -import type { IBridgeTransaction, BridgeHistory } from '@sora-substrate/util'; +import type { IBridgeTransaction } from '@sora-substrate/util'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; import type { EvmHistory } from '@sora-substrate/util/build/bridgeProxy/evm/types'; import type { SubHistory } from '@sora-substrate/util/build/bridgeProxy/sub/types'; @@ -64,39 +65,26 @@ export const getEvmTransactionRecieptByHash = async ( } }; +export const getBlockEventsByTxIndex = async (blockHash: string, index: number, api: ApiPromise) => { + const blockEvents = await soraApi.system.getBlockEvents(blockHash, api); + const transactionEvents = blockEvents.filter( + ({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.toNumber() === index + ); + + return transactionEvents; +}; + export const getTransactionEvents = async (blockHash: string, transactionHash: string, api: ApiPromise) => { const extrinsics = await soraApi.system.getExtrinsicsFromBlock(blockHash, api); const extrinsicIndex = extrinsics.findIndex((ext) => ext.hash.toString() === transactionHash); if (extrinsicIndex === -1) throw new Error(`Unable to find extrinsic "${transactionHash}" in block "${blockHash}"`); - const blockEvents = await soraApi.system.getBlockEvents(blockHash, api); - const transactionEvents = blockEvents.filter( - ({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.toNumber() === extrinsicIndex - ); + const transactionEvents = await getBlockEventsByTxIndex(blockHash, extrinsicIndex, api); return transactionEvents; }; -export const findEventInBlock = async ({ - api, - blockId, - section, - method, -}: { - api: ApiPromise; - blockId: string; - section: string; - method: string; -}) => { - const blockEvents = await soraApi.system.getBlockEvents(blockId, api); - const event = blockEvents.find(({ event }) => event.section === section && event.method === method); - - if (!event) throw new Error('Event not found'); - - return event.event.data; -}; - export const isOutgoingTransaction = (transaction: Nullable): boolean => { if (!transaction?.type) return false; @@ -106,7 +94,7 @@ export const isOutgoingTransaction = (transaction: Nullable) export const isUnsignedTx = (transaction: Nullable): boolean => { if (!transaction?.type) return true; - if (isBridgeOperation(transaction.type)) return isUnsignedEthTx(transaction as BridgeHistory); + if (isEthOperation(transaction.type)) return isUnsignedEthTx(transaction as EthHistory); if (isEvmOperation(transaction.type)) return isUnsignedEvmTx(transaction as EvmHistory); if (isSubstrateOperation(transaction.type)) return isUnsignedSubTx(transaction as SubHistory); diff --git a/src/utils/bridge/eth/api.ts b/src/utils/bridge/eth/api.ts index ab5dd794d..366271557 100644 --- a/src/utils/bridge/eth/api.ts +++ b/src/utils/bridge/eth/api.ts @@ -1,3 +1,3 @@ import { api } from '@soramitsu/soraneo-wallet-web'; -export const ethBridgeApi = api.bridge; +export const ethBridgeApi = api.bridgeProxy.eth; diff --git a/src/utils/bridge/eth/classes/history.ts b/src/utils/bridge/eth/classes/history.ts index 5a534c801..242c570b9 100644 --- a/src/utils/bridge/eth/classes/history.ts +++ b/src/utils/bridge/eth/classes/history.ts @@ -1,5 +1,5 @@ -import { BridgeTxStatus, Operation } from '@sora-substrate/util'; -import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; +import { Operation } from '@sora-substrate/util'; +import { BridgeNetworkType, BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; import { api, historyElementsFilter, @@ -19,8 +19,9 @@ import { getEvmTransactionRecieptByHash, isOutgoingTransaction } from '@/utils/b import { ethBridgeApi } from '@/utils/bridge/eth/api'; import ethersUtil from '@/utils/ethers-util'; -import type { BridgeHistory, NetworkFeesObject } from '@sora-substrate/util'; +import type { NetworkFeesObject } from '@sora-substrate/util'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; import type { ActionContext } from 'vuex'; export default class EtherscanHistoryProvider extends EtherscanProvider { @@ -44,7 +45,7 @@ const BRIDGE_INTERFACE = new ethers.Interface([ const { ETH_BRIDGE_STATES } = WALLET_CONSTS; -const isLocalHistoryItem = (item: BridgeHistory, txId: string, isOutgoing: boolean, requestHash: string) => { +const isLocalHistoryItem = (item: EthHistory, txId: string, isOutgoing: boolean, requestHash: string) => { if (item.txId === txId) return true; return isOutgoing ? item.hash === requestHash : item.externalHash === requestHash; @@ -92,7 +93,7 @@ const getTransactionState = (isOutgoing: boolean, soraPartCompleted: boolean, ex } }; -const hasFinishedState = (item: Nullable) => { +const hasFinishedState = (item: Nullable) => { if (!item) return false; const isOutgoing = isOutgoingTransaction(item); @@ -232,12 +233,14 @@ export class EthBridgeHistory { for (const tx of Object.values(transactions)) { try { - const decodedInput = BRIDGE_INTERFACE.parseTransaction(tx); + const data = (tx as any).input; // 'data' is named as 'input' + const decodedInput = BRIDGE_INTERFACE.parseTransaction({ data }); - if (decodedInput?.args.txHash.toLowerCase() === hash.toLowerCase()) { + if (decodedInput?.args.getValue('txHash').toLowerCase() === hash.toLowerCase()) { return tx; } } catch (err) { + console.info(err); continue; } } @@ -294,7 +297,7 @@ export class EthBridgeHistory { if (!historyElements.length) return; - const currentHistory = ethBridgeApi.historyList as BridgeHistory[]; + const currentHistory = ethBridgeApi.historyList as EthHistory[]; const { externalNetwork } = this; @@ -307,7 +310,7 @@ export class EthBridgeHistory { const { id: txId, blockHash: blockId, blockHeight, data: historyElementData } = historyElement; const { requestHash, amount, assetId: assetAddress, sidechainAddress } = historyElementData as HistoryElementData; - const localHistoryItem = currentHistory.find((item: BridgeHistory) => + const localHistoryItem = currentHistory.find((item: EthHistory) => isLocalHistoryItem(item, txId, isOutgoing, requestHash) ); @@ -354,16 +357,16 @@ export class EthBridgeHistory { externalBlockId, externalBlockHeight, externalNetwork, - externalNetworkType: BridgeNetworkType.EvmLegacy, + externalNetworkType: BridgeNetworkType.Eth, externalNetworkFee, to, }; // update or create local history item if (localHistoryItem) { - ethBridgeApi.saveHistory({ ...localHistoryItem, ...historyItemData } as BridgeHistory); + ethBridgeApi.saveHistory({ ...localHistoryItem, ...historyItemData } as EthHistory); } else { - ethBridgeApi.generateHistoryItem(historyItemData as BridgeHistory); + ethBridgeApi.generateHistoryItem(historyItemData as EthHistory); } await updateCallback?.(); @@ -395,7 +398,7 @@ export const updateEthBridgeHistory = bridge: { inProgressIds }, } = rootState; - const networkData = rootGetters.web3.availableNetworks[BridgeNetworkType.EvmLegacy][ethBridgeEvmNetwork]; + const networkData = rootGetters.web3.availableNetworks[BridgeNetworkType.Eth][ethBridgeEvmNetwork]; if (!networkData) { throw new Error( diff --git a/src/utils/bridge/eth/classes/reducers.ts b/src/utils/bridge/eth/classes/reducers.ts index dcc9454ec..935a0565c 100644 --- a/src/utils/bridge/eth/classes/reducers.ts +++ b/src/utils/bridge/eth/classes/reducers.ts @@ -1,20 +1,20 @@ -import { api, SUBQUERY_TYPES, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; -import { ethers } from 'ethers'; +import { SUBQUERY_TYPES, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; import first from 'lodash/fp/first'; import { BridgeReducer } from '@/utils/bridge/common/classes'; import type { IBridgeReducerOptions, GetBridgeHistoryInstance, SignExternal } from '@/utils/bridge/common/types'; import { getEvmTransactionRecieptByHash, - findEventInBlock, + getTransactionEvents, waitForEvmTransactionMined, } from '@/utils/bridge/common/utils'; import { ethBridgeApi } from '@/utils/bridge/eth/api'; import type { EthBridgeHistory } from '@/utils/bridge/eth/classes/history'; import { getTransaction, waitForApprovedRequest, waitForIncomingRequest } from '@/utils/bridge/eth/utils'; -import type { BridgeHistory, IBridgeTransaction } from '@sora-substrate/util'; +import type { IBridgeTransaction } from '@sora-substrate/util'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; const { ETH_BRIDGE_STATES } = WALLET_CONSTS; @@ -24,12 +24,12 @@ type EthBridgeReducerOptions = IBridgeReducerOptio signExternalIncoming: SignExternal; }; -export class EthBridgeReducer extends BridgeReducer { +export class EthBridgeReducer extends BridgeReducer { protected readonly getBridgeHistoryInstance!: GetBridgeHistoryInstance; protected readonly signExternalOutgoing!: SignExternal; protected readonly signExternalIncoming!: SignExternal; - constructor(options: EthBridgeReducerOptions) { + constructor(options: EthBridgeReducerOptions) { super(options); this.getBridgeHistoryInstance = options.getBridgeHistoryInstance; @@ -53,7 +53,7 @@ export class EthBridgeReducer extends BridgeReducer { ); } - // In BridgeHistory 'blockHeight' will store evm block number + // In EthHistory 'blockHeight' will store evm block number this.updateTransactionParams(id, { externalNetworkFee: fee, externalBlockHeight: blockNumber, @@ -97,7 +97,7 @@ export class EthBridgeReducer extends BridgeReducer { } export class EthBridgeOutgoingReducer extends EthBridgeReducer { - async changeState(transaction: BridgeHistory): Promise { + async changeState(transaction: EthHistory): Promise { if (!transaction.id) throw new Error('[Bridge]: TX ID cannot be empty'); switch (transaction.transactionState) { @@ -122,7 +122,7 @@ export class EthBridgeOutgoingReducer extends EthBridgeReducer { if (!tx.txId) { await this.beforeSign(id); const asset = this.getAssetByAddress(tx.assetAddress as string) as RegisteredAccountAsset; - await ethBridgeApi.transferToEth(asset, tx.to as string, tx.amount as string, id); + await ethBridgeApi.transfer(asset, tx.to as string, tx.amount as string, id); } // signed sora transaction has to be parsed by subquery @@ -153,18 +153,17 @@ export class EthBridgeOutgoingReducer extends EthBridgeReducer { await this.waitForTransactionStatus(id); await this.waitForTransactionBlockId(id); - const { blockId } = this.getTransaction(id); + const { blockId, txId, hash: soraHash } = this.getTransaction(id); - const eventData = await findEventInBlock({ - api: api.api, - blockId: blockId as string, - section: 'ethBridge', - method: 'RequestRegistered', - }); + if (!soraHash) { + const transactionEvents = await getTransactionEvents(blockId as string, txId as string, ethBridgeApi.api); + const requestEvent = transactionEvents.find((e) => + ethBridgeApi.api.events.ethBridge.RequestRegistered.is(e.event) + ); + const hash = requestEvent.event.data[0].toString(); - const hash = eventData[0].toString(); - - this.updateTransactionParams(id, { hash }); + this.updateTransactionParams(id, { hash }); + } const tx = this.getTransaction(id); @@ -205,7 +204,7 @@ export class EthBridgeOutgoingReducer extends EthBridgeReducer { } export class EthBridgeIncomingReducer extends EthBridgeReducer { - async changeState(transaction: BridgeHistory): Promise { + async changeState(transaction: EthHistory): Promise { if (!transaction.id) throw new Error('[Bridge]: TX ID cannot be empty'); switch (transaction.transactionState) { diff --git a/src/utils/bridge/eth/index.ts b/src/utils/bridge/eth/index.ts index 20befd17c..7ffa79ca0 100644 --- a/src/utils/bridge/eth/index.ts +++ b/src/utils/bridge/eth/index.ts @@ -9,13 +9,13 @@ import { EthBridgeOutgoingReducer, EthBridgeIncomingReducer } from '@/utils/brid import type { EthBridgeReducer } from '@/utils/bridge/eth/classes/reducers'; import { getTransaction, updateTransaction } from '@/utils/bridge/eth/utils'; -import type { BridgeHistory } from '@sora-substrate/util'; +import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/types'; -interface EthBridgeConstructorOptions extends IBridgeConstructorOptions { +interface EthBridgeConstructorOptions extends IBridgeConstructorOptions { getBridgeHistoryInstance: GetBridgeHistoryInstance; } -type EthBridge = Bridge; +type EthBridge = Bridge; const { ETH_BRIDGE_STATES } = WALLET_CONSTS; @@ -41,11 +41,11 @@ const ethBridge: EthBridge = new Bridge({ getTransaction, updateTransaction, // ui integration - showNotification: (tx: BridgeHistory) => store.commit.bridge.setNotificationData(tx as any), + showNotification: (tx: EthHistory) => store.commit.bridge.setNotificationData(tx as any), addTransactionToProgress: (id: string) => store.commit.bridge.addTxIdInProgress(id), removeTransactionFromProgress: (id: string) => store.commit.bridge.removeTxIdFromProgress(id), updateHistory: () => store.dispatch.bridge.updateInternalHistory(), - getActiveTransaction: () => store.getters.bridge.historyItem as BridgeHistory, + getActiveTransaction: () => store.getters.bridge.historyItem as EthHistory, // transaction signing beforeTransactionSign: () => store.dispatch.wallet.transactions.beforeTransactionSign(), // custom diff --git a/src/utils/bridge/eth/utils.ts b/src/utils/bridge/eth/utils.ts index 0e5b1f40e..9906fd366 100644 --- a/src/utils/bridge/eth/utils.ts +++ b/src/utils/bridge/eth/utils.ts @@ -1,11 +1,12 @@ -import { Operation, BridgeTxStatus } from '@sora-substrate/util'; +import { Operation } from '@sora-substrate/util'; +import { BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; import { ethBridgeApi } from '@/utils/bridge/eth/api'; -import type { BridgeHistory, BridgeApprovedRequest } from '@sora-substrate/util'; +import type { EthHistory, EthApprovedRequest } from '@sora-substrate/util/build/bridgeProxy/eth/types'; import type { Subscription } from 'rxjs'; -export const isUnsignedFromPart = (tx: BridgeHistory): boolean => { +export const isUnsignedFromPart = (tx: EthHistory): boolean => { if (tx.type === Operation.EthBridgeOutgoing) { return !tx.blockId && !tx.txId; } else if (tx.type === Operation.EthBridgeIncoming) { @@ -15,7 +16,7 @@ export const isUnsignedFromPart = (tx: BridgeHistory): boolean => { } }; -export const isUnsignedToPart = (tx: BridgeHistory): boolean => { +export const isUnsignedToPart = (tx: EthHistory): boolean => { if (tx.type === Operation.EthBridgeOutgoing) { return !tx.externalHash; } else if (tx.type === Operation.EthBridgeIncoming) { @@ -25,12 +26,12 @@ export const isUnsignedToPart = (tx: BridgeHistory): boolean => { } }; -export const isUnsignedTx = (tx: BridgeHistory): boolean => { +export const isUnsignedTx = (tx: EthHistory): boolean => { return isUnsignedFromPart(tx); }; -export const getTransaction = (id: string): BridgeHistory => { - const tx = ethBridgeApi.getHistory(id) as BridgeHistory; +export const getTransaction = (id: string): EthHistory => { + const tx = ethBridgeApi.getHistory(id) as EthHistory; if (!tx) throw new Error(`[Bridge]: Transaction is not exists: ${id}`); @@ -42,7 +43,7 @@ export const updateTransaction = async (id: string, params = {}) => { ethBridgeApi.saveHistory({ ...tx, ...params }); }; -export const waitForApprovedRequest = async (tx: BridgeHistory): Promise => { +export const waitForApprovedRequest = async (tx: EthHistory): Promise => { if (!tx.hash) throw new Error(`[Bridge]: Tx hash cannot be empty`); if (!Number.isFinite(tx.externalNetwork)) throw new Error(`[Bridge]: Tx externalNetwork should be a number, ${tx.externalNetwork} received`); @@ -54,6 +55,7 @@ export const waitForApprovedRequest = async (tx: BridgeHistory): Promise => { +export const waitForIncomingRequest = async (tx: EthHistory): Promise<{ hash: string; blockId: string }> => { if (!tx.externalHash) throw new Error('[Bridge]: externalHash cannot be empty!'); if (!Number.isFinite(tx.externalNetwork)) throw new Error(`[Bridge]: Tx externalNetwork should be a number, ${tx.externalNetwork} received`); @@ -81,6 +83,7 @@ export const waitForIncomingRequest = async (tx: BridgeHistory): Promise<{ hash: switch (request.status) { case BridgeTxStatus.Failed: case BridgeTxStatus.Frozen: + case BridgeTxStatus.Broken: reject(new Error('[Bridge]: Transaction was failed or canceled')); break; case BridgeTxStatus.Done: diff --git a/src/utils/bridge/evm/classes/reducers.ts b/src/utils/bridge/evm/classes/reducers.ts index fbffa42bc..3c91e6b5f 100644 --- a/src/utils/bridge/evm/classes/reducers.ts +++ b/src/utils/bridge/evm/classes/reducers.ts @@ -2,7 +2,7 @@ import { BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; import { BridgeReducer } from '@/utils/bridge/common/classes'; import type { RemoveTransactionByHash, IBridgeReducerOptions } from '@/utils/bridge/common/types'; -import { findEventInBlock } from '@/utils/bridge/common/utils'; +import { getTransactionEvents } from '@/utils/bridge/common/utils'; import { evmBridgeApi } from '@/utils/bridge/evm/api'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; @@ -97,14 +97,13 @@ export class EvmBridgeOutgoingReducer extends EvmBridgeReducer { if (tx.hash) return tx.hash; - const eventData = await findEventInBlock({ - api: evmBridgeApi.api, - blockId: tx.blockId as string, - section: 'bridgeProxy', - method: 'RequestStatusUpdate', - }); - - const hash = eventData[0].toString(); + const blockHash = tx.blockId as string; + const transactionHash = tx.txId as string; + const transactionEvents = await getTransactionEvents(blockHash, transactionHash, evmBridgeApi.api); + const requestEvent = transactionEvents.find((e) => + evmBridgeApi.api.events.bridgeProxy.RequestStatusUpdate.is(e.event) + ); + const hash = requestEvent.event.data[0].toString(); this.updateTransactionParams(id, { hash }); diff --git a/src/utils/bridge/sub/classes/adapter.ts b/src/utils/bridge/sub/classes/adapter.ts index 21f8cebcc..19b423720 100644 --- a/src/utils/bridge/sub/classes/adapter.ts +++ b/src/utils/bridge/sub/classes/adapter.ts @@ -34,7 +34,7 @@ export class SubAdapter { } get connected(): boolean { - return this.connection.opened; + return !!this.api?.isConnected; } public setEndpoint(endpoint: string): void { @@ -44,6 +44,7 @@ export class SubAdapter { public async connect(): Promise { if (!this.connected && this.endpoint) { await this.connection.open(this.endpoint); + await this.api.isReady; } } @@ -53,8 +54,12 @@ export class SubAdapter { } } + public getSoraParachainNetwork(): SubNetwork { + return subBridgeApi.getSoraParachain(this.subNetwork); + } + public getSoraParachainId(): number | undefined { - const soraParachain = subBridgeApi.getSoraParachain(this.subNetwork); + const soraParachain = this.getSoraParachainNetwork(); const soraParachainId = subBridgeApi.parachainIds[soraParachain]; return soraParachainId; @@ -63,6 +68,8 @@ export class SubAdapter { public async getBlockNumber(): Promise { if (!this.connected) return 0; + await this.api.isReady; + const result = await this.api.query.system.number(); return result.toNumber(); @@ -71,6 +78,8 @@ export class SubAdapter { protected async getAccountBalance(accountAddress: string): Promise { if (!(this.connected && accountAddress)) return ZeroStringValue; + await this.api.isReady; + const accountInfo = await this.api.query.system.account(accountAddress); const accountBalance = formatBalance(accountInfo.data); const balance = accountBalance.transferable; @@ -100,9 +109,13 @@ export class SubAdapter { } /* [Substrate 5] Runtime call transactionPaymentApi */ - public async getNetworkFee(): Promise { + public async getNetworkFee(asset: RegisteredAsset): Promise { + if (!this.connected) return ZeroStringValue; + + await this.api.isReady; + const decimals = this.api.registry.chainDecimals[0]; - const tx = this.getTransferExtrinsic({} as RegisteredAsset, '', ZeroStringValue); + const tx = this.getTransferExtrinsic(asset, '', ZeroStringValue); const res = await tx.paymentInfo(''); return new FPNumber(res.partialFee, decimals).toCodecString(); @@ -162,9 +175,9 @@ class KusamaAdapter extends SubAdapter { } /* Throws error until Substrate 5 migration */ - public async getNetworkFee(): Promise { + public async getNetworkFee(asset: RegisteredAsset): Promise { try { - return await super.getNetworkFee(); + return await super.getNetworkFee(asset); } catch { // Hardcoded value for Kusama - 0.0007 KSM return '700000000'; @@ -189,7 +202,15 @@ class KusamaAdapter extends SubAdapter { } } -class SubConnector { +export class SubNetworksConnector { + public network!: SubNetwork; + public parachainNetwork!: SubNetwork; + public networkAdapter!: SubAdapter; + public parachainAdapter!: SubAdapter; + public parachainId!: number; + + public static endpoints: SubNetworkApps = {}; + public readonly adapters = { [SubNetwork.Rococo]: () => new KusamaAdapter(SubNetwork.Rococo), [SubNetwork.Kusama]: () => new KusamaAdapter(SubNetwork.Kusama), @@ -197,19 +218,14 @@ class SubConnector { [SubNetwork.KusamaSora]: () => new SubAdapter(SubNetwork.KusamaSora), }; - public endpoints: SubNetworkApps = {}; - - /** Adapter for Substrate network. Used for network selected in app */ - public networkAdapter!: SubAdapter; - public getAdapterForNetwork(network: SubNetwork): SubAdapter { if (!(network in this.adapters)) { throw new Error(`[${this.constructor.name}] Adapter for "${network}" network not implemented`); } - if (!(network in this.endpoints)) { + if (!(network in SubNetworksConnector.endpoints)) { throw new Error(`[${this.constructor.name}] Endpoint for "${network}" network is not defined`); } - const endpoint = this.endpoints[network]; + const endpoint = SubNetworksConnector.endpoints[network]; const adapter = this.adapters[network](); adapter.setEndpoint(endpoint); @@ -217,24 +233,39 @@ class SubConnector { return adapter; } + public async init(network: SubNetwork): Promise { + this.network = network; + this.networkAdapter = this.getAdapterForNetwork(this.network); + this.parachainNetwork = this.networkAdapter.getSoraParachainNetwork(); + this.parachainAdapter = this.getAdapterForNetwork(this.parachainNetwork); + this.parachainId = this.parachainAdapter.getSoraParachainId() as number; + } + /** - * Open main connection with Substrate network + * Open main connection with Substrate network & Sora parachain */ public async open(network: SubNetwork): Promise { - // stop current adapter connection + // stop current connections await this.stop(); // set adapter for network arg - this.networkAdapter = this.getAdapterForNetwork(network); - // open adapter connection - await this.networkAdapter.connect(); + await this.init(network); + // open adapters connections + await this.start(); + } + + /** + * Connect to Substrate network & Sora parachain + */ + public async start(): Promise { + await Promise.all([this.networkAdapter.connect(), this.parachainAdapter.connect()]); } /** - * Close main connection to selected Substrate network + * Close connections to Substrate network & Sora parachain */ public async stop(): Promise { - await this.networkAdapter?.stop(); + await Promise.all([this.networkAdapter?.stop(), this.parachainAdapter?.stop()]); } } -export const subConnector = new SubConnector(); +export const subBridgeConnector = new SubNetworksConnector(); diff --git a/src/utils/bridge/sub/classes/history.ts b/src/utils/bridge/sub/classes/history.ts index 8a0f7f959..739606d3d 100644 --- a/src/utils/bridge/sub/classes/history.ts +++ b/src/utils/bridge/sub/classes/history.ts @@ -4,15 +4,10 @@ import { api } from '@soramitsu/soraneo-wallet-web'; import { ZeroStringValue } from '@/consts'; import { rootActionContext } from '@/store'; -import { findEventInBlock } from '@/utils/bridge/common/utils'; +import { getBlockEventsByTxIndex } from '@/utils/bridge/common/utils'; import { subBridgeApi } from '@/utils/bridge/sub/api'; -import { subConnector } from '@/utils/bridge/sub/classes/adapter'; -import type { SubAdapter } from '@/utils/bridge/sub/classes/adapter'; -import { - getRelayChainBlockNumber, - getMessageAcceptedNonces, - isMessageDispatchedNonces, -} from '@/utils/bridge/sub/utils'; +import { SubNetworksConnector } from '@/utils/bridge/sub/classes/adapter'; +import { getMessageAcceptedNonces, isMessageDispatchedNonces, formatSubAddress } from '@/utils/bridge/sub/utils'; import type { ApiPromise } from '@polkadot/api'; import type { NetworkFeesObject } from '@sora-substrate/util'; @@ -56,35 +51,17 @@ const findTxInBlock = async (blockHash: string, soraHash: string) => { return { hash: txHash, events: txEvents }; }; -class SubBridgeHistory { - private externalNetwork!: SubNetwork; - private parachainNetwork!: SubNetwork; - private externalNetworkAdapter!: SubAdapter; - private parachainNetworkAdapter!: SubAdapter; - private parachainId!: number; - - public async init(externalNetwork: SubNetwork): Promise { - this.externalNetwork = externalNetwork; - this.parachainNetwork = subBridgeApi.getSoraParachain(this.externalNetwork); - this.externalNetworkAdapter = subConnector.getAdapterForNetwork(this.externalNetwork); - this.parachainNetworkAdapter = subConnector.getAdapterForNetwork(this.parachainNetwork); - this.parachainId = subBridgeApi.parachainIds[this.parachainNetwork] as number; - } - +class SubBridgeHistory extends SubNetworksConnector { get soraApi(): ApiPromise { return subBridgeApi.api; } get parachainApi(): ApiPromise { - return this.parachainNetworkAdapter.api; + return this.parachainAdapter.api; } get externalApi(): ApiPromise { - return this.externalNetworkAdapter.api; - } - - private async connect() { - await Promise.all([this.externalNetworkAdapter.connect(), this.parachainNetworkAdapter.connect()]); + return this.networkAdapter.api; } public async clearHistory(updateCallback?: FnWithoutArgs | AsyncFnWithoutArgs): Promise { @@ -100,7 +77,7 @@ class SubBridgeHistory { updateCallback?: FnWithoutArgs | AsyncFnWithoutArgs ): Promise { try { - const transactions = await subBridgeApi.getUserTransactions(address, this.externalNetwork); + const transactions = await subBridgeApi.getUserTransactions(address, this.network); if (!transactions.length) return; @@ -114,7 +91,7 @@ class SubBridgeHistory { if ((localHistoryItem?.id as string) in inProgressIds) continue; if (hasFinishedState(localHistoryItem)) continue; - await this.connect(); + await this.start(); const historyItemData = await this.txDataToHistory(tx, networkFees, assetDataByAddress); @@ -130,8 +107,7 @@ class SubBridgeHistory { await updateCallback?.(); } } finally { - this.externalNetworkAdapter.stop(); - this.parachainNetworkAdapter.stop(); + this.stop(); } } @@ -153,7 +129,6 @@ class SubBridgeHistory { const asset = assetDataByAddress(tx.soraAssetAddress); const amount = FPNumber.fromCodecValue(tx.amount, asset?.decimals).toString(); const type = getType(isOutgoing); - const soraNetworkFee = networkFees[type] ?? ZeroStringValue; const history: SubHistory = { id, @@ -162,12 +137,11 @@ class SubBridgeHistory { hash: id, transactionState: tx.status, parachainBlockHeight, - externalNetwork: this.externalNetwork, + externalNetwork: this.network, externalNetworkType: BridgeNetworkType.Sub, amount, assetAddress: asset?.address, symbol: asset?.symbol, - soraNetworkFee, from: tx.soraAccount, }; @@ -182,7 +156,7 @@ class SubBridgeHistory { const [{ hash, events }, startTime, relayChainBlockNumber] = await Promise.all([ findTxInBlock(blockId, id), api.system.getBlockTimestamp(blockId, this.soraApi), - getRelayChainBlockNumber(parachainBlockId, this.parachainApi), + subBridgeApi.soraParachainApi.getRelayChainBlockNumber(parachainBlockId, this.parachainApi), ]); history.txId = hash; @@ -223,6 +197,7 @@ class SubBridgeHistory { asset: Nullable; events: any[]; }): Promise { + const soraFeeEvent = events.find((e) => this.soraApi.events.transactionPayment.TransactionFeePaid.is(e.event)); // sended from sora nonces const [soraBatchNonce, soraMessageNonce] = getMessageAcceptedNonces(events, this.soraApi); const parachainEvents = await api.system.getBlockEvents(parachainBlockId, this.parachainApi); @@ -264,11 +239,12 @@ class SubBridgeHistory { ); const received = balancesDepositEvent.event.data.amount.toString(); + history.soraNetworkFee = soraFeeEvent.event.data[1].toString(); history.externalNetworkFee = ZeroStringValue; history.externalBlockId = blockId; history.externalBlockHeight = n; history.amount2 = FPNumber.fromCodecValue(received, asset?.externalDecimals).toString(); - history.to = to; + history.to = formatSubAddress(tx.externalAccount, this.externalApi.registry.chainSS58 as number); break; } catch { continue; @@ -291,7 +267,7 @@ class SubBridgeHistory { const blockId = await api.system.getBlockHash(n, this.externalApi); const extrinsics = await api.system.getExtrinsicsFromBlock(blockId, this.externalApi); - for (const extrinsic of extrinsics) { + for (const [extrinsicIndex, extrinsic] of extrinsics.entries()) { try { if (!(extrinsic.method.section === 'xcmPallet' && extrinsic.method.method === 'reserveTransferAssets')) continue; @@ -300,21 +276,22 @@ class SubBridgeHistory { const parachainId = (dest as any).asV3.interior.asX1.asParachain.toNumber(); const accountId = (beneficiary as any).asV3.interior.asX1.asAccountId32.id.toString(); const receiver = subBridgeApi.formatAddress(accountId); + const from = subBridgeApi.formatAddress(history.from as string); - if (!(parachainId === this.parachainId && receiver === history.from)) continue; + if (!(parachainId === this.parachainId && receiver === from)) continue; - const feeData = await findEventInBlock({ - api: this.externalApi, - blockId, - section: 'transactionPayment', - method: 'TransactionFeePaid', - }); + const signer = extrinsic.signer.toString(); + const extrinsicEvents = await getBlockEventsByTxIndex(blockId, extrinsicIndex, this.externalApi); + const feeEvent = extrinsicEvents.find((e) => + this.externalApi.events.transactionPayment.TransactionFeePaid.is(e.event) + ); - history.externalNetworkFee = feeData[1].toString(); + history.soraNetworkFee = ZeroStringValue; + history.externalNetworkFee = feeEvent.event.data[1].toString(); history.externalBlockId = blockId; history.externalBlockHeight = n; - history.to = subBridgeApi.formatAddress(extrinsic.signer.toString()); - break; + history.to = formatSubAddress(signer, this.externalApi.registry.chainSS58 as number); + return; } catch { continue; } diff --git a/src/utils/bridge/sub/classes/reducers.ts b/src/utils/bridge/sub/classes/reducers.ts index 5c7bc581c..e0bc6b479 100644 --- a/src/utils/bridge/sub/classes/reducers.ts +++ b/src/utils/bridge/sub/classes/reducers.ts @@ -8,37 +8,32 @@ import { ZeroStringValue } from '@/consts'; import { BridgeReducer } from '@/utils/bridge/common/classes'; import { getTransactionEvents } from '@/utils/bridge/common/utils'; import { subBridgeApi } from '@/utils/bridge/sub/api'; -import { subConnector } from '@/utils/bridge/sub/classes/adapter'; -import type { SubAdapter } from '@/utils/bridge/sub/classes/adapter'; +import { SubNetworksConnector } from '@/utils/bridge/sub/classes/adapter'; import { getMessageAcceptedNonces, isMessageDispatchedNonces, isAssetAddedToChannel } from '@/utils/bridge/sub/utils'; -import type { ApiPromise } from '@polkadot/api'; +import type { ApiPromise, ApiRx } from '@polkadot/api'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; import type { SubHistory } from '@sora-substrate/util/build/bridgeProxy/sub/types'; import type { Subscription } from 'rxjs'; export class SubBridgeReducer extends BridgeReducer { - protected subNetworkAdapter!: SubAdapter; - protected soraParachainAdapter!: SubAdapter; - protected asset!: RegisteredAccountAsset; + protected connector!: SubNetworksConnector; - createConnections(id: string): void { + initConnector(id: string): void { const { externalNetwork } = this.getTransaction(id); if (!externalNetwork) throw new Error(`[${this.constructor.name}]: Transaction "externalNetwork" is not defined`); - const soraParachainNetwork = subBridgeApi.getSoraParachain(externalNetwork); - - this.subNetworkAdapter = subConnector.getAdapterForNetwork(externalNetwork); - this.soraParachainAdapter = subConnector.getAdapterForNetwork(soraParachainNetwork); + this.connector = new SubNetworksConnector(); + this.connector.init(externalNetwork); } - async closeConnections(): Promise { - await Promise.all([this.subNetworkAdapter?.stop(), this.soraParachainAdapter?.stop()]); + async closeConnector(): Promise { + await this.connector.stop(); } - async getHashesByBlockNumber(adapter: SubAdapter, blockHeight: number) { + async getHashesByBlockNumber(blockHeight: number, apiRx: ApiRx) { let blockId = ''; if (Number.isFinite(blockHeight)) { @@ -46,7 +41,7 @@ export class SubBridgeReducer extends BridgeReducer { try { await new Promise((resolve) => { - subscription = api.system.getBlockHashObservable(blockHeight, adapter.apiRx).subscribe((hash) => { + subscription = api.system.getBlockHashObservable(blockHeight, apiRx).subscribe((hash) => { if (hash) { blockId = hash; resolve(); @@ -87,7 +82,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { handler: async (id: string) => { try { this.beforeSubmit(id); - this.createConnections(id); + this.initConnector(id); this.updateTransactionParams(id, { transactionState: BridgeTxStatus.Pending }); await this.checkTxId(id); @@ -96,7 +91,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { await this.waitSoraBlockByHash(id); await this.onComplete(id); } finally { - this.closeConnections(); + this.closeConnector(); } }, }); @@ -121,11 +116,11 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { // transaction not signed await this.beforeSign(id); // open connections - await Promise.all([this.subNetworkAdapter.connect(), this.soraParachainAdapter.connect()]); + await this.connector.start(); // sign transaction - await this.subNetworkAdapter.transfer(asset, tx.to as string, tx.amount as string, id); + await this.connector.networkAdapter.transfer(asset, tx.to as string, tx.amount as string, id); // store sora parachain block number when tx was signed in external network - const parachainStartBlock = (await this.soraParachainAdapter.api.query.system.number()).toNumber(); + const parachainStartBlock = (await this.connector.parachainAdapter.api.query.system.number()).toNumber(); // update history to change tx status in ui this.updateTransactionParams(id, { payload: { @@ -148,12 +143,12 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { private async updateTxExternalData(id: string): Promise { const tx = this.getTransaction(id); - await this.subNetworkAdapter.connect(); + await this.connector.networkAdapter.connect(); if (!tx.externalBlockHeight) { const externalBlockHeight = await api.system.getBlockNumber( tx.externalBlockId as string, - this.subNetworkAdapter.api + this.connector.networkAdapter.api ); this.updateTransactionParams(id, { @@ -163,12 +158,14 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { const blockHash = tx.externalBlockId as string; const transactionHash = tx.externalHash as string; - const transactionEvents = await getTransactionEvents(blockHash, transactionHash, this.subNetworkAdapter.api); + const transactionEvents = await getTransactionEvents(blockHash, transactionHash, this.connector.networkAdapter.api); const feeEvent = transactionEvents.find((e) => - this.subNetworkAdapter.api.events.transactionPayment.TransactionFeePaid.is(e.event) + this.connector.networkAdapter.api.events.transactionPayment.TransactionFeePaid.is(e.event) + ); + const xcmEvent = transactionEvents.find((e) => + this.connector.networkAdapter.api.events.xcmPallet.Attempted.is(e.event) ); - const xcmEvent = transactionEvents.find((e) => this.subNetworkAdapter.api.events.xcmPallet.Attempted.is(e.event)); if (feeEvent) { const externalNetworkFee = feeEvent.event.data[1].toString(); @@ -204,11 +201,11 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { let blockNumber!: number; try { - await this.soraParachainAdapter.connect(); + await this.connector.parachainAdapter.connect(); await new Promise((resolve, reject) => { - const eventsObservable = api.system.getEventsObservable(this.soraParachainAdapter.apiRx); - const blockNumberObservable = api.system.getBlockNumberObservable(this.soraParachainAdapter.apiRx); + const eventsObservable = api.system.getEventsObservable(this.connector.parachainAdapter.apiRx); + const blockNumberObservable = api.system.getBlockNumberObservable(this.connector.parachainAdapter.apiRx); subscription = combineLatest([eventsObservable, blockNumberObservable]).subscribe( ([eventsVec, blockHeight]) => { @@ -221,13 +218,13 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { const events = [...eventsVec.toArray()].reverse(); const downwardMessagesProcessedEvent = events.find((e) => - this.soraParachainAdapter.api.events.parachainSystem.DownwardMessagesProcessed.is(e.event) + this.connector.parachainAdapter.api.events.parachainSystem.DownwardMessagesProcessed.is(e.event) ); if (!downwardMessagesProcessedEvent) return; const assetAddedToChannelEventIndex = events.findIndex((e) => - isAssetAddedToChannel(e, this.asset, to, sended, this.soraParachainAdapter.api) + isAssetAddedToChannel(e, this.asset, to, sended, this.connector.parachainAdapter.api) ); if (assetAddedToChannelEventIndex === -1) { @@ -238,7 +235,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { blockNumber = blockHeight; [batchNonce, messageNonce] = getMessageAcceptedNonces( events.slice(assetAddedToChannelEventIndex), - this.soraParachainAdapter.api + this.connector.parachainAdapter.api ); resolve(); @@ -252,14 +249,16 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { subscription.unsubscribe(); // run non blocking process promise - this.getHashesByBlockNumber(this.soraParachainAdapter, blockNumber) + this.getHashesByBlockNumber(blockNumber, this.connector.parachainAdapter.apiRx) .then(({ blockHeight, blockId }) => this.updateTransactionParams(id, { parachainBlockHeight: blockHeight, // parachain block number parachainBlockId: blockId, // parachain block hash }) ) - .finally(() => this.closeConnections()); + .finally(() => { + this.closeConnector(); + }); } const { payload: prevPayload } = this.getTransaction(id); @@ -359,7 +358,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { handler: async (id: string) => { try { this.beforeSubmit(id); - this.createConnections(id); + this.initConnector(id); this.updateTransactionParams(id, { transactionState: BridgeTxStatus.Pending }); await this.checkTxId(id); @@ -370,7 +369,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { await this.waitForDestinationMessageHash(id); await this.onComplete(id); } finally { - this.closeConnections(); + this.closeConnector(); } }, }); @@ -432,11 +431,11 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { let blockNumber!: number; try { - await this.soraParachainAdapter.connect(); + await this.connector.parachainAdapter.connect(); await new Promise((resolve, reject) => { - const eventsObservable = api.system.getEventsObservable(this.soraParachainAdapter.apiRx); - const blockNumberObservable = api.system.getBlockNumberObservable(this.soraParachainAdapter.apiRx); + const eventsObservable = api.system.getEventsObservable(this.connector.parachainAdapter.apiRx); + const blockNumberObservable = api.system.getBlockNumberObservable(this.connector.parachainAdapter.apiRx); subscription = combineLatest([eventsObservable, blockNumberObservable]).subscribe( ([eventsVec, blockHeight]) => { @@ -447,7 +446,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { tx.payload.batchNonce, tx.payload.messageNonce, e, - this.soraParachainAdapter.api + this.connector.parachainAdapter.api ) ); @@ -457,7 +456,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { const parachainSystemEvent = events .slice(substrateDispatchEventIndex) - .find((e) => this.soraParachainAdapter.api.events.parachainSystem.UpwardMessageSent.is(e.event)); + .find((e) => this.connector.parachainAdapter.api.events.parachainSystem.UpwardMessageSent.is(e.event)); if (!parachainSystemEvent) { throw new Error(`[${this.constructor.name}]: Unable to find "parachainSystem.UpwardMessageSent" event`); @@ -476,14 +475,16 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { subscription.unsubscribe(); // run non blocking proccess promise - this.getHashesByBlockNumber(this.soraParachainAdapter, blockNumber) + this.getHashesByBlockNumber(blockNumber, this.connector.parachainAdapter.apiRx) .then(({ blockHeight, blockId }) => this.updateTransactionParams(id, { parachainBlockHeight: blockHeight, // parachain block number parachainBlockId: blockId, // parachain block hash }) ) - .finally(() => this.soraParachainAdapter.stop()); + .finally(() => { + this.connector.parachainAdapter.stop(); + }); } const payload = { ...tx.payload, messageHash }; @@ -502,11 +503,11 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { let amount!: string; try { - await this.subNetworkAdapter.connect(); + await this.connector.networkAdapter.connect(); await new Promise((resolve, reject) => { - const eventsObservable = api.system.getEventsObservable(this.subNetworkAdapter.apiRx); - const blockNumberObservable = api.system.getBlockNumberObservable(this.subNetworkAdapter.apiRx); + const eventsObservable = api.system.getEventsObservable(this.connector.networkAdapter.apiRx); + const blockNumberObservable = api.system.getBlockNumberObservable(this.connector.networkAdapter.apiRx); subscription = combineLatest([eventsObservable, blockNumberObservable]).subscribe( ([eventsVec, blockHeight]) => { @@ -514,7 +515,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { const events = [...eventsVec.toArray()].reverse(); const messageQueueProcessedEventIndex = events.findIndex( (e) => - this.subNetworkAdapter.api.events.messageQueue.Processed.is(e.event) && + this.connector.networkAdapter.api.events.messageQueue.Processed.is(e.event) && e.event.data[0].toString() === messageHash ); @@ -527,8 +528,9 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { .slice(messageQueueProcessedEventIndex) .find( (e) => - this.subNetworkAdapter.api.events.balances.Deposit.is(e.event) && - subBridgeApi.formatAddress(e.event.data.who.toString()) === tx.to + this.connector.networkAdapter.api.events.balances.Deposit.is(e.event) && + subBridgeApi.formatAddress(e.event.data.who.toString()) === + subBridgeApi.formatAddress(tx.to as string) ); if (!balancesDepositEvent) @@ -547,14 +549,16 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { subscription.unsubscribe(); // run blocking process promise - await this.getHashesByBlockNumber(this.subNetworkAdapter, blockNumber) + await this.getHashesByBlockNumber(blockNumber, this.connector.networkAdapter.apiRx) .then(({ blockHeight, blockId }) => this.updateTransactionParams(id, { externalBlockHeight: blockHeight, // parachain block number externalBlockId: blockId, // parachain block hash }) ) - .finally(() => this.subNetworkAdapter.stop()); + .finally(() => { + this.connector.networkAdapter.stop(); + }); } const received = FPNumber.fromCodecValue(amount, this.asset.externalDecimals); diff --git a/src/utils/bridge/sub/utils.ts b/src/utils/bridge/sub/utils.ts index db508d9d9..19ab78010 100644 --- a/src/utils/bridge/sub/utils.ts +++ b/src/utils/bridge/sub/utils.ts @@ -1,3 +1,5 @@ +import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'; + import { subBridgeApi } from '@/utils/bridge/sub/api'; import type { ApiPromise } from '@polkadot/api'; @@ -23,13 +25,6 @@ export const updateTransaction = (id: string, params = {}): void => { subBridgeApi.saveHistory(data); }; -export const getRelayChainBlockNumber = async (blockHash: string, api: ApiPromise): Promise => { - const apiInstanceAtBlock = await api.at(blockHash); - const blockNumber = await apiInstanceAtBlock.query.parachainSystem.lastRelayChainBlockNumber(); - - return Number(blockNumber.toString()); -}; - export const getMessageAcceptedNonces = (events: Array, api: ApiPromise): [number, number] => { const messageAcceptedEvent = events.find((e) => api.events.substrateBridgeOutboundChannel.MessageAccepted.is(e.event) @@ -78,7 +73,7 @@ export const isAssetAddedToChannel = ( const { amount, assetId, recipient } = e.event.data[0].asTransfer; // address check - if (subBridgeApi.formatAddress(recipient.toString()) !== to) return false; + if (subBridgeApi.formatAddress(recipient.toString()) !== subBridgeApi.formatAddress(to)) return false; // asset check if (assetId.toString() !== asset.address) return false; // amount check @@ -87,3 +82,10 @@ export const isAssetAddedToChannel = ( return true; }; + +// [TECH] move to js-lib +export const formatSubAddress = (address: string, ss58: number): string => { + const publicKey = decodeAddress(address, false); + + return encodeAddress(publicKey, ss58); +}; diff --git a/src/utils/ethers-util.ts b/src/utils/ethers-util.ts index 39481f795..23445ffba 100644 --- a/src/utils/ethers-util.ts +++ b/src/utils/ethers-util.ts @@ -1,7 +1,8 @@ import detectEthereumProvider from '@metamask/detect-provider'; import { decodeAddress } from '@polkadot/util-crypto'; -import { FPNumber, BridgeRequestAssetKind } from '@sora-substrate/util'; +import { FPNumber } from '@sora-substrate/util'; import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; +import { EthAssetKind } from '@sora-substrate/util/build/bridgeProxy/eth/consts'; import WalletConnectProvider from '@walletconnect/web3-provider'; import { ethers } from 'ethers'; @@ -44,14 +45,14 @@ const gasLimit = { /** * It's in gwei. */ -const getEthBridgeGasLimit = (assetEvmAddress: string, kind: BridgeRequestAssetKind, isSoraToEvm: boolean) => { +const getEthBridgeGasLimit = (assetEvmAddress: string, kind: EthAssetKind, isSoraToEvm: boolean) => { if (isSoraToEvm) { switch (kind) { - case BridgeRequestAssetKind.SidechainOwned: + case EthAssetKind.SidechainOwned: return gasLimit.mintTokensByPeers; - case BridgeRequestAssetKind.Thischain: + case EthAssetKind.Thischain: return gasLimit.receiveBySidechainAssetId; - case BridgeRequestAssetKind.Sidechain: + case EthAssetKind.Sidechain: return isNativeEvmTokenAddress(assetEvmAddress) ? gasLimit.receiveByEthereumAssetAddress.ETH : gasLimit.receiveByEthereumAssetAddress.OTHER; @@ -309,7 +310,7 @@ async function getEvmNetworkFee( const ethersInstance = await getEthersInstance(); const { maxFeePerGas } = await ethersInstance.getFeeData(); const gasPrice = maxFeePerGas ?? BigInt(0); - const gasLimit = BigInt(getEthBridgeGasLimit(assetEvmAddress, assetKind as BridgeRequestAssetKind, isSoraToEvm)); + const gasLimit = BigInt(getEthBridgeGasLimit(assetEvmAddress, assetKind as EthAssetKind, isSoraToEvm)); const fee = calcEvmFee(gasPrice, gasLimit); return fee; @@ -339,7 +340,7 @@ async function getEvmTransactionReceipt(hash: string): Promise { const ethersInstance = await getEthersInstance(); - const block = await ethersInstance.getBlock(number); + const block = await ethersInstance.getBlock(Number(number)); return block; } diff --git a/src/utils/index.ts b/src/utils/index.ts index ca79c69c1..6ada87f47 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -66,7 +66,7 @@ export const isMaxButtonAvailable = ( return !FPNumber.eq(fpMaxBalance, fpAmount) && !hasInsufficientXorForFee(xorAsset, fee, isXorOutputSwap); }; -const getMaxBalance = ( +export const getMaxBalance = ( asset: AssetWithBalance, fee: CodecString, { isExternalBalance = false, isExternalNative = false, isBondedBalance = false } = {} diff --git a/src/views/Bridge.vue b/src/views/Bridge.vue index c0ffd2d45..b67c7e3bc 100644 --- a/src/views/Bridge.vue +++ b/src/views/Bridge.vue @@ -12,7 +12,7 @@
-
+
-
+
@@ -317,13 +332,13 @@ import { hasInsufficientBalance, hasInsufficientXorForFee, hasInsufficientNativeTokenForFee, - getMaxValue, + getMaxBalance, getAssetBalance, asZeroValue, delay, } from '@/utils'; -import type { IBridgeTransaction } from '@sora-substrate/util'; +import type { IBridgeTransaction, CodecString } from '@sora-substrate/util'; import type { AccountAsset, RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; @Component({ @@ -332,6 +347,7 @@ import type { AccountAsset, RegisteredAccountAsset } from '@sora-substrate/util/ BridgeSelectNetwork: lazyComponent(Components.BridgeSelectNetwork), BridgeSelectAccount: lazyComponent(Components.BridgeSelectAccount), BridgeTransactionDetails: lazyComponent(Components.BridgeTransactionDetails), + BridgeLimitCard: lazyComponent(Components.BridgeLimitCard), GenericPageHeader: lazyComponent(Components.GenericPageHeader), ConfirmBridgeTransactionDialog: lazyComponent(Components.ConfirmBridgeTransactionDialog), NetworkFeeWarningDialog: lazyComponent(Components.NetworkFeeWarningDialog), @@ -356,13 +372,13 @@ export default class Bridge extends Mixins( readonly KnownSymbols = KnownSymbols; readonly FocusedField = FocusedField; - @state.bridge.externalNetworkFeeFetching private externalNetworkFeeFetching!: boolean; - @state.bridge.externalBalancesFetching private externalBalancesFetching!: boolean; - @state.bridge.assetLockedBalanceFetching private assetLockedBalanceFetching!: boolean; + @state.bridge.externalTransferFee private externalTransferFee!: CodecString; + @state.bridge.balancesFetching private balancesFetching!: boolean; + @state.bridge.feesAndLockedFundsFetching private feesAndLockedFundsFetching!: boolean; + @state.assets.registeredAssetsFetching private registeredAssetsFetching!: boolean; @state.bridge.amountSend amountSend!: string; @state.bridge.amountReceived amountReceived!: string; @state.bridge.isSoraToEvm isSoraToEvm!: boolean; - @state.assets.registeredAssetsFetching registeredAssetsFetching!: boolean; @getter.bridge.isRegisteredAsset isRegisteredAsset!: boolean; @getter.bridge.operation private operation!: Operation; @@ -371,6 +387,7 @@ export default class Bridge extends Mixins( @mutation.bridge.setSoraToEvm private setSoraToEvm!: (value: boolean) => void; @mutation.bridge.setHistoryId private setHistoryId!: (id?: string) => void; @mutation.bridge.setFocusedField setFocusedField!: (field: FocusedField) => void; + @mutation.web3.setSelectNetworkDialogVisibility private setSelectNetworkDialogVisibility!: (flag: boolean) => void; @action.bridge.setSendedAmount setSendedAmount!: (value?: string) => void; @action.bridge.setReceivedAmount setReceivedAmount!: (value?: string) => void; @@ -400,7 +417,7 @@ export default class Bridge extends Mixins( return await this.waitOnExternalFeeWarningConfirmation(); } - get areNetworksConnected(): boolean { + get areAccountsConnected(): boolean { return !!this.sender && !!this.recipient; } @@ -442,35 +459,31 @@ export default class Bridge extends Mixins( if (!(this.asset && this.isRegisteredAsset)) return ZeroStringValue; const fee = this.isSoraToEvm ? this.soraNetworkFee : this.externalNetworkFee; - const maxBalance = getMaxValue(this.asset, fee, { + const maxBalance = getMaxBalance(this.asset, fee, { isExternalBalance: !this.isSoraToEvm, isExternalNative: this.isNativeTokenSelected, }); - if (this.assetLockedBalance && this.isSoraToEvm) { - const fpBalance = this.getFPNumber(maxBalance, this.asset.decimals); - const fpLocked = this.getFPNumberFromCodec(this.assetLockedBalance, this.asset.externalDecimals); - - if (FPNumber.gt(fpBalance, fpLocked)) return fpLocked.toString(); + if (this.isSoraToEvm && this.outgoingMaxAmount) { + if (FPNumber.gt(maxBalance, this.outgoingMaxAmount)) return this.outgoingMaxAmount.toString(); } - return maxBalance; + return maxBalance.toString(); } get isMaxAvailable(): boolean { - if (!(this.asset && this.isRegisteredAsset && this.areNetworksConnected && !asZeroValue(this.maxValue))) + if (!(this.asset && this.isRegisteredAsset && this.areAccountsConnected && !asZeroValue(this.maxValue))) return false; return this.maxValue !== this.amountSend; } - get isInsufficientLiquidity(): boolean { - if (!(this.asset && this.assetLockedBalance && this.isSoraToEvm)) return false; - - const fpAmount = new FPNumber(this.amountSend, this.asset.decimals); - const fpLocked = FPNumber.fromCodecValue(this.assetLockedBalance, this.asset.externalDecimals); + get isGreaterThanMaxAmount(): boolean { + return this.isGreaterThanOutgoingMaxAmount(this.amountSend, this.asset, this.isSoraToEvm, this.isRegisteredAsset); + } - return FPNumber.gt(fpAmount, fpLocked); + get isLowerThanMinAmount(): boolean { + return this.isLowerThanIncomingMinAmount(this.amountSend, this.asset, this.isSoraToEvm, this.isRegisteredAsset); } get isInsufficientXorForFee(): boolean { @@ -509,28 +522,32 @@ export default class Bridge extends Mixins( return this.getStringFromCodec(this.externalNetworkFee, this.nativeTokenDecimals); } + get formattedExternalTransferFee(): string { + return this.getStringFromCodec(this.externalTransferFee, this.asset?.externalDecimals); + } + get isConfirmTxDisabled(): boolean { return ( !this.isAssetSelected || !this.isRegisteredAsset || - !this.areNetworksConnected || + !this.areAccountsConnected || !this.isValidNetwork || this.isZeroAmountSend || this.isZeroAmountReceived || this.isInsufficientXorForFee || this.isInsufficientNativeTokenForFee || this.isInsufficientBalance || - this.isInsufficientLiquidity + this.isGreaterThanMaxAmount || + this.isLowerThanMinAmount ); } get isConfirmTxLoading(): boolean { return ( this.isSelectAssetLoading || - this.externalNetworkFeeFetching || - this.externalBalancesFetching || - this.registeredAssetsFetching || - this.assetLockedBalanceFetching + this.balancesFetching || + this.feesAndLockedFundsFetching || + this.registeredAssetsFetching ); } @@ -564,8 +581,11 @@ export default class Bridge extends Mixins( return FPNumber.gte(fpAfterTransfer, fpFee); } - getDecimals(isSora = true): number | undefined { - return isSora ? this.asset?.decimals : this.asset?.externalDecimals; + get amountDecimals(): number { + const internal = this.asset?.decimals ?? FPNumber.DEFAULT_PRECISION; + const external = this.asset?.externalDecimals ?? FPNumber.DEFAULT_PRECISION; + + return Math.min(internal, external); } private getBalance(isSora = true): Nullable { @@ -576,7 +596,7 @@ export default class Bridge extends Mixins( if (!balance) { return null; } - const decimals = this.getDecimals(isSora); + const decimals = isSora ? this.asset?.decimals : this.asset?.externalDecimals; return this.getFPNumberFromCodec(balance, decimals); } @@ -585,14 +605,16 @@ export default class Bridge extends Mixins( } async created(): Promise { - const { address, amount, isIncoming } = this.$route.params; - if (address) { - this.setAssetAddress(address); - } - if (isIncoming) { - this.setSoraToEvm(false); - } - this.setSendedAmount(amount); + await this.withParentLoading(async () => { + const { address, amount, isIncoming } = this.$route.params; + this.setSendedAmount(amount); + if (isIncoming) { + this.setSoraToEvm(false); + } + if (address) { + this.updateAssetAddress(address); + } + }); } getBridgeItemTitle(isSoraNetwork = false): string { @@ -634,10 +656,6 @@ export default class Bridge extends Mixins( this.showConfirmTransactionDialog = true; } - handleViewTransactionsHistory(): void { - router.push({ name: PageNames.BridgeTransactionsHistory }); - } - handleChangeNetwork(): void { this.setSelectNetworkDialogVisibility(true); } @@ -649,8 +667,12 @@ export default class Bridge extends Mixins( async selectAsset(selectedAsset?: RegisteredAccountAsset): Promise { if (!selectedAsset) return; + await this.updateAssetAddress(selectedAsset.address); + } + + private async updateAssetAddress(address: string): Promise { await this.withSelectAssetLoading(async () => { - await this.setAssetAddress(selectedAsset.address); + await this.setAssetAddress(address); }); } @@ -773,5 +795,9 @@ $bridge-input-color: var(--s-color-base-content-tertiary); line-height: var(--s-line-height-big); color: var(--s-color-base-content-secondary); } + + &-limit-card { + margin-top: $inner-spacing-medium; + } } diff --git a/src/views/BridgeContainer.vue b/src/views/BridgeContainer.vue index 65065713f..6b86e12ec 100644 --- a/src/views/BridgeContainer.vue +++ b/src/views/BridgeContainer.vue @@ -1,7 +1,7 @@