├── .gitignore ├── LICENSE ├── Scarb.lock ├── Scarb.toml ├── readme.md ├── src └── lib.cairo ├── tests ├── my_test.cairo └── test_contract.cairo └── web ├── .env.template ├── .eslintrc.json ├── .gitignore ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── abis │ └── abi.ts ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── Providers.tsx │ ├── WalletBar.tsx │ └── ui │ │ └── Button.tsx └── lib │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .snfoundry_cache/ 3 | .DS_Store 4 | snfoundry.toml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nestor Bonilla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Scarb.lock: -------------------------------------------------------------------------------- 1 | # Code generated by scarb DO NOT EDIT. 2 | version = 1 3 | 4 | [[package]] 5 | name = "snforge_std" 6 | version = "0.23.0" 7 | source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.23.0#f2bff8f796763ada77fe6033ec1b034ceee22abd" 8 | 9 | [[package]] 10 | name = "workshop_frontend" 11 | version = "0.1.0" 12 | dependencies = [ 13 | "snforge_std", 14 | ] 15 | -------------------------------------------------------------------------------- /Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workshop_frontend" 3 | version = "0.1.0" 4 | edition = "2023_11" 5 | 6 | # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html 7 | 8 | [dependencies] 9 | starknet = "2.6.3" 10 | 11 | [dev-dependencies] 12 | snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.23.0" } 13 | 14 | [[target.starknet-contract]] 15 | sierra = true 16 | casm = true 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Starknet Frontend Workshop 2 | 3 | This workshop teaches you how to build a Starknet frontend application using NextJS, StarknetJS v6, and Starknet-react hooks. It's designed for developers with basic React and TypeScript knowledge who want to learn Starknet frontend development. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have met the following requirements: 8 | 9 | * Node.js (version 14 or later) and npm installed. Download them [here](https://nodejs.org/en/download/). 10 | * Basic understanding of Starknet Foundry (if you want to deploy your own contract instance). 11 | * Familiarity with TypeScript and React. 12 | 13 | ## Getting Started 14 | 15 | ### Starknet Foundry 16 | 17 | This project includes a Starknet Foundry repository with a smart contract used in the frontend web app. The contract implements the following functionality: 18 | 19 | - Increase balance: Add to the contract's balance (emits an event). 20 | - Get balance: Retrieve the current balance. 21 | - Reset balance: Set the balance to zero (owner-only function). 22 | 23 | The contract also includes an owner field for access control and emits events for balance increases. 24 | 25 | To deploy your own instance, use `sncast` to [declare](https://foundry-rs.github.io/starknet-foundry/starknet/declare.html) changes and/or [deploy](https://foundry-rs.github.io/starknet-foundry/starknet/deploy.html) an instance. 26 | 27 | ### NextJS App 28 | 29 | The `web` directory contains a Next.js app based on the [starknet-react](https://github.com/apibara/starknet-react) template. Recent updates include: 30 | 31 | - Compatibility with the latest versions of Starknet JS and Starknet-react. 32 | - Upgrade to StarknetJS v6 (breaking changes from v5). 33 | - Full TypeScript support for type-safe development. 34 | 35 | To get started: 36 | 37 | 1. Navigate to the `web` directory 38 | 2. Copy `.env.template` to `.env.local` and fill in the required values 39 | 3. Install dependencies: 40 | ```bash 41 | npm install 42 | # or yarn, pnpm, bun 43 | ``` 44 | 4. Run the development server: 45 | ```bash 46 | npm run dev 47 | # or yarn dev, pnpm dev, bun dev 48 | ``` 49 | 5. Open http://localhost:3000 in your browser 50 | 51 | ## Workshop Steps 52 | 53 | This workshop consists of seven branches, each focusing on a specific step: 54 | 55 | **Getting Started (branch: 0-getting-started)**: Initial workshop structure setup. 56 | 57 | Then, proceed with: 58 | 59 | 1. **Read Data (branch: 1-read-data)**: Retrieve the latest block number. 60 | 2. **Read Balance (branch: 2-read-balance)**: Fetch your account balance. 61 | 3. **Read Contract (branch: 3-read-contract)**: Get data from a deployed smart contract. 62 | 4. **Write Contract (branch: 4-write-contract)**: Update the smart contract's state. 63 | 5. **Reset Balance (branch: 5-reset-balance)**: Allow the owner to reset the counter balance. 64 | 6. **Get Events (branch: 6-get-events)**: Retrieve and display events from the smart contract. 65 | 66 | The `main` branch will contain all changes, serving as a complete reference. 67 | 68 | ## Troubleshooting 69 | 70 | If you encounter issues, check the [issues](https://github.com/nestorbonilla/starknet-workshop-frontend/issues) in this repository or open a new one if needed. 71 | 72 | ## Contributing 73 | 74 | To contribute to this workshop: 75 | 76 | 1. Fork the repository. 77 | 2. Clone your fork locally. 78 | 3. Create a new branch (`git checkout -b feature/AmazingFeature`). 79 | 4. Commit your changes (`git commit -m 'Add some AmazingFeature'`). 80 | 5. Push to the branch (`git push origin feature/AmazingFeature`). 81 | 6. Open a Pull Request. 82 | 83 | Let's get started! 84 | -------------------------------------------------------------------------------- /src/lib.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::interface] 2 | pub trait IHelloStarknet { 3 | fn increase_balance(ref self: TContractState, amount: u256); 4 | fn get_balance(self: @TContractState) -> u256; 5 | fn reset_balance(ref self: TContractState); 6 | } 7 | 8 | #[starknet::contract] 9 | mod HelloStarknet { 10 | use starknet::ContractAddress; 11 | use starknet::get_caller_address; 12 | 13 | #[storage] 14 | struct Storage { 15 | balance: u256, 16 | owner: ContractAddress, 17 | } 18 | 19 | #[event] 20 | #[derive(Drop, starknet::Event)] 21 | enum Event { 22 | BalanceIncreased: BalanceIncreased, 23 | } 24 | 25 | #[derive(Drop, starknet::Event)] 26 | struct BalanceIncreased { 27 | #[key] 28 | sender: ContractAddress, 29 | amount: u256, 30 | new_balance: u256, 31 | } 32 | 33 | #[constructor] 34 | fn constructor(ref self: ContractState, initial_owner: ContractAddress) { 35 | self.owner.write(initial_owner); 36 | self.balance.write(0); 37 | } 38 | 39 | #[abi(embed_v0)] 40 | impl HelloStarknetImpl of super::IHelloStarknet { 41 | fn increase_balance(ref self: ContractState, amount: u256) { 42 | assert(amount != 0, 'Amount cannot be 0'); 43 | let new_balance = self.balance.read() + amount; 44 | self.balance.write(new_balance); 45 | 46 | self.emit(BalanceIncreased { 47 | sender: get_caller_address(), 48 | amount: amount, 49 | new_balance: new_balance, 50 | }); 51 | } 52 | 53 | fn get_balance(self: @ContractState) -> u256 { 54 | self.balance.read() 55 | } 56 | 57 | fn reset_balance(ref self: ContractState) { 58 | let caller = get_caller_address(); 59 | assert(caller == self.owner.read(), 'Only owner can reset balance'); 60 | self.balance.write(0); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /tests/my_test.cairo: -------------------------------------------------------------------------------- 1 | use starknet::ContractAddress; 2 | use snforge_std::{declare, ContractClassTrait}; 3 | use workshop_frontend::IHelloStarknetDispatcher; 4 | use workshop_frontend::IHelloStarknetDispatcherTrait; 5 | 6 | #[test] 7 | fn test_get_balance() { 8 | let contract = declare("HelloStarknet").unwrap(); 9 | let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); 10 | let dispatcher = IHelloStarknetDispatcher { contract_address}; 11 | let balance = dispatcher.get_balance(); 12 | assert(balance == 0, 'Balance should be 0'); 13 | dispatcher.increase_balance(10); 14 | let changed_balance = dispatcher.get_balance(); 15 | assert(changed_balance == 10, 'Balance should be 10'); 16 | } -------------------------------------------------------------------------------- /tests/test_contract.cairo: -------------------------------------------------------------------------------- 1 | use starknet::ContractAddress; 2 | 3 | use snforge_std::{declare, ContractClassTrait}; 4 | 5 | use workshop_frontend::IHelloStarknetSafeDispatcher; 6 | use workshop_frontend::IHelloStarknetSafeDispatcherTrait; 7 | use workshop_frontend::IHelloStarknetDispatcher; 8 | use workshop_frontend::IHelloStarknetDispatcherTrait; 9 | 10 | fn deploy_contract(name: ByteArray) -> ContractAddress { 11 | let contract = declare(name).unwrap(); 12 | let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); 13 | contract_address 14 | } 15 | 16 | #[test] 17 | #[ignore] 18 | fn test_increase_balance() { 19 | let contract_address = deploy_contract("HelloStarknet"); 20 | 21 | let dispatcher = IHelloStarknetDispatcher { contract_address }; 22 | 23 | let balance_before = dispatcher.get_balance(); 24 | assert(balance_before == 0, 'Invalid balance'); 25 | 26 | dispatcher.increase_balance(42); 27 | 28 | let balance_after = dispatcher.get_balance(); 29 | assert(balance_after == 42, 'Invalid balance'); 30 | } 31 | 32 | #[test] 33 | #[ignore] 34 | #[feature("safe_dispatcher")] 35 | fn test_cannot_increase_balance_with_zero_value() { 36 | let contract_address = deploy_contract("HelloStarknet"); 37 | 38 | let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address }; 39 | 40 | let balance_before = safe_dispatcher.get_balance().unwrap(); 41 | assert(balance_before == 0, 'Invalid balance'); 42 | 43 | match safe_dispatcher.increase_balance(0) { 44 | Result::Ok(_) => core::panic_with_felt252('Should have panicked'), 45 | Result::Err(panic_data) => { 46 | assert(*panic_data.at(0) == 'Amount cannot be 0', *panic_data.at(0)); 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /web/.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_RPC_URL= -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | package-lock.json 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | .next 40 | 41 | # Environment variables 42 | .env 43 | .env.local 44 | .env.*.local 45 | 46 | # Include template 47 | !.env.template -------------------------------------------------------------------------------- /web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": { 6 | "name": "Nestor Bonilla", 7 | "url": "https://0xNestor.com" 8 | }, 9 | "scripts": { 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "lint": "next lint" 14 | }, 15 | "dependencies": { 16 | "@headlessui/react": "^2.0.3", 17 | "@starknet-react/chains": "3.0.0-beta.2", 18 | "@starknet-react/core": "3.0.0-beta.2", 19 | "@starknet-react/typescript-config": "0.0.0", 20 | "get-starknet-core": "^4.0.0", 21 | "lodash": "^4.17.21", 22 | "next": "14.2.8", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "starknet": "6.12.1" 26 | }, 27 | "devDependencies": { 28 | "@tailwindcss/forms": "^0.5.7", 29 | "@types/lodash": "^4.17.7", 30 | "@types/node": "^20", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "autoprefixer": "^10.0.1", 34 | "eslint": "^8", 35 | "eslint-config-next": "14.0.2", 36 | "postcss": "^8", 37 | "tailwindcss": "^3.4.3", 38 | "typescript": "^5" 39 | } 40 | } -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /web/src/abis/abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI = [ 2 | { 3 | type: "impl", 4 | name: "HelloStarknetImpl", 5 | interface_name: "workshop_frontend::IHelloStarknet" 6 | }, 7 | { 8 | type: "struct", 9 | name: "core::integer::u256", 10 | members: [ 11 | { name: "low", type: "core::integer::u128" }, 12 | { name: "high", type: "core::integer::u128" } 13 | ] 14 | }, 15 | { 16 | type: "interface", 17 | name: "workshop_frontend::IHelloStarknet", 18 | items: [ 19 | { 20 | type: "function", 21 | name: "increase_balance", 22 | inputs: [{ name: "amount", type: "core::integer::u256" }], 23 | outputs: [], 24 | state_mutability: "external" 25 | }, 26 | { 27 | type: "function", 28 | name: "get_balance", 29 | inputs: [], 30 | outputs: [{ type: "core::integer::u256" }], 31 | state_mutability: "view" 32 | }, 33 | { 34 | type: "function", 35 | name: "reset_balance", 36 | inputs: [], 37 | outputs: [], 38 | state_mutability: "external" 39 | } 40 | ] 41 | }, 42 | { 43 | type: "event", 44 | name: "workshop_frontend::HelloStarknet::BalanceIncreased", 45 | kind: "struct", 46 | members: [ 47 | { 48 | name: "sender", 49 | type: "core::starknet::contract_address::ContractAddress", 50 | kind: "key" 51 | }, 52 | { name: "amount", type: "core::integer::u256", kind: "data" }, 53 | { name: "new_balance", type: "core::integer::u256", kind: "data" } 54 | ] 55 | }, 56 | { 57 | type: "event", 58 | name: "workshop_frontend::HelloStarknet::Event", 59 | kind: "enum", 60 | variants: [ 61 | { 62 | name: "BalanceIncreased", 63 | type: "workshop_frontend::HelloStarknet::BalanceIncreased", 64 | kind: "nested" 65 | } 66 | ] 67 | } 68 | ] as const; -------------------------------------------------------------------------------- /web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestorbonilla/starknet-workshop-frontend/004ab762111f4be1673f4b3587fe080998b7752a/web/src/app/favicon.ico -------------------------------------------------------------------------------- /web/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @media (prefers-color-scheme: dark) { 6 | :root { 7 | --foreground-rgb: 255, 255, 255; 8 | --background-start-rgb: 0, 0, 0; 9 | --background-end-rgb: 0, 0, 0; 10 | } 11 | } 12 | 13 | body { 14 | color: rgb(var(--foreground-rgb)); 15 | background: linear-gradient(to bottom, 16 | transparent, 17 | rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); 18 | } -------------------------------------------------------------------------------- /web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { Providers } from "@/components/Providers"; 4 | import "./globals.css"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Basecamp X", 10 | description: "Starknet Frontend Workshop", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 3 | import dynamic from 'next/dynamic'; 4 | import { useAccount, useBalance, useBlockNumber, useContract, useReadContract, useSendTransaction, useTransactionReceipt } from '@starknet-react/core'; 5 | import { BlockNumber, Contract, RpcProvider } from "starknet"; 6 | import { ABI } from "../abis/abi"; 7 | import { type Abi } from "starknet"; 8 | import { formatAmount, shortenAddress } from '@/lib/utils'; 9 | const WalletBar = dynamic(() => import('../components/WalletBar'), { ssr: false }) 10 | const Page: FC = () => { 11 | 12 | // Step 1 --> Read the latest block -- Start 13 | const { data: blockNumberData, isLoading: blockNumberIsLoading, isError: blockNumberIsError } = useBlockNumber({ 14 | blockIdentifier: 'latest' as BlockNumber 15 | }); 16 | const workshopEnds = 180000; 17 | // Step 1 --> Read the latest block -- End 18 | 19 | // Step 2 --> Read your balance -- Start 20 | const { address: userAddress } = useAccount(); 21 | const { isLoading: balanceIsLoading, isError: balanceIsError, error: balanceError, data: balanceData } = useBalance({ 22 | address: userAddress, 23 | watch: true 24 | }); 25 | // Step 2 --> Read your balance -- End 26 | 27 | // Step 3 --> Read from a contract -- Start 28 | const contractAddress = "0x1c758616421a10f9df071a5d985c72e3907cf98d553204cf8dee354647c736"; 29 | const { data: readData, refetch: dataRefetch, isError: readIsError, isLoading: readIsLoading, error: readError } = useReadContract({ 30 | functionName: "get_balance", 31 | args: [], 32 | abi: ABI as Abi, 33 | address: contractAddress, 34 | watch: true, 35 | refetchInterval: 1000 36 | }); 37 | // Step 3 --> Read from a contract -- End 38 | 39 | // Step 4 --> Write to a contract -- Start 40 | const [amount, setAmount] = useState(0); 41 | const handleSubmit = async (event: React.FormEvent) => { 42 | event.preventDefault(); 43 | console.log("Form submitted with amount ", amount); 44 | writeAsync(); 45 | }; 46 | const typedABI = ABI as Abi; 47 | const { contract } = useContract({ 48 | abi: typedABI, 49 | address: contractAddress, 50 | }); 51 | const calls = useMemo(() => { 52 | if (!userAddress || !contract) return []; 53 | const safeAmount = amount || 0; 54 | return [contract.populate("increase_balance", [safeAmount])]; 55 | }, [contract, userAddress, amount]); 56 | const { 57 | send: writeAsync, 58 | data: writeData, 59 | isPending: writeIsPending, 60 | } = useSendTransaction({ 61 | calls, 62 | }); 63 | const { 64 | data: waitData, 65 | status: waitStatus, 66 | isLoading: waitIsLoading, 67 | isError: waitIsError, 68 | error: waitError 69 | } = useTransactionReceipt({ hash: writeData?.transaction_hash, watch: true }) 70 | const handleAmountChange = (event: React.ChangeEvent) => { 71 | const value = event.target.value; 72 | setAmount(value === '' ? '' : Number(value)); 73 | }; 74 | const LoadingState = ({ message }: { message: string }) => ( 75 |
76 |
77 | 78 | 79 | 80 |
81 | {message} 82 |
83 | ); 84 | const buttonContent = () => { 85 | if (writeIsPending) { 86 | return ; 87 | } 88 | 89 | if (waitIsLoading) { 90 | return ; 91 | } 92 | 93 | if (waitStatus === "error") { 94 | return ; 95 | } 96 | 97 | if (waitStatus === "success") { 98 | return "Transaction confirmed"; 99 | } 100 | 101 | return "Send"; 102 | }; 103 | // Step 4 --> Write to a contract -- End 104 | 105 | // Step 5 --> Reset balance -- Start 106 | const resetBalanceCall = useMemo(() => { 107 | if (!contract) return undefined; 108 | try { 109 | return contract.populate("reset_balance"); 110 | } catch (error) { 111 | console.error("Error populating reset_balance call:", error); 112 | return undefined; 113 | } 114 | }, [contract]); 115 | const { 116 | send: resetBalance, 117 | isPending: resetIsPending, 118 | data: resetData, 119 | } = useSendTransaction({ 120 | calls: resetBalanceCall ? [resetBalanceCall] : [], 121 | }); 122 | // Step 5 --> Reset balance -- End 123 | 124 | // Step 6 --> Get events from a contract -- Start 125 | type ContractEvent = { 126 | from_address: string; 127 | keys: string[]; 128 | data: string[]; 129 | }; 130 | const provider = useMemo(() => new RpcProvider({ nodeUrl: process.env.NEXT_PUBLIC_RPC_URL }), []); 131 | const [events, setEvents] = useState([]); 132 | const lastCheckedBlockRef = useRef(0); 133 | const { data: blockNumber } = useBlockNumber({ refetchInterval: 3000 }); 134 | const checkForEvents = useCallback(async (contract: Contract, currentBlockNumber: number) => { 135 | if (currentBlockNumber <= lastCheckedBlockRef.current) { 136 | return; // No new blocks, skip checking for events 137 | } 138 | try { 139 | // Fetch events only for the new blocks 140 | const fromBlock = lastCheckedBlockRef.current + 1; 141 | const fetchedEvents = await provider.getEvents({ 142 | address: contract.address, 143 | from_block: { block_number: fromBlock }, 144 | to_block: { block_number: currentBlockNumber }, 145 | chunk_size: 500, 146 | }); 147 | 148 | if (fetchedEvents && fetchedEvents.events) { 149 | setEvents(prevEvents => [...prevEvents, ...fetchedEvents.events]); 150 | } 151 | 152 | lastCheckedBlockRef.current = currentBlockNumber; 153 | } catch (error) { 154 | console.error('Error checking for events:', error); 155 | } 156 | }, [provider]); 157 | 158 | useEffect(() => { 159 | if (contract && blockNumber) { 160 | checkForEvents(contract, blockNumber); 161 | } 162 | }, [contract, blockNumber, checkForEvents]); 163 | const lastFiveEvents = useMemo(() => { 164 | return [...events].reverse().slice(0, 5); 165 | }, [events]); 166 | // Step 6 --> Get events from a contract -- End 167 | 168 | return ( 169 |
170 |

