From 8c5392cc1abe3aaa4054a2bff6241ee8387041b0 Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sat, 9 Sep 2023 06:40:48 +1000 Subject: [PATCH 1/5] Add support for GT and distributing AA to primary account Co-authored-by: loothero --- contracts/game/Scarb.toml | 2 + contracts/game/src/game/interfaces.cairo | 2 +- contracts/game/src/lib.cairo | 106 +++++++++++++---------- contracts/game/src/tests/test_game.cairo | 4 +- 4 files changed, 67 insertions(+), 47 deletions(-) diff --git a/contracts/game/Scarb.toml b/contracts/game/Scarb.toml index 9fbf2ecf9..40e2189b9 100644 --- a/contracts/game/Scarb.toml +++ b/contracts/game/Scarb.toml @@ -10,6 +10,8 @@ market = { path = "../market" } obstacles = { path = "../obstacles" } game_entropy = { path = "../game_entropy" } openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } +goldenToken = { git = "https://github.com/BibliothecaDAO/golden-token" } +arcade_account = { git = "https://github.com/BibliothecaDAO/arcade-account" } [[target.starknet-contract]] allowed-libfuncs-list.name = "experimental" diff --git a/contracts/game/src/game/interfaces.cairo b/contracts/game/src/game/interfaces.cairo index e6c12dac4..7d5f25e36 100644 --- a/contracts/game/src/game/interfaces.cairo +++ b/contracts/game/src/game/interfaces.cairo @@ -12,7 +12,7 @@ use game_entropy::game_entropy::{GameEntropy}; trait IGame { // ------ Game Actions ------ fn new_game( - ref self: TContractState, client_reward_address: ContractAddress, weapon: u8, name: u128 + ref self: TContractState, client_reward_address: ContractAddress, weapon: u8, name: u128, golden_token_id: u256 ); fn explore(ref self: TContractState, adventurer_id: felt252, till_beast: bool); fn attack(ref self: TContractState, adventurer_id: felt252, to_the_death: bool); diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index e8c2b8a1d..d501f2a73 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -26,6 +26,18 @@ mod Game { use openzeppelin::token::erc20::interface::{ IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait, IERC20CamelLibraryDispatcher }; + use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + + use goldenToken::ERC721::{ + GoldenToken, GoldenTokenDispatcher, GoldenTokenDispatcherTrait, GoldenTokenLibraryDispatcher + }; + + use arcade_account::{ + account::interface::{ + IMasterControl, IMasterControlDispatcher, IMasterControlDispatcherTrait + }, + Account, ARCADE_ACCOUNT_ID + }; use super::game::{ interfaces::{IGame}, @@ -74,10 +86,11 @@ mod Game { _game_counter: felt252, _game_entropy: GameEntropy, _genesis_block: u64, + _golden_token: ContractAddress, + _item_specials: LegacyMap::<(felt252, u8), ItemSpecialsStorage>, _leaderboard: Leaderboard, _lords: ContractAddress, _owner: LegacyMap::, - _item_specials: LegacyMap::<(felt252, u8), ItemSpecialsStorage>, } #[event] @@ -115,7 +128,8 @@ mod Game { ref self: ContractState, lords: ContractAddress, dao: ContractAddress, - collectible_beasts: ContractAddress + collectible_beasts: ContractAddress, + golden_token: ContractAddress, ) { // set the contract addresses self._lords.write(lords); @@ -125,6 +139,9 @@ mod Game { // set the genesis block self._genesis_block.write(starknet::get_block_info().unbox().block_number.into()); + // set golden token address + self._golden_token.write(golden_token); + // initialize game entropy let current_block_info = starknet::get_block_info().unbox(); let new_game_entropy = ImplGameEntropy::new( @@ -152,13 +169,23 @@ mod Game { /// @param weapon A u8 representing the weapon to start the game with. Valid options are: {wand: 12, book: 17, short sword: 46, club: 76} /// @param name A u128 value representing the player's name. fn new_game( - ref self: ContractState, client_reward_address: ContractAddress, weapon: u8, name: u128, + ref self: ContractState, + client_reward_address: ContractAddress, + weapon: u8, + name: u128, + golden_token_id: u256 ) { // assert provided weapon _assert_valid_starter_weapon(weapon); - // process payment for game and distribute rewards - _process_payment_and_distribute_rewards(ref self, client_reward_address); + // if player has a golden token + let golden_token = _golden_token_dispatcher(ref self); + if (golden_token.can_play(golden_token_id) && golden_token_id != 0) { + // pay with the golden token + golden_token.play(golden_token_id); + } else { + _process_payment_and_distribute_rewards(ref self, client_reward_address); + } // start the game _start_game(ref self, weapon, name); @@ -211,7 +238,12 @@ mod Game { // process explore or apply idle penalty if !idle { _explore( - ref self, ref adventurer, adventurer_id, adventurer_entropy, game_entropy.hash, till_beast + ref self, + ref adventurer, + adventurer_id, + adventurer_entropy, + game_entropy.hash, + till_beast ); } else { _apply_idle_penalty(ref self, adventurer_id, ref adventurer, num_blocks); @@ -1030,13 +1062,19 @@ mod Game { amount * 10 ^ 18 } + fn _golden_token_dispatcher(ref self: ContractState) -> GoldenTokenDispatcher { + GoldenTokenDispatcher { contract_address: self._golden_token.read() } + } + + fn _lords_dispatcher(ref self: ContractState) -> IERC20CamelDispatcher { + IERC20CamelDispatcher { contract_address: self._lords.read() } + } fn _process_payment_and_distribute_rewards( ref self: ContractState, client_address: ContractAddress ) { let caller = get_caller_address(); let block_number = starknet::get_block_info().unbox().block_number; - let lords = self._lords.read(); let genesis_block = self._genesis_block.read(); let dao_address = self._dao.read(); @@ -1051,7 +1089,7 @@ mod Game { // the purpose of this is to let a decent set of top scores get set before payouts begin // without this, there would be an incentive to start and die immediately after contract is deployed // to capture the rewards from the launch hype - IERC20CamelDispatcher { contract_address: lords } + _lords_dispatcher(ref self) .transferFrom(caller, dao_address, _to_ether(COST_TO_PLAY.into())); __event_RewardDistribution( @@ -1108,53 +1146,22 @@ mod Game { // DAO if (week.DAO != 0) { - IERC20CamelDispatcher { contract_address: lords } - .transferFrom(caller, dao_address, _to_ether(week.DAO)); + _lords_dispatcher(ref self).transferFrom(caller, self._dao.read(), week.DAO); } // interface if (week.INTERFACE != 0) { - IERC20CamelDispatcher { contract_address: lords } - .transferFrom(caller, client_address, _to_ether(week.INTERFACE)); + _lords_dispatcher(ref self).transferFrom(caller, client_address, week.INTERFACE); } // first place - IERC20CamelDispatcher { contract_address: lords } - .transferFrom(caller, first_place_address, _to_ether(week.FIRST_PLACE)); + _lords_dispatcher(ref self).transferFrom(caller, first_place_address, week.FIRST_PLACE); // second place - IERC20CamelDispatcher { contract_address: lords } - .transferFrom(caller, second_place_address, _to_ether(week.SECOND_PLACE)); + _lords_dispatcher(ref self).transferFrom(caller, second_place_address, week.SECOND_PLACE); // third place - IERC20CamelDispatcher { contract_address: lords } - .transferFrom(caller, third_place_address, _to_ether(week.THIRD_PLACE)); - - __event_RewardDistribution( - ref self, - RewardDistribution { - first_place: PlayerReward { - adventurer_id: leaderboard.first.adventurer_id.into(), - rank: 1, - amount: week.FIRST_PLACE, - address: first_place_address - }, - second_place: PlayerReward { - adventurer_id: leaderboard.second.adventurer_id.into(), - rank: 2, - amount: week.SECOND_PLACE, - address: second_place_address - }, - third_place: PlayerReward { - adventurer_id: leaderboard.third.adventurer_id.into(), - rank: 3, - amount: week.THIRD_PLACE, - address: third_place_address - }, - client: ClientReward { amount: week.INTERFACE, address: client_address }, - dao: week.DAO - } - ); + _lords_dispatcher(ref self).transferFrom(caller, third_place_address, week.THIRD_PLACE); } fn _start_game(ref self: ContractState, weapon: u8, name: u128) { @@ -2564,6 +2571,17 @@ mod Game { // get current leaderboard which will be mutated as part of this function let mut leaderboard = self._leaderboard.read(); + // if the player scored a highscore using an Arcade account, we change the owner of the + // adventurer_id they played with to the master account since the arcade account is intended to be disposable. + // By doing this, when rewards are later distributed to that adventurer_id, we'll send them to primary account instead of arcade account + let caller = get_caller_address(); + let account = ISRC5Dispatcher { contract_address: caller }; + if account.supports_interface(ARCADE_ACCOUNT_ID) { + let primary_account = IMasterControlDispatcher { contract_address: caller } + .get_master_account(); + self._owner.write(adventurer_id, primary_account); + }; + // create a score struct for the players score let player_score = Score { adventurer_id: adventurer_id.try_into().unwrap(), diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index 950eff38d..2dea18e2d 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -100,7 +100,7 @@ mod tests { } fn add_adventurer_to_game(ref game: IGameDispatcher) { - game.new_game(INTERFACE_ID(), ItemId::Wand, 'loothero'); + game.new_game(INTERFACE_ID(), ItemId::Wand, 'loothero', 0); let original_adventurer = game.get_adventurer(ADVENTURER_ID); assert(original_adventurer.xp == 0, 'wrong starting xp'); @@ -118,7 +118,7 @@ mod tests { let name = 'abcdefghijklmno'; // start new game - game.new_game(INTERFACE_ID(), starting_weapon, name); + game.new_game(INTERFACE_ID(), starting_weapon, name, 0); // get adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); From 2db3154270f48cfbcf259ebb4bb61f4a3aa41aaa Mon Sep 17 00:00:00 2001 From: loothero Date: Tue, 3 Oct 2023 23:15:32 +0000 Subject: [PATCH 2/5] minor cleanup (squash before merge) --- contracts/game/src/lib.cairo | 53 ++++++++++++++++++------ contracts/game/src/tests/test_game.cairo | 18 +++++++- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index d501f2a73..ff02bf14c 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -126,24 +126,24 @@ mod Game { #[constructor] fn constructor( ref self: ContractState, - lords: ContractAddress, - dao: ContractAddress, - collectible_beasts: ContractAddress, - golden_token: ContractAddress, + lords_address: ContractAddress, + dao_address: ContractAddress, + beasts_address: ContractAddress, + golden_token_address: ContractAddress, ) { // set the contract addresses - self._lords.write(lords); - self._dao.write(dao); - self._collectible_beasts.write(collectible_beasts); + self._lords.write(lords_address); + self._dao.write(dao_address); + self._collectible_beasts.write(beasts_address); - // set the genesis block - self._genesis_block.write(starknet::get_block_info().unbox().block_number.into()); + // set the genesis block number + let current_block_info = starknet::get_block_info().unbox(); + self._genesis_block.write(current_block_info.block_number); // set golden token address - self._golden_token.write(golden_token); + self._golden_token.write(golden_token_address); // initialize game entropy - let current_block_info = starknet::get_block_info().unbox(); let new_game_entropy = ImplGameEntropy::new( current_block_info.block_number, current_block_info.block_timestamp, @@ -180,7 +180,7 @@ mod Game { // if player has a golden token let golden_token = _golden_token_dispatcher(ref self); - if (golden_token.can_play(golden_token_id) && golden_token_id != 0) { + if (golden_token_id != 0 && golden_token.can_play(golden_token_id)) { // pay with the golden token golden_token.play(golden_token_id); } else { @@ -1146,7 +1146,7 @@ mod Game { // DAO if (week.DAO != 0) { - _lords_dispatcher(ref self).transferFrom(caller, self._dao.read(), week.DAO); + _lords_dispatcher(ref self).transferFrom(caller, dao_address, week.DAO); } // interface @@ -1162,6 +1162,33 @@ mod Game { // third place _lords_dispatcher(ref self).transferFrom(caller, third_place_address, week.THIRD_PLACE); + + // emit reward distribution event + __event_RewardDistribution( + ref self, + RewardDistribution { + first_place: PlayerReward { + adventurer_id: leaderboard.first.adventurer_id.into(), + rank: 1, + amount: week.FIRST_PLACE, + address: first_place_address + }, + second_place: PlayerReward { + adventurer_id: leaderboard.second.adventurer_id.into(), + rank: 2, + amount: week.SECOND_PLACE, + address: second_place_address + }, + third_place: PlayerReward { + adventurer_id: leaderboard.third.adventurer_id.into(), + rank: 3, + amount: week.THIRD_PLACE, + address: third_place_address + }, + client: ClientReward { amount: week.INTERFACE, address: client_address }, + dao: week.DAO + } + ); } fn _start_game(ref self: ContractState, weapon: u8, name: u128) { diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index 2dea18e2d..d857665c0 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -51,6 +51,10 @@ mod tests { contract_address_const::<1>() } + fn GOLDEN_TOKEN_ADDRESS() -> ContractAddress { + contract_address_const::<1>() + } + const ADVENTURER_ID: felt252 = 1; const MAX_LORDS: u256 = 500000000000000000000; @@ -84,6 +88,7 @@ mod tests { calldata.append(lords.into()); calldata.append(DAO().into()); calldata.append(COLLECTIBLE_BEASTS().into()); + calldata.append(GOLDEN_TOKEN_ADDRESS().into()); let (address0, _) = deploy_syscall( Game::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false @@ -100,7 +105,9 @@ mod tests { } fn add_adventurer_to_game(ref game: IGameDispatcher) { - game.new_game(INTERFACE_ID(), ItemId::Wand, 'loothero', 0); + let golden_token_id: u256 = 0; + + game.new_game(INTERFACE_ID(), ItemId::Wand, 'loothero', golden_token_id); let original_adventurer = game.get_adventurer(ADVENTURER_ID); assert(original_adventurer.xp == 0, 'wrong starting xp'); @@ -116,9 +123,10 @@ mod tests { let starting_weapon = ItemId::Wand; let name = 'abcdefghijklmno'; + let golden_token_id: u256 = 0; // start new game - game.new_game(INTERFACE_ID(), starting_weapon, name, 0); + game.new_game(INTERFACE_ID(), starting_weapon, name, golden_token_id); // get adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); @@ -455,6 +463,12 @@ mod tests { // let mut game = new_adventurer_lvl11_equipped(5); // } + #[test] + #[available_gas(3000000000000)] + fn test_setup() { + setup(1000); + } + #[test] #[available_gas(300000000000)] fn test_start() { From 2182e23fd2f0349cf175ab4783b358a1d7cfcfc1 Mon Sep 17 00:00:00 2001 From: starknetdev Date: Thu, 5 Oct 2023 15:38:30 +0100 Subject: [PATCH 3/5] - add token image - add token addresses into env (currently 0) - separate balance fetching to separate file - add check if player owns at least one golden token --- ui/.env | 2 ++ ui/public/icons/golden-token.png | Bin 0 -> 26017 bytes ui/src/app/components/ArcadeDialog.tsx | 40 +++++-------------------- ui/src/app/hooks/useContracts.tsx | 6 ++++ ui/src/app/lib/balances.tsx | 35 ++++++++++++++++++++++ ui/src/app/lib/constants.ts | 4 +++ ui/src/app/lib/utils/syscalls.ts | 35 ++++++++++++++++++++++ ui/src/app/page.tsx | 4 ++- 8 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 ui/public/icons/golden-token.png create mode 100644 ui/src/app/lib/balances.tsx diff --git a/ui/.env b/ui/.env index e340c0723..2da6fcf88 100644 --- a/ui/.env +++ b/ui/.env @@ -7,6 +7,8 @@ NEXT_PUBLIC_GOERLI_GAME_CONTRACT_ADDRESS=0x01263ecbc05e28d1e99f531894838db10b90c NEXT_PUBLIC_MAINNET_GAME_CONTRACT_ADDRESS=0x0 NEXT_PUBLIC_GOERLI_LORDS_CONTRACT_ADDRESS=0x059dac5df32cbce17b081399e97d90be5fba726f97f00638f838613d088e5a47 NEXT_PUBLIC_MAINNET_LORDS_CONTRACT_ADDRESS=0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49 +NEXT_PUBLIC_GOERLI_GOLDEN_TOKEN_CONTRACT_ADDRESS=0x0 +NEXT_PUBLIC_MAINNET_GOLDEN_TOKEN_CONTRACT_ADDRESS=0x0 NEXT_PUBLIC_RPC_GOERLI_ENDPOINT=https://starknet-goerli.infura.io/v3/6c536e8272f84d3ba63bf9f248c5e128 NEXT_PUBLIC_RPC_MAINNET_ENDPOINT=https://starknet-mainnet.infura.io/v3/6c536e8272f84d3ba63bf9f248c5e128 NEXT_PUBLIC_GOERLI_APP_URL=https://goerli-survivor.realms.world/ diff --git a/ui/public/icons/golden-token.png b/ui/public/icons/golden-token.png new file mode 100644 index 0000000000000000000000000000000000000000..7589d6fa980efbc977f6068c8eea9037922b8f5e GIT binary patch literal 26017 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 { const { account: walletAccount, address, connector } = useAccount(); @@ -52,43 +49,22 @@ export const ArcadeDialog = () => { ); }, [available]); - const fetchBalanceWithRetry = async ( - accountName: string, - retryCount: number = 0 - ): Promise => { - try { - const result = await ethContract!.call( - "balanceOf", - CallData.compile({ account: accountName }) - ); - return uint256.uint256ToBN(balanceSchema.parse(result).balance); - } catch (error) { - if (retryCount < MAX_RETRIES) { - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); // delay before retry - return fetchBalanceWithRetry(accountName, retryCount + 1); - } else { - throw new Error( - `Failed to fetch balance after ${MAX_RETRIES} retries.` - ); - } - } - }; - const getBalances = async () => { const localBalances: Record = {}; const balancePromises = arcadeConnectors().map((account) => { - return fetchBalanceWithRetry(account.name).then((balance) => { - localBalances[account.name] = balance; - return balance; - }); + return fetchBalanceWithRetry(ethContract!, account.name).then( + (balance) => { + localBalances[account.name] = balance; + return balance; + } + ); }); - console.log(balancePromises); await Promise.all(balancePromises); setBalances(localBalances); }; const getBalance = async (account: string) => { - const balance = await fetchBalanceWithRetry(account); + const balance = await fetchBalanceWithRetry(ethContract!, account); setBalances({ ...balances, [account]: balance }); }; diff --git a/ui/src/app/hooks/useContracts.tsx b/ui/src/app/hooks/useContracts.tsx index 19dd36c10..5a5fba618 100644 --- a/ui/src/app/hooks/useContracts.tsx +++ b/ui/src/app/hooks/useContracts.tsx @@ -81,9 +81,15 @@ export const useContracts = () => { abi: ethBalanceABIFragment, }); + const { contract: goldenTokenContract } = useContract({ + address: contracts?.goldenToken, + abi: Adventurer, // TODO: change to golden token bi + }); + return { gameContract, lordsContract, ethContract, + goldenTokenContract, }; }; diff --git a/ui/src/app/lib/balances.tsx b/ui/src/app/lib/balances.tsx new file mode 100644 index 000000000..30d2e08bf --- /dev/null +++ b/ui/src/app/lib/balances.tsx @@ -0,0 +1,35 @@ +import { Contract, CallData, uint256 } from "starknet"; +import { z } from "zod"; + +const MAX_RETRIES = 10; +const RETRY_DELAY = 2000; // 2 seconds + +export const fetchBalanceWithRetry = async ( + contract: Contract, + accountName: string, + retryCount: number = 0 +): Promise => { + try { + const result = await contract!.call( + "balanceOf", + CallData.compile({ account: accountName }) + ); + return uint256.uint256ToBN(balanceSchema.parse(result).balance); + } catch (error) { + if (retryCount < MAX_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); // delay before retry + return fetchBalanceWithRetry(contract, accountName, retryCount + 1); + } else { + throw new Error(`Failed to fetch balance after ${MAX_RETRIES} retries.`); + } + } +}; + +export const uint256Schema = z.object({ + low: z.bigint(), + high: z.bigint(), +}); + +export const balanceSchema = z.object({ + balance: uint256Schema, +}); diff --git a/ui/src/app/lib/constants.ts b/ui/src/app/lib/constants.ts index 197a650ea..9b53401c0 100644 --- a/ui/src/app/lib/constants.ts +++ b/ui/src/app/lib/constants.ts @@ -39,12 +39,16 @@ export function getContracts() { eth: process.env.NEXT_PUBLIC_GOERLI_ETH_CONTRACT_ADDRESS, game: process.env.NEXT_PUBLIC_GOERLI_GAME_CONTRACT_ADDRESS, lords: process.env.NEXT_PUBLIC_GOERLI_LORDS_CONTRACT_ADDRESS, + goldenToken: + process.env.NEXT_PUBLIC_GOERLI_GOLDEN_TOKEN_CONTRACT_ADDRESS, }; case "mainnet": return { eth: process.env.NEXT_PUBLIC_MAINNET_ETH_CONTRACT_ADDRESS, game: process.env.NEXT_PUBLIC_MAINNET_GAME_CONTRACT_ADDRESS, lords: process.env.NEXT_PUBLIC_MAINNET_LORDS_CONTRACT_ADDRESS, + goldenToken: + process.env.NEXT_PUBLIC_MAINNET_GOLDEN_TOKEN_CONTRACT_ADDRESS, }; } } diff --git a/ui/src/app/lib/utils/syscalls.ts b/ui/src/app/lib/utils/syscalls.ts index c273a9c55..8148345b5 100644 --- a/ui/src/app/lib/utils/syscalls.ts +++ b/ui/src/app/lib/utils/syscalls.ts @@ -4,6 +4,9 @@ import { Account, AccountInterface, provider, + CallData, + Contract, + uint256, } from "starknet"; import { GameData } from "@/app/components/GameData"; import { @@ -19,10 +22,12 @@ import { parseEvents } from "./parseEvents"; import { processNotifications } from "@/app/components/notifications/NotificationHandler"; import Storage from "../storage"; import { FEE_CHECK_BALANCE } from "../constants"; +import { fetchBalanceWithRetry } from "../balances"; export interface SyscallsProps { gameContract: any; lordsContract: any; + goldenTokenContract: Contract; addTransaction: any; queryData: any; resetData: (...args: any[]) => any; @@ -86,6 +91,26 @@ async function checkArcadeBalance( } } +async function checkGoldenToken( + goldenToken: Contract, + account: AccountInterface +) { + const storage: BurnerStorage = Storage.get("burners"); + let accountAddress = ""; + if (account && (account?.address ?? "0x0") in storage) { + const masterAddress = storage[account?.address]["masterAccount"]; + accountAddress = masterAddress; + } else { + accountAddress = account?.address ?? "0x0"; + } + const balance = await fetchBalanceWithRetry(goldenToken, accountAddress); + if (balance >= BigInt(1)) { + return true; + } else { + return false; + } +} + function handleEquip( events: any[], setData: (...args: any[]) => any, @@ -140,6 +165,7 @@ function handleDrop( export function syscalls({ gameContract, lordsContract, + goldenTokenContract, addTransaction, account, queryData, @@ -232,6 +258,15 @@ export function syscalls({ }; const spawn = async (formData: FormData) => { + // First check if the account (master account) has a golden token + const hasGoldenToken = await checkGoldenToken( + goldenTokenContract, + account! + ); + // if (!hasGoldenToken) { + // const approve + // addToCalls() + // } const mintLords = { contractAddress: lordsContract?.address ?? "", entrypoint: "mint", diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 222f8a8e8..20877278e 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -101,7 +101,8 @@ export default function Home() { const showTopUpDialog = useUIStore((state) => state.showTopUpDialog); const setTopUpAccount = useUIStore((state) => state.setTopUpAccount); const setEstimatingFee = useUIStore((state) => state.setEstimatingFee); - const { gameContract, lordsContract, ethContract } = useContracts(); + const { gameContract, lordsContract, ethContract, goldenTokenContract } = + useContracts(); const { addTransaction } = useTransactionManager(); const addToCalls = useTransactionCartStore((state) => state.addToCalls); const handleSubmitCalls = useTransactionCartStore( @@ -133,6 +134,7 @@ export default function Home() { const { spawn, explore, attack, flee, upgrade, multicall } = syscalls({ gameContract, lordsContract, + goldenTokenContract: goldenTokenContract!, addTransaction, queryData: data, resetData, From 34fd9d0abddb2a26cecddb310dcea6e182014166 Mon Sep 17 00:00:00 2001 From: starknetdev Date: Thu, 5 Oct 2023 16:12:13 +0100 Subject: [PATCH 4/5] - add logic to check if golden token owner can play - add logic to check if golden token has game whitelisted - add tx to whitelist game contract --- ui/src/app/lib/{balances.tsx => balances.ts} | 0 ui/src/app/lib/utils/syscalls.ts | 78 +++++++++++++------- 2 files changed, 53 insertions(+), 25 deletions(-) rename ui/src/app/lib/{balances.tsx => balances.ts} (100%) diff --git a/ui/src/app/lib/balances.tsx b/ui/src/app/lib/balances.ts similarity index 100% rename from ui/src/app/lib/balances.tsx rename to ui/src/app/lib/balances.ts diff --git a/ui/src/app/lib/utils/syscalls.ts b/ui/src/app/lib/utils/syscalls.ts index 8148345b5..46f9b1518 100644 --- a/ui/src/app/lib/utils/syscalls.ts +++ b/ui/src/app/lib/utils/syscalls.ts @@ -263,24 +263,56 @@ export function syscalls({ goldenTokenContract, account! ); - // if (!hasGoldenToken) { - // const approve - // addToCalls() - // } - const mintLords = { - contractAddress: lordsContract?.address ?? "", - entrypoint: "mint", - calldata: [formatAddress, (100 * 10 ** 18).toString(), "0"], - }; - addToCalls(mintLords); - - const approveLordsTx = { - contractAddress: lordsContract?.address ?? "", - entrypoint: "approve", - calldata: [gameContract?.address ?? "", (100 * 10 ** 18).toString(), "0"], - }; - addToCalls(approveLordsTx); - + let spawnCalls = []; + if (hasGoldenToken) { + const tokenIds: string[] = []; // TODO: Implement indexer/api fetch to get tokenIds of account + // loop through tokenIds + for (let tokenId in tokenIds) { + const gameApproved = await goldenTokenContract.call( + "can_play", + CallData.compile({ token_id: tokenId }) + ); // check whether player can use the current token + if (gameApproved) { + // check whether the game contract is whitelisted + const gameWhitelisted = await goldenTokenContract.call( + "caller_approved", + CallData.compile({ + token_id: tokenId, + contract: gameContract?.address, + }) + ); + if (!gameWhitelisted) { + const whitelistGameToGoldenTokenTx = { + contractAddress: goldenTokenContract?.address ?? "", + entrypoint: "set_approved_to_call", + calldata: [tokenId, "0", gameContract?.address, "1"], // Approve the game contract for this tokenId + }; + addToCalls(whitelistGameToGoldenTokenTx); + spawnCalls.push(whitelistGameToGoldenTokenTx); + break; + } + } + } + } else { + const mintLords = { + contractAddress: lordsContract?.address ?? "", + entrypoint: "mint", + calldata: [formatAddress, (100 * 10 ** 18).toString(), "0"], + }; + addToCalls(mintLords); + + const approveLordsTx = { + contractAddress: lordsContract?.address ?? "", + entrypoint: "approve", + calldata: [ + gameContract?.address ?? "", + (100 * 10 ** 18).toString(), + "0", + ], + }; + addToCalls(approveLordsTx); + spawnCalls.push(mintLords, approveLordsTx); + } const mintAdventurerTx = { contractAddress: gameContract?.address ?? "", entrypoint: "start", @@ -295,8 +327,9 @@ export function syscalls({ }; addToCalls(mintAdventurerTx); + spawnCalls.push(mintAdventurerTx); const balanceEmpty = await checkArcadeBalance( - [...calls, mintLords, approveLordsTx, mintAdventurerTx], + [...calls, ...spawnCalls], ethBalance, showTopUpDialog, setTopUpAccount, @@ -312,12 +345,7 @@ export function syscalls({ undefined ); try { - const tx = await handleSubmitCalls(account, [ - ...calls, - mintLords, - approveLordsTx, - mintAdventurerTx, - ]); + const tx = await handleSubmitCalls(account, [...calls, ...spawnCalls]); setTxHash(tx?.transaction_hash); addTransaction({ hash: tx?.transaction_hash, From ad273a0f933be08b5f4a79f66c40d3d416061016 Mon Sep 17 00:00:00 2001 From: democritus Date: Fri, 13 Oct 2023 08:59:08 +0200 Subject: [PATCH 5/5] tests --- contracts/game/src/lib.cairo | 50 +++++++++++++++---- contracts/game/src/tests/test_game.cairo | 61 ++++++++++++++++++++---- 2 files changed, 92 insertions(+), 19 deletions(-) diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index 5e267548e..576cdb7b4 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -19,7 +19,8 @@ mod Game { array::{SpanTrait, ArrayTrait}, integer::u256_try_as_non_zero, traits::{TryInto, Into}, clone::Clone, poseidon::poseidon_hash_span, option::OptionTrait, box::BoxTrait, starknet::{ - get_caller_address, ContractAddress, ContractAddressIntoFelt252, contract_address_const + get_caller_address, ContractAddress, ContractAddressIntoFelt252, contract_address_const, + get_block_timestamp }, }; @@ -28,6 +29,10 @@ mod Game { }; use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use openzeppelin::token::erc721::interface::{ + IERC721, IERC721Dispatcher, IERC721DispatcherTrait, IERC721LibraryDispatcher + }; + use goldenToken::ERC721::{ GoldenToken, GoldenTokenDispatcher, GoldenTokenDispatcherTrait, GoldenTokenLibraryDispatcher }; @@ -97,6 +102,7 @@ mod Game { _leaderboard: Leaderboard, _lords: ContractAddress, _owner: LegacyMap::, + _golden_token_last_use: LegacyMap::, } #[event] @@ -185,10 +191,9 @@ mod Game { _assert_valid_starter_weapon(weapon); // if player has a golden token - let golden_token = _golden_token_dispatcher(ref self); - if (golden_token_id != 0 && golden_token.can_play(golden_token_id)) { - // pay with the golden token - golden_token.play(golden_token_id); + // there is no 0 token + if (golden_token_id != 0) { + _play_with_token(ref self, golden_token_id); } else { _process_payment_and_distribute_rewards(ref self, client_reward_address); } @@ -1166,8 +1171,8 @@ mod Game { } } - fn _golden_token_dispatcher(ref self: ContractState) -> GoldenTokenDispatcher { - GoldenTokenDispatcher { contract_address: self._golden_token.read() } + fn _golden_token_dispatcher(ref self: ContractState) -> IERC721Dispatcher { + IERC721Dispatcher { contract_address: self._golden_token.read() } } fn _lords_dispatcher(ref self: ContractState) -> IERC20CamelDispatcher { @@ -1193,8 +1198,7 @@ mod Game { // the purpose of this is to let a decent set of top scores get set before payouts begin // without this, there would be an incentive to start and die immediately after contract is deployed // to capture the rewards from the launch hype - _lords_dispatcher(ref self) - .transferFrom(caller, dao_address, _to_ether(COST_TO_PLAY.into())); + _lords_dispatcher(ref self).transferFrom(caller, dao_address, COST_TO_PLAY.into()); __event_RewardDistribution( ref self, @@ -3343,4 +3347,32 @@ mod Game { fn isMinted(self: @T, beast: u8, prefix: u8, suffix: u8) -> bool; fn getMinter(self: @T) -> ContractAddress; } + + const DAY: felt252 = 86400; + + fn _can_play(self: @ContractState, token_id: u256) -> bool { + _last_usage(self, token_id) + DAY.into() < get_block_timestamp().into() + } + + fn _play_with_token(ref self: ContractState, token_id: u256) { + let golden_token = _golden_token_dispatcher(ref self); + + let account = ISRC5Dispatcher { contract_address: get_caller_address() }; + // let player = if account.supports_interface(ARCADE_ACCOUNT_ID) { + // IMasterControlDispatcher { contract_address: get_caller_address() }.get_master_account() + // } else { + // get_caller_address() + // }; + + assert(_can_play(@self, token_id), 'Cant play'); + assert(golden_token.owner_of(token_id) == account, 'Not owner'); + + self + ._golden_token_last_use + .write(token_id.try_into().unwrap(), get_block_timestamp().into()); + } + + fn _last_usage(self: @ContractState, token_id: u256) -> u256 { + self._golden_token_last_use.read(token_id.try_into().unwrap()).into() + } } diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index 95fb612ee..81193a585 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -14,6 +14,11 @@ mod tests { use openzeppelin::token::erc20::interface::{ IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait, IERC20CamelLibraryDispatcher }; + + use goldenToken::ERC721::{ + GoldenToken, GoldenTokenDispatcher, GoldenTokenDispatcherTrait, GoldenTokenLibraryDispatcher + }; + use openzeppelin::token::erc20::erc20::ERC20; use market::market::{ImplMarket, LootWithPrice, ItemPurchase}; use lootitems::{loot::{Loot, ImplLoot, ILoot}, constants::{ItemId}}; @@ -65,42 +70,65 @@ mod tests { contract_address_const::<10>() } - fn deploy_lords() -> ContractAddress { + fn deploy_erc20(symbol: felt252) -> ContractAddress { let mut calldata = array![]; calldata.append_serde(NAME); - calldata.append_serde(SYMBOL); + calldata.append_serde(symbol); calldata.append_serde(MAX_LORDS); calldata.append_serde(OWNER()); - let lords0 = utils::deploy(CamelERC20Mock::TEST_CLASS_HASH, calldata); + utils::deploy(CamelERC20Mock::TEST_CLASS_HASH, calldata) + } - lords0 + fn deploy_golden_token(eth: ContractAddress) -> (ContractAddress, GoldenTokenDispatcher) { + let mut calldata = ArrayTrait::new(); + calldata.append(NAME); + calldata.append(SYMBOL); + calldata.append(OWNER().into()); + calldata.append(DAO().into()); + calldata.append(eth.into()); + + let (golden_token, _) = deploy_syscall( + goldenToken::ERC721::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + + (golden_token, GoldenTokenDispatcher { contract_address: golden_token }) } fn setup(starting_block: u64) -> IGameDispatcher { testing::set_block_number(starting_block); testing::set_block_timestamp(1696201757); - let lords = deploy_lords(); + let lords = deploy_erc20('LORDS'); + let eth = deploy_erc20('ETH'); + + let (golden_token_address, golden_token_dispatcher) = deploy_golden_token(eth); let mut calldata = ArrayTrait::new(); calldata.append(lords.into()); calldata.append(DAO().into()); calldata.append(COLLECTIBLE_BEASTS().into()); - calldata.append(GOLDEN_TOKEN_ADDRESS().into()); + calldata.append(golden_token_address.into()); - let (address0, _) = deploy_syscall( + let (game_address, _) = deploy_syscall( Game::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false ) .unwrap(); testing::set_contract_address(OWNER()); - let lordsContract = IERC20CamelDispatcher { contract_address: lords }; + let lords_contract = IERC20CamelDispatcher { contract_address: lords }; + lords_contract.approve(game_address, APPROVE.into()); - lordsContract.approve(address0, APPROVE.into()); + let eth_contract = IERC20CamelDispatcher { contract_address: eth }; + eth_contract.approve(golden_token_address, APPROVE.into()); - IGameDispatcher { contract_address: address0 } + golden_token_dispatcher.open(); + golden_token_dispatcher.mint(); + golden_token_dispatcher.mint(); + + IGameDispatcher { contract_address: game_address } } fn add_adventurer_to_game(ref game: IGameDispatcher) { @@ -1869,4 +1897,17 @@ mod tests { game.attack(ADVENTURER_ID, false); game.attack(ADVENTURER_ID, false); } + + #[test] + #[available_gas(944417814)] + fn test_golden_token_mint() { + let mut game = setup(1000); + + let starting_weapon = ItemId::Wand; + let name = 'abcdefghijklmno'; + let golden_token_id: u256 = 1; + + // start new game + game.new_game(INTERFACE_ID(), starting_weapon, name, golden_token_id); + } }