diff --git a/apps/staking/src/api.ts b/apps/staking/src/api.ts index f14cb160c..184ea5346 100644 --- a/apps/staking/src/api.ts +++ b/apps/staking/src/api.ts @@ -1,7 +1,7 @@ // TODO remove these disables when moving off the mock APIs /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { HermesClient } from "@pythnetwork/hermes-client"; +import type { HermesClient, PublisherCaps } from "@pythnetwork/hermes-client"; import { epochToDate, extractPublisherData, @@ -38,7 +38,6 @@ type Data = { expiry: Date; } | undefined; - locked: bigint; unlockSchedule: { date: Date; amount: bigint; @@ -141,7 +140,7 @@ export type AccountHistoryAction = ReturnType< (typeof AccountHistoryAction)[keyof typeof AccountHistoryAction] >; -type AccountHistory = { +export type AccountHistory = { timestamp: Date; action: AccountHistoryAction; amount: bigint; @@ -159,38 +158,59 @@ export const getStakeAccounts = async ( export const loadData = async ( client: PythStakingClient, hermesClient: HermesClient, - stakeAccount: StakeAccountPositions, + stakeAccount?: StakeAccountPositions | undefined, +): Promise => + stakeAccount === undefined + ? loadDataNoStakeAccount(client, hermesClient) + : loadDataForStakeAccount(client, hermesClient, stakeAccount); + +const loadDataNoStakeAccount = async ( + client: PythStakingClient, + hermesClient: HermesClient, ): Promise => { + const { publishers, ...baseInfo } = await loadBaseInfo(client, hermesClient); + + return { + ...baseInfo, + lastSlash: undefined, + availableRewards: 0n, + expiringRewards: undefined, + total: 0n, + governance: { + warmup: 0n, + staked: 0n, + cooldown: 0n, + cooldown2: 0n, + }, + unlockSchedule: [], + integrityStakingPublishers: publishers.map( + ({ stakeAccount, ...publisher }) => ({ + ...publisher, + isSelf: false, + }), + ), + }; +}; + +const loadDataForStakeAccount = async ( + client: PythStakingClient, + hermesClient: HermesClient, + stakeAccount: StakeAccountPositions, +) => { const [ + { publishers, ...baseInfo }, stakeAccountCustody, - poolData, - ownerPythBalance, unlockSchedule, - poolConfig, claimableRewards, currentEpoch, - publisherRankingsResponse, - publisherCaps, ] = await Promise.all([ + loadBaseInfo(client, hermesClient), client.getStakeAccountCustody(stakeAccount.address), - client.getPoolDataAccount(), - client.getOwnerPythBalance(), client.getUnlockSchedule(stakeAccount.address), - client.getPoolConfigAccount(), client.getClaimableRewards(stakeAccount.address), getCurrentEpoch(client.connection), - fetch("/api/publishers-ranking"), - hermesClient.getLatestPublisherCaps({ - parsed: true, - }), ]); - const publishers = extractPublisherData(poolData); - - const publisherRankings = publishersRankingSchema.parse( - await publisherRankingsResponse.json(), - ); - const filterGovernancePositions = (positionState: PositionState) => getAmountByTargetAndState({ stakeAccountPositions: stakeAccount, @@ -210,19 +230,12 @@ export const loadData = async ( epoch: currentEpoch, }); - const getPublisherCap = (publisher: PublicKey) => - BigInt( - publisherCaps.parsed?.[0]?.publisher_stake_caps.find( - ({ publisher: p }) => p === publisher.toBase58(), - )?.cap ?? 0, - ); - return { + ...baseInfo, lastSlash: undefined, // TODO availableRewards: claimableRewards, expiringRewards: undefined, // TODO total: stakeAccountCustody.amount, - yieldRate: poolConfig.y, governance: { warmup: filterGovernancePositions(PositionState.LOCKING), staked: filterGovernancePositions(PositionState.LOCKED), @@ -230,51 +243,92 @@ export const loadData = async ( cooldown2: filterGovernancePositions(PositionState.UNLOCKED), }, unlockSchedule, - locked: unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n), - walletAmount: ownerPythBalance, - integrityStakingPublishers: publishers.map((publisherData) => { - const publisherPubkeyString = publisherData.pubkey.toBase58(); - const publisherRanking = publisherRankings.find( - (ranking) => ranking.publisher === publisherPubkeyString, - ); - const apyHistory = publisherData.apyHistory.map(({ epoch, apy }) => ({ - date: epochToDate(epoch + 1n), - apy: Number(apy), - })); - return { - apyHistory, - isSelf: - publisherData.stakeAccount?.equals(stakeAccount.address) ?? false, - name: undefined, // TODO - numFeeds: publisherRanking?.numSymbols ?? 0, - poolCapacity: getPublisherCap(publisherData.pubkey), - poolUtilization: publisherData.totalDelegation, - publicKey: publisherData.pubkey, - qualityRanking: publisherRanking?.rank ?? 0, - selfStake: publisherData.selfDelegation, + integrityStakingPublishers: publishers.map( + ({ stakeAccount: publisherStakeAccount, ...publisher }) => ({ + ...publisher, + isSelf: publisherStakeAccount?.equals(stakeAccount.address) ?? false, positions: { warmup: filterOISPositions( - publisherData.pubkey, + publisher.publicKey, PositionState.LOCKING, ), - staked: filterOISPositions( - publisherData.pubkey, - PositionState.LOCKED, - ), + staked: filterOISPositions(publisher.publicKey, PositionState.LOCKED), cooldown: filterOISPositions( - publisherData.pubkey, + publisher.publicKey, PositionState.PREUNLOCKING, ), cooldown2: filterOISPositions( - publisherData.pubkey, + publisher.publicKey, PositionState.UNLOCKED, ), }, - }; - }), + }), + ), }; }; +const loadBaseInfo = async ( + client: PythStakingClient, + hermesClient: HermesClient, +) => { + const [publishers, walletAmount, poolConfig] = await Promise.all([ + loadPublisherData(client, hermesClient), + client.getOwnerPythBalance(), + client.getPoolConfigAccount(), + ]); + + return { yieldRate: poolConfig.y, walletAmount, publishers }; +}; + +const loadPublisherData = async ( + client: PythStakingClient, + hermesClient: HermesClient, +) => { + const [poolData, publisherRankings, publisherCaps] = await Promise.all([ + client.getPoolDataAccount(), + getPublisherRankings(), + hermesClient.getLatestPublisherCaps({ + parsed: true, + }), + ]); + + return extractPublisherData(poolData).map((publisher) => { + const publisherPubkeyString = publisher.pubkey.toBase58(); + const publisherRanking = publisherRankings.find( + (ranking) => ranking.publisher === publisherPubkeyString, + ); + const apyHistory = publisher.apyHistory.map(({ epoch, apy }) => ({ + date: epochToDate(epoch + 1n), + apy: Number(apy), + })); + + return { + apyHistory, + name: undefined, // TODO + numFeeds: publisherRanking?.numSymbols ?? 0, + poolCapacity: getPublisherCap(publisherCaps, publisher.pubkey), + poolUtilization: publisher.totalDelegation, + publicKey: publisher.pubkey, + qualityRanking: publisherRanking?.rank ?? 0, + selfStake: publisher.selfDelegation, + stakeAccount: publisher.stakeAccount, + }; + }); +}; + +const getPublisherRankings = async () => { + const response = await fetch("/api/publishers-ranking"); + const responseAsJson: unknown = await response.json(); + return publishersRankingSchema.parseAsync(responseAsJson); +}; + +const getPublisherCap = (publisherCaps: PublisherCaps, publisher: PublicKey) => + BigInt( + publisherCaps.parsed?.[0]?.publisher_stake_caps.find( + ({ publisher: p }) => p === publisher.toBase58(), + )?.cap ?? 0, + ); + export const loadAccountHistory = async ( _client: PythStakingClient, _stakeAccount: PublicKey, @@ -283,6 +337,14 @@ export const loadAccountHistory = async ( return mkMockHistory(); }; +export const createStakeAccountAndDeposit = async ( + _client: PythStakingClient, + _amount: bigint, +): Promise => { + await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY)); + throw new NotImplementedError(); +}; + export const deposit = async ( client: PythStakingClient, stakeAccount: PublicKey, @@ -431,3 +493,10 @@ const mkMockHistory = (): AccountHistory => [ locked: 0n, }, ]; + +class NotImplementedError extends Error { + constructor() { + super("Not yet implemented!"); + this.name = "NotImplementedError"; + } +} diff --git a/apps/staking/src/components/AccountHistory/index.tsx b/apps/staking/src/components/AccountHistory/index.tsx index 817e2759a..7516b8d12 100644 --- a/apps/staking/src/components/AccountHistory/index.tsx +++ b/apps/staking/src/components/AccountHistory/index.tsx @@ -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: diff --git a/apps/staking/src/components/AccountSummary/index.tsx b/apps/staking/src/components/AccountSummary/index.tsx index 8a3a4bfc4..cdb0a17ba 100644 --- a/apps/staking/src/components/AccountSummary/index.tsx +++ b/apps/staking/src/components/AccountSummary/index.tsx @@ -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: { @@ -38,6 +39,7 @@ type Props = { }; export const AccountSummary = ({ + api, locked, unlockSchedule, lastSlash, @@ -114,7 +116,7 @@ export const AccountSummary = ({ actionDescription="Add funds to your balance" actionName="Add Tokens" max={walletAmount} - transfer={deposit} + transfer={api.deposit} /> @@ -130,8 +132,9 @@ 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, + })} /> } /> @@ -139,7 +142,15 @@ export const AccountSummary = ({ name="Available Rewards" amount={availableRewards} description="Rewards you have earned from OIS" - action={} + action={ + api.type === ApiStateType.Loaded ? ( + + ) : ( + + ) + } {...(expiringRewards !== undefined && expiringRewards.amount > 0n && { warning: ( @@ -187,13 +198,15 @@ const BalanceCategory = ({ ); -const ClaimButton = ( - props: Omit< - ComponentProps, - "onClick" | "disabled" | "loading" - >, -) => { - const { state, execute } = useTransfer(claim); +type ClaimButtonProps = Omit< + ComponentProps, + "onClick" | "disabled" | "loading" +> & { + api: States[ApiStateType.Loaded]; +}; + +const ClaimButton = ({ api, ...props }: ClaimButtonProps) => { + const { state, execute } = useAsync(api.claim); const doClaim = useCallback(() => { execute().catch(() => { @@ -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 diff --git a/apps/staking/src/components/Button/index.tsx b/apps/staking/src/components/Button/index.tsx index c531fbbf9..bfe29c813 100644 --- a/apps/staking/src/components/Button/index.tsx +++ b/apps/staking/src/components/Button/index.tsx @@ -11,9 +11,10 @@ type VariantProps = { size?: "small" | "nopad" | undefined; }; -type ButtonProps = ComponentProps & +type ButtonProps = Omit, "isDisabled"> & VariantProps & { isLoading?: boolean | undefined; + isDisabled?: boolean | undefined; }; export const Button = ({ diff --git a/apps/staking/src/components/Dashboard/index.tsx b/apps/staking/src/components/Dashboard/index.tsx index 39c522094..677c5fb18 100644 --- a/apps/staking/src/components/Dashboard/index.tsx +++ b/apps/staking/src/components/Dashboard/index.tsx @@ -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: | { @@ -22,7 +24,6 @@ type Props = { expiry: Date; } | undefined; - locked: bigint; unlockSchedule: { amount: bigint; date: Date; @@ -40,6 +41,7 @@ type Props = { }; export const Dashboard = ({ + api, total, lastSlash, walletAmount, @@ -47,7 +49,6 @@ export const Dashboard = ({ expiringRewards, governance, integrityStakingPublishers, - locked, unlockSchedule, yieldRate, }: Props) => { @@ -84,6 +85,11 @@ export const Dashboard = ({ "cooldown2", ); + const locked = useMemo( + () => unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n), + [unlockSchedule], + ); + const availableToStakeIntegrity = useMemo( () => total - @@ -110,6 +116,7 @@ export const Dashboard = ({ return (
); diff --git a/apps/staking/src/components/Home/index.tsx b/apps/staking/src/components/Home/index.tsx index 33ef3fa80..018a743a8 100644 --- a/apps/staking/src/components/Home/index.tsx +++ b/apps/staking/src/components/Home/index.tsx @@ -5,13 +5,14 @@ import { useCallback } from "react"; import { useIsSSR } from "react-aria"; import { - StateType as DashboardDataStateType, - useDashboardData, -} from "../../hooks/use-dashboard-data"; + type States, + StateType as ApiStateType, + useApi, +} from "../../hooks/use-api"; import { - StateType as StakeAccountStateType, - useStakeAccount, -} from "../../hooks/use-stake-account"; + StateType as DashboardDataStateType, + useData, +} from "../../hooks/use-data"; import { Button } from "../Button"; import { Dashboard } from "../Dashboard"; import { Error as ErrorPage } from "../Error"; @@ -24,33 +25,22 @@ export const Home = () => { }; const MountedHome = () => { - const stakeAccountState = useStakeAccount(); + const api = useApi(); - switch (stakeAccountState.type) { - case StakeAccountStateType.Initialized: - case StakeAccountStateType.Loading: { + switch (api.type) { + case ApiStateType.NotLoaded: + case ApiStateType.LoadingStakeAccounts: { return ; } - case StakeAccountStateType.NoAccounts: { - return ( -
-

No stake account found for your wallet!

-
- ); - } - case StakeAccountStateType.NoWallet: { + case ApiStateType.NoWallet: { return ; } - case StakeAccountStateType.Error: { - return ( - - ); + case ApiStateType.ErrorLoadingStakeAccounts: { + return ; } - case StakeAccountStateType.Loaded: { - return ; + case ApiStateType.LoadedNoStakeAccount: + case ApiStateType.Loaded: { + return ; } } }; @@ -78,8 +68,12 @@ const NoWalletHome = () => { ); }; -const StakeAccountLoadedHome = () => { - const data = useDashboardData(); +type StakeAccountLoadedHomeProps = { + api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; +}; + +const StakeAccountLoadedHome = ({ api }: StakeAccountLoadedHomeProps) => { + const data = useData(api.dashboardDataCacheKey, api.loadData); switch (data.type) { case DashboardDataStateType.NotLoaded: @@ -94,7 +88,7 @@ const StakeAccountLoadedHome = () => { case DashboardDataStateType.Loaded: { return (
- +
); } diff --git a/apps/staking/src/components/OracleIntegrityStaking/index.tsx b/apps/staking/src/components/OracleIntegrityStaking/index.tsx index cab6815ba..907f8bd03 100644 --- a/apps/staking/src/components/OracleIntegrityStaking/index.tsx +++ b/apps/staking/src/components/OracleIntegrityStaking/index.tsx @@ -3,7 +3,7 @@ import { XMarkIcon, MagnifyingGlassIcon, } from "@heroicons/react/24/outline"; -import { calculateApy, type PythStakingClient } from "@pythnetwork/staking-sdk"; +import { calculateApy } from "@pythnetwork/staking-sdk"; import { PublicKey } from "@solana/web3.js"; import clsx from "clsx"; import { @@ -23,11 +23,7 @@ import { Label, } from "react-aria-components"; -import { - delegateIntegrityStaking, - cancelWarmupIntegrityStaking, - unstakeIntegrityStaking, -} from "../../api"; +import { type States, StateType as ApiStateType } from "../../hooks/use-api"; import { Button } from "../Button"; import { ProgramSection } from "../ProgramSection"; import { SparkChart } from "../SparkChart"; @@ -39,6 +35,7 @@ import { AmountType, TransferButton } from "../TransferButton"; const PAGE_SIZE = 10; type Props = { + api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; availableToStake: bigint; locked: bigint; warmup: bigint; @@ -50,6 +47,7 @@ type Props = { }; export const OracleIntegrityStaking = ({ + api, availableToStake, locked, warmup, @@ -112,6 +110,7 @@ export const OracleIntegrityStaking = ({ Quality ranking - {availableToStake > 0n && ( - - )} + {paginatedPublishers.map((publisher) => ( {publisher.numFeeds} + + {publisher.qualityRanking} + - {publisher.qualityRanking} + - {availableToStake > 0 && ( - - - - )} {(warmup !== undefined || staked !== undefined) && ( @@ -659,6 +658,7 @@ const Publisher = ({ const PublisherTableCell = Styled("td", "py-4 px-5 whitespace-nowrap"); type StakeToPublisherButtonProps = { + api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; publisherName: string | undefined; publisherKey: PublicKey; availableToStake: bigint; @@ -670,6 +670,7 @@ type StakeToPublisherButtonProps = { }; const StakeToPublisherButton = ({ + api, publisherName, publisherKey, poolCapacity, @@ -680,7 +681,7 @@ const StakeToPublisherButton = ({ yieldRate, }: StakeToPublisherButtonProps) => { const delegate = useTransferActionForPublisher( - delegateIntegrityStaking, + api.type === ApiStateType.Loaded ? api.delegateIntegrityStaking : undefined, publisherKey, ); @@ -726,17 +727,14 @@ const StakeToPublisherButton = ({ }; const useTransferActionForPublisher = ( - action: ( - client: PythStakingClient, - stakingAccount: PublicKey, - publisher: PublicKey, - amount: bigint, - ) => Promise, + action: ((publisher: PublicKey, amount: bigint) => Promise) | undefined, publisher: PublicKey, ) => - useCallback( - (client: PythStakingClient, stakingAccount: PublicKey, amount: bigint) => - action(client, stakingAccount, publisher, amount), + useMemo( + () => + action === undefined + ? undefined + : (amount: bigint) => action(publisher, amount), [action, publisher], ); diff --git a/apps/staking/src/components/ProgramSection/index.tsx b/apps/staking/src/components/ProgramSection/index.tsx index 724863882..7eed64f37 100644 --- a/apps/staking/src/components/ProgramSection/index.tsx +++ b/apps/staking/src/components/ProgramSection/index.tsx @@ -14,15 +14,11 @@ type Props = HTMLAttributes & { staked: bigint; cooldown: bigint; cooldown2: bigint; + available: bigint; availableToStakeDetails?: ReactNode | ReactNode[] | undefined; } & ( + | { stake?: never; stakeDescription?: never } | { - stake?: never; - stakeDescription?: never; - available?: bigint | undefined; - } - | { - available: bigint; stake: ComponentProps["transfer"] | undefined; stakeDescription: string; } @@ -73,32 +69,27 @@ export const ProgramSection = ({

{name}

{description}

- {available !== undefined && ( - <> - 0n && { - actions: ( - - - - ), - })} - > - {available} - - - - )} + + + + ), + })} + > + {available} + + ), - ...(cancelWarmup !== undefined && { - actions: ( - - ), - }), + })} + {...(cancelWarmupDescription !== undefined && { + actions: ( + + ), })} > {warmup} @@ -130,21 +121,20 @@ export const ProgramSection = ({ 0n && { - actions: ( - - - - ), - })} + {...(unstakeDescription !== undefined && { + actions: ( + + + + ), + })} > {staked} diff --git a/apps/staking/src/components/Root/index.tsx b/apps/staking/src/components/Root/index.tsx index a9badcc16..6f16a5174 100644 --- a/apps/staking/src/components/Root/index.tsx +++ b/apps/staking/src/components/Root/index.tsx @@ -12,8 +12,8 @@ import { RPC, IS_MAINNET, } from "../../config/server"; +import { ApiProvider } from "../../hooks/use-api"; import { LoggerProvider } from "../../hooks/use-logger"; -import { StakeAccountProvider } from "../../hooks/use-stake-account"; import { Amplitude } from "../Amplitude"; import { Footer } from "../Footer"; import { Header } from "../Header"; @@ -48,7 +48,7 @@ export const Root = ({ children }: Props) => ( : WalletAdapterNetwork.Devnet } > - + ( {AMPLITUDE_API_KEY && } {!IS_PRODUCTION_SERVER && } - + diff --git a/apps/staking/src/components/TransferButton/index.tsx b/apps/staking/src/components/TransferButton/index.tsx index 67ba4e005..0f2ca15e3 100644 --- a/apps/staking/src/components/TransferButton/index.tsx +++ b/apps/staking/src/components/TransferButton/index.tsx @@ -1,5 +1,3 @@ -import type { PythStakingClient } from "@pythnetwork/staking-sdk"; -import type { PublicKey } from "@solana/web3.js"; import { type ComponentProps, type ReactNode, @@ -17,7 +15,7 @@ import { Group, } from "react-aria-components"; -import { StateType, useTransfer } from "../../hooks/use-transfer"; +import { StateType, useAsync } from "../../hooks/use-async"; import { stringToTokens, tokensToString } from "../../tokens"; import { Button } from "../Button"; import { ModalDialog } from "../ModalDialog"; @@ -35,11 +33,7 @@ type Props = Omit, "children"> & { | ReactNode | ReactNode[] | undefined; - transfer: ( - client: PythStakingClient, - stakingAccount: PublicKey, - amount: bigint, - ) => Promise; + transfer?: ((amount: bigint) => Promise) | undefined; }; export const TransferButton = ({ @@ -50,11 +44,16 @@ export const TransferButton = ({ max, transfer, children, + isDisabled, ...props }: Props) => { const [closeDisabled, setCloseDisabled] = useState(false); - return ( + return transfer === undefined || isDisabled === true || max === 0n ? ( + + ) : ( Promise; setCloseDisabled: (value: boolean) => void; submitButtonText: string; close: () => void; @@ -118,14 +117,14 @@ const DialogContents = ({ }, [amount]); const doTransfer = useCallback( - (client: PythStakingClient, stakingAccount: PublicKey) => + () => amount.type === AmountType.Valid - ? transfer(client, stakingAccount, amount.amount) + ? transfer(amount.amount) : Promise.reject(new InvalidAmountError()), [amount, transfer], ); - const { execute, state } = useTransfer(doTransfer); + const { execute, state } = useAsync(doTransfer); const handleSubmit = useCallback( (e: FormEvent) => { @@ -176,7 +175,7 @@ const DialogContents = ({ variant="secondary" className="pointer-events-auto" onPress={setMax} - isDisabled={state.type === StateType.Submitting} + isDisabled={state.type === StateType.Running} > max @@ -194,7 +193,7 @@ const DialogContents = ({