diff --git a/assets/icons/rakki-ticket.svg b/assets/icons/rakki-ticket.svg
new file mode 100644
index 0000000000..2f0c53237a
--- /dev/null
+++ b/assets/icons/rakki-ticket.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/ticket.svg b/assets/icons/ticket.svg
new file mode 100644
index 0000000000..f95928c5c4
--- /dev/null
+++ b/assets/icons/ticket.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/logos/rakki-ticket.png b/assets/logos/rakki-ticket.png
new file mode 100644
index 0000000000..498e7e67dd
Binary files /dev/null and b/assets/logos/rakki-ticket.png differ
diff --git a/assets/logos/rakki-ticket.svg b/assets/logos/rakki-ticket.svg
deleted file mode 100644
index f7a3c37364..0000000000
--- a/assets/logos/rakki-ticket.svg
+++ /dev/null
@@ -1,150 +0,0 @@
-
diff --git a/packages/components/gradientText/GradientText.tsx b/packages/components/gradientText/GradientText.tsx
index 846dbd7f63..b34ba177f0 100644
--- a/packages/components/gradientText/GradientText.tsx
+++ b/packages/components/gradientText/GradientText.tsx
@@ -17,8 +17,8 @@ import {
gradientColorPurple,
gradientColorSalmon,
gradientColorTurquoise,
- rakkiYellow,
- rakkiYellowLight,
+ gradientColorRakkiYellow,
+ gradientColorRakkiYellowLight,
} from "../../utils/style/colors";
import { BrandText } from "../BrandText";
@@ -115,7 +115,7 @@ const gradient = (type: GradientType): LinearGradientProps => {
};
case "yellow":
return {
- colors: [rakkiYellow, rakkiYellowLight],
+ colors: [gradientColorRakkiYellow, gradientColorRakkiYellowLight],
start,
end,
};
diff --git a/packages/components/gradientText/GradientText.web.tsx b/packages/components/gradientText/GradientText.web.tsx
index e7ddd31819..4612a21696 100644
--- a/packages/components/gradientText/GradientText.web.tsx
+++ b/packages/components/gradientText/GradientText.web.tsx
@@ -13,10 +13,10 @@ import {
gradientColorLightLavender,
gradientColorPink,
gradientColorPurple,
+ gradientColorRakkiYellow,
+ gradientColorRakkiYellowLight,
gradientColorSalmon,
gradientColorTurquoise,
- rakkiYellow,
- rakkiYellowLight,
} from "../../utils/style/colors";
import { exoFontFamilyFromFontWeight } from "../../utils/style/fonts";
@@ -43,7 +43,7 @@ const gradient = (type: GradientType) => {
case "grayLight":
return `90deg, ${gradientColorLighterGray} 0%, ${gradientColorLightLavender} 100%`;
case "yellow":
- return `267deg, ${rakkiYellow} 0%, ${rakkiYellowLight} 100%`;
+ return `267deg, ${gradientColorRakkiYellow} 0%, ${gradientColorRakkiYellowLight} 100%`;
case getMapPostTextGradientType(PostCategory.Normal):
return getMapPostTextGradientString(PostCategory.Normal);
case getMapPostTextGradientType(PostCategory.Article):
diff --git a/packages/hooks/rakki/useRakkiTicketsByUser.ts b/packages/hooks/rakki/useRakkiTicketsByUser.ts
new file mode 100644
index 0000000000..9894c09ece
--- /dev/null
+++ b/packages/hooks/rakki/useRakkiTicketsByUser.ts
@@ -0,0 +1,43 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { RakkiQueryClient } from "../../contracts-clients/rakki/Rakki.client";
+import {
+ NetworkFeature,
+ getNetworkFeature,
+ getNonSigningCosmWasmClient,
+ parseUserId,
+} from "../../networks";
+
+export const useRakkiTicketsCountByUser = (userId?: string) => {
+ const { data: ticketsCount = null, ...other } = useQuery(
+ ["rakkiTicketsCountByUser", userId],
+ async () => {
+ if (!userId) {
+ return null;
+ }
+ const [network, userAddress] = parseUserId(userId);
+ const networkId = network?.id;
+ if (!networkId) {
+ return null;
+ }
+ const rakkiFeature = getNetworkFeature(
+ networkId,
+ NetworkFeature.CosmWasmRakki,
+ );
+ if (!rakkiFeature) {
+ return null;
+ }
+ const cosmWasmClient = await getNonSigningCosmWasmClient(networkId);
+ if (!cosmWasmClient) {
+ return null;
+ }
+ const client = new RakkiQueryClient(
+ cosmWasmClient,
+ rakkiFeature.contractAddress,
+ );
+ return await client.ticketsCountByUser({ userAddr: userAddress });
+ },
+ { staleTime: Infinity, refetchInterval: 10000, enabled: !!userId },
+ );
+ return { ticketsCount, ...other };
+};
diff --git a/packages/screens/Rakki/RakkiScreen.tsx b/packages/screens/Rakki/RakkiScreen.tsx
index c3c2ba1510..46324c8db5 100644
--- a/packages/screens/Rakki/RakkiScreen.tsx
+++ b/packages/screens/Rakki/RakkiScreen.tsx
@@ -1,51 +1,22 @@
-import { useQueryClient } from "@tanstack/react-query";
-import Long from "long";
-import moment from "moment";
-import { useEffect, useState } from "react";
-import { StyleProp, TextInput, TextStyle, View, ViewStyle } from "react-native";
-
-import rakkiTicketSVG from "../../../assets/logos/rakki-ticket.svg";
-import { BrandText } from "../../components/BrandText";
-import { SVG } from "../../components/SVG";
-import { ScreenContainer } from "../../components/ScreenContainer";
-import { Box, BoxStyle } from "../../components/boxes/Box";
-import { TertiaryBox } from "../../components/boxes/TertiaryBox";
-import { PrimaryButton } from "../../components/buttons/PrimaryButton";
-import { SecondaryButton } from "../../components/buttons/SecondaryButton";
-import { GradientText } from "../../components/gradientText";
-import { UserAvatarWithFrame } from "../../components/images/AvatarWithFrame";
-import { GridList } from "../../components/layout/GridList";
-import { LoaderFullSize } from "../../components/loaders/LoaderFullScreen";
-import ModalBase from "../../components/modals/ModalBase";
-import { Username } from "../../components/user/Username";
-import { useFeedbacks } from "../../context/FeedbacksProvider";
-import { Info } from "../../contracts-clients/rakki/Rakki.types";
-import { useRakkiHistory } from "../../hooks/rakki/useRakkiHistory";
-import { useRakkiInfo } from "../../hooks/rakki/useRakkiInfo";
-import { useBalances } from "../../hooks/useBalances";
-import { useMaxResolution } from "../../hooks/useMaxResolution";
-import { useSelectedNetworkId } from "../../hooks/useSelectedNetwork";
-import useSelectedWallet from "../../hooks/useSelectedWallet";
-import { NetworkFeature, getNetworkFeature } from "../../networks";
-import { prettyPrice } from "../../utils/coins";
-import { ScreenFC } from "../../utils/navigation";
-import { errorColor } from "../../utils/style/colors";
-import {
- fontMedium10,
- fontSemibold12,
- fontSemibold13,
- fontSemibold14,
- fontSemibold16,
- fontSemibold28,
-} from "../../utils/style/fonts";
-import { modalMarginPadding } from "../../utils/style/modals";
-import { joinElements } from "../Multisig/components/MultisigRightSection";
-
-import { RakkiClient } from "@/contracts-clients/rakki";
-import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
-
-// TODO: replace all placeholders text with real values
-// TODO: jap gradient
+import { View } from "react-native";
+
+import { NetworkFeature } from "../../networks";
+
+import { BrandText } from "@/components/BrandText";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { LoaderFullSize } from "@/components/loaders/LoaderFullScreen";
+import { useRakkiInfo } from "@/hooks/rakki/useRakkiInfo";
+import { useMaxResolution } from "@/hooks/useMaxResolution";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import { GameBox } from "@/screens/Rakki/components/GameBox";
+import { Help } from "@/screens/Rakki/components/Help";
+import { PrizeInfo } from "@/screens/Rakki/components/PrizeInfo";
+import { RakkiHistory } from "@/screens/Rakki/components/RakkiHistory";
+import { RakkiLogo } from "@/screens/Rakki/components/RakkiLogo";
+import { TicketsRemaining } from "@/screens/Rakki/components/TicketsRamaining";
+import { sectionLabelCStyle } from "@/screens/Rakki/styles";
+import { ScreenFC } from "@/utils/navigation";
+import { layout } from "@/utils/style/layout";
export const RakkiScreen: ScreenFC<"Rakki"> = () => {
const networkId = useSelectedNetworkId();
@@ -66,6 +37,7 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => {
width: "100%",
alignItems: "center",
justifyContent: "center",
+ marginTop: 100,
}}
>
@@ -82,17 +54,24 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => {
networkId={networkId}
style={{ marginTop: 50 }}
/>
-
+
-
-
+
>
);
@@ -102,744 +81,9 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => {
footerChildren={rakkiInfo === undefined ? <>> : undefined}
forceNetworkFeatures={[NetworkFeature.CosmWasmRakki]}
>
- {content}
-
- );
-};
-
-const BuyTicketsButton: React.FC<{ networkId: string; info: Info }> = ({
- networkId,
- info,
-}) => {
- const selectedWallet = useSelectedWallet();
- const [modalVisible, setModalVisible] = useState(false);
- const remainingTickets = info.config.max_tickets - info.current_tickets_count;
- const [ticketAmount, setTicketAmount] = useState("1");
- const queryClient = useQueryClient();
- const ticketAmountNumber = Long.fromString(ticketAmount || "0");
- useEffect(() => {
- if (remainingTickets > 0 && ticketAmountNumber.gt(remainingTickets)) {
- setTicketAmount(remainingTickets.toString());
- }
- }, [ticketAmountNumber, remainingTickets]);
- const totalPrice = ticketAmountNumber.mul(
- Long.fromString(info.config.ticket_price.amount),
- );
- const { balances } = useBalances(networkId, selectedWallet?.address);
- const ticketDenomBalance =
- balances.find((b) => b.denom === info.config.ticket_price.denom)?.amount ||
- "0";
- const canPay = Long.fromString(ticketDenomBalance).gte(totalPrice);
- const canBuy = ticketAmountNumber.gt(0) && canPay;
- const { wrapWithFeedback } = useFeedbacks();
- return (
-
- setModalVisible(true)}
- textStyle={{ fontWeight: "400" }}
- text="Buy tickets"
- size="XS"
- />
- setModalVisible(false)}
- >
-
-
-
-
-
- 1 ticket price{" "}
-
- {prettyPrice(
- networkId,
- info.config.ticket_price.amount,
- info.config.ticket_price.denom,
- )}
-
-
-
-
-
- Number of Lottery Tickets
-
-
- {
- if (!newAmount) {
- setTicketAmount(newAmount);
- return;
- }
- const newAmountNumber = +newAmount;
- if (isNaN(newAmountNumber)) {
- return;
- }
- if (newAmountNumber > remainingTickets) {
- return;
- }
- setTicketAmount(newAmountNumber.toString());
- }}
- style={[
- fontSemibold16,
- {
- paddingLeft: 16,
- paddingRight: 10,
- fontWeight: "400",
- color: "white",
- },
- { outlineStyle: "none" } as TextStyle,
- ]}
- />
-
-
- Total price
-
-
- {prettyPrice(
- networkId,
- totalPrice.toString(),
- info.config.ticket_price.denom,
- )}
-
-
-
-
-
-
- Available Balance{" "}
-
- {prettyPrice(
- networkId,
- ticketDenomBalance,
- info.config.ticket_price.denom,
- )}
-
-
-
-
-
- setModalVisible(false)}
- />
- {
- if (!selectedWallet?.address) {
- throw new Error("No wallet with valid address selected");
- }
- const cosmWasmClient =
- await getKeplrSigningCosmWasmClient(networkId);
- const feature = getNetworkFeature(
- networkId,
- NetworkFeature.CosmWasmRakki,
- );
- if (feature?.type !== NetworkFeature.CosmWasmRakki) {
- throw new Error("Rakki not supported on this network");
- }
- const rakkiClient = new RakkiClient(
- cosmWasmClient,
- selectedWallet.address,
- feature.contractAddress,
- );
- const count = ticketAmountNumber.toNumber();
- const price = {
- amount: Long.fromString(info.config.ticket_price.amount)
- .multiply(count)
- .toString(),
- denom: info.config.ticket_price.denom,
- };
- await rakkiClient.buyTickets(
- {
- count,
- },
- "auto",
- undefined,
- [price],
- );
- await Promise.all([
- queryClient.invalidateQueries(["rakkiInfo", networkId]),
- queryClient.invalidateQueries([
- "balances",
- networkId,
- selectedWallet.address,
- ]),
- queryClient.invalidateQueries(["rakkiHistory", networkId]),
- ]);
- setModalVisible(false);
- })}
- text="Buy Tickets"
- size="M"
- />
-
-
-
-
-
- );
-};
-
-const History: React.FC<{
- style?: StyleProp;
- networkId: string;
- info: Info;
-}> = ({ style, networkId, info }) => {
- const { width } = useMaxResolution();
- const isSmallScreen = width < 400;
- const { rakkiHistory } = useRakkiHistory(networkId);
- if (!rakkiHistory?.length) {
- return null;
- }
- return (
-
- RAKKi Finished Rounds
-
-
-
- Rounds
-
-
- {rakkiHistory.length}
-
-
- {joinElements(
- rakkiHistory.map((historyItem) => {
- return (
-
-
-
-
-
-
- Drawn{" "}
- {moment(historyItem.date.getTime()).format(
- "MMM D, YYYY, h:mm A",
- )}
-
-
- );
- }),
- ,
- )}
-
-
-
-
-
-
- );
-};
-
-interface HelpBoxDefinition {
- title: string;
- description: string;
-}
-
-const Help: React.FC<{ style?: StyleProp }> = ({ style }) => {
- const helpBoxes: HelpBoxDefinition[] = [
- {
- title: "Buy Tickets",
- description:
- "Prices are $10 USDC per ticket.\nGamblers can buy multiple tickets.",
- },
- {
- title: "Wait for the Draw",
- description:
- "Players just have to wait until the cash prize pool is reached.",
- },
- {
- title: "Check for Prizes",
- description:
- "Once the cashprize pool is reached, the winner receive the $10,000 transaction directly!",
- },
- ];
- return (
-
- How to Play RAKKi
-
- {`When the community lottery pool reaches the 10k USDC amount, only one will be the winner!\nSimple!`}
-
-
-
- minElemWidth={280}
- gap={14}
- keyExtractor={(item) => item.title}
- noFixedHeight
- data={helpBoxes}
- renderItem={({ item, index }, width) => {
- return (
-
-
-
- {item.title}
-
-
- Step {index + 1}
-
-
-
- {item.description}
-
-
- );
- }}
- />
-
-
- );
-};
-
-const GameBox: React.FC<{
- networkId: string;
- info: Info;
- style?: StyleProp;
-}> = ({ networkId, info, style }) => {
- const totalPrizeAmount = Long.fromString(info.config.ticket_price.amount).mul(
- info.current_tickets_count,
- );
- const feePrizeAmount = totalPrizeAmount
- .mul(info.config.fee_per10k)
- .div(10000);
- const winnerPrizeAmount = totalPrizeAmount.sub(feePrizeAmount);
- return (
-
-
-
- Next Draw
-
-
- When the {info.config.max_tickets - info.current_tickets_count}{" "}
- remaining tickets will be sold out.
-
-
-
- Prize Pot
-
-
- ~
- {prettyPrice(
- networkId,
- winnerPrizeAmount.toString(),
- info.config.ticket_price.denom,
- )}
-
-
- ({info.current_tickets_count} TICKETS)
-
-
+
+ {content}
-
- Your tickets
-
-
-
- );
-};
-
-const GetTicketCTA: React.FC<{ info: Info; style?: StyleProp }> = ({
- info,
- style,
-}) => {
- return (
-
- Get your tickets now!
-
-
- {info.config.max_tickets - info.current_tickets_count}
-
-
- tickets
-
-
- remaining
-
-
-
- );
-};
-
-const PrizeInfo: React.FC<{
- info: Info;
- networkId: string;
- style?: StyleProp;
-}> = ({ info, networkId, style }) => {
- const totalPrizeAmount = Long.fromString(info.config.ticket_price.amount).mul(
- info.config.max_tickets,
- );
- const feePrizeAmount = totalPrizeAmount
- .mul(info.config.fee_per10k)
- .div(10000);
- const winnerPrizeAmount = totalPrizeAmount.sub(feePrizeAmount);
- return (
-
-
- Automated Lottery
-
-
- {prettyPrice(
- networkId,
- winnerPrizeAmount.toString(),
- info.config.ticket_price.denom,
- )}
-
-
- in prizes!
-
-
-
- );
-};
-
-const RakkiLogo: React.FC<{ style?: StyleProp }> = ({ style }) => {
- return (
-
-
-
- RAKKi
-
-
-
- );
-};
-
-const RakkiJap: React.FC = () => {
- return (
-
-
- ラ
-
- ッ
-
- キー
-
-
+
);
};
-
-const rakkiJapTextCStyle: TextStyle = {
- textAlign: "center",
- fontSize: 51.933,
- lineHeight: 62.319 /* 120% */,
- letterSpacing: -2.077,
- fontWeight: "600",
-};
-
-const gameBoxLabelCStyle: TextStyle = {
- ...fontSemibold12,
- color: "#777",
- textAlign: "center",
-};
-
-const sectionLabelCStyle: TextStyle = {
- ...fontSemibold28,
- textAlign: "center",
- marginBottom: 12,
-};
diff --git a/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx b/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx
new file mode 100644
index 0000000000..4262f79b0b
--- /dev/null
+++ b/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx
@@ -0,0 +1,53 @@
+import { FC, useState } from "react";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { Box } from "@/components/boxes/Box";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { BuyTicketsModal } from "@/screens/Rakki/components/BuyTickets/BuyTicketsModal";
+import { neutral00, neutralFF, rakkiYellow } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+
+export const BuyTicketsButton: FC<{ networkId: string; info: Info }> = ({
+ networkId,
+ info,
+}) => {
+ const [isButtonHovered, setButtonHovered] = useState(false);
+ const [isModalVisible, setModalVisible] = useState(false);
+
+ return (
+
+ setModalVisible(true)}
+ onHoverIn={() => setButtonHovered(true)}
+ onHoverOut={() => setButtonHovered(false)}
+ >
+
+
+ Buy ッ Tickets
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx b/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx
new file mode 100644
index 0000000000..e516063cd9
--- /dev/null
+++ b/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx
@@ -0,0 +1,335 @@
+import { useQueryClient } from "@tanstack/react-query";
+import Long from "long";
+import { Dispatch, FC, SetStateAction, useEffect, useState } from "react";
+import { TextInput, TextStyle, View } from "react-native";
+
+import rakkiTicketSVG from "@/assets/icons/rakki-ticket.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { Box } from "@/components/boxes/Box";
+import { PrimaryButton } from "@/components/buttons/PrimaryButton";
+import { SecondaryButton } from "@/components/buttons/SecondaryButton";
+import { MainConnectWalletButton } from "@/components/connectWallet/MainConnectWalletButton";
+import { GradientText } from "@/components/gradientText";
+import ModalBase from "@/components/modals/ModalBase";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { Info, RakkiClient } from "@/contracts-clients/rakki";
+import { useBalances } from "@/hooks/useBalances";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { getNetworkFeature, NetworkFeature } from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+import { ModalTicketImage } from "@/screens/Rakki/components/BuyTickets/ModalTicketImage";
+import { prettyPrice } from "@/utils/coins";
+import {
+ errorColor,
+ neutral00,
+ neutral17,
+ neutral22,
+ neutral33,
+ neutral77,
+ neutralA3,
+ neutralFF,
+} from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold16,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { modalMarginPadding } from "@/utils/style/modals";
+
+export const BuyTicketsModal: FC<{
+ visible: boolean;
+ setModalVisible: Dispatch>;
+ info: Info;
+ networkId: string;
+}> = ({ visible, setModalVisible, info, networkId }) => {
+ const selectedWallet = useSelectedWallet();
+ const remainingTickets = info.config.max_tickets - info.current_tickets_count;
+ const [ticketAmount, setTicketAmount] = useState("1");
+ const queryClient = useQueryClient();
+ const ticketAmountNumber = Long.fromString(ticketAmount || "0");
+ useEffect(() => {
+ if (remainingTickets > 0 && ticketAmountNumber.gt(remainingTickets)) {
+ setTicketAmount(remainingTickets.toString());
+ }
+ }, [ticketAmountNumber, remainingTickets]);
+ const totalPrice = ticketAmountNumber.mul(
+ Long.fromString(info.config.ticket_price.amount),
+ );
+ const { balances } = useBalances(networkId, selectedWallet?.address);
+ const ticketDenomBalance =
+ balances.find((b) => b.denom === info.config.ticket_price.denom)?.amount ||
+ "0";
+ const canPay = Long.fromString(ticketDenomBalance).gte(totalPrice);
+ const canBuy = ticketAmountNumber.gt(0) && canPay;
+ const { wrapWithFeedback } = useFeedbacks();
+
+ const prettyTicketPrice = prettyPrice(
+ networkId,
+ info.config.ticket_price.amount,
+ info.config.ticket_price.denom,
+ );
+ const prettyTotalPrice = prettyPrice(
+ networkId,
+ totalPrice.toString(),
+ info.config.ticket_price.denom,
+ );
+ const prettyAvailableBalance = prettyPrice(
+ networkId,
+ ticketDenomBalance,
+ info.config.ticket_price.denom,
+ );
+
+ const onPressBuyTickets = wrapWithFeedback(async () => {
+ if (!selectedWallet?.address) {
+ throw new Error("No wallet with valid address selected");
+ }
+ const cosmWasmClient = await getKeplrSigningCosmWasmClient(networkId);
+ const feature = getNetworkFeature(networkId, NetworkFeature.CosmWasmRakki);
+ if (feature?.type !== NetworkFeature.CosmWasmRakki) {
+ throw new Error("Rakki not supported on this network");
+ }
+ const rakkiClient = new RakkiClient(
+ cosmWasmClient,
+ selectedWallet.address,
+ feature.contractAddress,
+ );
+ const count = ticketAmountNumber.toNumber();
+ const price = {
+ amount: Long.fromString(info.config.ticket_price.amount)
+ .multiply(count)
+ .toString(),
+ denom: info.config.ticket_price.denom,
+ };
+ await rakkiClient.buyTickets(
+ {
+ count,
+ },
+ "auto",
+ undefined,
+ [price],
+ );
+ await Promise.all([
+ queryClient.invalidateQueries(["rakkiInfo", networkId]),
+ queryClient.invalidateQueries([
+ "balances",
+ networkId,
+ selectedWallet.address,
+ ]),
+ queryClient.invalidateQueries(["rakkiHistory", networkId]),
+ ]);
+ setModalVisible(false);
+ });
+
+ return (
+ setModalVisible(false)}
+ >
+
+
+
+
+
+
+
+
+
+ 1 ticket price{" "}
+
+ {prettyTicketPrice}
+
+
+
+
+
+ Number of Lottery Tickets
+
+
+ {
+ if (!newAmount) {
+ setTicketAmount(newAmount);
+ return;
+ }
+ const newAmountNumber = +newAmount;
+ if (isNaN(newAmountNumber)) {
+ return;
+ }
+ if (newAmountNumber > remainingTickets) {
+ return;
+ }
+ setTicketAmount(newAmountNumber.toString());
+ }}
+ style={[
+ fontSemibold16,
+ {
+ paddingLeft: layout.spacing_x2,
+ paddingRight: layout.spacing_x1_25,
+ color: neutralFF,
+ },
+ { outlineStyle: "none" } as TextStyle,
+ ]}
+ />
+
+
+ Total price
+
+
+ {prettyTotalPrice}
+
+
+
+
+
+ {!selectedWallet?.address ? (
+
+ Not connected
+
+ ) : (
+
+ Available Balance{" "}
+
+ {prettyAvailableBalance}
+
+
+ )}
+
+
+
+ setModalVisible(false)}
+ />
+
+ {!selectedWallet?.address ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx b/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx
new file mode 100644
index 0000000000..d0b9d8e844
--- /dev/null
+++ b/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx
@@ -0,0 +1,46 @@
+import { TextStyle, View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { TicketImage } from "@/screens/Rakki/components/TicketImage";
+import { neutral67, neutralA3 } from "@/utils/style/colors";
+import { fontSemibold30 } from "@/utils/style/fonts";
+
+export const ModalTicketImage = () => {
+ return (
+
+
+
+
+
+ ラ
+
+ ッ
+
+ キー
+
+
+
+
+ );
+};
+
+const japaneseTextCStyle: TextStyle = {
+ ...fontSemibold30,
+ textAlign: "center",
+ letterSpacing: 6,
+};
diff --git a/packages/screens/Rakki/components/GameBox.tsx b/packages/screens/Rakki/components/GameBox.tsx
new file mode 100644
index 0000000000..4f58165679
--- /dev/null
+++ b/packages/screens/Rakki/components/GameBox.tsx
@@ -0,0 +1,115 @@
+import Long from "long";
+import { FC } from "react";
+import { StyleProp, View } from "react-native";
+
+import { netCurrentPrizeAmount } from "./../utils";
+
+import { BrandText } from "@/components/BrandText";
+import { Box, BoxStyle } from "@/components/boxes/Box";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { useRakkiTicketsCountByUser } from "@/hooks/rakki/useRakkiTicketsByUser";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { TicketsAndPrice } from "@/screens/Rakki/components/TicketsAndPrice";
+import { gameBoxLabelCStyle } from "@/screens/Rakki/styles";
+import { prettyPrice } from "@/utils/coins";
+import { neutral22, neutral33, neutralA3 } from "@/utils/style/colors";
+import { fontMedium10, fontSemibold12 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const GameBox: FC<{
+ networkId: string;
+ info: Info;
+ style?: StyleProp;
+}> = ({ networkId, info, style }) => {
+ const selectedWallet = useSelectedWallet();
+ const { ticketsCount: userTicketsCount } = useRakkiTicketsCountByUser(
+ selectedWallet?.userId,
+ );
+ const userAmount = userTicketsCount
+ ? Long.fromString(info.config.ticket_price.amount).mul(userTicketsCount)
+ : 0;
+
+ const prettyCurrentPrizeAmount = prettyPrice(
+ networkId,
+ netCurrentPrizeAmount(info),
+ info.config.ticket_price.denom,
+ );
+ const prettyUserTicketsPriceAmount = prettyPrice(
+ networkId,
+ userAmount.toString(),
+ info.config.ticket_price.denom,
+ );
+
+ return (
+
+
+
+ Next Draw
+
+
+ When the {info.config.max_tickets - info.current_tickets_count}{" "}
+ remaining tickets will be sold out.
+
+
+
+ Prize Pot
+
+
+
+ Your tickets
+ {userTicketsCount !== null ? (
+
+ ) : (
+
+ Not connected
+
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/Help.tsx b/packages/screens/Rakki/components/Help.tsx
new file mode 100644
index 0000000000..ecc52a8d57
--- /dev/null
+++ b/packages/screens/Rakki/components/Help.tsx
@@ -0,0 +1,125 @@
+import { FC } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { grossMaxPrizeAmount, netMaxPrizeAmount } from "../utils";
+
+import { BrandText } from "@/components/BrandText";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { GridList } from "@/components/layout/GridList";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { gameBoxLabelCStyle } from "@/screens/Rakki/styles";
+import { prettyPrice } from "@/utils/coins";
+import { neutral33, neutral77 } from "@/utils/style/colors";
+import {
+ fontMedium10,
+ fontSemibold12,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+interface HelpBoxDefinition {
+ title: string;
+ description: string;
+}
+
+export const Help: FC<{
+ info: Info;
+ networkId: string;
+ style?: StyleProp;
+}> = ({ info, style, networkId }) => {
+ const prettyTicketPrice = prettyPrice(
+ networkId,
+ info.config.ticket_price.amount,
+ info.config.ticket_price.denom,
+ );
+ const prettyNetMaxPrize = prettyPrice(
+ networkId,
+ netMaxPrizeAmount(info),
+ info.config.ticket_price.denom,
+ );
+ const prettyMaxPrize = prettyPrice(
+ networkId,
+ grossMaxPrizeAmount(info),
+ info.config.ticket_price.denom,
+ );
+ const feePercent = (info.config.fee_per10k / 10000) * 100;
+
+ const helpBoxes: HelpBoxDefinition[] = [
+ {
+ title: "Buy Tickets",
+ description: `Prices are ${prettyTicketPrice} per ticket.\nGamblers can buy multiple tickets.`,
+ },
+ {
+ title: "Wait for the Draw",
+ description:
+ "Players just have to wait until the cash prize pool is reached.",
+ },
+ {
+ title: "Check for Prizes",
+ description: `Once the cashprize pool is reached, the winner receive the ${prettyNetMaxPrize} transaction directly!`,
+ },
+ ];
+
+ return (
+
+ How to Play RAKKi
+
+ {`When the community lottery pool reaches the ${prettyMaxPrize} amount, only one will be the winner!\nSimple!`}
+
+
+
+ minElemWidth={212}
+ gap={layout.spacing_x1_75}
+ keyExtractor={(item) => item.title}
+ noFixedHeight
+ data={helpBoxes}
+ renderItem={({ item, index }, width) => {
+ return (
+
+
+ {item.title}
+
+ Step {index + 1}
+
+
+
+ {item.description}
+
+
+ );
+ }}
+ />
+
+ *On the total amount, {feePercent}% are sent to a multisig wallet to
+ buyback and burn $TORI token.
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/IntroJapText.tsx b/packages/screens/Rakki/components/IntroJapText.tsx
new file mode 100644
index 0000000000..66b56903e0
--- /dev/null
+++ b/packages/screens/Rakki/components/IntroJapText.tsx
@@ -0,0 +1,34 @@
+import { FC } from "react";
+import { TextStyle, View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { neutral67, neutralFF } from "@/utils/style/colors";
+
+export const IntroJapText: FC = () => {
+ return (
+
+
+ ラ
+
+ ッ
+
+ キー
+
+
+ );
+};
+
+const japaneseTextCStyle: TextStyle = {
+ textAlign: "center",
+ fontSize: 51.933,
+ lineHeight: 62.319 /* 120% */,
+ letterSpacing: -2.077,
+ fontWeight: "600",
+};
diff --git a/packages/screens/Rakki/components/IntroTicketImageButton.tsx b/packages/screens/Rakki/components/IntroTicketImageButton.tsx
new file mode 100644
index 0000000000..619dd19f8b
--- /dev/null
+++ b/packages/screens/Rakki/components/IntroTicketImageButton.tsx
@@ -0,0 +1,29 @@
+import { FC } from "react";
+import { View } from "react-native";
+
+import { Info } from "@/contracts-clients/rakki";
+import { BuyTicketsButton } from "@/screens/Rakki/components/BuyTickets/BuyTicketsButton";
+import { TicketImage } from "@/screens/Rakki/components/TicketImage";
+
+export const IntroTicketImageButton: FC<{
+ networkId: string;
+ info: Info;
+}> = ({ networkId, info }) => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/PrizeInfo.tsx b/packages/screens/Rakki/components/PrizeInfo.tsx
new file mode 100644
index 0000000000..7704f73c43
--- /dev/null
+++ b/packages/screens/Rakki/components/PrizeInfo.tsx
@@ -0,0 +1,61 @@
+import { FC } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { netMaxPrizeAmount } from "../utils";
+
+import { BrandText } from "@/components/BrandText";
+import { GradientText } from "@/components/gradientText";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { IntroTicketImageButton } from "@/screens/Rakki/components/IntroTicketImageButton";
+import { prettyPrice } from "@/utils/coins";
+import {
+ fontSemibold14,
+ fontSemibold20,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const PrizeInfo: FC<{
+ info: Info;
+ networkId: string;
+ style?: StyleProp;
+}> = ({ info, networkId, style }) => {
+ const prettyMaxPrizeAmount = prettyPrice(
+ networkId,
+ netMaxPrizeAmount(info),
+ info.config.ticket_price.denom,
+ );
+
+ return (
+
+
+ Automated Lottery
+
+
+ {prettyMaxPrizeAmount}
+
+
+ in prizes!
+
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/RakkiHistory.tsx b/packages/screens/Rakki/components/RakkiHistory.tsx
new file mode 100644
index 0000000000..a7972c5638
--- /dev/null
+++ b/packages/screens/Rakki/components/RakkiHistory.tsx
@@ -0,0 +1,130 @@
+import moment from "moment";
+import { FC } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { Box } from "@/components/boxes/Box";
+import { UserAvatarWithFrame } from "@/components/images/AvatarWithFrame";
+import { Username } from "@/components/user/Username";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { useRakkiHistory } from "@/hooks/rakki/useRakkiHistory";
+import { useMaxResolution } from "@/hooks/useMaxResolution";
+import { BuyTicketsButton } from "@/screens/Rakki/components/BuyTickets/BuyTicketsButton";
+import { gameBoxLabelCStyle, sectionLabelCStyle } from "@/screens/Rakki/styles";
+import { joinElements } from "@/utils/react";
+import { neutral22, neutral33, neutral77 } from "@/utils/style/colors";
+import { fontMedium10, fontSemibold12 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const RakkiHistory: FC<{
+ style?: StyleProp;
+ networkId: string;
+ info: Info;
+}> = ({ style, networkId, info }) => {
+ const { width } = useMaxResolution();
+ const isSmallScreen = width < 400;
+ const { rakkiHistory } = useRakkiHistory(networkId);
+
+ if (!rakkiHistory?.length) {
+ return null;
+ }
+ return (
+
+ RAKKi Finished Rounds
+
+
+
+ Rounds
+
+
+ {rakkiHistory.length}
+
+
+ {joinElements(
+ rakkiHistory.map((historyItem) => {
+ return (
+
+
+
+
+
+
+ Drawn{" "}
+ {moment(historyItem.date.getTime()).format(
+ "MMM D, YYYY, h:mm A",
+ )}
+
+
+ );
+ }),
+ ,
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/RakkiLogo.tsx b/packages/screens/Rakki/components/RakkiLogo.tsx
new file mode 100644
index 0000000000..2fcc1a6a09
--- /dev/null
+++ b/packages/screens/Rakki/components/RakkiLogo.tsx
@@ -0,0 +1,25 @@
+import { FC } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { IntroJapText } from "@/screens/Rakki/components/IntroJapText";
+
+export const RakkiLogo: FC<{ style?: StyleProp }> = ({ style }) => {
+ return (
+
+
+
+ RAKKi
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/TicketImage.tsx b/packages/screens/Rakki/components/TicketImage.tsx
new file mode 100644
index 0000000000..704540c321
--- /dev/null
+++ b/packages/screens/Rakki/components/TicketImage.tsx
@@ -0,0 +1,15 @@
+import { FC } from "react";
+
+import rakkiTicketImage from "@/assets/logos/rakki-ticket.png";
+import { OptimizedImage } from "@/components/OptimizedImage";
+
+export const TicketImage: FC = () => {
+ return (
+
+ );
+};
diff --git a/packages/screens/Rakki/components/TicketsAndPrice.tsx b/packages/screens/Rakki/components/TicketsAndPrice.tsx
new file mode 100644
index 0000000000..a304e62bfa
--- /dev/null
+++ b/packages/screens/Rakki/components/TicketsAndPrice.tsx
@@ -0,0 +1,36 @@
+import { FC } from "react";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { GradientText } from "@/components/gradientText";
+import { neutralA3 } from "@/utils/style/colors";
+import { fontMedium10, fontSemibold14 } from "@/utils/style/fonts";
+
+export const TicketsAndPrice: FC<{
+ ticketsCount: number;
+ price: string;
+}> = ({ ticketsCount, price }) => {
+ return (
+
+
+ ~{price}
+
+
+ ({ticketsCount} TICKETS)
+
+
+ );
+};
diff --git a/packages/screens/Rakki/components/TicketsRamaining.tsx b/packages/screens/Rakki/components/TicketsRamaining.tsx
new file mode 100644
index 0000000000..10fa0b5a42
--- /dev/null
+++ b/packages/screens/Rakki/components/TicketsRamaining.tsx
@@ -0,0 +1,64 @@
+import { FC } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { Info } from "@/contracts-clients/rakki/Rakki.types";
+import { sectionLabelCStyle } from "@/screens/Rakki/styles";
+import { primaryColor } from "@/utils/style/colors";
+import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const TicketsRemaining: FC<{
+ info: Info;
+ style?: StyleProp;
+}> = ({ info, style }) => {
+ return (
+
+ Get your tickets now!
+
+
+ {info.config.max_tickets - info.current_tickets_count}
+
+
+ tickets
+
+
+ remaining
+
+
+
+ );
+};
diff --git a/packages/screens/Rakki/styles.ts b/packages/screens/Rakki/styles.ts
new file mode 100644
index 0000000000..50cde7636b
--- /dev/null
+++ b/packages/screens/Rakki/styles.ts
@@ -0,0 +1,17 @@
+import { TextStyle } from "react-native";
+
+import { neutral77 } from "@/utils/style/colors";
+import { fontSemibold12, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const sectionLabelCStyle: TextStyle = {
+ ...fontSemibold28,
+ textAlign: "center",
+ marginBottom: layout.spacing_x1_5,
+};
+
+export const gameBoxLabelCStyle: TextStyle = {
+ ...fontSemibold12,
+ color: neutral77,
+ textAlign: "center",
+};
diff --git a/packages/screens/Rakki/utils.ts b/packages/screens/Rakki/utils.ts
new file mode 100644
index 0000000000..d414ebe7e1
--- /dev/null
+++ b/packages/screens/Rakki/utils.ts
@@ -0,0 +1,23 @@
+import Long from "long";
+
+import { Info } from "../../contracts-clients/rakki/Rakki.types";
+
+const grossTicketsPrizeAmount = (info: Info, ticketsCount: number) =>
+ Long.fromString(info.config.ticket_price.amount).mul(ticketsCount);
+
+const netPrizeAmount = (info: Info, ticketsCount: number) => {
+ const feePrizeAmount = grossTicketsPrizeAmount(info, ticketsCount)
+ .mul(info.config.fee_per10k)
+ .div(10000);
+ // Net prize amount
+ return grossTicketsPrizeAmount(info, ticketsCount).sub(feePrizeAmount);
+};
+
+export const netCurrentPrizeAmount = (info: Info) =>
+ netPrizeAmount(info, info.current_tickets_count).toString();
+
+export const netMaxPrizeAmount = (info: Info) =>
+ netPrizeAmount(info, info.config.max_tickets).toString();
+
+export const grossMaxPrizeAmount = (info: Info) =>
+ grossTicketsPrizeAmount(info, info.config.max_tickets).toString();
diff --git a/packages/utils/style/colors.ts b/packages/utils/style/colors.ts
index 22f480d306..4d10c43fdc 100644
--- a/packages/utils/style/colors.ts
+++ b/packages/utils/style/colors.ts
@@ -57,9 +57,7 @@ export const dangerColor = "#E44C39";
export const trashBackground = "rgba(244, 111, 118, 0.1)";
export const orangeLight = "#EAA54B";
-
-export const rakkiYellow = "#FFD83D";
-export const rakkiYellowLight = "#FFEDAE";
+export const rakkiYellow = "#FFDC5F";
export const gradientColorTurquoise = "#A5FECB";
export const gradientColorLightLavender = "#C3CFE2";
@@ -74,6 +72,8 @@ export const gradientColorPink = "#F46FBF";
export const gradientColorGray = "#676767";
export const gradientColorLightGray = "#B7B7B7";
export const gradientColorLighterGray = "#F5F7FA";
+export const gradientColorRakkiYellow = "#FFD83D";
+export const gradientColorRakkiYellowLight = "#FFEDAE";
export const currencyTORIcolor = primaryColor;
export const currencyETHcolor = "#232731";