Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/webapp-yield' into webapp-dev
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/extension-koni-ui/src/components/Earning/HorizontalEarningItem.tsx
  • Loading branch information
saltict committed Oct 23, 2023
2 parents 3b1ad87 + 03a3ca5 commit ea74f75
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 131 deletions.
4 changes: 2 additions & 2 deletions packages/extension-base/src/background/KoniTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2207,12 +2207,12 @@ export interface YieldStepDetail {

export interface OptimalYieldPath {
totalFee: YieldTokenBaseInfo[],
steps: YieldStepDetail[]
steps: YieldStepDetail[],
connectionError?: string
}

export enum YieldValidationStatus {
NOT_ENOUGH_FEE = 'NOT_ENOUGH_FEE',
NOT_ENOUGH_MIN_AMOUNT = 'NOT_ENOUGH_MIN_AMOUNT',
NOT_ENOUGH_BALANCE = 'NOT_ENOUGH_BALANCE',
NOT_ENOUGH_MIN_JOIN_POOL = 'NOT_ENOUGH_MIN_JOIN_POOL',
OK = 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { SWError } from '@subwallet/extension-base/background/errors/SWError';
import { BasicTxErrorType, StakingTxErrorType, TransactionErrorType, TransferTxErrorType } from '@subwallet/extension-base/background/KoniTypes';
import { BasicTxErrorType, StakingTxErrorType, TransactionErrorType, TransferTxErrorType, YieldValidationStatus } from '@subwallet/extension-base/background/KoniTypes';
import { detectTranslate } from '@subwallet/extension-base/utils';
import { t } from 'i18next';

Expand Down Expand Up @@ -83,6 +83,14 @@ const defaultErrorMap = {
[TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT]: {
message: detectTranslate('Receiver is not enough existential deposit'),
code: undefined
},
[YieldValidationStatus.NOT_ENOUGH_FEE]: {
message: detectTranslate('Insufficient balance'),
code: undefined
},
[YieldValidationStatus.NOT_ENOUGH_MIN_JOIN_POOL]: {
message: detectTranslate('Not enough min earning amount'),
code: undefined
}
} as Record<TransactionErrorType, { message: string, code?: number }>;

Expand Down
15 changes: 14 additions & 1 deletion packages/extension-base/src/koni/api/yield/helper/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,20 @@ export const DEFAULT_YIELD_FIRST_STEP: YieldStepDetail = {
type: YieldStepType.DEFAULT
};

export const YIELD_EXTRINSIC_TYPES = [ExtrinsicType.MINT_VDOT, ExtrinsicType.MINT_LDOT, ExtrinsicType.MINT_SDOT, ExtrinsicType.MINT_QDOT];
export const YIELD_EXTRINSIC_TYPES = [
ExtrinsicType.MINT_VDOT,
ExtrinsicType.MINT_LDOT,
ExtrinsicType.MINT_SDOT,
ExtrinsicType.MINT_QDOT,
ExtrinsicType.REDEEM_QDOT,
ExtrinsicType.REDEEM_SDOT,
ExtrinsicType.REDEEM_VDOT,
ExtrinsicType.REDEEM_LDOT,
ExtrinsicType.STAKING_JOIN_POOL,
ExtrinsicType.STAKING_CLAIM_REWARD,
ExtrinsicType.STAKING_LEAVE_POOL,
ExtrinsicType.STAKING_POOL_WITHDRAW
];

export const YIELD_POOL_STAT_REFRESH_INTERVAL = 300000;

Expand Down
234 changes: 138 additions & 96 deletions packages/extension-base/src/koni/api/yield/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,115 +178,157 @@ export async function generatePathForLiquidStaking (params: OptimalYieldPathPara
steps: [DEFAULT_YIELD_FIRST_STEP]
};

const poolOriginSubstrateApi = await params.substrateApiMap[params.poolInfo.chain].isReady;

const inputTokenSlug = params.poolInfo.inputAssets[0]; // assume that the pool only has 1 input token, will update later
const inputTokenInfo = params.assetInfoMap[inputTokenSlug];

const altInputTokenSlug = params.poolInfo.altInputAssets ? params.poolInfo?.altInputAssets[0] : '';
const altInputTokenInfo = params.assetInfoMap[altInputTokenSlug];

const [inputTokenBalance, altInputTokenBalance] = await Promise.all([
balanceService.getTokenFreeBalance(params.address, inputTokenInfo.originChain, inputTokenSlug),
balanceService.getTokenFreeBalance(params.address, altInputTokenInfo.originChain, altInputTokenSlug)
]);

const bnInputTokenBalance = new BN(inputTokenBalance.value);
const defaultFeeTokenSlug = params.poolInfo.feeAssets[0];

if (!bnInputTokenBalance.gte(bnAmount)) {
if (params.poolInfo.altInputAssets) {
const bnAltInputTokenBalance = new BN(altInputTokenBalance.value || '0');

if (bnAltInputTokenBalance.gt(BN_ZERO)) {
result.steps.push({
id: result.steps.length,
metadata: {
sendingValue: bnAmount.toString(),
try {
const poolOriginSubstrateApi = await params.substrateApiMap[params.poolInfo.chain].isReady;

const inputTokenSlug = params.poolInfo.inputAssets[0]; // assume that the pool only has 1 input token, will update later
const inputTokenInfo = params.assetInfoMap[inputTokenSlug];

const altInputTokenSlug = params.poolInfo.altInputAssets ? params.poolInfo?.altInputAssets[0] : '';
const altInputTokenInfo = params.assetInfoMap[altInputTokenSlug];

const [inputTokenBalance, altInputTokenBalance] = await Promise.all([
balanceService.getTokenFreeBalance(params.address, inputTokenInfo.originChain, inputTokenSlug),
balanceService.getTokenFreeBalance(params.address, altInputTokenInfo.originChain, altInputTokenSlug)
]);

const bnInputTokenBalance = new BN(inputTokenBalance.value);
const defaultFeeTokenSlug = params.poolInfo.feeAssets[0];

if (!bnInputTokenBalance.gte(bnAmount)) {
if (params.poolInfo.altInputAssets) {
const bnAltInputTokenBalance = new BN(altInputTokenBalance.value || '0');

if (bnAltInputTokenBalance.gt(BN_ZERO)) {
result.steps.push({
id: result.steps.length,
metadata: {
sendingValue: bnAmount.toString(),
originTokenInfo: altInputTokenInfo,
destinationTokenInfo: inputTokenInfo
},
name: 'Transfer DOT from Polkadot',
type: YieldStepType.XCM
});

const xcmOriginSubstrateApi = await params.substrateApiMap[altInputTokenInfo.originChain].isReady;

const xcmTransfer = await createXcmExtrinsic({
originTokenInfo: altInputTokenInfo,
destinationTokenInfo: inputTokenInfo
},
name: 'Transfer DOT from Polkadot',
type: YieldStepType.XCM
});

const xcmOriginSubstrateApi = await params.substrateApiMap[altInputTokenInfo.originChain].isReady;

const xcmTransfer = await createXcmExtrinsic({
originTokenInfo: altInputTokenInfo,
destinationTokenInfo: inputTokenInfo,
sendingValue: bnAmount.toString(),
recipient: fakeAddress,
chainInfoMap: params.chainInfoMap,
substrateApi: xcmOriginSubstrateApi
});

const _xcmFeeInfo = await xcmTransfer.paymentInfo(fakeAddress);
const xcmFeeInfo = _xcmFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;
// TODO: calculate fee for destination chain

result.totalFee.push({
slug: altInputTokenSlug,
amount: (xcmFeeInfo.partialFee * 1.2).toString() // TODO
});
destinationTokenInfo: inputTokenInfo,
sendingValue: bnAmount.toString(),
recipient: fakeAddress,
chainInfoMap: params.chainInfoMap,
substrateApi: xcmOriginSubstrateApi
});

const _xcmFeeInfo = await xcmTransfer.paymentInfo(fakeAddress);
const xcmFeeInfo = _xcmFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;
// TODO: calculate fee for destination chain

result.totalFee.push({
slug: altInputTokenSlug,
amount: (xcmFeeInfo.partialFee * 1.2).toString() // TODO
});
}
}
}
}

let mintFee = '0';

if (params.poolInfo.slug === 'DOT___bifrost_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint vDOT',
type: YieldStepType.MINT_VDOT
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.vtokenMinting.mint(_getTokenOnChainInfo(inputTokenInfo), params.amount, null).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;
let mintFee = '0';

if (params.poolInfo.slug === 'DOT___bifrost_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint vDOT',
type: YieldStepType.MINT_VDOT
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.vtokenMinting.mint(_getTokenOnChainInfo(inputTokenInfo), params.amount, null).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___acala_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint LDOT',
type: YieldStepType.MINT_LDOT
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.homa.mint(params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___interlay_lending') {
result.steps.push({
id: result.steps.length,
name: 'Mint qDOT',
type: YieldStepType.MINT_QDOT
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.loans.mint(_getTokenOnChainInfo(inputTokenInfo), params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___parallel_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint sDOT',
type: YieldStepType.MINT_SDOT
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.liquidStaking.stake(params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

mintFee = mintFeeInfo.partialFee.toString();
}

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___acala_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint LDOT',
type: YieldStepType.MINT_LDOT
result.totalFee.push({
slug: defaultFeeTokenSlug,
amount: mintFee
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.homa.mint(params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;
return result;
} catch (e) {
// @ts-ignore
const errorMessage = e.message as string;

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___interlay_lending') {
result.steps.push({
id: result.steps.length,
name: 'Mint qDOT',
type: YieldStepType.MINT_QDOT
});
if (errorMessage.includes('network')) {
result.connectionError = errorMessage.split(' ')[0];
}

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.loans.mint(_getTokenOnChainInfo(inputTokenInfo), params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;
if (params.poolInfo.slug === 'DOT___bifrost_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint vDOT',
type: YieldStepType.MINT_VDOT
});
} else if (params.poolInfo.slug === 'DOT___acala_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint LDOT',
type: YieldStepType.MINT_LDOT
});
} else if (params.poolInfo.slug === 'DOT___interlay_lending') {
result.steps.push({
id: result.steps.length,
name: 'Mint qDOT',
type: YieldStepType.MINT_QDOT
});
} else if (params.poolInfo.slug === 'DOT___parallel_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint sDOT',
type: YieldStepType.MINT_SDOT
});
}

mintFee = mintFeeInfo.partialFee.toString();
} else if (params.poolInfo.slug === 'DOT___parallel_liquid_staking') {
result.steps.push({
id: result.steps.length,
name: 'Mint sDOT',
type: YieldStepType.MINT_SDOT
result.totalFee.push({
slug: params.poolInfo.feeAssets[0],
amount: '0'
});

const _mintFeeInfo = await poolOriginSubstrateApi.api.tx.liquidStaking.stake(params.amount).paymentInfo(fakeAddress);
const mintFeeInfo = _mintFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

mintFee = mintFeeInfo.partialFee.toString();
return result;
}

result.totalFee.push({
slug: defaultFeeTokenSlug,
amount: mintFee
});

return result;
}

export async function validateEarningProcess (address: string, params: OptimalYieldPathParams, path: OptimalYieldPath, balanceService: BalanceService): Promise<TransactionError[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3915,8 +3915,6 @@ export default class KoniExtension {
substrateApiMap: this.#koniState.getSubstrateApiMap()
}, inputData, path, inputData.currentStep, this.#koniState.balanceService);

console.log('extrinsic', extrinsic.toHex());

const isMintingStep = YIELD_EXTRINSIC_TYPES.includes(extrinsicType);
const isPoolSupportAlternativeFee = yieldPoolInfo.feeAssets.length > 1;

Expand Down Expand Up @@ -3953,8 +3951,6 @@ export default class KoniExtension {
substrateApiMap: this.#koniState.getSubstrateApiMap()
}, address, amount, yieldPositionInfo);

console.log('extrinsic', extrinsic.toHex());

return await this.#koniState.transactionService.handleTransaction({
address,
chain: yieldPoolInfo.chain,
Expand Down Expand Up @@ -4010,6 +4006,8 @@ export default class KoniExtension {
substrateApiMap: this.#koniState.getSubstrateApiMap()
};

return [];

return validateYieldProcess(
inputData.address,
params,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { _getSubstrateGenesisHash, _isChainEvmCompatible } from '@subwallet/exte
import { EarningCalculatorModal, EarningItem, EarningToolbar, EmptyList } from '@subwallet/extension-koni-ui/components';
import EarningInfoModal from '@subwallet/extension-koni-ui/components/Modal/Earning/EarningInfoModal';
import Search from '@subwallet/extension-koni-ui/components/Search';
import { BN_TEN, CREATE_RETURN, DEFAULT_ROUTER_PATH, DEFAULT_YIELD_PARAMS, EARNING_INFO_MODAL, STAKING_CALCULATOR_MODAL, YIELD_TRANSACTION } from '@subwallet/extension-koni-ui/constants';
import { BN_TEN, CREATE_RETURN, DEFAULT_ROUTER_PATH, DEFAULT_YIELD_PARAMS, EARNING_INFO_MODAL, EXCLUSIVE_REWARD_SLUGS, STAKING_CALCULATOR_MODAL, YIELD_TRANSACTION } from '@subwallet/extension-koni-ui/constants';
import { ScreenContext } from '@subwallet/extension-koni-ui/contexts/ScreenContext';
import { useFilterModal, usePreCheckAction, useTranslation } from '@subwallet/extension-koni-ui/hooks';
import { RootState } from '@subwallet/extension-koni-ui/stores';
Expand Down Expand Up @@ -82,7 +82,7 @@ const Component: React.FC<Props> = (props: Props) => {
},
{
desc: true,
label: t('Incentive'),
label: t('Rewards'),
value: SortKey.INCENTIVE
}
];
Expand Down Expand Up @@ -238,12 +238,14 @@ const Component: React.FC<Props> = (props: Props) => {
const aInputDecimals = aInputAsset.decimals || 0;
const aTotalValue = new BigN(a.stats?.tvl || '0').div(BN_TEN.pow(aInputDecimals));
const aTotalApy = a.stats?.totalApy ?? calculateReward(a.stats?.totalApr || 0, 0, YieldCompoundingPeriod.YEARLY).apy ?? 0;
const aIncentive = EXCLUSIVE_REWARD_SLUGS.includes(a.slug) ? 1 : 0;

const bInputSlug = b.inputAssets[0];
const bInputAsset = assetRegistry[bInputSlug];
const bInputDecimals = bInputAsset.decimals || 0;
const bTotalValue = new BigN(b.stats?.tvl || '0').div(BN_TEN.pow(bInputDecimals));
const bTotalApy = b.stats?.totalApy ?? calculateReward(b.stats?.totalApr || 0, 0, YieldCompoundingPeriod.YEARLY).apy ?? 0;
const bIncentive = EXCLUSIVE_REWARD_SLUGS.includes(b.slug) ? 1 : 0;

switch (sortSelection) {
case SortKey.TOTAL_VALUE:
Expand All @@ -252,6 +254,9 @@ const Component: React.FC<Props> = (props: Props) => {
case SortKey.APY:
return bTotalApy - aTotalApy;

case SortKey.INCENTIVE:
return (bIncentive - aIncentive) || new BigN(bTotalValue).minus(aTotalValue).toNumber();

default:
return 0;
}
Expand Down
Loading

0 comments on commit ea74f75

Please sign in to comment.