@@ -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 (
-
-
- {displayUsdMode ? (
- <>
- $
-
- {(parseFloat(formatted) * price).toLocaleString("en-US", {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
- >
- ) : (
- <>
- {parseFloat(formatted).toFixed(4)}
-
- {targetNetwork.nativeCurrency.symbol}
-
- >
- )}
-
-
+ <>
+
+
+ {displayUsdMode ? (
+
+ $
+
+ {totalBalanceInUsd.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}
+
+
+ ) : (
+ <>
+
+
+ {parseFloat(formatted).toFixed(4)}
+
+ {targetNetwork.nativeCurrency.symbol}
+
+
+
+
+ {parseFloat(strkFormatted).toFixed(4)}
+
+ {strkSymbol}
+
+
+
+ >
+ )}
+
+
+ >
);
};
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 ? (
+
+ ) : (
+
{
+ setAddressCopied(true);
+ setTimeout(() => {
+ setAddressCopied(false);
+ }, 800);
+ }}
+ >
+
+
+ )}
+
+ );
+};
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;
+};