diff --git a/.env.example b/.env.example index 9a06f73917..e8bf2c0377 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,5 @@ COINBASE_PAY_ID=1dbd2a0b94 NFT_MINTING_HOST=http://... CHAINFLIP_BROKER_API=93c2bff017e243f29ffb14e42dccbec8 BRANCH_NAME=master +BITTENSOR_API_KEY_1=abavbav +BITTENSOR_API_KEY_2=abafdbad diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 4ae7d15692..d81865e220 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -47,6 +47,8 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} GH_RELEASE_FILES: master-build.zip,master-src.zip COMMIT_MESSAGE: ${{ github.event.head_commit.message }} REF_NAME: ${{ github.ref_name }} diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 61ae163f5e..68a907aac8 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -28,6 +28,8 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-web-runner.yml b/.github/workflows/push-web-runner.yml index 2bf06ab5a3..50a9ba09ea 100644 --- a/.github/workflows/push-web-runner.yml +++ b/.github/workflows/push-web-runner.yml @@ -32,6 +32,8 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BRANCH_NAME: master run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-webapp.yml b/.github/workflows/push-webapp.yml index bfb2461861..c29a5e1155 100644 --- a/.github/workflows/push-webapp.yml +++ b/.github/workflows/push-webapp.yml @@ -32,6 +32,8 @@ jobs: COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} NFT_MINTING_HOST: ${{ secrets.NFT_MINTING_HOST }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/package.json b/package.json index fc61522823..e7d00385ef 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@polkadot/types-support": "^12.0.2", "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", - "@subwallet/chain-list": "0.2.88", + "@subwallet/chain-list": "0.2.88-beta.6", "@subwallet/keyring": "^0.1.7", "@subwallet/react-ui": "5.1.2-b79", "@subwallet/ui-keyring": "^0.1.7", diff --git a/packages/extension-base/src/constants/index.ts b/packages/extension-base/src/constants/index.ts index f576ef2edc..a5ec5f5d90 100644 --- a/packages/extension-base/src/constants/index.ts +++ b/packages/extension-base/src/constants/index.ts @@ -20,6 +20,8 @@ export const CRON_REFRESH_CHAIN_NOMINATOR_METADATA = 1800000; export const CRON_RECOVER_HISTORY_INTERVAL = 30000; export const CRON_SYNC_MANTA_PAY = 300000; export const MANTA_PAY_BALANCE_INTERVAL = 30000; +export const BITTENSOR_REFRESH_STAKE_INFO = 30000; +export const BITTENSOR_REFRESH_STAKE_APY = 300000; export const CRON_REFRESH_EARNING_REWARD_HISTORY_INTERVAL = 15 * BASE_MINUTE_INTERVAL; export const ALL_ACCOUNT_KEY = 'ALL'; diff --git a/packages/extension-base/src/koni/api/staking/bonding/utils.ts b/packages/extension-base/src/koni/api/staking/bonding/utils.ts index 98cbde9c2b..3c496546b2 100644 --- a/packages/extension-base/src/koni/api/staking/bonding/utils.ts +++ b/packages/extension-base/src/koni/api/staking/bonding/utils.ts @@ -541,7 +541,7 @@ export function getEarningStatusByNominations (bnTotalActiveStake: BN, nominatio export function getValidatorLabel (chain: string) { if (_STAKING_CHAIN_GROUP.astar.includes(chain)) { return 'dApp'; - } else if (_STAKING_CHAIN_GROUP.relay.includes(chain)) { + } else if (_STAKING_CHAIN_GROUP.relay.includes(chain) || _STAKING_CHAIN_GROUP.bittensor.includes(chain)) { return 'Validator'; } 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 dc8662aa9e..80fc3ffc1b 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 @@ -17,6 +17,8 @@ import { getDefaultWeightV2 } from '@subwallet/extension-base/koni/api/contract- import { _BALANCE_CHAIN_GROUP, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _SubstrateAdapterSubscriptionArgs, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _checkSmartContractSupportByChain, _getAssetExistentialDeposit, _getChainExistentialDeposit, _getChainNativeTokenSlug, _getContractAddressOfToken, _getTokenOnChainAssetId, _getTokenOnChainInfo, _getTokenTypesSupportedByChain, _getXcmAssetMultilocation, _isBridgedToken, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { fetchTaoDelegateState } from '@subwallet/extension-base/services/earning-service/handlers/native-staking/tao'; +import { getTaoTotalStake } from '@subwallet/extension-base/services/earning-service/utils'; import { BalanceItem, SubscribeBasePalletBalance, SubscribeSubstratePalletBalance } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -142,6 +144,16 @@ const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo ); } + let bittensorStakingBalances = new Array(addresses.length).fill(BigInt(0)); + + if (['bittensor'].includes(chainInfo.slug)) { + bittensorStakingBalances = await Promise.all(addresses.map(async (address) => { + const rawDelegateState = await fetchTaoDelegateState(address); + + return getTaoTotalStake(rawDelegateState); + })); + } + const subscription = substrateApi.subscribeDataWithMulti(params, (rs) => { const balances = rs[systemAccountKey]; const poolMemberInfos = rs[poolMembersKey]; @@ -161,6 +173,8 @@ const subscribeWithSystemAccountPallet = async ({ addresses, callback, chainInfo totalLockedFromTransfer += nominationPoolBalance; } + totalLockedFromTransfer += bittensorStakingBalances[index]; + return ({ address: addresses[index], tokenSlug: _getChainNativeTokenSlug(chainInfo), diff --git a/packages/extension-base/src/services/earning-service/constants/chains.ts b/packages/extension-base/src/services/earning-service/constants/chains.ts index f992973b18..7a8b73b5aa 100644 --- a/packages/extension-base/src/services/earning-service/constants/chains.ts +++ b/packages/extension-base/src/services/earning-service/constants/chains.ts @@ -14,7 +14,8 @@ export const _STAKING_CHAIN_GROUP = { liquidStaking: ['bifrost_dot', 'acala', 'parallel', 'moonbeam'], lending: ['interlay'], krest_network: ['krest_network'], - manta: ['manta_network'] + manta: ['manta_network'], + bittensor: ['bittensor', 'bittensor_devnet'] }; export const MaxEraRewardPointsEras = 14; diff --git a/packages/extension-base/src/services/earning-service/handlers/native-staking/index.ts b/packages/extension-base/src/services/earning-service/handlers/native-staking/index.ts index 2b981c1770..7ac3e4465f 100644 --- a/packages/extension-base/src/services/earning-service/handlers/native-staking/index.ts +++ b/packages/extension-base/src/services/earning-service/handlers/native-staking/index.ts @@ -5,3 +5,4 @@ export { default as AmplitudeNativeStakingPoolHandler } from './amplitude'; export { default as AstarNativeStakingPoolHandler } from './astar'; export { default as RelayNativeStakingPoolHandler } from './relay-chain'; export { default as ParaNativeStakingPoolHandler } from './para-chain'; +export { default as TaoNativeStakingPoolHandler } from './tao'; diff --git a/packages/extension-base/src/services/earning-service/handlers/native-staking/tao.ts b/packages/extension-base/src/services/earning-service/handlers/native-staking/tao.ts new file mode 100644 index 0000000000..6361d15765 --- /dev/null +++ b/packages/extension-base/src/services/earning-service/handlers/native-staking/tao.ts @@ -0,0 +1,489 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainInfo } from '@subwallet/chain-list/types'; +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; +import { ExtrinsicType, NominationInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { BITTENSOR_REFRESH_STAKE_APY, BITTENSOR_REFRESH_STAKE_INFO } from '@subwallet/extension-base/constants'; +import { getEarningStatusByNominations } from '@subwallet/extension-base/koni/api/staking/bonding/utils'; +import BaseParaStakingPoolHandler from '@subwallet/extension-base/services/earning-service/handlers/native-staking/base-para'; +import { BaseYieldPositionInfo, BasicTxErrorType, EarningStatus, NativeYieldPoolInfo, StakeCancelWithdrawalParams, SubmitJoinNativeStaking, TransactionData, UnstakingInfo, ValidatorInfo, YieldPoolInfo, YieldPositionInfo, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { reformatAddress } from '@subwallet/extension-base/utils'; +import BigN from 'bignumber.js'; + +import { BN, BN_ZERO } from '@polkadot/util'; + +import { calculateReward } from '../../utils'; + +interface TaoStakingStakeOption { + owner: string, + amount: string, +} + +interface Hotkey { + ss58: string; +} + +export interface RawDelegateState { + items: Array<{ + balance: string; + delegate_address: { + ss58: string; + }; + }>; +} + +interface ValidatorResponse { + count: number; + validators: Validator[]; +} + +interface Validator { + validator_stake: string; + amount: string; + nominators: number; + apr: string; + hot_key: { + ss58: string; + }; + take: string; + system_total_stake: string; +} + +// interface ValidatorName { +// count: number; +// delegates: { +// name: string; +// }[]; +// } +export const BITTENSOR_API_KEY_1 = process.env.BITTENSOR_API_KEY_1 || ''; +export const BITTENSOR_API_KEY_2 = process.env.BITTENSOR_API_KEY_2 || ''; + +function random (...keys: string[]) { + const validKeys = keys.filter((key) => key); + const randomIndex = Math.floor(Math.random() * validKeys.length); + + return validKeys[randomIndex]; +} + +export const bittensorApiKey = (): string => { + return random(BITTENSOR_API_KEY_1, BITTENSOR_API_KEY_2); +}; + +/* Fetch data */ + +export async function fetchDelegates (): Promise { + const apiKey = bittensorApiKey(); + + return new Promise(function (resolve) { + fetch('https://api.taostats.io/api/v1/validator?order=amount%3Adesc&limit=100', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${apiKey}` + } + }).then((resp) => { + resolve(resp.json()); + }).catch(console.error); + }); +} + +export async function fetchTaoDelegateState (address: string): Promise { + const apiKey = bittensorApiKey(); + + return new Promise(function (resolve) { + fetch(`https://api.taostats.io/api/v1/delegate/balance?nominator_address=${address}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${apiKey}` + } + }).then((resp) => { + resolve(resp.json()); + }).catch(console.error); + }); +} + +/* Fetch data */ + +const testnetDelegate = { + '5G6wdAdS7hpBuH1tjuZDhpzrGw9Wf71WEVakDCxHDm1cxEQ2': { + name: '0x436c6f776e4e616d65f09fa4a1', + url: 'https://example.com ', + image: 'https://example.com/image.png', + discord: '0xe28094446973636f7264', + description: 'This is an example identity.', + additional: '' + } +}; + +export default class TaoNativeStakingPoolHandler extends BaseParaStakingPoolHandler { + /* Unimplemented function */ + public override handleYieldWithdraw (address: string, unstakingInfo: UnstakingInfo): Promise { + return Promise.reject(new TransactionError(BasicTxErrorType.UNSUPPORTED)); + } + + public override handleYieldCancelUnstake (params: StakeCancelWithdrawalParams): Promise { + return Promise.reject(new TransactionError(BasicTxErrorType.UNSUPPORTED)); + } + /* Unimplemented function */ + + // async fetchDelegatesInfo (address: string): Promise { + // const apiKey = this.bittensorApiKey; + + // return new Promise(function (resolve) { + // fetch(`https://api.taostats.io/api/v1/delegate/info?address=${address}`, { + // method: 'GET', + // headers: { + // 'Content-Type': 'application/json', + // Authorization: `${apiKey}` + // } + // }).then((resp) => { + // resolve(resp.json()); + // }).catch(console.error); + // }); + // } + + /* Subscribe pool info */ + + async subscribePoolInfo (callback: (data: YieldPoolInfo) => void): Promise { + let cancel = false; + const substrateApi = this.substrateApi; + + const updateStakingInfo = async () => { + try { + if (cancel) { + return; + } + + const minDelegatorStake = await substrateApi.api.query.subtensorModule.nominatorMinRequiredStake(); + + const BNminDelegatorStake = new BigN(minDelegatorStake.toString()); + + const data: NativeYieldPoolInfo = { + ...this.baseInfo, + type: this.type, + metadata: { + ...this.metadataInfo, + description: this.getDescription('0') + }, + statistic: { + assetEarning: [ + { + slug: this.nativeToken.slug + } + ], + maxCandidatePerFarmer: 16, + maxWithdrawalRequestPerFarmer: 1, + earningThreshold: { + join: BNminDelegatorStake.toString(), + defaultUnstake: '0', + fastUnstake: '0' + }, + eraTime: 1.2, + era: 0, + unstakingPeriod: 1.2 + } + }; + + callback(data); + } catch (error) { + console.log(error); + } + }; + + const subscribeStakingMetadataInterval = () => { + updateStakingInfo().catch(console.error); + }; + + await substrateApi.isReady; + + subscribeStakingMetadataInterval(); + const interval = setInterval(subscribeStakingMetadataInterval, BITTENSOR_REFRESH_STAKE_APY); + + return () => { + cancel = true; + clearInterval(interval); + }; + } + + /* Subscribe pool position */ + + async parseNominatorMetadata (chainInfo: _ChainInfo, address: string, delegatorState: TaoStakingStakeOption[]): Promise> { + const nominationList: NominationInfo[] = []; + const getMinDelegatorStake = this.substrateApi.api.query.subtensorModule.nominatorMinRequiredStake(); + const minDelegatorStake = (await getMinDelegatorStake).toString(); + let allActiveStake = BN_ZERO; + + for (const delegate of delegatorState) { + const activeStake = delegate.amount; + const bnActiveStake = new BN(activeStake); + + if (bnActiveStake.gt(BN_ZERO)) { + const delegationStatus = EarningStatus.EARNING_REWARD; + + allActiveStake = allActiveStake.add(bnActiveStake); + + nominationList.push({ + status: delegationStatus, + chain: chainInfo.slug, + validatorAddress: delegate.owner, + activeStake: activeStake, + validatorMinStake: minDelegatorStake + }); + } + } + + const stakingStatus = getEarningStatusByNominations(allActiveStake, nominationList); + + return { + status: stakingStatus, + balanceToken: this.nativeToken.slug, + totalStake: allActiveStake.toString(), + activeStake: allActiveStake.toString(), + unstakeBalance: '0', + isBondedBefore: true, + nominations: nominationList, + unstakings: [] + } as unknown as YieldPositionInfo; + } + + override async subscribePoolPosition (useAddresses: string[], rsCallback: (rs: YieldPositionInfo) => void): Promise { + let cancel = false; + const substrateApi = await this.substrateApi.isReady; + const defaultInfo = this.baseInfo; + const chainInfo = this.chainInfo; + + const getDevnetPoolPosition = async () => { + const testnetAddress = Object.keys(testnetDelegate)[0]; + const delegatorState: TaoStakingStakeOption[] = []; + let bnTotalBalance = BN_ZERO; + + const stakePromises = useAddresses.map(async (address) => { + const stakeAmount = (await substrateApi.api.query.subtensorModule.stake(testnetAddress, address)).toString(); + const bnStakeAmount = new BN(stakeAmount); + + bnTotalBalance = bnTotalBalance.add(bnStakeAmount); + + delegatorState.push({ + owner: testnetAddress, + amount: bnStakeAmount.toString() + }); + + rsCallback({ + ...defaultInfo, + type: this.type, + address: address, + balanceToken: this.nativeToken.slug, + totalStake: bnTotalBalance.toString(), + activeStake: bnStakeAmount.toString(), + unstakeBalance: '0', + status: EarningStatus.EARNING_REWARD, + isBondedBefore: true, + nominations: delegatorState.map((delegate) => ({ + chain: this.chain, + validatorAddress: delegate.owner, + activeStake: delegate.amount, + status: EarningStatus.EARNING_REWARD + })), + unstakings: [] + }); + }); + + await Promise.all(stakePromises); + }; + + const getMainnetPoolPosition = async () => { + const rawDelegateStateInfos = await Promise.all( + useAddresses.map((address) => fetchTaoDelegateState(address)) + ); + + if (rawDelegateStateInfos.length > 0) { + rawDelegateStateInfos.forEach((rawDelegateStateInfo, i) => { + const owner = reformatAddress(useAddresses[i], 42); + const delegatorState: TaoStakingStakeOption[] = []; + let bnTotalBalance = BN_ZERO; + const delegateStateInfo = rawDelegateStateInfo.items; + + for (const delegate of delegateStateInfo) { + bnTotalBalance = bnTotalBalance.add(new BN(delegate.balance)); + delegatorState.push({ + owner: delegate.delegate_address.ss58, + amount: delegate.balance.toString() + }); + } + + if (delegateStateInfo && delegateStateInfo.length > 0) { + this.parseNominatorMetadata(chainInfo, owner, delegatorState) + .then((nominatorMetadata) => { + rsCallback({ + ...defaultInfo, + ...nominatorMetadata, + address: owner, + type: this.type + }); + }) + .catch(console.error); + } else { + 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: [] + }); + } + }); + } + }; + + const getStakingPositionInterval = async () => { + if (cancel) { + return; + } + + if (this.chain === 'bittensor_devnet') { + await getDevnetPoolPosition(); + } else { + await getMainnetPoolPosition(); + } + }; + + getStakingPositionInterval().catch(console.error); + + const intervalId = setInterval(() => { + getStakingPositionInterval().catch(console.error); + }, BITTENSOR_REFRESH_STAKE_INFO); + + return () => { + cancel = true; + clearInterval(intervalId); + }; + } + + /* Subscribe pool position */ + + /* Get pool targets */ + + // eslint-disable-next-line @typescript-eslint/require-await + private async getDevnetPoolTargets (): Promise { + const _topValidator = testnetDelegate; + const validatorAddresses = Object.keys(_topValidator); + + return validatorAddresses.map((address) => { + return { + address: address, + totalStake: '0', + ownStake: '0', + otherStake: '0', + minBond: '0', + nominatorCount: 0, + commission: '0', + expectedReturn: 0, + blocked: false, + isVerified: false, + chain: this.chain, + isCrowded: false, + identity: address + } as unknown as ValidatorInfo; + }); + } + + private async getMainnetPoolTargets (): Promise { + const _topValidator = await fetchDelegates(); + const topValidator = _topValidator as unknown as Record>>; + const getNominatorMinRequiredStake = this.substrateApi.api.query.subtensorModule.nominatorMinRequiredStake(); + const nominatorMinRequiredStake = (await getNominatorMinRequiredStake).toString(); + const bnMinBond = new BN(nominatorMinRequiredStake); + const validatorList = topValidator.validators; + const validatorAddresses = Object.keys(validatorList); + + const results = await Promise.all( + validatorAddresses.map((i) => { + const address = (validatorList[i].hot_key as unknown as Hotkey).ss58; + const bnTotalStake = new BN(validatorList[i].amount); + const bnOwnStake = new BN(validatorList[i].validator_stake); + const otherStake = bnTotalStake.sub(bnOwnStake); + const nominatorCount = validatorList[i].nominators; + const commission = validatorList[i].take; + const roundedCommission = (parseFloat(commission) * 100).toFixed(0); + + const apr = ((parseFloat(validatorList[i].apr) / 10 ** 9) * 100).toFixed(2); + const apyCalculate = calculateReward(parseFloat(apr)); + + // let name = ''; + // const delegateInfo = await this.fetchDelegatesInfo(address); + + // name = delegateInfo.delegates[0]?.name || address; + + return { + address: address, + totalStake: bnTotalStake.toString(), + ownStake: bnOwnStake.toString(), + otherStake: otherStake.toString(), + minBond: bnMinBond.toString(), + nominatorCount: nominatorCount, + commission: roundedCommission, + expectedReturn: apyCalculate.apy, + blocked: false, + isVerified: false, + chain: this.chain, + isCrowded: false, + identity: address // name + } as unknown as ValidatorInfo; + }) + ); + + return results; + } + + async getPoolTargets (): Promise { + if (this.chain === 'bittensor_devnet') { + return this.getDevnetPoolTargets(); + } else { + return this.getMainnetPoolTargets(); + } + } + + /* Get pool targets */ + + /* Join pool action */ + + async createJoinExtrinsic (data: SubmitJoinNativeStaking, positionInfo?: YieldPositionInfo, bondDest = 'Staked'): Promise<[TransactionData, YieldTokenBaseInfo]> { + const { amount, selectedValidators: targetValidators } = data; + const chainApi = await this.substrateApi.isReady; + const binaryAmount = new BN(amount); + const selectedValidatorInfo = targetValidators[0]; + const hotkey = selectedValidatorInfo.address; + + const extrinsic = chainApi.api.tx.subtensorModule.addStake(hotkey, binaryAmount); + + return [extrinsic, { slug: this.nativeToken.slug, amount: '0' }]; + } + + /* Join pool action */ + + /* Leave pool action */ + + async handleYieldUnstake (amount: string, address: string, selectedTarget?: string): Promise<[ExtrinsicType, TransactionData]> { + const apiPromise = await this.substrateApi.isReady; + const binaryAmount = new BN(amount); + const poolPosition = await this.getPoolPosition(address); + + if (!selectedTarget || !poolPosition) { + return Promise.reject(new TransactionError(BasicTxErrorType.INVALID_PARAMS)); + } + + const extrinsic = apiPromise.api.tx.subtensorModule.removeStake(selectedTarget, binaryAmount); + + return [ExtrinsicType.STAKING_LEAVE_POOL, extrinsic]; + } + + /* Leave pool action */ +} diff --git a/packages/extension-base/src/services/earning-service/service.ts b/packages/extension-base/src/services/earning-service/service.ts index 28cc60b7b3..1a8ac593a2 100644 --- a/packages/extension-base/src/services/earning-service/service.ts +++ b/packages/extension-base/src/services/earning-service/service.ts @@ -17,7 +17,7 @@ import { addLazy, categoryAddresses, createPromiseHandler, PromiseHandler, remov import { fetchStaticCache } from '@subwallet/extension-base/utils/fetchStaticCache'; import { BehaviorSubject } from 'rxjs'; -import { AcalaLiquidStakingPoolHandler, AmplitudeNativeStakingPoolHandler, AstarNativeStakingPoolHandler, BasePoolHandler, BifrostLiquidStakingPoolHandler, BifrostMantaLiquidStakingPoolHandler, InterlayLendingPoolHandler, NominationPoolHandler, ParallelLiquidStakingPoolHandler, ParaNativeStakingPoolHandler, RelayNativeStakingPoolHandler, StellaSwapLiquidStakingPoolHandler } from './handlers'; +import { AcalaLiquidStakingPoolHandler, AmplitudeNativeStakingPoolHandler, AstarNativeStakingPoolHandler, BasePoolHandler, BifrostLiquidStakingPoolHandler, BifrostMantaLiquidStakingPoolHandler, InterlayLendingPoolHandler, NominationPoolHandler, ParallelLiquidStakingPoolHandler, ParaNativeStakingPoolHandler, RelayNativeStakingPoolHandler, StellaSwapLiquidStakingPoolHandler, TaoNativeStakingPoolHandler } from './handlers'; const fetchPoolsData = async () => { const fetchData = await fetchStaticCache<{data: Record}>('earning/yield-pools.json', { data: {} }); @@ -82,6 +82,10 @@ export default class EarningService implements StoppableServiceInterface, Persis handlers.push(new AmplitudeNativeStakingPoolHandler(this.state, chain)); } + if (_STAKING_CHAIN_GROUP.bittensor.includes(chain)) { + handlers.push(new TaoNativeStakingPoolHandler(this.state, chain)); + } + if (_STAKING_CHAIN_GROUP.nominationPool.includes(chain)) { handlers.push(new NominationPoolHandler(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 cf36f80560..8819111c75 100644 --- a/packages/extension-base/src/services/earning-service/utils/index.ts +++ b/packages/extension-base/src/services/earning-service/utils/index.ts @@ -4,6 +4,7 @@ import { PalletIdentityRegistration, PalletIdentitySuper } from '@subwallet/extension-base/koni/api/staking/bonding/utils'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/earning-service/constants'; +import { RawDelegateState } from '@subwallet/extension-base/services/earning-service/handlers/native-staking/tao'; import { LendingYieldPoolInfo, LiquidYieldPoolInfo, NativeYieldPoolInfo, NominationYieldPoolInfo, YieldAssetExpectedEarning, YieldCompoundingPeriod, YieldPoolInfo, YieldPoolType } from '@subwallet/extension-base/types'; import { BN, hexToString, isHex } from '@polkadot/util'; @@ -126,6 +127,8 @@ export function isActionFromValidator (stakingType: YieldPoolType, chain: string return true; } else if (_STAKING_CHAIN_GROUP.para.includes(chain)) { return true; + } else if (_STAKING_CHAIN_GROUP.bittensor.includes(chain)) { + return true; } return false; @@ -152,3 +155,17 @@ export function applyDecimal (bnNumber: BN, decimals: number) { return bnNumber.div(bnDecimals); } + +export function getTaoTotalStake (rawDelegateState: RawDelegateState) { + const nodeInfos = rawDelegateState.items; + const stakes = nodeInfos.map((stake) => stake.balance); + let totalStake = BigInt(0); + + for (const _stake of stakes) { + const stakeAmount = BigInt(_stake); + + totalStake += stakeAmount; + } + + return totalStake; +} diff --git a/packages/extension-base/src/types/transaction.ts b/packages/extension-base/src/types/transaction.ts index 0925ba89e6..e106f2cd15 100644 --- a/packages/extension-base/src/types/transaction.ts +++ b/packages/extension-base/src/types/transaction.ts @@ -5,4 +5,19 @@ import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; +export enum BasicTxErrorType { + NOT_ENOUGH_BALANCE = 'NOT_ENOUGH_BALANCE', + CHAIN_DISCONNECTED = 'CHAIN_DISCONNECTED', + INVALID_PARAMS = 'INVALID_PARAMS', + DUPLICATE_TRANSACTION = 'DUPLICATE_TRANSACTION', + UNABLE_TO_SIGN = 'UNABLE_TO_SIGN', + USER_REJECT_REQUEST = 'USER_REJECT_REQUEST', + UNABLE_TO_SEND = 'UNABLE_TO_SEND', + SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED', + INTERNAL_ERROR = 'INTERNAL_ERROR', + UNSUPPORTED = 'UNSUPPORTED', + TIMEOUT = 'TIMEOUT', + NOT_ENOUGH_EXISTENTIAL_DEPOSIT = 'NOT_ENOUGH_EXISTENTIAL_DEPOSIT', +} + export type TransactionData = SubmittableExtrinsic<'promise'> | TransactionConfig; diff --git a/packages/extension-koni-ui/package.json b/packages/extension-koni-ui/package.json index 6172f2b261..8d47ecbef2 100644 --- a/packages/extension-koni-ui/package.json +++ b/packages/extension-koni-ui/package.json @@ -34,7 +34,7 @@ "@polkadot/util-crypto": "^12.6.2", "@ramonak/react-progress-bar": "^5.0.3", "@reduxjs/toolkit": "^1.9.1", - "@subwallet/chain-list": "0.2.88", + "@subwallet/chain-list": "0.2.88-beta.6", "@subwallet/extension-base": "^1.2.32-0", "@subwallet/extension-chains": "^1.2.32-0", "@subwallet/extension-dapp": "^1.2.32-0", diff --git a/packages/extension-koni/webpack.shared.cjs b/packages/extension-koni/webpack.shared.cjs index 49f093fc82..49147ee240 100644 --- a/packages/extension-koni/webpack.shared.cjs +++ b/packages/extension-koni/webpack.shared.cjs @@ -46,7 +46,9 @@ const _additionalEnv = { BANXA_TEST_MODE: JSON.stringify(false), INFURA_API_KEY: JSON.stringify(process.env.INFURA_API_KEY), INFURA_API_KEY_SECRET: JSON.stringify(process.env.INFURA_API_KEY_SECRET), - CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API) + CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API), + BITTENSOR_API_KEY_1: JSON.stringify(process.env.BITTENSOR_API_KEY_1), + BITTENSOR_API_KEY_2: JSON.stringify(process.env.BITTENSOR_API_KEY_2) }; const additionalEnvDict = { diff --git a/packages/web-runner/webpack.config.cjs b/packages/web-runner/webpack.config.cjs index 7abf2b3df2..5562db1278 100644 --- a/packages/web-runner/webpack.config.cjs +++ b/packages/web-runner/webpack.config.cjs @@ -52,7 +52,9 @@ const _additionalEnv = { NFT_MINTING_HOST: JSON.stringify(process.env.NFT_MINTING_HOST), INFURA_API_KEY: JSON.stringify(process.env.INFURA_API_KEY), INFURA_API_KEY_SECRET: JSON.stringify(process.env.INFURA_API_KEY_SECRET), - CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API) + CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API), + BITTENSOR_API_KEY_1: JSON.stringify(process.env.BITTENSOR_API_KEY_1), + BITTENSOR_API_KEY_2: JSON.stringify(process.env.BITTENSOR_API_KEY_2) }; // Overwrite babel babel config from polkadot dev diff --git a/packages/webapp/webpack.config.cjs b/packages/webapp/webpack.config.cjs index d884f525d9..04a6c1c787 100644 --- a/packages/webapp/webpack.config.cjs +++ b/packages/webapp/webpack.config.cjs @@ -57,7 +57,9 @@ const _additionalEnv = { BANXA_TEST_MODE: JSON.stringify(false), INFURA_API_KEY: JSON.stringify(process.env.INFURA_API_KEY), INFURA_API_KEY_SECRET: JSON.stringify(process.env.INFURA_API_KEY_SECRET), - CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API) + CHAINFLIP_BROKER_API: JSON.stringify(process.env.CHAINFLIP_BROKER_API), + BITTENSOR_API_KEY_1: JSON.stringify(process.env.BITTENSOR_API_KEY_1), + BITTENSOR_API_KEY_2: JSON.stringify(process.env.BITTENSOR_API_KEY_2) }; const createConfig = (entry, alias = {}, useSplitChunk = false) => { diff --git a/yarn.lock b/yarn.lock index 33856b80fb..c790c1f171 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6268,14 +6268,14 @@ __metadata: languageName: node linkType: hard -"@subwallet/chain-list@npm:0.2.88": - version: 0.2.88 - resolution: "@subwallet/chain-list@npm:0.2.88" +"@subwallet/chain-list@npm:0.2.88-beta.6": + version: 0.2.88-beta.6 + resolution: "@subwallet/chain-list@npm:0.2.88-beta.6" dependencies: "@polkadot/dev": 0.67.167 "@polkadot/util": ^12.5.1 eventemitter3: ^5.0.1 - checksum: 80640b5b555c238398ecaf9d6a5ae0c27680b67e3352408da5c1b0e3da79e792eb5f75ca1db866e529655071c7a158cae55fc90086b77908218f19e3deeca338 + checksum: 2d2db1dd398a1d373e8eb663d16e1c878033c9cc97042e3a0ade81da522b91f269bc192f6b538617ecbf8c17c3b5113ead446620108a7b9357b5a981c4ef7f3d languageName: node linkType: hard @@ -6451,7 +6451,7 @@ __metadata: "@polkadot/util-crypto": ^12.6.2 "@ramonak/react-progress-bar": ^5.0.3 "@reduxjs/toolkit": ^1.9.1 - "@subwallet/chain-list": 0.2.88 + "@subwallet/chain-list": 0.2.88-beta.6 "@subwallet/extension-base": ^1.2.32-0 "@subwallet/extension-chains": ^1.2.32-0 "@subwallet/extension-dapp": ^1.2.32-0