├── frontend ├── .eslintrc.json ├── next.config.mjs ├── app │ ├── favicon.ico │ ├── layout.tsx │ ├── CustomProvider.tsx │ ├── globals.css │ └── page.tsx ├── postcss.config.mjs ├── lib │ └── utils.ts ├── components │ ├── Footer.tsx │ ├── Header.tsx │ └── ui │ │ ├── input.tsx │ │ ├── alert.tsx │ │ └── button.tsx ├── components.json ├── .gitignore ├── public │ ├── vercel.svg │ └── next.svg ├── tsconfig.json ├── package.json ├── README.md ├── tailwind.config.ts └── constants │ └── index.ts ├── backend ├── tsconfig.json ├── ignition │ └── modules │ │ └── BenBKToken.ts ├── .gitignore ├── hardhat.config.ts ├── README.md ├── package.json ├── contracts │ ├── BenBKToken.sol │ └── DataConsumerV3.sol └── test │ └── BenBKToken.test.ts └── README.md /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenBktech/Fullstack-DApp-ChainLink-Data-Feeds-Forking-Hardhat-Typescript-NextJS-Wagmi/HEAD/frontend/app/favicon.ico -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 | © All rights reserved Ben BK {new Date().getFullYear()} 7 |
8 | ) 9 | } 10 | 11 | export default Footer -------------------------------------------------------------------------------- /backend/ignition/modules/BenBKToken.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const BenBKTokenModule = buildModule("BenBKTokenModule", (m) => { 4 | 5 | const BenBKToken = m.contract("BenBKToken"); 6 | 7 | return { BenBKToken }; 8 | }); 9 | 10 | export default BenBKTokenModule; 11 | -------------------------------------------------------------------------------- /frontend/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "@rainbow-me/rainbowkit" 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |
Logo
7 | 8 |
9 | ) 10 | } 11 | 12 | export default Header -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | # Hardhat Ignition default folder for deployments against a local node 17 | ignition/deployments/chain-31337 18 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /backend/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | 4 | const config: HardhatUserConfig = { 5 | solidity: "0.8.24", 6 | networks: { 7 | hardhat: { 8 | forking: { 9 | url: "https://mainnet.infura.io/v3/6e4dcb86b707401a84e505b5ae7c8964", 10 | blockNumber: 20228494 11 | } 12 | } 13 | } 14 | }; 15 | 16 | export default config; -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat help 9 | npx hardhat test 10 | REPORT_GAS=true npx hardhat test 11 | npx hardhat node 12 | npx hardhat ignition deploy ./ignition/modules/Lock.ts 13 | ``` 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import CustomProvider from "./CustomProvider"; 2 | import type { Metadata } from "next"; 3 | import "./globals.css"; 4 | 5 | import { Inter as FontSans } from "next/font/google" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const fontSans = FontSans({ 10 | subsets: ["latin"], 11 | variable: "--font-sans", 12 | }) 13 | 14 | export const metadata: Metadata = { 15 | title: "Create Next App", 16 | description: "Generated by create next app", 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 27 | 33 | 34 | {children} 35 | 36 | 37 | 38 | ); 39 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-slot": "^1.1.0", 13 | "@rainbow-me/rainbowkit": "^2.1.3", 14 | "@tanstack/react-query": "^5.49.2", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.400.0", 18 | "next": "14.2.4", 19 | "react": "^18", 20 | "react-dom": "^18", 21 | "tailwind-merge": "^2.3.0", 22 | "tailwindcss-animate": "^1.0.7", 23 | "viem": "^2.17.0", 24 | "wagmi": "^2.10.9" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "eslint": "^8", 31 | "eslint-config-next": "14.2.4", 32 | "postcss": "^8", 33 | "tailwindcss": "^3.4.1", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@chainlink/contracts": "^1.1.1", 8 | "@openzeppelin/contracts": "^5.0.2", 9 | "hardhat": "^2.22.6" 10 | }, 11 | "devDependencies": { 12 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", 13 | "@nomicfoundation/hardhat-ethers": "^3.0.0", 14 | "@nomicfoundation/hardhat-ignition": "^0.15.0", 15 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", 16 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 17 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 18 | "@nomicfoundation/hardhat-verify": "^2.0.0", 19 | "@typechain/ethers-v6": "^0.5.0", 20 | "@typechain/hardhat": "^9.0.0", 21 | "@types/chai": "^4.2.0", 22 | "@types/mocha": ">=9.1.0", 23 | "@types/node": ">=18.0.0", 24 | "chai": "^4.2.0", 25 | "ethers": "^6.4.0", 26 | "hardhat-gas-reporter": "^1.0.8", 27 | "solidity-coverage": "^0.8.0", 28 | "ts-node": ">=8.0.0", 29 | "typechain": "^8.3.0", 30 | "typescript": ">=4.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/app/CustomProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import '@rainbow-me/rainbowkit/styles.css'; 3 | 4 | import { 5 | getDefaultConfig, 6 | RainbowKitProvider, 7 | darkTheme 8 | } from '@rainbow-me/rainbowkit'; 9 | import { WagmiProvider } from 'wagmi'; 10 | import { 11 | hardhat 12 | } from 'wagmi/chains'; 13 | import { 14 | QueryClientProvider, 15 | QueryClient, 16 | } from "@tanstack/react-query"; 17 | 18 | const config = getDefaultConfig({ 19 | appName: 'My RainbowKit App', 20 | projectId: 'YOUR_PROJECT_ID', 21 | chains: [hardhat], 22 | ssr: true, // If your dApp uses server side rendering (SSR) 23 | }); 24 | 25 | const queryClient = new QueryClient(); 26 | 27 | const CustomProvider = ({ 28 | children, 29 | }: Readonly<{ 30 | children: React.ReactNode; 31 | }>) => { 32 | return ( 33 | 34 | 35 | 38 | {children} 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default CustomProvider -------------------------------------------------------------------------------- /backend/contracts/BenBKToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import "hardhat/console.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "./DataConsumerV3.sol"; 9 | 10 | contract BenBKToken is ERC20, Ownable, DataConsumerV3 { 11 | 12 | uint256 private constant PRICE_PER_TOKEN = 10; // in dollars 13 | 14 | constructor() 15 | ERC20("BenBK", "BBK") 16 | Ownable(msg.sender) 17 | {} 18 | 19 | function mint(address _to, uint256 _amount) external payable { 20 | int256 ethInDollars = getChainlinkDataFeedLatestAnswer(); 21 | require(ethInDollars > 0, "Invalid price feed value"); 22 | 23 | // Convert int256 to uint256 for division 24 | uint256 ethInDollarsUint = uint256(ethInDollars / 1e8); 25 | 26 | uint256 expectedPriceInWei = (PRICE_PER_TOKEN * 1 ether * _amount) / ethInDollarsUint; // Convert to wei 27 | console.log("ethInDollars:", ethInDollarsUint); 28 | console.log("expectedPriceInWei:", expectedPriceInWei); 29 | console.log("value provided:", msg.value); 30 | 31 | require(msg.value >= expectedPriceInWei, "Not enough funds provided"); 32 | _mint(_to, _amount); 33 | } 34 | } -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BBK Token Minting DApp 2 | 3 | ## Overview 4 | 5 | This project is a decentralized application (DApp) that allows users to mint their own BBK tokens. The application is built using React, Tailwind CSS, and shadcn UI components, and interacts with an Ethereum smart contract using the Wagmi library. 6 | 7 | ## Features 8 | 9 | - **Connect Wallet**: Users can connect their Ethereum wallet to the DApp. 10 | - **Mint Tokens**: Users can mint BBK tokens by selecting different amounts ($100, $200, $500, $1000). 11 | - **Transaction Status**: Users can see the transaction hash, waiting confirmation status, and transaction confirmation status. 12 | - **Error Handling**: Errors during the transaction process are displayed to the user. 13 | 14 | ## Technologies Used 15 | 16 | - **React**: Frontend framework for building user interfaces. 17 | - **Tailwind CSS**: Utility-first CSS framework for styling. 18 | - **shadcn UI**: Component library used for buttons, alerts, and other UI elements. 19 | - **Wagmi**: React hooks library for Ethereum. 20 | - **Viem**: Library for Ethereum-related utility functions. 21 | 22 | ## Setup 23 | 24 | ### Prerequisites 25 | 26 | - Node.js (LTS or later) 27 | - npm or yarn 28 | 29 | ### Contributing 30 | 31 | - Fork the repository 32 | - Create your feature branch (git checkout -b feature/fooBar) 33 | - Commit your changes (git commit -m 'Add some fooBar') 34 | - Push to the branch (git push origin feature/fooBar) 35 | - Create a new Pull Request 36 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /backend/contracts/DataConsumerV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; 5 | 6 | /** 7 | * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED 8 | * VALUES FOR CLARITY. 9 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 10 | * DO NOT USE THIS CODE IN PRODUCTION. 11 | */ 12 | 13 | /** 14 | * If you are reading data feeds on L2 networks, you must 15 | * check the latest answer from the L2 Sequencer Uptime 16 | * Feed to ensure that the data is accurate in the event 17 | * of an L2 sequencer outage. See the 18 | * https://docs.chain.link/data-feeds/l2-sequencer-feeds 19 | * page for details. 20 | */ 21 | 22 | contract DataConsumerV3 { 23 | AggregatorV3Interface internal dataFeed; 24 | 25 | /** 26 | * Network: Sepolia 27 | * Aggregator: ETH/USD 28 | * Address: 0x694AA1769357215DE4FAC081bf1f309aDC325306 29 | */ 30 | constructor() { 31 | dataFeed = AggregatorV3Interface( 32 | 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 33 | ); 34 | } 35 | 36 | /** 37 | * Returns the latest answer. 38 | */ 39 | function getChainlinkDataFeedLatestAnswer() public view returns (int) { 40 | // prettier-ignore 41 | ( 42 | /* uint80 roundID */, 43 | int answer, 44 | /*uint startedAt*/, 45 | /*uint timeStamp*/, 46 | /*uint80 answeredInRound*/ 47 | ) = dataFeed.latestRoundData(); 48 | return answer; 49 | } 50 | } -------------------------------------------------------------------------------- /frontend/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /frontend/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | const { fontFamily } = require("tailwindcss/defaultTheme") 3 | 4 | const config = { 5 | darkMode: ["class"], 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | prefix: "", 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: "2rem", 17 | screens: { 18 | "2xl": "1400px", 19 | }, 20 | }, 21 | extend: { 22 | fontFamily: { 23 | sans: ["var(--font-sans)", ...fontFamily.sans], 24 | }, 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | }, 60 | borderRadius: { 61 | lg: "var(--radius)", 62 | md: "calc(var(--radius) - 2px)", 63 | sm: "calc(var(--radius) - 4px)", 64 | }, 65 | keyframes: { 66 | "accordion-down": { 67 | from: { height: "0" }, 68 | to: { height: "var(--radix-accordion-content-height)" }, 69 | }, 70 | "accordion-up": { 71 | from: { height: "var(--radix-accordion-content-height)" }, 72 | to: { height: "0" }, 73 | }, 74 | }, 75 | animation: { 76 | "accordion-down": "accordion-down 0.2s ease-out", 77 | "accordion-up": "accordion-up 0.2s ease-out", 78 | }, 79 | }, 80 | }, 81 | plugins: [require("tailwindcss-animate")], 82 | } satisfies Config 83 | 84 | export default config -------------------------------------------------------------------------------- /backend/test/BenBKToken.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | loadFixture, 3 | } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 4 | import { expect, assert } from "chai"; 5 | import hre from "hardhat"; 6 | 7 | describe("BenBKToken Tests", function () { 8 | // We define a fixture to reuse the same setup in every test. 9 | // We use loadFixture to run this setup once, snapshot that state, 10 | // and reset Hardhat Network to that snapshot in every test. 11 | async function deployFixture() { 12 | // Contracts are deployed using the first signer/account by default 13 | const [owner, otherAccount] = await hre.ethers.getSigners(); 14 | 15 | const BenBKToken = await hre.ethers.getContractFactory("BenBKToken"); 16 | const benBKToken = await BenBKToken.deploy(); 17 | 18 | return { benBKToken, owner, otherAccount }; 19 | } 20 | 21 | describe("Deployment", function () { 22 | it("Should deploy the contract and get the right price for 1 ETH", async function () { 23 | const { benBKToken, owner, otherAccount } = await loadFixture(deployFixture); 24 | const ethPriceFromChainlink = await benBKToken.getChainlinkDataFeedLatestAnswer(); 25 | const ethInDollars = hre.ethers.formatUnits(ethPriceFromChainlink, 8); 26 | assert(parseInt(ethInDollars) >= 3000 && parseInt(ethInDollars) <= 3500); 27 | }); 28 | }); 29 | 30 | describe("Mint", function () { 31 | it("Should NOT mint if not enough funds are provided", async function () { 32 | const { benBKToken, owner, otherAccount } = await loadFixture(deployFixture); 33 | const ethPriceFromChainlink = await benBKToken.getChainlinkDataFeedLatestAnswer(); 34 | const ethInDollars = hre.ethers.formatUnits(ethPriceFromChainlink, 8); 35 | const amountMint = 18; 36 | const amountEthFor18Tokens = (10 * amountMint) / parseInt(ethInDollars) 37 | const priceFor18Tokens = hre.ethers.parseEther(amountEthFor18Tokens.toString()); 38 | console.log(priceFor18Tokens); 39 | await expect(benBKToken.mint(owner.address, 20, { value: priceFor18Tokens })).to.be.revertedWith('Not enough funds provided'); 40 | }); 41 | 42 | it("Should mint if enough funds are provided", async function () { 43 | const { benBKToken, owner, otherAccount } = await loadFixture(deployFixture); 44 | const ethPriceFromChainlink = await benBKToken.getChainlinkDataFeedLatestAnswer(); 45 | const ethInDollars = hre.ethers.formatUnits(ethPriceFromChainlink, 8); 46 | const amountMint = 20; 47 | const amountEthFor20Tokens = (10 * amountMint) / parseInt(ethInDollars) 48 | const priceFor20Tokens = hre.ethers.parseEther(amountEthFor20Tokens.toString()); 49 | console.log(priceFor20Tokens) 50 | await benBKToken.mint(owner.address, 20, { value: priceFor20Tokens }); 51 | const balanceOfOwner = await benBKToken.balanceOf(owner.address); 52 | assert(Number(balanceOfOwner) === 20); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useEffect, useState } from "react"; 3 | import Footer from "@/components/Footer"; 4 | import Header from "@/components/Header"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Input } from "@/components/ui/input"; 7 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 8 | import { contractAddress, contractAbi } from "@/constants"; 9 | import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, type BaseError } from "wagmi"; 10 | import { parseEther } from "viem"; 11 | 12 | export default function Home() { 13 | const { isConnected, address } = useAccount(); 14 | const [ethPriceInUSD, setEthPriceInUSD] = useState(null); 15 | 16 | const { data: ethPrice } = useReadContract({ 17 | address: contractAddress, 18 | abi: contractAbi, 19 | functionName: 'getChainlinkDataFeedLatestAnswer', 20 | }); 21 | 22 | useEffect(() => { 23 | if (ethPrice) { 24 | const ethPriceInUSD = Number(ethPrice) / 10**8; 25 | setEthPriceInUSD(ethPriceInUSD); // Price of 1 ETHER in USD 26 | } 27 | }, [ethPrice]); 28 | 29 | const { data: hash, error, isPending, writeContract } = useWriteContract(); 30 | 31 | const mint = async (amountInUSD: number) => { 32 | let price; 33 | try { 34 | if (ethPriceInUSD) { 35 | price = (amountInUSD + 0.01*(amountInUSD)) / ethPriceInUSD; 36 | price = parseEther((price).toString()); 37 | } 38 | console.log(price); 39 | writeContract({ 40 | address: contractAddress, 41 | abi: contractAbi, 42 | functionName: 'mint', 43 | args: [address, amountInUSD / 10], 44 | value: price 45 | }); 46 | console.log('oups'); 47 | } catch (error) { 48 | console.log(error); 49 | } 50 | }; 51 | 52 | const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ 53 | hash, 54 | }); 55 | 56 | useEffect(() => { 57 | if (error) { 58 | console.log((error as BaseError).shortMessage || error.message); 59 | } 60 | }, [error]); 61 | 62 | const renderMintOption = (amountInUSD: number) => ( 63 |
64 |

