Skip to content

Commit

Permalink
[ECO-2003] Add overloaded rewards swap and confetti winner animation (#…
Browse files Browse the repository at this point in the history
…174)

Co-authored-by: matt <[email protected]>
  • Loading branch information
xbtmatt and matt authored Jul 23, 2024
1 parent 6c6aefc commit 0daa30e
Show file tree
Hide file tree
Showing 11 changed files with 583 additions and 23 deletions.
3 changes: 3 additions & 0 deletions src/typescript/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NEXT_PUBLIC_APTOS_NETWORK="testnet"
# The address of the main `emojicoin_dot_fun` contract module
NEXT_PUBLIC_MODULE_ADDRESS="0xbabe8f471b7f4744502b1397530bafe80e3731b358c0dfeba26b38b2603bd00d"

# The address of the rewards module with the overloaded swap function
NEXT_PUBLIC_REWARDS_MODULE_ADDRESS="0x76044a237dcc3f71af75fb314f016e8032633587f7d70df4e70777f2b0221e75"

# The default URL for a local inbox deployment. Do not add a slash to the end
INBOX_URL="http://localhost:3000"

Expand Down
1 change: 1 addition & 0 deletions src/typescript/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ REVALIDATION_TIME="10"
NEXT_PUBLIC_APTOS_NETWORK="local"
NEXT_PUBLIC_INTEGRATOR_ADDRESS="0x4bab58978ec1b1bef032eeb285ad47a6a9b997d646c19b598c35f46b26ff9ece"
NEXT_PUBLIC_MODULE_ADDRESS="0x4bab58978ec1b1bef032eeb285ad47a6a9b997d646c19b598c35f46b26ff9ece"
NEXT_PUBLIC_REWARDS_MODULE_ADDRESS="0x76044a237dcc3f71af75fb314f016e8032633587f7d70df4e70777f2b0221e75"

# Randomly generated private key for tests. This is only used in e2e tests and for script data generation.
PUBLISHER_PK="29479e9e5fe47ba9a8af509dd6da1f907510bcf8917bfb19b7079d8c63c0b720"
Expand Down
2 changes: 2 additions & 0 deletions src/typescript/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
"next": "^14.2.4",
"petra-plugin-wallet-adapter": "^0.4.5",
"react": "^18.3.1",
"react-confetti": "^6.1.0",
"react-dom": "^18.3.1",
"react-infinite-scroll-component": "^6.1.0",
"react-popper": "^2.3.0",
"react-toastify": "^9.1.3",
"react-use": "^17.5.1",
"semver": "^7.6.2",
"server-only": "^0.0.1",
"sharp": "^0.33.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { ExplorerLink } from "components/link/component";
import { motion } from "framer-motion";
import { APTOS_NETWORK } from "lib/env";

