From 37cbf40ea7be6824933f5db84c3f117fe9a96205 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Wed, 4 Feb 2026 12:20:20 +0700 Subject: [PATCH 01/11] [Issue-#4946] Init TAO delegate staking --- .../helpers/subscribe/substrate/index.ts | 27 +- .../helpers/subscribe/substrate/utils.ts | 105 +++- .../src/services/balance-service/index.ts | 9 + .../handlers/delegate-staking/index.ts | 4 + .../handlers/delegate-staking/tao.ts | 494 ++++++++++++++++++ .../handlers/native-staking/base.ts | 4 +- .../src/services/earning-service/service.ts | 6 +- .../services/earning-service/utils/index.ts | 2 +- .../inapp-notification-service/index.ts | 3 + .../services/substrate-proxy-service/index.ts | 8 +- .../extension-base/src/types/balance/index.ts | 6 +- .../substrateProxyAccount/actions/index.ts | 2 + .../src/types/substrateProxyAccount/index.ts | 6 + .../src/types/yield/actions/join/submit.ts | 18 +- .../src/types/yield/info/account/info.ts | 6 +- .../src/types/yield/info/account/target.ts | 7 + .../src/types/yield/info/base.ts | 7 +- .../src/types/yield/info/chain/info.ts | 9 +- .../src/types/yield/info/chain/target.ts | 11 +- .../variants/AddSubstrateProxyAccount.tsx | 47 +- .../EarningEntry/EarningOptions/index.tsx | 2 +- .../EarningEntry/EarningPositions/index.tsx | 3 +- .../Popup/Home/Earning/EarningPools/index.tsx | 7 +- .../EarningPositionDetail/RewardInfoPart.tsx | 1 + .../Tokens/TotalEquivalentDetailModal.tsx | 144 +++++ .../Popup/Transaction/parts/FreeBalance.tsx | 54 +- .../src/Popup/Transaction/variants/Earn.tsx | 209 ++++++-- .../src/Popup/Transaction/variants/Unbond.tsx | 124 +++-- .../Field/Earning/EarningStrategySelector.tsx | 394 ++++++++++++++ .../src/components/Field/Earning/index.ts | 1 + .../src/components/MetaInfo/MetaInfo.tsx | 4 + .../Modal/Earning/EarningInstructionModal.tsx | 27 + .../Earning/EarningStrategyDetailModal.tsx | 96 ++++ .../src/components/Modal/Earning/index.ts | 1 + .../StakingItem/StakingStrategyItem.tsx | 177 +++++++ .../src/constants/earning/staticData.ts | 50 ++ .../extension-koni-ui/src/constants/modal.ts | 1 + .../src/hooks/earning/useGetPoolTargetList.ts | 2 +- .../hooks/earning/useGroupYieldPosition.ts | 2 +- .../hooks/earning/useYieldPositionDetail.ts | 2 +- .../extension-koni-ui/src/types/earning.ts | 11 +- .../extension-koni-ui/src/utils/earning.ts | 12 +- .../public/locales/en/translation.json | 31 +- .../public/locales/ja/translation.json | 31 +- .../public/locales/ru/translation.json | 31 +- .../public/locales/vi/translation.json | 31 +- .../public/locales/zh/translation.json | 31 +- 47 files changed, 2088 insertions(+), 172 deletions(-) create mode 100644 packages/extension-base/src/services/earning-service/handlers/delegate-staking/index.ts create mode 100644 packages/extension-base/src/services/earning-service/handlers/delegate-staking/tao.ts create mode 100644 packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx create mode 100644 packages/extension-koni-ui/src/components/Field/Earning/EarningStrategySelector.tsx create mode 100644 packages/extension-koni-ui/src/components/Modal/Earning/EarningStrategyDetailModal.tsx create mode 100644 packages/extension-koni-ui/src/components/StakingItem/StakingStrategyItem.tsx diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/index.ts index d14b14f7e12..7f42aa9d2a8 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/index.ts @@ -16,7 +16,7 @@ import { getPSP22ContractPromise } from '@subwallet/extension-base/koni/api/cont import { getDefaultWeightV2 } from '@subwallet/extension-base/koni/api/contract-handler/wasm/utils'; import { _BALANCE_CHAIN_GROUP, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX, USE_MULTILOCATION_INDEX } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _SubstrateAdapterSubscriptionArgs, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _checkSmartContractSupportByChain, _getAssetExistentialDeposit, _getAssetNetuid, _getChainExistentialDeposit, _getChainNativeTokenSlug, _getContractAddressOfToken, _getTokenOnChainAssetId, _getTokenOnChainInfo, _getTokenTypesSupportedByChain, _getXcmAssetMultilocation, _isBridgedToken, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _checkSmartContractSupportByChain, _getAssetDecimals, _getAssetExistentialDeposit, _getAssetNetuid, _getChainExistentialDeposit, _getChainNativeTokenSlug, _getContractAddressOfToken, _getTokenOnChainAssetId, _getTokenOnChainInfo, _getTokenTypesSupportedByChain, _getXcmAssetMultilocation, _isBridgedToken, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { TaoStakeInfo } from '@subwallet/extension-base/services/earning-service/handlers/native-staking/tao'; import { BalanceItem, SubscribeBasePalletBalance, SubscribeSubstratePalletBalance } from '@subwallet/extension-base/types'; import { filterAlphaAssetsByChain, filterAssetsByChainAndType } from '@subwallet/extension-base/utils'; @@ -28,7 +28,7 @@ import { ContractPromise } from '@polkadot/api-contract'; import { subscribeERC20Interval } from '../evm'; import { subscribeEquilibriumTokenBalance } from './equilibrium'; import { subscribeGRC20Balance, subscribeVftBalance } from './gear'; -import { buildLockedDetails, getSpecialStakingBalances } from './utils'; +import { buildLockedDetails, getSpecialStakingBalancesWithDetails } from './utils'; export const subscribeSubstrateBalance = async (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, substrateApi: _SubstrateApi, evmApi: _EvmApi, callback: (rs: BalanceItem[]) => void, extrinsicType?: ExtrinsicType) => { let unsubNativeToken: () => void; @@ -124,7 +124,7 @@ export const subscribeSubstrateBalance = async (addresses: string[], chainInfo: }; // eslint-disable-next-line @typescript-eslint/require-await -const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo, extrinsicType, substrateApi }: SubscribeSubstratePalletBalance) => { +const subscribeWithSystemAccountPallet = async ({ addresses, assetMap, callback, chainInfo, extrinsicType, substrateApi }: SubscribeSubstratePalletBalance) => { const systemAccountKey = 'query_system_account'; const poolMembersKey = 'query_nominationPools_poolMembers'; @@ -155,7 +155,15 @@ const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo const balances = rs[systemAccountKey]; const poolMemberInfos = rs[poolMembersKey]; - const bittensorStakingBalances = await getSpecialStakingBalances(chainInfo, addresses, substrateApi); + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + const nativeTokenInfo = assetMap[nativeTokenSlug]; + + const bittensorStakingDetails = await getSpecialStakingBalancesWithDetails( + chainInfo, + addresses, + substrateApi, + _getAssetDecimals(nativeTokenInfo) + ); // Precompute totalLockedFromTransfer for each account to decide if need fetch locks/holds const preItems = balances.map((_balance, index) => { @@ -171,7 +179,7 @@ const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo totalLockedFromTransfer += nominationPoolBalance; } - totalLockedFromTransfer += BigInt(bittensorStakingBalances[index].toString()); + totalLockedFromTransfer += BigInt(bittensorStakingDetails[index].native.toString()); return { index, totalLockedFromTransfer, balanceInfo }; }); @@ -222,15 +230,18 @@ const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo const allLockEntries = [...lockItems, ...holdItems, ...freezeItems]; + const transferableBalance = _getSystemPalletTransferable(balanceInfo, _getChainExistentialDeposit(chainInfo), extrinsicType); + + const stakingDetails = bittensorStakingDetails[index]; + const lockedDetails = buildLockedDetails( allLockEntries, totalLockedFromTransfer, _getSystemPalletReservedBalance(balanceInfo), - bittensorStakingBalances[index] + stakingDetails?.native, + stakingDetails?.total ); - const transferableBalance = _getSystemPalletTransferable(balanceInfo, _getChainExistentialDeposit(chainInfo), extrinsicType); - return { address: addresses[index], tokenSlug: _getChainNativeTokenSlug(chainInfo), diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/utils.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/utils.ts index f98306aa3ef..829a80b3f2d 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/utils.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/substrate/utils.ts @@ -5,31 +5,11 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { FrameBalancesFreezesInfo, FrameBalancesHoldsInfo, FrameBalancesLocksInfo } from '@subwallet/extension-base/core/substrate/types'; import { _BALANCE_CHAIN_GROUP, _BALANCE_LOCKED_ID_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; +import { alphaPriceCache } from '@subwallet/extension-base/services/earning-service/handlers/delegate-staking/tao'; import { TaoStakeInfo } from '@subwallet/extension-base/services/earning-service/handlers/native-staking/tao'; import { LockedBalanceDetails } from '@subwallet/extension-base/types'; import BigN from 'bignumber.js'; -export async function getSpecialStakingBalances (chainInfo: _ChainInfo, addresses: string[], substrateApi: _SubstrateApi): Promise { - // Default: 0 for all addresses - let balances = new Array(addresses.length).fill(new BigN(0)); - - // --- Bittensor ---------------------------------------------------------------- - if (_BALANCE_CHAIN_GROUP.bittensor.includes(chainInfo.slug)) { - const rawData = await substrateApi.api.call.stakeInfoRuntimeApi.getStakeInfoForColdkeys(addresses); - const values: Array<[string, TaoStakeInfo[]]> = rawData.toPrimitive() as Array<[string, TaoStakeInfo[]]>; - - balances = values.map(([, stakes]) => - stakes - .filter((i) => i.netuid === 0) - .reduce((prev, curr) => prev.plus(curr.stake), BigN(0)) - ); - - return balances; - } - - return balances; -} - // handler according to different logic const extractId = (id: string | Record | undefined): string => { if (!id) { @@ -45,7 +25,7 @@ const extractId = (id: string | Record | undefined): string => return keys.length ? keys[0] : ''; }; -export function buildLockedDetails (item: (FrameBalancesLocksInfo | FrameBalancesHoldsInfo | FrameBalancesFreezesInfo)[], totalLockedFromTransfer: bigint, reserved: bigint, externalStaking?: BigN): LockedBalanceDetails { +export function buildLockedDetails (item: (FrameBalancesLocksInfo | FrameBalancesHoldsInfo | FrameBalancesFreezesInfo)[], totalLockedFromTransfer: bigint, reserved: bigint, externalStaking?: BigN, totalStakingEquivalent?: BigN): LockedBalanceDetails { let stakingBalance = externalStaking || new BigN(0); let govBalance = new BigN(0); let democracyBalance = new BigN(0); @@ -73,6 +53,85 @@ export function buildLockedDetails (item: (FrameBalancesLocksInfo | FrameBalance governance: govBalance.toFixed(), democracy: democracyBalance.toFixed(), reserved: reservedBN.toFixed(), - others: others.gt(0) ? others.toFixed() : '0' + others: others.gt(0) ? others.toFixed() : '0', + totalStakingEquivalent: totalStakingEquivalent?.toFixed(0) || undefined }; } + +export type SpecialStakingBalance = { + /** Total staking value in native token (TAO) */ + total: BigN; + + /** Native TAO stake (netuid = 0) */ + native: BigN; + + /** Alpha stake converted to native TAO */ + alphaConverted: BigN; +}; + +export async function getSpecialStakingBalancesWithDetails (chainInfo: _ChainInfo, addresses: string[], substrateApi: _SubstrateApi, nativeDecimals: number): Promise { + const result: SpecialStakingBalance[] = addresses.map(() => ({ + total: new BigN(0), + native: new BigN(0), + alphaConverted: new BigN(0) + })); + + // Only apply for Bittensor + if (!_BALANCE_CHAIN_GROUP.bittensor.includes(chainInfo.slug)) { + return result; + } + + const api = await substrateApi.isReady; + + const rawData = + await api.api.call.stakeInfoRuntimeApi.getStakeInfoForColdkeys(addresses); + + const values = rawData.toPrimitive() as Array<[string, TaoStakeInfo[]]>; + + for (let i = 0; i < values.length; i++) { + const [, stakes] = values[i]; + const alphaByNetuid = new Map(); + + // Separate native & alpha + for (const stake of stakes) { + const amount = new BigN(stake.stake); + + if (stake.netuid === 0) { + result[i].native = result[i].native.plus(amount); + } else { + const prev = alphaByNetuid.get(stake.netuid) || new BigN(0); + + alphaByNetuid.set(stake.netuid, prev.plus(amount)); + } + } + + // Convert alpha → native + for (const [netuid, totalAlpha] of alphaByNetuid.entries()) { + try { + const price = await alphaPriceCache.getAlphaPrice( + { chain: chainInfo.slug, netuid }, + async () => { + const raw = + await api.api.call.swapRuntimeApi.currentAlphaPrice(netuid); + + return new BigN(raw.toString()); + } + ); + + const rate = price.dividedBy(new BigN(10).pow(nativeDecimals)); + const taoEquivalent = totalAlpha.multipliedBy(rate); + + result[i].alphaConverted = + result[i].alphaConverted.plus(taoEquivalent); + } catch (e) { + console.warn(`Failed to convert alpha for netuid ${netuid}`, e); + } + } + + // Final total + result[i].total = + result[i].native.plus(result[i].alphaConverted); + } + + return result; +} diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 1fb41c269b4..2b4f2c1815d 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -266,6 +266,15 @@ export class BalanceService implements StoppableServiceInterface { } break; + + case BalanceType.TOTAL_STAKE_EQUIVALENT: + value = rs.lockedDetails?.totalStakingEquivalent || '0'; + break; + + case BalanceType.TOTAL_EQUIVALENT: + value = new BigN(rs.lockedDetails?.totalStakingEquivalent || 0).plus(new BigN(rs.free)).toFixed(); + break; + default: value = rs.free; } diff --git a/packages/extension-base/src/services/earning-service/handlers/delegate-staking/index.ts b/packages/extension-base/src/services/earning-service/handlers/delegate-staking/index.ts new file mode 100644 index 00000000000..dec560ac451 --- /dev/null +++ b/packages/extension-base/src/services/earning-service/handlers/delegate-staking/index.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export { default as TaoDelegateStakingPoolHandler } from './tao'; diff --git a/packages/extension-base/src/services/earning-service/handlers/delegate-staking/tao.ts b/packages/extension-base/src/services/earning-service/handlers/delegate-staking/tao.ts new file mode 100644 index 00000000000..3d3005cda9d --- /dev/null +++ b/packages/extension-base/src/services/earning-service/handlers/delegate-staking/tao.ts @@ -0,0 +1,494 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; +import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { BITTENSOR_REFRESH_STAKE_INFO } from '@subwallet/extension-base/constants'; +import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; +import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils'; +import { BasicTxErrorType, DelegatedStrategyInfo, DelegatedYieldPoolInfo, EarningStatus, HandleYieldStepData, OptimalYieldPath, PrimitiveSubstrateProxyAccountItem, RequestDelegateStakingSubmit, RequestEarlyValidateYield, ResponseEarlyValidateYield, StakeCancelWithdrawalParams, StrategyInfo, SubmitJoinDelegateStaking, SubmitYieldJoinData, TransactionData, UnstakingInfo, YieldPoolInfo, YieldPoolMethodInfo, YieldPoolType, YieldPositionInfo, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { BN_TEN, isSameAddress, toBNString } from '@subwallet/extension-base/utils'; +import BigNumber from 'bignumber.js'; + +import BaseNativeStakingPoolHandler from '../native-staking/base'; +import { TaoStakeInfo } from '../native-staking/tao'; + +type CacheItem = { + value: BigNumber; + expiredAt: number; +} + +export interface TrustedStakeStrategy { + id: string; + name: string; + description: string; + proxyAddress: string; + strategyType: 'STANDARD_ETF' | string; + + targetConstituents: { + subnetWeights: Record; + }; + + minBalance: number; + isActive: boolean; + type: 'official' | 'custom' | string; +} + +class AlphaPriceCache { + // Store resolved alpha prices with expiration time + private readonly valueCache = new Map(); + private readonly TTL = BITTENSOR_REFRESH_STAKE_INFO; + + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + public constructor () {} + + // Build a unique cache key per chain and subnet (netuid) + private buildCacheKey (chain: string, netuid: number): string { + return `${chain}__${netuid}`; + } + + /** + * Get alpha price with TTL-based caching + * + * Flow: + * 1. Check cache and return value if still valid + * 2. Otherwise, fetch fresh value using provided fetcher + * 3. Cache fetched value with expiration timestamp + */ + public async getAlphaPrice ( + params: { chain: string; netuid: number }, + fetcher: () => Promise + ): Promise { + const cacheKey = this.buildCacheKey(params.chain, params.netuid); + const now = Date.now(); + + // Return cached value if it exists and has not expired + const cached = this.valueCache.get(cacheKey); + + if (cached && cached.expiredAt > now) { + return cached.value; + } + + // Fetch fresh alpha price from runtime API + const value = await fetcher(); + + // Cache fetched value with TTL + this.valueCache.set(cacheKey, { + value, + expiredAt: Date.now() + this.TTL + }); + + return value; + } + + // Clear all cached values + public clearAll () { + this.valueCache.clear(); + } +} + +export const alphaPriceCache = new AlphaPriceCache(); +const trustedStakeApi = 'https://app.trustedstake.ai/api/strategies-active'; + +export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPoolHandler { + // @ts-ignore + public override readonly type = YieldPoolType.DELEGATED_STAKING; + + override readonly availableMethod: YieldPoolMethodInfo = { + join: true, + defaultUnstake: true, + fastUnstake: false, + cancelUnstake: false, + withdraw: false, + claimReward: false, + changeValidator: true + }; + + constructor (state: KoniState, chain: string) { + super(state, chain); + + const symbol = this.nativeToken.symbol; + const chainSlug = this.chainInfo.slug; + + this.slug = `${symbol}___delegated_staking___${chainSlug}`; + } + + override async checkAccountHaveStake (): Promise> { + return Promise.resolve([]); + } + + public override async earlyValidate (data: RequestEarlyValidateYield): Promise { + return Promise.resolve({ passed: true }); + } + + private async getJoinThresholdFromTrustedStake (): Promise { + try { + const res = await fetch(trustedStakeApi); + const data = await res.json() as TrustedStakeStrategy[]; + + const activeStrategies = data.filter((s) => s.isActive); + + if (!activeStrategies.length) { + return null; + } + + const minBalance = Math.min( + ...activeStrategies.map((s) => s.minBalance) + ); + + return minBalance.toString(); + } catch (e) { + console.warn('Failed to fetch joinThreshold', e); + + return null; + } + } + + async subscribePoolInfo (callback: (data: YieldPoolInfo) => void): Promise { + let cancel = false; + + const defaultCallback = async () => { + const chainApi = await this.substrateApi.isReady; + + // Get proxy pallet deposit info + let proxyDeposit = '0'; + + try { + const proxyDepositBase = chainApi.api.consts.proxy.proxyDepositBase; + const proxyDepositFactor = chainApi.api.consts.proxy.proxyDepositFactor; + + // Calculate earning threshold as baseDeposit + depositFactor + proxyDeposit = proxyDepositBase.add(proxyDepositFactor).toString(); + } catch (error) { + console.warn('Failed to fetch proxy deposit info:', error); + } + + const joinThreshold = toBNString((await this.getJoinThresholdFromTrustedStake() || 0), _getAssetDecimals(this.nativeToken)) || proxyDeposit; + + const data: DelegatedYieldPoolInfo = { + ...this.baseInfo, + type: this.type, + metadata: { + ...this.metadataInfo, + description: this.getDescription(joinThreshold), + minValidate: joinThreshold, + maintainBalance: joinThreshold, + name: 'TAO Delegated Staking' + }, + statistic: { + assetEarning: [{ slug: this.nativeToken.slug }], + maxCandidatePerFarmer: 1, + maxWithdrawalRequestPerFarmer: 1, + earningThreshold: { + join: joinThreshold, + defaultUnstake: '0', + fastUnstake: '0' + }, + eraTime: 4, + era: 0, + unstakingPeriod: 0, + totalApy: 0 + }, + proxyDeposit: proxyDeposit + }; + + const poolInfo = await this.getPoolInfo(); + + !poolInfo && callback(data); + }; + + if (!this.isActive) { + await defaultCallback(); + + return () => { + cancel = true; + }; + } + + await defaultCallback(); + + const intervalId = setInterval(() => { + if (!cancel) { + defaultCallback().catch(console.error); + } + }, BITTENSOR_REFRESH_STAKE_INFO); + + return () => { + cancel = true; + clearInterval(intervalId); + }; + } + + override async subscribePoolPosition (useAddresses: string[], rsCallback: (rs: YieldPositionInfo) => void): Promise { + let cancel = false; + + const chainApi = await this.substrateApi.isReady; + const defaultInfo = this.baseInfo; + const chainInfo = this.chainInfo; + + const proxiesList = await chainApi.api.query.proxy.proxies.multi(useAddresses); + + // ===== Fetch TrustedStake strategies ===== + let trustedStrategies: TrustedStakeStrategy[] = []; + + try { + const res = await fetch(trustedStakeApi); + const data = await res.json() as TrustedStakeStrategy[]; + + trustedStrategies = data.filter((s) => s.isActive); + } catch (e) { + console.warn('Failed to fetch TrustedStake strategies', e); + } + + const getPoolPosition = async () => { + const rawStakeInfos = await chainApi.api.call.stakeInfoRuntimeApi.getStakeInfoForColdkeys(useAddresses); + + const stakeInfos = rawStakeInfos.toPrimitive() as Array<[string, TaoStakeInfo[]]>; + + for (let i = 0; i < stakeInfos.length; i++) { + const [owner, delegateStateInfo] = stakeInfos[i]; + + const [proxies] = + proxiesList[i].toPrimitive() as unknown as [PrimitiveSubstrateProxyAccountItem[], string]; + + const stakingProxies = + proxies?.filter((p) => p.proxyType === 'Staking') || []; + + if (!stakingProxies.length || !delegateStateInfo?.length) { + rsCallback({ + ...defaultInfo, + type: this.type, + address: owner, + balanceToken: this.nativeToken.slug, + totalStake: '0', + activeStake: '0', + unstakeBalance: '0', + status: EarningStatus.NOT_STAKING, + isBondedBefore: false, + nominations: [], + unstakings: [] + }); + + continue; + } + + // ===== 1. Calculate total stake ===== + const transferableBalance = await this.state.balanceService.getTransferableBalance( + owner, + this.chain, + this.nativeToken.slug + ); + + let taoNativeTotal = new BigNumber(transferableBalance.value || '0'); + + const alphaByNetuid = new Map(); + + for (const delegate of delegateStateInfo) { + const stake = new BigNumber(delegate.stake); + const netuid = delegate.netuid; + + if (netuid === 0) { + taoNativeTotal = taoNativeTotal.plus(stake); + } else { + const prev = alphaByNetuid.get(netuid) || new BigNumber(0); + + alphaByNetuid.set(netuid, prev.plus(stake)); + } + } + + // ===== 2. Convert alpha -> TAO ===== + let totalTao = taoNativeTotal; + + for (const [netuid, totalAlpha] of alphaByNetuid.entries()) { + try { + const price = await alphaPriceCache.getAlphaPrice( + { chain: this.chain, netuid }, + async () => { + const chainApi = await this.substrateApi.isReady; + const priceRaw = + await chainApi.api.call.swapRuntimeApi.currentAlphaPrice(netuid); + + return new BigNumber(priceRaw.toString()); + } + ); + + const rate = new BigNumber(price.toString()).dividedBy( + BN_TEN.pow(_getAssetDecimals(this.nativeToken)) + ); + + const taoEquivalent = totalAlpha.multipliedBy(rate); + + totalTao = totalTao.plus(taoEquivalent); + } catch (e) { + console.warn(`Failed alpha price for netuid ${netuid}`, e); + } + } + + // ===== 3. Filter trusted staking proxies ===== + const nominations: DelegatedStrategyInfo[] = stakingProxies + .map((proxy) => { + const strategy = trustedStrategies.find((s) => + isSameAddress(s.proxyAddress, proxy.delegate) + ); + + if (!strategy) { + return null; + } + + return { + status: EarningStatus.EARNING_REWARD, + chain: chainInfo.slug, + validatorAddress: proxy.delegate, + activeStake: totalTao.toFixed(), + validatorMinStake: '0', + validatorIdentity: strategy.name, + substrateProxyType: proxy.proxyType, + delay: proxy.delay + }; + }) + .filter(Boolean) as DelegatedStrategyInfo[]; + + const isTrusted = nominations.length > 0; + + rsCallback({ + ...defaultInfo, + type: this.type, + address: owner, + balanceToken: this.nativeToken.slug, + totalStake: totalTao.toFixed(), + activeStake: totalTao.toFixed(), + unstakeBalance: '0', + status: isTrusted + ? EarningStatus.EARNING_REWARD + : EarningStatus.NOT_STAKING, + isBondedBefore: totalTao.gt(0), + nominations, + unstakings: [] + }); + } + }; + + const getStakingPositionInterval = async () => { + if (!cancel) { + await getPoolPosition(); + } + }; + + await getStakingPositionInterval(); + + const intervalId = setInterval(() => { + getStakingPositionInterval().catch(console.error); + }, BITTENSOR_REFRESH_STAKE_INFO); + + return () => { + cancel = true; + clearInterval(intervalId); + }; + } + + public override async getPoolTargets (): Promise { + try { + const res = await fetch(trustedStakeApi); + const strategies = await res.json() as TrustedStakeStrategy[]; + + const decimals = _getAssetDecimals(this.nativeToken); + + return strategies + .filter((s) => s.isActive) + .map((strategy): StrategyInfo => ({ + address: strategy.proxyAddress, + chain: this.chain, + minBond: toBNString(strategy.minBalance, decimals), + constituents: Object.keys(strategy.targetConstituents.subnetWeights), + identity: strategy.name, + expectedReturn: undefined + })); + } catch (e) { + console.warn('Failed to fetch pool targets', e); + + return []; + } + } + + public override validateYieldJoin (data: SubmitYieldJoinData, path: OptimalYieldPath): Promise { + return Promise.resolve([]); + } + + override async createJoinExtrinsic (data: SubmitJoinDelegateStaking, positionInfo?: YieldPositionInfo, bondDest?: string, netuid?: number): Promise<[TransactionData, YieldTokenBaseInfo]> { + const chainApi = await this.substrateApi.isReady; + const { substrateProxyAddress, substrateProxyDeposit } = data; + + if (!substrateProxyAddress) { + return Promise.reject(new TransactionError(BasicTxErrorType.INVALID_PARAMS)); + } + + const txList = []; + const proxies = await chainApi.api.query.proxy.proxies(substrateProxyAddress); + + if (proxies) { + const [proxyDefs] = proxies; + + proxyDefs.forEach((proxyDef) => { + txList.push( + chainApi.api.tx.proxy.removeProxy( + proxyDef.delegate, + proxyDef.proxyType, + proxyDef.delay + ) + ); + }); + } + + txList.push(chainApi.api.tx.proxy.addProxy(substrateProxyAddress, 'Staking', 0)); + + const extrinsic = txList.length === 1 ? txList[0] : chainApi.api.tx.utility.batchAll(txList); + + return [extrinsic, { slug: this.nativeToken.slug, amount: txList.length === 1 ? substrateProxyDeposit : '0' }]; + } + + override async handleYieldJoin (_data: SubmitYieldJoinData, path: OptimalYieldPath, currentStep: number): Promise { + const data = _data as SubmitJoinDelegateStaking; + const { address, amount, slug, substrateProxyAddress, substrateProxyDeposit } = data; + + const positionInfo = await this.getPoolPosition(address, slug); + const [extrinsic, yieldTokenInfo] = await this.createJoinExtrinsic(data, positionInfo); + + if (!positionInfo) { + return Promise.reject(new TransactionError(BasicTxErrorType.INVALID_PARAMS)); + } + + const delegateData: RequestDelegateStakingSubmit = { + poolPosition: positionInfo, + slug: this.slug, + amount, + address, + substrateProxyAddress, + substrateProxyDeposit + }; + + return { + txChain: this.chain, + extrinsicType: ExtrinsicType.ADD_SUBSTRATE_PROXY_ACCOUNT, + extrinsic, + txData: delegateData, + transferNativeAmount: yieldTokenInfo.amount || '0', + chainType: ChainType.SUBSTRATE + }; + } + + public override validateYieldLeave (amount: string, address: string, fastLeave: boolean, selectedTarget?: string, slug?: string, poolInfo?: YieldPoolInfo): Promise { + return Promise.resolve([]); + } + + protected override handleYieldUnstake (amount: string, address: string, selectedTarget?: string): Promise<[ExtrinsicType, TransactionData]> { + throw new Error('Method not implemented.'); // Handle by remove proxy in substrate proxy service + } + + public override handleYieldWithdraw (address: string, unstakingInfo: UnstakingInfo): Promise { + throw new Error('Method not implemented.'); + } + + public override handleYieldCancelUnstake (params: StakeCancelWithdrawalParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/extension-base/src/services/earning-service/handlers/native-staking/base.ts b/packages/extension-base/src/services/earning-service/handlers/native-staking/base.ts index 9c5869da5b2..52d9c8bf778 100644 --- a/packages/extension-base/src/services/earning-service/handlers/native-staking/base.ts +++ b/packages/extension-base/src/services/earning-service/handlers/native-staking/base.ts @@ -5,7 +5,7 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/Tr import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { STAKING_IDENTITY_API_SLUG } from '@subwallet/extension-base/services/earning-service/constants'; -import { BasicTxErrorType, EarningRewardHistoryItem, EarningRewardItem, HandleYieldStepData, OptimalYieldPath, OptimalYieldPathParams, RequestBondingSubmit, SubmitChangeValidatorStaking, SubmitJoinNativeStaking, SubmitYieldJoinData, TransactionData, ValidatorInfo, YieldPoolMethodInfo, YieldPoolType, YieldPositionInfo, YieldStepBaseInfo, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { BasicTxErrorType, EarningRewardHistoryItem, EarningRewardItem, HandleYieldStepData, OptimalYieldPath, OptimalYieldPathParams, RequestBondingSubmit, SubmitChangeValidatorStaking, SubmitJoinDelegateStaking, SubmitJoinNativeStaking, SubmitYieldJoinData, TransactionData, ValidatorInfo, YieldPoolMethodInfo, YieldPoolType, YieldPositionInfo, YieldStepBaseInfo, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; import { noop } from '@polkadot/util'; @@ -135,7 +135,7 @@ export default abstract class BaseNativeStakingPoolHandler extends BasePoolHandl ]; } - abstract createJoinExtrinsic (data: SubmitJoinNativeStaking, positionInfo?: YieldPositionInfo, bondDest?: string, netuid?: number): Promise<[TransactionData, YieldTokenBaseInfo]> + abstract createJoinExtrinsic (data: SubmitJoinNativeStaking | SubmitJoinDelegateStaking, positionInfo?: YieldPositionInfo, bondDest?: string, netuid?: number): Promise<[TransactionData, YieldTokenBaseInfo]> protected async getSubmitStep (params: OptimalYieldPathParams): Promise { const { address, amount, netuid, slug, targets } = params; diff --git a/packages/extension-base/src/services/earning-service/service.ts b/packages/extension-base/src/services/earning-service/service.ts index b75e6f9cab1..444c2823525 100644 --- a/packages/extension-base/src/services/earning-service/service.ts +++ b/packages/extension-base/src/services/earning-service/service.ts @@ -18,6 +18,7 @@ import { addLazy, createPromiseHandler, filterAddressByChainInfo, PromiseHandler import { fetchStaticCache } from '@subwallet/extension-base/utils/fetchStaticCache'; import { BehaviorSubject, combineLatest } from 'rxjs'; +import { TaoDelegateStakingPoolHandler } from './handlers/delegate-staking'; import { EarningImpactResult } from './handlers/native-staking/dtao'; import TanssiNativeStakingPoolHandler from './handlers/native-staking/tanssi'; import { AcalaLiquidStakingPoolHandler, AmplitudeNativeStakingPoolHandler, AstarNativeStakingPoolHandler, BasePoolHandler, BifrostLiquidStakingPoolHandler, BifrostMantaLiquidStakingPoolHandler, EnergyNativeStakingPoolHandler, InterlayLendingPoolHandler, NominationPoolHandler, ParallelLiquidStakingPoolHandler, ParaNativeStakingPoolHandler, RelayNativeStakingPoolHandler, StellaSwapLiquidStakingPoolHandler, SubnetTaoStakingPoolHandler, TaoNativeStakingPoolHandler } from './handlers'; @@ -48,7 +49,7 @@ export default class EarningService implements StoppableServiceInterface, Persis private dbService: DatabaseService; private eventService: EventService; - private useOnlineCacheOnly = true; + private useOnlineCacheOnly = false; private validatorInfoCachingInterval: NodeJS.Timeout | undefined; private inactivePoolReady: PromiseHandler = createPromiseHandler(); @@ -113,9 +114,8 @@ export default class EarningService implements StoppableServiceInterface, Persis } if (_STAKING_CHAIN_GROUP.bittensor.includes(chain)) { - // todo: check support for testnet - // Mainnet only handlers.push(new TaoNativeStakingPoolHandler(this.state, chain)); + handlers.push(new TaoDelegateStakingPoolHandler(this.state, chain)); handlers.push(new SubnetTaoStakingPoolHandler(this.state, chain)); } diff --git a/packages/extension-base/src/services/earning-service/utils/index.ts b/packages/extension-base/src/services/earning-service/utils/index.ts index fb1e2f0a65c..c28d5d86180 100644 --- a/packages/extension-base/src/services/earning-service/utils/index.ts +++ b/packages/extension-base/src/services/earning-service/utils/index.ts @@ -116,7 +116,7 @@ export async function parseIdentity (substrateApi: _SubstrateApi, address: strin } export function isActionFromValidator (stakingType: YieldPoolType, chain: string) { - if (stakingType === YieldPoolType.NOMINATION_POOL || stakingType === YieldPoolType.LIQUID_STAKING || stakingType === YieldPoolType.LENDING) { + if (stakingType === YieldPoolType.NOMINATION_POOL || stakingType === YieldPoolType.LIQUID_STAKING || stakingType === YieldPoolType.LENDING || stakingType === YieldPoolType.DELEGATED_STAKING) { return false; } diff --git a/packages/extension-base/src/services/inapp-notification-service/index.ts b/packages/extension-base/src/services/inapp-notification-service/index.ts index a95833a2795..90a9f0bfdc3 100644 --- a/packages/extension-base/src/services/inapp-notification-service/index.ts +++ b/packages/extension-base/src/services/inapp-notification-service/index.ts @@ -448,6 +448,9 @@ export class InappNotificationService implements CronServiceInterface { case YieldPoolType.SUBNET_STAKING: method = 'Subnet staking'; // todo: confirm with tester break; + case YieldPoolType.DELEGATED_STAKING: + method = 'Delegated staking'; + break; } title = '[{{accountName}}] STAKED {{asset}}' diff --git a/packages/extension-base/src/services/substrate-proxy-service/index.ts b/packages/extension-base/src/services/substrate-proxy-service/index.ts index 5b9f7fabaf1..f4c5b248841 100644 --- a/packages/extension-base/src/services/substrate-proxy-service/index.ts +++ b/packages/extension-base/src/services/substrate-proxy-service/index.ts @@ -4,7 +4,7 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { BasicTxErrorType, TransactionData } from '@subwallet/extension-base/types'; -import { AddSubstrateProxyAccountParams, RemoveSubstrateProxyAccountParams, RequestGetSubstrateProxyAccountGroup, SubstrateProxyAccountGroup, SubstrateProxyAccountItem, SubstrateProxyType } from '@subwallet/extension-base/types/substrateProxyAccount'; +import { AddSubstrateProxyAccountParams, PrimitiveSubstrateProxyAccountItem, RemoveSubstrateProxyAccountParams, RequestGetSubstrateProxyAccountGroup, SubstrateProxyAccountGroup, SubstrateProxyAccountItem } from '@subwallet/extension-base/types/substrateProxyAccount'; import { reformatAddress } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -13,12 +13,6 @@ import { Enum } from '@polkadot/types'; import { _SubstrateApi } from '../chain-service/types'; import { txTypeToSubstrateProxyMap } from './constant'; -type PrimitiveSubstrateProxyAccountItem = { - delegate: string; - proxyType: SubstrateProxyType; // type of proxy retrieved from on-chain data - delay: number; -}; - export default class SubstrateProxyAccountService { protected readonly state: KoniState; diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index bd63635b268..18ed6750e56 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -17,7 +17,9 @@ export enum BalanceType { TRANSFERABLE = 'transferable', // free TOTAL = 'total', // free + locked TOTAL_MINUS_RESERVED = 'totalMinusReserved', // free + locked - reserved - KEEP_ALIVE = 'keepAlive' + KEEP_ALIVE = 'keepAlive', + TOTAL_STAKE_EQUIVALENT = 'totalStakeEquivalent', // only for bittensor (delegate staking) + TOTAL_EQUIVALENT = 'totalEquivalent' // only for bittensor (delegate staking) } /** @@ -37,8 +39,8 @@ export interface LockedBalanceDetails { democracy: string; reserved: string; others: string; + totalStakingEquivalent?: string; // currtently only used in Bittensor delegate staking to show total native token equivalent (locked alpha token convert to native token) } - export interface BalanceItem { // metadata address: string; diff --git a/packages/extension-base/src/types/substrateProxyAccount/actions/index.ts b/packages/extension-base/src/types/substrateProxyAccount/actions/index.ts index 208b3919f55..dbc13947adc 100644 --- a/packages/extension-base/src/types/substrateProxyAccount/actions/index.ts +++ b/packages/extension-base/src/types/substrateProxyAccount/actions/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { BaseRequestSign, InternalRequestSign } from '../../transaction'; +import { YieldPoolInfo } from '../../yield'; import { SubstrateProxyAccountItem, SubstrateProxyType } from '..'; export interface AddSubstrateProxyAccountParams extends BaseRequestSign { @@ -19,6 +20,7 @@ export interface RemoveSubstrateProxyAccountParams extends BaseRequestSign { chain: string; selectedSubstrateProxyAccounts: SubstrateProxyAccountItem[] isRemoveAll?: boolean; + poolInfo?: YieldPoolInfo // use for delegated staking } export type RequestRemoveSubstrateProxyAccount = InternalRequestSign; diff --git a/packages/extension-base/src/types/substrateProxyAccount/index.ts b/packages/extension-base/src/types/substrateProxyAccount/index.ts index 7fbbe8ac4b9..c3af71d5b85 100644 --- a/packages/extension-base/src/types/substrateProxyAccount/index.ts +++ b/packages/extension-base/src/types/substrateProxyAccount/index.ts @@ -30,4 +30,10 @@ export interface SubstrateProxyAccountGroup { substrateProxyDeposit: string; } +export type PrimitiveSubstrateProxyAccountItem = { + delegate: string; + proxyType: SubstrateProxyType; // type of proxy retrieved from on-chain data + delay: number; +}; + export * from './actions'; diff --git a/packages/extension-base/src/types/yield/actions/join/submit.ts b/packages/extension-base/src/types/yield/actions/join/submit.ts index c7cdf310ca4..e56dcac5526 100644 --- a/packages/extension-base/src/types/yield/actions/join/submit.ts +++ b/packages/extension-base/src/types/yield/actions/join/submit.ts @@ -39,6 +39,11 @@ export interface SubmitJoinNominationPool extends AbstractSubmitYieldJoinData { selectedPool: NominationPoolInfo; } +export interface SubmitJoinDelegateStaking extends AbstractSubmitYieldJoinData { + substrateProxyAddress: string; + substrateProxyDeposit: string; +} + export interface SubmitYieldStepData extends AbstractSubmitYieldJoinData { // TODO exchangeRate: number, // reward token amount = input token amount * exchange rate inputTokenSlug: string, @@ -47,7 +52,7 @@ export interface SubmitYieldStepData extends AbstractSubmitYieldJoinData { // TO feeTokenSlug: string } -export type SubmitYieldJoinData = SubmitYieldStepData | SubmitJoinNativeStaking | SubmitJoinNominationPool; +export type SubmitYieldJoinData = SubmitYieldStepData | SubmitJoinNativeStaking | SubmitJoinNominationPool | SubmitJoinDelegateStaking; export enum EarningProcessType { NOMINATION_POOL = 'NOMINATION_POOL', @@ -107,6 +112,17 @@ export interface BondingSubmitParams extends BaseRequestSign { export type RequestBondingSubmit = InternalRequestSign; +export interface DelegateStakingSubmitParams extends BaseRequestSign { + slug: string, + poolPosition: YieldPositionInfo, + amount: string, + address: string, + substrateProxyAddress: string, + substrateProxyDeposit: string +} + +export type RequestDelegateStakingSubmit = InternalRequestSign; + export type SubmitChangeValidatorStaking = InternalRequestSign; export interface SubmitBittensorChangeValidatorStaking extends SubmitJoinNativeStaking { diff --git a/packages/extension-base/src/types/yield/info/account/info.ts b/packages/extension-base/src/types/yield/info/account/info.ts index b9d03912e70..dc7fea5a549 100644 --- a/packages/extension-base/src/types/yield/info/account/info.ts +++ b/packages/extension-base/src/types/yield/info/account/info.ts @@ -128,7 +128,11 @@ export interface SubnetYieldPositionInfo extends AbstractYieldPositionInfo { }; } +export interface DelegatedYieldPositionInfo extends AbstractYieldPositionInfo { + type: YieldPoolType.DELEGATED_STAKING; +} + /** * Info of yield pool * */ -export type YieldPositionInfo = NativeYieldPositionInfo | NominationYieldPositionInfo | LiquidYieldPositionInfo | LendingYieldPositionInfo | SubnetYieldPositionInfo; +export type YieldPositionInfo = NativeYieldPositionInfo | NominationYieldPositionInfo | LiquidYieldPositionInfo | LendingYieldPositionInfo | SubnetYieldPositionInfo | DelegatedYieldPositionInfo; diff --git a/packages/extension-base/src/types/yield/info/account/target.ts b/packages/extension-base/src/types/yield/info/account/target.ts index 3cf1862be12..cb46dc6a8d6 100644 --- a/packages/extension-base/src/types/yield/info/account/target.ts +++ b/packages/extension-base/src/types/yield/info/account/target.ts @@ -1,6 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { SubstrateProxyType } from '@subwallet/extension-base/types/substrateProxyAccount'; + /** * @enum {string} * @description The earning status of an account in a pool. @@ -46,3 +48,8 @@ export interface NominationInfo { /** The staking status of the account */ status: EarningStatus; } + +export interface DelegatedStrategyInfo extends NominationInfo { + substrateProxyType: SubstrateProxyType + delay: number; +} diff --git a/packages/extension-base/src/types/yield/info/base.ts b/packages/extension-base/src/types/yield/info/base.ts index c0cc7df8f69..7f20f903fc9 100644 --- a/packages/extension-base/src/types/yield/info/base.ts +++ b/packages/extension-base/src/types/yield/info/base.ts @@ -24,8 +24,11 @@ export enum YieldPoolType { /** Parachain staking */ PARACHAIN_STAKING = 'PARACHAIN_STAKING', - /** Subnet stakingg */ - SUBNET_STAKING = 'SUBNET_STAKING' + /** Subnet staking */ + SUBNET_STAKING = 'SUBNET_STAKING', + + /* Delegate staking */ + DELEGATED_STAKING = 'DELEGATED_STAKING' } /** diff --git a/packages/extension-base/src/types/yield/info/chain/info.ts b/packages/extension-base/src/types/yield/info/chain/info.ts index 2f8ccce8ce6..c275a595916 100644 --- a/packages/extension-base/src/types/yield/info/chain/info.ts +++ b/packages/extension-base/src/types/yield/info/chain/info.ts @@ -332,10 +332,17 @@ export interface NativeYieldPoolInfo extends AbstractYieldPoolInfo { maxPoolMembers?: number; } +export interface DelegatedYieldPoolInfo extends AbstractYieldPoolInfo { + type: YieldPoolType.DELEGATED_STAKING; + statistic?: NormalYieldPoolStatistic; + maxPoolMembers?: number; + proxyDeposit?: string; +} + /** * Info of yield pool * */ -export type YieldPoolInfo = NativeYieldPoolInfo | NominationYieldPoolInfo | LiquidYieldPoolInfo | LendingYieldPoolInfo; +export type YieldPoolInfo = NativeYieldPoolInfo | NominationYieldPoolInfo | LiquidYieldPoolInfo | LendingYieldPoolInfo | DelegatedYieldPoolInfo; /** * @interface YieldAssetExpectedEarning diff --git a/packages/extension-base/src/types/yield/info/chain/target.ts b/packages/extension-base/src/types/yield/info/chain/target.ts index f496b210d57..9b6dd0a86cd 100644 --- a/packages/extension-base/src/types/yield/info/chain/target.ts +++ b/packages/extension-base/src/types/yield/info/chain/target.ts @@ -38,12 +38,21 @@ export interface ValidatorInfo { topQuartile?: boolean; } +export interface StrategyInfo { + address: string; + chain: string; + minBond: string; + constituents: string[] + identity?: string; + expectedReturn?: number; // in %, annually +} + export interface AllValidatorInfo { currentSelectedValidatorList: ValidatorInfo[]; waitingValidatorList: ValidatorInfo[]; } -export type YieldPoolTarget = NominationPoolInfo | ValidatorInfo; +export type YieldPoolTarget = NominationPoolInfo | ValidatorInfo | StrategyInfo; export interface RequestGetYieldPoolTargets { slug: string; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/AddSubstrateProxyAccount.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/AddSubstrateProxyAccount.tsx index a6ba5b488f6..6aa9e1efff6 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/AddSubstrateProxyAccount.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/AddSubstrateProxyAccount.tsx @@ -1,9 +1,9 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { RequestAddSubstrateProxyAccount } from '@subwallet/extension-base/types'; +import { RequestAddSubstrateProxyAccount, RequestDelegateStakingSubmit } from '@subwallet/extension-base/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; -import { CommonTransactionInfo } from '@subwallet/extension-koni-ui/components'; +import { AlertBox, CommonTransactionInfo } from '@subwallet/extension-koni-ui/components'; import MetaInfo from '@subwallet/extension-koni-ui/components/MetaInfo/MetaInfo'; import useGetNativeTokenBasicInfo from '@subwallet/extension-koni-ui/hooks/common/useGetNativeTokenBasicInfo'; import CN from 'classnames'; @@ -18,7 +18,9 @@ type Props = BaseTransactionConfirmationProps; const Component: React.FC = (props: Props) => { const { className, transaction } = props; const { t } = useTranslation(); - const data = transaction.data as RequestAddSubstrateProxyAccount; + const data = transaction.data as RequestAddSubstrateProxyAccount | RequestDelegateStakingSubmit; + + const isDelegateStaking = 'poolPosition' in data; const { decimals, symbol } = useGetNativeTokenBasicInfo(transaction.chain); return ( @@ -37,14 +39,25 @@ const Component: React.FC = (props: Props) => { label={t('ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.proxyAccount')} /> - - - {data.substrateProxyType} - - + {isDelegateStaking + ? ( + + ) + : ( + + + {data.substrateProxyType} + + + )} = (props: Props) => { chainSlug={transaction.chain} label={t('ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith')} /> - } + {isDelegateStaking && + } ); }; @@ -84,6 +103,10 @@ const AddSubstrateProxyAccountTransactionConfirmation = styled(Component) '.__selected-validator-type': { color: token['magenta-6'] + }, + + '&.alert-box': { + marginTop: token.marginSM } }; }); diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningOptions/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningOptions/index.tsx index a733f134cd5..e8ed0258624 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningOptions/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningOptions/index.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils'; -import { _STAKING_CHAIN_GROUP, RELAY_HANDLER_DIRECT_STAKING_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; +import { RELAY_HANDLER_DIRECT_STAKING_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { isLendingPool, isLiquidPool } from '@subwallet/extension-base/services/earning-service/utils'; import { YieldPoolInfo, YieldPoolType } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningPositions/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningPositions/index.tsx index c60bf11f17e..c92811a9949 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningPositions/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningEntry/EarningPositions/index.tsx @@ -199,7 +199,8 @@ function Component ({ className, earningPositions, setEntryView, setLoading }: P { label: t('ui.EARNING.screen.EarningPositions.lending'), value: YieldPoolType.LENDING }, { label: t('ui.EARNING.screen.EarningPositions.parachainStaking'), value: YieldPoolType.PARACHAIN_STAKING }, { label: t('ui.EARNING.screen.EarningPositions.singleFarming'), value: YieldPoolType.SINGLE_FARMING }, - { label: t('ui.EARNING.screen.EarningPositions.subnetStaking'), value: YieldPoolType.SUBNET_STAKING } + { label: t('ui.EARNING.screen.EarningPositions.subnetStaking'), value: YieldPoolType.SUBNET_STAKING }, + { label: t('ui.EARNING.screen.EarningPositions.delegatedStaking'), value: YieldPoolType.DELEGATED_STAKING } ]; const filterFunction = useMemo<(item: ExtraYieldPositionInfo) => boolean>(() => { diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPools/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPools/index.tsx index c6f3be5fe0f..2bbcf04378c 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPools/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPools/index.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils'; -import { _STAKING_CHAIN_GROUP, RELAY_HANDLER_DIRECT_STAKING_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; +import { RELAY_HANDLER_DIRECT_STAKING_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { isLendingPool, isLiquidPool } from '@subwallet/extension-base/services/earning-service/utils'; import { YieldPoolInfo, YieldPoolType } from '@subwallet/extension-base/types'; import { EmptyList, FilterModal, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; @@ -63,7 +63,8 @@ function Component ({ poolGroup, symbol }: ComponentProps) { { label: t('ui.EARNING.screen.EarningPools.lending'), value: YieldPoolType.LENDING }, { label: t('ui.EARNING.screen.EarningPools.parachainStaking'), value: YieldPoolType.PARACHAIN_STAKING }, { label: t('ui.EARNING.screen.EarningPools.singleFarming'), value: YieldPoolType.SINGLE_FARMING }, - { label: t('ui.EARNING.screen.EarningPools.subnetStaking'), value: YieldPoolType.SUBNET_STAKING } + { label: t('ui.EARNING.screen.EarningPools.subnetStaking'), value: YieldPoolType.SUBNET_STAKING }, + { label: t('ui.EARNING.screen.EarningPools.delegatedStaking'), value: YieldPoolType.DELEGATED_STAKING } ], [t]); const positionSlugs = useMemo(() => { @@ -158,6 +159,8 @@ function Component ({ poolGroup, symbol }: ComponentProps) { return true; } else if (filter === YieldPoolType.SUBNET_STAKING && item.type === YieldPoolType.SUBNET_STAKING) { return true; + } else if (filter === YieldPoolType.DELEGATED_STAKING && item.type === YieldPoolType.DELEGATED_STAKING) { + return true; } // Uncomment the following code block if needed // else if (filter === YieldPoolType.PARACHAIN_STAKING && item.type === YieldPoolType.PARACHAIN_STAKING) { diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx index 1b759fa1fbe..d4e3f62e7c5 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx @@ -53,6 +53,7 @@ function Component ({ className, closeAlert, compound, inputAsset, isShowBalance switch (type) { case YieldPoolType.LENDING: case YieldPoolType.LIQUID_STAKING: + case YieldPoolType.DELEGATED_STAKING: return false; case YieldPoolType.SUBNET_STAKING: case YieldPoolType.NATIVE_STAKING: diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx new file mode 100644 index 00000000000..03ddec893d3 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx @@ -0,0 +1,144 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { BalanceType } from '@subwallet/extension-base/types'; +import { NumberDisplay } from '@subwallet/extension-koni-ui/components'; +import { useGetBalance } from '@subwallet/extension-koni-ui/hooks'; +import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { ActivityIndicator, SwModal } from '@subwallet/react-ui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + id: string; + onCancel: () => void; + address?: string; + chain?: string; + tokenSlug?: string; + isSubscribe?: boolean; + extrinsicType?: ExtrinsicType; + symbol: string; + decimals?: number; +}; + +function Component ({ address, chain, className, decimals, extrinsicType, id, isSubscribe, onCancel, symbol, tokenSlug }: Props): React.ReactElement { + const { t } = useTranslation(); + + const { error: errorFree, isLoading: isLoadingFree, nativeTokenBalance: transferableNative } = useGetBalance(chain, address, tokenSlug, isSubscribe, extrinsicType, BalanceType.TRANSFERABLE); + const { error: errorTotal, isLoading: isLoadingTotal, nativeTokenBalance: totalStakedInNative } = useGetBalance(chain, address, tokenSlug, isSubscribe, extrinsicType, BalanceType.TOTAL_STAKE_EQUIVALENT); + + const isLoading = isLoadingFree || isLoadingTotal; + const error = errorFree || errorTotal; + + const transferableValue = useMemo(() => { + return transferableNative?.value || 0; + }, [transferableNative]); + + const stakedValue = useMemo(() => { + return totalStakedInNative?.value || 0; + }, [totalStakedInNative]); + + const overviewItems = useMemo(() => ([ + { + key: 'transferable', + label: t('ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable'), + value: transferableValue + }, + { + key: 'staked', + label: t('ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked'), + value: stakedValue + } + ]), [t, transferableValue, stakedValue]); + + return ( + +
+
+
+ {isLoading && ( +
+ +
+ )} + + {!isLoading && error && ( +
{error}
+ )} + + {!isLoading && !error && overviewItems.map((item) => ( +
+
{item.label}
+ + +
+ ))} +
+
+
+
+ ); +} + +export const TotalEquivalentDetailModal = styled(Component)(({ theme: { token } }: Props) => { + return ({ + '.__container': { + borderRadius: token.borderRadiusLG, + backgroundColor: token.colorBgSecondary, + padding: '12px', + minHeight: 80 + }, + + '.__loading': { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: 60 + }, + + '.__row': { + display: 'flex', + justifyContent: 'space-between' + }, + + '.__row:not(:last-child)': { + marginBottom: token.margin + }, + + '.__label': { + paddingRight: token.paddingSM + }, + + '.__locked-others': { + cursor: 'pointer' + }, + + '.__locked-others-icon': { + color: token.colorTextLight3, + marginLeft: token.marginXXS + }, + + '.__balance': { + display: 'flex', + justifyContent: 'flex-end' + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx b/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx index 40ff3cc1a56..8ad6a2b81ae 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx @@ -6,12 +6,14 @@ import { BalanceType } from '@subwallet/extension-base/types'; import { useGetBalance } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { ActivityIndicator, Icon, Number, Tooltip, Typography } from '@subwallet/react-ui'; +import { ActivityIndicator, Icon, ModalContext, Number, Tooltip, Typography } from '@subwallet/react-ui'; import CN from 'classnames'; import { Info } from 'phosphor-react'; -import React, { useEffect } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import styled, { useTheme } from 'styled-components'; +import { TotalEquivalentDetailModal } from '../../Home/Tokens/TotalEquivalentDetailModal'; + type Props = ThemeProps & { address?: string, tokenSlug?: string; @@ -37,8 +39,23 @@ const Component = ({ address, onBalanceReady, tokenSlug }: Props) => { const { t } = useTranslation(); const { token } = useTheme() as Theme; + const { activeModal, inactiveModal } = useContext(ModalContext); + const totalEquivalentDetailModalId = 'total-equivalent-details-modal'; + const { error, isLoading, nativeTokenBalance, nativeTokenSlug, tokenBalance } = useGetBalance(chain, address, tokenSlug, isSubscribe, extrinsicType, balanceType); + const [lockedDetailsModalVisible, setLockedDetailsModalVisible] = useState(false); + + const handleOpenLockedDetailsModal = useCallback(() => { + setLockedDetailsModalVisible(true); + activeModal(totalEquivalentDetailModalId); + }, [activeModal]); + + const handleCloseLockedDetailsModal = useCallback(() => { + inactiveModal(totalEquivalentDetailModalId); + setLockedDetailsModalVisible(false); + }, [inactiveModal]); + useEffect(() => { onBalanceReady?.(!isLoading && !error); }, [error, isLoading, onBalanceReady]); @@ -119,6 +136,35 @@ const Component = ({ address, ) } + {balanceType === BalanceType.TOTAL_EQUIVALENT && !isLoading && !error && ( + <> + + + + + {lockedDetailsModalVisible && ( + + )} + + ) + } ); }; @@ -153,6 +199,10 @@ const FreeBalance = styled(Component)(({ theme: { token } }: Props) => { '&.ant-typography': { marginBottom: 0 + }, + + '.__info-icon-wrapper': { + cursor: 'pointer' } }; }); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx index b64c36787bf..185d46c92ea 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx @@ -7,15 +7,15 @@ import { _handleDisplayForEarningError, _handleDisplayInsufficientEarningError } import { _getAssetDecimals, _getAssetSymbol } from '@subwallet/extension-base/services/chain-service/utils'; import { isLendingPool, isLiquidPool } from '@subwallet/extension-base/services/earning-service/utils'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { BalanceType, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, ProcessType, SlippageType, SubmitJoinNativeStaking, SubmitJoinNominationPool, SubmitYieldJoinData, ValidatorInfo, YieldPoolType, YieldStepType } from '@subwallet/extension-base/types'; +import { BalanceType, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, ProcessType, SlippageType, StrategyInfo, SubmitJoinDelegateStaking, SubmitJoinNativeStaking, SubmitJoinNominationPool, SubmitYieldJoinData, ValidatorInfo, YieldPoolType, YieldStepType } from '@subwallet/extension-base/types'; import { addLazy } from '@subwallet/extension-base/utils'; import { getId } from '@subwallet/extension-base/utils/getId'; -import { AccountAddressSelector, AlertBox, AmountInput, EarningPoolSelector, EarningValidatorSelector, HiddenInput, InfoIcon, LoadingScreen, MetaInfo } from '@subwallet/extension-koni-ui/components'; +import { AccountAddressSelector, AlertBox, AmountInput, EarningPoolSelector, EarningStrategySelector, EarningValidatorSelector, HiddenInput, InfoIcon, LoadingScreen, MetaInfo } from '@subwallet/extension-koni-ui/components'; import { EarningProcessItem } from '@subwallet/extension-koni-ui/components/Earning'; import { getInputValuesFromString } from '@subwallet/extension-koni-ui/components/Field/AmountInput'; import { EarningInstructionModal } from '@subwallet/extension-koni-ui/components/Modal/Earning'; import { SlippageModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; -import { EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; +import { DELEGATED_STAKING_ALERT_DATA, EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; import { useChainConnection, useCoreCreateReformatAddress, useCreateGetSubnetStakingTokenName, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; @@ -24,7 +24,7 @@ import { fetchPoolTarget, getOptimalYieldPath, submitJoinYieldPool, submitProces import { DEFAULT_YIELD_PROCESS, EarningActionType, earningReducer } from '@subwallet/extension-koni-ui/reducer'; import { store } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, EarnParams, FormCallbacks, FormFieldData, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { convertFieldToObject, getExtrinsicTypeByPoolInfo, parseNominations, reformatAddress, simpleCheckForm } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, getExtrinsicTypeByPoolInfo, parseNominations, reformatAddress, simpleCheckForm, toShort } from '@subwallet/extension-koni-ui/utils'; import { ActivityIndicator, Button, ButtonProps, Form, Icon, Logo, ModalContext, Number, Tooltip } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -142,7 +142,7 @@ const Component = () => { }, [slug, getCurrentConfirmation]); const mustChooseTarget = useMemo( - () => [YieldPoolType.NATIVE_STAKING, YieldPoolType.SUBNET_STAKING, YieldPoolType.NOMINATION_POOL].includes(poolType), + () => [YieldPoolType.NATIVE_STAKING, YieldPoolType.SUBNET_STAKING, YieldPoolType.NOMINATION_POOL, YieldPoolType.DELEGATED_STAKING].includes(poolType), [poolType] ); @@ -151,6 +151,10 @@ const Component = () => { return BalanceType.TOTAL_MINUS_RESERVED; } + if ([YieldPoolType.DELEGATED_STAKING].includes(poolType)) { + return BalanceType.TOTAL_EQUIVALENT; + } + return undefined; }, [poolType]); @@ -282,6 +286,28 @@ const Component = () => { } }); + return result; + } else if (YieldPoolType.DELEGATED_STAKING === poolType) { + const strategyList = _poolTargets as StrategyInfo[]; + + if (!strategyList) { + return []; + } + + const result: StrategyInfo[] = []; + const nominations = parseNominations(poolTargetValue); + const newStrategyList: { [address: string]: StrategyInfo } = {}; + + strategyList.forEach((strategy) => { + newStrategyList[reformatAddress(strategy.address)] = strategy; + }); + nominations.forEach((nomination) => { + if (newStrategyList?.[reformatAddress(nomination)]) { + // remember the format of the address + result.push(newStrategyList[reformatAddress(nomination)]); + } + }); + return result; } else { return []; @@ -324,7 +350,14 @@ const Component = () => { const onFieldsChange: FormCallbacks['onFieldsChange'] = useCallback((changedFields: FormFieldData[], allFields: FormFieldData[]) => { // TODO: field change - const { empty, error } = simpleCheckForm(allFields, ['--asset', '--fromAccountProxy']); + const requiredFields: string[] = ['--asset', '--fromAccountProxy']; + + // Do not validate amount when pool type is delegated staking + if (poolType === YieldPoolType.DELEGATED_STAKING) { + requiredFields.push('--value'); + } + + const { empty, error } = simpleCheckForm(allFields, requiredFields); const values = convertFieldToObject(allFields); @@ -480,7 +513,7 @@ const Component = () => { let processId = processState.processId; const getData = (submitStep: number): SubmitYieldJoinData => { - if ([YieldPoolType.NOMINATION_POOL, YieldPoolType.NATIVE_STAKING, YieldPoolType.SUBNET_STAKING].includes(poolInfo.type) && target) { + if ([YieldPoolType.NOMINATION_POOL, YieldPoolType.NATIVE_STAKING, YieldPoolType.SUBNET_STAKING, YieldPoolType.DELEGATED_STAKING].includes(poolInfo.type) && target) { const targets = poolTargets; if (poolInfo.type === YieldPoolType.NOMINATION_POOL) { @@ -493,6 +526,17 @@ const Component = () => { selectedPool, selectedValidators: targets } as SubmitJoinNominationPool; + } else if (poolInfo.type === YieldPoolType.DELEGATED_STAKING) { + const selectedStrategy = targets[0]; + + return { + slug: slug, + address: from, + amount: 0, + substrateProxyAddress: selectedStrategy.address, + selectedValidators: targets, + substrateProxyDeposit: poolInfo.proxyDeposit + } as unknown as SubmitJoinDelegateStaking; } else { return { slug: slug, @@ -686,6 +730,8 @@ const Component = () => { }, [currentConfirmation, mktCampaignModalContext, onSubmit, renderConfirmationButtons]); const isSubnetStaking = useMemo(() => [YieldPoolType.SUBNET_STAKING].includes(poolType), [poolType]); + const isDelegatedStaking = useMemo(() => [YieldPoolType.DELEGATED_STAKING].includes(poolType), [poolType]); + const subnetToken = useMemo(() => { return getSubnetStakingTokenName(poolInfo.chain, poolInfo.metadata.subnetData?.netuid || 0); }, [getSubnetStakingTokenName, poolInfo.chain, poolInfo.metadata.subnetData?.netuid]); @@ -831,13 +877,54 @@ const Component = () => { }, [amountValue, assetDecimals, earningRate, inputAsset.symbol, isDisabledSubnetContent, isSlippageAcceptable, maxSlippage.slippage, onOpenSlippageModal, poolChain, poolInfo.metadata.shortName, poolInfo.metadata?.subnetData?.subnetSymbol, subnetToken, t]); // For subnet staking + const renderDelegatedStaking = useCallback(() => { + const targeted = poolTargets?.[0]; + + if (!targeted || !('address' in targeted)) { + return null; + } + + const proxyDeposit = poolInfo && 'proxyDeposit' in poolInfo ? poolInfo.proxyDeposit : 0; + + return ( + <> + {'constituents' in targeted && targeted.constituents?.length > 0 && ( + + {targeted.constituents.length}{' '} + {targeted.constituents.length > 1 + ? t('ui.TRANSACTION.screen.Transaction.Earn.subnets') + : t('ui.TRANSACTION.screen.Transaction.Earn.subnet')} + + )} + + + {toShort(targeted.address)} + + + {!!proxyDeposit && + } + + ); + }, [assetDecimals, inputAsset.symbol, poolInfo, poolTargets, t]); const isDisabledButton = useMemo( () => // checkMintLoading || stepLoading || !!connectionError || - !amountValue || + (!amountValue && !isDelegatedStaking) || !isBalanceReady || isFormInvalid || submitLoading || @@ -845,7 +932,7 @@ const Component = () => { !isSlippageAcceptable || (mustChooseTarget && !poolTargetValue), - [stepLoading, connectionError, amountValue, isBalanceReady, isFormInvalid, submitLoading, targetLoading, isSlippageAcceptable, mustChooseTarget, poolTargetValue] + [stepLoading, connectionError, amountValue, isDelegatedStaking, isBalanceReady, isFormInvalid, submitLoading, targetLoading, isSlippageAcceptable, mustChooseTarget, poolTargetValue] ); const renderMetaInfo = useCallback(() => { @@ -911,14 +998,16 @@ const Component = () => { /> )} - {!isSubnetStaking - ? ( - - ) - : (renderSubnetStaking()) + {(isDelegatedStaking && !!poolTargetValue) + ? (renderDelegatedStaking()) + : !isSubnetStaking + ? ( + + ) + : (renderSubnetStaking()) } {showFee && ( { )} ); - }, [amountValue, assetDecimals, chainAsset, chainValue, currencyData?.isPrefix, currencyData.symbol, estimatedFee, inputAsset.symbol, isSubnetStaking, poolInfo.metadata, poolInfo.statistic, poolInfo?.type, poolTargets, renderSubnetStaking, t]); + }, [amountValue, assetDecimals, chainAsset, chainValue, currencyData?.isPrefix, currencyData.symbol, estimatedFee, inputAsset.symbol, isDelegatedStaking, isSubnetStaking, poolInfo.metadata, poolInfo.statistic, poolInfo?.type, poolTargetValue, poolTargets, renderDelegatedStaking, renderSubnetStaking, t]); const onPreCheck = usePreCheckAction(fromValue); @@ -1278,31 +1367,33 @@ const Component = () => { chain={poolInfo.chain} hidden={[YieldStepType.XCM].includes(submitStepType)} isSubscribe={true} - label={`${t('ui.TRANSACTION.screen.Transaction.Earn.availableBalance')}:`} + label={balanceTypeForPool === BalanceType.TOTAL_EQUIVALENT ? `${t('ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent', { symbol: inputAsset.symbol })}` : `${t('ui.TRANSACTION.screen.Transaction.Earn.availableBalance')}`} tokenSlug={inputAsset.slug} /> - - - - - -
- -
- + {!isDelegatedStaking && ( + <> + + + + +
+ +
+ + )} {poolType === YieldPoolType.NOMINATION_POOL && ( { /> )} + + {poolType === YieldPoolType.DELEGATED_STAKING && + ( + + + + )} {renderMetaInfo()} - + {isDelegatedStaking + ? ( + ) + : ( + + )} {!isSlippageAcceptable && (
diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx index d74ccf927ce..d4eed534326 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx @@ -6,16 +6,17 @@ import { AmountData, ExtrinsicType, NominationInfo } from '@subwallet/extension- import { getValidatorLabel } from '@subwallet/extension-base/koni/api/staking/bonding/utils'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/earning-service/constants'; import { isActionFromValidator } from '@subwallet/extension-base/services/earning-service/utils'; -import { AccountJson, RequestYieldLeave, SlippageType, SpecialYieldPoolMetadata, SubnetYieldPositionInfo, YieldPoolInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; +import { AccountJson, DelegatedStrategyInfo, RequestRemoveSubstrateProxyAccount, RequestYieldLeave, SlippageType, SpecialYieldPoolMetadata, SubnetYieldPositionInfo, YieldPoolInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { AccountSelector, AlertBox, AmountInput, HiddenInput, InstructionItem, MetaInfo, NominationSelector } from '@subwallet/extension-koni-ui/components'; -import { BN_ZERO, UNSTAKE_ALERT_DATA, UNSTAKE_BIFROST_ALERT_DATA, UNSTAKE_BITTENSOR_ALERT_DATA, UNSTAKE_TANSSI_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; +import { BN_ZERO, UNSTAKE_ALERT_DATA, UNSTAKE_BIFROST_ALERT_DATA, UNSTAKE_BITTENSOR_ALERT_DATA, UNSTAKE_DELEGATED_STRATEGY, UNSTAKE_TANSSI_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; import { useHandleSubmitTransaction, useInitValidateTransaction, usePreCheckAction, useRestoreTransaction, useSelector, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; import { useTaoStakingFee } from '@subwallet/extension-koni-ui/hooks/earning/useTaoStakingFee'; import { yieldSubmitLeavePool } from '@subwallet/extension-koni-ui/messaging'; +import { handleRemoveSubstrateProxyAccount } from '@subwallet/extension-koni-ui/messaging/transaction/substrateProxy'; import { FormCallbacks, FormFieldData, ThemeProps, UnStakeParams } from '@subwallet/extension-koni-ui/types'; -import { convertFieldToObject, getBannerButtonIcon, getEarningTimeText, noop, simpleCheckForm } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, getBannerButtonIcon, getEarningTimeText, noop, simpleCheckForm, toShort } from '@subwallet/extension-koni-ui/utils'; import { BackgroundIcon, Button, Checkbox, Form, Icon } from '@subwallet/react-ui'; import { getAlphaColor } from '@subwallet/react-ui/lib/theme/themes/default/colorAlgorithm'; import BigN, { BigNumber } from 'bignumber.js'; @@ -119,6 +120,8 @@ const Component: React.FC = () => { // For subnet staking const isSubnetStaking = useMemo(() => [YieldPoolType.SUBNET_STAKING].includes(poolType), [poolType]); + const isDelegateStaking = useMemo(() => [YieldPoolType.DELEGATED_STAKING].includes(poolType), [poolType]); + const [maxSlippage, setMaxSlippage] = useState({ slippage: new BigN(0.005), isCustomType: true }); const isDisabledSubnetContent = useMemo( @@ -280,6 +283,14 @@ const Component: React.FC = () => { [chainInfoMap, chainValue] ); + const nominators = useMemo(() => { + if (fromValue && positionInfo?.nominations && positionInfo.nominations.length) { + return positionInfo.nominations.filter((n) => new BigN(n.activeStake || '0').gt(BN_ZERO)); + } + + return []; + }, [fromValue, positionInfo?.nominations]); + const exType = useMemo(() => { if (poolType === YieldPoolType.NOMINATION_POOL || poolType === YieldPoolType.NATIVE_STAKING || poolType === YieldPoolType.SUBNET_STAKING) { return ExtrinsicType.STAKING_UNBOND; @@ -313,6 +324,10 @@ const Component: React.FC = () => { } } + if (poolType === YieldPoolType.DELEGATED_STAKING) { + return ExtrinsicType.REMOVE_SUBSTRATE_PROXY_ACCOUNT; + } + return ExtrinsicType.STAKING_UNBOND; }, [poolType, chainValue, slug]); @@ -379,6 +394,25 @@ const Component: React.FC = () => { // send unstake transaction const sendPromise = (signerSubstrateProxyAddress?: string) => { + if (isDelegateStaking) { + const nominator = nominators[0] as DelegatedStrategyInfo; + + const selectedSubstrateProxyAccount = { + substrateProxyAddress: nominator.validatorAddress, + substrateProxyType: nominator.substrateProxyType, + delay: nominator.delay + }; + + const request: RequestRemoveSubstrateProxyAccount = { + address: from, + chain, + poolInfo, + selectedSubstrateProxyAccounts: [selectedSubstrateProxyAccount] + }; + + return handleRemoveSubstrateProxyAccount({ ...request, signerSubstrateProxyAddress }).then(onSuccess); + } + return yieldSubmitLeavePool({ ...request, signerSubstrateProxyAddress @@ -412,7 +446,7 @@ const Component: React.FC = () => { setLoading(false); }); }, 300); - }, [currentValidator, exType, maxSlippage.slippage, mustChooseValidator, onError, onSuccess, poolInfo, positionInfo, selectSubstrateProxyAccountsToSign, stakingFee]); + }, [currentValidator, exType, isDelegateStaking, maxSlippage.slippage, mustChooseValidator, nominators, onError, onSuccess, poolInfo, positionInfo, selectSubstrateProxyAccountsToSign, stakingFee]); const onClickSubmit = useCallback((values: UnStakeParams) => { if (currentConfirmation) { @@ -454,14 +488,6 @@ const Component: React.FC = () => { return accounts.filter(filterAccount(allPositions, chainInfoMap, poolInfo)); }, [accounts, allPositions, chainInfoMap, poolInfo]); - const nominators = useMemo(() => { - if (fromValue && positionInfo?.nominations && positionInfo.nominations.length) { - return positionInfo.nominations.filter((n) => new BigN(n.activeStake || '0').gt(BN_ZERO)); - } - - return []; - }, [fromValue, positionInfo?.nominations]); - useEffect(() => { if (poolInfo.metadata.availableMethod.defaultUnstake && poolInfo.metadata.availableMethod.fastUnstake) { // @@ -507,6 +533,10 @@ const Component: React.FC = () => { }, [chainValue]); const unstakeAlertData = useMemo(() => { + if (poolType === YieldPoolType.DELEGATED_STAKING) { + return UNSTAKE_DELEGATED_STRATEGY; + } + switch (true) { case poolChain === 'bifrost_dot': return UNSTAKE_BIFROST_ALERT_DATA; @@ -519,7 +549,7 @@ const Component: React.FC = () => { default: return UNSTAKE_ALERT_DATA; } - }, [poolChain]); + }, [poolChain, poolType]); return ( <> @@ -567,37 +597,51 @@ const Component: React.FC = () => { poolInfo={poolInfo} /> - - { - mustChooseValidator && ( + {!(poolInfo.type === YieldPoolType.DELEGATED_STAKING) + ? ( <> - {renderBounded()} - - ) - } - - + {mustChooseValidator && ( + <> + {renderBounded()} + + )} + + + + { + !isDisabledSubnetContent && earningRate > 0 && ( + <> + {renderRate()} + + ) + } - { - !isDisabledSubnetContent && earningRate > 0 && ( - <> - {renderRate()} + {!mustChooseValidator && renderBounded()} ) - } - - {!mustChooseValidator && renderBounded()} - + : ( + + + + {nominators[0].validatorIdentity || toShort(nominators[0].validatorAddress)} + + + )}
+ )} + > + {`${delegatedConstituents.length} ${delegatedConstituents.length > 1 + ? t('ui.EARNING.components.Modal.Earning.EarningInfo.subnets') + : t('ui.EARNING.components.Modal.Earning.EarningInfo.subnet')}`} + + + {delegatedStrategyApy !== undefined && ( + + )} + + - ) - : ( + -
- - {poolInfo.metadata.shortName} +
+ + {toShort(delegatedNomination?.validatorAddress || '-', 6)} + + + + + + +
- )} - {totalApy !== undefined && ( + + + + )} + + {!isDelegatedStaking && !isSubnetStaking && ( + + )} + + {!isDelegatedStaking && isSubnetStaking && ( + +
+ + {poolInfo.metadata.shortName} +
+
+ )} + + {!isDelegatedStaking && totalApy !== undefined && ( )} - - {unstakePeriod !== undefined && ( + {!isDelegatedStaking && ( + + )} + {!isDelegatedStaking && unstakePeriod !== undefined && ( {(poolInfo.type === YieldPoolType.LIQUID_STAKING || poolInfo.type === YieldPoolType.SUBNET_STAKING) && Up to} {getEarningTimeText(t, unstakePeriod)} @@ -110,6 +251,28 @@ export const EarningInfoPart = styled(Component)(({ theme: { token } }: P '.__label': { paddingRight: token.paddingXXS }, + '.__proxy-address': { + display: 'flex', + gap: '4px' + }, + '.__proxy-address-copy': { + cursor: 'pointer' + }, + '.__constituents-info-label': { + border: 0, + padding: 0, + background: 'transparent', + color: token.colorTextLight4, + display: 'inline-flex', + alignItems: 'center', + gap: token.sizeXXS + + }, + + '.__constituents-info-button': { + cursor: 'pointer' + }, + '.__subnet-wrapper': { display: 'flex', alignItems: 'center', diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx index 761cec8a82c..1a73a40f4ac 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx @@ -331,7 +331,9 @@ function Component ({ compound, diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx index 03ddec893d3..5429ece3172 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/TotalEquivalentDetailModal.tsx @@ -131,6 +131,10 @@ export const TotalEquivalentDetailModal = styled(Component)(({ theme: { t cursor: 'pointer' }, + '.error-message': { + color: token.colorError + }, + '.__locked-others-icon': { color: token.colorTextLight3, marginLeft: token.marginXXS diff --git a/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx b/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx index 8ad6a2b81ae..76cbe484867 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/parts/FreeBalance.tsx @@ -9,7 +9,7 @@ import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { ActivityIndicator, Icon, ModalContext, Number, Tooltip, Typography } from '@subwallet/react-ui'; import CN from 'classnames'; import { Info } from 'phosphor-react'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import styled, { useTheme } from 'styled-components'; import { TotalEquivalentDetailModal } from '../../Home/Tokens/TotalEquivalentDetailModal'; @@ -44,16 +44,12 @@ const Component = ({ address, const { error, isLoading, nativeTokenBalance, nativeTokenSlug, tokenBalance } = useGetBalance(chain, address, tokenSlug, isSubscribe, extrinsicType, balanceType); - const [lockedDetailsModalVisible, setLockedDetailsModalVisible] = useState(false); - const handleOpenLockedDetailsModal = useCallback(() => { - setLockedDetailsModalVisible(true); activeModal(totalEquivalentDetailModalId); }, [activeModal]); const handleCloseLockedDetailsModal = useCallback(() => { inactiveModal(totalEquivalentDetailModalId); - setLockedDetailsModalVisible(false); }, [inactiveModal]); useEffect(() => { @@ -148,20 +144,18 @@ const Component = ({ address, /> - {lockedDetailsModalVisible && ( - - )} + ) } diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx index 83cec224a2f..b0d5a4a2221 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx @@ -10,12 +10,12 @@ import { SWTransactionResponse } from '@subwallet/extension-base/services/transa import { AccountProxyType, BalanceType, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, ProcessType, SlippageType, StrategyInfo, SubmitJoinDelegateStaking, SubmitJoinNativeStaking, SubmitJoinNominationPool, SubmitYieldJoinData, ValidatorInfo, YieldPoolType, YieldStepType } from '@subwallet/extension-base/types'; import { addLazy } from '@subwallet/extension-base/utils'; import { getId } from '@subwallet/extension-base/utils/getId'; -import { AccountAddressSelector, AlertBox, AmountInput, EarningPoolSelector, EarningStrategySelector, EarningValidatorSelector, HiddenInput, InfoIcon, LoadingScreen, MetaInfo } from '@subwallet/extension-koni-ui/components'; +import { AccountAddressSelector, AlertBox, AmountInput, EarningPoolSelector, EarningStrategySelector, EarningValidatorSelector, HiddenInput, InfoIcon, LoadingScreen, MetaInfo, NumberDisplay } from '@subwallet/extension-koni-ui/components'; import { EarningProcessItem } from '@subwallet/extension-koni-ui/components/Earning'; import { getInputValuesFromString } from '@subwallet/extension-koni-ui/components/Field/AmountInput'; -import { EarningInstructionModal } from '@subwallet/extension-koni-ui/components/Modal/Earning'; +import { EarningConstituentsModal, EarningInstructionModal } from '@subwallet/extension-koni-ui/components/Modal/Earning'; import { SlippageModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; -import { DELEGATED_STAKING_ALERT_DATA, EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; +import { DELEGATED_STAKING_ALERT_DATA, EARNING_CONSTITUENTS_MODAL, EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; import { useChainConnection, useCoreCreateReformatAddress, useCreateGetSubnetStakingTokenName, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; @@ -864,6 +864,10 @@ const Component = () => { }); }, [notify, t]); + const onOpenConstituentsModal = useCallback(() => { + activeModal(EARNING_CONSTITUENTS_MODAL); + }, [activeModal]); + // For subnet staking const renderDelegatedStaking = useCallback(() => { const targeted = poolTargets?.[0]; @@ -873,21 +877,37 @@ const Component = () => { } const proxyDeposit = poolInfo && 'proxyDeposit' in poolInfo ? poolInfo.proxyDeposit : 0; + const constituents = 'constituents' in targeted && targeted.constituents ? targeted.constituents : []; return ( <> - {'constituents' in targeted && targeted.constituents?.length > 0 && ( + {constituents.length > 0 && ( - {targeted.constituents.length}{' '} - {targeted.constituents.length > 1 - ? t('ui.TRANSACTION.screen.Transaction.Earn.subnets') - : t('ui.TRANSACTION.screen.Transaction.Earn.subnet')} +
+ {`${constituents.length} ${constituents.length > 1 + ? t('ui.TRANSACTION.screen.Transaction.Earn.subnets') + : t('ui.TRANSACTION.screen.Transaction.Earn.subnet')}`} +
+ +
+
+ )} + {
- {!!proxyDeposit && - } + {!!proxyDeposit && ( + +
+ + +
+ +
+
+
+
+ )} ); - }, [_onClickCopyButton, assetDecimals, inputAsset.symbol, poolInfo, poolTargets, t]); + }, [_onClickCopyButton, assetDecimals, onOpenConstituentsModal, inputAsset.symbol, poolInfo, poolTargets, t]); const isDisabledButton = useMemo( () => @@ -1452,12 +1490,18 @@ const Component = () => { {isDelegatedStaking ? ( - ) + <> + {DELEGATED_STAKING_ALERT_DATA.map((alert, index) => ( + + ))} + + ) : ( { openAlert={openAlert} slug={slug} /> + {isSlippageModalVisible && ( (({ theme: { token } }: Props) => { '.__proxy-address-copy': { cursor: 'pointer' + }, + + '.__proxy-deposit-value': { + display: 'inline-flex', + alignItems: 'center', + gap: token.sizeXXS + }, + + '.__proxy-deposit-info-button': { + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center' + }, + + '.__constituents-info': { + border: 0, + padding: 0, + background: 'transparent', + color: token.colorTextLight4, + display: 'inline-flex', + alignItems: 'center', + gap: token.sizeXXS + + }, + + '.__constituents-info-button': { + cursor: 'pointer' } }; }); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx index 4cea3fd12f5..db6f20faed6 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Unbond.tsx @@ -414,11 +414,10 @@ const Component: React.FC = () => { } return yieldSubmitLeavePool({ - ...request, + ...request }); }; - setLoading(true); setTimeout(() => { @@ -429,8 +428,7 @@ const Component: React.FC = () => { setLoading(false); }); }, 300); - - }, [currentValidator, exType, isDelegateStaking, maxSlippage.slippage, mustChooseValidator, nominators, onError, onSuccess, poolInfo, positionInfo, stakingFee]); + }, [currentValidator, isDelegateStaking, maxSlippage.slippage, mustChooseValidator, nominators, onError, onSuccess, poolInfo, positionInfo, stakingFee]); const onClickSubmit = useCallback((values: UnStakeParams) => { if (currentConfirmation) { diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/EarningConstituentsModal.tsx b/packages/extension-koni-ui/src/components/Modal/Earning/EarningConstituentsModal.tsx new file mode 100644 index 00000000000..168decaf464 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Earning/EarningConstituentsModal.tsx @@ -0,0 +1,134 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { EARNING_CONSTITUENTS_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { useCreateGetSubnetStakingTokenName, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getAssetDisplayName } from '@subwallet/extension-koni-ui/utils'; +import { Logo, ModalContext, SwModal } from '@subwallet/react-ui'; +import React, { useCallback, useContext, useMemo } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + constituents: string[]; +}; + +function Component (props: Props): React.ReactElement { + const { className, constituents } = props; + const { t } = useTranslation(); + const { inactiveModal } = useContext(ModalContext); + const getSubnetStakingTokenName = useCreateGetSubnetStakingTokenName(); + const assetRegistryMap = useSelector((state) => state.assetRegistry.assetRegistry); + + const assetRegistryLowerMap = useMemo(() => { + const rs: Record = {}; + + Object.entries(assetRegistryMap).forEach(([key, value]) => { + rs[key.toLowerCase()] = value; + }); + + return rs; + }, [assetRegistryMap]); + + const constituentsData = useMemo(() => { + return constituents.map((item) => { + const netuid = Number(item); + const token = Number.isNaN(netuid) ? undefined : getSubnetStakingTokenName('bittensor', netuid); + const subnetAsset = token ? assetRegistryLowerMap[token.toLowerCase()] : undefined; + const netuidText = Number.isNaN(netuid) ? item : String(netuid); + const shortLabel = getAssetDisplayName(subnetAsset, `SN${netuidText}`); + + return { + key: `${item}`, + netuid: netuidText, + shortLabel, + token + }; + }); + }, [assetRegistryLowerMap, constituents, getSubnetStakingTokenName]); + + const onCloseConstituentsModal = useCallback(() => { + inactiveModal(EARNING_CONSTITUENTS_MODAL); + }, [inactiveModal]); + + return ( + +
+ {constituentsData.map((item) => ( +
+
+ + {item.shortLabel} +
+ {item.netuid.padStart(2, '0')} +
+ ))} +
+
+ ); +} + +const EarningConstituentsModal = styled(Component)(({ theme: { token } }: Props) => { + return ({ + '.__constituents-list': { + background: token.colorBgSecondary, + borderRadius: token.borderRadiusLG, + padding: token.paddingSM, + display: 'flex', + flexDirection: 'column', + gap: token.sizeSM + }, + + '.__constituent-item': { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: token.sizeSM + }, + + '.__constituent-left': { + display: 'flex', + alignItems: 'center', + gap: token.sizeXS, + minWidth: 0 + }, + + '.__constituent-name': { + color: token.colorTextTertiary, + fontSize: token.fontSizeHeading6, + lineHeight: token.lineHeightHeading6, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + }, + + '.__constituent-netuid': { + minWidth: 34, + textAlign: 'center', + borderRadius: token.borderRadiusSM, + padding: '2px 8px', + background: 'rgba(217, 217, 217, 0.1)', + color: token.colorTextLight2, + fontSize: token.fontSizeSM, + fontWeight: token.headingFontWeight + } + }); +}); + +export default EarningConstituentsModal; diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx index fa0ca21b2e1..e2f7b75db4f 100644 --- a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx @@ -261,6 +261,7 @@ const Component: React.FC = (props: Props) => { replaceEarningValue(_item, '{maintainBalance}', maintainBalance); replaceEarningValue(_item, '{maintainSymbol}', maintainSymbol); replaceEarningValue(_item, '{shortName}', poolInfo.metadata.shortName); + replaceEarningValue(_item, '{proxyDeposit}', getInputValuesFromString((poolInfo)?.proxyDeposit || '0', maintainDecimals || 0)); if (paidOut !== undefined) { replaceEarningValue(_item, '{paidOut}', paidOut >= 1 ? paidOut.toString() : (paidOut * 60).toString()); diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/EarningStrategyDetailModal.tsx b/packages/extension-koni-ui/src/components/Modal/Earning/EarningStrategyDetailModal.tsx index a65e9ded995..03edac6b222 100644 --- a/packages/extension-koni-ui/src/components/Modal/Earning/EarningStrategyDetailModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Earning/EarningStrategyDetailModal.tsx @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import { MetaInfo } from '@subwallet/extension-koni-ui/components'; -import { STRATEGY_DETAIL_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { EARNING_CONSTITUENTS_MODAL, STRATEGY_DETAIL_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useGetChainPrefixBySlug } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { StrategyDataType, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { ModalContext, SwModal } from '@subwallet/react-ui'; +import { Icon, ModalContext, SwModal } from '@subwallet/react-ui'; +import { Info } from 'phosphor-react'; import React, { useCallback, useContext } from 'react'; import styled from 'styled-components'; +import EarningConstituentsModal from './EarningConstituentsModal'; + type Props = ThemeProps & { onCancel?: () => void; strategyItem: StrategyDataType; @@ -26,16 +29,21 @@ function Component (props: Props): React.ReactElement { minBond: minStake, symbol } = strategyItem; const { t } = useTranslation(); - const { inactiveModal } = useContext(ModalContext); + const { activeModal, inactiveModal } = useContext(ModalContext); const networkPrefix = useGetChainPrefixBySlug(chain); const _onCancel = useCallback(() => { inactiveModal(STRATEGY_DETAIL_MODAL); + inactiveModal(EARNING_CONSTITUENTS_MODAL); onCancel && onCancel(); }, [inactiveModal, onCancel]); + const onOpenConstituentsModal = useCallback(() => { + activeModal(EARNING_CONSTITUENTS_MODAL); + }, [activeModal]); + return ( { - {constituents.length} {constituents.length > 1 ? t('ui.EARNING.components.Modal.Earning.StrategyDetail.subnets') : t('ui.EARNING.components.Modal.Earning.StrategyDetail.subnet')} +
+ {constituents.length} {constituents.length > 1 ? t('ui.EARNING.components.Modal.Earning.StrategyDetail.subnets') : t('ui.EARNING.components.Modal.Earning.StrategyDetail.subnet')} +
+ +
+
+ + {!!earningEstimated && ( (({ theme: { token } '.__maximum-validator .__value': { display: 'flex' }, + '.__tooltip-label': { display: 'flex', alignItems: 'center', cursor: 'pointer', gap: token.sizeXXS + }, + + '.__constituents-info': { + border: 0, + padding: 0, + background: 'transparent', + color: token.colorTextLight4, + display: 'inline-flex', + alignItems: 'center', + gap: token.sizeXXS + }, + + '.__constituents-info-button': { + cursor: 'pointer' } }); }); diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/index.ts b/packages/extension-koni-ui/src/components/Modal/Earning/index.ts index 1d0804d2a3c..ab493756f92 100644 --- a/packages/extension-koni-ui/src/components/Modal/Earning/index.ts +++ b/packages/extension-koni-ui/src/components/Modal/Earning/index.ts @@ -8,3 +8,4 @@ export { default as EarningValidatorDetailModal } from './EarningValidatorDetail export { default as EarningActiveStakeDetailsModal } from './EarningActiveStakeDetailsModal'; export { default as EarningStrategyDetailModal } from './EarningStrategyDetailModal'; export { default as EarningBittensorClaimRewardTypeModal } from './EarningBittensorClaimRewardTypeModal'; +export { default as EarningConstituentsModal } from './EarningConstituentsModal'; diff --git a/packages/extension-koni-ui/src/constants/earning/staticData.ts b/packages/extension-koni-ui/src/constants/earning/staticData.ts index 856c92ef685..0a51901f997 100644 --- a/packages/extension-koni-ui/src/constants/earning/staticData.ts +++ b/packages/extension-koni-ui/src/constants/earning/staticData.ts @@ -232,13 +232,13 @@ export const EARNING_DATA_RAW = { icon: 'Aperture', title: 'Minimum balance required', description: - 'A minimum {maintainSymbol}-equivalent balance of {maintainBalance} {maintainSymbol} is required to delegate to any strategy', + 'A minimum {maintainSymbol}-equivalent balance of {maintainBalance} {maintainSymbol} with at least {proxyDeposit} {maintainSymbol} in your transferable balance is required to start delegating', iconColor: '#e6478e' }, { icon: 'Coins', title: 'Delegate and undelegate', - description: 'Once delegated, the staking proxy will have full control over your Bittensor balances. You can remove it at any time by undelegating. Keep in mind thay neither action is automated and will incur fees', + description: 'Once delegated, the staking proxy will have control over your Bittensor balances while you retain full control of your funds. You can remove it at any time by undelegating. Keep in mind that neither action is automated and will incur fees', iconColor: '#e6dc25' }, { @@ -276,10 +276,16 @@ export const STAKE_ALERT_DATA = { 'Don’t stake all your funds. Keep in mind that you need at least {tokenAmount} in your free balance to pay gas fees for claiming rewards, unstaking and withdrawing' }; -export const DELEGATED_STAKING_ALERT_DATA = { - title: 'All balances will be used', - description: 'Your entire {symbol}-equivalent balance will be used for staking and setting up proxy' -}; +export const DELEGATED_STAKING_ALERT_DATA = [ + { + title: 'Staking requires full balances', + description: 'Your entire {symbol}-equivalent balance will be used for staking and setting up proxy' + }, + { + title: 'All existing proxies will be removed', + description: 'If your account has any existing proxies, they will be removed before staking on this strategy' + } +]; export const UNSTAKE_ALERT_DATA = [ { diff --git a/packages/extension-koni-ui/src/constants/modal.ts b/packages/extension-koni-ui/src/constants/modal.ts index cd4989dda4d..04b5c2fbe11 100644 --- a/packages/extension-koni-ui/src/constants/modal.ts +++ b/packages/extension-koni-ui/src/constants/modal.ts @@ -17,6 +17,7 @@ export const CONFIRMATION_SCAN_MODAL = 'confirmation-scan-modal'; export const REQUEST_CAMERA_ACCESS_MODAL = 'request-camera-access-modal'; export const VALIDATOR_DETAIL_MODAL = 'validator-detail-modal'; export const STRATEGY_DETAIL_MODAL = 'strategy-detail-modal'; +export const EARNING_CONSTITUENTS_MODAL = 'strategy-detail-modal-constituents'; export const NOTIFICATION_DETAIL_MODAL = 'notification-detail-modal'; export const HISTORY_DETAIL_MODAL = 'history-detail-modal'; export const MULTISIG_HISTORY_INFO_MODAL = 'multisig-history-info-modal'; diff --git a/packages/extension-koni/public/locales/en/translation.json b/packages/extension-koni/public/locales/en/translation.json index ae6c03b497d..9e58d40af18 100644 --- a/packages/extension-koni/public/locales/en/translation.json +++ b/packages/extension-koni/public/locales/en/translation.json @@ -1554,7 +1554,7 @@ "ui.TRANSACTION.screen.Transaction.ClaimReward.stakeRewardAfterClaim": "Stake reward after claim", "ui.TRANSACTION.screen.Transaction.Earn.dualStakingFreezeWarning": "You're currently staking {{symbol}} via direct nomination. Due to {{originChain}}'s upcoming changes, continuing to stake via nomination pool will lead to pool-staked funds being frozen (e.g., can't unstake, claim rewards)", "ui.TRANSACTION.screen.Transaction.Earn.continueStaking": "Continue staking?", - "ui.TRANSACTION.screen.Transaction.Earn.subnet": "Subnet", + "ui.TRANSACTION.screen.Transaction.Earn.subnet": "subnet", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.subnet": "Subnet", "ui.TRANSACTION.screen.Transaction.Earn.expectedAlphaAmount": "Expected alpha amount", "ui.TRANSACTION.screen.Transaction.Earn.conversionRate": "Conversion rate", @@ -1562,6 +1562,7 @@ "ui.TRANSACTION.screen.Transaction.Earn.youllReceive": "You'll receive", "ui.TRANSACTION.screen.Transaction.Earn.minimumActiveStake": "Minimum active stake", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.minimumActiveStake": "Minimum active stake", + "ui.EARNING.components.Modal.Earning.StrategyDetail.proxyAddress": "Proxy address", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarningProcess": "Cancel earning process?", "ui.TRANSACTION.screen.Transaction.Earn.confirmCancelEarningProcess": "Going back will cancel the current earning process. Do you wish to cancel?", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarning": "Cancel earning", @@ -2599,12 +2600,12 @@ "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "Subnets", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "Subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", "ui.EARNING.components.StakingItem.Strategy.apy": "APY", - "ui.TRANSACTION.screen.Transaction.Earn.subnetLabel": "Subnet", - "ui.TRANSACTION.screen.Transaction.Earn.subnets": "Subnets", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", @@ -2784,5 +2785,11 @@ "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith": "Sign with" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy" } diff --git a/packages/extension-koni/public/locales/ja/translation.json b/packages/extension-koni/public/locales/ja/translation.json index 4429487a021..951f5ea4a7c 100644 --- a/packages/extension-koni/public/locales/ja/translation.json +++ b/packages/extension-koni/public/locales/ja/translation.json @@ -1592,6 +1592,7 @@ "ui.TRANSACTION.screen.Transaction.Earn.youllReceive": "You'll receive", "ui.TRANSACTION.screen.Transaction.Earn.minimumActiveStake": "Minimum active stake", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.minimumActiveStake": "Minimum active stake", + "ui.EARNING.components.Modal.Earning.StrategyDetail.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarningProcess": "Cancel earning process?", "ui.TRANSACTION.screen.Transaction.Earn.confirmCancelEarningProcess": "Going back will cancel the current earning process. Do you wish to cancel?", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarning": "Cancel earning", @@ -2603,7 +2604,7 @@ "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnetLabel": "", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", @@ -2783,5 +2784,11 @@ "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith": "" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "" } diff --git a/packages/extension-koni/public/locales/ru/translation.json b/packages/extension-koni/public/locales/ru/translation.json index bb6a511c72e..1ebbbd0f8bd 100644 --- a/packages/extension-koni/public/locales/ru/translation.json +++ b/packages/extension-koni/public/locales/ru/translation.json @@ -1592,6 +1592,7 @@ "ui.TRANSACTION.screen.Transaction.Earn.youllReceive": "You'll receive", "ui.TRANSACTION.screen.Transaction.Earn.minimumActiveStake": "Minimum active stake", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.minimumActiveStake": "Minimum active stake", + "ui.EARNING.components.Modal.Earning.StrategyDetail.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarningProcess": "Cancel earning process?", "ui.TRANSACTION.screen.Transaction.Earn.confirmCancelEarningProcess": "Going back will cancel the current earning process. Do you wish to cancel?", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarning": "Cancel earning", @@ -2605,7 +2606,7 @@ "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnetLabel": "", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", @@ -2785,5 +2786,11 @@ "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith": "" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "" } diff --git a/packages/extension-koni/public/locales/vi/translation.json b/packages/extension-koni/public/locales/vi/translation.json index 72025d1bae2..20c10a012eb 100644 --- a/packages/extension-koni/public/locales/vi/translation.json +++ b/packages/extension-koni/public/locales/vi/translation.json @@ -1592,6 +1592,7 @@ "ui.TRANSACTION.screen.Transaction.Earn.youllReceive": "You'll receive", "ui.TRANSACTION.screen.Transaction.Earn.minimumActiveStake": "Minimum active stake", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.minimumActiveStake": "Minimum active stake", + "ui.EARNING.components.Modal.Earning.StrategyDetail.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarningProcess": "Cancel earning process?", "ui.TRANSACTION.screen.Transaction.Earn.confirmCancelEarningProcess": "Going back will cancel the current earning process. Do you wish to cancel?", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarning": "Cancel earning", @@ -2603,7 +2604,7 @@ "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnetLabel": "", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", @@ -2783,5 +2784,11 @@ "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith": "" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "" } diff --git a/packages/extension-koni/public/locales/zh/translation.json b/packages/extension-koni/public/locales/zh/translation.json index 7b3572655c9..72e13466c2e 100644 --- a/packages/extension-koni/public/locales/zh/translation.json +++ b/packages/extension-koni/public/locales/zh/translation.json @@ -1592,6 +1592,7 @@ "ui.TRANSACTION.screen.Transaction.Earn.youllReceive": "You'll receive", "ui.TRANSACTION.screen.Transaction.Earn.minimumActiveStake": "Minimum active stake", "ui.EARNING.screen.EarningPositionDetail.EarningInfo.minimumActiveStake": "Minimum active stake", + "ui.EARNING.components.Modal.Earning.StrategyDetail.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarningProcess": "Cancel earning process?", "ui.TRANSACTION.screen.Transaction.Earn.confirmCancelEarningProcess": "Going back will cancel the current earning process. Do you wish to cancel?", "ui.TRANSACTION.screen.Transaction.Earn.cancelEarning": "Cancel earning", @@ -2603,7 +2604,7 @@ "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnetLabel": "", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", @@ -2783,5 +2784,11 @@ "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.signWith": "" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "" } From 27340fd13a642dd4e778370b023ee4e697253434 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Tue, 24 Mar 2026 17:01:13 +0700 Subject: [PATCH 07/11] [Issue-#4946] Apply i18n for some message in bg --- .../handlers/delegated-staking/tao.ts | 25 ++++++++++++++++--- .../public/locales/en/translation.json | 4 +++ .../public/locales/ja/translation.json | 4 +++ .../public/locales/ru/translation.json | 4 +++ .../public/locales/vi/translation.json | 4 +++ .../public/locales/zh/translation.json | 4 +++ 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts index 71aa9bf5286..2a8ef9db284 100644 --- a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts +++ b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts @@ -9,6 +9,7 @@ import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-serv import { BalanceType, BasicTxErrorType, DelegatedStrategyInfo, DelegatedYieldPoolInfo, EarningStatus, HandleYieldStepData, OptimalYieldPath, PrimitiveSubstrateProxyAccountItem, RequestDelegateStakingSubmit, RequestEarlyValidateYield, ResponseEarlyValidateYield, StakeCancelWithdrawalParams, StakingTxErrorType, StrategyInfo, SubmitJoinDelegateStaking, SubmitYieldJoinData, TransactionData, UnstakingInfo, YieldPoolInfo, YieldPoolMethodInfo, YieldPoolType, YieldPositionInfo, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; import { BN_TEN, isSameAddress, toBNString } from '@subwallet/extension-base/utils'; import BigNumber from 'bignumber.js'; +import { t } from 'i18next'; import { getAlphaToTaoRate } from '../../utils/alpha-price'; import BaseNativeStakingPoolHandler from '../native-staking/base'; @@ -96,7 +97,7 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool if (!poolInfo) { return { passed: false, - errorMessage: 'There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.' + errorMessage: t('bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError') }; } @@ -133,7 +134,14 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool if (bnTotalEquivalent.lt(bnMaintainBalance) || bnTransferable.lt(bnProxyDeposit)) { return { passed: false, - errorMessage: `You need a minimum total ${symbol}-equivalent balance of ${parsedMaintainBalance} ${symbol} with ${parsedProxyDeposit} ${symbol} on ${this.chainInfo.name} in your transferable balance to start earning` + errorMessage: t('bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning', { + replace: { + chainName: this.chainInfo.name, + parsedMaintainBalance, + parsedProxyDeposit, + symbol + } + }) }; } @@ -586,13 +594,22 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool // If account does not have staking proxy and transferable balance is less than proxy deposit -> error if (!hasStakingProxy && bnTransferable.lt(bnProxyDeposit)) { - errors.push(new TransactionError(StakingTxErrorType.NOT_ENOUGH_MIN_STAKE, `Insufficient ${symbol}-equivalent balance to delegate to this strategy. Select another one and try again`)); + errors.push(new TransactionError(StakingTxErrorType.NOT_ENOUGH_MIN_STAKE, t('bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate', { + replace: { + symbol + } + }))); return errors; } if (bnTotalEquivalent.lt(bnMinBond)) { - errors.push(new TransactionError(StakingTxErrorType.NOT_ENOUGH_MIN_STAKE, `You need at least ${parsedProxyDeposit} ${symbol} in your transferable balance to start earning `)); + errors.push(new TransactionError(StakingTxErrorType.NOT_ENOUGH_MIN_STAKE, t('bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning', { + replace: { + parsedProxyDeposit, + symbol + } + }))); return errors; } diff --git a/packages/extension-koni/public/locales/en/translation.json b/packages/extension-koni/public/locales/en/translation.json index 9e58d40af18..98ec312de33 100644 --- a/packages/extension-koni/public/locales/en/translation.json +++ b/packages/extension-koni/public/locales/en/translation.json @@ -1983,6 +1983,10 @@ "bg.EARNING.services.service.earning.liquidStaking.unstakeMinimumAmount": "You need to unstake at least {{amount}} {{token}}", "bg.EARNING.services.service.earning.nativeStaking.tao.insufficientStakeToEarn": "Insufficient stake. You need to stake at least {{bnMinStake}} {{symbol}} to earn rewards", "bg.EARNING.services.service.earning.nativeStaking.tao.unstakeAmountTooLow": "Amount too low. You need to unstake at least {{bnMinUnstake}} {{symbol}}", + "bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError": "There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning": "You need a minimum total {{symbol}}-equivalent balance of {{parsedMaintainBalance}} {{symbol}} with {{parsedProxyDeposit}} {{symbol}} on {{chainName}} in your transferable balance to start earning", + "bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate": "Insufficient {{symbol}}-equivalent balance to delegate to this strategy. Select another one and try again", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning": "You need at least {{parsedProxyDeposit}} {{symbol}} in your transferable balance to start earning", "bg.ACCOUNT.services.keyring.handler.Derive.accountExistsWithName": "Account already exists under the name \"{{name}}\"", "bg.ACCOUNT.services.keyring.handler.Json.incorrectPassword": "Incorrect password", "bg.ACCOUNT.services.keyring.handler.Json.noAccountsFoundToImport": "No accounts found to import", diff --git a/packages/extension-koni/public/locales/ja/translation.json b/packages/extension-koni/public/locales/ja/translation.json index 951f5ea4a7c..8181a9bae2a 100644 --- a/packages/extension-koni/public/locales/ja/translation.json +++ b/packages/extension-koni/public/locales/ja/translation.json @@ -2013,6 +2013,10 @@ "bg.EARNING.services.service.earning.liquidStaking.unstakeMinimumAmount": "You need to unstake at least {{amount}} {{token}}", "bg.EARNING.services.service.earning.nativeStaking.tao.insufficientStakeToEarn": "Insufficient stake. You need to stake at least {{bnMinStake}} {{symbol}} to earn rewards", "bg.EARNING.services.service.earning.nativeStaking.tao.unstakeAmountTooLow": "Amount too low. You need to unstake at least {{bnMinUnstake}} {{symbol}}", + "bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError": "There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning": "You need a minimum total {{symbol}}-equivalent balance of {{parsedMaintainBalance}} {{symbol}} with {{parsedProxyDeposit}} {{symbol}} on {{chainName}} in your transferable balance to start earning", + "bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate": "Insufficient {{symbol}}-equivalent balance to delegate to this strategy. Select another one and try again", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning": "You need at least {{parsedProxyDeposit}} {{symbol}} in your transferable balance to start earning", "bg.ACCOUNT.services.keyring.handler.Derive.accountExistsWithName": "Account already exists under the name \"{{name}}\"", "bg.ACCOUNT.services.keyring.handler.Json.incorrectPassword": "Incorrect password", "bg.ACCOUNT.services.keyring.handler.Json.noAccountsFoundToImport": "No accounts found to import", diff --git a/packages/extension-koni/public/locales/ru/translation.json b/packages/extension-koni/public/locales/ru/translation.json index 1ebbbd0f8bd..6d0a75aa47d 100644 --- a/packages/extension-koni/public/locales/ru/translation.json +++ b/packages/extension-koni/public/locales/ru/translation.json @@ -2013,6 +2013,10 @@ "bg.EARNING.services.service.earning.liquidStaking.unstakeMinimumAmount": "You need to unstake at least {{amount}} {{token}}", "bg.EARNING.services.service.earning.nativeStaking.tao.insufficientStakeToEarn": "Insufficient stake. You need to stake at least {{bnMinStake}} {{symbol}} to earn rewards", "bg.EARNING.services.service.earning.nativeStaking.tao.unstakeAmountTooLow": "Amount too low. You need to unstake at least {{bnMinUnstake}} {{symbol}}", + "bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError": "There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning": "You need a minimum total {{symbol}}-equivalent balance of {{parsedMaintainBalance}} {{symbol}} with {{parsedProxyDeposit}} {{symbol}} on {{chainName}} in your transferable balance to start earning", + "bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate": "Insufficient {{symbol}}-equivalent balance to delegate to this strategy. Select another one and try again", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning": "You need at least {{parsedProxyDeposit}} {{symbol}} in your transferable balance to start earning", "bg.ACCOUNT.services.keyring.handler.Derive.accountExistsWithName": "Account already exists under the name \"{{name}}\"", "bg.ACCOUNT.services.keyring.handler.Json.incorrectPassword": "Incorrect password", "bg.ACCOUNT.services.keyring.handler.Json.noAccountsFoundToImport": "No accounts found to import", diff --git a/packages/extension-koni/public/locales/vi/translation.json b/packages/extension-koni/public/locales/vi/translation.json index 20c10a012eb..3c23da26dda 100644 --- a/packages/extension-koni/public/locales/vi/translation.json +++ b/packages/extension-koni/public/locales/vi/translation.json @@ -2013,6 +2013,10 @@ "bg.EARNING.services.service.earning.liquidStaking.unstakeMinimumAmount": "You need to unstake at least {{amount}} {{token}}", "bg.EARNING.services.service.earning.nativeStaking.tao.insufficientStakeToEarn": "Insufficient stake. You need to stake at least {{bnMinStake}} {{symbol}} to earn rewards", "bg.EARNING.services.service.earning.nativeStaking.tao.unstakeAmountTooLow": "Amount too low. You need to unstake at least {{bnMinUnstake}} {{symbol}}", + "bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError": "There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning": "You need a minimum total {{symbol}}-equivalent balance of {{parsedMaintainBalance}} {{symbol}} with {{parsedProxyDeposit}} {{symbol}} on {{chainName}} in your transferable balance to start earning", + "bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate": "Insufficient {{symbol}}-equivalent balance to delegate to this strategy. Select another one and try again", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning": "You need at least {{parsedProxyDeposit}} {{symbol}} in your transferable balance to start earning", "bg.ACCOUNT.services.keyring.handler.Derive.accountExistsWithName": "Account already exists under the name \"{{name}}\"", "bg.ACCOUNT.services.keyring.handler.Json.incorrectPassword": "Incorrect password", "bg.ACCOUNT.services.keyring.handler.Json.noAccountsFoundToImport": "No accounts found to import", diff --git a/packages/extension-koni/public/locales/zh/translation.json b/packages/extension-koni/public/locales/zh/translation.json index 72e13466c2e..635cd803093 100644 --- a/packages/extension-koni/public/locales/zh/translation.json +++ b/packages/extension-koni/public/locales/zh/translation.json @@ -2013,6 +2013,10 @@ "bg.EARNING.services.service.earning.liquidStaking.unstakeMinimumAmount": "You need to unstake at least {{amount}} {{token}}", "bg.EARNING.services.service.earning.nativeStaking.tao.insufficientStakeToEarn": "Insufficient stake. You need to stake at least {{bnMinStake}} {{symbol}} to earn rewards", "bg.EARNING.services.service.earning.nativeStaking.tao.unstakeAmountTooLow": "Amount too low. You need to unstake at least {{bnMinUnstake}} {{symbol}}", + "bg.EARNING.services.service.earning.delegatedStaking.tao.fetchPoolInfoError": "There is a problem fetching your data. Check your Internet connection or change the network endpoint and try again.", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumBalanceToStartEarning": "You need a minimum total {{symbol}}-equivalent balance of {{parsedMaintainBalance}} {{symbol}} with {{parsedProxyDeposit}} {{symbol}} on {{chainName}} in your transferable balance to start earning", + "bg.EARNING.services.service.earning.delegatedStaking.tao.insufficientBalanceToDelegate": "Insufficient {{symbol}}-equivalent balance to delegate to this strategy. Select another one and try again", + "bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning": "You need at least {{parsedProxyDeposit}} {{symbol}} in your transferable balance to start earning", "bg.ACCOUNT.services.keyring.handler.Derive.accountExistsWithName": "Account already exists under the name \"{{name}}\"", "bg.ACCOUNT.services.keyring.handler.Json.incorrectPassword": "Incorrect password", "bg.ACCOUNT.services.keyring.handler.Json.noAccountsFoundToImport": "No accounts found to import", From 02245ba7b8e93d7b28540a4cf8169802685e46e5 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Wed, 25 Mar 2026 16:04:46 +0700 Subject: [PATCH 08/11] [Issue-#4946] Chore: fix types and add cmt --- .../src/hooks/earning/useYieldPositionDetail.ts | 4 ++-- packages/extension-koni-ui/src/utils/transaction/stake.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts b/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts index 6c45e50e8de..5d54848a7df 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; -import { AbstractYieldPositionInfo, EarningStatus, LendingYieldPositionInfo, LiquidYieldPositionInfo, NativeYieldPositionInfo, NominationYieldPositionInfo, SubnetYieldPositionInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; +import { AbstractYieldPositionInfo, DelegatedYieldPositionInfo, EarningStatus, LendingYieldPositionInfo, LiquidYieldPositionInfo, NativeYieldPositionInfo, NominationYieldPositionInfo, SubnetYieldPositionInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; import { useGetChainAndExcludedTokenByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import BigN from 'bignumber.js'; @@ -102,7 +102,7 @@ const useYieldPositionDetail = (slug: string, address?: string): Result => { case YieldPoolType.DELEGATED_STAKING: rs = { ...base - } as SubnetYieldPositionInfo; + } as DelegatedYieldPositionInfo; break; } diff --git a/packages/extension-koni-ui/src/utils/transaction/stake.ts b/packages/extension-koni-ui/src/utils/transaction/stake.ts index 3eb8d940332..31a73c50ac2 100644 --- a/packages/extension-koni-ui/src/utils/transaction/stake.ts +++ b/packages/extension-koni-ui/src/utils/transaction/stake.ts @@ -18,5 +18,6 @@ export const parseNominations = (nomination: string) => { }; export const parseDelegatedNominations = (nomination: string): string[] => { + // filter(Boolean) removes empty strings caused by splitting '' or trailing commas return (nomination || '').split(',').filter(Boolean); }; From 3e9c124a8e29c1efcd2c7ca7973e114283dc87bb Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Tue, 19 May 2026 18:52:37 +0700 Subject: [PATCH 09/11] [Issue-#4946] Chore: fix lint --- packages/extension-base/src/types/balance/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index 35700b14c66..23ce94d5d51 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -18,7 +18,7 @@ export enum BalanceType { TOTAL = 'total', // free + locked TOTAL_MINUS_RESERVED = 'totalMinusReserved', // free + locked - reserved KEEP_ALIVE = 'keepAlive', - STAKING = 'staking' // staking balance, + STAKING = 'staking', // staking balance TOTAL_STAKE_EQUIVALENT = 'totalStakeEquivalent', // only for bittensor (delegate staking) TOTAL_EQUIVALENT = 'totalEquivalent' // only for bittensor (delegate staking) } From 6e33a9f7c2c1143aa188fdc188c098b79d5f2385 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Wed, 20 May 2026 11:57:01 +0700 Subject: [PATCH 10/11] [Issue-#4946] Add content and add fallback --- .../handlers/delegated-staking/tao.ts | 20 +++-- .../public/locales/en/translation.json | 45 +++++++++- .../public/locales/ja/translation.json | 88 +++++++++---------- .../public/locales/ru/translation.json | 86 +++++++++--------- .../public/locales/vi/translation.json | 51 +++++++++-- .../public/locales/zh/translation.json | 45 +++++++++- 6 files changed, 231 insertions(+), 104 deletions(-) diff --git a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts index 2a8ef9db284..5e21607918b 100644 --- a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts +++ b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts @@ -194,9 +194,9 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool /** * Fetch weighted APY per strategy with TTL cache. - * NOTE: strategyId in the APY endpoint differs from id in strategies-active, - * so APY is keyed by normalized strategy name (lowercase trim) instead. - * Returns a Map. + * APY is keyed by both normalized strategy name (lowercase trim) AND strategyId (UUID) + * for reliable lookup, since strategyId in the APY endpoint differs from id in strategies-active. + * Returns a Map. */ private async getTrustedStakeApyMap (): Promise> { const now = Date.now(); @@ -222,8 +222,11 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool const apy = Number(item.weightedApy); - // strategyId is different from strategies-active id, so map APY by strategy name. + // Map by normalized name for name-based lookup apyMap.set(this.normalizeStrategyName(item.strategyName), apy); + + // Also map by strategyId (UUID) as a more reliable fallback key + apyMap.set(item.strategyId, apy); }); this.trustedStakeApyCache = { @@ -450,7 +453,8 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool activeStake: totalTao.toFixed(), validatorMinStake: toBNString(strategy.minBalance, decimals), validatorIdentity: strategy.name, - expectedReturn: apyMap.get(this.normalizeStrategyName(strategy.name)), + // Lookup APY by name first, fallback to strategy id + expectedReturn: apyMap.get(this.normalizeStrategyName(strategy.name)) ?? apyMap.get(strategy.id), substrateProxyType: proxy.proxyType, delay: proxy.delay }; @@ -519,7 +523,8 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool minBond: toBNString(strategy.minBalance, decimals), constituents: Object.keys(strategy.targetConstituents.subnetWeights), identity: strategy.name, - expectedReturn: apyMap.get(this.normalizeStrategyName(strategy.name)) + // Lookup APY by name first, fallback to strategy id + expectedReturn: apyMap.get(this.normalizeStrategyName(strategy.name)) ?? apyMap.get(strategy.id) })); } catch (e) { console.warn('Failed to fetch pool targets', e); @@ -588,7 +593,6 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool const symbol = this.nativeToken.symbol; const pow = BN_TEN.pow(decimals); - const bnMinBond = new BigNumber(minBond); const bnProxyDeposit = new BigNumber(proxyDeposit); const parsedProxyDeposit = bnProxyDeposit.dividedBy(pow).toFixed(); @@ -603,7 +607,7 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool return errors; } - if (bnTotalEquivalent.lt(bnMinBond)) { + if (bnTotalEquivalent.lt(minBond)) { errors.push(new TransactionError(StakingTxErrorType.NOT_ENOUGH_MIN_STAKE, t('bg.EARNING.services.service.earning.delegatedStaking.tao.minimumTransferableBalanceToStartEarning', { replace: { parsedProxyDeposit, diff --git a/packages/extension-koni/public/locales/en/translation.json b/packages/extension-koni/public/locales/en/translation.json index d70d63115c1..627471c7918 100644 --- a/packages/extension-koni/public/locales/en/translation.json +++ b/packages/extension-koni/public/locales/en/translation.json @@ -2786,5 +2786,48 @@ "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "Select", "Only shows referenda you've voted directly in": "", "Only shows referenda you've voted through delegation": "", - "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "" + "You're delegating votes for the referendum's track with account named \"{{name}}\". Ask your delegatee to vote or remove your delegated votes, then try again": "", + "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "Highest annual return", + "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "Lowest min active stake", + "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "Strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "Select strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.apply": "Apply", + "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "Search or enter strategy", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "Strategy details", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", + "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", + "ui.EARNING.components.StakingItem.Strategy.apy": "APY", + "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "Feature not available for {{accountTitle}}. Change to another account type and try again.", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "Staking process", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "Once unstaked, the proxy deposit will be unlocked and returned to your accounts as transferable balance", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "Proxy deposit", + "ui.EARNING.screen.EarningPositions.delegatedStaking": "Delegated staking", + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "Your balances will fluctuate every 4 hours as the rebalancing process occurs to align with your delegated staking strategy", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "Balance fluctuations", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "Transferable", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "Staked", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "Balance details", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "Unable to turn off all networks. Please try again later", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "Turn off all networks", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "This button will automatically be toggled off once you enable any network from the network list", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "You sure you want to turn off all networks?", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "Turn off", + "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "Copy to the clipboard", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", + "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", + "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy", + "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "Total {{symbol}}-equivalent balance" } diff --git a/packages/extension-koni/public/locales/ja/translation.json b/packages/extension-koni/public/locales/ja/translation.json index 47b6123f664..b5aa06b5505 100644 --- a/packages/extension-koni/public/locales/ja/translation.json +++ b/packages/extension-koni/public/locales/ja/translation.json @@ -2602,36 +2602,36 @@ "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.proxyType": "", "ui.TRANSACTION.screen.Transaction.RemoveSubstrateProxyAccount.availableBalance": "", "ui.ACCOUNT.components.SubstrateProxyAccount.SelectorItem.copiedToClipboard": "", - "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "", - "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "", - "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "", - "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "", - "ui.EARNING.components.Field.Earning.StrategySelector.apply": "", - "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", - "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", - "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", - "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "", - "ui.EARNING.screen.EarningPositions.delegatedStaking": "", - "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "", - "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "", - "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "", - "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "", - "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "", - "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "", + "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "Total {{symbol}}-equivalent balance", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "Transferable", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "Staked", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "Balance details", + "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "Highest annual return", + "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "Strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "Select strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.apply": "Apply", + "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "Search or enter strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "Strategy details", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", + "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", + "ui.EARNING.components.StakingItem.Strategy.apy": "APY", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", + "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", + "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "Staking process", + "ui.EARNING.screen.EarningPositions.delegatedStaking": "Delegated staking", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "Your balances will fluctuate every 4 hours as the rebalancing process occurs to align with your delegated staking strategy", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "Balance fluctuations", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "Once unstaked, the proxy deposit will be unlocked and returned to your accounts as transferable balance", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "Copy to the clipboard", + "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "Lowest min active stake", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.othersCardDescription": "", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.swapCardTitle": "", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.keepCardTitle": "", @@ -2812,17 +2812,17 @@ "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.saveFailed": "", "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.loadFailed": "", "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "", - "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", - "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", - "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "", - "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "" -} \ No newline at end of file + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "Turn off all networks", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "This button will automatically be toggled off once you enable any network from the network list", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "You sure you want to turn off all networks?", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "Turn off", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "Unable to turn off all networks. Please try again later", + "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "Feature not available for {{accountTitle}}. Change to another account type and try again." +} diff --git a/packages/extension-koni/public/locales/ru/translation.json b/packages/extension-koni/public/locales/ru/translation.json index 5df9ecb3c1e..2d87cc89664 100644 --- a/packages/extension-koni/public/locales/ru/translation.json +++ b/packages/extension-koni/public/locales/ru/translation.json @@ -2604,36 +2604,36 @@ "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.proxyType": "", "ui.TRANSACTION.screen.Transaction.RemoveSubstrateProxyAccount.availableBalance": "", "ui.ACCOUNT.components.SubstrateProxyAccount.SelectorItem.copiedToClipboard": "", - "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "", - "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "", - "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "", - "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "", - "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "", - "ui.EARNING.components.Field.Earning.StrategySelector.apply": "", - "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "", - "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "", - "ui.EARNING.components.StakingItem.Strategy.apy": "", - "ui.TRANSACTION.screen.Transaction.Earn.constituents": "", - "ui.TRANSACTION.screen.Transaction.Earn.subnets": "", - "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "", - "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "", - "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "", - "ui.EARNING.screen.EarningPositions.delegatedStaking": "", - "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "", - "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "", - "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "", - "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "", - "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "", - "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "", + "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "Total {{symbol}}-equivalent balance", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "Transferable", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "Staked", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "Balance details", + "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "Highest annual return", + "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "Strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "Select strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.apply": "Apply", + "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "Search or enter strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "Strategy details", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", + "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", + "ui.EARNING.components.StakingItem.Strategy.apy": "APY", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", + "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", + "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "Staking process", + "ui.EARNING.screen.EarningPositions.delegatedStaking": "Delegated staking", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "Your balances will fluctuate every 4 hours as the rebalancing process occurs to align with your delegated staking strategy", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "Balance fluctuations", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "Once unstaked, the proxy deposit will be unlocked and returned to your accounts as transferable balance", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "Copy to the clipboard", + "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "Lowest min active stake", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.othersCardDescription": "", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.swapCardTitle": "", "ui.EARNING.components.Modal.Earning.BittensorClaimRewardType.keepCardTitle": "", @@ -2814,17 +2814,17 @@ "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.loadFailed": "", "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "", "ui.ACCOUNT.screen.Account.NewMultisigAccount.setApprovalThreshold": "", - "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", - "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", - "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "", - "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "", - "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "" + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "Turn off all networks", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "This button will automatically be toggled off once you enable any network from the network list", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "You sure you want to turn off all networks?", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "Turn off", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "Unable to turn off all networks. Please try again later", + "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "Feature not available for {{accountTitle}}. Change to another account type and try again." } diff --git a/packages/extension-koni/public/locales/vi/translation.json b/packages/extension-koni/public/locales/vi/translation.json index 02f136779f6..f4a3376334c 100644 --- a/packages/extension-koni/public/locales/vi/translation.json +++ b/packages/extension-koni/public/locales/vi/translation.json @@ -2783,11 +2783,48 @@ "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.saveFailed": "", "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.loadFailed": "", "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "", - "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "", - "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "", + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", "ui.EARNING.components.Modal.Earning.EarningInfo.totalReward": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "", - "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "", - "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "" -} \ No newline at end of file + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy", + "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "Highest annual return", + "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "Lowest min active stake", + "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "Strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "Select strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.apply": "Apply", + "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "Search or enter strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "Strategy details", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", + "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", + "ui.EARNING.components.StakingItem.Strategy.apy": "APY", + "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "Feature not available for {{accountTitle}}. Change to another account type and try again.", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "Staking process", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "Once unstaked, the proxy deposit will be unlocked and returned to your accounts as transferable balance", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "Proxy deposit", + "ui.EARNING.screen.EarningPositions.delegatedStaking": "Delegated staking", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "Your balances will fluctuate every 4 hours as the rebalancing process occurs to align with your delegated staking strategy", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "Balance fluctuations", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "Transferable", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "Staked", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "Balance details", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "Unable to turn off all networks. Please try again later", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "Turn off all networks", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "This button will automatically be toggled off once you enable any network from the network list", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "You sure you want to turn off all networks?", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "Turn off", + "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "Copy to the clipboard", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", + "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", + "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "Total {{symbol}}-equivalent balance" +} diff --git a/packages/extension-koni/public/locales/zh/translation.json b/packages/extension-koni/public/locales/zh/translation.json index 01edda603cd..be39dc1b035 100644 --- a/packages/extension-koni/public/locales/zh/translation.json +++ b/packages/extension-koni/public/locales/zh/translation.json @@ -2782,5 +2782,48 @@ "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.saveSuccess": "", "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.saveFailed": "", "ui.SETTINGS.screen.Setting.Account.SubscanApiConfigModal.loadFailed": "", - "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "" + "ui.ACCOUNT.components.Modal.Selector.WrappedTransactionSignerSelectorModal.select": "", + "ui.EARNING.components.Field.Earning.StrategySelector.highestAnnualReturn": "Highest annual return", + "ui.EARNING.components.Field.Earning.StrategySelector.lowestMinActiveStake": "Lowest min active stake", + "ui.EARNING.components.Field.Earning.StrategySelector.noStrategyAvailable": "Strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.selectStrategy": "Select strategy", + "ui.EARNING.components.Field.Earning.StrategySelector.apply": "Apply", + "ui.EARNING.components.Field.Earning.StrategySelector.searchStrategy": "Search or enter strategy", + "ui.EARNING.components.Modal.Earning.EarningConstituentsModal.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategyDetails": "Strategy details", + "ui.EARNING.components.Modal.Earning.StrategyDetail.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.StrategyDetail.minimumStakeRequired": "Minimum stake required", + "ui.EARNING.components.Modal.Earning.StrategyDetail.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.StrategyDetail.subnet": "subnet", + "ui.EARNING.components.Modal.Earning.StrategyDetail.estimatedApy": "APY", + "ui.EARNING.components.StakingItem.Strategy.apy": "APY", + "ui.DAPP.Confirmations.Message.NotSupport.featureNotAvailableForAccountType": "Feature not available for {{accountTitle}}. Change to another account type and try again.", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcessDescription": "Once delegated to the proxy account, your staked funds will be distributed to subnets within the strategy. This process will take about 4 hours to complete", + "ui.TRANSACTION.Confirmations.AddSubstrateProxyAccount.stakingProcess": "Staking process", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDepositDescription": "Once unstaked, the proxy deposit will be unlocked and returned to your accounts as transferable balance", + "ui.TRANSACTION.Confirmations.RemoveSubstrateProxyAccount.proxyDeposit": "Proxy deposit", + "ui.EARNING.screen.EarningPositions.delegatedStaking": "Delegated staking", + "ui.EARNING.screen.EarningPositionDetail.EarningInfo.copiedToClipboard": "Copied to clipboard", + "ui.EARNING.components.Modal.Earning.EarningInfo.strategy": "Strategy", + "ui.EARNING.components.Modal.Earning.EarningInfo.constituents": "Constituents", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnets": "subnets", + "ui.EARNING.components.Modal.Earning.EarningInfo.subnet": "subnet", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuationsDescription": "Your balances will fluctuate every 4 hours as the rebalancing process occurs to align with your delegated staking strategy", + "ui.BALANCE.screen.Tokens.DetailList.balanceFluctuations": "Balance fluctuations", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.transferable": "Transferable", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.staked": "Staked", + "ui.BALANCE.screen.Tokens.TotalEquivalentDetailModal.balanceDetails": "Balance details", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworksUnable": "Unable to turn off all networks. Please try again later", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOffAllNetworks": "Turn off all networks", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkConfirmation": "This button will automatically be toggled off once you enable any network from the network list", + "ui.SETTINGS.screen.Setting.Chains.Manage.disableAllNetworkSubtitle": "You sure you want to turn off all networks?", + "ui.SETTINGS.screen.Setting.Chains.Manage.turnOff": "Turn off", + "ui.TRANSACTION.screen.Transaction.Earn.copiedToClipboard": "Copy to the clipboard", + "ui.TRANSACTION.screen.Transaction.Earn.constituents": "Constituents", + "ui.TRANSACTION.screen.Transaction.Earn.subnets": "subnets", + "ui.TRANSACTION.screen.Transaction.Earn.proxyAddress": "Proxy address", + "ui.TRANSACTION.screen.Transaction.Earn.proxyDeposit": "Proxy deposit", + "ui.TRANSACTION.screen.Transaction.Earn.amountRequiredToActivateStakingProxy": "Amount required to activate the staking proxy", + "ui.TRANSACTION.screen.Transaction.Earn.totalEquivalent": "Total {{symbol}}-equivalent balance" } From d3eef42c6cc6571bc2cfb674b704f3e2dd489a20 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Wed, 10 Jun 2026 18:15:49 +0700 Subject: [PATCH 11/11] [Issue-#4946] Chore: change url --- .../handlers/delegated-staking/tao.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts index 5e21607918b..5d42318b008 100644 --- a/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts +++ b/packages/extension-base/src/services/earning-service/handlers/delegated-staking/tao.ts @@ -5,6 +5,7 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/Tr import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY, BITTENSOR_REFRESH_STAKE_INFO } from '@subwallet/extension-base/constants'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; +import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils'; import { BalanceType, BasicTxErrorType, DelegatedStrategyInfo, DelegatedYieldPoolInfo, EarningStatus, HandleYieldStepData, OptimalYieldPath, PrimitiveSubstrateProxyAccountItem, RequestDelegateStakingSubmit, RequestEarlyValidateYield, ResponseEarlyValidateYield, StakeCancelWithdrawalParams, StakingTxErrorType, StrategyInfo, SubmitJoinDelegateStaking, SubmitYieldJoinData, TransactionData, UnstakingInfo, YieldPoolInfo, YieldPoolMethodInfo, YieldPoolType, YieldPositionInfo, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; import { BN_TEN, isSameAddress, toBNString } from '@subwallet/extension-base/utils'; @@ -47,9 +48,14 @@ interface TrustedStakeApyResponse { strategies: TrustedStakeApyStrategy[]; } -// TrustedStake external APIs — strategies list and APY data -const trustedStakeApi = 'https://app.trustedstake.ai/api/strategies-active'; -const trustedStakeApyApi = 'https://api.app.trustedstake.ai/tmc-apy'; +// TrustedStake APIs proxied through btc-api.koni.studio — strategies list and APY data +const BITCOIN_API_URL = 'https://btc-api.koni.studio'; +const trustedStakeApi = `${BITCOIN_API_URL}/proxy/trustedstake/strategies/active`; +const trustedStakeApyApi = `${BITCOIN_API_URL}/proxy/trustedstake/tmc-apy`; +const trustedStakeHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${_BTC_SERVICE_TOKEN}` +}; export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPoolHandler { // @ts-ignore @@ -170,7 +176,7 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool this.trustedStakeStrategiesPromise = (async () => { try { - const res = await fetch(trustedStakeApi); + const res = await fetch(trustedStakeApi, { headers: trustedStakeHeaders }); const data = await res.json() as TrustedStakeStrategy[]; const activeStrategies = data.filter((s) => s.isActive); @@ -211,7 +217,7 @@ export default class TaoDelegateStakingPoolHandler extends BaseNativeStakingPool this.trustedStakeApyPromise = (async () => { try { - const res = await fetch(trustedStakeApyApi); + const res = await fetch(trustedStakeApyApi, { headers: trustedStakeHeaders }); const data = await res.json() as TrustedStakeApyResponse; const apyMap = new Map();