diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml new file mode 100644 index 00000000..98f41b2c --- /dev/null +++ b/.github/workflows/demo.yaml @@ -0,0 +1,64 @@ +name: scaffold-stark-demo workflow + +on: + pull_request: + types: [closed] + branches: [main] + paths: + - 'packages/nextjs/**' + +jobs: + version-bump-nextjs: + runs-on: ubuntu-22.04 + steps: + + - name: Checkout Source Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/scaffold-stark-2 + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: source_repo + + - name: Modify scaffoldConfig in Source Repository + run: | + cd source_repo + sed -i 's/targetNetworks: \[chains.devnet\]/targetNetworks: \[chains.sepolia\]/' packages/nextjs/scaffold.config.ts + cat packages/nextjs/scaffold.config.ts + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://registry.yarnpkg.com' + + - name: Deploy to vercel + if: success() + id: deploy + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + cd source_repo + yarn install + vercel link --yes --project $VERCEL_PROJECT_ID --token $VERCEL_TOKEN --scope $VERCEL_ORG_ID + vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --prod --token $VERCEL_TOKEN --scope $VERCEL_ORG_ID + + - name: Notify Slack on Success + if: success() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub deployed to vercel result: ${{ job.status }}\nRepository Name: ${{ github.repository }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Notify Slack on Failure + if: failure() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub deployed to vercel result: ${{ job.status }}\nRepository Name: ${{ github.repository }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4c4ea6e0..748006d3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -34,12 +34,6 @@ jobs: run: yarn install --immutable working-directory: ./packages/nextjs - - name: Install scarb - run: curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.5.4 - - - name: Check Code Format - run: npm run format:check - - name: Run Next.js lint run: yarn next:lint --max-warnings=0 working-directory: ./packages/nextjs diff --git a/.github/workflows/release-create-stark.yaml b/.github/workflows/release-create-stark.yaml new file mode 100644 index 00000000..47c698cc --- /dev/null +++ b/.github/workflows/release-create-stark.yaml @@ -0,0 +1,114 @@ +name: Version Bump and Notify + +on: + pull_request: + types: [closed] + branches: [main] + +jobs: + version-bump: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout Source Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/scaffold-stark-2 + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: source_repo + + - name: Checkout Destination Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/create-stark + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: destination_repo + + - name: Determine version bump type + id: version + run: | + cd source_repo + commit_message=$(git log -1 --pretty=%B) + if [[ "$commit_message" == *"[major]"* ]]; then + echo "type=major" >> "$GITHUB_ENV" + elif [[ "$commit_message" == *"[minor]"* ]]; then + echo "type=minor" >> "$GITHUB_ENV" + else + echo "type=patch" >> "$GITHUB_ENV" + fi + + - name: Bump version in Source Repository + id: bump-version-source + run: | + cd source_repo + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + new_version=$(npm version ${{ env.type }} -m "chore(release): %s [skip ci]") + echo "NEW_VERSION=${new_version}" >> "$GITHUB_ENV" + git push origin main --follow-tags + + - name: Copy Files to Destination Repository + run: | + rsync -av --delete --exclude='.git' source_repo/ destination_repo/templates/base + cd destination_repo + git add . + git commit -m "chore: sync files from scaffold-stark-2 [skip ci]" + + - name: Format .gitignore files + run: | + find destination_repo/templates/base -type f -name ".gitignore" | while read -r gitignore_file; do + mjs_file="${gitignore_file%/*}/.gitignore.template.mjs" + gitignore_content=$(cat "$gitignore_file") + cat > "$mjs_file" <<-EOF + const contents = () => + \`${gitignore_content}\` + + export default contents; + EOF + rm "$gitignore_file" + done + cd destination_repo + git add . + git commit -m "Processed $gitignore_file into $mjs_file" + + - name: Bump version in Destination Repository + id: bump-version-destination + run: | + cd destination_repo + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + new_version=$(npm version ${{ env.type }} -m "chore(release): %s [skip ci]") + git push origin main --follow-tags + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org/' + + - name: Publish release + if: success() + id: publish-release + run: | + cd destination_repo + npm install && npm run build && npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Notify Slack on Success + if: success() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub Action succeeded for version bump to ${{ env.NEW_VERSION }}." + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Notify Slack on Failure + if: failure() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub Action failed for version bump." + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/README.md b/README.md index 447bcc92..c10e5ee4 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ Before you begin, you need to install the following tools: ### Compatible versions -- Scarb - v2.5.4 -- Snforge - v0.25 -- Cairo - v2.5.4 +- Scarb - v2.6.5 +- Snforge - v0.27 +- Cairo - v2.6.4 Make sure you have the compatible versions otherwise refer to [Scaffold-Stark Requirements](https://github.com/Quantum3-Labs/scaffold-stark-2?.tab=readme-ov-file#requirements) @@ -36,7 +36,7 @@ Then download the challenge to your computer and install dependencies by running ```sh -git clone https://github.com/Quantum3-Labs/speedrunstark.git --recurse-submodules dice-game +git clone https://github.com/Quantum3-Labs/speedrunstark.git dice-game cd dice-game git checkout dice-game @@ -104,7 +104,7 @@ Next add a `riggedRoll()` function. This function should predict the randomness - [ ] Uncomment the code in `packages/nextjs/app/dice/page.tsx` to show a riggedRoll button and contract balance on the main UI tab. Now you can test your function without switching tabs. - [ ] Does your riggedRoll function only call `rollTheDice()` when it's going to be a winning roll? What happens when it does call `rollTheDice()`? -![RiggedLosingRoll](https://raw.githubusercontent.com/Quantum3-Labs/speedrunstark/gabi/dice-game/packages/nextjs/public/ch3-roll.png) +![RiggedLosingRoll](./packages/nextjs/public/ch3-roll.png) --- @@ -125,7 +125,7 @@ You have beaten the game, but where is your money? Since the RiggedRoll contract - [ ] Lock the withdraw function so it can only be called by the owner. -![WithdrawOnlyOwner](https://raw.githubusercontent.com/Quantum3-Labs/speedrunstark/gabi/dice-game/packages/nextjs/public/ch3-debug.png) +![WithdrawOnlyOwner](./packages/nextjs/public/ch3-debug.png) > ⚠️ But wait, I am not the owner! You will want to set your front end address as the owner in `deploy.ts`. This will allow your front end address to call the withdraw function. diff --git a/package.json b/package.json index e21e893e..bfc7b50a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ss-2", - "version": "0.2.3", + "version": "0.2.6", "author": "Q3 Labs", "license": "MIT", "private": true, @@ -13,6 +13,7 @@ "scripts": { "chain": "yarn workspace @ss-2/snfoundry chain", "deploy": "yarn workspace @ss-2/snfoundry deploy", + "deploy:reset": "yarn workspace @ss-2/snfoundry deploy:reset", "test": "yarn workspace @ss-2/snfoundry test", "compile": "yarn workspace @ss-2/snfoundry compile", "start": "yarn workspace @ss-2/nextjs dev", diff --git a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx index 3d6d1010..c713c7e4 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx @@ -10,6 +10,7 @@ import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; import { ContractName } from "~~/utils/scaffold-stark/contract"; import { ContractVariables } from "./ContractVariables"; import { ContractWriteMethods } from "./ContractWriteMethods"; +import { ClassHash } from "~~/components/scaffold-stark/ClassHash"; type ContractUIProps = { contractName: ContractName; @@ -58,6 +59,10 @@ export const ContractUI = ({
{contractName}
+
Balance: )}
+
{ return ( <> + {/*
+

Debug Contracts

+

+ You can debug & interact with your deployed contracts here. +
Check{" "} + + packages / nextjs / app / debug / page.tsx + {" "} +

+
*/} ); }; diff --git a/packages/nextjs/components/Footer.tsx b/packages/nextjs/components/Footer.tsx index 31f75fea..2b1d5990 100644 --- a/packages/nextjs/components/Footer.tsx +++ b/packages/nextjs/components/Footer.tsx @@ -1,15 +1,13 @@ import React from "react"; -import Link from "next/link"; -import { - CurrencyDollarIcon, - MagnifyingGlassIcon, -} from "@heroicons/react/24/outline"; +import { CurrencyDollarIcon } from "@heroicons/react/24/outline"; import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; import { useGlobalState } from "~~/services/store/store"; -import { devnet } from "@starknet-react/chains"; -import { Faucet } from "./scaffold-stark"; -import { getBlockExplorerLink } from "~~/utils/scaffold-stark"; +import { devnet, sepolia, mainnet } from "@starknet-react/chains"; +import { Faucet } from "~~/components/scaffold-stark/Faucet"; +import { FaucetSepolia } from "~~/components/scaffold-stark/FaucetSepolia"; +import { BlockExplorerSepolia } from "./scaffold-stark/BlockExplorerSepolia"; +import { BlockExplorer } from "./scaffold-stark/BlockExplorer"; /** * Site footer @@ -20,12 +18,14 @@ export const Footer = () => { ); const { targetNetwork } = useTargetNetwork(); const isLocalNetwork = targetNetwork.id === devnet.id; + const isSepoliaNetwork = targetNetwork.id === sepolia.id; + const isMainnetNetwork = targetNetwork.id === mainnet.id; return (
-
-
+
+
{nativeCurrencyPrice > 0 && (
@@ -34,27 +34,28 @@ export const Footer = () => {
)} + {isSepoliaNetwork && ( + <> + + + + )} {isLocalNetwork && ( <> - - - Block Explorer - + + )} + {isMainnetNetwork && ( + <> + )}
-
-
    -
    +
    +
      +
      · + {/*
      +

      + Built with by +

      + + + Q3 Labs + +

      at

      + + + BuidlGuidl + +
      + · */}
      { export const Header = () => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); const burgerMenuRef = useRef(null); - useOutsideClick( burgerMenuRef, useCallback(() => setIsDrawerOpen(false), []), ); const { targetNetwork } = useTargetNetwork(); const isLocalNetwork = targetNetwork.id === devnet.id; + const { provider } = useProvider(); const { address, status } = useAccount(); const [isDeployed, setIsDeployed] = useState(true); diff --git a/packages/nextjs/components/MenuItem/MenuItem.tsx b/packages/nextjs/components/MenuItem/MenuItem.tsx deleted file mode 100644 index abc66fe5..00000000 --- a/packages/nextjs/components/MenuItem/MenuItem.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Link from "next/link"; -import React from "react"; -import { HeaderMenuLink } from "~~/components/Header"; - -interface MenuItemProps { - link: HeaderMenuLink; - isActive: boolean; -} - -const MenuItem: React.FC = ({ link, isActive }) => { - //console.log({ isActive }, link.label); - return ( -
    • - - {link.icon} - {link.label} - -
    • - ); -}; - -export default MenuItem; diff --git a/packages/nextjs/components/scaffold-stark/Balance.tsx b/packages/nextjs/components/scaffold-stark/Balance.tsx index f4cd43a2..43a15f91 100644 --- a/packages/nextjs/components/scaffold-stark/Balance.tsx +++ b/packages/nextjs/components/scaffold-stark/Balance.tsx @@ -5,6 +5,7 @@ import { Address } from "@starknet-react/chains"; import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; import useScaffoldEthBalance from "~~/hooks/scaffold-stark/useScaffoldEthBalance"; import { useGlobalState } from "~~/services/store/store"; +import useScaffoldStrkBalance from "~~/hooks/scaffold-stark/useScaffoldStrkBalance"; type BalanceProps = { address?: Address; @@ -17,21 +18,36 @@ type BalanceProps = { */ export const Balance = ({ address, className = "", usdMode }: BalanceProps) => { const price = useGlobalState((state) => state.nativeCurrencyPrice); + const strkPrice = useGlobalState((state) => state.strkCurrencyPrice); const { targetNetwork } = useTargetNetwork(); const { formatted, isLoading, isError } = useScaffoldEthBalance({ address, }); + const { + formatted: strkFormatted, + isLoading: strkIsLoading, + isError: strkIsError, + symbol: strkSymbol, + } = useScaffoldStrkBalance({ + address, + }); const [displayUsdMode, setDisplayUsdMode] = useState( price > 0 ? Boolean(usdMode) : false, ); const toggleBalanceMode = () => { - if (price > 0) { + if (price > 0 || strkPrice > 0) { setDisplayUsdMode((prevMode) => !prevMode); } }; - if (!address || isLoading || formatted === null) { + if ( + !address || + isLoading || + formatted === null || + strkIsLoading || + strkFormatted === null + ) { return (
      @@ -52,33 +68,49 @@ export const Balance = ({ address, className = "", usdMode }: BalanceProps) => { ); } - //const formattedBalance = balance ? Number(balance.formatted) : 0; + // Calculate the total balance in USD + const ethBalanceInUsd = parseFloat(formatted) * price; + const strkBalanceInUsd = parseFloat(strkFormatted) * strkPrice; + const totalBalanceInUsd = ethBalanceInUsd + strkBalanceInUsd; return ( - + <> + + ); }; diff --git a/packages/nextjs/components/scaffold-stark/BlockExplorer.tsx b/packages/nextjs/components/scaffold-stark/BlockExplorer.tsx new file mode 100644 index 00000000..68ca23c8 --- /dev/null +++ b/packages/nextjs/components/scaffold-stark/BlockExplorer.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Address as AddressType, mainnet } from "@starknet-react/chains"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useNetwork } from "@starknet-react/core"; +import Image from "next/image"; + +export const BlockExplorer = () => { + const { chain: ConnectedChain } = useNetwork(); + + const blockExplorers = [ + { + name: "Starkscan", + img: "/sn-symbol-gradient.png", + link: "https://starkscan.co/", + }, + { + name: "Voyager", + img: "/voyager-icon.svg", + link: "https://voyager.online/", + }, + { + name: "Stark Compass", + img: "/starkcompass-icon.svg", + link: "https://starkcompass.com/", + }, + ]; + + // Render only on mainnet chain + if (ConnectedChain?.id !== mainnet.id) { + return null; + } + + return ( +
      + ); +}; diff --git a/packages/nextjs/components/scaffold-stark/BlockExplorerSepolia.tsx b/packages/nextjs/components/scaffold-stark/BlockExplorerSepolia.tsx new file mode 100644 index 00000000..bad4d101 --- /dev/null +++ b/packages/nextjs/components/scaffold-stark/BlockExplorerSepolia.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Address as AddressType, sepolia } from "@starknet-react/chains"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useNetwork } from "@starknet-react/core"; +import Image from "next/image"; + +export const BlockExplorerSepolia = () => { + const { chain: ConnectedChain } = useNetwork(); + + const sepoliaBlockExplorers = [ + { + name: "Starkscan", + img: "/sn-symbol-gradient.png", + link: "https://sepolia.starkscan.co/", + }, + { + name: "Voyager", + img: "/voyager-icon.svg", + link: "https://sepolia.voyager.online/", + }, + { + name: "Stark Compass", + img: "/starkcompass-icon.svg", + link: "https://starkcompass.com/sepolia/", + }, + ]; + + // Render only on sepolia chain + if (ConnectedChain?.id !== sepolia.id) { + return null; + } + + return ( +
      + + + +
      + ); +}; diff --git a/packages/nextjs/components/scaffold-stark/ClassHash.tsx b/packages/nextjs/components/scaffold-stark/ClassHash.tsx new file mode 100644 index 00000000..32b3e501 --- /dev/null +++ b/packages/nextjs/components/scaffold-stark/ClassHash.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { Address as AddressType } from "@starknet-react/chains"; +import { devnet } from "@starknet-react/chains"; +import { + CheckCircleIcon, + DocumentDuplicateIcon, +} from "@heroicons/react/24/outline"; +import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; +import { getBlockExplorerClasshashLink } from "~~/utils/scaffold-stark"; + +type ClasshashProps = { + classHash: AddressType; + format?: "short" | "long"; + size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; +}; + +/** + * Displays a Classhash and option to copy classHash. + */ +export const ClassHash = ({ + classHash, + format, + size = "xs", +}: ClasshashProps) => { + const [addressCopied, setAddressCopied] = useState(false); + const { targetNetwork } = useTargetNetwork(); + + const blockExplorerAddressLink = getBlockExplorerClasshashLink( + targetNetwork, + classHash, + ); + + let displayClasshash = classHash?.slice(0, 6) + "..." + classHash?.slice(-4); + + if (format === "long") { + displayClasshash = classHash; + } + + return ( +
      +
      + class hash: +
      + {targetNetwork.network === devnet.network ? ( + + {displayClasshash} + + ) : ( + + {displayClasshash} + + )} + {addressCopied ? ( +
      + ); +}; diff --git a/packages/nextjs/components/scaffold-stark/CustomConnectButton/index.tsx b/packages/nextjs/components/scaffold-stark/CustomConnectButton/index.tsx index 4b9135e9..9f170919 100644 --- a/packages/nextjs/components/scaffold-stark/CustomConnectButton/index.tsx +++ b/packages/nextjs/components/scaffold-stark/CustomConnectButton/index.tsx @@ -12,12 +12,14 @@ import { useAccount, useNetwork } from "@starknet-react/core"; import { Address } from "@starknet-react/chains"; import { useState } from "react"; import ConnectModal from "./ConnectModal"; +import { useTheme } from "next-themes"; /** * Custom Connect Button (watch balance + custom design) */ export const CustomConnectButton = () => { useAutoConnect(); + const { theme } = useTheme(); const networkColor = useNetworkColor(); const { targetNetwork } = useTargetNetwork(); const { address, status, chainId, ...props } = useAccount(); diff --git a/packages/nextjs/components/scaffold-stark/FaucetSepolia.tsx b/packages/nextjs/components/scaffold-stark/FaucetSepolia.tsx new file mode 100644 index 00000000..0940a228 --- /dev/null +++ b/packages/nextjs/components/scaffold-stark/FaucetSepolia.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Address as AddressType, sepolia } from "@starknet-react/chains"; +import { BanknotesIcon } from "@heroicons/react/24/outline"; +import { useNetwork } from "@starknet-react/core"; +import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; +import { RpcProvider } from "starknet"; +import { notification } from "~~/utils/scaffold-stark"; +import Image from "next/image"; + +/** + * Faucet modal which displays external websites that lets you send small amounts of L2 Sepolia ETH/STRK to an account address on Starknet Sepolia.. + */ +export const FaucetSepolia = () => { + const { chain: ConnectedChain } = useNetwork(); + const { targetNetwork } = useTargetNetwork(); + + const sepoliaFaucets = [ + { + name: "Starknet Foundation", + img: "/sn-symbol-gradient.png", + link: "https://starknet-faucet.vercel.app/", + }, + { + name: "Alchemy", + img: "/logo_alchemy.png", + link: "https://www.alchemy.com/faucets/starknet-sepolia", + }, + { + name: "Blast", + img: "/blast-icon-color.svg", + link: "https://blastapi.io/faucets/starknet-sepolia-eth", + }, + ]; + + const publicNodeUrl = targetNetwork.rpcUrls.public.http[0]; + + // Use useMemo to memoize the publicClient object + const publicClient = useMemo(() => { + return new RpcProvider({ + nodeUrl: publicNodeUrl, + }); + }, [publicNodeUrl]); + + useEffect(() => { + const checkChain = async () => { + try { + const providerInfo = await publicClient.getBlock(); + } catch (error) { + console.error("⚡️ ~ file: Faucet.tsx:checkChain ~ error", error); + notification.error( + <> +

      + Cannot connect to local provider +

      +

      + - Did you forget to run{" "} + + yarn chain + {" "} + ? +

      +

      + - Or you can change{" "} + + targetNetwork + {" "} + in{" "} + + scaffold.config.ts + +

      + , + { + duration: 5000, + }, + ); + } + }; + checkChain().then(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Render only on sepolia chain + if (ConnectedChain?.id !== sepolia.id) { + return null; + } + + return ( +
      + + + +
      + ); +}; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 87adbd43..6ed2fce5 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -7,7 +7,7 @@ const deployedContracts = { devnet: { DiceGame: { address: - "0x56532915f8755d81641ef7da74e358d88ac31ba40bce0c2a53ddef7a6c8dedd", + "0x682753de433000cc2ea0752e86fa44c10023b9c1a3547c641ddd2834a51fd16", abi: [ { type: "impl", @@ -167,10 +167,12 @@ const deployedContracts = { ], }, ], + classHash: + "0x30c5a46ac42247a384b77d044a851fb5f284c853043d014b19cf29eeaed03f8", }, RiggedRoll: { address: - "0x59d7a0a3a619fcecedc8122ff6b9e08cf2a689afc61b84c7939576361c09276", + "0x36b4b658a6ad3c82eb386252b5fa2a2c83a64a96dcb4484271cd927b92f78a1", abi: [ { type: "impl", @@ -377,6 +379,8 @@ const deployedContracts = { ], }, ], + classHash: + "0xce37e709ce13262a7803866491fb40eb7b1d8275d915c188a1aaa32172c8f9", }, }, } as const; diff --git a/packages/nextjs/contracts/predeployedContracts.ts b/packages/nextjs/contracts/predeployedContracts.ts index dd925ba5..4c3b211e 100644 --- a/packages/nextjs/contracts/predeployedContracts.ts +++ b/packages/nextjs/contracts/predeployedContracts.ts @@ -5,6 +5,9 @@ const universalEthAddress = "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7"; +const universalStrkAddress = + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + const preDeployedContracts = { devnet: { Eth: { @@ -212,6 +215,216 @@ const preDeployedContracts = { ], }, ], + classHash: + "0x046ded64ae2dead6448e247234bab192a9c483644395b66f2155f2614e5804b0", + }, + Strk: { + address: universalStrkAddress, + abi: [ + { + type: "impl", + name: "ERC20Impl", + interface_name: "openzeppelin::token::erc20::interface::IERC20", + }, + { + name: "openzeppelin::token::erc20::interface::IERC20", + type: "interface", + items: [ + { + name: "name", + type: "function", + inputs: [], + outputs: [ + { + type: "core::felt252", + }, + ], + state_mutability: "view", + }, + { + name: "symbol", + type: "function", + inputs: [], + outputs: [ + { + type: "core::felt252", + }, + ], + state_mutability: "view", + }, + { + name: "decimals", + type: "function", + inputs: [], + outputs: [ + { + type: "core::integer::u8", + }, + ], + state_mutability: "view", + }, + { + name: "allowance", + type: "function", + inputs: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "transfer", + type: "function", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + { + name: "approve", + type: "function", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + ], + }, + { + name: "ERC20CamelOnlyImpl", + type: "impl", + interface_name: + "openzeppelin::token::erc20::interface::IERC20CamelOnly", + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20CamelOnly", + items: [ + { + name: "totalSupply", + type: "function", + inputs: [], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "balanceOf", + type: "function", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "transferFrom", + type: "function", + inputs: [ + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + ], + }, + { + kind: "struct", + name: "openzeppelin::token::erc20_v070::erc20::ERC20::Transfer", + type: "event", + members: [ + { + kind: "data", + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + kind: "data", + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { + kind: "data", + name: "value", + type: "core::integer::u256", + }, + ], + }, + { + kind: "enum", + name: "openzeppelin::token::erc20_v070::erc20::ERC20::Event", + type: "event", + variants: [ + { + kind: "nested", + name: "Transfer", + type: "openzeppelin::token::erc20_v070::erc20::ERC20::Transfer", + }, + ], + }, + ], + classHash: + "0x046ded64ae2dead6448e247234bab192a9c483644395b66f2155f2614e5804b0", }, }, sepolia: { @@ -471,6 +684,267 @@ const preDeployedContracts = { ], }, ], + classHash: + "0x07f3777c99f3700505ea966676aac4a0d692c2a9f5e667f4c606b51ca1dd3420", + }, + Strk: { + address: universalStrkAddress, + abi: [ + { + type: "impl", + name: "ERC20Impl", + interface_name: "openzeppelin::token::erc20::interface::IERC20", + }, + { + name: "openzeppelin::token::erc20::interface::IERC20", + type: "interface", + items: [ + { + name: "name", + type: "function", + inputs: [], + outputs: [ + { + type: "core::felt252", + }, + ], + state_mutability: "view", + }, + { + name: "symbol", + type: "function", + inputs: [], + outputs: [ + { + type: "core::felt252", + }, + ], + state_mutability: "view", + }, + { + name: "decimals", + type: "function", + inputs: [], + outputs: [ + { + type: "core::integer::u8", + }, + ], + state_mutability: "view", + }, + { + name: "total_supply", + type: "function", + inputs: [], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "balance_of", + type: "function", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "allowance", + type: "function", + inputs: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "transfer", + type: "function", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + { + name: "transfer_from", + type: "function", + inputs: [ + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + { + name: "approve", + type: "function", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + ], + }, + { + name: "ERC20CamelOnlyImpl", + type: "impl", + interface_name: + "openzeppelin::token::erc20::interface::IERC20CamelOnly", + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20CamelOnly", + items: [ + { + name: "totalSupply", + type: "function", + inputs: [], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "balanceOf", + type: "function", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + name: "transferFrom", + type: "function", + inputs: [ + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + ], + }, + { + kind: "struct", + name: "openzeppelin::token::erc20_v070::erc20::ERC20::Transfer", + type: "event", + members: [ + { + kind: "data", + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + kind: "data", + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { + kind: "data", + name: "value", + type: "core::integer::u256", + }, + ], + }, + { + kind: "enum", + name: "openzeppelin::token::erc20_v070::erc20::ERC20::Event", + type: "event", + variants: [ + { + kind: "nested", + name: "Transfer", + type: "openzeppelin::token::erc20_v070::erc20::ERC20::Transfer", + }, + ], + }, + ], + classHash: + "0x04ad3c1dc8413453db314497945b6903e1c766495a1e60492d44da9c2a986e4b", }, }, } as const; diff --git a/packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts b/packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts index 1562dff9..4a9fd14f 100644 --- a/packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts +++ b/packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts @@ -13,23 +13,33 @@ export const useNativeCurrencyPrice = () => { const nativeCurrencyPrice = useGlobalState( (state) => state.nativeCurrencyPrice, ); + const strkCurrencyPrice = useGlobalState((state) => state.strkCurrencyPrice); const setNativeCurrencyPrice = useGlobalState( (state) => state.setNativeCurrencyPrice, ); - // Get the price of ETH from Coingecko on mount + const setStrkCurrencyPrice = useGlobalState( + (state) => state.setStrkCurrencyPrice, + ); + // Get the price of ETH & STRK from Coingecko on mount useEffect(() => { (async () => { if (nativeCurrencyPrice == 0) { - const price = await fetchPriceFromCoingecko(targetNetwork); + const price = await fetchPriceFromCoingecko("ETH"); setNativeCurrencyPrice(price); } + if (strkCurrencyPrice == 0) { + const strkPrice = await fetchPriceFromCoingecko("STRK"); + setStrkCurrencyPrice(strkPrice); + } })(); }, [targetNetwork]); - // Get the price of ETH from Coingecko at a given interval + // Get the price of ETH & STRK from Coingecko at a given interval useInterval(async () => { - const price = await fetchPriceFromCoingecko(targetNetwork); + const price = await fetchPriceFromCoingecko("ETH"); setNativeCurrencyPrice(price); + const strkPrice = await fetchPriceFromCoingecko("STRK"); + setStrkCurrencyPrice(strkPrice); }, scaffoldConfig.pollingInterval); //return nativeCurrencyPrice; diff --git a/packages/nextjs/hooks/scaffold-stark/useScaffoldStrkBalance.ts b/packages/nextjs/hooks/scaffold-stark/useScaffoldStrkBalance.ts new file mode 100644 index 00000000..ac80b742 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-stark/useScaffoldStrkBalance.ts @@ -0,0 +1,34 @@ +import { Address } from "@starknet-react/chains"; +import { useDeployedContractInfo } from "./useDeployedContractInfo"; +import { useContractRead } from "@starknet-react/core"; +import { BlockNumber } from "starknet"; +import { Abi } from "abi-wan-kanabi"; +import { formatUnits } from "ethers"; + +type UseScaffoldStrkBalanceProps = { + address?: Address | string; +}; + +const useScaffoldStrkBalance = ({ address }: UseScaffoldStrkBalanceProps) => { + const { data: deployedContract } = useDeployedContractInfo("Strk"); + + const { data, ...props } = useContractRead({ + functionName: "balanceOf", + address: deployedContract?.address, + abi: deployedContract?.abi as Abi as any[], + watch: true, + enabled: true, + args: address ? [address] : [], + blockIdentifier: "pending" as BlockNumber, + }); + + return { + value: data as unknown as bigint, + decimals: 18, + symbol: "STRK", + formatted: data ? formatUnits(data as unknown as bigint) : "0", + ...props, + }; +}; + +export default useScaffoldStrkBalance; diff --git a/packages/nextjs/public/blast-icon-color.svg b/packages/nextjs/public/blast-icon-color.svg new file mode 100644 index 00000000..6de46364 --- /dev/null +++ b/packages/nextjs/public/blast-icon-color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/nextjs/public/debug-image.png b/packages/nextjs/public/debug-image.png new file mode 100644 index 00000000..e6795e8a Binary files /dev/null and b/packages/nextjs/public/debug-image.png differ diff --git a/packages/nextjs/public/logo_alchemy.png b/packages/nextjs/public/logo_alchemy.png new file mode 100644 index 00000000..9c4be167 Binary files /dev/null and b/packages/nextjs/public/logo_alchemy.png differ diff --git a/packages/nextjs/public/sn-symbol-gradient.png b/packages/nextjs/public/sn-symbol-gradient.png new file mode 100644 index 00000000..c7856ba8 Binary files /dev/null and b/packages/nextjs/public/sn-symbol-gradient.png differ diff --git a/packages/nextjs/public/starkcompass-icon.svg b/packages/nextjs/public/starkcompass-icon.svg new file mode 100644 index 00000000..c6eee44b --- /dev/null +++ b/packages/nextjs/public/starkcompass-icon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/voyager-icon.svg b/packages/nextjs/public/voyager-icon.svg new file mode 100644 index 00000000..6d4a7675 --- /dev/null +++ b/packages/nextjs/public/voyager-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nextjs/services/store/store.ts b/packages/nextjs/services/store/store.ts index 691b7e57..1137f3ea 100644 --- a/packages/nextjs/services/store/store.ts +++ b/packages/nextjs/services/store/store.ts @@ -13,15 +13,20 @@ import { ChainWithAttributes } from "~~/utils/scaffold-stark"; type GlobalState = { nativeCurrencyPrice: number; + strkCurrencyPrice: number; setNativeCurrencyPrice: (newNativeCurrencyPriceState: number) => void; + setStrkCurrencyPrice: (newNativeCurrencyPriceState: number) => void; targetNetwork: ChainWithAttributes; setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => void; }; export const useGlobalState = create((set) => ({ nativeCurrencyPrice: 0, + strkCurrencyPrice: 0, setNativeCurrencyPrice: (newValue: number): void => set(() => ({ nativeCurrencyPrice: newValue })), + setStrkCurrencyPrice: (newValue: number): void => + set(() => ({ strkCurrencyPrice: newValue })), targetNetwork: scaffoldConfig.targetNetworks[0], setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => set(() => ({ targetNetwork: newTargetNetwork })), diff --git a/packages/nextjs/services/web3/faucet.ts b/packages/nextjs/services/web3/faucet.ts index cd61e211..a4e4f620 100644 --- a/packages/nextjs/services/web3/faucet.ts +++ b/packages/nextjs/services/web3/faucet.ts @@ -1,15 +1,25 @@ import { Address } from "@starknet-react/chains"; export async function mintEth(inputAddress: Address, eth: string) { - await fetch("http://0.0.0.0:5050/mint", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - address: inputAddress, - amount: parseFloat(eth) * 10 ** 18, - unit: "WEI", - }), - }); + try { + const response = await fetch("http://0.0.0.0:5050/mint", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + address: inputAddress, + amount: parseFloat(eth) * 10 ** 18, + unit: "WEI", + }), + }); + if (!response.ok) { + throw new Error(`${response.statusText}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("There was a problem with the operation", error); + return error; + } } diff --git a/packages/nextjs/utils/scaffold-stark/contract.ts b/packages/nextjs/utils/scaffold-stark/contract.ts index b5fc1e68..dba6a54f 100644 --- a/packages/nextjs/utils/scaffold-stark/contract.ts +++ b/packages/nextjs/utils/scaffold-stark/contract.ts @@ -63,7 +63,9 @@ export enum ContractCodeStatus { export type GenericContract = { address: Address; abi: Abi; + classHash: String; }; + export type GenericContractsDeclaration = { [network: string]: { [contractName: string]: GenericContract; diff --git a/packages/nextjs/utils/scaffold-stark/fetchPriceFromCoingecko.ts b/packages/nextjs/utils/scaffold-stark/fetchPriceFromCoingecko.ts index f4b5e345..801258b6 100644 --- a/packages/nextjs/utils/scaffold-stark/fetchPriceFromCoingecko.ts +++ b/packages/nextjs/utils/scaffold-stark/fetchPriceFromCoingecko.ts @@ -4,15 +4,10 @@ import { ChainWithAttributes } from "~~/utils/scaffold-stark"; const priceCache: Record = {}; export const fetchPriceFromCoingecko = async ( - targetNetwork: ChainWithAttributes, + symbol: string, retryCount = 3, ): Promise => { - const { symbol } = targetNetwork.nativeCurrency; - if ( - symbol !== "ETH" && - symbol !== "SEP" && - !targetNetwork.nativeCurrencyTokenAddress - ) { + if (symbol !== "ETH" && symbol !== "SEP" && symbol !== "STRK") { return 0; } @@ -32,11 +27,15 @@ const updatePriceCache = async ( let attempt = 0; while (attempt < retries) { try { - const response = await fetch( - `https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd`, - ); + let apiUrl = ""; + if (symbol === "ETH") { + apiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd`; + } else if (symbol === "STRK") { + apiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=starknet&vs_currencies=usd`; + } + const response = await fetch(apiUrl); const data = await response.json(); - const price = data.ethereum.usd; + const price = symbol === "ETH" ? data.ethereum.usd : data.starknet.usd; priceCache[symbol] = price; console.log(`Price updated for ${symbol}: ${price}`); return price; diff --git a/packages/nextjs/utils/scaffold-stark/networks.ts b/packages/nextjs/utils/scaffold-stark/networks.ts index 4601bf92..4ff4a990 100644 --- a/packages/nextjs/utils/scaffold-stark/networks.ts +++ b/packages/nextjs/utils/scaffold-stark/networks.ts @@ -66,6 +66,26 @@ export function getBlockExplorerAddressLink( return `${blockExplorerBaseURL}/contract/${address}`; } +/** + * Gives the block explorer URL for a given classhash. + * Defaults to Etherscan if no (wagmi) block explorer is configured for the network. + */ +export function getBlockExplorerClasshashLink( + network: chains.Chain, + address: string, +) { + const blockExplorerBaseURL = network.explorers?.starkscan[0]; + if (network.network === chains.devnet.network) { + return `/blockexplorer/class/${address}`; + } + + if (!blockExplorerBaseURL) { + return `https://starkscan.co/class/${address}`; + } + + return `${blockExplorerBaseURL}/class/${address}`; +} + export function getBlockExplorerLink(network: chains.Chain) { switch (network) { case chains.mainnet: diff --git a/packages/snfoundry/contracts/src/DiceGame.cairo b/packages/snfoundry/contracts/src/DiceGame.cairo index b86b4abe..000c9645 100644 --- a/packages/snfoundry/contracts/src/DiceGame.cairo +++ b/packages/snfoundry/contracts/src/DiceGame.cairo @@ -1,38 +1,39 @@ use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; + #[starknet::interface] pub trait IDiceGame { fn roll_dice(ref self: T, amount: u256); fn last_dice_value(self: @T) -> u256; fn nonce(self: @T) -> u256; fn prize(self: @T) -> u256; - fn eth_token(self: @T) -> IERC20CamelDispatcher; + fn eth_token_dispatcher(self: @T) -> IERC20CamelDispatcher; } #[starknet::contract] -mod DiceGame { +pub mod DiceGame { use keccak::keccak_u256s_le_inputs; use starknet::{ContractAddress, get_contract_address, get_block_number, get_caller_address}; use super::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait, IDiceGame}; #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { Roll: Roll, Winner: Winner, } #[derive(Drop, starknet::Event)] - struct Roll { + pub struct Roll { #[key] - player: ContractAddress, - amount: u256, - roll: u256, + pub player: ContractAddress, + pub amount: u256, + pub roll: u256, } #[derive(Drop, starknet::Event)] - struct Winner { - winner: ContractAddress, - amount: u256, + pub struct Winner { + pub winner: ContractAddress, + pub amount: u256, } #[storage] @@ -92,7 +93,7 @@ mod DiceGame { fn prize(self: @ContractState) -> u256 { self.prize.read() } - fn eth_token(self: @ContractState) -> IERC20CamelDispatcher { + fn eth_token_dispatcher(self: @ContractState) -> IERC20CamelDispatcher { self.eth_token.read() } } diff --git a/packages/snfoundry/contracts/src/RiggedRoll.cairo b/packages/snfoundry/contracts/src/RiggedRoll.cairo index ee8e6824..c34a43e2 100644 --- a/packages/snfoundry/contracts/src/RiggedRoll.cairo +++ b/packages/snfoundry/contracts/src/RiggedRoll.cairo @@ -1,3 +1,4 @@ +use contracts::DiceGame::{IDiceGameDispatcher, IDiceGameDispatcherTrait}; use starknet::ContractAddress; #[starknet::interface] @@ -6,17 +7,16 @@ pub trait IRiggedRoll { fn withdraw(ref self: T, to: ContractAddress, amount: u256); fn last_dice_value(self: @T) -> u256; fn predicted_roll(self: @T) -> u256; - fn dice_game(self: @T) -> ContractAddress; + fn dice_game_dispatcher(self: @T) -> IDiceGameDispatcher; } #[starknet::contract] mod RiggedRoll { - use contracts::DiceGame::{IDiceGameDispatcher, IDiceGameDispatcherTrait}; use keccak::keccak_u256s_le_inputs; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::token::erc20::interface::IERC20CamelDispatcherTrait; use starknet::{ContractAddress, get_contract_address, get_block_number, get_caller_address}; - use super::IRiggedRoll; + use super::{IRiggedRoll, IDiceGameDispatcher, IDiceGameDispatcherTrait}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -65,8 +65,8 @@ mod RiggedRoll { fn predicted_roll(self: @ContractState) -> u256 { self.predicted_roll.read() } - fn dice_game(self: @ContractState) -> ContractAddress { - self.dice_game.read().contract_address + fn dice_game_dispatcher(self: @ContractState) -> IDiceGameDispatcher { + self.dice_game.read() } } } diff --git a/packages/snfoundry/contracts/src/lib.cairo b/packages/snfoundry/contracts/src/lib.cairo index 5e64d101..760de311 100644 --- a/packages/snfoundry/contracts/src/lib.cairo +++ b/packages/snfoundry/contracts/src/lib.cairo @@ -1,6 +1,10 @@ mod DiceGame; mod RiggedRoll; +mod mock_contracts { + pub mod MockETHToken; +} #[cfg(test)] mod test { mod TestContract; } + diff --git a/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo b/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo new file mode 100644 index 00000000..07064311 --- /dev/null +++ b/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo @@ -0,0 +1,34 @@ +#[starknet::contract] +pub mod MockETHToken { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + let name = "MockETH"; + let symbol = "ETH"; + + self.erc20.initializer(name, symbol); + let amount_to_mint = initial_supply / 10; + self.erc20.mint(recipient, amount_to_mint); + } +} diff --git a/packages/snfoundry/contracts/src/test/TestContract.cairo b/packages/snfoundry/contracts/src/test/TestContract.cairo index 8b137891..b10f0977 100644 --- a/packages/snfoundry/contracts/src/test/TestContract.cairo +++ b/packages/snfoundry/contracts/src/test/TestContract.cairo @@ -1 +1,216 @@ +use contracts::DiceGame::{IDiceGameDispatcher, IDiceGameDispatcherTrait, DiceGame}; +use contracts::RiggedRoll::{IRiggedRollDispatcher, IRiggedRollDispatcherTrait}; +use contracts::mock_contracts::MockETHToken; +use keccak::keccak_u256s_le_inputs; +use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use snforge_std::cheatcodes::events::EventsFilterTrait; +use snforge_std::{ + declare, ContractClassTrait, spy_events, EventSpyAssertionsTrait, EventSpyTrait, Event, + cheat_caller_address, cheat_block_timestamp, CheatSpan +}; +use starknet::{ContractAddress, get_contract_address, get_block_number, get_caller_address}; +use starknet::{contract_address_const, get_block_timestamp}; + +fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +const ROLL_DICE_AMOUNT: u256 = 2000000000000000; // 0.002_ETH_IN_WEI +// Should deploy the MockETHToken contract +fn deploy_mock_eth_token() -> ContractAddress { + let erc20_class_hash = declare("MockETHToken").unwrap(); + let INITIAL_SUPPLY: u256 = 100000000000000000000; // 100_ETH_IN_WEI + let reciever = OWNER(); + let mut calldata = array![]; + calldata.append_serde(INITIAL_SUPPLY); + calldata.append_serde(reciever); + let (eth_token_address, _) = erc20_class_hash.deploy(@calldata).unwrap(); + eth_token_address +} + +// Should deploy the DiceGame contract +fn deploy_dice_game_contract() -> ContractAddress { + let eth_token_address = deploy_mock_eth_token(); + let dice_game_class_hash = declare("DiceGame").unwrap(); + let mut calldata = array![]; + calldata.append_serde(eth_token_address); + let (dice_game_contract_address, _) = dice_game_class_hash.deploy(@calldata).unwrap(); + println!("-- Dice Game contract deployed on: {:?}", dice_game_contract_address); + dice_game_contract_address +} + +fn deploy_rigged_roll_contract() -> ContractAddress { + let dice_game_contract_address = deploy_dice_game_contract(); + let rigged_roll_class_hash = declare("RiggedRoll").unwrap(); + let mut calldata = array![]; + calldata.append_serde(dice_game_contract_address); + calldata.append_serde(OWNER()); + let (rigged_roll_contract_address, _) = rigged_roll_class_hash.deploy(@calldata).unwrap(); + println!("-- Rigged Roll contract deployed on: {:?}", rigged_roll_contract_address); + rigged_roll_contract_address +} + +fn get_roll(get_roll_less_than_5: bool, rigged_roll_dispatcher: IRiggedRollDispatcher) -> u256 { + let mut expected_roll = 0; + let dice_game_dispatcher = rigged_roll_dispatcher.dice_game_dispatcher(); + let dice_game_contract_address = dice_game_dispatcher.contract_address; + let tester_address = OWNER(); + while true { + let prev_block: u256 = get_block_number().into() - 1; + let array = array![prev_block, dice_game_dispatcher.nonce()]; + expected_roll = keccak_u256s_le_inputs(array.span()) % 16; + println!("-- Produced roll: {:?}", expected_roll); + if expected_roll <= 5 == get_roll_less_than_5 { + break; + } + let eth_token_dispatcher = dice_game_dispatcher.eth_token_dispatcher(); + cheat_caller_address( + eth_token_dispatcher.contract_address, tester_address, CheatSpan::TargetCalls(1) + ); + eth_token_dispatcher.approve(dice_game_contract_address, ROLL_DICE_AMOUNT); + cheat_caller_address(dice_game_contract_address, tester_address, CheatSpan::TargetCalls(1)); + dice_game_dispatcher.roll_dice(ROLL_DICE_AMOUNT); + }; + expected_roll +} +#[test] +fn test_deploy_dice_game() { + deploy_dice_game_contract(); +} + +#[test] +fn test_deploy_rigged_roll() { + deploy_rigged_roll_contract(); +} + +#[test] +#[should_panic(expected: ('Not enough ETH',))] +fn test_rigged_roll_fails() { + let rigged_roll_contract_address = deploy_rigged_roll_contract(); + let rigged_roll_dispatcher = IRiggedRollDispatcher { + contract_address: rigged_roll_contract_address + }; + let eth_amount_wei: u256 = 1000000000000000; // 0.001_ETH_IN_WEI + + let tester_address = OWNER(); + let eth_token_dispatcher = rigged_roll_dispatcher.dice_game_dispatcher().eth_token_dispatcher(); + cheat_caller_address( + eth_token_dispatcher.contract_address, tester_address, CheatSpan::TargetCalls(1) + ); + eth_token_dispatcher.approve(rigged_roll_contract_address, eth_amount_wei); + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + rigged_roll_dispatcher.rigged_roll(eth_amount_wei); +} + +#[test] +fn test_rigged_roll_call_dice_game() { + let rigged_roll_contract_address = deploy_rigged_roll_contract(); + let rigged_roll_dispatcher = IRiggedRollDispatcher { + contract_address: rigged_roll_contract_address + }; + let dice_game_dispatcher = rigged_roll_dispatcher.dice_game_dispatcher(); + + let get_roll_less_than_5 = true; + let expected_roll = get_roll(get_roll_less_than_5, rigged_roll_dispatcher); + println!("-- Expect roll to be less than or equal to 5. DiceGame Roll:: {:?}", expected_roll); + let tester_address = OWNER(); + let eth_token_dispatcher = dice_game_dispatcher.eth_token_dispatcher(); + cheat_caller_address( + eth_token_dispatcher.contract_address, tester_address, CheatSpan::TargetCalls(1) + ); + eth_token_dispatcher.approve(rigged_roll_contract_address, ROLL_DICE_AMOUNT); + + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + + let mut spy = spy_events(); + rigged_roll_dispatcher.rigged_roll(ROLL_DICE_AMOUNT); + + let dice_game_contract = dice_game_dispatcher.contract_address; + let events = spy.get_events().emitted_by(dice_game_contract); + + assert_eq!(events.events.len(), 2, "There should be two events emitted by DiceGame contract"); + spy + .assert_emitted( + @array![ + ( + dice_game_contract, + DiceGame::Event::Roll( + DiceGame::Roll { + player: rigged_roll_contract_address, + amount: ROLL_DICE_AMOUNT, + roll: expected_roll + } + ) + ) + ] + ); + let (_, event) = events.events.at(1); + assert(event.keys.at(0) == @selector!("Winner"), 'Expected Winner event'); +} + +#[test] +fn test_rigged_roll_should_not_call_dice_game() { + let rigged_roll_contract_address = deploy_rigged_roll_contract(); + let rigged_roll_dispatcher = IRiggedRollDispatcher { + contract_address: rigged_roll_contract_address + }; + let dice_game_dispatcher = rigged_roll_dispatcher.dice_game_dispatcher(); + + let get_roll_less_than_5 = false; + let expected_roll = get_roll(get_roll_less_than_5, rigged_roll_dispatcher); + println!("-- Expect roll to be greater than 5. DiceGame Roll:: {:?}", expected_roll); + let tester_address = OWNER(); + let eth_token_dispatcher = dice_game_dispatcher.eth_token_dispatcher(); + cheat_caller_address( + eth_token_dispatcher.contract_address, tester_address, CheatSpan::TargetCalls(1) + ); + eth_token_dispatcher.approve(rigged_roll_contract_address, ROLL_DICE_AMOUNT); + + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + + let mut spy = spy_events(); + + rigged_roll_dispatcher.rigged_roll(ROLL_DICE_AMOUNT); + + let dice_game_contract = dice_game_dispatcher.contract_address; + let events = spy.get_events().emitted_by(dice_game_contract); + + assert_eq!(events.events.len(), 0, "There should be no events emitted by DiceGame contract"); +} + +#[test] +fn test_withdraw() { + let rigged_roll_contract_address = deploy_rigged_roll_contract(); + let rigged_roll_dispatcher = IRiggedRollDispatcher { + contract_address: rigged_roll_contract_address + }; + + let get_roll_less_than_5 = true; + let expected_roll = get_roll(get_roll_less_than_5, rigged_roll_dispatcher); + println!("-- Expect roll to be less than or equal to 5. DiceGame Roll:: {:?}", expected_roll); + let tester_address = OWNER(); + let eth_token_dispatcher = rigged_roll_dispatcher.dice_game_dispatcher().eth_token_dispatcher(); + cheat_caller_address( + eth_token_dispatcher.contract_address, tester_address, CheatSpan::TargetCalls(1) + ); + eth_token_dispatcher.approve(rigged_roll_contract_address, ROLL_DICE_AMOUNT); + + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + + rigged_roll_dispatcher.rigged_roll(ROLL_DICE_AMOUNT); + + let tester_address_prev_balance = eth_token_dispatcher.balanceOf(tester_address); + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + let rigged_roll_balance = eth_token_dispatcher.balanceOf(rigged_roll_contract_address); + + cheat_caller_address(rigged_roll_contract_address, tester_address, CheatSpan::TargetCalls(1)); + rigged_roll_dispatcher.withdraw(tester_address, rigged_roll_balance); + let tester_address_new_balance = eth_token_dispatcher.balanceOf(tester_address); + assert_eq!( + tester_address_new_balance, + tester_address_prev_balance + rigged_roll_balance, + "Tester address should have the balance of the rigged_roll_contract_address" + ); +} diff --git a/packages/snfoundry/package.json b/packages/snfoundry/package.json index 17456ef3..06aa5cc1 100644 --- a/packages/snfoundry/package.json +++ b/packages/snfoundry/package.json @@ -4,6 +4,7 @@ "scripts": { "chain": "starknet-devnet --seed 0 --account-class cairo1", "deploy": "ts-node scripts-ts/helpers/deploy-wrapper.ts", + "deploy:reset": "ts-node scripts-ts/helpers/deploy-wrapper.ts --reset", "test": "cd contracts && snforge test", "test-eslint": "node eslint-contract-name/eslint-plugin-contract-names.test.js", "compile": "cd contracts && scarb build", diff --git a/packages/snfoundry/scripts-ts/deploy-contract.ts b/packages/snfoundry/scripts-ts/deploy-contract.ts index d11f9763..dd9ed7c6 100644 --- a/packages/snfoundry/scripts-ts/deploy-contract.ts +++ b/packages/snfoundry/scripts-ts/deploy-contract.ts @@ -3,40 +3,63 @@ import path from "path"; import { networks } from "./helpers/networks"; import yargs from "yargs"; import { - BlockIdentifier, CallData, - hash, stark, RawArgs, - constants, - ec, - validateAndParseAddress, transaction, -} from "starknet"; -import { Network } from "./types"; -import { - LegacyContractClass, - CompiledSierra, extractContractHashes, + DeclareContractPayload, + UniversalDetails, } from "starknet"; +import { DeployContractParams, Network } from "./types"; +import { green, red, yellow } from "./helpers/colorize-log"; -const argv = yargs(process.argv.slice(2)).argv; -const networkName: string = argv["network"]; +interface Arguments { + network: string; + reset: boolean; + [x: string]: unknown; + _: (string | number)[]; + $0: string; +} -let deployments = {}; +const argv = yargs(process.argv.slice(2)) + .option("network", { + type: "string", + description: "Specify the network", + demandOption: true, + }) + .option("reset", { + alias: "r", + type: "boolean", + description: "Reset deployments", + default: false, + }) + .parseSync() as Arguments; + +const networkName: string = argv.network; +const resetDeployments: boolean = argv.reset; +let deployments = {}; let deployCalls = []; const { provider, deployer }: Network = networks[networkName]; -const declareIfNot_NotWait = async (payload: any) => { +const declareIfNot_NotWait = async ( + payload: DeclareContractPayload, + options?: UniversalDetails +) => { const declareContractPayload = extractContractHashes(payload); try { await provider.getClassByHash(declareContractPayload.classHash); } catch (error) { - let { transaction_hash } = await deployer.declare(payload); - if (networkName == "sepolia" || networkName == "mainnet") { - await provider.waitForTransaction(transaction_hash); + try { + const { transaction_hash } = await deployer.declare(payload, options); + if (networkName === "sepolia" || networkName === "mainnet") { + await provider.waitForTransaction(transaction_hash); + } + } catch (e) { + console.error(red("Error declaring contract:"), e); + throw e; } } return { @@ -49,38 +72,64 @@ const deployContract_NotWait = async (payload: { classHash: string; constructorCalldata: RawArgs; }) => { - let { calls, addresses } = transaction.buildUDCCall( - payload, - deployer.address - ); - deployCalls.push(...calls); - return { - contractAddress: addresses[0], - }; + try { + const { calls, addresses } = transaction.buildUDCCall( + payload, + deployer.address + ); + deployCalls.push(...calls); + return { + contractAddress: addresses[0], + }; + } catch (error) { + console.error(red("Error building UDC call:"), error); + throw error; + } }; +/** + * Deploy a contract using the specified parameters. + * + * @param {DeployContractParams} params - The parameters for deploying the contract. + * @param {string} params.contract - The name of the contract to deploy. + * @param {string} [params.contractName] - The name to export the contract as (optional). + * @param {RawArgs} [params.constructorArgs] - The constructor arguments for the contract (optional). + * @param {UniversalDetails} [params.options] - Additional deployment options (optional). + * + * @returns {Promise<{ classHash: string; address: string }>} The deployed contract's class hash and address. + * + * @example + * ///Example usage of deployContract function + * await deployContract({ + * contract: "YourContract", + * contractName: "YourContractExportName", + * constructorArgs: { owner: deployer.address }, + * options: { maxFee: BigInt(1000000000000) } + * }); + */ const deployContract = async ( - constructorArgs: RawArgs, - contractName: string, - exportContractName?: string, - options?: { - maxFee: bigint; - } + params: DeployContractParams ): Promise<{ classHash: string; address: string; }> => { + const { contract, constructorArgs, contractName, options } = params; + try { await deployer.getContractVersion(deployer.address); } catch (e) { if (e.toString().includes("Contract not found")) { - throw new Error( - `The wallet you're using to deploy the contract is not deployed in ${networkName} network` - ); + const errorMessage = `The wallet you're using to deploy the contract is not deployed in the ${networkName} network.`; + console.error(red(errorMessage)); + throw new Error(errorMessage); + } else { + console.error(red("Error getting contract version: "), e); + throw e; } } let compiledContractCasm; + let compiledContractSierra; try { compiledContractCasm = JSON.parse( @@ -88,7 +137,7 @@ const deployContract = async ( .readFileSync( path.resolve( __dirname, - `../contracts/target/dev/contracts_${contractName}.compiled_contract_class.json` + `../contracts/target/dev/contracts_${contract}.compiled_contract_class.json` ) ) .toString("ascii") @@ -102,12 +151,14 @@ const deployContract = async ( const match = error.message.match( /\/dev\/(.+?)\.compiled_contract_class/ ); - const contractName = match ? match[1].split("_").pop() : "Unknown"; + const missingContract = match ? match[1].split("_").pop() : "Unknown"; console.error( - `The contract "${contractName}" doesn't exist or is not compiled` + red( + `The contract "${missingContract}" doesn't exist or is not compiled` + ) ); } else { - console.error(error); + console.error(red("Error reading compiled contract class file: "), error); } return { classHash: "", @@ -115,27 +166,38 @@ const deployContract = async ( }; } - const compiledContractSierra = JSON.parse( - fs - .readFileSync( - path.resolve( - __dirname, - `../contracts/target/dev/contracts_${contractName}.contract_class.json` + try { + compiledContractSierra = JSON.parse( + fs + .readFileSync( + path.resolve( + __dirname, + `../contracts/target/dev/contracts_${contract}.contract_class.json` + ) ) - ) - .toString("ascii") - ); + .toString("ascii") + ); + } catch (error) { + console.error(red("Error reading contract class file: "), error); + return { + classHash: "", + address: "", + }; + } const contractCalldata = new CallData(compiledContractSierra.abi); const constructorCalldata = constructorArgs ? contractCalldata.compile("constructor", constructorArgs) : []; - console.log("Deploying Contract ", contractName); + console.log(yellow("Deploying Contract "), contract); - let { classHash } = await declareIfNot_NotWait({ - contract: compiledContractSierra, - casm: compiledContractCasm, - }); + let { classHash } = await declareIfNot_NotWait( + { + contract: compiledContractSierra, + casm: compiledContractCasm, + }, + options + ); let randomSalt = stark.randomAddress(); @@ -145,14 +207,14 @@ const deployContract = async ( constructorCalldata, }); - console.log("Contract Deployed at ", contractAddress); + console.log(green("Contract Deployed at "), contractAddress); - let finalContractName = exportContractName || contractName; + let finalContractName = contractName || contract; deployments[finalContractName] = { classHash: classHash, address: contractAddress, - contract: contractName, + contract: contract, }; return { @@ -161,27 +223,45 @@ const deployContract = async ( }; }; -const executeDeployCalls = async () => { +const executeDeployCalls = async (options?: UniversalDetails) => { + if (deployCalls.length < 1) { + throw new Error( + red( + "Aborted: No contract to deploy. Please prepare the contracts with `deployContract`" + ) + ); + } + try { - let { transaction_hash } = await deployer.execute(deployCalls); - console.log("Deploy Calls Executed at ", transaction_hash); - if (networkName == "sepolia" || networkName == "mainnet") { - const receipt = await provider.waitForTransaction(transaction_hash); - console.log("Deploy Calls Executed at ", receipt); + let { transaction_hash } = await deployer.execute(deployCalls, options); + console.log(green("Deploy Calls Executed at "), transaction_hash); + if (networkName === "sepolia" || networkName === "mainnet") { + await provider.waitForTransaction(transaction_hash); } } catch (error) { + console.error(red("Error executing deploy calls: "), error); // split the calls in half and try again recursively if (deployCalls.length > 1) { - let half = deployCalls.length / 2; + let half = Math.ceil(deployCalls.length / 2); let firstHalf = deployCalls.slice(0, half); - let secondHalf = deployCalls.slice(half, deployCalls.length); + let secondHalf = deployCalls.slice(half); deployCalls = firstHalf; - await executeDeployCalls(); + await executeDeployCalls(options); deployCalls = secondHalf; - await executeDeployCalls(); + await executeDeployCalls(options); } } }; +const loadExistingDeployments = () => { + const networkPath = path.resolve( + __dirname, + `../deployments/${networkName}_latest.json` + ); + if (fs.existsSync(networkPath)) { + return JSON.parse(fs.readFileSync(networkPath, "utf8")); + } + return {}; +}; const exportDeployments = () => { const networkPath = path.resolve( @@ -189,7 +269,11 @@ const exportDeployments = () => { `../deployments/${networkName}_latest.json` ); - if (fs.existsSync(networkPath)) { + let finalDeployments = resetDeployments + ? deployments + : { ...loadExistingDeployments(), ...deployments }; + + if (fs.existsSync(networkPath) && !resetDeployments) { const currentTimestamp = new Date().getTime(); fs.renameSync( networkPath, @@ -197,13 +281,15 @@ const exportDeployments = () => { ); } - fs.writeFileSync(networkPath, JSON.stringify(deployments, null, 2)); + fs.writeFileSync(networkPath, JSON.stringify(finalDeployments, null, 2)); }; export { deployContract, provider, deployer, + loadExistingDeployments, exportDeployments, executeDeployCalls, + resetDeployments, }; diff --git a/packages/snfoundry/scripts-ts/deploy.ts b/packages/snfoundry/scripts-ts/deploy.ts index a02784f2..88fb370e 100644 --- a/packages/snfoundry/scripts-ts/deploy.ts +++ b/packages/snfoundry/scripts-ts/deploy.ts @@ -5,18 +5,18 @@ import { exportDeployments, deployer, } from "./deploy-contract"; +import { green } from "./helpers/colorize-log"; import preDeployedContracts from "../../nextjs/contracts/predeployedContracts"; const deployScript = async (): Promise => { - const { address: diceGameAddr } = await deployContract( - { + const { address: diceGameAddr } = await deployContract({ + contract: "DiceGame", + constructorArgs: { eth_token_address: "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7", }, - - "DiceGame" - ); + }); const ethAbi = preDeployedContracts.devnet.Eth.abi as Abi; const ethAddress = preDeployedContracts.devnet.Eth.address as `0x${string}`; @@ -26,22 +26,22 @@ const deployScript = async (): Promise => { diceGameAddr, 1000000000000000000n, ]); - //const receipt = await provider.waitForTransaction(tx.transaction_hash); + // const receipt = await provider.waitForTransaction(tx.transaction_hash); - await deployContract( - { + await deployContract({ + contract: "RiggedRoll", + constructorArgs: { dice_game_address: diceGameAddr, owner: deployer.address, }, - "RiggedRoll" - ); + }); }; deployScript() - .then(() => { - executeDeployCalls().then(() => { - exportDeployments(); - }); - console.log("All Setup Done"); + .then(async () => { + await executeDeployCalls(); + exportDeployments(); + + console.log(green("All Setup Done")); }) .catch(console.error); diff --git a/packages/snfoundry/scripts-ts/helpers/colorize-log.ts b/packages/snfoundry/scripts-ts/helpers/colorize-log.ts new file mode 100644 index 00000000..1e37792c --- /dev/null +++ b/packages/snfoundry/scripts-ts/helpers/colorize-log.ts @@ -0,0 +1,16 @@ +const colors = { + reset: "\x1b[0m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", +}; + +const colorize = (color: string, message: string): string => { + return `${color}${message}${colors.reset}`; +}; + +export const red = (message: string): string => colorize(colors.red, message); +export const green = (message: string): string => + colorize(colors.green, message); +export const yellow = (message: string): string => + colorize(colors.yellow, message); diff --git a/packages/snfoundry/scripts-ts/helpers/deploy-wrapper.ts b/packages/snfoundry/scripts-ts/helpers/deploy-wrapper.ts index 59adce37..60ecb945 100644 --- a/packages/snfoundry/scripts-ts/helpers/deploy-wrapper.ts +++ b/packages/snfoundry/scripts-ts/helpers/deploy-wrapper.ts @@ -6,22 +6,28 @@ interface CommandLineOptions { _: string[]; // Non-hyphenated arguments are usually under the `_` key $0: string; // The script name or path is under the `$0` key network?: string; // The --network option + reset?: boolean; } const argv = yargs(process.argv.slice(2)) .options({ network: { type: "string" }, + reset: { type: "boolean", default: false }, }) .parseSync() as CommandLineOptions; // Set the NETWORK environment variable based on the --network argument process.env.NETWORK = argv.network || "devnet"; +// Set the RESET environment variable based on the --reset flag + // Execute the deploy script execSync( - "cd contracts && scarb build && ts-node ../scripts-ts/deploy.ts --network " + + "cd contracts && scarb build && ts-node ../scripts-ts/deploy.ts" + + " --network " + process.env.NETWORK + + (argv.reset ? " --reset" : "") + " && ts-node ../scripts-ts/helpers/parse-deployments.ts" + - " && cd ../..", + " && cd ..", { stdio: "inherit" } ); diff --git a/packages/snfoundry/scripts-ts/helpers/parse-deployments.ts b/packages/snfoundry/scripts-ts/helpers/parse-deployments.ts index a64d12bd..aa2490c5 100644 --- a/packages/snfoundry/scripts-ts/helpers/parse-deployments.ts +++ b/packages/snfoundry/scripts-ts/helpers/parse-deployments.ts @@ -14,11 +14,11 @@ const generatedContractComment = `/** const getContractDataFromDeployments = (): Record< string, - Record + Record > => { const allContractsData: Record< string, - Record + Record > = {}; files.forEach((file) => { @@ -49,6 +49,7 @@ const getContractDataFromDeployments = (): Record< [contractName]: { address: contractData.address, abi: abiContent.abi.filter((item) => item.type !== "l1_handler"), + classHash: contractData.classHash, }, }; } catch (e) {} diff --git a/packages/snfoundry/scripts-ts/types.ts b/packages/snfoundry/scripts-ts/types.ts index a5965a61..eabc5c9f 100644 --- a/packages/snfoundry/scripts-ts/types.ts +++ b/packages/snfoundry/scripts-ts/types.ts @@ -1,4 +1,4 @@ -import { Account, RpcProvider } from "starknet"; +import { Account, RawArgs, RpcProvider, UniversalDetails } from "starknet"; export type Networks = Record<"devnet" | "sepolia" | "mainnet", Network>; @@ -6,3 +6,10 @@ export type Network = { provider: RpcProvider; deployer: Account; }; + +export type DeployContractParams = { + contract: string; + contractName?: string; + constructorArgs?: RawArgs; + options?: UniversalDetails; +};