Skip to content

Commit

Permalink
feat(staking): create a stake account on initial deposit
Browse files Browse the repository at this point in the history
  • Loading branch information
cprussin committed Sep 11, 2024
1 parent ad20151 commit 2ccb362
Show file tree
Hide file tree
Showing 18 changed files with 688 additions and 632 deletions.
180 changes: 114 additions & 66 deletions apps/staking/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export type AccountHistoryAction = ReturnType<
(typeof AccountHistoryAction)[keyof typeof AccountHistoryAction]
>;

type AccountHistory = {
export type AccountHistory = {
timestamp: Date;
action: AccountHistoryAction;
amount: bigint;
Expand All @@ -144,73 +144,114 @@ export const getStakeAccounts = async (

export const loadData = async (
client: PythStakingClient,
stakeAccount: StakeAccountPositions,
stakeAccount?: StakeAccountPositions | undefined,
): Promise<Data> => {
const [
stakeAccountCustody,
publishers,
ownerAtaAccount,
currentEpoch,
unlockSchedule,
] = await Promise.all([
client.getStakeAccountCustody(stakeAccount.address),
client.getPublishers(),
client.getOwnerPythAtaAccount(),
getCurrentEpoch(client.connection),
client.getUnlockSchedule(stakeAccount.address),
]);

const filterGovernancePositions = (positionState: PositionState) =>
getAmountByTargetAndState({
stakeAccountPositions: stakeAccount,
targetWithParameters: { voting: {} },
positionState,
epoch: currentEpoch,
});

const filterOISPositions = (
publisher: PublicKey,
positionState: PositionState,
) =>
getAmountByTargetAndState({
stakeAccountPositions: stakeAccount,
targetWithParameters: { integrityPool: { publisher } },
positionState,
epoch: currentEpoch,
});

return {
lastSlash: undefined, // TODO
availableRewards: 0n, // TODO
expiringRewards: undefined, // TODO
total: stakeAccountCustody.amount,
governance: {
warmup: filterGovernancePositions(PositionState.LOCKING),
staked: filterGovernancePositions(PositionState.LOCKED),
cooldown: filterGovernancePositions(PositionState.PREUNLOCKING),
cooldown2: filterGovernancePositions(PositionState.UNLOCKED),
},
unlockSchedule,
locked: unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n),
walletAmount: ownerAtaAccount.amount,
integrityStakingPublishers: publishers.map(({ pubkey: publisher }) => ({
apyHistory: [], // TODO
isSelf: false, // TODO
name: undefined, // TODO
numFeeds: 0, // TODO
poolCapacity: 100n, // TODO
poolUtilization: 0n, // TODO
publicKey: publisher,
qualityRanking: 0, // TODO
selfStake: 0n, // TODO
positions: {
warmup: filterOISPositions(publisher, PositionState.LOCKING),
staked: filterOISPositions(publisher, PositionState.LOCKED),
cooldown: filterOISPositions(publisher, PositionState.PREUNLOCKING),
cooldown2: filterOISPositions(publisher, PositionState.UNLOCKED),
if (stakeAccount === undefined) {
const [integrityStakingPublishers, ownerAtaAccount] = await Promise.all([
loadPublisherData(client),
client.getOwnerPythAtaAccount(),
]);

return {
lastSlash: undefined,
availableRewards: 0n,
expiringRewards: undefined,
total: 0n,
governance: {
warmup: 0n,
staked: 0n,
cooldown: 0n,
cooldown2: 0n,
},
})),
};
unlockSchedule: [],
locked: 0n,
walletAmount: ownerAtaAccount.amount,
integrityStakingPublishers,
};
} else {
const [
stakeAccountCustody,
publishers,
ownerAtaAccount,
currentEpoch,
unlockSchedule,
] = await Promise.all([
client.getStakeAccountCustody(stakeAccount.address),
loadPublisherData(client),
client.getOwnerPythAtaAccount(),
getCurrentEpoch(client.connection),
client.getUnlockSchedule(stakeAccount.address),
]);

const filterGovernancePositions = (positionState: PositionState) =>
getAmountByTargetAndState({
stakeAccountPositions: stakeAccount,
targetWithParameters: { voting: {} },
positionState,
epoch: currentEpoch,
});

const filterOISPositions = (
publisher: PublicKey,
positionState: PositionState,
) =>
getAmountByTargetAndState({
stakeAccountPositions: stakeAccount,
targetWithParameters: { integrityPool: { publisher } },
positionState,
epoch: currentEpoch,
});

return {
lastSlash: undefined, // TODO
availableRewards: 0n, // TODO
expiringRewards: undefined, // TODO
total: stakeAccountCustody.amount,
governance: {
warmup: filterGovernancePositions(PositionState.LOCKING),
staked: filterGovernancePositions(PositionState.LOCKED),
cooldown: filterGovernancePositions(PositionState.PREUNLOCKING),
cooldown2: filterGovernancePositions(PositionState.UNLOCKED),
},
unlockSchedule,
locked: unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n),
walletAmount: ownerAtaAccount.amount,
integrityStakingPublishers: publishers.map((publisher) => ({
...publisher,
positions: {
warmup: filterOISPositions(
publisher.publicKey,
PositionState.LOCKING,
),
staked: filterOISPositions(publisher.publicKey, PositionState.LOCKED),
cooldown: filterOISPositions(
publisher.publicKey,
PositionState.PREUNLOCKING,
),
cooldown2: filterOISPositions(
publisher.publicKey,
PositionState.UNLOCKED,
),
},
})),
};
}
};

const loadPublisherData = async (client: PythStakingClient) => {
const publishers = await client.getPublishers();

return publishers.map(({ pubkey: publisher }) => ({
apyHistory: [], // TODO
isSelf: false, // TODO
name: undefined, // TODO
numFeeds: 0, // TODO
poolCapacity: 100n, // TODO
poolUtilization: 0n, // TODO
publicKey: publisher,
qualityRanking: 0, // TODO
selfStake: 0n, // TODO
}));
};

export const loadAccountHistory = async (
Expand All @@ -221,6 +262,13 @@ export const loadAccountHistory = async (
return mkMockHistory();
};

export const createStakeAccountAndDeposit = async (
_client: PythStakingClient,
_amount: bigint,
): Promise<StakeAccountPositions> => {
throw new NotImplementedError();
};

export const deposit = async (
client: PythStakingClient,
stakeAccount: PublicKey,
Expand Down
9 changes: 6 additions & 3 deletions apps/staking/src/components/AccountHistory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
AccountHistoryItemType,
StakeType,
} from "../../api";
import { StateType, useAccountHistory } from "../../hooks/use-account-history";
import type { States, StateType as ApiStateType } from "../../hooks/use-api";
import { StateType, useData } from "../../hooks/use-data";
import { Tokens } from "../Tokens";

export const AccountHistory = () => {
const history = useAccountHistory();
type Props = { api: States[ApiStateType.Loaded] };

export const AccountHistory = ({ api }: Props) => {
const history = useData(api.accountHisoryCacheKey, api.loadAccountHistory);

switch (history.type) {
case StateType.NotLoaded:
Expand Down
41 changes: 27 additions & 14 deletions apps/staking/src/components/AccountSummary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
} from "react-aria-components";

import background from "./background.png";
import { deposit, withdraw, claim } from "../../api";
import { StateType, useTransfer } from "../../hooks/use-transfer";
import { type States, StateType as ApiStateType } from "../../hooks/use-api";
import { StateType, useAsync } from "../../hooks/use-async";
import { Button } from "../Button";
import { ModalDialog } from "../ModalDialog";
import { Tokens } from "../Tokens";
import { TransferButton } from "../TransferButton";

type Props = {
api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
total: bigint;
locked: bigint;
unlockSchedule: {
Expand All @@ -38,6 +39,7 @@ type Props = {
};

export const AccountSummary = ({
api,
locked,
unlockSchedule,
lastSlash,
Expand Down Expand Up @@ -114,7 +116,7 @@ export const AccountSummary = ({
actionDescription="Add funds to your balance"
actionName="Add Tokens"
max={walletAmount}
transfer={deposit}
transfer={api.deposit}
/>
</div>
</div>
Expand All @@ -130,16 +132,25 @@ export const AccountSummary = ({
actionDescription="Move funds from your account back to your wallet"
actionName="Withdraw"
max={availableToWithdraw}
transfer={withdraw}
isDisabled={availableToWithdraw === 0n}
{...(api.type === ApiStateType.Loaded && {
transfer: api.withdraw,
})}
/>
}
/>
<BalanceCategory
name="Available Rewards"
amount={availableRewards}
description="Rewards you have earned from OIS"
action={<ClaimButton isDisabled={availableRewards === 0n} />}
action={
api.type === ApiStateType.Loaded ? (
<ClaimButton isDisabled={availableRewards === 0n} api={api} />
) : (
<Button size="small" variant="secondary" isDisabled={true}>
Claim
</Button>
)
}
{...(expiringRewards !== undefined &&
expiringRewards.amount > 0n && {
warning: (
Expand Down Expand Up @@ -187,13 +198,15 @@ const BalanceCategory = ({
</div>
);

const ClaimButton = (
props: Omit<
ComponentProps<typeof Button>,
"onClick" | "disabled" | "loading"
>,
) => {
const { state, execute } = useTransfer(claim);
type ClaimButtonProps = Omit<
ComponentProps<typeof Button>,
"onClick" | "disabled" | "loading"
> & {
api: States[ApiStateType.Loaded];
};

const ClaimButton = ({ api, ...props }: ClaimButtonProps) => {
const { state, execute } = useAsync(api.claim);

const doClaim = useCallback(() => {
execute().catch(() => {
Expand All @@ -207,7 +220,7 @@ const ClaimButton = (
variant="secondary"
onPress={doClaim}
isDisabled={state.type !== StateType.Base}
isLoading={state.type === StateType.Submitting}
isLoading={state.type === StateType.Running}
{...props}
>
Claim
Expand Down
3 changes: 2 additions & 1 deletion apps/staking/src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ type VariantProps = {
size?: "small" | "nopad" | undefined;
};

type ButtonProps = ComponentProps<typeof ReactAriaButton> &
type ButtonProps = Omit<ComponentProps<typeof ReactAriaButton>, "isDisabled"> &
VariantProps & {
isLoading?: boolean | undefined;
isDisabled?: boolean | undefined;
};

export const Button = ({
Expand Down
6 changes: 6 additions & 0 deletions apps/staking/src/components/Dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { type ComponentProps, useMemo } from "react";
import { Tabs, TabList, Tab, TabPanel } from "react-aria-components";

import type { States, StateType as ApiStateType } from "../../hooks/use-api";
import { AccountSummary } from "../AccountSummary";
import { Governance } from "../Governance";
import { OracleIntegrityStaking } from "../OracleIntegrityStaking";
import { Styled } from "../Styled";

type Props = {
api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
total: bigint;
lastSlash:
| {
Expand Down Expand Up @@ -39,6 +41,7 @@ type Props = {
};

export const Dashboard = ({
api,
total,
lastSlash,
walletAmount,
Expand Down Expand Up @@ -108,6 +111,7 @@ export const Dashboard = ({
return (
<div className="flex w-full flex-col gap-8">
<AccountSummary
api={api}
locked={locked}
unlockSchedule={unlockSchedule}
lastSlash={lastSlash}
Expand Down Expand Up @@ -140,6 +144,7 @@ export const Dashboard = ({
</DashboardTabPanel>
<DashboardTabPanel id={TabIds.Governance}>
<Governance
api={api}
availableToStake={availableToStakeGovernance}
warmup={governance.warmup}
staked={governance.staked}
Expand All @@ -149,6 +154,7 @@ export const Dashboard = ({
</DashboardTabPanel>
<DashboardTabPanel id={TabIds.IntegrityStaking}>
<OracleIntegrityStaking
api={api}
availableToStake={availableToStakeIntegrity}
locked={locked}
warmup={integrityStakingWarmup}
Expand Down
Loading

0 comments on commit 2ccb362

Please sign in to comment.