From 84b6489233f9374ba3e172b1c50479c77942fe09 Mon Sep 17 00:00:00 2001 From: Nikita Polyakov <53777036+Nikita-Polyakov@users.noreply.github.com> Date: Tue, 30 Mar 2021 11:31:58 +0300 Subject: [PATCH] Feature/rewards claiming (#110) * Polkaswap layout redesign (#98) * pss-498 header redesign * wip pss-496 * style fixes * add HelpDialog component * reexport package.json * exchange routing fix * add Rewards stub with coming soon text * refactoring SidebarItemContent * refactoring styles * add FAUCET_URL to env.json * fixes after review * fix sidebar item hover css * fix disabled item css * Move bridge functionality (#103) * pss-498 header redesign * wip pss-496 * style fixes * add HelpDialog component * reexport package.json * exchange routing fix * add Rewards stub with coming soon text * refactoring SidebarItemContent * refactoring styles * add FAUCET_URL to env.json * fixes after review * Move bridge functionality Co-authored-by: Nikita-Polyakov * Update wallet & api * PSS-524: Bridge (#107) * pss-498 header redesign * wip pss-496 * style fixes * add HelpDialog component * reexport package.json * exchange routing fix * add Rewards stub with coming soon text * refactoring SidebarItemContent * refactoring styles * add FAUCET_URL to env.json * fixes after review * Move bridge functionality * Bridge: Updated unauthorized routes. * Updated Generic Page Header, updated Bridge screens. * Fixed token icons. * Bridge: Updated styles. * Updated tooltips. * Removed unused token images. * Refactored due to PR comments. Co-authored-by: Nikita-Polyakov Co-authored-by: Stefan Popov * wip initial screen * token row wip * Fix/pss 539 metamsk lock issue (#108) * improve subscribers * add check account is connected * Fix/balance flickering (#109) * wip update balance flow * refactoring views and store modules * remove unused code * fixes after review * add TokensRow component * add rewards amount components * add ordinal translations * rename actions * Update yarn.lock * wip rewards states * fix types * wip rewards parsing * update states * add states flags * remove comments * sync yarn.lock * refactoring bridge to use WalletConnectMixin * css fixes * change account flow * save signature * convert address to hex * notification for no rewards * fetch network fee for claiming rewards * reset rewards for testing * add types to store * notification * disconnect process * add types for components * fixes * improve loading state * improve notification * success state ui update * fetch rewards and network fee parallel * improve error message * unsubscribe ethereum events Co-authored-by: Stefan Popov Co-authored-by: Alex Natalia <38787212+alexnatalia@users.noreply.github.com> --- package.json | 2 +- src/components/Rewards/AmountHeader.vue | 81 +++++ src/components/Rewards/AmountTable.vue | 35 ++ src/components/Rewards/GradientBox.vue | 39 +++ src/components/Rewards/TokensRow.vue | 54 +++ src/components/ToggleTextButton.vue | 49 +++ src/components/TokenLogo.vue | 1 + src/components/mixins/TransactionMixin.ts | 20 +- src/components/mixins/TranslationMixin.ts | 20 ++ src/components/mixins/WalletConnectMixin.ts | 78 +++++ src/consts/index.ts | 10 +- src/lang/en/index.ts | 55 ++- src/router/index.ts | 2 +- src/store/bridge.ts | 3 +- src/store/rewards.ts | 245 +++++++++++++ src/store/web3.ts | 10 +- src/types/rewards.ts | 10 + src/utils/index.ts | 1 - src/utils/web3-util.ts | 29 +- src/views/Bridge.vue | 101 ++---- src/views/Rewards.vue | 359 +++++++++++++++++++- yarn.lock | 60 ++-- 22 files changed, 1119 insertions(+), 145 deletions(-) create mode 100644 src/components/Rewards/AmountHeader.vue create mode 100644 src/components/Rewards/AmountTable.vue create mode 100644 src/components/Rewards/GradientBox.vue create mode 100644 src/components/Rewards/TokensRow.vue create mode 100644 src/components/ToggleTextButton.vue create mode 100644 src/components/mixins/WalletConnectMixin.ts create mode 100644 src/store/rewards.ts create mode 100644 src/types/rewards.ts diff --git a/package.json b/package.json index 868571cb2..317285066 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@metamask/detect-provider": "^1.2.0", - "@soramitsu/soraneo-wallet-web": "^0.7.9", + "@soramitsu/soraneo-wallet-web": "^0.7.12", "@walletconnect/web3-provider": "^1.3.3", "axios": "^0.19.2", "core-js": "^3.6.4", diff --git a/src/components/Rewards/AmountHeader.vue b/src/components/Rewards/AmountHeader.vue new file mode 100644 index 000000000..94caba53f --- /dev/null +++ b/src/components/Rewards/AmountHeader.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/Rewards/AmountTable.vue b/src/components/Rewards/AmountTable.vue new file mode 100644 index 000000000..545de8235 --- /dev/null +++ b/src/components/Rewards/AmountTable.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/components/Rewards/GradientBox.vue b/src/components/Rewards/GradientBox.vue new file mode 100644 index 000000000..073667386 --- /dev/null +++ b/src/components/Rewards/GradientBox.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/components/Rewards/TokensRow.vue b/src/components/Rewards/TokensRow.vue new file mode 100644 index 000000000..2567baa4a --- /dev/null +++ b/src/components/Rewards/TokensRow.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/components/ToggleTextButton.vue b/src/components/ToggleTextButton.vue new file mode 100644 index 000000000..439ecc304 --- /dev/null +++ b/src/components/ToggleTextButton.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/TokenLogo.vue b/src/components/TokenLogo.vue index 36e1f0ef1..d9c18d17e 100644 --- a/src/components/TokenLogo.vue +++ b/src/components/TokenLogo.vue @@ -61,4 +61,5 @@ $token-background-color: var(--s-color-base-on-accent); @include element-size('token-logo--mini', 16px); @include element-size('token-logo--small', 24px); @include element-size('token-logo--medium'); +@include element-size('token-logo--large', 80px); diff --git a/src/components/mixins/TransactionMixin.ts b/src/components/mixins/TransactionMixin.ts index 87511e9e3..730503bae 100644 --- a/src/components/mixins/TransactionMixin.ts +++ b/src/components/mixins/TransactionMixin.ts @@ -1,10 +1,10 @@ import { Component, Mixins } from 'vue-property-decorator' -import { History, TransactionStatus, Operation } from '@sora-substrate/util' +import { History, TransactionStatus, Operation, RewardInfo } from '@sora-substrate/util' import { api } from '@soramitsu/soraneo-wallet-web' import findLast from 'lodash/fp/findLast' import { Action } from 'vuex-class' -import { formatAddress } from '@/utils' +import { formatAddress, delay } from '@/utils' import TranslationMixin from './TranslationMixin' import LoadingMixin from './LoadingMixin' import NumberFormatterMixin from './NumberFormatterMixin' @@ -32,17 +32,29 @@ export default class TransactionMixin extends Mixins(TranslationMixin, LoadingMi if ([Operation.AddLiquidity, Operation.CreatePair, Operation.Swap].includes(value.type)) { params.amount2 = params.amount2 ? this.formatStringValue(params.amount2) : '' } + if (value.type === Operation.ClaimRewards) { + params.rewards = params.rewards.reduce((result, item: RewardInfo) => { + if (+item.amount === 0) return result + + const amount = this.getStringFromCodec(item.amount, item.asset.decimals) + const itemString = `${amount} ${item.asset.symbol}` + + result.push(itemString) + + return result + }, []).join(` ${this.t('rewards.andText')} `) + } return this.t(`operations.${value.status}.${value.type}`, params) } private async getLastTransaction (): Promise { // Now we are checking every transaction with 1 second interval const tx = findLast( - item => Math.abs(Number(item.startTime) - this.time) < 1000, + item => Number(item.startTime) > this.time, api.accountHistory ) if (!tx) { - await new Promise(resolve => setTimeout(resolve, 50)) + await delay() return await this.getLastTransaction() } this.transaction = tx diff --git a/src/components/mixins/TranslationMixin.ts b/src/components/mixins/TranslationMixin.ts index 6f404e794..2bf09b43b 100644 --- a/src/components/mixins/TranslationMixin.ts +++ b/src/components/mixins/TranslationMixin.ts @@ -1,5 +1,21 @@ import { Vue, Component } from 'vue-property-decorator' +const OrdinalRules = { + en: (v) => { + const n = +v + + if (!Number.isFinite(n) || n === 0) return v + + const remainder = n % 10 + + if (remainder === 1) return `${n}st` + if (remainder === 2) return `${n}nd` + if (remainder === 3) return `${n}rd` + + return `${n}th` + } +} + @Component export default class TranslationMixin extends Vue { t (key: string, values?: any): string { @@ -13,4 +29,8 @@ export default class TranslationMixin extends Vue { te (key: string): boolean { return this.$root.$te(key) } + + tOrdinal (n) { + return OrdinalRules[this.$i18n.locale]?.(n) ?? n + } } diff --git a/src/components/mixins/WalletConnectMixin.ts b/src/components/mixins/WalletConnectMixin.ts new file mode 100644 index 000000000..09960a631 --- /dev/null +++ b/src/components/mixins/WalletConnectMixin.ts @@ -0,0 +1,78 @@ +import { Component, Mixins } from 'vue-property-decorator' +import { Action, Getter, State } from 'vuex-class' + +import router from '@/router' +import { isWalletConnected, getWalletAddress, formatAddress } from '@/utils' +import { PageNames } from '@/consts' +import web3Util, { Provider } from '@/utils/web3-util' + +import TranslationMixin from '@/components/mixins/TranslationMixin' + +@Component +export default class WalletConnectMixin extends Mixins(TranslationMixin) { + @State(state => state.web3.ethAddress) ethAddress!: string + + @Getter('isExternalAccountConnected', { namespace: 'web3' }) isExternalAccountConnected!: boolean + + @Action('setEthNetwork', { namespace: 'web3' }) setEthNetwork!: (network?: string) => Promise + @Action('connectExternalAccount', { namespace: 'web3' }) connectExternalAccount!: (options) => Promise + @Action('switchExternalAccount', { namespace: 'web3' }) switchExternalAccount!: (options) => Promise + @Action('disconnectExternalAccount', { namespace: 'web3' }) disconnectExternalAccount!: () => Promise + + getWalletAddress = getWalletAddress + formatAddress = formatAddress + + isExternalWalletConnecting = false + + get isSoraAccountConnected (): boolean { + return isWalletConnected() + } + + get areNetworksConnected (): boolean { + return this.isSoraAccountConnected && this.isExternalAccountConnected + } + + connectInternalWallet (): void { + router.push({ name: PageNames.Wallet }) + } + + async connectExternalWallet (): Promise { + // For now it's only Metamask + if (this.isExternalWalletConnecting) { + return + } + this.isExternalWalletConnecting = true + try { + await this.connectExternalAccount({ provider: Provider.Metamask }) + } catch (error) { + const provider = this.t(error.message) + this.$alert(this.t('walletProviderConnectionError', { provider })) + } finally { + this.isExternalWalletConnecting = false + } + } + + // TODO: Check why we can't choose another account + async changeExternalWallet (options?: any): Promise { + // For now it's only Metamask + if (this.isExternalWalletConnecting) { + return + } + this.isExternalWalletConnecting = true + try { + await this.switchExternalAccount(options) + } catch (error) { + console.error(error) + } finally { + this.isExternalWalletConnecting = false + } + } + + // TODO: remove this check, when MetaMask issue will be resolved + // https://github.com/MetaMask/metamask-extension/issues/10368 + async checkExternalAccountIsConnected (): Promise { + const account = await web3Util.getAccount() + + return !!account && account.toLowerCase() === this.ethAddress.toLowerCase() + } +} diff --git a/src/consts/index.ts b/src/consts/index.ts index 06016fa68..8e736d751 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -49,7 +49,12 @@ export enum Components { SelectRegisteredAsset = 'SelectRegisteredAsset', ConfirmBridgeTransactionDialog = 'ConfirmBridgeTransactionDialog', BridgeTransaction = 'BridgeTransaction', - BridgeTransactionsHistory = 'BridgeTransactionsHistory' + BridgeTransactionsHistory = 'BridgeTransactionsHistory', + ToggleTextButton = 'ToggleTextButton', + GradientBox = 'Rewards/GradientBox', + TokensRow = 'Rewards/TokensRow', + RewardsAmountHeader = 'Rewards/AmountHeader', + RewardsAmountTable = 'Rewards/AmountTable' } interface SidebarMenuItem { @@ -147,7 +152,8 @@ export const AboutTopics = [ export enum LogoSize { MINI = 'mini', SMALL = 'small', - MEDIUM = 'medium' + MEDIUM = 'medium', + LARGE = 'large' } export enum InfoTooltipPosition { diff --git a/src/lang/en/index.ts b/src/lang/en/index.ts index 40d20b223..06c8b6b13 100644 --- a/src/lang/en/index.ts +++ b/src/lang/en/index.ts @@ -11,12 +11,16 @@ export default { transactionSubmittedText: 'Transaction was submitted', unknownErrorText: 'ERROR Something went wrong...', connectWalletText: 'Connect account', + changeWalletText: 'Change wallet', + connectedText: 'Connected', connectWalletTextTooltip: 'Connect to SORA Network with polkadot{.js}', + walletProviderConnectionError: '{provider} is not found. Please install it!', bridgeText: 'Bridge', comingSoonText: 'Coming Soon', poweredBy: 'Powered by', confirmText: 'Confirm', confirmTransactionText: 'Confirm transaction in {direction}', + retryText: 'Retry', assetNames: { [KnownSymbols.XOR]: 'SORA', [KnownSymbols.VAL]: 'SORA Validator Token', @@ -82,7 +86,8 @@ export default { [Operation.AddLiquidity]: 'Supplied {amount} {symbol} and {amount2} {symbol2}', [Operation.RemoveLiquidity]: 'Removed {amount} {symbol} and {amount2} {symbol2}', [Operation.CreatePair]: 'Supplied {amount} {symbol} and {amount2} {symbol2}', - [Operation.RegisterAsset]: 'Registered {symbol} asset' + [Operation.RegisterAsset]: 'Registered {symbol} asset', + [Operation.ClaimRewards]: 'Reward claimed successfully {rewards}' }, [TransactionStatus.Error]: { [Operation.Transfer]: 'Failed to send {amount} {symbol} to {address}', @@ -90,7 +95,8 @@ export default { [Operation.AddLiquidity]: 'Failed to supply {amount} {symbol} and {amount2} {symbol2}', [Operation.RemoveLiquidity]: 'Failed to remove {amount} {symbol} and {amount2} {symbol2}', [Operation.CreatePair]: 'Failed to supply {amount} {symbol} and {amount2} {symbol2}', - [Operation.RegisterAsset]: 'Failed to register {symbol} asset' + [Operation.RegisterAsset]: 'Failed to register {symbol} asset', + [Operation.ClaimRewards]: 'Failed to claim rewards {rewards}' } }, pageNotFound: { @@ -183,8 +189,8 @@ export default { info: 'Convert your tokens from SORA Network to Ethereum Network and vice versa.', balance: 'Balance', connectWallet: '@:connectWalletText', - connected: 'Connected', - changeWallet: 'Change wallet', + connected: '@:connectedText', + changeWallet: '@:changeWalletText', changeNetwork: '@:changeNetworkText', next: 'Next', connectWallets: 'Connect wallets to view respective transaction history.', @@ -195,8 +201,7 @@ export default { viewHistory: 'View transactions history', transactionSubmitted: 'Transaction submitted', transactionMessage: '{assetA} for {assetB}', - notRegisteredAsset: 'Asset {assetSymbol} is not registered', - walletProviderConnectionError: '{provider} is not found. Please install it!' + notRegisteredAsset: 'Asset {assetSymbol} is not registered' }, selectRegisteredAsset: { title: 'Select a token', @@ -237,7 +242,7 @@ export default { }, status: { pending: '{step} transactions pending...', - failed: '{step} transactions failed. Retry.', + failed: '{step} transactions failed. @:retryText.', confirm: 'Confirm 2nd of 2 transactions...', complete: 'Complete', convertionComplete: 'Conversion complete' @@ -265,7 +270,7 @@ export default { ethereum: '@:ethereumText', sora: '@:soraText', pending: '{network} transaction pending...', - retry: 'Retry', + retry: '@:retryText', metamask: '@:metamask', confirm: '@:confirmTransactionText', newTransaction: 'Create new transaction', @@ -350,5 +355,39 @@ export default { resultDialog: { title: 'Transaction submitted', ok: 'OK' + }, + rewards: { + title: 'Claim Rewards', + changeWallet: '@:changeWalletText', + connected: '@:connectedText', + networkFee: '@:soraText network fee', + andText: 'and', + claiming: { + pending: 'Claiming...', + success: 'Claimed successfully' + }, + transactions: { + confimation: 'Confirm {order} of {total} transactions...', + success: 'Your will receive your rewards shortly', + failed: '{order} of {total} transactions failed. @:retryText' + }, + hint: { + connectAccounts: 'To claim your PSWAP and VAL rewards you need to connect both your @:soraText and @:ethereumText accounts.', + connectAnotherAccount: 'Connect another @:ethereumText account to check for available PSWAP and VAL rewards.', + howToClaimRewards: 'To claim your PSWAP and VAL rewards you need to sign 2 transactions in your @:soraText and @:ethereumText accounts respectively. Rewards will be deposited to your @:soraText account.' + }, + action: { + connectWallet: '@:connectWalletText', + connectExternalWallet: 'Connect @:ethereumText account', + signAndClaim: 'Sign and claim', + pendingInternal: '@:soraText transaction pending...', + pendingExternal: '@:ethereumText transaction pending...', + retry: '@:retryText', + checkRewards: 'Check', + insufficientBalance: 'Insufficient XOR balance' + }, + notification: { + empty: 'No available claims for this account' + } } } diff --git a/src/router/index.ts b/src/router/index.ts index c8a1fa6ca..b55095062 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -105,7 +105,7 @@ const router = new VueRouter({ router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { - if (BridgeChildPages.includes(to.name as PageNames) && isWalletConnected() && !store.getters['web3/isEthAccountConnected']) { + if (BridgeChildPages.includes(to.name as PageNames) && isWalletConnected() && !store.getters['web3/isExternalAccountConnected']) { next({ name: PageNames.Bridge }) return } diff --git a/src/store/bridge.ts b/src/store/bridge.ts index 29b81c25a..691a3b786 100644 --- a/src/store/bridge.ts +++ b/src/store/bridge.ts @@ -5,7 +5,6 @@ import flow from 'lodash/fp/flow' import concat from 'lodash/fp/concat' import { FPNumber, BridgeApprovedRequest, BridgeCurrencyType, BridgeTxStatus, BridgeRequest, Operation, BridgeHistory, TransactionStatus, KnownAssets } from '@sora-substrate/util' import { api } from '@soramitsu/soraneo-wallet-web' -import { decodeAddress } from '@polkadot/util-crypto' import { STATES } from '@/utils/fsm' import web3Util, { ABI, KnownBridgeAsset, OtherContractType } from '@/utils/web3-util' @@ -579,7 +578,7 @@ const actions = { await web3.eth.getTransactionReceipt(tx.transactionHash) } const soraAccountAddress = rootGetters.account.address - const accountId = web3.utils.bytesToHex(Array.from(decodeAddress(soraAccountAddress).values())) + const accountId = await web3Util.accountAddressToHex(soraAccountAddress) contractInstance = new web3.eth.Contract(contract[OtherContractType.Bridge].abi) contractInstance.options.address = contractAddress.MASTER const methodArgs = [ diff --git a/src/store/rewards.ts b/src/store/rewards.ts new file mode 100644 index 000000000..d06756f1a --- /dev/null +++ b/src/store/rewards.ts @@ -0,0 +1,245 @@ +import map from 'lodash/fp/map' +import flatMap from 'lodash/fp/flatMap' +import fromPairs from 'lodash/fp/fromPairs' +import flow from 'lodash/fp/flow' +import concat from 'lodash/fp/concat' +import { api } from '@soramitsu/soraneo-wallet-web' +import { FPNumber, KnownSymbols, KnownAssets, RewardInfo, CodecString, AccountAsset } from '@sora-substrate/util' +import web3Util from '@/utils/web3-util' +import { RewardsAmountHeaderItem } from '@/types/rewards' + +const types = flow( + flatMap(x => [x + '_REQUEST', x + '_SUCCESS', x + '_FAILURE']), + concat([ + 'RESET', + 'SET_TRANSACTION_STEP', + 'SET_TRANSACTION_ERROR', + 'SET_REWARDS_CLAIMING', + 'SET_REWARDS_RECIEVED', + 'SET_SIGNATURE' + ]), + map(x => [x, x]), + fromPairs +)([ + 'GET_REWARDS', + 'GET_FEE' +]) + +interface RewardsState { + fee: CodecString; + rewards: Array; + rewardsFetching: boolean; + rewardsClaiming: boolean; + rewardsRecieved: boolean; + transactionError: boolean; + transactionStep: number; + transactionStepsCount: number; + signature: string; +} + +function initialState (): RewardsState { + return { + fee: '', + rewards: [], + rewardsFetching: false, + rewardsClaiming: false, + rewardsRecieved: false, + transactionError: false, + transactionStep: 1, + transactionStepsCount: 2, + signature: '' + } +} + +const state = initialState() + +const getters = { + tokenXOR (state, getters, rootState, rootGetters): AccountAsset { + const token = KnownAssets.get(KnownSymbols.XOR) + + return rootGetters['assets/getAssetDataByAddress'](token?.address) + }, + claimableRewards (state: RewardsState): Array { + return state.rewards.reduce((claimableList: Array, item: RewardInfo) => { + if (FPNumber.fromCodecValue(item.amount, item.asset.decimals).isZero()) return claimableList + + claimableList.push(item) + + return claimableList + }, []) + }, + rewardsFetched (state): boolean { + return state.rewards.length !== 0 + }, + rewardsAvailable (state, getters): boolean { + return getters.claimableRewards.length !== 0 + }, + rewardsByAssetsList (state, getters): Array { + if (!getters.rewardsAvailable) { + return [ + { + symbol: KnownSymbols.PSWAP, + amount: '' + } as RewardsAmountHeaderItem, + { + symbol: KnownSymbols.VAL, + amount: '' + } as RewardsAmountHeaderItem + ] + } + + const rewardsHash = getters.claimableRewards.reduce((result, { asset, amount }: RewardInfo) => { + const { address, decimals } = asset + const current = result[address] || new FPNumber(0, decimals) + const addValue = FPNumber.fromCodecValue(amount, decimals) + + result[address] = current.add(addValue) + + return result + }, {}) + + return Object.entries(rewardsHash).reduce((total: Array, [address, amount]) => { + if ((amount as FPNumber).isZero()) return total + + const item = { + symbol: KnownAssets.get(address).symbol, + amount: (amount as FPNumber).format() + } as RewardsAmountHeaderItem + + total.push(item) + + return total + }, []) + } +} + +const mutations = { + [types.RESET] (state: RewardsState) { + const s = initialState() + + Object.keys(s).forEach(key => { + state[key] = s[key] + }) + }, + + [types.SET_TRANSACTION_STEP] (state: RewardsState, transactionStep: number) { + state.transactionStep = transactionStep + }, + [types.SET_REWARDS_CLAIMING] (state: RewardsState, flag: boolean) { + state.rewardsClaiming = flag + }, + [types.SET_TRANSACTION_ERROR] (state: RewardsState, flag: boolean) { + state.transactionError = flag + }, + [types.SET_REWARDS_RECIEVED] (state: RewardsState, flag: boolean) { + state.rewardsRecieved = flag + }, + [types.SET_SIGNATURE] (state: RewardsState, signature: string) { + state.signature = signature + }, + + [types.GET_REWARDS_REQUEST] (state: RewardsState) { + state.rewards = [] + state.rewardsFetching = true + }, + [types.GET_REWARDS_SUCCESS] (state: RewardsState, rewards) { + state.rewards = rewards + state.rewardsFetching = false + }, + [types.GET_REWARDS_FAILURE] (state: RewardsState) { + state.rewards = [] + state.rewardsFetching = false + }, + + [types.GET_FEE_REQUEST] (state: RewardsState) {}, + [types.GET_FEE_SUCCESS] (state: RewardsState, fee: CodecString) { + state.fee = fee + }, + [types.GET_FEE_FAILURE] (state: RewardsState) {} +} + +const actions = { + reset ({ commit }) { + commit(types.RESET) + }, + + setTransactionStep ({ commit }, transactionStep: number) { + commit(types.SET_TRANSACTION_STEP, transactionStep) + }, + + async getNetworkFee ({ commit }) { + commit(types.GET_FEE_REQUEST) + try { + const fee = await api.getClaimRewardsNetworkFee() + commit(types.GET_FEE_SUCCESS, fee) + } catch (error) { + console.error(error) + commit(types.GET_FEE_FAILURE, error) + } + }, + + async getRewards ({ commit, state }, address) { + commit(types.GET_REWARDS_REQUEST) + try { + const rewards = await api.checkExternalAccountRewards(address) + + commit(types.GET_REWARDS_SUCCESS, rewards) + } catch (error) { + console.error(error) + commit(types.GET_REWARDS_FAILURE) + } + + return state.rewards + }, + + async claimRewards ( + { commit, state, rootGetters }: { commit: any; state: RewardsState; rootGetters: any }, + { internalAddress = '', externalAddress = '' } = {} + ) { + if (!internalAddress || !externalAddress) return + + try { + const web3 = await web3Util.getInstance() + + commit(types.SET_REWARDS_CLAIMING, true) + commit(types.SET_TRANSACTION_ERROR, false) + + if (state.transactionStep === 1) { + const internalAddressHex = await web3Util.accountAddressToHex(internalAddress) + const message = web3.utils.sha3(internalAddressHex) as string + + const signature = await web3.eth.personal.sign(message, externalAddress, '') + + commit(types.SET_SIGNATURE, signature) + commit(types.SET_TRANSACTION_STEP, 2) + } + if (state.transactionStep === 2 && state.signature) { + await api.claimRewards( + state.signature, + externalAddress, + state.fee, + state.rewards + ) + + // update ui to success state if user not changed external account + if (rootGetters['web3/ethAddress'] === externalAddress) { + commit(types.SET_TRANSACTION_STEP, 1) + commit(types.SET_REWARDS_RECIEVED, true) + commit(types.SET_REWARDS_CLAIMING, false) + } + } + } catch (error) { + commit(types.SET_TRANSACTION_ERROR, true) + commit(types.SET_REWARDS_CLAIMING, false) + throw error + } + } +} + +export default { + namespaced: true, + state, + getters, + mutations, + actions +} diff --git a/src/store/web3.ts b/src/store/web3.ts index 21eb657c8..3a1d3a9da 100644 --- a/src/store/web3.ts +++ b/src/store/web3.ts @@ -70,8 +70,8 @@ const getters = { addressOTHER (state) { return state.contractAddress.OTHER }, - isEthAccountConnected (state) { - return !(state.ethAddress === '' || state.ethAddress === 'undefined') + isExternalAccountConnected (state) { + return !!state.ethAddress && state.ethAddress !== 'undefined' }, ethAddress (state) { return state.ethAddress @@ -167,7 +167,7 @@ const mutations = { } const actions = { - async connectEthWallet ({ commit, getters, dispatch }, { provider }) { + async connectExternalAccount ({ commit, getters, dispatch }, { provider }) { commit(types.CONNECT_ETH_WALLET_REQUEST) try { const address = await web3Util.onConnect({ provider }) @@ -182,7 +182,7 @@ const actions = { } }, - async switchEthAccount ({ commit, dispatch }, { address }) { + async switchExternalAccount ({ commit, dispatch }, { address = '' } = {}) { commit(types.SWITCH_ETH_WALLET_REQUEST) try { web3Util.removeEthUserAddress() @@ -219,7 +219,7 @@ const actions = { } }, - async disconnectEthWallet ({ commit }) { + async disconnectExternalAccount ({ commit }) { commit(types.DISCONNECT_ETH_WALLET_REQUEST) try { web3Util.removeEthUserAddress() diff --git a/src/types/rewards.ts b/src/types/rewards.ts new file mode 100644 index 000000000..527f954eb --- /dev/null +++ b/src/types/rewards.ts @@ -0,0 +1,10 @@ +import { KnownSymbols } from '@sora-substrate/util' + +export interface RewardsAmountHeaderItem { + amount: string; + symbol: KnownSymbols; +} + +export interface RewardsAmountTableItem extends RewardsAmountHeaderItem { + title?: string; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 2cc45c956..6f0002368 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,4 @@ import { Asset, AccountAsset, RegisteredAccountAsset, KnownSymbols, FPNumber, CodecString, KnownAssets } from '@sora-substrate/util' - import storage from './storage' export const copyToClipboard = async (text: string): Promise => { diff --git a/src/utils/web3-util.ts b/src/utils/web3-util.ts index fe0710a87..f14da3cfd 100644 --- a/src/utils/web3-util.ts +++ b/src/utils/web3-util.ts @@ -2,6 +2,7 @@ import Web3 from 'web3' import { AbiItem } from 'web3-utils' import WalletConnectProvider from '@walletconnect/web3-provider' import detectEthereumProvider from '@metamask/detect-provider' +import { decodeAddress } from '@polkadot/util-crypto' import axios from '@/api' import storage from './storage' @@ -165,7 +166,11 @@ async function onConnectWallet (url = 'https://cloudflare-eth.com'): Promise { try { const web3Instance = await getInstance() - const accounts = await web3Instance.eth.getAccounts() + let accounts = await web3Instance.eth.getAccounts() + + if (!Array.isArray(accounts) || !accounts.length) { + accounts = await web3Instance.eth.requestAccounts() + } return accounts.length ? accounts[0] : '' } catch (error) { @@ -188,18 +193,21 @@ async function watchEthereum (cb: { onAccountChange: Function; onNetworkChange: Function; onDisconnect: Function; -}) { +}): Promise { + await getInstance() + const ethereum = (window as any).ethereum if (ethereum) { ethereum.on('accountsChanged', cb.onAccountChange) ethereum.on('chainChanged', cb.onNetworkChange) + ethereum.on('disconnect', cb.onDisconnect) } - await getInstance() - - if (provider) { - provider.on('disconnect', cb.onDisconnect) + return function disconnect () { + if (ethereum) { + ethereum.removeAllListeners() + } } } @@ -253,6 +261,12 @@ function getInfoFromContract (contract: JsonContract): InfoContract { } } +async function accountAddressToHex (address: string): Promise { + const web3 = await getInstance() + + return web3.utils.bytesToHex(Array.from(decodeAddress(address).values())) +} + async function executeContractMethod ({ contractInfo, contractAddress, @@ -293,5 +307,6 @@ export default { watchEthereum, readSmartContract, getInfoFromContract, - executeContractMethod + executeContractMethod, + accountAddressToHex } diff --git a/src/views/Bridge.vue b/src/views/Bridge.vue index 07505ca8b..ee3d04a9c 100644 --- a/src/views/Bridge.vue +++ b/src/views/Bridge.vue @@ -179,9 +179,10 @@ diff --git a/yarn.lock b/yarn.lock index e84c1a0af..ac7f97d23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2498,39 +2498,39 @@ resolved "https://registry.yarnpkg.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87" integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w== -"@sora-substrate/api-derive@^0.6.12": - version "0.6.12" - resolved "https://registry.yarnpkg.com/@sora-substrate/api-derive/-/api-derive-0.6.12.tgz#e947466cf64693332e1926a4390663b46ad6ba91" - integrity sha512-PfG+n9tCVLq+g2k6EtJVGsROzL2vsVkpuTkGBFvuLdfhPjiwa3b367dUx6F8Ru3oe21trZQ9WNYqcYUG8bKsXw== +"@sora-substrate/api-derive@^0.6.16": + version "0.6.16" + resolved "https://registry.yarnpkg.com/@sora-substrate/api-derive/-/api-derive-0.6.16.tgz#c3666ac5fd94cf24e59d66fd5e5f00dc80b6a79d" + integrity sha512-Mn4TZqmQSPvRl+tjeDL+TwRg3gzYEvq8EAbwOzql7OeMtVNupXk6JVIKx7jd1+UxXGp30B0gnbhyhvdbuSxvrA== dependencies: "@babel/runtime" "^7.10.2" "@polkadot/api-derive" "^4.1.1" - "@sora-substrate/types" "^0.6.12" + "@sora-substrate/types" "^0.6.16" rxjs "^6.5.5" -"@sora-substrate/api@^0.6.12": - version "0.6.12" - resolved "https://registry.yarnpkg.com/@sora-substrate/api/-/api-0.6.12.tgz#4fdedb7264b4ae7b9c7b04385d085201b13f1e8d" - integrity sha512-ocswy/aYf/WMKihefRYtwASuwfx4dMiOBMOgNGn414XK+tHHKcyZzpkQ+6XwHQeSDM/3SRm8r8nqU10E3vyaag== +"@sora-substrate/api@^0.6.16": + version "0.6.16" + resolved "https://registry.yarnpkg.com/@sora-substrate/api/-/api-0.6.16.tgz#83e02394c8870ff14c4a635ecfabcd53471dd698" + integrity sha512-5KxQrltWu0+ncsEdNU34lcZeJdOxpdx2qcwkk8KpVUpO/9YJhDTPiIywiu8goocsdn8Wys/uEBi4ANRHi15elA== dependencies: "@babel/runtime" "^7.10.2" "@open-web3/orml-api-derive" "^0.8.2-9" "@polkadot/api" "^4.1.1" "@polkadot/rpc-core" "^4.1.1" - "@sora-substrate/api-derive" "^0.6.12" - "@sora-substrate/types" "^0.6.12" + "@sora-substrate/api-derive" "^0.6.16" + "@sora-substrate/types" "^0.6.16" -"@sora-substrate/type-definitions@^0.6.12": - version "0.6.12" - resolved "https://registry.yarnpkg.com/@sora-substrate/type-definitions/-/type-definitions-0.6.12.tgz#f7eef7eb26c091e35d2629a9f99fa8d97ce01424" - integrity sha512-ZhF5+ghB2s2uvvjDxlCjjJOTB/d7R7gXFVi01VhDWW0xC0Tdz9ccWpn6o5fuFq7Y4b0NVCL8NOokzFQJBPgOaA== +"@sora-substrate/type-definitions@^0.6.16": + version "0.6.16" + resolved "https://registry.yarnpkg.com/@sora-substrate/type-definitions/-/type-definitions-0.6.16.tgz#326bd202b2d2e05ecdd4f4f820850babfbf51ff6" + integrity sha512-LUg3IXOAv2o1hoCAHJOfwuuBDsu38CDsg7y8vg7CxFqCqBaO6GrVoiEH7R3K3D8sCYP0BYhOCk6WWmm5O+qBkA== dependencies: "@open-web3/orml-type-definitions" "^0.8.2-9" -"@sora-substrate/types@^0.6.12": - version "0.6.12" - resolved "https://registry.yarnpkg.com/@sora-substrate/types/-/types-0.6.12.tgz#5b438c71380ece071162dc1f1f5a619a305b53d0" - integrity sha512-nFd90R6tXRIrv/BW9/aChKjn+f79HDIAtlBtKEeWKNsWvCRsuWMYgzvmp5+8yR0tari4s5IXyAXTGrxJszgXyw== +"@sora-substrate/types@^0.6.16": + version "0.6.16" + resolved "https://registry.yarnpkg.com/@sora-substrate/types/-/types-0.6.16.tgz#569b0f33b97655efa18129e91020a00d7314fb92" + integrity sha512-LGwDeXryUP1uSoUjlFn8mmmUKpvIOxKMOJNhgpqcunPrUlyghm+fydb+bPAqq32B/sqLtmJo03c7Tzf/jYFN1A== dependencies: "@babel/runtime" "^7.10.2" "@open-web3/api-mobx" "^0.8.2-9" @@ -2538,15 +2538,15 @@ "@polkadot/api" "^4.1.1" "@polkadot/typegen" "^4.1.1" "@polkadot/types" "^4.1.1" - "@sora-substrate/type-definitions" "^0.6.12" + "@sora-substrate/type-definitions" "^0.6.16" -"@sora-substrate/util@^0.6.12": - version "0.6.12" - resolved "https://registry.yarnpkg.com/@sora-substrate/util/-/util-0.6.12.tgz#ff0726e28c776431bbeab66646779696d67ddf10" - integrity sha512-pkKFC3qTGrkJCnmJ+UOfd9oMBqivhewR6EOXbNkH3U3MZsfSuNwh/UGEPrvbubftjfWipLXvwX/i3cu4H/1ktA== +"@sora-substrate/util@^0.6.16": + version "0.6.16" + resolved "https://registry.yarnpkg.com/@sora-substrate/util/-/util-0.6.16.tgz#43ca618604d29dd7816f13df3ee4b3e50b037c6b" + integrity sha512-ODngFnGO4kAom4FWDoFwKQdOm6T+6D6FYjlIf+nTHtKheXflsYFuI/kE6ZHu1yKVUcjhGyIZYq/XG9VnxSNFMQ== dependencies: "@polkadot/ui-keyring" "^0.61.1" - "@sora-substrate/api" "^0.6.12" + "@sora-substrate/api" "^0.6.16" bignumber.js "^9.0.1" crypto-js "^4.0.0" lodash "^4.17.15" @@ -2566,14 +2566,14 @@ vuex "^3.1.3" vuex-class "^0.3.2" -"@soramitsu/soraneo-wallet-web@^0.7.9": - version "0.7.9" - resolved "https://nexus.iroha.tech/repository/npm-group/@soramitsu/soraneo-wallet-web/-/soraneo-wallet-web-0.7.9.tgz#48e5613b27dd1cc125d77a0974c4ad1f01422aa5" - integrity sha512-F2mEt24FvPJzzzLUBX9L5XS+yknhvRGybUqveBuMxVs3/GnCfKFOZ0sA1mzXj8xFur6HTy7ZvP+yhd8Q90K/Yg== +"@soramitsu/soraneo-wallet-web@^0.7.12": + version "0.7.12" + resolved "https://nexus.iroha.tech/repository/npm-group/@soramitsu/soraneo-wallet-web/-/soraneo-wallet-web-0.7.12.tgz#db6776850d8b081a5ba08ad94e37d9f092dcf24b" + integrity sha512-pUmGRPA7Y2ja0lqFtsJtvVf5hORRoYPLd4rJHZDNXeJO3SJyUkpMVGJSvgo7E0zBOx/z5EzJD98ySFW0Jf2QCw== dependencies: "@polkadot/extension-dapp" "^0.35.0-beta.35" "@polkadot/vue-identicon" "^0.72.1" - "@sora-substrate/util" "^0.6.12" + "@sora-substrate/util" "^0.6.16" "@soramitsu/soramitsu-js-ui" "^0.8.5" axios "^0.19.2" core-js "^3.6.4"