65 | ${amountInUSD} (~{ethPriceInUSD && (amountInUSD / ethPriceInUSD).toFixed(4)} ETH) 66 |

67 | 70 |
71 | ); 72 | 73 | return ( 74 |
75 |
76 |
77 | {isConnected ? ( 78 | <> 79 |

Mint Your Own BBK Tokens

80 |

Choose the amount you want to mint:

81 |
82 | {renderMintOption(100)} 83 | {renderMintOption(200)} 84 | {renderMintOption(500)} 85 | {renderMintOption(1000)} 86 |
87 |
88 | {hash && 89 | Transaction Hash 90 | 91 | {hash} 92 | 93 | } 94 | {isConfirming && 95 | A moment.. 96 | 97 | Waiting for confirmation... 98 | 99 | } 100 | {isConfirmed && 101 | Congratulations! 102 | 103 | Transaction confirmed. 104 | 105 | } 106 | {error && ( 107 | Error! 108 | 109 | {(error as BaseError).shortMessage || error.message} 110 | 111 | )} 112 |
113 | 114 | ) : ( 115 |
Please connect your wallet to mint tokens.
116 | )} 117 |
118 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /frontend/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const contractAddress="0x00B0517de6b2b09aBD3a7B69d66D85eFdb2c7d94"; 2 | export const contractAbi=[ 3 | { 4 | "inputs": [], 5 | "stateMutability": "nonpayable", 6 | "type": "constructor" 7 | }, 8 | { 9 | "inputs": [ 10 | { 11 | "internalType": "address", 12 | "name": "spender", 13 | "type": "address" 14 | }, 15 | { 16 | "internalType": "uint256", 17 | "name": "allowance", 18 | "type": "uint256" 19 | }, 20 | { 21 | "internalType": "uint256", 22 | "name": "needed", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "ERC20InsufficientAllowance", 27 | "type": "error" 28 | }, 29 | { 30 | "inputs": [ 31 | { 32 | "internalType": "address", 33 | "name": "sender", 34 | "type": "address" 35 | }, 36 | { 37 | "internalType": "uint256", 38 | "name": "balance", 39 | "type": "uint256" 40 | }, 41 | { 42 | "internalType": "uint256", 43 | "name": "needed", 44 | "type": "uint256" 45 | } 46 | ], 47 | "name": "ERC20InsufficientBalance", 48 | "type": "error" 49 | }, 50 | { 51 | "inputs": [ 52 | { 53 | "internalType": "address", 54 | "name": "approver", 55 | "type": "address" 56 | } 57 | ], 58 | "name": "ERC20InvalidApprover", 59 | "type": "error" 60 | }, 61 | { 62 | "inputs": [ 63 | { 64 | "internalType": "address", 65 | "name": "receiver", 66 | "type": "address" 67 | } 68 | ], 69 | "name": "ERC20InvalidReceiver", 70 | "type": "error" 71 | }, 72 | { 73 | "inputs": [ 74 | { 75 | "internalType": "address", 76 | "name": "sender", 77 | "type": "address" 78 | } 79 | ], 80 | "name": "ERC20InvalidSender", 81 | "type": "error" 82 | }, 83 | { 84 | "inputs": [ 85 | { 86 | "internalType": "address", 87 | "name": "spender", 88 | "type": "address" 89 | } 90 | ], 91 | "name": "ERC20InvalidSpender", 92 | "type": "error" 93 | }, 94 | { 95 | "inputs": [ 96 | { 97 | "internalType": "address", 98 | "name": "owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "OwnableInvalidOwner", 103 | "type": "error" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "address", 109 | "name": "account", 110 | "type": "address" 111 | } 112 | ], 113 | "name": "OwnableUnauthorizedAccount", 114 | "type": "error" 115 | }, 116 | { 117 | "anonymous": false, 118 | "inputs": [ 119 | { 120 | "indexed": true, 121 | "internalType": "address", 122 | "name": "owner", 123 | "type": "address" 124 | }, 125 | { 126 | "indexed": true, 127 | "internalType": "address", 128 | "name": "spender", 129 | "type": "address" 130 | }, 131 | { 132 | "indexed": false, 133 | "internalType": "uint256", 134 | "name": "value", 135 | "type": "uint256" 136 | } 137 | ], 138 | "name": "Approval", 139 | "type": "event" 140 | }, 141 | { 142 | "anonymous": false, 143 | "inputs": [ 144 | { 145 | "indexed": true, 146 | "internalType": "address", 147 | "name": "previousOwner", 148 | "type": "address" 149 | }, 150 | { 151 | "indexed": true, 152 | "internalType": "address", 153 | "name": "newOwner", 154 | "type": "address" 155 | } 156 | ], 157 | "name": "OwnershipTransferred", 158 | "type": "event" 159 | }, 160 | { 161 | "anonymous": false, 162 | "inputs": [ 163 | { 164 | "indexed": true, 165 | "internalType": "address", 166 | "name": "from", 167 | "type": "address" 168 | }, 169 | { 170 | "indexed": true, 171 | "internalType": "address", 172 | "name": "to", 173 | "type": "address" 174 | }, 175 | { 176 | "indexed": false, 177 | "internalType": "uint256", 178 | "name": "value", 179 | "type": "uint256" 180 | } 181 | ], 182 | "name": "Transfer", 183 | "type": "event" 184 | }, 185 | { 186 | "inputs": [ 187 | { 188 | "internalType": "address", 189 | "name": "owner", 190 | "type": "address" 191 | }, 192 | { 193 | "internalType": "address", 194 | "name": "spender", 195 | "type": "address" 196 | } 197 | ], 198 | "name": "allowance", 199 | "outputs": [ 200 | { 201 | "internalType": "uint256", 202 | "name": "", 203 | "type": "uint256" 204 | } 205 | ], 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [ 211 | { 212 | "internalType": "address", 213 | "name": "spender", 214 | "type": "address" 215 | }, 216 | { 217 | "internalType": "uint256", 218 | "name": "value", 219 | "type": "uint256" 220 | } 221 | ], 222 | "name": "approve", 223 | "outputs": [ 224 | { 225 | "internalType": "bool", 226 | "name": "", 227 | "type": "bool" 228 | } 229 | ], 230 | "stateMutability": "nonpayable", 231 | "type": "function" 232 | }, 233 | { 234 | "inputs": [ 235 | { 236 | "internalType": "address", 237 | "name": "account", 238 | "type": "address" 239 | } 240 | ], 241 | "name": "balanceOf", 242 | "outputs": [ 243 | { 244 | "internalType": "uint256", 245 | "name": "", 246 | "type": "uint256" 247 | } 248 | ], 249 | "stateMutability": "view", 250 | "type": "function" 251 | }, 252 | { 253 | "inputs": [], 254 | "name": "decimals", 255 | "outputs": [ 256 | { 257 | "internalType": "uint8", 258 | "name": "", 259 | "type": "uint8" 260 | } 261 | ], 262 | "stateMutability": "view", 263 | "type": "function" 264 | }, 265 | { 266 | "inputs": [], 267 | "name": "getChainlinkDataFeedLatestAnswer", 268 | "outputs": [ 269 | { 270 | "internalType": "int256", 271 | "name": "", 272 | "type": "int256" 273 | } 274 | ], 275 | "stateMutability": "view", 276 | "type": "function" 277 | }, 278 | { 279 | "inputs": [ 280 | { 281 | "internalType": "address", 282 | "name": "_to", 283 | "type": "address" 284 | }, 285 | { 286 | "internalType": "uint256", 287 | "name": "_amount", 288 | "type": "uint256" 289 | } 290 | ], 291 | "name": "mint", 292 | "outputs": [], 293 | "stateMutability": "payable", 294 | "type": "function" 295 | }, 296 | { 297 | "inputs": [], 298 | "name": "name", 299 | "outputs": [ 300 | { 301 | "internalType": "string", 302 | "name": "", 303 | "type": "string" 304 | } 305 | ], 306 | "stateMutability": "view", 307 | "type": "function" 308 | }, 309 | { 310 | "inputs": [], 311 | "name": "owner", 312 | "outputs": [ 313 | { 314 | "internalType": "address", 315 | "name": "", 316 | "type": "address" 317 | } 318 | ], 319 | "stateMutability": "view", 320 | "type": "function" 321 | }, 322 | { 323 | "inputs": [], 324 | "name": "renounceOwnership", 325 | "outputs": [], 326 | "stateMutability": "nonpayable", 327 | "type": "function" 328 | }, 329 | { 330 | "inputs": [], 331 | "name": "symbol", 332 | "outputs": [ 333 | { 334 | "internalType": "string", 335 | "name": "", 336 | "type": "string" 337 | } 338 | ], 339 | "stateMutability": "view", 340 | "type": "function" 341 | }, 342 | { 343 | "inputs": [], 344 | "name": "totalSupply", 345 | "outputs": [ 346 | { 347 | "internalType": "uint256", 348 | "name": "", 349 | "type": "uint256" 350 | } 351 | ], 352 | "stateMutability": "view", 353 | "type": "function" 354 | }, 355 | { 356 | "inputs": [ 357 | { 358 | "internalType": "address", 359 | "name": "to", 360 | "type": "address" 361 | }, 362 | { 363 | "internalType": "uint256", 364 | "name": "value", 365 | "type": "uint256" 366 | } 367 | ], 368 | "name": "transfer", 369 | "outputs": [ 370 | { 371 | "internalType": "bool", 372 | "name": "", 373 | "type": "bool" 374 | } 375 | ], 376 | "stateMutability": "nonpayable", 377 | "type": "function" 378 | }, 379 | { 380 | "inputs": [ 381 | { 382 | "internalType": "address", 383 | "name": "from", 384 | "type": "address" 385 | }, 386 | { 387 | "internalType": "address", 388 | "name": "to", 389 | "type": "address" 390 | }, 391 | { 392 | "internalType": "uint256", 393 | "name": "value", 394 | "type": "uint256" 395 | } 396 | ], 397 | "name": "transferFrom", 398 | "outputs": [ 399 | { 400 | "internalType": "bool", 401 | "name": "", 402 | "type": "bool" 403 | } 404 | ], 405 | "stateMutability": "nonpayable", 406 | "type": "function" 407 | }, 408 | { 409 | "inputs": [ 410 | { 411 | "internalType": "address", 412 | "name": "newOwner", 413 | "type": "address" 414 | } 415 | ], 416 | "name": "transferOwnership", 417 | "outputs": [], 418 | "stateMutability": "nonpayable", 419 | "type": "function" 420 | } 421 | ] --------------------------------------------------------------------------------