Starknet Frontend Workshop

171 | 172 |
173 | 174 |
175 | 176 |
177 |

Wallet Connection

178 | 179 |
180 | 181 | {/* Step 1 --> Read the latest block -- Start */} 182 | {!blockNumberIsLoading && !blockNumberIsError && ( 183 |
184 |

Read the Blockchain

185 |

Current Block: {blockNumberData}

186 |

{blockNumberData! < workshopEnds ? "Workshop is live" : "Workshop has ended"}

187 |
188 | )} 189 | {/*
190 |

Read the Blockchain

191 |

Current Block: 0

192 |
*/} 193 | {/* Step 1 --> Read the latest block -- End */} 194 | 195 | {/* Step 2 --> Read your balance -- Start */} 196 | {!balanceIsLoading && !balanceIsError && userAddress && ( 197 |
198 |

Your Balance

199 |

Symbol: {balanceData?.symbol}

200 |

Balance: {Number(balanceData?.formatted).toFixed(4)}

201 |
202 | )} 203 | {/*
204 |

Your Balance

205 |

Symbol: XYZ

206 |

Balance: 100

207 |
*/} 208 | {/* Step 2 --> Read your balance -- End */} 209 | 210 | {/* Step 5 --> Reset balance by owner only -- Start */} 211 |
212 |

