Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sync chains and token with url #398

Merged
merged 9 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/consts/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum WARP_QUERY_PARAMS {
Xaroz marked this conversation as resolved.
Show resolved Hide resolved
ORIGIN = 'origin',
DESTINATION = 'destination',
TOKEN = 'token',
}
5 changes: 2 additions & 3 deletions src/features/chains/ChainSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useChainDisplayName } from './hooks';
type Props = {
name: string;
label: string;
onChange?: (id: ChainName) => void;
onChange?: (id: ChainName, fieldName: string) => void;
disabled?: boolean;
customListItemField: ChainSearchMenuProps['customListItemField'];
};
Expand All @@ -25,8 +25,7 @@ export function ChainSelectField({ name, label, onChange, disabled, customListIt
// Reset other fields on chain change
setFieldValue('recipient', '');
setFieldValue('amount', '');
setFieldValue('tokenIndex', undefined);
if (onChange) onChange(chainName);
if (onChange) onChange(chainName, name);
};

const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down
10 changes: 10 additions & 0 deletions src/features/chains/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,13 @@ export function getNumRoutesWithSelectedChain(
data,
};
}

/**
* Check if given chainName has valid chain metadata and return chainName if chain is valid
*/
export function getValidChain(
Xaroz marked this conversation as resolved.
Show resolved Hide resolved
chainName: string | null,
multiProvider: MultiProtocolProvider,
): string | undefined {
return chainName && multiProvider.tryGetChainMetadata(chainName) ? chainName : undefined;
}
24 changes: 5 additions & 19 deletions src/features/tokens/TokenSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { useField, useFormikContext } from 'formik';
import { useEffect, useState } from 'react';
import { TokenIcon } from '../../components/icons/TokenIcon';

import { TransferFormValues } from '../transfer/types';
import { TokenListModal } from './TokenListModal';
import { getIndexForToken, getTokenByIndex, useWarpCore } from './hooks';
Expand All @@ -11,9 +12,10 @@ type Props = {
name: string;
disabled?: boolean;
setIsNft: (value: boolean) => void;
onChangeToken: (addressOrDenom: string) => void;
};

