From d3918a51fe93d507c0dcd9eb7a27c35a6f57a829 Mon Sep 17 00:00:00 2001 From: 0xjaqbek Date: Thu, 25 Jul 2024 13:18:04 +0200 Subject: [PATCH] c1 --- src/LeaderboardPage.tsx | 6 +- src/contracts/race.fc | 154 ++++++++++++++++++++++++++++++++++++++++ src/hooks/useRace.ts | 68 ++++++++++++++++++ 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/contracts/race.fc create mode 100644 src/hooks/useRace.ts diff --git a/src/LeaderboardPage.tsx b/src/LeaderboardPage.tsx index b6837c8..5655258 100644 --- a/src/LeaderboardPage.tsx +++ b/src/LeaderboardPage.tsx @@ -250,9 +250,11 @@ const LeaderboardPage: React.FC = ({ elapsedTime, onClose, {showSaveScoreWindow && ( -

Connect your wallet to set user Id and save your score:

+

Connect your wallet to play OnChain:

- Save Score + + console.log("OnChain Race clicked")} style={{ marginTop: '10px' }} disabled={!rawAddress}> OnChain Race + Save Score

Close
diff --git a/src/contracts/race.fc b/src/contracts/race.fc new file mode 100644 index 0000000..60a7652 --- /dev/null +++ b/src/contracts/race.fc @@ -0,0 +1,154 @@ +const int OP_JOIN_RACE = 1; +const int OP_SUBMIT_TIME = 2; +const int OP_FINALIZE_RACE = 3; +const int OP_DISTRIBUTE_PRIZE = 5; +const int OP_REFUND_ENTRY_FEE = 6; +const int OP_WITHDRAW_FUNDS = 7; + +() recv_internal(int my_balance, cell in_msg, slice in_msg_body) impure { + slice cs = in_msg_body; + int op = cs~load_uint(32); + + ;; Contract state variables + int race_start_time; + map participants; ;; Map to store participant addresses, join times, and their race times + slice owner_address; + bool race_finalized = false; + int total_entry_fees = 0; ;; Track total entry fees collected + int participant_entry_fee = 1000000000; ;; 1 TON = 1000000000 nanoton + + ;; Load contract state from persistent storage + slice data = get_data(); + if (data.slice_refs() > 0) { + (race_start_time, participants, owner_address, race_finalized, total_entry_fees) = data~load_tuple(); + } + + ;; Join Race + if (op == OP_JOIN_RACE) { + slice participant_address = cs~load_msg_addr(); + int current_time = now(); + + if (race_start_time == 0) { + race_start_time = current_time; + } + + tuple participant_info = (current_time, []); + participants = participants.set(participant_address, participant_info); + + total_entry_fees += participant_entry_fee; ;; Add entry fee to total + send_raw_message(build_internal_message(participant_address, participant_entry_fee, false, null()), 128); + } + + ;; Submit Time + if (op == OP_SUBMIT_TIME) { + slice participant_address = cs~load_msg_addr(); + int time = cs~load_uint(64); + int current_time = now(); + + tuple participant_info = participants[participant_address]; + int join_time = participant_info.get(0); + cell times = participant_info.get(1); + + if (current_time <= join_time + 20 * 60) { ;; 20 minutes after join time + if (times.size() < 3) { + times = times.push(time); + participants = participants.set(participant_address, (join_time, times)); + } else { + throw(101); ;; "You can only submit up to 3 times." + } + + send_raw_message(build_internal_message(participant_address, 0.002 ton, false, null()), 128); + } else { + throw(102); ;; "Time to submit results has expired." + } + } + + ;; Finalize Race + if (op == OP_FINALIZE_RACE) { + int current_time = now(); + int finalization_time = race_start_time + 3600; ;; 1 hour after race start + + if (current_time > finalization_time) { + race_finalized = true; + } else { + throw(103); ;; "Race finalization time has not ended yet." + } + } + + ;; Distribute Prize + if (op == OP_DISTRIBUTE_PRIZE) { + if (!race_finalized) { + throw(104); ;; "Race has not been finalized yet." + } + + slice participant_address = cs~load_msg_addr(); + slice winner_address; + int best_time = 9223372036854775807; ;; Maximum 64-bit signed integer value + int winner_entry_fee = 0; + + participants.each { (slice addr, tuple participant_info) => + cell times = participant_info.get(1); + int best_participant_time = times.min(); + if (best_participant_time < best_time) { + best_time = best_participant_time; + winner_address = addr; + winner_entry_fee = participant_entry_fee; + } + } + + if (participant_address == winner_address) { + ;; Calculate the prize: 50% of winner's entry fee + 95% of others' entry fees + int prize_amount = (winner_entry_fee / 2) + ((95 * (total_entry_fees - winner_entry_fee)) / 100); + + ;; Add a small portion (gas fee) to be left in the contract + int gas_fee = 2000000; + int final_amount = prize_amount - gas_fee; + + send_raw_message(build_internal_message(winner_address, final_amount, false, null()), 128); + } else { + throw(105); ;; "Only the winner can claim the prize." + } + } + + ;; Refund Entry Fee + if (op == OP_REFUND_ENTRY_FEE) { + slice participant_address = cs~load_msg_addr(); + int current_time = now(); + int join_end_time = race_start_time + 40 * 60; ;; 40 minutes after race start + + if (current_time > join_end_time && participants.size() == 1) { + ;; Return entry fee (1 TON) + int entry_fee = participant_entry_fee; + send_raw_message(build_internal_message(participant_address, entry_fee, false, null()), 128); + } else { + throw(106); ;; "Refund is not allowed." + } + } + + ;; Withdraw Funds by Owner + if (op == OP_WITHDRAW_FUNDS) { + slice sender_address = in_msg~load_msg_addr(); + if (sender_address != owner_address) { + throw(107); ;; "Only the owner can withdraw funds." + } + + ;; Subtract the gas fee from the current balance to calculate withdrawal amount + int gas_fee = 2000000; + int withdrawal_amount = my_balance - gas_fee; + + ;; Ensure the withdrawal amount is non-negative + if (withdrawal_amount < 0) { + throw(108); ;; "Insufficient balance for withdrawal." + } + + ;; Send the remaining balance to the owner + send_raw_message(build_internal_message(owner_address, withdrawal_amount, false, null()), 128); + } + + ;; Save contract state + set_data(store_tuple((race_start_time, participants, owner_address, race_finalized, total_entry_fees))); +} + +() recv_external(int my_balance, cell in_msg, slice in_msg_body) impure { + throw(100); ;; External messages not supported +} diff --git a/src/hooks/useRace.ts b/src/hooks/useRace.ts new file mode 100644 index 0000000..bcdd66c --- /dev/null +++ b/src/hooks/useRace.ts @@ -0,0 +1,68 @@ +import { useState, useEffect } from "react"; +import Race from "../contracts/race"; +import { useTonClient } from "./useTonClient"; +import { useAsyncInitialize } from "./useAsyncInitialize"; +import { useTonConnect } from "./useTonConnect"; +import { Address, OpenedContract } from "ton-core"; +import { useQuery } from "@tanstack/react-query"; +import { CHAIN } from "@tonconnect/protocol"; + +export function useRaceContract() { + const { client } = useTonClient(); + const { sender, network } = useTonConnect(); + + const raceContract = useAsyncInitialize(async () => { + if (!client) return; + const owner = Address.parse("YourOwnerAddressHere"); // Replace with the owner's address + const contract = new Race( + Address.parse( + network === CHAIN.MAINNET + ? "EQBpRaceMainNetAddressHere" // Replace with mainnet address + : "EQBraceTestNetAddressHere" // Replace with testnet address + ), + owner + ); + return client.open(contract) as OpenedContract; + }, [client]); + + const { data: raceStartTime, refetch: refetchRaceStartTime } = useQuery( + ["raceStartTime"], + async () => { + if (!raceContract) return null; + return raceContract.getRaceStartTime(); + }, + { refetchInterval: 3000 } + ); + + const { data: participants, refetch: refetchParticipants } = useQuery( + ["participants"], + async () => { + if (!raceContract) return null; + // Fetch and return participants (you may need to implement this method in Race contract) + }, + { refetchInterval: 3000 } + ); + + const { data: winner, refetch: refetchWinner } = useQuery( + ["winner"], + async () => { + if (!raceContract) return null; + return raceContract.getWinner(); + }, + { refetchInterval: 3000 } + ); + + return { + raceStartTime, + participants, + winner, + joinRace: () => raceContract?.joinRace(sender), + submitTime: (time: number) => raceContract?.submitTime(sender, time), + finalizeRace: () => raceContract?.finalizeRace(sender), + distributePrize: () => raceContract?.distributePrize(sender), + refundEntryFee: () => raceContract?.refundEntryFee(sender), + refetchRaceStartTime, + refetchParticipants, + refetchWinner, + }; +}