diff --git a/src/components/common/FormatBalance.vue b/src/components/common/FormatBalance.vue index 96f12a4e3..b604edd34 100644 --- a/src/components/common/FormatBalance.vue +++ b/src/components/common/FormatBalance.vue @@ -12,7 +12,7 @@ export default defineComponent({ components: { Balance }, props: { balance: { - type: Object as PropType | Object as PropType | undefined, + type: Object as PropType | PropType | undefined, required: true, }, }, diff --git a/src/components/dapp-staking/my-staking/MyDapps.vue b/src/components/dapp-staking/my-staking/MyDapps.vue index 621510c6f..3184e231b 100644 --- a/src/components/dapp-staking/my-staking/MyDapps.vue +++ b/src/components/dapp-staking/my-staking/MyDapps.vue @@ -76,7 +76,8 @@ @@ -108,6 +109,10 @@ export default defineComponent({ const router = useRouter(); const showModalUnbond = ref(false); + const setShowModalUnbond = (isOpen: boolean): void => { + showModalUnbond.value = isOpen; + }; + const isUnregistered = (info: MyStakeInfo): boolean => { return !info.isRegistered && info.stakersCount > 0; }; @@ -135,6 +140,7 @@ export default defineComponent({ canClaim, claimAll, isUnregistered, + setShowModalUnbond, }; }, }); diff --git a/src/components/dapp-staking/my-staking/UnbondingList.vue b/src/components/dapp-staking/my-staking/UnbondingList.vue index 8d1305090..5847aa726 100644 --- a/src/components/dapp-staking/my-staking/UnbondingList.vue +++ b/src/components/dapp-staking/my-staking/UnbondingList.vue @@ -42,7 +42,7 @@
{ + showModalWithdraw.value = isOpen; + }; + const showModalRebond = ref(false); // MEMO: since not possible to withdraw each chunk currently, use total amount of withdraw const totalAmount = ref(''); @@ -112,6 +116,7 @@ export default defineComponent({ showWithdrawDialog, showRebondDialog, withdraw, + setShowModalWithdraw, }; }, }); diff --git a/src/components/dapp-staking/my-staking/components/modals/ModalUnbondDapp.vue b/src/components/dapp-staking/my-staking/components/modals/ModalUnbondDapp.vue index cdd08fc95..f2b8a38b4 100644 --- a/src/components/dapp-staking/my-staking/components/modals/ModalUnbondDapp.vue +++ b/src/components/dapp-staking/my-staking/components/modals/ModalUnbondDapp.vue @@ -88,6 +88,10 @@ export default defineComponent({ type: Object as PropType, required: true, }, + setIsOpen: { + type: Function, + required: true, + }, }, setup(props, { emit }) { const { unbondingPeriod, handleUnbound } = useUnbound(); @@ -117,13 +121,13 @@ export default defineComponent({ const closeModal = async (): Promise => { isClosingModal.value = true; await wait(fadeDuration); - emit('update:is-open', false); + props.setIsOpen(false); isClosingModal.value = false; }; const unbound = async (): Promise => { - await handleUnbound(props.dapp?.dappAddress, amount.value); await closeModal(); + await handleUnbound(props.dapp?.dappAddress, amount.value); }; return { diff --git a/src/components/dapp-staking/my-staking/components/modals/ModalWithdraw.vue b/src/components/dapp-staking/my-staking/components/modals/ModalWithdraw.vue index 8c875ab43..a437d33cc 100644 --- a/src/components/dapp-staking/my-staking/components/modals/ModalWithdraw.vue +++ b/src/components/dapp-staking/my-staking/components/modals/ModalWithdraw.vue @@ -42,6 +42,10 @@ export default defineComponent({ type: String, default: null, }, + setIsOpen: { + type: Function, + required: true, + }, }, emits: ['update:is-open', 'confirm'], setup(props, { emit }) { @@ -49,7 +53,7 @@ export default defineComponent({ const closeModal = async (): Promise => { isClosingModal.value = true; await wait(fadeDuration); - emit('update:is-open', false); + props.setIsOpen(false); isClosingModal.value = false; }; diff --git a/src/components/dapp-staking/my-staking/items/MyDappItem.vue b/src/components/dapp-staking/my-staking/items/MyDappItem.vue index 1f0bafdcf..30831b231 100644 --- a/src/components/dapp-staking/my-staking/items/MyDappItem.vue +++ b/src/components/dapp-staking/my-staking/items/MyDappItem.vue @@ -48,7 +48,8 @@
@@ -81,6 +82,10 @@ export default defineComponent({ const router = useRouter(); const showModalUnbond = ref(false); + const setShowModalUnbond = (isOpen: boolean): void => { + showModalUnbond.value = isOpen; + }; + const isUnregistered = (info: MyStakeInfo): boolean => !info.isRegistered && info.stakersCount > 0; @@ -105,6 +110,7 @@ export default defineComponent({ claimAll, canClaim, isUnregistered, + setShowModalUnbond, }; }, }); diff --git a/src/components/dapp-staking/my-staking/items/UnbondingItem.vue b/src/components/dapp-staking/my-staking/items/UnbondingItem.vue index b59e0f0be..f6c84dd7a 100644 --- a/src/components/dapp-staking/my-staking/items/UnbondingItem.vue +++ b/src/components/dapp-staking/my-staking/items/UnbondingItem.vue @@ -31,7 +31,7 @@
{ + showModalWithdraw.value = isOpen; + }; + const showModalRebond = ref(false); // MEMO: since not possible to withdraw each chunk currently, use total amount of withdraw const totalAmount = ref(''); @@ -88,6 +92,7 @@ export default defineComponent({ showWithdrawDialog, showRebondDialog, withdraw, + setShowModalWithdraw, }; }, }); diff --git a/src/hooks/custom-signature/message.ts b/src/hooks/custom-signature/message.ts index 34cbe46cb..c84e0ab21 100644 --- a/src/hooks/custom-signature/message.ts +++ b/src/hooks/custom-signature/message.ts @@ -6,9 +6,10 @@ import { hasExtrinsicFailedEvent } from 'src/store/dapp-staking/actions'; export enum TxType { dappsStaking = 'dappsStaking', - requiredClaim = 'requiredClaim', + withdrawUnbonded = 'withdrawUnbonded', } +// @TODO: we need to clean up this later in a way that can be solved without send over the store export const displayCustomMessage = ({ txType, store, @@ -29,10 +30,11 @@ export const displayCustomMessage = ({ senderAddress, t, }); - } else if (txType === TxType.requiredClaim) { - dispatchRequiredClaimMessage({ + } else if (txType === TxType.withdrawUnbonded) { + dispatchUnbondedMessage({ result, store, + senderAddress, t, }); } @@ -49,7 +51,7 @@ const dispatchClaimMessage = ({ senderAddress: string; t: (...arg: any) => void; }): void => { - if (result.status.isFinalized) { + if (result.isCompleted) { if (!hasExtrinsicFailedEvent(result.events, store.dispatch)) { const totalClaimedStaker = calculateClaimedStaker({ events: result.events, @@ -78,35 +80,28 @@ const dispatchClaimMessage = ({ } }; -const dispatchRequiredClaimMessage = ({ +const dispatchUnbondedMessage = ({ store, result, + senderAddress, t, }: { store: Store; result: ISubmittableResult; + senderAddress: string; t: (...arg: any) => void; }): void => { - if (result.status.isFinalized) { - let errorMessage = ''; - const res = hasExtrinsicFailedEvent( - result.events, - store.dispatch, - (message: string) => (errorMessage = message) - ); - if (res) { - if (errorMessage.includes('TooManyEraStakeValues')) { - const msg = t('dappStaking.toast.requiredClaimFirst'); - - store.dispatch( - 'general/showAlertMsg', - { - msg, - alertType: 'error', - }, - { root: true } - ); - } + if (result.isCompleted) { + if (!hasExtrinsicFailedEvent(result.events, store.dispatch)) { + store.commit('dapps/setUnlockingChunks', -1); + store.dispatch( + 'general/showAlertMsg', + { + msg: t('dappStaking.toast.successfullyWithdrew'), + alertType: 'success', + }, + { root: true } + ); } } }; diff --git a/src/hooks/dapps-staking/useNominationTransfer.ts b/src/hooks/dapps-staking/useNominationTransfer.ts deleted file mode 100644 index a93559f55..000000000 --- a/src/hooks/dapps-staking/useNominationTransfer.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { - useGasPrice, - useAccount, - useCustomSignature, - useGetMinStaking, - useStakingList, - useNetworkInfo, -} from 'src/hooks'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { ethers } from 'ethers'; -import { getDappAddressEnum } from 'src/modules/dapp-staking'; -import { showError } from 'src/modules/extrinsic'; -import { useStore } from 'src/store'; -import { computed, ref, watch, watchEffect } from 'vue'; -import { TxType } from 'src/hooks/custom-signature/message'; -import { ASTAR_DECIMALS, balanceFormatter } from 'src/hooks/helper/plasmUtils'; -import { signAndSend } from 'src/hooks/helper/wallet'; -import { $api } from 'src/boot/api'; - -export function useNominationTransfer() { - const { currentAccount } = useAccount(); - const { minStaking } = useGetMinStaking(); - const { stakingList } = useStakingList(); - const store = useStore(); - const addressTransferFrom = ref(currentAccount.value); - const isEnableNominationTransfer = ref(false); - const substrateAccounts = computed(() => store.getters['general/substrateAccounts']); - const { isCustomSig, handleResult, handleCustomExtrinsic } = useCustomSignature({ - txType: TxType.requiredClaim, - }); - const { selectedTip, nativeTipPrice, setSelectedTip } = useGasPrice(); - - const setIsEnableNominationTransfer = () => { - try { - const metadata = $api!.runtimeMetadata; - const metadataJson = JSON.stringify(metadata.toJSON()); - const result = metadataJson.includes('nomination_transfer'); - isEnableNominationTransfer.value = result; - } catch (error) { - console.error(error); - isEnableNominationTransfer.value = false; - } - }; - - const setAddressTransferFrom = (address: string) => { - addressTransferFrom.value = address; - }; - - const { nativeTokenSymbol } = useNetworkInfo(); - - const formattedTransferFrom = computed(() => { - const defaultData = { text: '', item: null, isNominationTransfer: false }; - try { - const stakingListRef = stakingList.value; - if (!stakingListRef) return defaultData; - const item = stakingListRef.find((it) => it.address === addressTransferFrom.value); - if (!item) return defaultData; - - const name = item.name === currentAccount.value ? 'Transferable Balance' : item.name; - const isNominationTransfer = item.address !== currentAccount.value; - - const formattedText = `${name} (${balanceFormatter(item.balance, ASTAR_DECIMALS)})`; - return { text: formattedText, item, isNominationTransfer }; - } catch (error) { - console.error(error); - return defaultData; - } - }); - - const formattedMinStaking = computed(() => { - return Number(ethers.utils.formatEther(minStaking.value).toString()); - }); - - const isDisabledNominationTransfer = ({ - amount, - stakedAmount, - }: { - amount: number; - stakedAmount: number; - }): boolean => { - if (!formattedTransferFrom.value.item) return false; - const stakeAmount = amount + stakedAmount; - const isNotEnoughMinAmount = formattedMinStaking.value > stakeAmount; - const transferFromRef = formattedTransferFrom.value; - if (!transferFromRef) return isNotEnoughMinAmount; - - const balTransferFrom = Number( - ethers.utils.formatEther(formattedTransferFrom.value.item.balance.toString()) - ); - const isShortageFromBalance = amount > balTransferFrom; - - const result = isNotEnoughMinAmount || isShortageFromBalance; - return result; - }; - - const nominationTransfer = async ({ - amount, - targetContractId, - }: { - amount: string; - targetContractId: string; - }): Promise => { - try { - const apiRef = $api!; - const transferFromRef = formattedTransferFrom.value; - if (!transferFromRef || !formattedTransferFrom.value.item) return false; - - const value = ethers.utils.parseEther(String(amount)).toString(); - const transaction = apiRef.tx.dappsStaking.nominationTransfer( - getDappAddressEnum(formattedTransferFrom.value.item.address), - value, - getDappAddressEnum(targetContractId) - ); - - const txResHandler = async (result: ISubmittableResult): Promise => { - return await handleResult(result); - }; - - await signAndSend({ - transaction, - senderAddress: currentAccount.value, - substrateAccounts: substrateAccounts.value, - isCustomSignature: isCustomSig.value, - txResHandler, - handleCustomExtrinsic, - dispatch: store.dispatch, - tip: selectedTip.value.price, - }); - return true; - } catch (error: any) { - console.error(error); - showError(store.dispatch, error.message); - return false; - } - }; - - watchEffect(() => { - setIsEnableNominationTransfer(); - }); - - watch( - [currentAccount], - () => { - addressTransferFrom.value = currentAccount.value; - }, - { immediate: true } - ); - - return { - formattedTransferFrom, - addressTransferFrom, - currentAccount, - formattedMinStaking, - nativeTokenSymbol, - isEnableNominationTransfer, - setAddressTransferFrom, - nominationTransfer, - isDisabledNominationTransfer, - selectedTip, - nativeTipPrice, - setSelectedTip, - }; -} diff --git a/src/hooks/dapps-staking/useStakerInfo.ts b/src/hooks/dapps-staking/useStakerInfo.ts index cc8b6b46c..82108f777 100644 --- a/src/hooks/dapps-staking/useStakerInfo.ts +++ b/src/hooks/dapps-staking/useStakerInfo.ts @@ -1,14 +1,15 @@ import { BN } from 'bn.js'; -import { $api } from 'boot/api'; import { ethers } from 'ethers'; import { useAccount } from 'src/hooks'; -import { getStakeInfo } from 'src/modules/dapp-staking/utils/index'; import { useStore } from 'src/store'; import { StakeInfo } from 'src/store/dapp-staking/actions'; import { DappItem } from 'src/store/dapp-staking/state'; import { DappCombinedInfo } from 'src/v2/models/DappsStaking'; import { computed, ref, watch, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; +import { container } from 'src/v2/common'; +import { Symbols } from 'src/v2/symbols'; +import { IDappStakingService } from 'src/v2/services'; export type MyStakeInfo = StakeInfo | DappItem; @@ -26,21 +27,17 @@ export function useStakerInfo() { const dapps = computed(() => store.getters['dapps/getAllDapps']); const isH160 = computed(() => store.getters['general/isH160Formatted']); - const getData = async (address: string) => { - return await getStakeInfo({ - api: $api!, - dappAddress: address, - currentAccount: currentAccount.value, - }); - }; - const setStakeInfo = async () => { let data: StakeInfo[] = []; let myData: MyStakeInfo[] = []; + const dappStakingService = container.get(Symbols.DappStakingService); data = await Promise.all( dapps.value.map(async (it: DappCombinedInfo) => { - const stakeData = await getData(it.dapp?.address!); + const stakeData = await dappStakingService.getStakeInfo( + it.dapp?.address!, + currentAccount.value + ); if (stakeData?.hasStake) { myData.push({ ...stakeData, ...it.dapp }); } @@ -65,7 +62,7 @@ export function useStakerInfo() { }; watchEffect(async () => { - if (isLoading.value || !dapps.value) { + if (isLoading.value || !dapps.value || !currentAccount.value) { return; } try { diff --git a/src/hooks/dapps-staking/useUnbonding.ts b/src/hooks/dapps-staking/useUnbonding.ts index d7e7250d2..b6212880a 100644 --- a/src/hooks/dapps-staking/useUnbonding.ts +++ b/src/hooks/dapps-staking/useUnbonding.ts @@ -3,10 +3,8 @@ import { u32 } from '@polkadot/types'; import { ISubmittableResult } from '@polkadot/types/types'; import BN from 'bn.js'; import { $api } from 'boot/api'; -import { useCustomSignature, useGasPrice } from 'src/hooks'; -import { signAndSend } from 'src/hooks/helper/wallet'; +import { displayCustomMessage, TxType } from 'src/hooks/custom-signature/message'; import { useUnbondWithdraw } from 'src/hooks/useUnbondWithdraw'; -import { hasExtrinsicFailedEvent } from 'src/modules/extrinsic'; import { useStore } from 'src/store'; import { computed, onUnmounted, ref, watch } from 'vue'; import { container } from 'src/v2/common'; @@ -18,12 +16,6 @@ import { useI18n } from 'vue-i18n'; export function useUnbonding() { const store = useStore(); const { t } = useI18n(); - const { isCustomSig, handleCustomExtrinsic } = useCustomSignature({ - fn: () => { - store.commit('dapps/setUnlockingChunks', -1); - }, - }); - const { selectedTip } = useGasPrice(); const selectedAccountAddress = computed(() => store.getters['general/selectedAddress']); const unlockingChunksCount = computed(() => store.getters['dapps/getUnlockingChunks']); const maxUnlockingChunks = computed(() => store.getters['dapps/getMaxUnlockingChunks']); @@ -32,40 +24,30 @@ export function useUnbonding() { const canWithdraw = ref(false); const totalToWithdraw = ref(new BN(0)); const { canUnbondWithdraw } = useUnbondWithdraw($api); - const substrateAccounts = computed(() => store.getters['general/substrateAccounts']); const withdraw = async (): Promise => { try { const transaction = $api!.tx.dappsStaking.withdrawUnbonded(); - const txResHandler = (result: ISubmittableResult): Promise => { - return new Promise(async (resolve) => { - if (result.status.isFinalized) { - if (!hasExtrinsicFailedEvent(result.events, store.dispatch)) { - store.commit('dapps/setUnlockingChunks', -1); - store.dispatch('general/showAlertMsg', { - msg: t('dappStaking.toast.successfullyWithdrew'), - alertType: 'success', - txHash: result.txHash, - }); - } - store.commit('general/setLoading', false); - resolve(true); - } else { - store.commit('general/setLoading', true); - } + const finalizedCallback = (result: ISubmittableResult): void => { + displayCustomMessage({ + txType: TxType.withdrawUnbonded, + result, + senderAddress: selectedAccountAddress.value, + store, + t, }); }; - await signAndSend({ - transaction, - senderAddress: selectedAccountAddress.value, - substrateAccounts: substrateAccounts.value, - isCustomSignature: isCustomSig.value, - txResHandler, - handleCustomExtrinsic, - dispatch: store.dispatch, - tip: selectedTip.value.price, - }); + try { + const dappStakingService = container.get(Symbols.DappStakingService); + await dappStakingService.sendTx({ + senderAddress: selectedAccountAddress.value, + transaction, + finalizedCallback, + }); + } catch (error: any) { + console.error(error.message); + } } catch (error) { console.error(error); } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 724eb54dc..d6fa3ec4b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -26,7 +26,6 @@ export * from './useAppRouter'; export * from './chain/useAvgBlockTime'; export * from './dapps-staking/useStake'; export * from './dapps-staking/useUnbond'; -export * from './dapps-staking/useNominationTransfer'; export * from './dapps-staking/useStakerInfo'; export * from './dapps-staking/useStakingList'; export * from './dapps-staking/useSignPayload'; diff --git a/src/hooks/useClaimAll.ts b/src/hooks/useClaimAll.ts index f656e2b7c..b0ac28545 100644 --- a/src/hooks/useClaimAll.ts +++ b/src/hooks/useClaimAll.ts @@ -1,18 +1,17 @@ import { ISubmittableResult } from '@polkadot/types/types'; import { BN } from '@polkadot/util'; import { $api } from 'boot/api'; -import { useCurrentEra, useCustomSignature, useGasPrice, RewardDestination } from 'src/hooks'; -import { TxType } from 'src/hooks/custom-signature/message'; +import { useCurrentEra } from 'src/hooks'; +import { displayCustomMessage, TxType } from 'src/hooks/custom-signature/message'; import { ExtrinsicPayload } from 'src/hooks/helper'; import { getIndividualClaimTxs, PayloadWithWeight } from 'src/hooks/helper/claim'; -import { signAndSend } from 'src/hooks/helper/wallet'; import { useStore } from 'src/store'; -import { hasExtrinsicFailedEvent } from 'src/store/dapp-staking/actions'; import { container } from 'src/v2/common'; import { DappCombinedInfo } from 'src/v2/models/DappsStaking'; import { IDappStakingService } from 'src/v2/services'; import { Symbols } from 'src/v2/symbols'; import { computed, ref, watchEffect } from 'vue'; +import { useI18n } from 'vue-i18n'; const MAX_BATCH_WEIGHT = new BN('50000000000'); @@ -24,16 +23,11 @@ export function useClaimAll() { const isLoading = ref(true); const store = useStore(); const senderAddress = computed(() => store.getters['general/selectedAddress']); - const substrateAccounts = computed(() => store.getters['general/substrateAccounts']); const dapps = computed(() => store.getters['dapps/getAllDapps']); const isH160 = computed(() => store.getters['general/isH160Formatted']); const isSendingTx = computed(() => store.getters['general/isLoading']); - const { nativeTipPrice } = useGasPrice(); - + const { t } = useI18n(); const { era } = useCurrentEra(); - const { handleResult, handleCustomExtrinsic, isCustomSig } = useCustomSignature({ - txType: TxType.dappsStaking, - }); watchEffect(async () => { try { @@ -90,7 +84,7 @@ export function useClaimAll() { } const txsToExecute: ExtrinsicPayload[] = []; - let totalWeight: BN = new BN(0); + let totalWeight: BN = new BN('0'); for (let i = 0; i < batchTxsRef.length; i++) { const tx = batchTxsRef[i]; const weight = tx.isWeightV2 ? tx.asWeightV2().refTime.toBn() : tx.asWeightV1(); @@ -106,24 +100,22 @@ export function useClaimAll() { `Batch weight: ${totalWeight.toString()}, transactions no. ${txsToExecute.length}` ); const transaction = api.tx.utility.batch(txsToExecute); + const finalizedCallback = (result: ISubmittableResult): void => { + displayCustomMessage({ + txType: TxType.dappsStaking, + result, + senderAddress: senderAddress.value, + store, + t, + }); + }; try { - const txResHandler = async (result: ISubmittableResult): Promise => { - const res = await handleResult(result); - hasExtrinsicFailedEvent(result.events, store.dispatch); - return res; - }; - - await signAndSend({ - transaction, + const dappStakingService = container.get(Symbols.DappStakingService); + await dappStakingService.sendTx({ senderAddress: senderAddress.value, - substrateAccounts: substrateAccounts.value, - isCustomSignature: isCustomSig.value, - txResHandler, - handleCustomExtrinsic, - dispatch: store.dispatch, - tip: nativeTipPrice.value.fast, //note: this is a quick hack to speed of the tx. We should add the custom speed modal later - //tip: selectedTip.value.price, + transaction, + finalizedCallback, }); } catch (error: any) { console.error(error.message); diff --git a/src/modules/dapp-staking/utils/index.ts b/src/modules/dapp-staking/utils/index.ts index fc5c3b527..d8dd4d74b 100644 --- a/src/modules/dapp-staking/utils/index.ts +++ b/src/modules/dapp-staking/utils/index.ts @@ -10,12 +10,6 @@ import { EraStakingPoints, StakeInfo } from 'src/store/dapp-staking/actions'; import { isEthereumAddress } from '@polkadot/util-crypto'; import { isValidAddressPolkadotAddress } from 'src/hooks/helper/plasmUtils'; -interface StakeData { - address: string; - balance: string; - name: string; -} - export const checkIsLimitedProvider = (): boolean => { const limitedProvider = ['onfinality']; const selectedEndpoint = JSON.parse( diff --git a/src/v2/repositories/IDappStakingRepository.ts b/src/v2/repositories/IDappStakingRepository.ts index cfa98f4b1..5c1bf061a 100644 --- a/src/v2/repositories/IDappStakingRepository.ts +++ b/src/v2/repositories/IDappStakingRepository.ts @@ -10,6 +10,7 @@ import { import { EditDappItem } from 'src/store/dapp-staking/state'; import { u32 } from '@polkadot/types'; import { GeneralStakerInfo } from 'src/hooks/helper/claim'; +import { StakeInfo } from 'src/store/dapp-staking/actions'; /** * Definition of repository to access dapps staking pallet. @@ -100,4 +101,6 @@ export interface IDappStakingRepository { stakerAddress: string, contractAddress: string ): Promise>; + + getStakeInfo(dappAddress: string, currentAccount: string): Promise; } diff --git a/src/v2/repositories/implementations/DappStakingRepository.ts b/src/v2/repositories/implementations/DappStakingRepository.ts index 0e7c20637..2a3b674e6 100644 --- a/src/v2/repositories/implementations/DappStakingRepository.ts +++ b/src/v2/repositories/implementations/DappStakingRepository.ts @@ -1,14 +1,14 @@ -import { isValidAddressPolkadotAddress } from 'src/hooks/helper/plasmUtils'; +import { isValidAddressPolkadotAddress, balanceFormatter } from 'src/hooks/helper/plasmUtils'; import { BN } from '@polkadot/util'; import { u32, Option, Struct } from '@polkadot/types'; import { Codec, ISubmittableResult } from '@polkadot/types/types'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import { AccountId, Balance, EraIndex } from '@polkadot/types/interfaces'; +import { ApiPromise } from '@polkadot/api'; import { injectable, inject } from 'inversify'; import { IDappStakingRepository } from 'src/v2/repositories'; import { IApi } from 'src/v2/integration'; import { Symbols } from 'src/v2/symbols'; - import { RewardDestination, SmartContract, @@ -17,14 +17,17 @@ import { DappStakingConstants, } from 'src/v2/models/DappsStaking'; import { EventAggregator, NewEraMessage } from 'src/v2/messaging'; -import { GeneralStakerInfo } from 'src/hooks/helper/claim'; +import { GeneralStakerInfo, checkIsDappRegistered } from 'src/hooks/helper/claim'; import { ethers } from 'ethers'; import { EditDappItem } from 'src/store/dapp-staking/state'; +import { StakeInfo, EraStakingPoints } from 'src/store/dapp-staking/actions'; import { TOKEN_API_URL } from 'src/modules/token-api'; import axios from 'axios'; import { getDappAddressEnum } from 'src/modules/dapp-staking/utils'; import { Guard } from 'src/v2/common'; import { AccountLedger } from 'src/v2/models/DappsStaking'; +import { wait } from 'src/hooks/helper/common'; +import { checkIsLimitedProvider } from 'src/modules/dapp-staking/utils'; // TODO type generation interface EraInfo extends Struct { @@ -337,7 +340,109 @@ export class DappStakingRepository implements IDappStakingRepository { } } - private getAddressEnum(address: string) { - return { Evm: address }; + public async getStakeInfo( + dappAddress: string, + currentAccount: string + ): Promise { + const api = await this.api.getApi(); + const stakeInfo = new Promise(async (resolve) => { + const data = await this.handleGetStakeInfo({ api, dappAddress, currentAccount }); + resolve(data); + }); + const fallbackTimeout = new Promise(async (resolve) => { + const timeout = 4 * 1000; + await wait(timeout); + resolve('timeout'); + }); + + const race = Promise.race([stakeInfo, fallbackTimeout]); + const result = race.then((res) => { + if (res === 'timeout') { + return undefined; + } else { + return res as StakeInfo; + } + }); + return result; + } + + private async handleGetStakeInfo({ + api, + dappAddress, + currentAccount, + }: { + api: ApiPromise; + dappAddress: string; + currentAccount: string; + }): Promise { + const initialYourStake = { + formatted: '', + denomAmount: new BN('0'), + }; + + const stakeInfo = await this.getLatestStakePoint(api, dappAddress); + if (!stakeInfo) return undefined; + + const data = { + totalStake: balanceFormatter(stakeInfo.total.toString()), + yourStake: initialYourStake, + claimedRewards: '0', + hasStake: false, + stakersCount: Number(stakeInfo.numberOfStakers.toString()), + dappAddress, + isRegistered: true, + }; + + try { + const [stakerInfo, { isRegistered }] = await Promise.all([ + api.query.dappsStaking.generalStakerInfo( + currentAccount, + getDappAddressEnum(dappAddress) + ), + checkIsDappRegistered({ dappAddress, api }), + ]); + + const balance = stakerInfo.stakes.length && stakerInfo.stakes.slice(-1)[0].staked.toString(); + const yourStake = balance + ? { + formatted: balanceFormatter(balance), + denomAmount: new BN(balance.toString()), + } + : initialYourStake; + + return { + ...data, + hasStake: Number(balance.toString()) > 0, + yourStake, + isRegistered, + }; + } catch (error) { + return data; + } + } + + private async getLatestStakePoint( + api: ApiPromise, + contract: string + ): Promise { + if (!contract) { + return undefined; + } + const currentEra = await (await api.query.dappsStaking.currentEra()).toNumber(); + const contractAddress = getDappAddressEnum(contract); + // iterate from currentEra backwards until you find record for ContractEraStake + for (let era = currentEra; era > 0; era -= 1) { + // Memo: wait for avoiding provider limitation + checkIsLimitedProvider() && (await wait(200)); + const stakeInfoPromise = await api.query.dappsStaking.contractEraStake< + Option + >(contractAddress, era); + const stakeInfo = stakeInfoPromise.unwrapOr(undefined); + if (stakeInfo) { + return stakeInfo; + } + } + + return undefined; } } diff --git a/src/v2/services/IDappStakingService.ts b/src/v2/services/IDappStakingService.ts index 2aa9b81b3..bf5c7166f 100644 --- a/src/v2/services/IDappStakingService.ts +++ b/src/v2/services/IDappStakingService.ts @@ -1,8 +1,11 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { ISubmittableResult } from '@polkadot/types/types'; import { BN } from '@polkadot/util'; import { EditDappItem } from 'src/store/dapp-staking/state'; import { TvlModel } from 'src/v2/models'; import { DappCombinedInfo, StakerInfo } from '../models/DappsStaking'; import { AccountLedger } from '../models/DappsStaking'; +import { StakeInfo } from 'src/store/dapp-staking/actions'; /** * Definition of service used to manage dapps staking. @@ -14,7 +17,7 @@ export interface IDappStakingService { getTvl(): Promise; /** - * Stakes given ammount to contract. + * Stakes given amount to contract. * @param contractAddress Contract address. * @param stakerAddress Staked address. * @param amount Amount to stake. @@ -80,4 +83,19 @@ export interface IDappStakingService { * @param accountAddress User account address */ canClaimRewardWithoutErrors(accountAddress: string): Promise; + + /** + * claim dApp staking rewards + */ + sendTx({ + senderAddress, + transaction, + finalizedCallback, + }: { + senderAddress: string; + transaction: SubmittableExtrinsic<'promise'>; + finalizedCallback: (result: ISubmittableResult) => void; + }): Promise; + + getStakeInfo(dappAddress: string, currentAccount: string): Promise; } diff --git a/src/v2/services/implementations/DappStakingService.ts b/src/v2/services/implementations/DappStakingService.ts index 6da1e676f..db52f96d5 100644 --- a/src/v2/services/implementations/DappStakingService.ts +++ b/src/v2/services/implementations/DappStakingService.ts @@ -16,6 +16,9 @@ import { IBalanceFormatterService, IDappStakingService } from 'src/v2/services'; import { Symbols } from 'src/v2/symbols'; import { IWalletService } from '../IWalletService'; import { AccountLedger } from 'src/v2/models/DappsStaking'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { StakeInfo } from 'src/store/dapp-staking/actions'; @injectable() export class DappStakingService implements IDappStakingService { @@ -193,4 +196,34 @@ export class DappStakingService implements IDappStakingService { return true; } + + public async sendTx({ + senderAddress, + transaction, + finalizedCallback, + }: { + senderAddress: string; + transaction: SubmittableExtrinsic<'promise'>; + finalizedCallback: (result?: ISubmittableResult) => void; + }): Promise { + Guard.ThrowIfUndefined('senderAddress', senderAddress); + Guard.ThrowIfUndefined('transaction', transaction); + + await this.wallet.signAndSend( + transaction, + senderAddress, + undefined, + undefined, + finalizedCallback + ); + } + + public async getStakeInfo( + dappAddress: string, + currentAccount: string + ): Promise { + Guard.ThrowIfUndefined('currentAccount', currentAccount); + + return await this.dappStakingRepository.getStakeInfo(dappAddress, currentAccount); + } } diff --git a/src/v2/services/implementations/WalletService.ts b/src/v2/services/implementations/WalletService.ts index 89ff16bc0..70db6589f 100644 --- a/src/v2/services/implementations/WalletService.ts +++ b/src/v2/services/implementations/WalletService.ts @@ -1,7 +1,11 @@ +import { Null, Result } from '@polkadot/types-codec'; +import { DispatchError, EventRecord } from '@polkadot/types/interfaces'; import { ITuple } from '@polkadot/types/types'; -import { EventRecord, DispatchError } from '@polkadot/types/interfaces'; import { ExtrinsicStatusMessage, IEventAggregator } from 'src/v2/messaging'; -import { Null, Result } from '@polkadot/types-codec'; + +enum ErrorCode { + TooManyEraStakeValues = 'dappsStaking.TooManyEraStakeValues', +} export class WalletService { constructor(protected readonly eventAggregator: IEventAggregator) {} @@ -34,9 +38,15 @@ export class WalletService { }); if (result) { - this.eventAggregator.publish(new ExtrinsicStatusMessage(false, message)); + let msg = ''; + if (message === ErrorCode.TooManyEraStakeValues) { + msg = 'Please claim your rewards before sending transaction'; + } else { + msg = message; + } + this.eventAggregator.publish(new ExtrinsicStatusMessage(false, msg)); + throw Error(msg); } - return result; } diff --git a/src/v2/test/mocks/repositories/DappStakingRepositoryMock.ts b/src/v2/test/mocks/repositories/DappStakingRepositoryMock.ts index ae1566d7c..8fd281466 100644 --- a/src/v2/test/mocks/repositories/DappStakingRepositoryMock.ts +++ b/src/v2/test/mocks/repositories/DappStakingRepositoryMock.ts @@ -8,7 +8,7 @@ import { EditDappItem } from 'src/store/dapp-staking/state'; import { AccountLedger } from 'src/v2/models/DappsStaking'; import { u32 } from '@polkadot/types'; import { GeneralStakerInfo } from 'src/hooks/helper/claim'; - +import { StakeInfo } from 'src/store/dapp-staking/actions'; @injectable() export class DappStakingRepositoryMock implements IDappStakingRepository { public readonly bondAndStakeCallMock = jest.fn(); @@ -104,4 +104,11 @@ export class DappStakingRepositoryMock implements IDappStakingRepository { public async getApr(network: string): Promise<{ apr: number; apy: number }> { return { apr: 0, apy: 0 }; } + + public async getStakeInfo( + dappAddress: string, + currentAccount: string + ): Promise { + return {} as StakeInfo; + } } diff --git a/src/v2/test/services/DappStakingService.spec.ts b/src/v2/test/services/DappStakingService.spec.ts index a7a53d0ec..bf85312c4 100644 --- a/src/v2/test/services/DappStakingService.spec.ts +++ b/src/v2/test/services/DappStakingService.spec.ts @@ -98,4 +98,10 @@ describe('DappStakingService.ts', () => { expect(wallet.walletSignAndSendMock).toBeCalledTimes(1); expect(wallet.walletSignAndSendMock).toBeCalledWith({}, stakerAddress, expect.any(String)); }); + + it('getStakeInfo - throws exception if invalid argument', async () => { + const sut = container.get(Symbols.DappStakingService); + + await expect(sut.getStakeInfo('', '')).rejects.toThrow('Invalid argument currentAccount'); + }); });