Skip to content

Commit

Permalink
feat: pool staked claim happy path working
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt561 committed Oct 24, 2024
1 parent e721d23 commit 58543da
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import { strings } from '../../../../../../../../locales/i18n';
import Button, {
ButtonVariants,
} from '../../../../../../../component-library/components/Buttons/Button';
import useTooltipModal from '../../../../../../hooks/useTooltipModal';
import { BannerProps } from '../../../../../../../component-library/components/Banners/Banner/Banner.types';
import { useStyles } from '../../../../../../../component-library/hooks';
import styleSheet from './ClaimBanner.styles';
import usePoolStakedClaim from '../../../../hooks/usePoolStakedClaim';
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccount } from '../../../../../../../selectors/accountsController';
import usePooledStakes from '../../../../hooks/usePooledStakes';
import Engine from '../../../../../../../core/Engine';

type StakeBannerProps = Pick<BannerProps, 'style'> & {
claimableAmount: string;
Expand All @@ -23,9 +27,30 @@ type StakeBannerProps = Pick<BannerProps, 'style'> & {
const ClaimBanner = ({ claimableAmount, style }: StakeBannerProps) => {
const { styles } = useStyles(styleSheet, {});

const { openTooltipModal } = useTooltipModal();
const activeAccount = useSelector(selectSelectedInternalAccount);

const onClaimPress = () => openTooltipModal('TODO', 'Connect to claim flow');
const { attemptPoolStakedClaimTransaction } = usePoolStakedClaim();

const { pooledStakesData, refreshPooledStakes } = usePooledStakes();

const onClaimPress = async () => {
if (!activeAccount?.address) return;

const txRes = await attemptPoolStakedClaimTransaction(
activeAccount?.address,
pooledStakesData,
);

const transactionId = txRes?.transactionMeta.id;

Engine.controllerMessenger.subscribeOnceIf(
'TransactionController:transactionConfirmed',
() => {
refreshPooledStakes();
},
(transactionMeta) => transactionMeta.id === transactionId,
);
};

return (
<Banner
Expand Down
132 changes: 132 additions & 0 deletions app/components/UI/Stake/hooks/usePoolStakedClaim/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { PooledStake, PooledStakingContract } from '@metamask/stake-sdk';
import { useStakeContext } from '../useStakeContext';
import trackErrorAsAnalytics from '../../../../../util/metrics/TrackError/trackErrorAsAnalytics';
import { WalletDevice } from '@metamask/transaction-controller';
import { addTransaction } from '../../../../../util/transaction-controller';
import { ORIGIN_METAMASK } from '@metamask/controller-utils';
import {
generateClaimTxParams,
isRequestClaimable,
transformAggregatedClaimableExitRequestToMulticallArgs,
} from './utils';

const attemptMultiCallClaimTransaction = async (
pooledStakesData: PooledStake,
poolStakingContract: PooledStakingContract,
activeAccountAddress: string,
) => {
const multiCallData = transformAggregatedClaimableExitRequestToMulticallArgs(
pooledStakesData.exitRequests,
);

const gasLimit = await poolStakingContract.estimateMulticallGas(
multiCallData,
activeAccountAddress,
);

const { data, chainId } =
await poolStakingContract.encodeMulticallTransactionData(
multiCallData,
activeAccountAddress,
{ gasLimit, gasBufferPct: 30 }, // 30% buffer
);

const txParams = generateClaimTxParams(
activeAccountAddress,
poolStakingContract.contract.address,
data,
chainId,
gasLimit.toString(),
);

return addTransaction(txParams, {
deviceConfirmedOn: WalletDevice.MM_MOBILE,
origin: ORIGIN_METAMASK,
});
};

const attemptSingleClaimTransaction = async (
pooledStakesData: PooledStake,
poolStakingContract: PooledStakingContract,
activeAccountAddress: string,
) => {
const { positionTicket, timestamp, exitQueueIndex } =
pooledStakesData.exitRequests[0];

if (!isRequestClaimable(exitQueueIndex, timestamp)) return;

const gasLimit = await poolStakingContract.estimateClaimExitedAssetsGas(
positionTicket,
timestamp,
exitQueueIndex,
activeAccountAddress,
);

const { data, chainId } =
await poolStakingContract.encodeClaimExitedAssetsTransactionData(
positionTicket,
timestamp,
exitQueueIndex,
activeAccountAddress,
{
gasLimit,
gasBufferPct: 30, // 30% buffer
},
);

const txParams = generateClaimTxParams(
activeAccountAddress,
poolStakingContract.contract.address,
data,
chainId,
gasLimit.toString(),
);

return addTransaction(txParams, {
deviceConfirmedOn: WalletDevice.MM_MOBILE,
origin: ORIGIN_METAMASK,
});
};

// TODO: Add tests
const attemptPoolStakedClaimTransaction =
(poolStakingContract: PooledStakingContract) =>
async (activeAccountAddress: string, pooledStakesData: PooledStake) => {
try {
if (pooledStakesData.exitRequests.length === 0) return;

const isMultiCallClaim = pooledStakesData.exitRequests.length > 1;

return isMultiCallClaim
? attemptMultiCallClaimTransaction(
pooledStakesData,
poolStakingContract,
activeAccountAddress,
)
: attemptSingleClaimTransaction(
pooledStakesData,
poolStakingContract,
activeAccountAddress,
);
} catch (e) {
const errorMessage = (e as Error).message;
trackErrorAsAnalytics(
'Pooled Staking Claim Transaction Failed',
errorMessage,
);
}
};

const usePoolStakedClaim = () => {
const poolStakeContext = useStakeContext();

const stakingContract =
poolStakeContext?.stakingContract as PooledStakingContract;

return {
attemptPoolStakedClaimTransaction:
attemptPoolStakedClaimTransaction(stakingContract),
};
};

export default usePoolStakedClaim;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type MultiCallData = { functionName: string; args: string[] }[];
65 changes: 65 additions & 0 deletions app/components/UI/Stake/hooks/usePoolStakedClaim/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ChainId, PooledStakeExitRequest } from '@metamask/stake-sdk';
import { BigNumber } from 'ethers';
import { MultiCallData } from './usePoolStakedClaim.types';
import { TransactionParams } from '@metamask/transaction-controller';
import { toHex } from '@metamask/controller-utils';

const TWENTY_FOUR_HOURS_IN_SECONDS = 86400;
const CLAIM_EXITED_ASSETS = 'claimExitedAssets';

export const have24HoursPassed = (timestamp: string) => {
const current = Math.floor(Number(new Date().getTime() / 1000));
const timestampInSeconds = Math.floor(Number(timestamp) / 1000);

const difference = Number(current) - Number(timestampInSeconds);

return difference > TWENTY_FOUR_HOURS_IN_SECONDS;
};

export const isRequestClaimable = (
exitQueueIndex: string,
timestamp: string,
) => {
const isValidExitQueueIndex = exitQueueIndex && exitQueueIndex !== '-1';
return isValidExitQueueIndex && have24HoursPassed(timestamp);
};

export const transformAggregatedClaimableExitRequestToMulticallArgs = (
exitRequests: PooledStakeExitRequest[],
): MultiCallData => {
const result: MultiCallData = [];

for (const { positionTicket, timestamp, exitQueueIndex } of exitRequests) {
// claimExitedAssets rules: https://docs.google.com/document/d/1LJYXaTxdOaze8F7PwgJDG10yWw9xW0Vqinq2Nyn2Hp4/edit?tab=t.0#heading=h.a8yj0zi6pn8h
if (!isRequestClaimable(exitQueueIndex, timestamp)) continue;

const claim = {
functionName: CLAIM_EXITED_ASSETS,
args: [
BigNumber.from(positionTicket).toString(),
// Convert timestamp from milliseconds to seconds.
BigNumber.from(timestamp).div(1000).toString(),
BigNumber.from(exitQueueIndex).toString(),
],
};

result.push(claim);
}

return result;
};

export const generateClaimTxParams = (
activeAccountAddress: string,
contractAddress: string,
encodedClaimTransactionData: string,
chainId: ChainId,
gasLimit: string,
): TransactionParams => ({
to: contractAddress,
from: activeAccountAddress,
chainId: `0x${chainId}`,
data: encodedClaimTransactionData,
value: '0',
gas: toHex(gasLimit),
});

0 comments on commit 58543da

Please sign in to comment.