Skip to content

Commit

Permalink
[FRE-960] feat(widget-v2): swap page route logic (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
codingki authored Aug 30, 2024
1 parent b120caf commit a81fb70
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 30 deletions.
12 changes: 11 additions & 1 deletion packages/widget-v2/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ export default tseslint.config(
"react-hooks": fixupPluginRules(reactHooks),
},
rules: {
...reactHooks.configs.recommended.rules,
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/no-unused-vars": ["error", {
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}],
"no-console": ["error", { allow: ["warn", "error"] }],
"quotes": ["error", "double", { "avoidEscape": true }],
...reactHooks.configs.recommended.rules,

},
}
);
1 change: 1 addition & 0 deletions packages/widget-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@tanstack/query-core": "^5.51.21",
"chain-registry": "^1.63.43",
"jotai": "^2.9.1",
"jotai-effect": "^1.0.2",
"jotai-tanstack-query": "^0.8.6",
"lodash.debounce": "^4.0.8",
"match-sorter": "^6.3.4",
Expand Down
85 changes: 82 additions & 3 deletions packages/widget-v2/src/components/AssetChainInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useAtom } from "jotai";
import { skipAssetsAtom, skipChainsAtom } from "@/state/skipClient";
import { useUsdValue } from "@/utils/useUsdValue";
import { formatUSD } from "@/utils/intl";
import { BigNumber } from "bignumber.js";
import { formatNumberWithCommas, formatNumberWithoutCommas } from "@/utils/number";