Reset Balance

213 | 220 | {resetData?.transaction_hash && ( 221 |

222 | Transaction sent: {resetData.transaction_hash} 223 |

224 | )} 225 |
226 | {/*
227 |

Reset Balance

228 | 235 |

236 | Transaction sent: url 237 |

238 |
*/} 239 | {/* Step 5 --> Reset balance by owner only -- End */} 240 |
241 | 242 |
243 | 244 | {/* Step 3 --> Read from a contract -- Start */} 245 |
246 |

Contract Balance

247 |

Balance: {readData?.toString()}

248 | 254 |
255 | {/*
256 |

Contract Balance

257 |

Balance: 0

258 | 264 |
*/} 265 | {/* Step 3 --> Read from a contract -- End */} 266 | 267 | {/* Step 4 --> Write to a contract -- Start */} 268 |
269 |

Write to Contract

270 | 271 | 278 | 285 | {writeData?.transaction_hash && ( 286 | 292 | Check TX on Sepolia 293 | 294 | )} 295 |
296 | {/*
297 |

Write to Contract

298 | 299 | 304 | 310 | 316 | Check TX on Sepolia 317 | 318 |
*/} 319 | {/* Step 4 --> Write to a contract -- End */} 320 | 321 | {/* Step 6 --> Get events from a contract -- Start */} 322 |
323 |

