Skip to content

Commit

Permalink
feat(bex): add new swap-fees component and create pools using that fee
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulMcInnis committed Nov 5, 2024
1 parent 7dc7be8 commit 85bdb96
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 20 deletions.
51 changes: 32 additions & 19 deletions apps/hub/src/app/pools/create/CreatePageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { type Token } from "@bera/berajs";
import { balancerPoolCreationHelper, balancerVaultAddress } from "@bera/config";
import { ActionButton, ApproveButton, useAnalytics } from "@bera/shared-ui";
import {
ActionButton,
ApproveButton,
SwapFeeInput,
useAnalytics,
} from "@bera/shared-ui";
import { cn } from "@bera/ui";
import { Alert, AlertDescription, AlertTitle } from "@bera/ui/alert";
import { Button } from "@bera/ui/button";
Expand Down Expand Up @@ -40,10 +45,11 @@ export default function CreatePageContent() {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [enableLiquidityInput, setEnableLiquidityInput] =
useState<boolean>(false);
const [swapFee, setSwapFee] = useState<number>(0.01);

// handle max/min tokens per https://docs.balancer.fi/concepts/pools/more/configuration.html
const minTokensLength = poolType === PoolType.Weighted ? 3 : 2; // i.e. for stable/composable stable it's 2
const maxTokensLength = poolType === PoolType.Weighted ? 8 : 4; // i.e. for stable/composable stable it's 4 w/ BPT as a token
const minTokensLength = poolType === PoolType.Weighted ? 3 : 2; // i.e. for meta/stable it's 2
const maxTokensLength = poolType === PoolType.Weighted ? 8 : 5; // i.e. for meta/stable it's 5

// check for token approvals
const { needsApproval: tokensNeedApproval, refresh: refreshAllowances } =
Expand Down Expand Up @@ -126,6 +132,7 @@ export default function CreatePageContent() {
tokens,
weights: weights,
poolType,
swapFee,
onSuccess: () => {
track("create_pool_success");
router.push("/pools");
Expand Down Expand Up @@ -183,7 +190,7 @@ export default function CreatePageContent() {
<Card
onClick={() => setPoolType(PoolType.ComposableStable)}
className={cn(
"flex w-full cursor-pointer flex-col gap-0 border-2 p-4",
"flex w-full cursor-pointer flex-col gap-0 border p-4",
poolType === PoolType.ComposableStable && "border-sky-600",
)}
>
Expand All @@ -199,7 +206,7 @@ export default function CreatePageContent() {
<Card
onClick={() => setPoolType(PoolType.Weighted)}
className={cn(
"flex w-full cursor-pointer flex-col gap-0 border-2 p-4",
"flex w-full cursor-pointer flex-col gap-0 border p-4",
poolType === PoolType.Weighted && "border-sky-600",
)}
>
Expand All @@ -214,7 +221,7 @@ export default function CreatePageContent() {
<Card
onClick={() => setPoolType(PoolType.MetaStable)}
className={cn(
"flex w-full cursor-pointer flex-col gap-0 border-2 p-4",
"flex w-full cursor-pointer flex-col gap-0 border p-4",
poolType === PoolType.MetaStable && "border-sky-600",
)}
>
Expand Down Expand Up @@ -242,13 +249,20 @@ export default function CreatePageContent() {
}
/>
{tokens.length > minTokensLength && (
<Button onClick={() => removeTokenInput(index)}></Button>
<Button
onClick={() => removeTokenInput(index)}
variant="ghost"
>
x
</Button>
)}
</div>
))}
{tokens.length < maxTokensLength && (
<div className="mr-auto">
<Button onClick={addTokenInput}>+ Add Token</Button>
<Button onClick={addTokenInput} variant="ghost">
+ Add Token
</Button>
</div>
)}
</div>
Expand Down Expand Up @@ -349,22 +363,20 @@ export default function CreatePageContent() {
<section className="flex w-full flex-col gap-10">
<h1 className="self-start text-3xl font-semibold">Set Swap Fee</h1>
<div className="flex flex-col gap-4">
<Card className="flex w-full cursor-pointer flex-col gap-0 border-2 p-4">
<span className="text-lg font-semibold">Swap Fee</span>
<span className="mt-[-4px] text-sm text-muted-foreground">
Fee charged on each swap
</span>
<span className="mt-[24px] text-sm text-muted-foreground">
Fee:{" "}
<span className="font-medium text-foreground">0.01%</span>
</span>
<Card className="flex w-full cursor-pointer flex-col gap-0 border p-4">
<SwapFeeInput
initialFee={swapFee}
onFeeChange={(fee) => {
setSwapFee(fee);
}}
/>
</Card>
</div>
</section>

<section className="flex w-full flex-col gap-10 py-12">
<div className="flex flex-col gap-4">
<Card className="flex w-full cursor-pointer flex-col gap-0 border-2 p-4">
<Card className="flex w-full cursor-pointer flex-col gap-0 border p-4">
<InputWithLabel
label="Pool Name"
value={poolName}
Expand All @@ -374,7 +386,7 @@ export default function CreatePageContent() {
}}
/>
</Card>
<Card className="flex w-full cursor-pointer flex-col gap-0 border-2 p-4">
<Card className="flex w-full cursor-pointer flex-col gap-0 border p-4">
<InputWithLabel
label="Pool Symbol"
value={poolSymbol}
Expand Down Expand Up @@ -407,6 +419,7 @@ export default function CreatePageContent() {

{tokensNeedApproval.length > 0 &&
(() => {
// NOTE: we might avoid doing this if we can return TokenInput amount in the ApprovalToken[]
const approvalTokenIndex = tokens.findIndex(
(t) => t.address === tokensNeedApproval[0]?.address,
);
Expand Down
7 changes: 6 additions & 1 deletion apps/hub/src/hooks/useCreatePool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface UseCreatePoolProps {
tokens: TokenInput[];
weights: number[]; // NOTE: weights are an array of percentages summing to 100
poolType: PoolType;
swapFee: number;
onSuccess?: (hash: string) => void;
onError?: (e?: Error) => void;
}
Expand All @@ -40,6 +41,7 @@ export const useCreatePool = ({
tokens,
weights,
poolType,
swapFee,
onSuccess,
onError,
}: UseCreatePoolProps): UseCreatePoolReturn => {
Expand Down Expand Up @@ -156,10 +158,13 @@ export const useCreatePool = ({

const tokensAddresses = tokens.map((token) => token.address);
const rateProviders = tokens.map(() => ADDRESS_ZERO);
const swapFeePercentage = BigInt(parseFixed("0.01", 16).toString());
const swapFeePercentage = parseUnits(swapFee.toString(), 16);

// TODO: the below 3 inputs will be connected as a part of stable pool create PR
const tokenRateCacheDurations = tokens.map(() => BigInt(100));
const exemptFromYieldProtocolFeeFlag = tokens.map(() => false);
const amplificationParameter = BigInt(62);

const amountsIn = tokens.map((token) =>
parseUnits(token.amount || "0", token.decimals ?? 18),
);
Expand Down
1 change: 1 addition & 0 deletions packages/shared-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ export * from "./hooks";
export * from "./utils";
export { BgtStationBanner } from "./bgt-station-banner";
export { Combobox } from "./combo-box";
export * from "./swap-fee";
87 changes: 87 additions & 0 deletions packages/shared-ui/src/swap-fee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import React, { useState } from "react";
import { cn } from "@bera/ui";

interface SwapFeeInputProps {
initialFee?: number;
onFeeChange?: (fee: number) => void;
}

export function SwapFeeInput({
initialFee = 0,
onFeeChange,
}: SwapFeeInputProps) {
const [fee, setFee] = useState<number>(initialFee);
const [isInvalid, setIsInvalid] = useState<boolean>(false);

const predefinedFees = [0.1, 0.2, 0.3];

const handleFeeChange = (value: string) => {
const parsedValue = parseFloat(value);
setFee(parsedValue);

// Validate the fee range and update the invalid state
if (parsedValue >= 0.00001 && parsedValue <= 10) {
setIsInvalid(false);
onFeeChange?.(parsedValue); // Pass valid fee to parent, if callback provided
} else {
setIsInvalid(true);
}
};

const handlePredefinedFeeClick = (value: number) => {
setFee(value);
setIsInvalid(false);
onFeeChange?.(value);
};

return (
<section className="flex w-full flex-col gap-6">
<h1 className="self-start text-3xl font-semibold">Set Swap Fee</h1>

<div className="flex flex-col gap-4">
<div className="relative">
<input
type="number"
value={fee}
onChange={(e) => handleFeeChange(e.target.value)}
placeholder="Enter swap fee"
className={cn(
"w-full rounded-md border-2 p-2 pr-10 text-2xl",
isInvalid ? "border-red-500" : "border-gray-300",
)}
aria-label="Swap Fee Input"
/>
<span className="absolute right-4 top-1/2 -translate-y-1/2 transform text-lg text-gray-500">
%
</span>
</div>

<div className="flex gap-2">
{predefinedFees.map((preset) => (
<button
type="button"
key={preset}
onClick={() => handlePredefinedFeeClick(preset)}
className={cn(
"rounded-md border-2 px-4 py-2",
fee === preset ? "border-blue-500" : "border-gray-300",
)}
aria-label="Swap Fee Input"
>
{preset}%
</button>
))}
</div>

{isInvalid && (
<div className="mt-2 rounded-md border border-red-500 p-2 text-sm text-red-500">
<i className="mr-2">⚠️</i>
Invalid fee. Ensure the entered fee is between 0.00001% and 10%.
</div>
)}
</div>
</section>
);
}

0 comments on commit 85bdb96

Please sign in to comment.