export const CongratulationsToast = ({ transactionHash }: { transactionHash: string }) => {
const { network } = useWallet();
return (
<div className="flex flex-col text-center">
<motion.span
animate={{
filter: ["hue-rotate(0deg)", "hue-rotate(360deg)"],
transition: {
repeat: Infinity,
duration: 2,
repeatType: "reverse",
},
}}
className="text-ec-blue font-forma-bold"
>
{"Congratulations!"}
</motion.span>
<span>{"You won 1 APT."}</span>
<div className="mt-[1ch]">
<span>{"View the transaction "}</span>
<ExplorerLink
className="font-forma inline font-bold text-orange-500 drop-shadow-text"
network={network?.name ?? APTOS_NETWORK}
value={transactionHash}
type="transaction"
>
{"here."}
</ExplorerLink>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { type AnimationControls, type HTMLMotionProps, motion } from "framer-motion";
import { useWindowSize } from "react-use";
import Confetti from "react-confetti";
import { type Dispatch, type MutableRefObject, type SetStateAction, useRef, useState } from "react";
import { sleep } from "@sdk/utils";

/**
* The tween duration for the confetti animation is very inaccurate- a few stray confetti fall after 1/3 of the way
* through, but the animation is mostly done at that point. To account for this, we end the animation manually after
* roughly half of the way through.
*/
const TWEEN_DURATION = 15000;
const INTERRUPT_AT = 0.4;

const renderHelper = (setter: Dispatch<SetStateAction<boolean>>) => {
setter(true);
sleep(TWEEN_DURATION * INTERRUPT_AT).then(() => {
setter(false);
});
};

/**
* In case we reach the end of the total number of confetti components, we cycle back, keeping track of the most recent
* confetti index in order to stop and then re-render the next. The delay to restart on cycle is how long we wait in
* between un-rendering and then rendering the confetti component.
*/
const DELAY_TO_RESTART_ON_CYCLE = 100;
const stopAndStartHelper = (nextSetter: Dispatch<SetStateAction<boolean>>) => {
nextSetter(false);
sleep(DELAY_TO_RESTART_ON_CYCLE).then(() => {
nextSetter(true);
});
};

/**
* The Confetti component we use. We specify the next confetti to start and the onConfettiComplete function.
*/
const CustomConfetti = ({
lastIdx, // The most recently used index for starting a confetti animation.
secondaryIndex, // The secondary index for this confetti.
setter,
}: {
lastIdx: MutableRefObject<number>;
secondaryIndex: number;
setter: Dispatch<SetStateAction<boolean>>;
}) => {
const { width, height } = useWindowSize();

return (
<Confetti
width={width}
height={height}
numberOfPieces={3000}
gravity={0.15}
tweenDuration={TWEEN_DURATION}
recycle={false}
onConfettiComplete={() => {
if (lastIdx.current !== secondaryIndex) {
// Note that we're basically just ensuring that we don't accidentally cancel a "restarted" animation due to
// an "regularly started" animation finishing.
setter(false);
}
}}
/>
);
};

/**
* To explain this sort of convoluted mess:
* - This animation cycles through confetti components, in order of 1, 2, 3, 4, 5, 6, where 4, 5, 6 represent
* the three components but having been restarted mid animation.
* - We keep track of the most recent confetti used, differentiating between regularly started and restarted
* confetti components with `lastIdx.current`, where 1 is regularly started and 4 is restarted, but they are
* the same component.
* - This facilitates seamlessly restarting confetti without having to create some arbitrary, infinite number
* of components.
*/
export const RewardsAnimation = ({
controls,
...props
}: {
controls: AnimationControls;
} & HTMLMotionProps<"div">) => {
const [renderFirst, setRenderFirst] = useState(false);
const [renderSecond, setRenderSecond] = useState(false);
const [renderThird, setRenderThird] = useState(false);
const lastIdx = useRef(0);

return (
<motion.div
initial={{
display: "none",
}}
animate={controls}
variants={{
celebration: {
display: "initial",
},
}}
onAnimationStart={() => {
if (!renderFirst) {
lastIdx.current = 1;
renderHelper(setRenderFirst);
} else if (!renderSecond) {
lastIdx.current = 2;
renderHelper(setRenderSecond);
} else if (!renderThird) {
lastIdx.current = 3;
renderHelper(setRenderThird);
} else {
if (lastIdx.current === 1) {
lastIdx.current = 5; // normal index + 3, to circumvent an in progress onConfettiComplete.
stopAndStartHelper(setRenderSecond);
} else if (lastIdx.current === 2) {
lastIdx.current = 6; // normal index + 3, to circumvent an in progress onConfettiComplete.
stopAndStartHelper(setRenderThird);
} else if (lastIdx.current === 3) {
lastIdx.current = 4; // normal index + 3, to circumvent an in progress onConfettiComplete.
stopAndStartHelper(setRenderFirst);
} else {
// All out of confetti...it should never reach here. Nevertheless, we do nothing.
}
}
}}
className="absolute top-0 left-0 z-[101]"
{...props}
>
{renderFirst && (
<CustomConfetti lastIdx={lastIdx} secondaryIndex={4} setter={setRenderFirst} />
)}
{renderSecond && (
<CustomConfetti lastIdx={lastIdx} secondaryIndex={5} setter={setRenderSecond} />
)}
{renderThird && (
<CustomConfetti lastIdx={lastIdx} secondaryIndex={6} setter={setRenderThird} />
)}
</motion.div>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import ButtonWithConnectWalletFallback from "components/header/wallet-button/ConnectWalletButton";
import Button from "components/button";
import { translationFunction } from "context/language-context";
import { Swap } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun";
import { SwapWithRewards } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun";
import { useAptos } from "context/wallet-context/AptosContextProvider";
import { INTEGRATOR_ADDRESS, INTEGRATOR_FEE_RATE_BPS } from "lib/env";
import { toCoinTypes } from "@sdk/markets/utils";
import { type AccountAddressString } from "@sdk/emojicoin_dot_fun";
import { type Dispatch, type SetStateAction, useEffect, useCallback } from "react";
import { isUserTransactionResponse } from "@aptos-labs/ts-sdk";
import { STRUCT_STRINGS } from "@sdk/utils";
import { useAnimationControls } from "framer-motion";
import { RewardsAnimation } from "./RewardsAnimation";
import { toast } from "react-toastify";
import { CongratulationsToast } from "./CongratulationsToast";

export const SwapButton = ({
inputAmount,
Expand All @@ -21,35 +26,51 @@ export const SwapButton = ({
}) => {
const { t } = translationFunction();
const { aptos, account, submit } = useAptos();
const controls = useAnimationControls();

const handleClick = useCallback(async () => {
if (!account) {
return;
}
const { emojicoin, emojicoinLP } = toCoinTypes(marketAddress);
const builderLambda = () =>
Swap.builder({
SwapWithRewards.builder({
aptosConfig: aptos.config,
swapper: account.address,
marketAddress,
inputAmount: BigInt(inputAmount),
isSell,
integrator: INTEGRATOR_ADDRESS,
integratorFeeRateBps: INTEGRATOR_FEE_RATE_BPS,
typeTags: [emojicoin, emojicoinLP],
});
await submit(builderLambda);
}, [account, aptos.config, inputAmount, isSell, marketAddress, submit]);
const res = await submit(builderLambda);
if (res && res.response && isUserTransactionResponse(res.response)) {
const rewardsEvent = res.response.events.find(
(e) => e.type === STRUCT_STRINGS.LotteryWinnerEvent
);
if (rewardsEvent) {
controls.start("celebration");
toast.success(<CongratulationsToast transactionHash={res.response.hash} />, {
pauseOnFocusLoss: false,
autoClose: 7000,
position: "top-center",
closeOnClick: false,
});
}
}
}, [account, aptos.config, inputAmount, isSell, marketAddress, submit, controls]);

useEffect(() => {
setSubmit(() => handleClick);
}, [handleClick, setSubmit]);

return (
<ButtonWithConnectWalletFallback>
<Button onClick={handleClick} scale="lg">
{t("Swap")}
</Button>
</ButtonWithConnectWalletFallback>
<>
<ButtonWithConnectWalletFallback>
<Button onClick={handleClick} scale="lg">
{t("Swap")}
</Button>
</ButtonWithConnectWalletFallback>
<RewardsAnimation controls={controls} />
</>
);
};
Loading

0 comments on commit 0daa30e

Please sign in to comment.