diff --git a/src/abi/ethereum/internal/MASTER.json b/src/abi/ethereum/internal/MASTER.json index ed2b5af6c..3a79fc419 100644 --- a/src/abi/ethereum/internal/MASTER.json +++ b/src/abi/ethereum/internal/MASTER.json @@ -1,54 +1,52 @@ -{ - "abi": [ - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "tokenAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "beneficiary", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "txHash", - "type": "bytes32" - }, - { - "internalType": "uint8[]", - "name": "v", - "type": "uint8[]" - }, - { - "internalType": "bytes32[]", - "name": "r", - "type": "bytes32[]" - }, - { - "internalType": "bytes32[]", - "name": "s", - "type": "bytes32[]" - }, - { - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "mintTokensByPeers", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } - ] -} +[ + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "internalType": "uint8[]", + "name": "v", + "type": "uint8[]" + }, + { + "internalType": "bytes32[]", + "name": "r", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "s", + "type": "bytes32[]" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + } + ], + "name": "mintTokensByPeers", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/ethereum/other/BRIDGE.json b/src/abi/ethereum/other/BRIDGE.json index 9b7d6a609..94de998c5 100644 --- a/src/abi/ethereum/other/BRIDGE.json +++ b/src/abi/ethereum/other/BRIDGE.json @@ -1,155 +1,153 @@ -{ - "abi": [ - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "_sidechainTokens", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "tokenAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address payable", - "name": "to", - "type": "address" - }, - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "txHash", - "type": "bytes32" - }, - { - "internalType": "uint8[]", - "name": "v", - "type": "uint8[]" - }, - { - "internalType": "bytes32[]", - "name": "r", - "type": "bytes32[]" - }, - { - "internalType": "bytes32[]", - "name": "s", - "type": "bytes32[]" - } - ], - "name": "receiveByEthereumAssetAddress", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "sidechainAssetId", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "txHash", - "type": "bytes32" - }, - { - "internalType": "uint8[]", - "name": "v", - "type": "uint8[]" - }, - { - "internalType": "bytes32[]", - "name": "r", - "type": "bytes32[]" - }, - { - "internalType": "bytes32[]", - "name": "s", - "type": "bytes32[]" - } - ], - "name": "receiveBySidechainAssetId", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "to", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "tokenAddress", - "type": "address" - } - ], - "name": "sendERC20ToSidechain", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "to", - "type": "bytes32" - } - ], - "name": "sendEthToSidechain", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } - ] -} +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "_sidechainTokens", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "to", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "internalType": "uint8[]", + "name": "v", + "type": "uint8[]" + }, + { + "internalType": "bytes32[]", + "name": "r", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "s", + "type": "bytes32[]" + } + ], + "name": "receiveByEthereumAssetAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "sidechainAssetId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "internalType": "uint8[]", + "name": "v", + "type": "uint8[]" + }, + { + "internalType": "bytes32[]", + "name": "r", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "s", + "type": "bytes32[]" + } + ], + "name": "receiveBySidechainAssetId", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "to", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "sendERC20ToSidechain", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "to", + "type": "bytes32" + } + ], + "name": "sendEthToSidechain", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/abi/ethereum/other/ERC20.json b/src/abi/ethereum/other/ERC20.json index 528b15672..0623a6669 100644 --- a/src/abi/ethereum/other/ERC20.json +++ b/src/abi/ethereum/other/ERC20.json @@ -1,130 +1,128 @@ -{ - "abi": [ - { - "inputs": [ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", "name": "", - "type": "uint256" + "type": "uint8" } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { - "internalType": "address", - "name": "spender", + "name": "to", "type": "address" }, { - "internalType": "uint256", "name": "value", "type": "uint256" } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ + ], + "name": "transfer", + "outputs": [ { - "internalType": "bool", "name": "", "type": "bool" } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "name": "", - "type": "uint8" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "to", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } - ] -} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/ethereum/other/moonbeam/xTokens.json b/src/abi/ethereum/other/moonbeam/xTokens.json new file mode 100644 index 000000000..34ee16fa2 --- /dev/null +++ b/src/abi/ethereum/other/moonbeam/xTokens.json @@ -0,0 +1,94 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "currency_address", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "parents", + "type": "uint8" + }, + { + "internalType": "bytes[]", + "name": "interior", + "type": "bytes[]" + } + ], + "internalType": "structXtokens.Multilocation", + "name": "destination", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "weight", + "type": "uint64" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "parents", + "type": "uint8" + }, + { + "internalType": "bytes[]", + "name": "interior", + "type": "bytes[]" + } + ], + "internalType": "structXtokens.Multilocation", + "name": "asset", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "parents", + "type": "uint8" + }, + { + "internalType": "bytes[]", + "name": "interior", + "type": "bytes[]" + } + ], + "internalType": "structXtokens.Multilocation", + "name": "destination", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "weight", + "type": "uint64" + } + ], + "name": "transfer_multiasset", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/assets/img/networks/alphanet.svg b/src/assets/img/networks/alphanet.svg new file mode 100644 index 000000000..1714c1059 --- /dev/null +++ b/src/assets/img/networks/alphanet.svg @@ -0,0 +1,136 @@ + + diff --git a/src/assets/img/networks/moonbeam.svg b/src/assets/img/networks/moonbeam.svg new file mode 100644 index 000000000..bfc782321 --- /dev/null +++ b/src/assets/img/networks/moonbeam.svg @@ -0,0 +1,48 @@ + + + + + + + diff --git a/src/components/mixins/InternalConnectMixin.ts b/src/components/mixins/InternalConnectMixin.ts index 15b61d701..cb5356ca3 100644 --- a/src/components/mixins/InternalConnectMixin.ts +++ b/src/components/mixins/InternalConnectMixin.ts @@ -3,11 +3,13 @@ import { Component, Mixins } from 'vue-property-decorator'; import TranslationMixin from '@/components/mixins/TranslationMixin'; import { PageNames } from '@/consts'; import { goTo } from '@/router'; -import { getter, mutation, state } from '@/store/decorators'; +import { action, getter, mutation, state } from '@/store/decorators'; import { formatAddress } from '@/utils'; @Component export default class InternalConnectMixin extends Mixins(TranslationMixin) { + @action.wallet.account.logout public logout!: () => Promise; + @state.wallet.account.address public soraAddress!: string; @getter.wallet.account.isLoggedIn public isLoggedIn!: boolean; @@ -20,6 +22,10 @@ export default class InternalConnectMixin extends Mixins(TranslationMixin) { this.setSoraAccountDialogVisibility(true); } + public disconnectSoraWallet(): void { + this.logout(); + } + public navigateToWallet(): void { goTo(PageNames.Wallet); } diff --git a/src/components/mixins/NetworkFormatterMixin.ts b/src/components/mixins/NetworkFormatterMixin.ts index 3d43475bd..60989601f 100644 --- a/src/components/mixins/NetworkFormatterMixin.ts +++ b/src/components/mixins/NetworkFormatterMixin.ts @@ -145,6 +145,8 @@ export default class NetworkFormatterMixin extends Mixins(TranslationMixin) { return 'acala'; case SubNetworkId.PolkadotAstar: return 'astar'; + case SubNetworkId.PolkadotMoonbeam: + return 'moonbeam'; case SubNetworkId.Kusama: return 'kusama'; case SubNetworkId.KusamaShiden: @@ -157,6 +159,10 @@ export default class NetworkFormatterMixin extends Mixins(TranslationMixin) { return 'sora-kusama'; case SubNetworkId.Liberland: return 'liberland'; + case SubNetworkId.Alphanet: + return 'alphanet'; + case SubNetworkId.AlphanetSora: + return 'sora-alphanet'; case SubNetworkId.AlphanetMoonbase: return 'moonbase'; default: diff --git a/src/components/mixins/WalletConnectMixin.ts b/src/components/mixins/WalletConnectMixin.ts index 83e87a60a..9d627a959 100644 --- a/src/components/mixins/WalletConnectMixin.ts +++ b/src/components/mixins/WalletConnectMixin.ts @@ -17,15 +17,18 @@ export default class WalletConnectMixin extends Mixins(InternalConnectMixin) { @state.web3.networkType networkType!: BridgeNetworkType; @getter.bridge.isSubBridge isSubBridge!: boolean; + @getter.bridge.isSubAccountType isSubAccountType!: boolean; @mutation.web3.setSubAccountDialogVisibility setSubAccountDialogVisibility!: (flag: boolean) => void; @mutation.web3.setSelectProviderDialogVisibility setSelectProviderDialogVisibility!: (flag: boolean) => void; @action.web3.changeEvmNetworkProvided changeEvmNetworkProvided!: AsyncFnWithoutArgs; @action.web3.selectEvmProvider selectEvmProvider!: (provider: Provider) => Promise; - @action.web3.resetEvmProviderConnection resetEvmProviderConnection!: FnWithoutArgs; @action.web3.disconnectExternalNetwork disconnectExternalNetwork!: AsyncFnWithoutArgs; + @action.web3.resetEvmProviderConnection disconnectEvmWallet!: FnWithoutArgs; + @action.web3.resetSubAccount disconnectSubWallet!: FnWithoutArgs; + connectSubWallet(): void { this.setSubAccountDialogVisibility(true); } diff --git a/src/components/pages/Bridge/AccountPanel.vue b/src/components/pages/Bridge/AccountPanel.vue index 937b192d2..8dcce635a 100644 --- a/src/components/pages/Bridge/AccountPanel.vue +++ b/src/components/pages/Bridge/AccountPanel.vue @@ -1,13 +1,37 @@ diff --git a/src/components/pages/Bridge/TransferNotification.vue b/src/components/pages/Bridge/TransferNotification.vue index 8b93f1c66..eb9c16a24 100644 --- a/src/components/pages/Bridge/TransferNotification.vue +++ b/src/components/pages/Bridge/TransferNotification.vue @@ -26,10 +26,13 @@ import { Component, Mixins } from 'vue-property-decorator'; import BridgeTransactionMixin from '@/components/mixins/BridgeTransactionMixin'; import { getter, state, mutation } from '@/store/decorators'; import { isOutgoingTransaction } from '@/utils/bridge/common/utils'; +import { subBridgeApi } from '@/utils/bridge/sub/api'; +import type { SubNetworksConnector } from '@/utils/bridge/sub/classes/adapter'; import ethersUtil from '@/utils/ethers-util'; import type { IBridgeTransaction } from '@sora-substrate/util'; import type { Whitelist, RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; +import type { SubNetwork } from '@sora-substrate/util/build/bridgeProxy/sub/types'; import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; @Component({ @@ -42,6 +45,7 @@ import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/typ }) export default class BridgeTransferNotification extends Mixins(BridgeTransactionMixin) { @state.bridge.notificationData private notificationData!: Nullable; + @state.bridge.subBridgeConnector private subBridgeConnector!: SubNetworksConnector; @getter.wallet.account.whitelist private whitelist!: Whitelist; @getter.assets.assetDataByAddress private getAsset!: (addr?: string) => Nullable; @@ -73,13 +77,22 @@ export default class BridgeTransferNotification extends Mixins(BridgeTransaction return this.asset?.symbol ?? ''; } - get addTokenBtnVisibility(): boolean { + get isSubEvm(): boolean { + return subBridgeApi.isEvmAccount(this.tx?.externalNetwork as SubNetwork); + } + + get isEvmNetwork(): boolean { if (!this.tx?.externalNetworkType) return false; - if (this.tx.externalNetworkType === BridgeNetworkType.Sub) return false; + if (this.tx.externalNetworkType === BridgeNetworkType.Sub && !this.isSubEvm) return false; + return true; + } + + get addTokenBtnVisibility(): boolean { + if (!this.isEvmNetwork) return false; + + const address = this.asset?.externalAddress; - return ( - !!this.asset && !ethersUtil.isNativeEvmTokenAddress(this.asset.externalAddress) && isOutgoingTransaction(this.tx) - ); + return !!address && !ethersUtil.isNativeEvmTokenAddress(address) && isOutgoingTransaction(this.tx); } get txLink() { @@ -118,11 +131,30 @@ export default class BridgeTransferNotification extends Mixins(BridgeTransaction async addToken(): Promise { if (!this.asset) return; - const { externalAddress, externalDecimals, symbol, address } = this.asset; - const image = this.whitelist[address]?.icon; - await ethersUtil.addToken(externalAddress, symbol, +externalDecimals, image); - - this.visibility = false; + try { + const { externalAddress, externalDecimals, symbol, address } = this.asset; + const image = this.whitelist[address]?.icon; + + let tokenAddress = externalAddress; + let tokenSymbol = symbol; + let tokenDecimals = +externalDecimals; + + if (this.isSubEvm) { + const adapter = this.subBridgeConnector.parachain; + if (!adapter) throw new Error('Adapter not found'); + const assetMeta = adapter.getAssetMeta(this.asset); + if (!assetMeta) throw new Error('Asset metadata not found'); + tokenAddress = adapter.assetIdToEvmContractAddress(externalAddress); + tokenSymbol = assetMeta.symbol; + tokenDecimals = assetMeta.decimals; + } + + await ethersUtil.addToken(tokenAddress, tokenSymbol, tokenDecimals, image); + } catch (error) { + console.error(error); + } finally { + this.visibility = false; + } } } diff --git a/src/components/shared/Dialog/SelectSoraAccount.vue b/src/components/shared/Dialog/SelectSoraAccount.vue index 095e52f18..bea8dffd7 100644 --- a/src/components/shared/Dialog/SelectSoraAccount.vue +++ b/src/components/shared/Dialog/SelectSoraAccount.vue @@ -33,7 +33,7 @@ export default class SelectSoraAccountDialog extends Mixins(TranslationMixin) { @getter.wallet.account.account public soraAccount!: Nullable; @action.wallet.account.loginAccount public loginAccount!: (account: WALLET_TYPES.PolkadotJsAccount) => Promise; - @action.wallet.account.logout public logout!: (forgetAddress?: string) => Promise; + @action.wallet.account.logout public logout!: () => Promise; @action.wallet.account.renameAccount public rename!: (data: { address: string; name: string }) => Promise; get chainApi() { diff --git a/src/consts/sub.ts b/src/consts/sub.ts index d2d05bd7f..8f7591fa3 100644 --- a/src/consts/sub.ts +++ b/src/consts/sub.ts @@ -98,6 +98,31 @@ export const SUB_NETWORKS: Partial> = { }, ], }, + [SubNetworkId.PolkadotMoonbeam]: { + id: SubNetworkId.PolkadotMoonbeam, + name: 'Moonbeam', + nativeCurrency: { + name: 'GLMR', + symbol: 'GLMR', + decimals: 18, + }, + endpointUrls: ['https://rpc.api.moonbeam.network', 'https://moonbeam-rpc.dwellir.com'], + blockExplorerUrls: ['https://moonbeam.subscan.io'], + shortName: 'Moonbeam', + nodes: [ + { + chain: 'Moonbeam', + name: 'Moonbeam Foundation', + address: 'wss://wss.api.moonbeam.network', + }, + { + chain: 'Moonbeam', + name: 'Dwellir', + address: 'wss://moonbeam-rpc.dwellir.com', + }, + ], + evmId: 1284, + }, [SubNetworkId.Rococo]: { id: SubNetworkId.Rococo, name: 'Rococo', @@ -130,7 +155,7 @@ export const SUB_NETWORKS: Partial> = { { chain: 'Moonbase Relay Testnet', name: 'Parity', - address: 'wss://frag-moonbase-relay-rpc-ws.g.moonbase.moonbeam.network', + address: 'wss://fro-moon-rpc-1-moonbase-relay-rpc-1.moonbase.ol-infra.network', }, ], }, @@ -142,8 +167,9 @@ export const SUB_NETWORKS: Partial> = { symbol: 'GLMR', // "DEV" decimals: 18, }, - blockExplorerUrls: [], - shortName: 'Alpha', + endpointUrls: ['https://rpc.api.moonbase.moonbeam.network', 'https://moonbase-rpc.dwellir.com'], + blockExplorerUrls: ['https://moonbase.subscan.io'], + shortName: 'Moonbase', nodes: [ { chain: 'Moonbase Alpha', @@ -151,6 +177,7 @@ export const SUB_NETWORKS: Partial> = { address: 'wss://wss.api.moonbase.moonbeam.network', }, ], + evmId: 1287, }, // SORA Parachains [SubNetworkId.RococoSora]: { @@ -300,10 +327,24 @@ export const SUB_TRANSFER_FEES: SubNetworksFees = { [BridgeTxDirection.Incoming]: '0', }, }, + [SubNetworkId.Alphanet]: { + ALPHA: { + [BridgeTxDirection.Outgoing]: '2700000000', + [BridgeTxDirection.Incoming]: '0', + }, + }, [SubNetworkId.AlphanetMoonbase]: { - ACA: { + GLMR: { [BridgeTxDirection.Outgoing]: '34313700000000', [BridgeTxDirection.Incoming]: '0', }, + ALPHA: { + [BridgeTxDirection.Outgoing]: '44415350668', + [BridgeTxDirection.Incoming]: '46453162841', + }, + XOR: { + [BridgeTxDirection.Outgoing]: '8140448382622083802', + [BridgeTxDirection.Incoming]: '0', + }, }, }; diff --git a/src/store/assets/actions.ts b/src/store/assets/actions.ts index c0c378edd..b0f6f2856 100644 --- a/src/store/assets/actions.ts +++ b/src/store/assets/actions.ts @@ -91,31 +91,6 @@ async function getSubRegisteredAssets( if (!subNetwork) return []; const subNetworkId = subNetwork as SubNetwork; - - // [TODO] remove when non native parachain tokens are supported - if (subNetworkId === SubNetworkId.PolkadotAcala) { - return [ - { - '0x001ddbe1a880031da72f7ea421260bec635fa7d1aa72593d5412795408b6b2ba': { - address: '', - decimals: 12, - kind: 'Sidechain', - }, - }, - ]; - } - if (subNetworkId === SubNetworkId.PolkadotAstar) { - return [ - { - '0x009dd037fcb32f4fe17c513abd4641a2ece844d106e30788124f0c0acc6e748e': { - address: '', - decimals: 18, - kind: 'Sidechain', - }, - }, - ]; - } - const networkAssets = await subBridgeApi.getRegisteredAssets(subNetworkId); const registeredAssets = Object.entries(networkAssets).map(([soraAddress, assetData]) => { return { @@ -130,6 +105,36 @@ async function getSubRegisteredAssets( return registeredAssets; } +async function updateSubAssetsData(context: ActionContext): Promise { + const { state, commit, rootState, rootGetters } = assetsActionContext(context); + const { registeredAssets } = state; + + const { destinationNetwork, soraParachain, parachain } = rootState.bridge.subBridgeConnector; + + if (!subBridgeApi.isParachain(destinationNetwork)) return; + + if (!(soraParachain && parachain)) return; + + await Promise.all([soraParachain.connect(), parachain.connect()]); + + const updatedEntries = await Promise.all( + Object.entries(registeredAssets).map(async ([soraAddress, assetData]) => { + const asset = { ...assetData }; + const soraAsset = rootGetters.wallet.account.assetsDataTable[soraAddress]; + if (!asset.address && soraAsset) { + const multilocation = await subBridgeApi.soraParachainApi.getAssetMulilocation(soraAddress, soraParachain.api); + const id = await parachain.getAssetIdByMultilocation(soraAsset, multilocation); + asset.address = id; + } + return [soraAddress, asset]; + }) + ); + + const assets = Object.fromEntries(updatedEntries); + + commit.setRegisteredAssets(assets); +} + async function getRegisteredAssets(context: ActionContext): Promise[]> { const { rootState } = assetsActionContext(context); @@ -173,14 +178,16 @@ const actions = defineActions({ async updateRegisteredAssets(context): Promise { const { commit, rootState } = assetsActionContext(context); - // only for ETH bridge, because of sora assets broken registration - if (rootState.web3.networkType === BridgeNetworkType.Eth) { - commit.setRegisteredAssetsFetching(true); - await updateEthAssetsData(context); + commit.setRegisteredAssetsFetching(true); - commit.setRegisteredAssetsFetching(false); + if (rootState.web3.networkType === BridgeNetworkType.Sub) { + await updateSubAssetsData(context); + } else { + await updateEthAssetsData(context); } + + commit.setRegisteredAssetsFetching(false); }, }); diff --git a/src/store/bridge/actions.ts b/src/store/bridge/actions.ts index 67d517d76..bec895a54 100644 --- a/src/store/bridge/actions.ts +++ b/src/store/bridge/actions.ts @@ -84,6 +84,26 @@ function getBridgeApi(context: ActionContext) { return ethBridgeApi; } +async function switchAmounts(context: ActionContext): Promise { + const { state, dispatch } = bridgeActionContext(context); + + if (state.focusedField === FocusedField.Received) { + await dispatch.setSendedAmount(state.amountReceived); + } else { + await dispatch.setReceivedAmount(state.amountSend); + } +} + +async function updateAmounts(context: ActionContext): Promise { + const { state, dispatch } = bridgeActionContext(context); + + if (state.focusedField === FocusedField.Received) { + await dispatch.setReceivedAmount(state.amountReceived); + } else { + await dispatch.setSendedAmount(state.amountSend); + } +} + function checkEvmNetwork(context: ActionContext): void { const { rootGetters } = bridgeActionContext(context); if (!rootGetters.web3.isValidNetwork) { @@ -394,7 +414,7 @@ async function updateSoraNetworkFee(context: ActionContext): Promise): Promise { +async function updateBalancesFeesAndAmounts(context: ActionContext): Promise { const { dispatch } = bridgeActionContext(context); await Promise.allSettled([ @@ -448,19 +468,14 @@ const actions = defineActions({ }, async switchDirection(context): Promise { - const { commit, dispatch, state } = bridgeActionContext(context); + const { commit, state } = bridgeActionContext(context); commit.setSoraToEvm(!state.isSoraToEvm); commit.setAssetSenderBalance(); commit.setAssetRecipientBalance(); - await updateBalancesAndFees(context); - - if (state.focusedField === FocusedField.Received) { - await dispatch.setSendedAmount(state.amountReceived); - } else { - await dispatch.setReceivedAmount(state.amountSend); - } + await updateBalancesFeesAndAmounts(context); + await switchAmounts(context); }, async setAssetAddress(context, address?: string): Promise { @@ -474,8 +489,9 @@ const actions = defineActions({ dispatch.updateOutgoingMinLimit(), dispatch.updateOutgoingMaxLimit(), dispatch.updateIncomingMinLimit(), - updateBalancesAndFees(context), + updateBalancesFeesAndAmounts(context), ]); + await updateAmounts(context); }, async updateExternalBalance(context): Promise { @@ -567,7 +583,7 @@ const actions = defineActions({ const subscription = api.system.updated.subscribe(() => { updateExternalBlockNumber(context); - updateBalancesAndFees(context); + updateBalancesFeesAndAmounts(context); }); commit.setBlockUpdatesSubscription(subscription); diff --git a/src/store/bridge/getters.ts b/src/store/bridge/getters.ts index 3d8a24dbd..2da5b4384 100644 --- a/src/store/bridge/getters.ts +++ b/src/store/bridge/getters.ts @@ -110,11 +110,22 @@ const getters = defineGetters()({ return assetIds[0]; }, + isSubAccountType(...args): boolean { + const { rootState } = bridgeGetterContext(args); + const { networkSelected, networkType } = rootState.web3; + + if (networkType === BridgeNetworkType.Sub) { + return !subBridgeApi.isEvmAccount(networkSelected as SubNetwork); + } + + return false; + }, + externalAccount(...args): string { const { getters, rootState } = bridgeGetterContext(args); const { evmAddress, subAddress } = rootState.web3; - if (getters.isSubBridge) { + if (getters.isSubAccountType) { return subAddress; } else { return evmAddress; @@ -128,7 +139,7 @@ const getters = defineGetters()({ if (state.isSoraToEvm) return soraAddress; - return getters.isSubBridge ? chainAddress(subAddress, state.subBridgeConnector) : evmAddress; + return getters.isSubAccountType ? chainAddress(subAddress, state.subBridgeConnector) : evmAddress; }, senderName(...args): string { @@ -138,7 +149,7 @@ const getters = defineGetters()({ if (state.isSoraToEvm) return soraName; - return getters.isSubBridge ? subAddressName : ''; + return getters.isSubAccountType ? subAddressName : ''; }, recipient(...args): string { @@ -148,7 +159,7 @@ const getters = defineGetters()({ if (!state.isSoraToEvm) return soraAddress; - return getters.isSubBridge ? chainAddress(subAddress, state.subBridgeConnector) : evmAddress; + return getters.isSubAccountType ? chainAddress(subAddress, state.subBridgeConnector) : evmAddress; }, recipientName(...args): string { @@ -158,7 +169,7 @@ const getters = defineGetters()({ if (!state.isSoraToEvm) return soraName; - return getters.isSubBridge ? subAddressName : ''; + return getters.isSubAccountType ? subAddressName : ''; }, isEthBridge(...args): boolean { diff --git a/src/store/moonpay/actions.ts b/src/store/moonpay/actions.ts index b23cd4384..eded9490d 100644 --- a/src/store/moonpay/actions.ts +++ b/src/store/moonpay/actions.ts @@ -84,7 +84,7 @@ const actions = defineActions({ }; } else { // Parse ERC-20 transfer - const abi = SmartContracts[SmartContractType.ERC20].abi; + const abi = SmartContracts[SmartContractType.ERC20]; const inter = new ethers.Interface(abi); const decodedInput = inter.parseTransaction({ data: tx.data }); diff --git a/src/store/web3/actions.ts b/src/store/web3/actions.ts index 77ba5f684..9fa2abe67 100644 --- a/src/store/web3/actions.ts +++ b/src/store/web3/actions.ts @@ -1,9 +1,8 @@ import { BridgeNetworkType } from '@sora-substrate/util/build/bridgeProxy/consts'; import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; import { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/types'; -import { accountUtils, WALLET_TYPES, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; +import { api as soraApi, accountUtils, WALLET_TYPES, WALLET_CONSTS } from '@soramitsu/soraneo-wallet-web'; import { defineActions } from 'direct-vuex'; -import { ethers } from 'ethers'; import { KnownEthBridgeAsset, SmartContracts, SmartContractType } from '@/consts/evm'; import { web3ActionContext } from '@/store/web3'; @@ -173,21 +172,18 @@ const actions = defineActions({ }, async changeEvmNetworkProvided(context): Promise { - const { getters, state } = web3ActionContext(context); + const { getters } = web3ActionContext(context); const { selectedNetwork } = getters; - const { networkType } = state; - if (selectedNetwork && networkType !== BridgeNetworkType.Sub) { - await ethersUtil.switchOrAddChain(selectedNetwork); - } + if (!selectedNetwork) return; + + await ethersUtil.switchOrAddChain(selectedNetwork); }, async getSupportedApps(context): Promise { const { commit, getters } = web3ActionContext(context); - // [TODO] uncomment - // const supportedApps = await api.bridgeProxy.getListApps(); - // [TODO] remove this production mock after nodes update - const supportedApps = { + // production mock + let supportedApps = { [BridgeNetworkType.Eth]: {}, [BridgeNetworkType.Evm]: {}, [BridgeNetworkType.Sub]: [ @@ -201,6 +197,12 @@ const actions = defineActions({ ], }; + try { + supportedApps = await soraApi.bridgeProxy.getListApps(); + } catch (error) { + console.error(error); + } + commit.setSupportedApps(supportedApps as any); const networks = getters.availableNetworks[BridgeNetworkType.Sub]; @@ -247,13 +249,12 @@ const actions = defineActions({ if (!soraAssetId) { return ''; } - const contractAbi = SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other].abi; + const contractAbi = SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other]; const contractAddress = getters.contractAddress(KnownEthBridgeAsset.Other); if (!contractAddress || !contractAbi) { throw new Error('Contract address/abi is not found'); } - const signer = await ethersUtil.getSigner(); - const contractInstance = new ethers.Contract(contractAddress, contractAbi, signer); + const contractInstance = await ethersUtil.getContract(contractAddress, contractAbi); const methodArgs = [soraAssetId]; const externalAddress = await contractInstance._sidechainTokens(...methodArgs); // Not (wrong) registered Sora asset on bridge contract return '0' address (like native token) diff --git a/src/store/web3/getters.ts b/src/store/web3/getters.ts index 7e3c675c8..10533d3db 100644 --- a/src/store/web3/getters.ts +++ b/src/store/web3/getters.ts @@ -14,7 +14,7 @@ import type { BridgeNetworkId } from '@sora-substrate/util/build/bridgeProxy/typ const getters = defineGetters()({ availableNetworks(...args): Record>> { - const { state, rootState } = web3GetterContext(args); + const { state } = web3GetterContext(args); const hashi = [state.ethBridgeEvmNetwork].reduce((buffer, id) => { const data = EVM_NETWORKS[id]; @@ -79,11 +79,21 @@ const getters = defineGetters()({ }, isValidNetwork(...args): boolean { - const { state } = web3GetterContext(args); + const { state, getters } = web3GetterContext(args); + const { evmProviderNetwork } = state; + const { selectedNetwork } = getters; - if (state.networkType === BridgeNetworkType.Sub) return true; + if (!selectedNetwork) return false; + + if (state.networkType === BridgeNetworkType.Sub) { + if (selectedNetwork.evmId) { + return evmProviderNetwork === selectedNetwork.evmId; + } else { + return true; + } + } - return state.evmProviderNetwork === state.networkSelected; + return evmProviderNetwork === selectedNetwork.id; }, contractAddress(...args): (asset: KnownEthBridgeAsset) => Nullable { diff --git a/src/styles/common.scss b/src/styles/common.scss index 396a81201..380a49e41 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -22,7 +22,7 @@ $country-emoji-font: 'Twemoji Country Flags'; } } -$networks: 'sora', 'ethereum', 'ethereum-classic', 'avalanche', 'klaytn', 'polygon', 'binance-smart-chain', 'rococo', 'polkadot', 'kusama', 'acala', 'astar', 'shiden', 'liberland', 'moonbase', 'sora-polkadot', 'sora-kusama'; +$networks: 'sora', 'ethereum', 'ethereum-classic', 'avalanche', 'klaytn', 'polygon', 'binance-smart-chain', 'rococo', 'polkadot', 'kusama', 'acala', 'astar', 'shiden', 'liberland', 'alphanet', 'moonbase', 'moonbeam', 'sora-polkadot', 'sora-kusama'; /* Networks Icons */ .network-icon { display: inline-block; @@ -41,15 +41,17 @@ $networks: 'sora', 'ethereum', 'ethereum-classic', 'avalanche', 'klaytn', 'polyg } } - &--sora-rococo { - background-image: url('~@/assets/img/networks/rococo.svg'); - &::after { - content: ''; - display: block; - width: 50%; - height: 50%; - filter: drop-shadow(2px 4px 6px black); - background-image: url('~@/assets/img/networks/sora.svg'); + @each $network in 'rococo', 'alphanet' { + &--sora-#{$network} { + background-image: url('~@/assets/img/networks/#{$network}.svg'); + &::after { + content: ''; + display: block; + width: 50%; + height: 50%; + filter: drop-shadow(2px 4px 6px black); + background-image: url('~@/assets/img/networks/sora.svg'); + } } } } diff --git a/src/types/bridge.ts b/src/types/bridge.ts index 1227f5467..f0e306b5d 100644 --- a/src/types/bridge.ts +++ b/src/types/bridge.ts @@ -19,6 +19,8 @@ export interface NetworkData { endpointUrls?: string[]; /** Nodes for Substrate network */ nodes?: Node[]; + /** Evm chain id for substrate network */ + evmId?: number; } export type SubNetworksFees = Partial>>>; diff --git a/src/utils/bridge/common/classes.ts b/src/utils/bridge/common/classes.ts index 12d6a6f7f..9ebef9dd5 100644 --- a/src/utils/bridge/common/classes.ts +++ b/src/utils/bridge/common/classes.ts @@ -18,6 +18,7 @@ import type { IBridgeConstructorOptions, TransactionHandlerPayload, } from '@/utils/bridge/common/types'; +import { isUnsignedTx } from '@/utils/bridge/common/utils'; import type { IBridgeTransaction } from '@sora-substrate/util'; @@ -180,25 +181,27 @@ export class BridgeReducer implements IB } private async checkTransactionBlockId(id: string): Promise { - const { blockId } = this.getTransaction(id); + const { blockId, externalBlockId } = this.getTransaction(id); - if (blockId) return; + if (blockId || externalBlockId) return; await delay(1_000); await this.checkTransactionBlockId(id); } async waitForTransactionBlockId(id: string): Promise { - const { txId } = this.getTransaction(id); + const tx = this.getTransaction(id); - if (!txId) { - throw new Error(`[${this.constructor.name}]: Transaction "id" is empty, first sign the transaction`); + if (isUnsignedTx(tx)) { + throw new Error( + `[${this.constructor.name}]: Transaction "id" or "externalHash" is empty, first sign the transaction` + ); } try { await Promise.race([ this.checkTransactionBlockId(id), - delay(BLOCK_PRODUCE_TIME * 6, false), // 36s + delay(BLOCK_PRODUCE_TIME * 10, false), // 60s ]); } catch (error) { console.info(`[${this.constructor.name}]: Implement "blockId" restoration by "txId"`); diff --git a/src/utils/bridge/common/utils.ts b/src/utils/bridge/common/utils.ts index 37c9737a9..3cb8cd989 100644 --- a/src/utils/bridge/common/utils.ts +++ b/src/utils/bridge/common/utils.ts @@ -1,10 +1,11 @@ -import { Operation, isEthOperation, isEvmOperation, isSubstrateOperation } from '@sora-substrate/util'; +import { isEthOperation, isEvmOperation, isSubstrateOperation } from '@sora-substrate/util'; import { api as soraApi } from '@soramitsu/soraneo-wallet-web'; import { ethers } from 'ethers'; -import { isUnsignedTx as isUnsignedEthTx } from '@/utils/bridge/eth/utils'; -import { isUnsignedTx as isUnsignedEvmTx } from '@/utils/bridge/evm/utils'; -import { isUnsignedTx as isUnsignedSubTx } from '@/utils/bridge/sub/utils'; +import type { GetTransaction, UpdateTransaction } from '@/utils/bridge/common/types'; +import { isUnsignedTx as isUnsignedEthTx, isOutgoingTx as isOutgoingEthTx } from '@/utils/bridge/eth/utils'; +import { isUnsignedTx as isUnsignedEvmTx, isOutgoingTx as isOutgoingEvmTx } from '@/utils/bridge/evm/utils'; +import { isUnsignedTx as isUnsignedSubTx, isOutgoingTx as isOutgoingSubTx } from '@/utils/bridge/sub/utils'; import ethersUtil from '@/utils/ethers-util'; import type { ApiPromise } from '@polkadot/api'; @@ -13,6 +14,13 @@ import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/type import type { EvmHistory } from '@sora-substrate/util/build/bridgeProxy/evm/types'; import type { SubHistory } from '@sora-substrate/util/build/bridgeProxy/sub/types'; +export const getEvmTransactionFee = (tx: ethers.TransactionResponse | ethers.TransactionReceipt) => { + const gasPrice = tx.gasPrice; + const gasAmount = 'gasUsed' in tx ? tx.gasUsed : tx.gasLimit; + + return ethersUtil.calcEvmFee(gasPrice, gasAmount); +}; + export const waitForEvmTransactionMined = async ( tx: ethers.TransactionResponse | null, replaceCallback?: (tx: ethers.TransactionResponse | null) => void @@ -77,7 +85,11 @@ export const getTransactionEvents = async (blockHash: string, transactionHash: s export const isOutgoingTransaction = (transaction: Nullable): boolean => { if (!transaction?.type) return false; - return [Operation.EthBridgeOutgoing, Operation.EvmOutgoing, Operation.SubstrateOutgoing].includes(transaction.type); + if (isEthOperation(transaction.type)) return isOutgoingEthTx(transaction as EthHistory); + if (isEvmOperation(transaction.type)) return isOutgoingEvmTx(transaction as EvmHistory); + if (isSubstrateOperation(transaction.type)) return isOutgoingSubTx(transaction as SubHistory); + + return false; }; export const isUnsignedTx = (transaction: Nullable): boolean => { @@ -89,3 +101,40 @@ export const isUnsignedTx = (transaction: Nullable): boolean return true; }; + +export const onEvmTransactionPending = async ( + id: string, + getTransaction: GetTransaction, + updateTransaction: UpdateTransaction +) => { + const tx = getTransaction(id); + const hash = tx.externalHash; + + if (!hash) throw new Error(`[onEvmTransactionPending] Evm transaction hash is empty`); + + const txResponse = await ethersUtil.getEvmTransaction(hash); + const txReceipt = await waitForEvmTransactionMined(txResponse, (replacedTx) => { + if (replacedTx) { + updateTransaction(id, { + externalHash: replacedTx.hash, + externalNetworkFee: getEvmTransactionFee(replacedTx), + }); + } + }); + + const { fee, blockNumber, blockHash } = txReceipt || {}; + + if (!(fee && blockNumber && blockHash)) { + updateTransaction(id, { externalHash: undefined, externalNetworkFee: undefined }); + throw new Error( + `[onEvmTransactionPending]: Ethereum transaction not found, hash: ${tx.externalHash}. 'externalHash' is reset` + ); + } + + // In EthHistory 'blockHeight' will store evm block number + updateTransaction(id, { + externalNetworkFee: fee.toString(), + externalBlockHeight: blockNumber, + externalBlockId: blockHash, + }); +}; diff --git a/src/utils/bridge/eth/classes/history.ts b/src/utils/bridge/eth/classes/history.ts index c122fa706..0d4f20e0c 100644 --- a/src/utils/bridge/eth/classes/history.ts +++ b/src/utils/bridge/eth/classes/history.ts @@ -33,8 +33,8 @@ export default class EtherscanHistoryProvider extends EtherscanProvider { } const BRIDGE_INTERFACE = new ethers.Interface([ - ...SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.XOR].abi, // XOR or VAL - ...SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other].abi, // Other + ...SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.XOR], // XOR or VAL + ...SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other], // Other ]); const { ETH_BRIDGE_STATES } = WALLET_CONSTS; diff --git a/src/utils/bridge/eth/classes/reducers.ts b/src/utils/bridge/eth/classes/reducers.ts index 09c6c1e0f..ee7a2f40f 100644 --- a/src/utils/bridge/eth/classes/reducers.ts +++ b/src/utils/bridge/eth/classes/reducers.ts @@ -3,16 +3,10 @@ import first from 'lodash/fp/first'; import { BridgeReducer } from '@/utils/bridge/common/classes'; import type { IBridgeReducerOptions, GetBridgeHistoryInstance, SignExternal } from '@/utils/bridge/common/types'; -import { getTransactionEvents, waitForEvmTransactionMined } from '@/utils/bridge/common/utils'; +import { getTransactionEvents, getEvmTransactionFee, onEvmTransactionPending } from '@/utils/bridge/common/utils'; import { ethBridgeApi } from '@/utils/bridge/eth/api'; import type { EthBridgeHistory } from '@/utils/bridge/eth/classes/history'; -import { - getTransaction, - getTransactionFee, - waitForApprovedRequest, - waitForIncomingRequest, -} from '@/utils/bridge/eth/utils'; -import ethersUtil from '@/utils/ethers-util'; +import { getTransaction, waitForApprovedRequest, waitForIncomingRequest } from '@/utils/bridge/eth/utils'; import type { IBridgeTransaction } from '@sora-substrate/util'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; @@ -40,36 +34,7 @@ export class EthBridgeReducer extends BridgeReducer { } async onEvmPending(id: string): Promise { - const tx = this.getTransaction(id); - const hash = tx.externalHash; - - if (!hash) throw new Error(`[${this.constructor.name}]: Ethereum transaction hash is empty`); - - const txResponse = await ethersUtil.getEvmTransaction(hash); - const txReceipt = await waitForEvmTransactionMined(txResponse, (replacedTx) => { - if (replacedTx) { - this.updateTransactionParams(id, { - externalHash: replacedTx.hash, - externalNetworkFee: getTransactionFee(replacedTx), - }); - } - }); - - const { fee, blockNumber, blockHash } = txReceipt || {}; - - if (!(fee && blockNumber && blockHash)) { - this.updateTransactionParams(id, { externalHash: undefined, externalNetworkFee: undefined }); - throw new Error( - `[${this.constructor.name}]: Ethereum transaction not found, hash: ${tx.externalHash}. 'externalHash' is reset` - ); - } - - // In EthHistory 'blockHeight' will store evm block number - this.updateTransactionParams(id, { - externalNetworkFee: fee.toString(), - externalBlockHeight: blockNumber, - externalBlockId: blockHash, - }); + await onEvmTransactionPending(id, this.getTransaction, this.updateTransactionParams); } async onEvmSubmitted(id: string, signExternal: SignExternal): Promise { @@ -83,7 +48,7 @@ export class EthBridgeReducer extends BridgeReducer { // update after sign this.updateTransactionParams(id, { externalHash: signedTx.hash, - externalNetworkFee: getTransactionFee(signedTx), + externalNetworkFee: getEvmTransactionFee(signedTx), }); } catch (error: any) { // maybe transaction already completed, try to restore ethereum transaction hash diff --git a/src/utils/bridge/eth/utils.ts b/src/utils/bridge/eth/utils.ts index cde815bb4..c6f4d3945 100644 --- a/src/utils/bridge/eth/utils.ts +++ b/src/utils/bridge/eth/utils.ts @@ -1,7 +1,6 @@ import { Operation, FPNumber } from '@sora-substrate/util'; import { BridgeTxStatus } from '@sora-substrate/util/build/bridgeProxy/consts'; import { EthCurrencyType, EthAssetKind } from '@sora-substrate/util/build/bridgeProxy/eth/consts'; -import { ethers } from 'ethers'; import { SmartContractType, KnownEthBridgeAsset, SmartContracts } from '@/consts/evm'; import { asZeroValue } from '@/utils'; @@ -20,13 +19,15 @@ type EthTxParams = { request?: EthApprovedRequest; }; +export const isOutgoingTx = (tx: EthHistory): boolean => { + return tx.type === Operation.EthBridgeOutgoing; +}; + export const isUnsignedFromPart = (tx: EthHistory): boolean => { - if (tx.type === Operation.EthBridgeOutgoing) { + if (isOutgoingTx(tx)) { return !tx.blockId && !tx.txId; - } else if (tx.type === Operation.EthBridgeIncoming) { - return !tx.externalHash; } else { - return true; + return !tx.externalHash; } }; @@ -124,14 +125,12 @@ export const waitForIncomingRequest = async (tx: EthHistory): Promise<{ hash: st export async function getIncomingEvmTransactionData({ asset, value, recipient, getContractAddress }: EthTxParams) { const isNativeEvmToken = ethersUtil.isNativeEvmTokenAddress(asset.externalAddress); - const signer = await ethersUtil.getSigner(); const accountId = ethersUtil.accountAddressToHex(recipient); - const amount = new FPNumber(value, asset.externalDecimals).toCodecString(); const contractAddress = getContractAddress(KnownEthBridgeAsset.Other) as string; - const contractAbi = SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other].abi; - const contract = new ethers.Contract(contractAddress, contractAbi, signer); + const contractAbi = SmartContracts[SmartContractType.EthBridge][KnownEthBridgeAsset.Other]; + const contract = await ethersUtil.getContract(contractAddress, contractAbi); const method = isNativeEvmToken ? 'sendEthToSidechain' : 'sendERC20ToSidechain'; const methodArgs = isNativeEvmToken @@ -162,14 +161,14 @@ export async function getOutgoingEvmTransactionData({ }: EthTxParams) { if (!request) throw new Error('request is required!'); - const signer = await ethersUtil.getSigner(); const symbol = asset.symbol as KnownEthBridgeAsset; const isValOrXor = [KnownEthBridgeAsset.XOR, KnownEthBridgeAsset.VAL].includes(symbol); const bridgeAsset: KnownEthBridgeAsset = isValOrXor ? symbol : KnownEthBridgeAsset.Other; + const contractAddress = getContractAddress(bridgeAsset) as string; - const contractAbi = SmartContracts[SmartContractType.EthBridge][bridgeAsset].abi; + const contractAbi = SmartContracts[SmartContractType.EthBridge][bridgeAsset]; + const contract = await ethersUtil.getContract(contractAddress, contractAbi); - const contract = new ethers.Contract(contractAddress, contractAbi, signer); const amount = new FPNumber(value, asset.externalDecimals).toCodecString(); const isEthereumCurrency = request.currencyType === EthCurrencyType.TokenAddress; @@ -292,10 +291,3 @@ export async function getEthNetworkFee( return ethersUtil.calcEvmFee(gasPrice, gasLimitTotal); } - -export const getTransactionFee = (tx: ethers.TransactionResponse | ethers.TransactionReceipt) => { - const gasPrice = tx.gasPrice; - const gasAmount = 'gasUsed' in tx ? tx.gasUsed : tx.gasLimit; - - return ethersUtil.calcEvmFee(gasPrice, gasAmount); -}; diff --git a/src/utils/bridge/evm/utils.ts b/src/utils/bridge/evm/utils.ts index 8640cd6ee..5dca0845c 100644 --- a/src/utils/bridge/evm/utils.ts +++ b/src/utils/bridge/evm/utils.ts @@ -4,13 +4,15 @@ import { evmBridgeApi } from '@/utils/bridge/evm/api'; import type { EvmHistory } from '@sora-substrate/util/build/bridgeProxy/evm/types'; +export const isOutgoingTx = (tx: EvmHistory): boolean => { + return tx.type === Operation.EvmOutgoing; +}; + export const isUnsignedTx = (tx: EvmHistory): boolean => { - if (tx.type === Operation.EvmOutgoing) { + if (isOutgoingTx(tx)) { return !tx.blockId && !tx.txId; - } else if (tx.type === Operation.EvmIncoming) { - return true; } else { - return true; + return !tx.externalHash; } }; diff --git a/src/utils/bridge/sub/classes/adapter.ts b/src/utils/bridge/sub/classes/adapter.ts index ca668c2b0..4880e970a 100644 --- a/src/utils/bridge/sub/classes/adapter.ts +++ b/src/utils/bridge/sub/classes/adapter.ts @@ -10,8 +10,10 @@ import { determineTransferType } from '@/utils/bridge/sub/utils'; import { AcalaParachainAdapter } from './adapters/parachain/acala'; import { AstarParachainAdapter } from './adapters/parachain/astar'; import { MoonbaseParachainAdapter } from './adapters/parachain/moonbase'; +import { ParachainAdapter } from './adapters/parachain/parachain'; import { SoraParachainAdapter } from './adapters/parachain/sora'; -import { RelaychainAdapter } from './adapters/relaychain'; +import { AlphanetRelaychainAdapter } from './adapters/relaychain/alphanet'; +import { RelaychainAdapter } from './adapters/relaychain/relaychain'; import { LiberlandAdapter } from './adapters/standalone/liberland'; import { SubAdapter } from './adapters/substrate'; @@ -27,8 +29,8 @@ type PathNetworks = { export class SubNetworksConnector { public soraParachain?: SoraParachainAdapter; - public relaychain?: SubAdapter; - public parachain?: SubAdapter; + public relaychain?: RelaychainAdapter; + public parachain?: ParachainAdapter; public standalone?: SubAdapter; public destinationNetwork!: SubNetwork; @@ -95,6 +97,9 @@ export class SubNetworksConnector { protected getAdapter(network: SubNetwork) { if (subBridgeApi.isRelayChain(network)) { + if (network === SubNetworkId.Alphanet) { + return new AlphanetRelaychainAdapter(network); + } return new RelaychainAdapter(network); } if (subBridgeApi.isParachain(network)) { @@ -110,6 +115,7 @@ export class SubNetworksConnector { if (subBridgeApi.isSoraParachain(network)) { return new SoraParachainAdapter(network); } + return new ParachainAdapter(network); } if (subBridgeApi.isStandalone(network)) { if (network === SubNetworkId.Liberland) { @@ -192,27 +198,38 @@ export class SubNetworksConnector { await Promise.all(this.uniqueAdapters.map((c) => c.stop())); } + /** + * Transfer funds from SORA to destination network + */ + public async outgoingTransfer(asset: RegisteredAsset, recipient: string, amount: string | number, historyId: string) { + await subBridgeApi.transfer(asset, recipient, amount, this.destinationNetwork, historyId); + } + /** * Transfer funds from destination network to SORA */ - public async transfer(asset: RegisteredAsset, recipient: string, amount: string | number, historyId?: string) { - const { api, accountPair, signer } = this.accountApi; - - if (!accountPair) throw new Error(`[${this.constructor.name}] Account pair is not set.`); - - const historyItem = subBridgeApi.getHistory(historyId as string) ?? { - type: Operation.SubstrateIncoming, - symbol: asset.symbol, - assetAddress: asset.address, - amount: `${amount}`, - externalNetwork: this.destinationNetwork, - externalNetworkType: BridgeNetworkType.Sub, - from: subBridgeApi.address, // "from" is always SORA account address - to: this.accountApi.address, - }; - - const extrinsic = this.network.getTransferExtrinsic(asset, recipient, amount); - // submit extrinsic using SORA api, because current implementation using "subHistory" from SORA api scope - await subBridgeApi.submitApiExtrinsic(api, extrinsic as any, accountPair, signer, historyItem); + public async incomingTransfer(asset: RegisteredAsset, recipient: string, amount: string | number, historyId: string) { + if (subBridgeApi.isEvmAccount(this.destinationNetwork)) { + await this.network.transfer(asset, recipient, amount, historyId); + } else { + const { api, accountPair, signer } = this.accountApi; + + if (!accountPair) throw new Error(`[${this.constructor.name}] Account pair is not set.`); + + const historyItem = subBridgeApi.getHistory(historyId as string) ?? { + type: Operation.SubstrateIncoming, + symbol: asset.symbol, + assetAddress: asset.address, + amount: `${amount}`, + externalNetwork: this.destinationNetwork, + externalNetworkType: BridgeNetworkType.Sub, + from: subBridgeApi.address, // "from" is always SORA account address + to: this.accountApi.address, + }; + + const extrinsic = this.network.getTransferExtrinsic(asset, recipient, amount); + // submit extrinsic using SORA api, because current implementation using "subHistory" from SORA api scope + await subBridgeApi.submitApiExtrinsic(api, extrinsic as any, accountPair, signer, historyItem); + } } } diff --git a/src/utils/bridge/sub/classes/adapters/parachain/acala.ts b/src/utils/bridge/sub/classes/adapters/parachain/acala.ts index 05ea461ea..5f8b7a97d 100644 --- a/src/utils/bridge/sub/classes/adapters/parachain/acala.ts +++ b/src/utils/bridge/sub/classes/adapters/parachain/acala.ts @@ -3,10 +3,10 @@ import { formatBalance } from '@sora-substrate/util/build/assets'; import { ZeroStringValue } from '@/consts'; -import { SubAdapter } from '../substrate'; +import { ParachainAdapter, type IParachainAssetMetadata } from './parachain'; import type { CodecString } from '@sora-substrate/util'; -import type { RegisteredAsset } from '@sora-substrate/util/build/assets/types'; +import type { Asset, RegisteredAsset } from '@sora-substrate/util/build/assets/types'; enum AcalaPrimitivesCurrencyCurrencyId { Token = 'Token', @@ -29,13 +29,6 @@ type IAcalaCurrencyId = [AcalaPrimitivesCurrencyCurrencyId.Erc20]: string; }; -type IAcalaAssetMetadata = { - id: IAcalaCurrencyId; - symbol: string; - decimals: number; - minimalBalance: string; -}; - function getAcalaCurrencyId(nature: any): Nullable { if (nature.isNativeAssetId) { const value = nature.asNativeAssetId; @@ -50,13 +43,18 @@ function getAcalaCurrencyId(nature: any): Nullable { return null; } -export class AcalaParachainAdapter extends SubAdapter { - protected assets: Record | null = null; +export class AcalaParachainAdapter extends ParachainAdapter { + // overrides "SubAdapter" + public override async connect(): Promise { + await super.connect(); + await this.getAssetsMetadata(); + } - protected async getAssetsMetadata(): Promise { - if (this.assets) return; + // overrides "ParachainAdapter" + protected override async getAssetsMetadata(): Promise { + if (Array.isArray(this.assets)) return; - const assets = {}; + const assets: IParachainAssetMetadata[] = []; const entries = await (this.api.query.assetRegistry as any).assetMetadatas.entries(); for (const [key, option] of entries) { @@ -69,32 +67,36 @@ export class AcalaParachainAdapter extends SubAdapter { const decimals = option.value.decimals.toNumber(); const minimalBalance = option.value.minimalBalance.toString(); - assets[symbol] = { id, symbol, decimals, minimalBalance }; + assets.push({ id, symbol, decimals, minimalBalance }); } this.assets = Object.freeze(assets); } - private getAssetMeta(asset: RegisteredAsset): Nullable { - if (!(asset.symbol && this.assets)) return null; + // overrides "ParachainAdapter" + public override getAssetMeta(asset: RegisteredAsset): Nullable> { + if (!Array.isArray(this.assets)) return null; - return this.assets[asset.symbol]; + return this.assets.find( + (item) => JSON.stringify(item.id) === asset.externalAddress || item.symbol === asset.symbol + ); } - // overrides SubAdapter - public override async connect(): Promise { - await super.connect(); - await this.getAssetsMetadata(); - } + // overrides "ParachainAdapter" + public override async getAssetIdByMultilocation(asset: Asset, multilocation: any): Promise { + const v3Multilocation = multilocation; - protected override async getAssetDeposit(asset: RegisteredAsset): Promise { - const assetMeta = this.getAssetMeta(asset); + const result = await (this.api.query.assetRegistry as any).locationToCurrencyIds(v3Multilocation); - if (!assetMeta) return ZeroStringValue; + let id!: Nullable; - const minBalance = assetMeta.minimalBalance; + if (result.isEmpty) { + id = { [AcalaPrimitivesCurrencyCurrencyId.Token]: asset.symbol }; + } else { + id = getAcalaCurrencyId(result.unwrap()); + } - return minBalance > '1' ? minBalance : ZeroStringValue; + return id ? JSON.stringify(id) : ''; } protected override async getAccountAssetBalance( @@ -113,7 +115,7 @@ export class AcalaParachainAdapter extends SubAdapter { }, ZeroStringValue); } - // overrides SubAdapter + // overrides "SubAdapter" public override getTransferExtrinsic(asset: RegisteredAsset, recipient: string, amount: number | string) { const assetMeta = this.getAssetMeta(asset); @@ -148,11 +150,4 @@ export class AcalaParachainAdapter extends SubAdapter { 'Unlimited' ); } - - public override async getNetworkFee(asset: RegisteredAsset, sender: string, recipient: string): Promise { - /* Throws error until Substrate 5 migration */ - // return await super.getNetworkFee(asset, sender, recipient); - // Hardcoded value for Acala - 0.003 ACA - return '3000000000'; - } } diff --git a/src/utils/bridge/sub/classes/adapters/parachain/astar.ts b/src/utils/bridge/sub/classes/adapters/parachain/astar.ts index eab8b563f..3c4239e38 100644 --- a/src/utils/bridge/sub/classes/adapters/parachain/astar.ts +++ b/src/utils/bridge/sub/classes/adapters/parachain/astar.ts @@ -1,60 +1,32 @@ import { FPNumber } from '@sora-substrate/util'; -import { formatBalance } from '@sora-substrate/util/build/assets'; import { ZeroStringValue } from '@/consts'; -import { SubAdapter } from '../substrate'; +import { ParachainAdapter } from './parachain'; import type { CodecString } from '@sora-substrate/util'; -import type { RegisteredAsset } from '@sora-substrate/util/build/assets/types'; - -type IAstarAssetMetadata = { - id: string; - symbol: string; - decimals: number; - minimalBalance: string; -}; - -export class AstarParachainAdapter extends SubAdapter { - protected assets: Record | null = null; - - protected async getAssetsMetadata(): Promise { - if (this.assets) return; - - const assets = {}; - const entries = await (this.api.query.assets as any).metadata.entries(); - - for (const [key, value] of entries) { - const id = key.args[0].toString(); - const symbol = new TextDecoder().decode(value.symbol); // bytes to string - const decimals = value.decimals.toNumber(); - const minimalBalance = value.deposit.toString(); - - assets[symbol] = { id, symbol, decimals, minimalBalance }; - } - - this.assets = Object.freeze(assets); - } - - private getAssetMeta(asset: RegisteredAsset): Nullable { - if (!(asset.symbol && this.assets)) return null; - - return this.assets[asset.symbol]; - } +import type { Asset, RegisteredAsset } from '@sora-substrate/util/build/assets/types'; +export class AstarParachainAdapter extends ParachainAdapter { + // overrides "SubAdapter" public override async connect(): Promise { await super.connect(); await this.getAssetsMetadata(); } - protected override async getAssetDeposit(asset: RegisteredAsset): Promise { - const assetMeta = this.getAssetMeta(asset); + // overrides "ParachainAdapter" + public override async getAssetIdByMultilocation(asset: Asset, multilocation: any): Promise { + const versionedMultilocation = { + V3: multilocation, + }; - if (!assetMeta) return ZeroStringValue; + const result = await (this.api.query.xcAssetConfig as any).assetLocationToId(versionedMultilocation); + + if (result.isEmpty) return ''; - const minBalance = assetMeta.minimalBalance; + const id = result.unwrap().toString(); - return minBalance > '1' ? minBalance : ZeroStringValue; + return id; } protected override async getAccountAssetBalance( @@ -68,6 +40,12 @@ export class AstarParachainAdapter extends SubAdapter { return await this.assetsAccountRequest(accountAddress, assetMeta.id); } + public override getTransferExtrinsic(asset: RegisteredAsset, recipient: string, amount: number | string) { + return asset.symbol === this.chainSymbol + ? this.getNativeTransferExtrinsic(asset, recipient, amount) + : this.getAssetTransferExtrinsic(asset, recipient, amount); + } + /** * Transfer native token (ASTR) */ @@ -157,17 +135,4 @@ export class AstarParachainAdapter extends SubAdapter { 'Unlimited' ); } - - public override getTransferExtrinsic(asset: RegisteredAsset, recipient: string, amount: number | string) { - return asset.symbol === this.chainSymbol - ? this.getNativeTransferExtrinsic(asset, recipient, amount) - : this.getAssetTransferExtrinsic(asset, recipient, amount); - } - - public async getNetworkFee(asset: RegisteredAsset, sender: string, recipient: string): Promise { - /* Throws error until Substrate 5 migration */ - // return await super.getNetworkFee(asset, sender, recipient); - // Hardcoded value for Astar - 0.057 ASTR - return '57000000000000000'; - } } diff --git a/src/utils/bridge/sub/classes/adapters/parachain/moonbase.ts b/src/utils/bridge/sub/classes/adapters/parachain/moonbase.ts index 3bc0669c7..76dfeebe6 100644 --- a/src/utils/bridge/sub/classes/adapters/parachain/moonbase.ts +++ b/src/utils/bridge/sub/classes/adapters/parachain/moonbase.ts @@ -1,13 +1,147 @@ -import { u8aToHex } from '@polkadot/util'; -import { addressToEvm } from '@polkadot/util-crypto'; - -import { SubAdapter } from '../substrate'; - -export class MoonbaseParachainAdapter extends SubAdapter { - // overrides SubAdapter method - public formatAddress = (address?: string): string => { - if (!address) return ''; - // [TODO] research how to get evm address as on moonbase - return u8aToHex(addressToEvm(address)); - }; +import { FPNumber, TransactionStatus } from '@sora-substrate/util'; +import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; +import BN from 'bignumber.js'; + +import xTokensAbi from '@/abi/ethereum/other/moonbeam/xTokens.json'; +import { ZeroStringValue } from '@/consts'; +import { SUB_NETWORKS } from '@/consts/sub'; +import { delay } from '@/utils'; +import { getEvmTransactionFee, onEvmTransactionPending } from '@/utils/bridge/common/utils'; +import { getTransaction, updateTransaction } from '@/utils/bridge/sub/utils'; +import ethersUtil from '@/utils/ethers-util'; + +import { ParachainAdapter } from './parachain'; + +import type { CodecString } from '@sora-substrate/util'; +import type { Asset, RegisteredAsset } from '@sora-substrate/util/build/assets/types'; +import type { ethers } from 'ethers'; + +const MOONBASE_DATA = SUB_NETWORKS[SubNetworkId.AlphanetMoonbase]; + +export class MoonbaseParachainAdapter extends ParachainAdapter { + protected nativeAssetContractAddress = '0x0000000000000000000000000000000000000802'; + protected xTokensContractAddress = '0x0000000000000000000000000000000000000804'; + + // overrides "WithConnectionApi" + override get chainSymbol(): string | undefined { + return MOONBASE_DATA?.nativeCurrency?.symbol; + } + + // overrides "WithConnectionApi" + public override formatAddress(address: string, _withPrefix = true): string { + // return evm address without changes + return address; + } + + // overrides "SubAdapter" + public override async connect(): Promise { + await super.connect(); + await this.getAssetsMetadata(); + } + + // overrides "SubAdapter" + protected override async getAccountAssetBalance( + accountAddress: string, + asset: RegisteredAsset + ): Promise { + return await this.assetsAccountRequest(accountAddress, asset.externalAddress); + } + + // Assets has not minimal deposit on Moonbase + // overrides "ParachainAdapter" + protected override async getAssetDeposit(asset: RegisteredAsset): Promise { + return ZeroStringValue; + } + + // overrides "ParachainAdapter" + public override async getAssetIdByMultilocation(asset: Asset, multilocation: any): Promise { + const assetType = { + XCM: multilocation, + }; + + const result = await (this.api.query.assetManager as any).assetTypeId(assetType); + + if (result.isEmpty) return ''; + + const id = result.unwrap().toString(); + + return id; + } + + // overrides "ParachainAdapter" + public override assetIdToEvmContractAddress(id: string): string { + const base = new BN(id).toString(16); + const padded = base.padStart(40, 'f'); + return `0x${padded}`; + } + + /** + * Convert parachain id to xcm junction + */ + protected toParachainAddress(id: number | undefined): string { + const selector = '0x00'; // Parachain selector + const address = Number(id ?? 0) + .toString(16) + .padStart(8, '0'); // bytes4 + + return selector + address; + } + + /** + * Convert substrate account address to xcm junction + */ + protected toAccountId32(address: string): string { + const selector = '0x01'; // AccountKey32 selector + const publicKey = this.getPublicKeyByAddress(address); // AccountId32 address in hex + const networkOption = '00'; // Network(Option) Null + + return selector + publicKey + networkOption; + } + + public override async transfer( + asset: RegisteredAsset, + recipient: string, + amount: string | number, + historyId: string + ) { + const currencyAddress = + asset.symbol === this.chainSymbol + ? this.nativeAssetContractAddress + : this.assetIdToEvmContractAddress(asset.externalAddress); + + const value = new FPNumber(amount, asset.externalDecimals).toCodecString(); + const weight = 5_000_000_000; // max weight, taken from successful xcm message on SORA parachain + + const parents = 1; + const parachainJunction = this.toParachainAddress(this.getSoraParachainId()); + const accountJunction = this.toAccountId32(recipient); + const interior = [parachainJunction, accountJunction]; // interior = X2 (the array has a length of 2) + const destination = [parents, interior]; + + const xTokens = await ethersUtil.getContract(this.xTokensContractAddress, xTokensAbi); + + const signedTx: ethers.TransactionResponse = await xTokens.transfer(currencyAddress, value, destination, weight); + + updateTransaction(historyId, { + externalHash: signedTx.hash, + externalNetworkFee: getEvmTransactionFee(signedTx), + status: TransactionStatus.InBlock, + }); + + // wait a little to update tx in storage + await delay(); + + // run non blocking promise to update tx data + onEvmTransactionPending(historyId, getTransaction, updateTransaction) + .then(() => { + updateTransaction(historyId, { + status: TransactionStatus.Finalized, + }); + }) + .catch(() => { + updateTransaction(historyId, { + status: TransactionStatus.Error, + }); + }); + } } diff --git a/src/utils/bridge/sub/classes/adapters/parachain/parachain.ts b/src/utils/bridge/sub/classes/adapters/parachain/parachain.ts new file mode 100644 index 000000000..b6f7f7cfd --- /dev/null +++ b/src/utils/bridge/sub/classes/adapters/parachain/parachain.ts @@ -0,0 +1,85 @@ +import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; + +import { ZeroStringValue } from '@/consts'; + +import { SubAdapter } from '../substrate'; + +import type { CodecString } from '@sora-substrate/util'; +import type { Asset, RegisteredAsset } from '@sora-substrate/util/build/assets/types'; + +export type IParachainAssetMetadata = { + id: AssetId; + symbol: string; + decimals: number; + minimalBalance?: string; +}; + +export class ParachainAdapter extends SubAdapter { + protected assets: readonly IParachainAssetMetadata[] | null = null; + + protected async getAssetsMetadata(): Promise { + if (Array.isArray(this.assets)) return; + + const assets: IParachainAssetMetadata[] = []; + const entries = await (this.api.query.assets as any).metadata.entries(); + + for (const [key, value] of entries) { + const id = key.args[0].toString(); + const symbol = new TextDecoder().decode(value.symbol); // bytes to string + const decimals = value.decimals.toNumber(); + + assets.push({ id, symbol, decimals }); + } + + this.assets = Object.freeze(assets); + } + + public getAssetMeta(asset: RegisteredAsset): Nullable> { + if (!Array.isArray(this.assets)) return null; + + return this.assets.find((item) => item.id === asset.externalAddress || item.symbol === asset.symbol); + } + + /** + * Get asset external address by multilocation (for "registeredAsset.externalAddress" struct)) + */ + public async getAssetIdByMultilocation(asset: Asset, multilocation: any): Promise { + throw new Error(`[${this.constructor.name}] "getAssetIdByMultilocation" method is not implemented`); + } + + /** + * Convert substrate asset id to evm token contract address + */ + public assetIdToEvmContractAddress(id: string): string { + throw new Error(`[${this.constructor.name}] "assetIdToEvmContractAddress" method is not implemented`); + } + + // overrides SubAdapter + protected override async getAssetDeposit(asset: RegisteredAsset): Promise { + const assetMeta = this.getAssetMeta(asset); + + if (!assetMeta) return ZeroStringValue; + + const { minimalBalance, id } = assetMeta; + + if (minimalBalance) return minimalBalance; + + return await this.assetMinBalanceRequest(id as string); + } + + public override async getNetworkFee(asset: RegisteredAsset, sender: string, recipient: string): Promise { + /* Throws error until Substrate 5 migration */ + // return await super.getNetworkFee(asset, sender, recipient); + // Hardcoded values + switch (this.subNetwork) { + case SubNetworkId.PolkadotAcala: + return '3000000000'; + case SubNetworkId.PolkadotAstar: + return '57000000000000000'; + case SubNetworkId.AlphanetMoonbase: + return '40000000000000'; + default: + return '0'; + } + } +} diff --git a/src/utils/bridge/sub/classes/adapters/relaychain/alphanet.ts b/src/utils/bridge/sub/classes/adapters/relaychain/alphanet.ts new file mode 100644 index 000000000..0c16e220c --- /dev/null +++ b/src/utils/bridge/sub/classes/adapters/relaychain/alphanet.ts @@ -0,0 +1,24 @@ +import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; + +import { SUB_NETWORKS } from '@/consts/sub'; + +import { RelaychainAdapter } from './relaychain'; + +const ALPHANET_DATA = SUB_NETWORKS[SubNetworkId.Alphanet]; + +export class AlphanetRelaychainAdapter extends RelaychainAdapter { + // overrides "WithConnectionApi" + override get chainSymbol(): string | undefined { + return ALPHANET_DATA?.nativeCurrency?.symbol; + } + + // overrides "WithConnectionApi" + override get chainDecimals(): number | undefined { + return ALPHANET_DATA?.nativeCurrency?.decimals; + } + + // overrides "WithConnectionApi" + override get chainSS58(): number | undefined { + return 42; // (Substrate, 42) + } +} diff --git a/src/utils/bridge/sub/classes/adapters/relaychain.ts b/src/utils/bridge/sub/classes/adapters/relaychain/relaychain.ts similarity index 94% rename from src/utils/bridge/sub/classes/adapters/relaychain.ts rename to src/utils/bridge/sub/classes/adapters/relaychain/relaychain.ts index 5adc2fcca..dc226789e 100644 --- a/src/utils/bridge/sub/classes/adapters/relaychain.ts +++ b/src/utils/bridge/sub/classes/adapters/relaychain/relaychain.ts @@ -1,7 +1,7 @@ import { FPNumber } from '@sora-substrate/util'; import { SubNetworkId } from '@sora-substrate/util/build/bridgeProxy/sub/consts'; -import { SubAdapter } from './substrate'; +import { SubAdapter } from '../substrate'; import type { CodecString } from '@sora-substrate/util'; import type { RegisteredAsset } from '@sora-substrate/util/build/assets/types'; @@ -71,11 +71,11 @@ export class RelaychainAdapter extends SubAdapter { case SubNetworkId.Rococo: return toCodec(0.000125); case SubNetworkId.Alphanet: - return toCodec(0.019); + return toCodec(0.019 + 0.037); case SubNetworkId.Kusama: return toCodec(0.002); case SubNetworkId.Polkadot: - return toCodec(0.059); + return toCodec(0.019 + 0.037); default: return '0'; } diff --git a/src/utils/bridge/sub/classes/adapters/standalone/liberland.ts b/src/utils/bridge/sub/classes/adapters/standalone/liberland.ts index dae7d0931..db4bbdea7 100644 --- a/src/utils/bridge/sub/classes/adapters/standalone/liberland.ts +++ b/src/utils/bridge/sub/classes/adapters/standalone/liberland.ts @@ -9,7 +9,9 @@ import type { RegisteredAsset } from '@sora-substrate/util/build/assets/types'; export class LiberlandAdapter extends SubAdapter { protected override async getAssetDeposit(asset: RegisteredAsset): Promise { - return await this.assetsAssetMinBalanceRequest(Number(asset.externalAddress)); + const assetId = Number(asset.externalAddress); + + return await this.assetMinBalanceRequest(assetId); } protected override async getAccountAssetBalance( diff --git a/src/utils/bridge/sub/classes/adapters/substrate.ts b/src/utils/bridge/sub/classes/adapters/substrate.ts index 68856c606..4b7ddf6e9 100644 --- a/src/utils/bridge/sub/classes/adapters/substrate.ts +++ b/src/utils/bridge/sub/classes/adapters/substrate.ts @@ -135,6 +135,10 @@ class BaseSubAdapter extends WithConnectionApi { public getTransferExtrinsic(asset: RegisteredAsset, recipient: string, amount: string | number) { throw new Error(`[${this.constructor.name}] "getTransferExtrinsic" method is not implemented`); } + + public async transfer(asset: RegisteredAsset, recipient: string, amount: string | number, historyId: string) { + throw new Error(`[${this.constructor.name}] "transfer" method is not implemented`); + } } export class SubAdapter extends BaseSubAdapter { @@ -154,7 +158,7 @@ export class SubAdapter extends BaseSubAdapter { }, ZeroStringValue); } - protected async assetsAssetMinBalanceRequest(assetId: number | string): Promise { + protected async assetMinBalanceRequest(assetId: number | string): Promise { return await this.withConnection(async () => { const result = await (this.api.query.assets as any).asset(assetId); diff --git a/src/utils/bridge/sub/classes/history.ts b/src/utils/bridge/sub/classes/history.ts index 36a498a0a..20da783af 100644 --- a/src/utils/bridge/sub/classes/history.ts +++ b/src/utils/bridge/sub/classes/history.ts @@ -12,10 +12,14 @@ import { getDepositedBalance, getMessageAcceptedNonces, getMessageDispatchedNonces, + getParachainSystemMessageHash, isMessageDispatchedNonces, getReceivedAmount, - isEvent, + isParaInclusion, isTransactionFeePaid, + isBridgeProxyHash, + isMessageAccepted, + isQueueMessage, } from '@/utils/bridge/sub/utils'; import type { ApiPromise } from '@polkadot/api'; @@ -45,9 +49,7 @@ const getTxEvents = (blockEvents: any[], txIndex: number) => { const findTxInBlock = async (blockHash: string, soraHash: string) => { const blockEvents = await api.system.getBlockEvents(blockHash); - const event = blockEvents.find( - (e) => isEvent(e, 'bridgeProxy', 'RequestStatusUpdate') && e.event.data?.[0]?.toString() === soraHash - ); + const event = blockEvents.find((e) => isBridgeProxyHash(e, soraHash)); if (!event) throw new Error('Unable to find "bridgeProxy.RequestStatusUpdate" event'); @@ -241,7 +243,7 @@ class SubBridgeHistory extends SubNetworksConnector { const soraFeeEvent = events.find((e) => isTransactionFeePaid(e)); history.soraNetworkFee = soraFeeEvent.event.data[1].toString(); // sended from SORA nonces - const [soraBatchNonce, soraMessageNonce] = getMessageAcceptedNonces(events, this.soraApi); + const [soraBatchNonce, soraMessageNonce] = getMessageAcceptedNonces(events); // api for Standalone network or SORA parachain const networkApi = this.getIntermediateApi(history); const networkBlockId = history.externalBlockId as string; @@ -250,7 +252,7 @@ class SubBridgeHistory extends SubNetworksConnector { const networkEventsReversed = [...networkEvents].reverse(); // Network received nonces const messageDispatchedIndex = networkEventsReversed.findIndex((e) => - isMessageDispatchedNonces(soraBatchNonce, soraMessageNonce, e, networkApi) + isMessageDispatchedNonces(soraBatchNonce, soraMessageNonce, e) ); if (messageDispatchedIndex === -1) { @@ -269,19 +271,15 @@ class SubBridgeHistory extends SubNetworksConnector { // SORA Parachain extrinsic events for next search const parachainExtrinsicEvents = networkEventsReversed.slice(messageDispatchedIndex); - // sended from SORA Parachain to Relaychain message hash (1) - const messageToRelaychain = parachainExtrinsicEvents.find((e) => - isEvent(e, 'parachainSystem', 'UpwardMessageSent') - ); - // sended from SORA Parachain to Parachain message hash (2) - const messageToParachain = parachainExtrinsicEvents.find((e) => isEvent(e, 'xcmpQueue', 'XcmpMessageSent')); + // sended from SORA Parachain message hash (1) + const messageHash = getParachainSystemMessageHash(parachainExtrinsicEvents); - if (!messageToRelaychain && !messageToParachain) { + if (!messageHash) { return await this.processOutgoingToSoraParachain(history, asset, parachainExtrinsicEvents); } - const isRelaychain = subBridgeApi.isRelayChain(externalNetwork) && messageToRelaychain; - const isParachain = subBridgeApi.isParachain(externalNetwork) && messageToParachain; + const isRelaychain = subBridgeApi.isRelayChain(externalNetwork); + const isParachain = subBridgeApi.isParachain(externalNetwork); if (!isRelaychain && !isParachain) { console.info(`[${history.id}] not "${externalNetwork}" transaction, skip;`); @@ -290,7 +288,6 @@ class SubBridgeHistory extends SubNetworksConnector { this.updateSoraParachainBlockData(history); - const messageHash = (messageToRelaychain ?? messageToParachain).event.data.messageHash.toString(); const relayChainBlockNumber = await subBridgeApi.soraParachainApi.getRelayChainBlockNumber( history.parachainBlockId as string, soraParachainApi @@ -334,7 +331,7 @@ class SubBridgeHistory extends SubNetworksConnector { const [receivedAmount, externalEventIndex] = getDepositedBalance( extrinsicEvents, history.to as string, - soraParachainApi + soraParachain ); // balances.Deposit event index history.externalEventIndex = externalEventIndex; @@ -367,23 +364,15 @@ class SubBridgeHistory extends SubNetworksConnector { txEvents: any[]; blockEvents: any[]; }): Promise> { - const { soraApi } = this; // Token is minted to account event - const [_, eventIndex] = getDepositedBalance(blockEvents, history.from as string, this.soraApi); + const [_, eventIndex] = getDepositedBalance(blockEvents, history.from as string, subBridgeApi); history.payload.eventIndex = eventIndex; // find SORA hash event index - const requestStatusUpdateEventIndex = txEvents.findIndex((e) => { - if (!isEvent(e, 'bridgeProxy', 'RequestStatusUpdate')) return false; - - const hash = e.event.data[0].toString(); - - return hash === history.id; - }); + const requestStatusUpdateEventIndex = txEvents.findIndex((e) => isBridgeProxyHash(e, history.id as string)); // Received on SORA nonces const [soraBatchNonce, soraMessageNonce] = getMessageDispatchedNonces( - txEvents.slice(requestStatusUpdateEventIndex), - soraApi + txEvents.slice(requestStatusUpdateEventIndex) ); // api for Standalone network or SORA parachain const networkApi = this.getIntermediateApi(history); @@ -392,9 +381,9 @@ class SubBridgeHistory extends SubNetworksConnector { const networkEvents = await api.system.getBlockEvents(networkBlockId, networkApi); // Network message sended to SORA const messageToSoraEvent = networkEvents.find((e) => { - if (!isEvent(e, 'substrateBridgeOutboundChannel', 'MessageAccepted')) return false; + if (!isMessageAccepted(e)) return false; - const [networkBatchNonce, networkMessageNonce] = getMessageAcceptedNonces([e], networkApi); + const [networkBatchNonce, networkMessageNonce] = getMessageAcceptedNonces([e]); return networkBatchNonce === soraBatchNonce && networkMessageNonce === soraMessageNonce; }); @@ -428,7 +417,7 @@ class SubBridgeHistory extends SubNetworksConnector { if (!soraParachainApi) throw new Error('SORA Parachain Api is not exists'); // If transfer received from Parachain, extrinsic events should have xcmpQueue.Success event - const messageEvent = networkExtrinsicEvents.find((e) => isEvent(e, 'xcmpQueue', 'Success')); + const messageEvent = networkExtrinsicEvents.find((e) => isQueueMessage(e)); const externalNetwork = history.externalNetwork as SubNetwork; const isRelayChain = subBridgeApi.isRelayChain(externalNetwork) && !messageEvent; const isParachain = @@ -447,7 +436,7 @@ class SubBridgeHistory extends SubNetworksConnector { ); if (isParachain) { - const messageHash = messageEvent.event.data.messageHash.toString(); + const messageHash = messageEvent.event.data[0].toString(); // Parachain block, found through relaychain validation data const parachainBlockId = await this.findParachainBlockIdOnRelaychain(history, relayChainBlockNumber, false); @@ -466,7 +455,7 @@ class SubBridgeHistory extends SubNetworksConnector { // relay chain should have send message in this blocks range const startSearch = relayChainBlockNumber; - const endSearch = startSearch - 6; + const endSearch = startSearch - 10; for (let relaychainBlockHeight = startSearch; relaychainBlockHeight >= endSearch; relaychainBlockHeight--) { const blockId = await api.system.getBlockHash(relaychainBlockHeight, this.externalApi); @@ -528,7 +517,7 @@ class SubBridgeHistory extends SubNetworksConnector { // relay chain should have send validation data in this blocks range const startSearch = relayChainBlockNumber; - const blocksRange = 3; + const blocksRange = 10; const endSearch = isOutgoing ? startSearch + blocksRange : startSearch - blocksRange; for (let relaychainBlockHeight = startSearch; relaychainBlockHeight !== endSearch; ) { @@ -536,11 +525,14 @@ class SubBridgeHistory extends SubNetworksConnector { const events = await api.system.getBlockEvents(blockId, relaychainApi); for (const e of events) { - if (!isEvent(e, 'paraInclusion', 'CandidateIncluded')) continue; + if (!isParaInclusion(e)) continue; const { descriptor } = e.event.data[0]; - if (descriptor.paraId.toNumber() !== network.getParachainId()) continue; + const descriptorParaId = descriptor.paraId.toNumber(); + const paraId = network.getParachainId(); + + if (descriptorParaId !== paraId) continue; history.relaychainBlockHeight = relaychainBlockHeight; history.relaychainBlockId = blockId; @@ -572,10 +564,10 @@ class SubBridgeHistory extends SubNetworksConnector { const extrinsicEvents = parachainBlockEvents.filter( ({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.toNumber() === extrinsicIndex ); - const messageSentEvent = extrinsicEvents.find((e) => isEvent(e, 'xcmpQueue', 'XcmpMessageSent')); - if (!messageSentEvent) continue; - if (messageSentEvent.event.data[0].toString() !== messageHash) continue; + const messageSentHash = getParachainSystemMessageHash(extrinsicEvents); + + if (messageSentHash !== messageHash) continue; const parachainBlockHeight = await api.system.getBlockNumber(parachainBlockId, externalApi); const signer = extrinsic.signer.toString(); @@ -604,15 +596,17 @@ class SubBridgeHistory extends SubNetworksConnector { endSearch: number ) { for (let blockHeight = startSearch; blockHeight <= endSearch; blockHeight++) { + let isReliableMessage = false; + try { const blockId = await api.system.getBlockHash(blockHeight, this.externalApi); const blockEvents = await api.system.getBlockEvents(blockId, this.externalApi); const messageEventIndex = blockEvents.findIndex((e) => { - if (isEvent(e, 'messageQueue', 'Processed') || isEvent(e, 'xcmpQueue', 'Success')) { - const messageHashMatches = e.event.data[0].toString() === messageHash; + if (isQueueMessage(e)) { + isReliableMessage = e.event.data[0].toString() === messageHash; - return messageHashMatches; + return true; } return false; }); @@ -626,7 +620,7 @@ class SubBridgeHistory extends SubNetworksConnector { const [receivedAmount, externalEventIndex] = getDepositedBalance( blockEvents.slice(0, messageEventIndex), history.to, - this.externalApi + this.network ); // Deposit event index @@ -643,7 +637,11 @@ class SubBridgeHistory extends SubNetworksConnector { return history; } catch { - continue; + if (isReliableMessage) { + break; + } else { + continue; + } } } diff --git a/src/utils/bridge/sub/classes/reducers.ts b/src/utils/bridge/sub/classes/reducers.ts index 5aa0958bd..031d09024 100644 --- a/src/utils/bridge/sub/classes/reducers.ts +++ b/src/utils/bridge/sub/classes/reducers.ts @@ -21,8 +21,9 @@ import { determineTransferType, getReceivedAmount, getParachainSystemMessageHash, - isEvent, + isXcmPalletAttempted, isTransactionFeePaid, + isQueueMessage, } from '@/utils/bridge/sub/utils'; import type { ApiRx } from '@polkadot/api'; @@ -105,6 +106,11 @@ export class SubBridgeReducer extends BridgeReducer { // update history data this.updateTransactionPayload(id, { startBlock }); } + + async waitForTxBlockAndStatus(id: string): Promise { + await this.waitForTransactionStatus(id); + await this.waitForTransactionBlockId(id); + } } export class SubBridgeIncomingReducer extends SubBridgeReducer { @@ -156,7 +162,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { // open connections await this.connector.start(); // sign transaction (from is sora account) - await this.connector.transfer(asset, tx.from as string, tx.amount as string, id); + await this.connector.incomingTransfer(asset, tx.from as string, tx.amount as string, id); // save start block when tx was signed await this.saveStartBlock(id); } @@ -183,6 +189,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { private async updateTxExternalData(id: string): Promise { const tx = this.getTransaction(id); + const adapter = this.connector.network; await adapter.connect(); @@ -200,7 +207,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { } if (this.transferType === SubTransferType.Relaychain) { - const xcmEvent = transactionEvents.find((e) => isEvent(e, 'xcmPallet', 'Attempted')); + const xcmEvent = transactionEvents.find((e) => isXcmPalletAttempted(e)); if (!xcmEvent?.event?.data?.[0]?.isComplete) { throw new Error(`[${this.constructor.name}]: Transaction is not completed`); @@ -209,8 +216,10 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { } private async updateTxIncomingData(id: string): Promise { - await this.waitForTransactionStatus(id); - await this.waitForTransactionBlockId(id); + await this.waitForTxBlockAndStatus(id); + + if (subBridgeApi.isEvmAccount(this.getTransaction(id).externalNetwork as SubNetwork)) return; + await this.updateTxSigningData(id); await this.updateTxExternalData(id); } @@ -231,8 +240,8 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { const isFirstStep = [SubTransferType.SoraParachain, SubTransferType.Standalone].includes(this.transferType); const sended = new FPNumber(tx.amount as string, this.asset.externalDecimals).toCodecString(); - const from = tx.from as string; - const to = tx.to as string; + const sender = tx.to as string; + const recipient = tx.from as string; let subscription!: Subscription; let messageNonce!: number; @@ -262,7 +271,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { if (!isStandalone) { assetSendEventIndex = events.findIndex((e) => - isAssetAddedToChannel(e, this.asset, to, sended, adapter.api) + isAssetAddedToChannel(e, this.asset, recipient, sended, adapter) ); if (assetSendEventIndex !== -1) { @@ -273,7 +282,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { } } else { assetSendEventIndex = events.findIndex((e) => - isSoraBridgeAppBurned(e, this.asset, from, to, sended, adapter.api) + isSoraBridgeAppBurned(e, this.asset, sender, recipient, sended, adapter) ); if (assetSendEventIndex !== -1) { @@ -284,7 +293,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { if (assetSendEventIndex === -1) return; blockNumber = blockHeight; - [batchNonce, messageNonce] = getMessageAcceptedNonces(events.slice(assetSendEventIndex), adapter.api); + [batchNonce, messageNonce] = getMessageAcceptedNonces(events.slice(assetSendEventIndex)); resolve(); } catch (error) { @@ -341,16 +350,16 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { try { const events = [...eventsVec.toArray()].reverse(); const substrateDispatchEventIndex = events.findIndex((e) => - isMessageDispatchedNonces(tx.payload.batchNonce, tx.payload.messageNonce, e, subBridgeApi.api) + isMessageDispatchedNonces(tx.payload.batchNonce, tx.payload.messageNonce, e) ); if (substrateDispatchEventIndex === -1) return; const foundedEvents = events.slice(substrateDispatchEventIndex); - soraHash = getBridgeProxyHash(foundedEvents, subBridgeApi.api); + soraHash = getBridgeProxyHash(foundedEvents); - [amount, eventIndex] = getDepositedBalance(foundedEvents, tx.to as string, subBridgeApi.api); + [amount, eventIndex] = getDepositedBalance(foundedEvents, tx.from as string, subBridgeApi); resolve(); } catch (error) { @@ -369,9 +378,9 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { } private async waitSoraBlockByHash(id: string): Promise { - const { hash, to, externalNetwork } = this.getTransaction(id); + const { hash, from, externalNetwork } = this.getTransaction(id); - if (!(hash && to && externalNetwork)) { + if (!(hash && from && externalNetwork)) { throw new Error(`[${this.constructor.name}] Lost transaction params`); } @@ -380,7 +389,7 @@ export class SubBridgeIncomingReducer extends SubBridgeReducer { try { await new Promise((resolve) => { - subscription = subBridgeApi.subscribeOnTransactionDetails(to, externalNetwork, hash).subscribe((data) => { + subscription = subBridgeApi.subscribeOnTransactionDetails(from, externalNetwork, hash).subscribe((data) => { if (data?.endBlock) { soraBlockNumber = data.endBlock; resolve(); @@ -417,8 +426,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { this.updateTransactionParams(id, { transactionState: BridgeTxStatus.Pending }); await this.checkTxId(id); - await this.waitForTransactionStatus(id); - await this.waitForTransactionBlockId(id); + await this.waitForTxBlockAndStatus(id); await this.waitForSendingExecution(id); await this.waitForIntermediateExecution(id); @@ -466,7 +474,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { // open connections await this.connector.start(); // sign transaction - await subBridgeApi.transfer(asset, tx.to as string, tx.amount as string, tx.externalNetwork as SubNetwork, id); + await this.connector.outgoingTransfer(asset, tx.to as string, tx.amount as string, id); // save start block when tx was signed await this.saveStartBlock(id); } @@ -480,10 +488,10 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { const transactionHash = tx.txId as string; const transactionEvents = await getTransactionEvents(blockHash, transactionHash, subBridgeApi.api); - const hash = getBridgeProxyHash(transactionEvents, subBridgeApi.api); + const hash = getBridgeProxyHash(transactionEvents); this.updateTransactionParams(id, { hash }); - const [batchNonce, messageNonce] = getMessageAcceptedNonces(transactionEvents, subBridgeApi.api); + const [batchNonce, messageNonce] = getMessageAcceptedNonces(transactionEvents); this.updateTransactionPayload(id, { batchNonce, messageNonce }); } @@ -521,7 +529,7 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { try { const events = [...eventsVec.toArray()].reverse(); const substrateDispatchEventIndex = events.findIndex((e) => - isMessageDispatchedNonces(tx.payload.batchNonce, tx.payload.messageNonce, e, adapter.api) + isMessageDispatchedNonces(tx.payload.batchNonce, tx.payload.messageNonce, e) ); if (substrateDispatchEventIndex === -1) return; @@ -534,11 +542,11 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { [amountReceived] = getDepositedBalance( events.slice(substrateDispatchEventIndex), tx.to as string, - adapter.api + adapter ); } } else { - messageHash = getParachainSystemMessageHash(events.slice(substrateDispatchEventIndex), adapter.api); + messageHash = getParachainSystemMessageHash(events.slice(substrateDispatchEventIndex)); } resolve(); @@ -601,11 +609,16 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { subscription = combineLatest([eventsObservable, blockNumberObservable]).subscribe( ([eventsVec, blockHeight]) => { + // when received message is equal to sended + let isReliableMessage = false; + try { const events = eventsVec.toArray(); const messageQueueProcessedEventIndex = events.findIndex((e) => { - if (isEvent(e, 'messageQueue', 'Processed') || isEvent(e, 'xcmpQueue', 'Success')) { - return e.event.data[0].toString() === messageHash; + if (isQueueMessage(e)) { + isReliableMessage = e.event.data[0].toString() === messageHash; + + return true; } return false; }); @@ -613,16 +626,19 @@ export class SubBridgeOutgoingReducer extends SubBridgeReducer { if (messageQueueProcessedEventIndex === -1) return; blockNumber = blockHeight; - + // throws error, is deposit not found [amount, externalEventIndex] = getDepositedBalance( events.slice(0, messageQueueProcessedEventIndex), tx.to as string, - adapter.api + adapter ); resolve(); } catch (error) { - reject(error); + // The message is reliable, but the deposit was not found + if (isReliableMessage) { + reject(error); + } } } ); diff --git a/src/utils/bridge/sub/utils.ts b/src/utils/bridge/sub/utils.ts index 401bd4a99..a328fd3fe 100644 --- a/src/utils/bridge/sub/utils.ts +++ b/src/utils/bridge/sub/utils.ts @@ -1,15 +1,21 @@ -import { FPNumber } from '@sora-substrate/util'; +import { FPNumber, Operation } from '@sora-substrate/util'; import { subBridgeApi } from '@/utils/bridge/sub/api'; import { SubTransferType } from '@/utils/bridge/sub/types'; -import type { ApiPromise } from '@polkadot/api'; -import type { CodecString } from '@sora-substrate/util'; +import type { CodecString, WithConnectionApi } from '@sora-substrate/util'; import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types'; import type { SubNetwork, SubHistory } from '@sora-substrate/util/build/bridgeProxy/sub/types'; +export const isOutgoingTx = (tx: SubHistory): boolean => { + return tx.type === Operation.SubstrateOutgoing; +}; + export const isUnsignedTx = (tx: SubHistory): boolean => { - return !tx.blockId && !tx.txId; + const signId = + subBridgeApi.isEvmAccount(tx.externalNetwork as SubNetwork) && !isOutgoingTx(tx) ? tx.externalHash : tx.txId; + + return !tx.blockId && !signId; }; export const getTransaction = (id: string): SubHistory => { @@ -38,8 +44,26 @@ export const determineTransferType = (network: SubNetwork) => { } }; -export const getBridgeProxyHash = (events: Array, api: ApiPromise): string => { - const bridgeProxyEvent = events.find((e) => api.events.bridgeProxy.RequestStatusUpdate.is(e.event)); +export const isEvent = (e, section: string, method: string) => { + return e.event.section === section && e.event.method === method; +}; + +export const isQueueMessage = (e) => + isEvent(e, 'messageQueue', 'Processed') || + isEvent(e, 'xcmpQueue', 'Success') || + isEvent(e, 'xcmpQueue', 'Fail') || + isEvent(e, 'dmpQueue', 'ExecutedDownward'); + +export const isParaInclusion = (e) => isEvent(e, 'paraInclusion', 'CandidateIncluded'); + +export const isXcmPalletAttempted = (e) => isEvent(e, 'xcmPallet', 'Attempted'); + +export const isTransactionFeePaid = (e) => isEvent(e, 'transactionPayment', 'TransactionFeePaid'); + +export const isBridgeProxyUpdate = (e) => isEvent(e, 'bridgeProxy', 'RequestStatusUpdate'); + +export const getBridgeProxyHash = (events: Array): string => { + const bridgeProxyEvent = events.find((e) => isBridgeProxyUpdate(e)); if (!bridgeProxyEvent) { throw new Error(`Unable to find "bridgeProxy.RequestStatusUpdate" event`); @@ -48,14 +72,16 @@ export const getBridgeProxyHash = (events: Array, api: ApiPromise): string return bridgeProxyEvent.event.data[0].toString(); }; -export const isEvent = (e, section: string, method: string) => { - return e.event.section === section && e.event.method === method; -}; +export const isBridgeProxyHash = (e: any, hash: string): boolean => { + if (!isBridgeProxyUpdate(e)) return false; -export const isTransactionFeePaid = (e) => isEvent(e, 'transactionPayment', 'TransactionFeePaid'); + const proxyHash = e.event.data[0].toString(); + + return hash === proxyHash; +}; -export const getDepositedBalance = (events: Array, to: string, api: ApiPromise): [string, number] => { - const recipient = subBridgeApi.formatAddress(to); +export const getDepositedBalance = (events: Array, to: string, chainApi: WithConnectionApi): [string, number] => { + const recipient = chainApi.formatAddress(to).toLowerCase(); const index = events.findIndex((e) => { let eventRecipient = ''; @@ -64,11 +90,15 @@ export const getDepositedBalance = (events: Array, to: string, api: ApiProm eventRecipient = e.event.data.who.toString(); } else if (isEvent(e, 'assets', 'Transfer')) { eventRecipient = e.event.data[1].toString(); + } else if (isEvent(e, 'assets', 'Issued')) { + eventRecipient = e.event.data.owner.toString(); } if (!eventRecipient) return false; - return subBridgeApi.formatAddress(eventRecipient) === recipient; + const formatted = chainApi.formatAddress(eventRecipient).toLowerCase(); + + return formatted === recipient; }); if (index === -1) throw new Error(`Unable to find balance deposit like event`); @@ -88,22 +118,22 @@ export const getReceivedAmount = (sendedAmount: string, receivedAmount: CodecStr return { amount: amount2, transferFee }; }; -export const getParachainSystemMessageHash = (events: Array, api: ApiPromise) => { +export const getParachainSystemMessageHash = (events: Array) => { const parachainSystemEvent = events.find( - (e) => api.events.parachainSystem.UpwardMessageSent.is(e.event) || api.events.xcmpQueue.XcmpMessageSent.is(e.event) + (e) => isEvent(e, 'parachainSystem', 'UpwardMessageSent') || isEvent(e, 'xcmpQueue', 'XcmpMessageSent') ); if (!parachainSystemEvent) { throw new Error(`Unable to find "parachainSystem.UpwardMessageSent" event`); } - return parachainSystemEvent.event.data.messageHash.toString(); + return parachainSystemEvent.event.data[0].toString(); }; -export const getMessageAcceptedNonces = (events: Array, api: ApiPromise): [number, number] => { - const messageAcceptedEvent = events.find((e) => - api.events.substrateBridgeOutboundChannel.MessageAccepted.is(e.event) - ); +export const isMessageAccepted = (e) => isEvent(e, 'substrateBridgeOutboundChannel', 'MessageAccepted'); + +export const getMessageAcceptedNonces = (events: Array): [number, number] => { + const messageAcceptedEvent = events.find((e) => isMessageAccepted(e)); if (!messageAcceptedEvent) { throw new Error('Unable to find "substrateBridgeOutboundChannel.MessageAccepted" event'); @@ -115,8 +145,10 @@ export const getMessageAcceptedNonces = (events: Array, api: ApiPromise): [ return [batchNonce, messageNonce]; }; -export const getMessageDispatchedNonces = (events: Array, api: ApiPromise): [number, number] => { - const messageDispatchedEvent = events.find((e) => api.events.substrateDispatch.MessageDispatched.is(e.event)); +export const isMessageDispatched = (e) => isEvent(e, 'substrateDispatch', 'MessageDispatched'); + +export const getMessageDispatchedNonces = (events: Array): [number, number] => { + const messageDispatchedEvent = events.find((e) => isMessageDispatched(e)); if (!messageDispatchedEvent) { throw new Error('Unable to find "substrateDispatch.MessageDispatched" event'); @@ -129,13 +161,8 @@ export const getMessageDispatchedNonces = (events: Array, api: ApiPromise): return [eventBatchNonce, eventMessageNonce]; }; -export const isMessageDispatchedNonces = ( - sendedBatchNonce: number, - sendedMessageNonce: number, - e: any, - api: ApiPromise -): boolean => { - if (!api.events.substrateDispatch.MessageDispatched.is(e.event)) return false; +export const isMessageDispatchedNonces = (sendedBatchNonce: number, sendedMessageNonce: number, e: any): boolean => { + if (!isMessageDispatched(e)) return false; const { batchNonce, messageNonce } = e.event.data[0]; @@ -156,13 +183,13 @@ export const isAssetAddedToChannel = ( asset: RegisteredAccountAsset, to: string, sended: CodecString, - api: ApiPromise + chainApi: WithConnectionApi ): boolean => { - if (!api.events.xcmApp.AssetAddedToChannel.is(e.event)) return false; + if (!isEvent(e, 'xcmApp', 'AssetAddedToChannel')) return false; const { amount, assetId, recipient } = e.event.data[0].asTransfer; // address check - if (subBridgeApi.formatAddress(recipient.toString()) !== subBridgeApi.formatAddress(to)) return false; + if (chainApi.formatAddress(recipient.toString()) !== chainApi.formatAddress(to)) return false; // asset check if (assetId.toString() !== asset.address) return false; // amount check @@ -179,9 +206,9 @@ export const isSoraBridgeAppBurned = ( from: string, to: string, sended: CodecString, - api: ApiPromise + chainApi: WithConnectionApi ) => { - if (!api.events.soraBridgeApp.Burned.is(e.event)) return false; + if (!isEvent(e, 'soraBridgeApp', 'Burned')) return false; const [networkIdCodec, assetIdCodec, senderCodec, recipientCodec, amountCodec] = e.event.data; @@ -193,8 +220,8 @@ export const isSoraBridgeAppBurned = ( const amount = amountCodec.toString(); // address check - if (subBridgeApi.formatAddress(sender) !== subBridgeApi.formatAddress(from)) return false; - if (subBridgeApi.formatAddress(recipient) !== subBridgeApi.formatAddress(to)) return false; + if (chainApi.formatAddress(sender) !== chainApi.formatAddress(from)) return false; + if (chainApi.formatAddress(recipient) !== chainApi.formatAddress(to)) return false; // asset check if (assetId !== asset.externalAddress) return false; // amount check diff --git a/src/utils/ethers-util.ts b/src/utils/ethers-util.ts index 4de377749..090f1f356 100644 --- a/src/utils/ethers-util.ts +++ b/src/utils/ethers-util.ts @@ -160,9 +160,15 @@ async function getAccount(): Promise { return signer.getAddress(); } -async function getTokenContract(tokenAddress: string): Promise { +async function getContract(contractAddress: string, contractAbi: ethers.InterfaceAbi): Promise { const signer = await getSigner(); - const contract = new ethers.Contract(tokenAddress, SmartContracts[SmartContractType.ERC20].abi, signer); + const contract = new ethers.Contract(contractAddress, contractAbi, signer); + + return contract; +} + +async function getTokenContract(tokenAddress: string): Promise { + const contract = await getContract(tokenAddress, SmartContracts[SmartContractType.ERC20]); return contract; } @@ -296,7 +302,7 @@ async function addToken(address: string, symbol: string, decimals: number, image * @param chainName translated chain name */ async function switchOrAddChain(network: NetworkData, chainName?: string): Promise { - const chainId = ethers.toQuantity(network.id); + const chainId = ethers.toQuantity(network.evmId ?? network.id); try { await ethereumProvider.request({ @@ -448,6 +454,7 @@ export default { getAccount, getAccountBalance, getAccountAssetBalance, + getContract, getTokenContract, getTokenDecimals, getAllowance, diff --git a/src/views/Bridge.vue b/src/views/Bridge.vue index a563e6cde..8a01feb8e 100644 --- a/src/views/Bridge.vue +++ b/src/views/Bridge.vue @@ -68,31 +68,15 @@ /> -
- - - - -
- - {{ t('changeAccountText') }} - - - {{ t('disconnectWalletText') }} - -
-
- - {{ t('connectWalletText') }} - + :address="sender" + :name="senderName" + :tooltip="getCopyTooltip(isSoraToEvm)" + :icon="getProviderIcon(isSoraToEvm)" + @connect="connectWallet(isSoraToEvm)" + @disconnect="disconnectWallet(isSoraToEvm)" + /> -
- - - - -
- - {{ t('changeAccountText') }} - - - {{ t('disconnectWalletText') }} - -
-
- - {{ t('connectWalletText') }} - + :address="recipient" + :name="recipientName" + :tooltip="getCopyTooltip(!isSoraToEvm)" + :icon="getProviderIcon(!isSoraToEvm)" + @connect="connectWallet(!isSoraToEvm)" + @disconnect="disconnectWallet(!isSoraToEvm)" + /> @@ -692,34 +683,9 @@ export default class Bridge extends Mixins(