export type AssetChainInputProps = {
value?: string;
Expand All @@ -19,7 +21,7 @@ export type AssetChainInputProps = {
};

export const AssetChainInput = ({
value = "0",
value,
onChangeValue,
selectedAssetDenom,
handleChangeAsset,
Expand All @@ -39,6 +41,80 @@ export const AssetChainInput = ({

const usdValue = useUsdValue({ ...selectedAsset, value });

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!onChangeValue) return;
let latest = e.target.value;

if (latest.match(/^[.,]/)) latest = `0.${latest}`; // Handle first character being a period or comma
latest = latest.replace(/^[0]{2,}/, "0"); // Remove leading zeros
latest = latest.replace(/[^\d.,]/g, ""); // Remove non-numeric and non-decimal characters
latest = latest.replace(/[.]{2,}/g, "."); // Remove multiple decimals
latest = latest.replace(/[,]{2,}/g, ","); // Remove multiple commas
onChangeValue?.(formatNumberWithoutCommas(latest));
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (!onChangeValue) return;

let value: BigNumber;

switch (event.key) {
case "Escape":
if (
event.currentTarget.selectionStart ===
event.currentTarget.selectionEnd
) {
event.currentTarget.select();
}
return;

case "ArrowUp":
event.preventDefault();
value = new BigNumber(
formatNumberWithoutCommas(event.currentTarget.value) || "0"
);

if (event.shiftKey) {
value = value.plus(10);
} else if (event.altKey || event.ctrlKey || event.metaKey) {
value = value.plus(0.1);
} else {
value = value.plus(1);
}

if (value.isNegative()) {
value = new BigNumber(0);
}

onChangeValue(value.toString());
break;

case "ArrowDown":
event.preventDefault();
value = new BigNumber(
formatNumberWithoutCommas(event.currentTarget.value) || "0"
);

if (event.shiftKey) {
value = value.minus(10);
} else if (event.altKey || event.ctrlKey || event.metaKey) {
value = value.minus(0.1);
} else {
value = value.minus(1);
}

if (value.isNegative()) {
value = new BigNumber(0);
}

onChangeValue(value.toString());
break;

default:
return;
}
};

return (
<StyledAssetChainInputWrapper
justify="space-between"
Expand All @@ -48,8 +124,11 @@ export const AssetChainInput = ({
<Row justify="space-between">
<StyledInput
type="text"
value={value}
onChange={(e) => onChangeValue?.(e.target.value)}
value={formatNumberWithCommas(value || "")}
placeholder="0"
inputMode="numeric"
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button onClick={handleChangeAsset} gap={5}>
{selectedAsset ? (
Expand Down
53 changes: 37 additions & 16 deletions packages/widget-v2/src/pages/SwapPage/SwapPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { useCallback, useMemo, useState } from "react";
import { useAtom } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { AssetChainInput } from "@/components/AssetChainInput";
import { Column } from "@/components/Layout";
import { MainButton } from "@/components/MainButton";
import { SmallText } from "@/components/Typography";
import { ICONS } from "@/icons";
import { skipAssetsAtom, getChainsContainingAsset, skipChainsAtom } from "@/state/skipClient";
import { sourceAssetAtom, destinationAssetAtom } from "@/state/swapPage";
import {
skipAssetsAtom,
getChainsContainingAsset,
skipChainsAtom,
skipRouteAtom,
} from "@/state/skipClient";
import {
sourceAssetAtom,
destinationAssetAtom,
swapDirectionAtom,
} from "@/state/swapPage";
import { TokenAndChainSelectorModal } from "@/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModal";
import { SwapPageSettings } from "./SwapPageSettings";
import { SwapPageFooter } from "./SwapPageFooter";
Expand All @@ -20,22 +29,31 @@ export const SwapPage = () => {
const [container, setContainer] = useState<HTMLDivElement>();
const [drawerOpen, setDrawerOpen] = useState(false);
const [sourceAsset, setSourceAsset] = useAtom(sourceAssetAtom);
const [destinationAsset, setDestinationAsset] = useAtom(destinationAssetAtom);
const [{ data: assets }] = useAtom(skipAssetsAtom);
const [{ data: chains }] = useAtom(skipChainsAtom);
const [destinationAsset, setDestinationAsset] = useAtom(destinationAssetAtom);

const setSwapDirection = useSetAtom(swapDirectionAtom);
const { isLoading: isRouteLoading } = useAtomValue(skipRouteAtom);
const swapFlowSettings = useModal(SwapPageSettings);
const tokenAndChainSelectorFlow = useModal(TokenAndChainSelectorModal);

const chainsContainingSourceAsset = useMemo(() => {
if (!chains || !assets || !sourceAsset?.symbol) return;
const result = getChainsContainingAsset(sourceAsset?.symbol, assets, chains);
const result = getChainsContainingAsset(
sourceAsset?.symbol,
assets,
chains
);
return result;
}, [assets, sourceAsset?.symbol, chains]);

const chainsContainingDestinationAsset = useMemo(() => {
if (!chains || !assets || !destinationAsset?.symbol) return;
const result = getChainsContainingAsset(destinationAsset?.symbol, assets, chains);
const result = getChainsContainingAsset(
destinationAsset?.symbol,
assets,
chains
);
return result;
}, [assets, destinationAsset?.symbol, chains]);

Expand Down Expand Up @@ -132,23 +150,26 @@ export const SwapPage = () => {
selectedAssetDenom={sourceAsset?.denom}
handleChangeAsset={handleChangeSourceAsset}
handleChangeChain={handleChangeSourceChain}
value={sourceAsset?.amount ?? "0"}
onChangeValue={(newValue) =>
setSourceAsset((old) => ({ ...old, amount: newValue }))
}
value={sourceAsset?.amount}
onChangeValue={(newValue) => {
setSourceAsset((old) => ({ ...old, amount: newValue }));
setSwapDirection("swap-in");
}}
/>
<SwapPageBridge />
<AssetChainInput
selectedAssetDenom={destinationAsset?.denom}
handleChangeAsset={handleChangeDestinationAsset}
handleChangeChain={handleChangeDestinationChain}
value={destinationAsset?.amount ?? "0"}
onChangeValue={(newValue) =>
setDestinationAsset((old) => ({ ...old, amount: newValue }))
}
value={destinationAsset?.amount}
onChangeValue={(newValue) => {
setDestinationAsset((old) => ({ ...old, amount: newValue }));
setSwapDirection("swap-out");
}}
/>
</Column>
<MainButton label="Connect Wallet" icon={ICONS.plus} />
{!isRouteLoading && <MainButton label="Connect Wallet" icon={ICONS.plus} />}
{isRouteLoading && <MainButton label="Finding Best Route..." loading={true} />}

<SwapPageFooter
showRouteInfo
Expand Down
24 changes: 23 additions & 1 deletion packages/widget-v2/src/state/skipClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from "@skip-go/client";
import { atomWithQuery } from "jotai-tanstack-query";
import { apiURL, endpointOptions } from "@/constants/skipClientDefault";
import { destinationAssetAtom, routeAmountEffect, sourceAssetAtom, swapDirectionAtom } from "./swapPage";
import { getAmountWei } from "@/utils/number";

export const skipClientConfigAtom = atom<SkipClientOptions>({
apiURL,
Expand Down Expand Up @@ -93,11 +95,30 @@ export const skipSwapVenuesAtom = atomWithQuery((get) => {
};
});

export const skipRouteRequestAtom = atom<RouteRequest>();
export const skipRouteRequestAtom = atom<RouteRequest | undefined>((get) => {
const sourceAsset = get(sourceAssetAtom);
const destinationAsset = get(destinationAssetAtom);
const direction = get(swapDirectionAtom);
if (!sourceAsset?.chainID || !sourceAsset.denom || !destinationAsset?.chainID || !destinationAsset.denom) {
return undefined;
}
const amount = direction === "swap-in"
? { amountIn: getAmountWei(sourceAsset.amount, sourceAsset.decimals) || "0" }
: { amountOut: getAmountWei(destinationAsset.amount, destinationAsset.decimals) || "0" };

return {
...amount,
sourceAssetChainID: sourceAsset.chainID,
sourceAssetDenom: sourceAsset.denom,
destAssetChainID: destinationAsset.chainID,
destAssetDenom: destinationAsset.denom,
};
});

export const skipRouteAtom = atomWithQuery((get) => {
const skip = get(skipClient);
const params = get(skipRouteRequestAtom);
get(routeAmountEffect)
return {
queryKey: ["skipRoute", params],
queryFn: async () => {
Expand All @@ -117,6 +138,7 @@ export const skipRouteAtom = atomWithQuery((get) => {
});
},
enabled: !!params,
refetchInterval: 1000 * 10,
};
});

Expand Down
33 changes: 32 additions & 1 deletion packages/widget-v2/src/state/swapPage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { atom } from "jotai";
import { ClientAsset } from "./skipClient";
import { ClientAsset, skipRouteAtom } from "./skipClient";
import { Wallet } from "@/components/RenderWalletList";
import { atomEffect } from "jotai-effect";
import { parseAmountWei } from "@/utils/number";

export type AssetAtom = Partial<ClientAsset> & {
amount?: string;
Expand All @@ -10,6 +12,35 @@ export const sourceAssetAtom = atom<AssetAtom>();

export const destinationAssetAtom = atom<AssetAtom>();

export const swapDirectionAtom = atom<"swap-in" | "swap-out">("swap-in");

export const connectedWalletAtom = atom<Wallet>();

export const destinationWalletAtom = atom<Wallet>();


export const routeAmountEffect = atomEffect((get, set) => {
const route = get(skipRouteAtom)
const direction = get(swapDirectionAtom)
const sourceAsset = get(sourceAssetAtom)
const destinationAsset = get(destinationAssetAtom)

const isSwapIn = direction === "swap-in";

if (!route.data || !sourceAsset || !destinationAsset) return

if (isSwapIn) {
const amount = parseAmountWei(route.data.amountOut, destinationAsset.decimals)
set(destinationAssetAtom, (old) => ({
...old,
amount,
}))
}
else {
const amount = parseAmountWei(route.data.amountIn, sourceAsset.decimals)
set(sourceAssetAtom, (old) => ({
...old,
amount,
}))
}
})
30 changes: 30 additions & 0 deletions packages/widget-v2/src/utils/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BigNumber } from "bignumber.js";
import { formatUnits } from "viem";

export function formatNumberWithCommas(str: string | number) {
const parts = str.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}

export function formatNumberWithoutCommas(str: string | number) {
return str.toString().replace(/,/g, "");
}

export function getAmountWei(amount?: string, decimals = 6) {
if (!amount || !amount) return "0";
try {
return new BigNumber(formatNumberWithoutCommas(amount)).shiftedBy(decimals ?? 6).toFixed(0);
} catch (_err) {
return "0";
}
}

export function parseAmountWei(amount?: string, decimals = 6) {
if (!amount) return "0";
try {
return formatUnits(BigInt(amount.replace(/,/g, "")), decimals ?? 6);
} catch (_err) {
return "0";
}
}
Loading

0 comments on commit a81fb70

Please sign in to comment.