Skip to content

Commit

Permalink
Implement warp form submit handler
Browse files Browse the repository at this point in the history
Create deployment review page skeleton
  • Loading branch information
jmrossy committed Dec 11, 2024
1 parent 9d57020 commit 5adc73e
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 37 deletions.
25 changes: 25 additions & 0 deletions src/features/deployment/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { WarpRouteDeployConfig } from '@hyperlane-xyz/sdk';

export enum DeploymentStatus {
Preparing = 'preparing',
CreatingTxs = 'creating-txs',
Expand Down Expand Up @@ -26,3 +28,26 @@ export interface DeploymentContext {
destination: ChainName;
timestamp: number;
}

export enum DeploymentType {
Warp = 'warp',
Core = 'core',
// Add more here as needed
}

interface ConfigBase {
type: DeploymentType;
config: unknown;
}

export interface WarpDeploymentConfig extends ConfigBase {
type: DeploymentType.Warp;
config: WarpRouteDeployConfig;
}

export interface CoreDeploymentConfig extends ConfigBase {
type: DeploymentType.Core;
config: any; // TODO
}

export type DeploymentConfig = WarpDeploymentConfig | CoreDeploymentConfig;
33 changes: 25 additions & 8 deletions src/features/deployment/warp/WarpDeploymentForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { arbitrum, ethereum } from '@hyperlane-xyz/registry';
import { TokenType } from '@hyperlane-xyz/sdk';
import { TokenType, WarpRouteDeployConfig } from '@hyperlane-xyz/sdk';
import { isNumeric } from '@hyperlane-xyz/utils';
import { Button, ErrorIcon, IconButton, useAccounts, XIcon } from '@hyperlane-xyz/widgets';
import clsx from 'clsx';
Expand All @@ -13,6 +13,7 @@ import { TextInput } from '../../../components/input/TextField';
import { H1 } from '../../../components/text/H1';
import { config } from '../../../consts/config';
import { CardPage } from '../../../flows/CardPage';
import { useCardNav } from '../../../flows/hooks';
import { Stepper } from '../../../flows/Stepper';
import PlusCircleIcon from '../../../images/icons/plus-circle.svg';
import { Color } from '../../../styles/Color';
Expand All @@ -21,8 +22,10 @@ import { ChainConnectionWarning } from '../../chains/ChainConnectionWarning';
import { ChainSelectField } from '../../chains/ChainSelectField';
import { ChainWalletWarning } from '../../chains/ChainWalletWarning';
import { useMultiProvider } from '../../chains/hooks';
import { useStore } from '../../store';
import { DeploymentType } from '../types';
import { TokenTypeSelectField } from './TokenTypeSelectField';
import { WarpDeploymentConfigEntry, WarpDeploymentFormValues } from './types';
import { WarpDeploymentConfigItem, WarpDeploymentFormValues } from './types';
import { isCollateralTokenType } from './utils';
import { validateWarpDeploymentForm } from './validation';

Expand All @@ -44,11 +47,25 @@ export function WarpDeploymentForm() {
const multiProvider = useMultiProvider();
const { accounts } = useAccounts(multiProvider, config.addressBlacklist);

const validate = (values: WarpDeploymentFormValues) =>
validateWarpDeploymentForm(values, accounts, multiProvider);
// Gets set in the validate method
let warpDeployConfig: WarpRouteDeployConfig | undefined = undefined;
const setWarpDeployConfig = (c: WarpRouteDeployConfig) => {
warpDeployConfig = c;
};

const onSubmitForm = (values: WarpDeploymentFormValues) => {
logger.debug('Deployment form values', values);
const validate = (values: WarpDeploymentFormValues) =>
validateWarpDeploymentForm(values, accounts, multiProvider, setWarpDeployConfig);

const { setPage } = useCardNav();
const { setDeploymentConfig } = useStore((s) => ({ setDeploymentConfig: s.setDeploymentConfig }));

const onSubmitForm = () => {
if (!warpDeployConfig) {
logger.warn('Warp deploy config is undefined, should have been set during validation');
return;
}
setDeploymentConfig({ type: DeploymentType.Warp, config: warpDeployConfig });
setPage(CardPage.WarpReview);
};

return (
Expand Down Expand Up @@ -98,14 +115,14 @@ function ConfigListSection() {
);
}

function ChainTokenConfig({ config, index }: { config: WarpDeploymentConfigEntry; index: number }) {
function ChainTokenConfig({ config, index }: { config: WarpDeploymentConfigItem; index: number }) {
const { values, setValues, errors, setErrors } = useFormikContext<WarpDeploymentFormValues>();

const isRemoveDisabled = values.configs.length <= 2;
const isCollateralized = isCollateralTokenType(config.tokenType);
const hasError = !!errors[index];

const onChange = (update: Partial<WarpDeploymentConfigEntry>) => {
const onChange = (update: Partial<WarpDeploymentConfigItem>) => {
const configs = [...values.configs];
const updatedConfig = { ...configs[index], ...update };
configs[index] = updatedConfig;
Expand Down
51 changes: 51 additions & 0 deletions src/features/deployment/warp/WarpDeploymentReview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BackButton } from '../../../components/buttons/BackButton';
import { SolidButton } from '../../../components/buttons/SolidButton';
import { H1 } from '../../../components/text/H1';
import { CardPage } from '../../../flows/CardPage';
import { useCardNav } from '../../../flows/hooks';
import { Stepper } from '../../../flows/Stepper';
import { useMultiProvider } from '../../chains/hooks';
import { useStore } from '../../store';

export function WarpDeploymentReview() {
return (
<div className="flex w-full flex-col items-stretch xs:min-w-112">
<div className="space-y-5">
<HeaderSection />
<ConfigSection />
<ButtonSection />
</div>
</div>
);
}

function HeaderSection() {
return (
<div className="flex items-center justify-between gap-10">
<H1>Review Deployment</H1>
<Stepper numSteps={5} currentStep={3} />
</div>
);
}

function ConfigSection() {
const _multiProvider = useMultiProvider();
const { deploymentConfig } = useStore((s) => ({ deploymentConfig: s.deploymentConfig }));
return <div>{JSON.stringify(deploymentConfig)}</div>;
}

function ButtonSection() {
const { setPage } = useCardNav();
const onClickContinue = () => {
setPage(CardPage.WarpDeploy);
};

return (
<div className="mt-4 flex items-center justify-between">
<BackButton page={CardPage.WarpForm} />
<SolidButton onClick={onClickContinue} className="px-3 py-1.5" color="accent">
Deploy
</SolidButton>
</div>
);
}
10 changes: 5 additions & 5 deletions src/features/deployment/warp/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TokenType } from '@hyperlane-xyz/sdk';

export interface WarpDeploymentFormValues {
configs: Array<WarpDeploymentConfigEntry>;
}

export interface WarpDeploymentConfigEntry {
export interface WarpDeploymentConfigItem {
chainName: ChainName;
tokenType: TokenType;
tokenAddress?: Address;
}

export interface WarpDeploymentFormValues {
configs: Array<WarpDeploymentConfigItem>;
}
11 changes: 6 additions & 5 deletions src/features/deployment/warp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
TokenType,
} from '@hyperlane-xyz/sdk';
import { assert } from '@hyperlane-xyz/utils';
import { WarpDeploymentConfigEntry } from './types';
import { WarpDeploymentConfigItem } from './types';

// TODO remove (see below)
const collateralTokenTypes = [
Expand Down Expand Up @@ -39,9 +39,9 @@ export function isSyntheticTokenType(tokenType: TokenType) {
return !isCollateralTokenType(tokenType) && !isNativeTokenType(tokenType);
}

// TODO use meta type when SDK is updated
export function formConfigToDeployConfig(
config: WarpDeploymentConfigEntry,
export function formItemToDeployConfig(
config: WarpDeploymentConfigItem,
// TODO use meta type when SDK is updated
tokenMetadata: any,
): TokenRouterConfig {
const { tokenType, tokenAddress } = config;
Expand All @@ -52,8 +52,9 @@ export function formConfigToDeployConfig(
};
}

// Consider caching results here for faster form validation
export function getTokenMetadata(
config: WarpDeploymentConfigEntry,
config: WarpDeploymentConfigItem,
multiProvider: MultiProtocolProvider,
// TODO add type when SDK is updated
) {
Expand Down
26 changes: 17 additions & 9 deletions src/features/deployment/warp/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChainMap,
MultiProtocolProvider,
TokenRouterConfig,
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
} from '@hyperlane-xyz/sdk';
import {
Expand All @@ -18,9 +19,8 @@ import {
import { AccountInfo, getAccountAddressForChain } from '@hyperlane-xyz/widgets';
import { logger } from '../../../utils/logger';
import { zodErrorToString } from '../../../utils/zod';
import { WarpDeploymentConfigEntry, WarpDeploymentFormValues } from './types';
import { WarpDeploymentConfigItem, WarpDeploymentFormValues } from './types';
import {
formConfigToDeployConfig,
getTokenMetadata,
isCollateralTokenType,
isNativeTokenType,
Expand All @@ -34,6 +34,7 @@ export async function validateWarpDeploymentForm(
{ configs }: WarpDeploymentFormValues,
accounts: Record<ProtocolType, AccountInfo>,
multiProvider: MultiProtocolProvider,
onSuccess: (c: WarpRouteDeployConfig) => void,
): Promise<Record<string | number, string>> {
try {
const chainNames = configs.map((c) => c.chainName);
Expand All @@ -45,24 +46,24 @@ export async function validateWarpDeploymentForm(
return { form: 'At least two chains are required' };
}

const configValidationResults = await Promise.all(
const configItemsResults = await Promise.all(
configs.map((c) => validateChainTokenConfig(c, multiProvider)),
);
const errors = configValidationResults.reduce<Record<number, string>>((acc, r, i) => {
const errors = configItemsResults.reduce<Record<number, string>>((acc, r, i) => {
if (!r.success) return { ...acc, [i]: r.error };
return acc;
}, {});
if (objLength(errors) > 0) return errors;

// If we reach here, all configs are valid
const deployConfigs = configValidationResults.filter((r) => !!r.success).map((r) => r.data);
const configItems = configItemsResults.filter((r) => !!r.success).map((r) => r.data);

let warpRouteDeployConfig: ChainMap<TokenRouterConfig> = configs.reduce(
(acc, currentConfig, index) => {
const chainName = currentConfig.chainName;
const owner = getAccountAddressForChain(multiProvider, chainName, accounts);
acc[chainName] = {
...deployConfigs[index],
...configItems[index],
mailbox: chainAddresses[chainName].mailbox,
owner,
};
Expand All @@ -72,7 +73,7 @@ export async function validateWarpDeploymentForm(
);

// Second pass to add token metadata to synthetic tokens
const firstNonSythetic = deployConfigs.find((c) => !isSyntheticTokenType(c.type));
const firstNonSythetic = configItems.find((c) => !isSyntheticTokenType(c.type));
if (!firstNonSythetic) return { form: 'Token types cannot all be synthetic' };

warpRouteDeployConfig = objMap(warpRouteDeployConfig, (_, config) => {
Expand All @@ -94,6 +95,7 @@ export async function validateWarpDeploymentForm(

// TODO check account balances for each chain

onSuccess(combinedConfigValidationResult.data);
return {};
} catch (error: any) {
logger.error('Error validating form', error);
Expand All @@ -107,7 +109,7 @@ export async function validateWarpDeploymentForm(
}

async function validateChainTokenConfig(
config: WarpDeploymentConfigEntry,
config: WarpDeploymentConfigItem,
multiProvider: MultiProtocolProvider,
): Promise<Result<TokenRouterConfig>> {
const { chainName, tokenType, tokenAddress } = config;
Expand All @@ -131,5 +133,11 @@ async function validateChainTokenConfig(
return failure('Address is not a valid token contract');
}

return success(formConfigToDeployConfig(config, tokenMetadata));
const deployConfig: TokenRouterConfig = {
type: tokenType,
token: tokenAddress,
...tokenMetadata,
};

return success(deployConfig);
}
29 changes: 23 additions & 6 deletions src/features/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { config } from '../consts/config';
import { CardPage } from '../flows/CardPage';
import { logger } from '../utils/logger';
import { assembleChainMetadata } from './chains/metadata';
import { DeploymentContext, DeploymentStatus, FinalDeploymentStatuses } from './deployment/types';
import {
DeploymentConfig,
DeploymentContext,
DeploymentStatus,
FinalDeploymentStatuses,
} from './deployment/types';

// Increment this when persist state has breaking changes
const PERSIST_STATE_VERSION = 0;
Expand Down Expand Up @@ -39,10 +44,15 @@ export interface AppState {
cardPage: CardPage;
direction: 'forward' | 'backward';
setCardPage: (page: CardPage) => void;
deploymentLoading: boolean;
setDeploymentLoading: (isLoading: boolean) => void;

deploymentConfig: DeploymentConfig | undefined;
setDeploymentConfig: (config: DeploymentConfig | undefined) => void;
isDeploymentLoading: boolean;
setIsDeploymentLoading: (isLoading: boolean) => void;

isSideBarOpen: boolean;
setIsSideBarOpen: (isOpen: boolean) => void;

showEnvSelectModal: boolean;
setShowEnvSelectModal: (show: boolean) => void;
}
Expand Down Expand Up @@ -107,14 +117,21 @@ export const useStore = create<AppState>()(
setCardPage: (page) => {
set((s) => ({ cardPage: page, direction: page >= s.cardPage ? 'forward' : 'backward' }));
},
deploymentLoading: false,
setDeploymentLoading: (isLoading) => {
set(() => ({ deploymentLoading: isLoading }));

deploymentConfig: undefined,
setDeploymentConfig: (config: DeploymentConfig | undefined) => {
set(() => ({ deploymentConfig: config }));
},
isDeploymentLoading: false,
setIsDeploymentLoading: (isLoading) => {
set(() => ({ isDeploymentLoading: isLoading }));
},

isSideBarOpen: false,
setIsSideBarOpen: (isSideBarOpen) => {
set(() => ({ isSideBarOpen }));
},

showEnvSelectModal: false,
setShowEnvSelectModal: (showEnvSelectModal) => {
set(() => ({ showEnvSelectModal }));
Expand Down
8 changes: 4 additions & 4 deletions src/features/wallet/SideBarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@ export function SideBarMenu({

const multiProvider = useMultiProvider();

const { deployments, resetDeployments, deploymentLoading } = useStore((s) => ({
const { deployments, resetDeployments, isDeploymentLoading } = useStore((s) => ({
deployments: s.deployments,
resetDeployments: s.resetDeployments,
deploymentLoading: s.deploymentLoading,
isDeploymentLoading: s.isDeploymentLoading,
}));

useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
} else if (deploymentLoading) {
} else if (isDeploymentLoading) {
setSelectedDeployment(deployments[deployments.length - 1]);
setIsModalOpen(true);
}
}, [deployments, deploymentLoading]);
}, [deployments, isDeploymentLoading]);

useEffect(() => {
setIsMenuOpen(isOpen);
Expand Down
Loading

0 comments on commit 5adc73e

Please sign in to comment.