diff --git a/src/hooks/wallet/useLedger.ts b/src/hooks/wallet/useLedger.ts index fb34d8c7c..60c3fd475 100644 --- a/src/hooks/wallet/useLedger.ts +++ b/src/hooks/wallet/useLedger.ts @@ -78,5 +78,5 @@ export const useLedger = () => { watch([currentAccount], handleLedgerData, { immediate: true }); - return { isLedgerNanoS }; + return { isLedgerNanoS, isLedger }; }; diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts index 0cba4cd43..9d6a36483 100644 --- a/src/i18n/en-US/index.ts +++ b/src/i18n/en-US/index.ts @@ -352,13 +352,6 @@ export default { uaw: 'Unique Active Wallets', cantClaimWihtoutError: 'You cannot claim with automatic re-stake because it has been a while since you claimed your rewards. Please turn off the Auto Re-Stake feature to be able to claim. After you claim rewards you can turn on re-stake again. The UI team is working to fix this issue.', - migrationSupport: { - actionRequired: 'Action Required', - yourTokensAreLocked: - 'Your tokens are locked in dAppStaking V2. Please migrate your funds to V3 today and start participating.', - balanceFromV2: 'Balance from V2 (locked)', - migrateNow: 'Migrate Now', - }, stakePage: { backToDappList: 'Back to dApps list', whereFundsFrom: 'Where would you like to bring your funds from?', @@ -893,6 +886,17 @@ export default { 'You will loose eligibility for bonus reward at the end of current period if you unstake more than {amount} tokens.', loyalStakerWarning: 'You will loose eligibility for bonus reward at the end of current period if you unstake tokens now.', + unbondFrom: 'Unbond from {name}', + startUnbonding: 'Start unbonding', + unbondingEra: 'Unbonding takes {unbondingPeriod} eras before you can withdraw', + migrationSupport: { + actionRequired: 'Action Required', + yourTokensAreLocked: + 'Your tokens are locked in dAppStaking V2. Please unbond and withdraw your tokens. dApp Staking V3 is temporally unavailable for those Ledger Astar Native App users, please move your funds to a soft wallet or a Ledger EVM account to be able to participate in dApp staking.', + migrateNow: 'Migrate Now', + }, + ledgerNotSupported: 'Ledger native accounts are not supported for dApp staking V3 yet.', + moreInfo: 'More info', unlockFrom: 'Unlock from {name}', startUnlocking: 'Start unlocking', unlockingDay: 'Unlocking takes {unbondingPeriod} days before you can withdraw', diff --git a/src/links/index.ts b/src/links/index.ts index be502f747..05e4f1ae5 100644 --- a/src/links/index.ts +++ b/src/links/index.ts @@ -8,6 +8,8 @@ export const docsUrl = { 'https://docs.astar.network/tutorial/how-to-send-astr-sdn-from-metamask-to-polkadot.js', troubleShooting: 'https://docs.astar.network/docs/use/user-guides/troubleshooting', createPromotion: 'https://docs.astar.network/docs/use/dapp-staking/for-devs/create-promotion/', + faqLedger: + 'https://docs.astar.network/docs/learn/dapp-staking/dapp-staking-faq/#q-i-am-a-leger-astar-native-app-user-what-do-i-need-to-do', }; export const socialUrl = { diff --git a/src/staking-v3/components/DiscoverV3.vue b/src/staking-v3/components/DiscoverV3.vue index e0bebdc09..2ae571206 100644 --- a/src/staking-v3/components/DiscoverV3.vue +++ b/src/staking-v3/components/DiscoverV3.vue @@ -41,7 +41,7 @@ diff --git a/src/staking-v3/components/my-staking/Staking.vue b/src/staking-v3/components/my-staking/Staking.vue index ef4ee085d..32b12213e 100644 --- a/src/staking-v3/components/my-staking/Staking.vue +++ b/src/staking-v3/components/my-staking/Staking.vue @@ -14,8 +14,7 @@
- - + (() => store.getters['general/getCurrentBlock']); @@ -278,10 +280,20 @@ export function useDappStaking() { }; const withdraw = async (): Promise => { - const stakingService = container.get<() => IDappStakingService>( - Symbols.DappStakingServiceFactoryV3 - )(); - await stakingService.claimUnlockedTokens(currentAccount.value, t('stakingV3.withdrawSuccess')); + if (isLedger.value) { + const stakingService = container.get( + Symbols.DappStakingServiceV2Ledger + ); + await stakingService.withdraw(currentAccount.value, t('stakingV3.withdrawSuccess')); + } else { + const stakingService = container.get<() => IDappStakingService>( + Symbols.DappStakingServiceFactoryV3 + )(); + await stakingService.claimUnlockedTokens( + currentAccount.value, + t('stakingV3.withdrawSuccess') + ); + } getCurrentEraInfo(); }; @@ -293,14 +305,21 @@ export function useDappStaking() { }; const unlock = async (amount: bigint): Promise => { - const stakingService = container.get<() => IDappStakingService>( - Symbols.DappStakingServiceFactoryV3 - )(); - await stakingService.unlockTokens( - currentAccount.value, - Number(ethers.utils.formatEther(amount)), - t('stakingV3.unlockSuccess') - ); + if (isLedger.value) { + const stakingService = container.get( + Symbols.DappStakingServiceV2Ledger + ); + await stakingService.unlock(currentAccount.value, amount, t('stakingV3.unlockSuccess')); + } else { + const stakingService = container.get<() => IDappStakingService>( + Symbols.DappStakingServiceFactoryV3 + )(); + await stakingService.unlockTokens( + currentAccount.value, + Number(ethers.utils.formatEther(amount)), + t('stakingV3.unlockSuccess') + ); + } }; const getAllRewards = async (): Promise => { @@ -576,6 +595,21 @@ export function useDappStaking() { return period.toString().padStart(3, '0'); }; + const warnIfLedger = (): void => { + // Show warning to ledger users. + const { isLedger } = useLedger(); + const { isDappStakingV3 } = useDappStaking(); + if (isLedger.value && isDappStakingV3.value) { + const eventAggregator = container.get(Symbols.EventAggregator); + eventAggregator.publish( + new ExtrinsicStatusMessage({ + success: false, + message: t('stakingV3.ledgerNotSupported'), + }) + ); + } + }; + return { protocolState, ledger, @@ -622,5 +656,6 @@ export function useDappStaking() { rewardExpiresInNextPeriod, getStakerInfo, formatPeriod, + warnIfLedger, }; } diff --git a/src/staking-v3/logic/repositories/DappStakingRepository.ts b/src/staking-v3/logic/repositories/DappStakingRepository.ts index 4d00ef693..f5b87ca77 100644 --- a/src/staking-v3/logic/repositories/DappStakingRepository.ts +++ b/src/staking-v3/logic/repositories/DappStakingRepository.ts @@ -462,6 +462,23 @@ export class DappStakingRepository implements IDappStakingRepository { return api.tx.dappStaking.cleanupExpiredEntries(); } + /** @inheritdoc */ + public async getUnbondAndUnstakeCall(amount: bigint): Promise { + const api = await this.api.getApi(); + // Memo: address is ignored by runtime, but we need to pass something + // because runtime needs to keep the method signature. + return api.tx.dappStaking.unbondAndUnstake( + getDappAddressEnum('ajYMsCKsEAhEvHpeA4XqsfiA9v1CdzZPrCfS6pEfeGHW9j8'), + amount + ); + } + + /** @inheritdoc */ + public async getWithdrawUnbondedCall(): Promise { + const api = await this.api.getApi(); + return api.tx.dappStaking.withdrawUnbonded(); + } + // ------------------ MAPPERS ------------------ private mapToModel(state: PalletDappStakingV3ProtocolState): ProtocolState { return { diff --git a/src/staking-v3/logic/repositories/IDappStakingRepository.ts b/src/staking-v3/logic/repositories/IDappStakingRepository.ts index 750877ec3..89041cee4 100644 --- a/src/staking-v3/logic/repositories/IDappStakingRepository.ts +++ b/src/staking-v3/logic/repositories/IDappStakingRepository.ts @@ -213,4 +213,14 @@ export interface IDappStakingRepository { * Gets dApps tier assignment map. */ getLeaderboard(): Promise>; + + /** + * Gets a call to the legacy code to support v2 ledger stakers to unlock their funds. + */ + getUnbondAndUnstakeCall(amount: bigint): Promise; + + /** + * Gets a call to the legacy code to support v2 ledger stakers to withdraw their funds. + */ + getWithdrawUnbondedCall(): Promise; } diff --git a/src/staking-v3/logic/services/DappStakingService.ts b/src/staking-v3/logic/services/DappStakingService.ts index 8a44e75ae..89099a28a 100644 --- a/src/staking-v3/logic/services/DappStakingService.ts +++ b/src/staking-v3/logic/services/DappStakingService.ts @@ -15,16 +15,19 @@ import { Guard } from 'src/v2/common'; import { IWalletService } from 'src/v2/services'; import { ExtrinsicPayload } from '@astar-network/astar-sdk-core'; import { ethers } from 'ethers'; +import { SignerService } from './SignerService'; @injectable() -export class DappStakingService implements IDappStakingService { +export class DappStakingService extends SignerService implements IDappStakingService { constructor( @inject(Symbols.DappStakingRepositoryV3) protected dappStakingRepository: IDappStakingRepository, @inject(Symbols.TokenApiProviderRepository) protected tokenApiRepository: IDataProviderRepository, - @inject(Symbols.WalletFactory) private walletFactory: () => IWalletService - ) {} + @inject(Symbols.WalletFactory) walletFactory: () => IWalletService + ) { + super(walletFactory); + } // @inheritdoc public async getDapps( @@ -714,17 +717,4 @@ export class DappStakingService implements IDappStakingService { ): boolean { return stakedPeriod < currentPeriod - rewardRetentionInPeriods; } - - private async signCall( - call: ExtrinsicPayload, - senderAddress: string, - successMessage: string - ): Promise { - const wallet = this.walletFactory(); - await wallet.signAndSend({ - extrinsic: call, - senderAddress: senderAddress, - successMessage, - }); - } } diff --git a/src/staking-v3/logic/services/DappStakingServiceV2Ledger.ts b/src/staking-v3/logic/services/DappStakingServiceV2Ledger.ts new file mode 100644 index 000000000..9c18d0ff3 --- /dev/null +++ b/src/staking-v3/logic/services/DappStakingServiceV2Ledger.ts @@ -0,0 +1,39 @@ +import { injectable, inject } from 'inversify'; +import { IDappStakingServiceV2Ledger } from './IDappStakingServiceV2Ledger'; +import { SignerService } from './SignerService'; +import { Symbols } from 'src/v2/symbols'; +import { IDappStakingRepository } from '../repositories'; +import { IWalletService } from 'src/v2/services'; +import { Guard } from 'src/v2/common'; + +@injectable() +export class DappStakingServiceV2Ledger + extends SignerService + implements IDappStakingServiceV2Ledger +{ + constructor( + @inject(Symbols.DappStakingRepositoryV3) + protected dappStakingRepository: IDappStakingRepository, + @inject(Symbols.WalletFactory) walletFactory: () => IWalletService + ) { + super(walletFactory); + } + + public async unlock( + senderAddress: string, + amount: bigint, + successMessage: string + ): Promise { + Guard.ThrowIfUndefined(senderAddress, 'senderAddress'); + + const call = await this.dappStakingRepository.getUnbondAndUnstakeCall(amount); + await this.signCall(call, senderAddress, successMessage); + } + + public async withdraw(senderAddress: string, successMessage: string): Promise { + Guard.ThrowIfUndefined(senderAddress, 'senderAddress'); + + const call = await this.dappStakingRepository.getWithdrawUnbondedCall(); + await this.signCall(call, senderAddress, successMessage); + } +} diff --git a/src/staking-v3/logic/services/IDappStakingServiceV2Ledger.ts b/src/staking-v3/logic/services/IDappStakingServiceV2Ledger.ts new file mode 100644 index 000000000..d3c31c5b9 --- /dev/null +++ b/src/staking-v3/logic/services/IDappStakingServiceV2Ledger.ts @@ -0,0 +1,8 @@ +/** + * Support for v2 ledger stakers to enable them to unlock and withdraw their tokens. + */ +export interface IDappStakingServiceV2Ledger { + unlock(senderAddress: string, amount: bigint, successMessage: string): Promise; + + withdraw(senderAddress: string, successMessage: string): Promise; +} diff --git a/src/staking-v3/logic/services/SignerService.ts b/src/staking-v3/logic/services/SignerService.ts new file mode 100644 index 000000000..e7a4601bb --- /dev/null +++ b/src/staking-v3/logic/services/SignerService.ts @@ -0,0 +1,22 @@ +import { ExtrinsicPayload } from '@astar-network/astar-sdk-core'; +import { injectable, inject } from 'inversify'; +import { IWalletService } from 'src/v2/services'; +import { Symbols } from 'src/v2/symbols'; + +@injectable() +export class SignerService { + constructor(@inject(Symbols.WalletFactory) private walletFactory: () => IWalletService) {} + + public async signCall( + call: ExtrinsicPayload, + senderAddress: string, + successMessage: string + ): Promise { + const wallet = this.walletFactory(); + await wallet.signAndSend({ + extrinsic: call, + senderAddress: senderAddress, + successMessage, + }); + } +} diff --git a/src/staking-v3/logic/services/index.ts b/src/staking-v3/logic/services/index.ts index 125f3fb29..d98fa8c14 100644 --- a/src/staking-v3/logic/services/index.ts +++ b/src/staking-v3/logic/services/index.ts @@ -2,3 +2,6 @@ export * from './IDappStakingService'; export * from './DappStakingService'; export * from './DappStakingServiceEvm'; export * from './DappStakingServiceV2V3'; +export * from './DappStakingServiceV2Ledger'; +export * from './IDappStakingServiceV2Ledger'; +export * from './SignerService'; diff --git a/src/v2/app.container.ts b/src/v2/app.container.ts index b9354b2d5..91ae97eac 100644 --- a/src/v2/app.container.ts +++ b/src/v2/app.container.ts @@ -85,7 +85,12 @@ import { DappStakingServiceV2V3, IDappStakingServiceV2V3, } from 'src/staking-v3/logic/services/DappStakingServiceV2V3'; -import { IDataProviderRepository, TokenApiProviderRepository } from '../staking-v3/logic'; +import { + DappStakingServiceV2Ledger, + IDappStakingServiceV2Ledger, + IDataProviderRepository, + TokenApiProviderRepository, +} from '../staking-v3/logic'; let currentWalletType = WalletType.Polkadot; let currentWalletName = ''; @@ -232,6 +237,11 @@ export default function buildDependencyContainer(network: endpointKey): void { Symbols.DappStakingServiceV2V3 ); + container.addSingleton( + DappStakingServiceV2Ledger, + Symbols.DappStakingServiceV2Ledger + ); + // Start block change subscription. Needed for remaining unlocking blocks calculation. container.get(Symbols.SystemRepository).startBlockSubscription(); } diff --git a/src/v2/symbols.ts b/src/v2/symbols.ts index 68e35adef..72a801d6c 100644 --- a/src/v2/symbols.ts +++ b/src/v2/symbols.ts @@ -43,4 +43,5 @@ export const Symbols = { DappStakingServiceV2V3: Symbol.for('DappStakingServiceV2V3'), TokenApiProviderRepository: Symbol.for('TokenApiProviderRepository'), InflationRepository: Symbol.for('InflationRepository'), + DappStakingServiceV2Ledger: Symbol.for('DappStakingServiceV2Ledger'), };