Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2
This commit is contained in:
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal file
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
|
||||
const drawArrow = (ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number) => {
|
||||
const [dx, dy] = [x1 - x2, y1 - y2];
|
||||
const norm = Math.sqrt(dx * dx + dy * dy);
|
||||
const [udx, udy] = [dx / norm, dy / norm];
|
||||
const size = norm / 7;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(x2 + udx * size - udy * size, y2 + udx * size + udy * size);
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(x2 + udx * size + udy * size, y2 - udx * size + udy * size);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export interface ICurveProps {
|
||||
ethReserve: number;
|
||||
tokenReserve: number;
|
||||
addingEth: number;
|
||||
addingToken: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const Curve: FC<ICurveProps> = (props: ICurveProps) => {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = ref.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textSize = 12;
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
if (props.ethReserve && props.tokenReserve) {
|
||||
const k = props.ethReserve * props.tokenReserve;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
let maxX = k / (props.ethReserve / 4);
|
||||
let minX = 0;
|
||||
|
||||
if (props.addingEth || props.addingToken) {
|
||||
maxX = k / (props.ethReserve * 0.4);
|
||||
//maxX = k/(props.ethReserve*0.8)
|
||||
minX = k / Math.max(0, 500 - props.ethReserve);
|
||||
}
|
||||
|
||||
const maxY = (maxX * height) / width;
|
||||
const minY = (minX * height) / width;
|
||||
|
||||
const plotX = (x: number) => {
|
||||
return ((x - minX) / (maxX - minX)) * width;
|
||||
};
|
||||
|
||||
const plotY = (y: number) => {
|
||||
return height - ((y - minY) / (maxY - minY)) * height;
|
||||
};
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.font = textSize + "px Arial";
|
||||
// +Y axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(plotX(minX), plotY(0));
|
||||
ctx.lineTo(plotX(minX), plotY(maxY));
|
||||
ctx.stroke();
|
||||
// +X axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(plotX(0), plotY(minY));
|
||||
ctx.lineTo(plotX(maxX), plotY(minY));
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
let first = true;
|
||||
for (let x = minX; x <= maxX; x += maxX / width) {
|
||||
/////
|
||||
const y = k / x;
|
||||
/////
|
||||
if (first) {
|
||||
ctx.moveTo(plotX(x), plotY(y));
|
||||
first = false;
|
||||
} else {
|
||||
ctx.lineTo(plotX(x), plotY(y));
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
if (props.addingEth) {
|
||||
const newEthReserve = props.ethReserve + parseFloat(props.addingEth.toString());
|
||||
|
||||
ctx.fillStyle = "#bbbbbb";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(newEthReserve), plotY(k / newEthReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = "#009900";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(props.tokenReserve),
|
||||
plotX(newEthReserve),
|
||||
plotY(props.tokenReserve),
|
||||
);
|
||||
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + props.addingEth + " ETH input",
|
||||
plotX(props.ethReserve) + textSize,
|
||||
plotY(props.tokenReserve) - textSize,
|
||||
);
|
||||
|
||||
ctx.strokeStyle = "#990000";
|
||||
drawArrow(ctx, plotX(newEthReserve), plotY(props.tokenReserve), plotX(newEthReserve), plotY(k / newEthReserve));
|
||||
|
||||
const amountGained = Math.round((10000 * (props.addingEth * props.tokenReserve)) / newEthReserve) / 10000;
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + amountGained + " 🎈 output (-0.3% fee)",
|
||||
plotX(newEthReserve) + textSize,
|
||||
plotY(k / newEthReserve),
|
||||
);
|
||||
} else if (props.addingToken) {
|
||||
const newTokenReserve = props.tokenReserve + parseFloat(props.addingToken.toString());
|
||||
|
||||
ctx.fillStyle = "#bbbbbb";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(k / newTokenReserve), plotY(newTokenReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
//console.log("newTokenReserve",newTokenReserve)
|
||||
ctx.strokeStyle = "#990000";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(props.tokenReserve),
|
||||
plotX(props.ethReserve),
|
||||
plotY(newTokenReserve),
|
||||
);
|
||||
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + props.addingToken + " 🎈 input",
|
||||
plotX(props.ethReserve) + textSize,
|
||||
plotY(props.tokenReserve),
|
||||
);
|
||||
|
||||
ctx.strokeStyle = "#009900";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(newTokenReserve),
|
||||
plotX(k / newTokenReserve),
|
||||
plotY(newTokenReserve),
|
||||
);
|
||||
|
||||
const amountGained = Math.round((10000 * (props.addingToken * props.ethReserve)) / newTokenReserve) / 10000;
|
||||
//console.log("amountGained",amountGained)
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + amountGained + " ETH output (-0.3% fee)",
|
||||
plotX(k / newTokenReserve) + textSize,
|
||||
plotY(newTokenReserve) - textSize,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(props.ethReserve), plotY(props.tokenReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: props.width, height: props.height }}>
|
||||
<canvas style={{ position: "absolute", left: 0, top: 0 }} ref={ref} width={props.width} height={props.height} />
|
||||
<div style={{ position: "absolute", left: "20%", bottom: -20 }}>-- ETH Reserve --{">"}</div>
|
||||
<div
|
||||
style={{ position: "absolute", left: -20, bottom: "20%", transform: "rotate(-90deg)", transformOrigin: "0 0" }}
|
||||
>
|
||||
-- Token Reserve --{">"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Curve";
|
||||
298
packages/nextjs/app/dex/page.tsx
Normal file
298
packages/nextjs/app/dex/page.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Curve } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { Address as AddressType, formatEther, isAddress, parseEther } from "viem";
|
||||
import { useAccount } from "wagmi";
|
||||
import { Address, AddressInput, Balance, EtherInput, IntegerInput } from "~~/components/scaffold-eth";
|
||||
import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
|
||||
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
|
||||
|
||||
// REGEX for number inputs (only allow numbers and a single decimal point)
|
||||
const NUMBER_REGEX = /^\.?\d+\.?\d*$/;
|
||||
|
||||
const Dex: NextPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [ethToTokenAmount, setEthToTokenAmount] = useState("");
|
||||
const [tokenToETHAmount, setTokenToETHAmount] = useState("");
|
||||
const [depositAmount, setDepositAmount] = useState("");
|
||||
const [withdrawAmount, setWithdrawAmount] = useState("");
|
||||
const [approveSpender, setApproveSpender] = useState("");
|
||||
const [approveAmount, setApproveAmount] = useState("");
|
||||
const [accountBalanceOf, setAccountBalanceOf] = useState("");
|
||||
|
||||
const { data: DEXInfo } = useDeployedContractInfo({ contractName: "DEX" });
|
||||
const { data: BalloonsInfo } = useDeployedContractInfo({ contractName: "Balloons" });
|
||||
const { address: connectedAccount } = useAccount();
|
||||
|
||||
const { data: DEXBalloonBalance } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [DEXInfo?.address?.toString()],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (DEXBalloonBalance !== undefined) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [DEXBalloonBalance]);
|
||||
|
||||
const { data: DEXtotalLiquidity } = useScaffoldReadContract({
|
||||
contractName: "DEX",
|
||||
functionName: "totalLiquidity",
|
||||
});
|
||||
|
||||
const { writeContractAsync: writeDexContractAsync } = useScaffoldWriteContract({ contractName: "DEX" });
|
||||
|
||||
const { writeContractAsync: writeBalloonsContractAsync } = useScaffoldWriteContract({ contractName: "Balloons" });
|
||||
|
||||
const { data: balanceOfWrite } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [accountBalanceOf as AddressType],
|
||||
query: {
|
||||
enabled: isAddress(accountBalanceOf),
|
||||
},
|
||||
});
|
||||
|
||||
const { data: contractBalance } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [DEXInfo?.address],
|
||||
});
|
||||
|
||||
const { data: userBalloons } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [connectedAccount],
|
||||
});
|
||||
|
||||
const { data: userLiquidity } = useScaffoldReadContract({
|
||||
contractName: "DEX",
|
||||
functionName: "getLiquidity",
|
||||
args: [connectedAccount],
|
||||
});
|
||||
|
||||
const { data: contractETHBalance } = useWatchBalance({ address: DEXInfo?.address });
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-center mb-4 mt-5">
|
||||
<span className="block text-xl text-right mr-7">
|
||||
🎈: {parseFloat(formatEther(userBalloons || 0n)).toFixed(4)}
|
||||
</span>
|
||||
<span className="block text-xl text-right mr-7">
|
||||
💦💦: {parseFloat(formatEther(userLiquidity || 0n)).toFixed(4)}
|
||||
</span>
|
||||
<span className="block text-2xl mb-2">SpeedRunEthereum</span>
|
||||
<span className="block text-4xl font-bold">Challenge: ⚖️ Build a DEX </span>
|
||||
</h1>
|
||||
<div className="items-start pt-10 grid grid-cols-1 md:grid-cols-2 content-start">
|
||||
<div className="px-5 py-5">
|
||||
<div className="bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-8 m-8">
|
||||
<div className="flex flex-col text-center">
|
||||
<span className="text-3xl font-semibold mb-2">DEX Contract</span>
|
||||
<span className="block text-2xl mb-2 mx-auto">
|
||||
<Address size="xl" address={DEXInfo?.address} />
|
||||
</span>
|
||||
<span className="flex flex-row mx-auto mt-5">
|
||||
{" "}
|
||||
<Balance className="text-xl" address={DEXInfo?.address} /> ⚖️
|
||||
{isLoading ? (
|
||||
<span>Loading...</span>
|
||||
) : (
|
||||
<span className="pl-8 text-xl">🎈 {parseFloat(formatEther(DEXBalloonBalance || 0n)).toFixed(4)}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
ethToToken{" "}
|
||||
<EtherInput
|
||||
value={ethToTokenAmount}
|
||||
onChange={value => {
|
||||
setTokenToETHAmount("");
|
||||
setEthToTokenAmount(value);
|
||||
}}
|
||||
name="ethToToken"
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "ethToToken",
|
||||
value: NUMBER_REGEX.test(ethToTokenAmount) ? parseEther(ethToTokenAmount) : 0n,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling ethToToken function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
tokenToETH{" "}
|
||||
<IntegerInput
|
||||
value={tokenToETHAmount}
|
||||
onChange={value => {
|
||||
setEthToTokenAmount("");
|
||||
setTokenToETHAmount(value.toString());
|
||||
}}
|
||||
name="tokenToETH"
|
||||
disableMultiplyBy1e18
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "tokenToEth",
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
args: [NUMBER_REGEX.test(tokenToETHAmount) ? parseEther(tokenToETHAmount) : tokenToETHAmount],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling tokenToEth function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-primary-content text-xl mt-8 -ml-8">
|
||||
Liquidity ({DEXtotalLiquidity ? parseFloat(formatEther(DEXtotalLiquidity || 0n)).toFixed(4) : "None"})
|
||||
</p>
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Deposit <EtherInput value={depositAmount} onChange={value => setDepositAmount(value)} />
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "deposit",
|
||||
value: NUMBER_REGEX.test(depositAmount) ? parseEther(depositAmount) : 0n,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling deposit function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Withdraw <EtherInput value={withdrawAmount} onChange={value => setWithdrawAmount(value)} />
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "withdraw",
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
args: [NUMBER_REGEX.test(withdrawAmount) ? parseEther(withdrawAmount) : withdrawAmount],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling withdraw function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl py-5 p-8 m-8">
|
||||
<div className="flex flex-col text-center mt-2 mb-4 px-4">
|
||||
<span className="block text-3xl font-semibold mb-2">Balloons</span>
|
||||
<span className="mx-auto">
|
||||
<Address size="xl" address={BalloonsInfo?.address} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className=" px-4 py-3">
|
||||
<div className="flex flex-col gap-4 mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Approve{" "}
|
||||
<AddressInput
|
||||
value={approveSpender ?? ""}
|
||||
onChange={value => setApproveSpender(value)}
|
||||
placeholder="Address Spender"
|
||||
/>
|
||||
</span>
|
||||
<span className="w-1/2">
|
||||
<IntegerInput
|
||||
value={approveAmount}
|
||||
onChange={value => setApproveAmount(value.toString())}
|
||||
placeholder="Amount"
|
||||
disableMultiplyBy1e18
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-auto"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeBalloonsContractAsync({
|
||||
functionName: "approve",
|
||||
args: [
|
||||
approveSpender as AddressType,
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
NUMBER_REGEX.test(approveAmount) ? parseEther(approveAmount) : approveAmount,
|
||||
],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling approve function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<span className="w-1/2">
|
||||
balanceOf{" "}
|
||||
<AddressInput
|
||||
value={accountBalanceOf}
|
||||
onChange={value => setAccountBalanceOf(value)}
|
||||
placeholder="address Account"
|
||||
/>
|
||||
</span>
|
||||
{balanceOfWrite === undefined ? (
|
||||
<h1></h1>
|
||||
) : (
|
||||
<span className="font-bold bg-primary px-3 rounded-2xl">
|
||||
BAL Balance: {parseFloat(formatEther(balanceOfWrite || 0n)).toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto p-8 m-8 md:sticky md:top-0">
|
||||
<Curve
|
||||
addingEth={ethToTokenAmount !== "" ? parseFloat(ethToTokenAmount.toString()) : 0}
|
||||
addingToken={tokenToETHAmount !== "" ? parseFloat(tokenToETHAmount.toString()) : 0}
|
||||
ethReserve={parseFloat(formatEther(contractETHBalance?.value || 0n))}
|
||||
tokenReserve={parseFloat(formatEther(contractBalance || 0n))}
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dex;
|
||||
Reference in New Issue
Block a user