324 | Contract Events ({events.length}) 325 |

326 |
327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | {lastFiveEvents.map((event, index) => ( 337 | 338 | 339 | 340 | 341 | 342 | ))} 343 | 344 |
SenderAddedNew Balance
{shortenAddress(event.keys[1])}{formatAmount(event.data[0])}{formatAmount(event.data[2])}
345 |
346 |
347 | {/*
348 |

349 | Contract Events (X) 350 |

351 |
352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 |
SenderAddedNew Balance
0xNestor11
368 |
369 |
*/} 370 | {/* Step 6 --> Get events from a contract -- End */} 371 |
372 |
373 |
374 | ); 375 | }; 376 | 377 | export default Page; -------------------------------------------------------------------------------- /web/src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ReactNode } from "react"; 3 | 4 | import { sepolia } from "@starknet-react/chains"; 5 | import { 6 | StarknetConfig, 7 | argent, 8 | braavos, 9 | useInjectedConnectors, 10 | jsonRpcProvider, 11 | voyager, 12 | } from "@starknet-react/core"; 13 | 14 | export function Providers({ children }: { children: ReactNode }) { 15 | const { connectors } = useInjectedConnectors({ 16 | // Show these connectors if the user has no connector installed. 17 | recommended: [argent(), braavos()], 18 | // Hide recommended connectors if the user has any connector installed. 19 | includeRecommended: "onlyIfNoConnectors", 20 | // Randomize the order of the connectors. 21 | order: "random", 22 | }); 23 | return ( 24 | ({ nodeUrl: process.env.NEXT_PUBLIC_RPC_URL }) })} 27 | connectors={connectors} 28 | explorer={voyager} 29 | > 30 | {children} 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /web/src/components/WalletBar.tsx: -------------------------------------------------------------------------------- 1 | import { useConnect, useDisconnect, useAccount } from '@starknet-react/core'; 2 | 3 | const WalletBar: React.FC = () => { 4 | const { connect, connectors } = useConnect(); 5 | const { disconnect } = useDisconnect(); 6 | const { address } = useAccount(); 7 | 8 | return ( 9 |
10 | {!address ? ( 11 |
12 | {connectors.map((connector) => ( 13 | 20 | ))} 21 |
22 | ) : ( 23 |
24 |
25 | Connected: {address.slice(0, 6)}...{address.slice(-4)} 26 |
27 | 33 |
34 | )} 35 |
36 | ); 37 | }; 38 | 39 | export default WalletBar; 40 | -------------------------------------------------------------------------------- /web/src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ButtonProps extends React.ButtonHTMLAttributes { 4 | children: React.ReactNode; 5 | } 6 | 7 | export const Button = ({ children, ...props }: ButtonProps) => { 8 | return ( 9 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | // Helper function to shorten address 3 | export const shortenAddress = (address: string) => { 4 | return `${address.slice(0, 6)}...${address.slice(-4)}`; 5 | }; 6 | 7 | // Helper function to convert hex to decimal and format it 8 | export const formatAmount = (hex: string) => { 9 | const decimal = parseInt(hex, 16); 10 | return decimal.toString(); 11 | }; -------------------------------------------------------------------------------- /web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [ 19 | require('@tailwindcss/forms'), 20 | ], 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } --------------------------------------------------------------------------------