export function TokenSelectField({ name, disabled, setIsNft }: Props) {
export function TokenSelectField({ name, disabled, setIsNft, onChangeToken }: Props) {
const { values } = useFormikContext<TransferFormValues>();
const [field, , helpers] = useField<number | undefined>(name);
const [isModalOpen, setIsModalOpen] = useState(false);
Expand All @@ -24,29 +26,13 @@ export function TokenSelectField({ name, disabled, setIsNft }: Props) {
const { origin, destination } = values;
useEffect(() => {
const tokensWithRoute = warpCore.getTokensForRoute(origin, destination);
let newFieldValue: number | undefined;
let newIsAutomatic: boolean;
// No tokens available for this route
if (tokensWithRoute.length === 0) {
newFieldValue = undefined;
newIsAutomatic = true;
}
// Exactly one found
else if (tokensWithRoute.length === 1) {
newFieldValue = getIndexForToken(warpCore, tokensWithRoute[0]);
newIsAutomatic = true;
// Multiple possibilities
} else {
newFieldValue = undefined;
newIsAutomatic = false;
}
helpers.setValue(newFieldValue);
setIsAutomaticSelection(newIsAutomatic);
setIsAutomaticSelection(tokensWithRoute.length <= 1);
}, [warpCore, origin, destination, helpers]);

const onSelectToken = (newToken: IToken) => {
// Set the token address value in formik state
helpers.setValue(getIndexForToken(warpCore, newToken));
onChangeToken(newToken.addressOrDenom);
// Update nft state in parent
setIsNft(newToken.isNft());
};
Expand Down
44 changes: 44 additions & 0 deletions src/features/tokens/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,47 @@ export function tryFindToken(
return null;
}
}

function getTokenIndexFromChains(
warpCore: WarpCore,
addressOrDenom: string | null,
origin: string,
destination: string,
) {
// find routes
const tokensWithRoute = warpCore.getTokensForRoute(origin, destination);
// find provided token addressOrDenom
const queryToken = tokensWithRoute.find((token) => token.addressOrDenom === addressOrDenom);

// if found return index
if (queryToken) return getIndexForToken(warpCore, queryToken);
// if tokens route has only one route return that index
else if (tokensWithRoute.length === 1) return getIndexForToken(warpCore, tokensWithRoute[0]);
// if 0 or more than 1 then return undefined
return undefined;
}

export function getInitialTokenIndex(
warpCore: WarpCore,
addressOrDenom: string | null,
originQuery?: string,
destinationQuery?: string,
): number | undefined {
const firstToken = warpCore.tokens[0];
const connectedToken = firstToken.connections?.[0];

// origin query and destination query is defined
if (originQuery && destinationQuery)
return getTokenIndexFromChains(warpCore, addressOrDenom, originQuery, destinationQuery);

// if none of those are defined, use default values and pass token query
if (connectedToken)
return getTokenIndexFromChains(
warpCore,
addressOrDenom,
firstToken.chainName,
connectedToken.token.chainName,
);

return undefined;
}
92 changes: 81 additions & 11 deletions src/features/transfer/TransferTokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,27 @@ import { ConnectAwareSubmitButton } from '../../components/buttons/ConnectAwareS
import { SolidButton } from '../../components/buttons/SolidButton';
import { TextField } from '../../components/input/TextField';
import { config } from '../../consts/config';
import { WARP_QUERY_PARAMS } from '../../consts/core';
import { Color } from '../../styles/Color';
import { logger } from '../../utils/logger';
import { getQueryParams, updateQueryParam } from '../../utils/queryParams';
import { ChainConnectionWarning } from '../chains/ChainConnectionWarning';
import { ChainSelectField } from '../chains/ChainSelectField';
import { ChainWalletWarning } from '../chains/ChainWalletWarning';
import { useChainDisplayName, useMultiProvider } from '../chains/hooks';
import { getNumRoutesWithSelectedChain } from '../chains/utils';
import { getNumRoutesWithSelectedChain, getValidChain } from '../chains/utils';
import { useIsAccountSanctioned } from '../sanctions/hooks/useIsAccountSanctioned';
import { useStore } from '../store';
import { SelectOrInputTokenIds } from '../tokens/SelectOrInputTokenIds';
import { TokenSelectField } from '../tokens/TokenSelectField';
import { useIsApproveRequired } from '../tokens/approval';
import { useDestinationBalance, useOriginBalance } from '../tokens/balances';
import { getIndexForToken, getTokenByIndex, useWarpCore } from '../tokens/hooks';
import {
getIndexForToken,
getInitialTokenIndex,
getTokenByIndex,
useWarpCore,
} from '../tokens/hooks';
import { useFetchMaxAmount } from './maxAmount';
import { TransferFormValues } from './types';
import { useRecipientBalanceWatcher } from './useBalanceWatcher';
Expand Down Expand Up @@ -86,7 +93,13 @@ export function TransferTokenForm() {
);
}

function SwapChainsButton({ disabled }: { disabled?: boolean }) {
function SwapChainsButton({
disabled,
onSwapChain,
}: {
disabled?: boolean;
onSwapChain: (origin: string, destination: string) => void;
}) {
const { values, setFieldValue } = useFormikContext<TransferFormValues>();
const { origin, destination } = values;

Expand All @@ -95,8 +108,8 @@ function SwapChainsButton({ disabled }: { disabled?: boolean }) {
setFieldValue('origin', destination);
setFieldValue('destination', origin);
// Reset other fields on chain change
setFieldValue('tokenIndex', undefined);
setFieldValue('recipient', '');
onSwapChain(destination, origin);
};

return (
Expand All @@ -116,7 +129,7 @@ function SwapChainsButton({ disabled }: { disabled?: boolean }) {
function ChainSelectSection({ isReview }: { isReview: boolean }) {
const warpCore = useWarpCore();

const { values } = useFormikContext<TransferFormValues>();
const { values, setFieldValue } = useFormikContext<TransferFormValues>();

const originRouteCounts = useMemo(() => {
return getNumRoutesWithSelectedChain(warpCore, values.origin, true);
Expand All @@ -126,22 +139,54 @@ function ChainSelectSection({ isReview }: { isReview: boolean }) {
return getNumRoutesWithSelectedChain(warpCore, values.destination, false);
}, [values.destination, warpCore]);

const setTokenOnChainChange = (origin: string, destination: string) => {
const tokensWithRoute = warpCore.getTokensForRoute(origin, destination);
let newFieldValue: number | undefined;
if (tokensWithRoute.length === 1) {
const token = tokensWithRoute[0];
newFieldValue = getIndexForToken(warpCore, token);
updateQueryParam(WARP_QUERY_PARAMS.TOKEN, token.addressOrDenom);
// Not found or Multiple possibilities
} else {
newFieldValue = undefined;
updateQueryParam(WARP_QUERY_PARAMS.TOKEN, undefined);
}

setFieldValue('tokenIndex', newFieldValue);
};

const handleChange = (chainName: string, fieldName: string) => {
if (fieldName === WARP_QUERY_PARAMS.ORIGIN)
setTokenOnChainChange(chainName, values.destination);
else if (fieldName === WARP_QUERY_PARAMS.DESTINATION)
setTokenOnChainChange(values.origin, chainName);
updateQueryParam(fieldName, chainName);
Xaroz marked this conversation as resolved.
Show resolved Hide resolved
};

const onSwapChain = (origin: string, destination: string) => {
updateQueryParam(WARP_QUERY_PARAMS.ORIGIN, origin);
updateQueryParam(WARP_QUERY_PARAMS.DESTINATION, destination);
Xaroz marked this conversation as resolved.
Show resolved Hide resolved
setTokenOnChainChange(origin, destination);
};

return (
<div className="mt-2 flex items-center justify-between gap-4">
<ChainSelectField
name="origin"
label="From"
disabled={isReview}
customListItemField={destinationRouteCounts}
onChange={handleChange}
/>
<div className="flex flex-1 flex-col items-center">
<SwapChainsButton disabled={isReview} />
<SwapChainsButton disabled={isReview} onSwapChain={onSwapChain} />
</div>
<ChainSelectField
name="destination"
label="To"
disabled={isReview}
customListItemField={originRouteCounts}
onChange={handleChange}
/>
</div>
);
Expand All @@ -154,12 +199,21 @@ function TokenSection({
setIsNft: (b: boolean) => void;
isReview: boolean;
}) {
const onChangeToken = (addressOrDenom: string) => {
updateQueryParam(WARP_QUERY_PARAMS.TOKEN, addressOrDenom);
};

return (
<div className="flex-1">
<label htmlFor="tokenIndex" className="block pl-0.5 text-sm text-gray-600">
Token
</label>
<TokenSelectField name="tokenIndex" disabled={isReview} setIsNft={setIsNft} />
<TokenSelectField
name="tokenIndex"
disabled={isReview}
setIsNft={setIsNft}
onChangeToken={onChangeToken}
/>
</div>
);
}
Expand Down Expand Up @@ -449,17 +503,33 @@ function WarningBanners() {

function useFormInitialValues(): TransferFormValues {
const warpCore = useWarpCore();
const params = getQueryParams();

const originQuery = getValidChain(params.get(WARP_QUERY_PARAMS.ORIGIN), warpCore.multiProvider);
const destinationQuery = getValidChain(
params.get(WARP_QUERY_PARAMS.DESTINATION),
warpCore.multiProvider,
);

const tokenIndex = getInitialTokenIndex(
warpCore,
params.get(WARP_QUERY_PARAMS.TOKEN),
originQuery,
destinationQuery,
);

return useMemo(() => {
const firstToken = warpCore.tokens[0];
const connectedToken = firstToken.connections?.[0];

return {
origin: firstToken.chainName,
destination: connectedToken?.token?.chainName || '',
tokenIndex: getIndexForToken(warpCore, firstToken),
origin: originQuery ? originQuery : firstToken.chainName,
destination: destinationQuery ? destinationQuery : connectedToken?.token?.chainName || '',
tokenIndex: tokenIndex,
amount: '',
recipient: '',
};
}, [warpCore]);
}, [warpCore, destinationQuery, originQuery, tokenIndex]);
}

const insufficientFundsErrMsg = /insufficient.[funds|lamports]/i;
Expand Down
19 changes: 19 additions & 0 deletions src/utils/queryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function getQueryParams() {
return new URLSearchParams(window.location.search);
}

export function updateQueryParam(key: string, value?: string | number) {
const params = getQueryParams(); // Get current query parameters

if (value === undefined || value === null) {
// Remove the parameter if the value is undefined or null
params.delete(key);
} else {
// Add or update the parameter
params.set(key, value.toString());
}

// Update the browser's URL without reloading the page
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, '', newUrl);
}
Loading