├── 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 |
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 |
119 |
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 | ]
--------------------------------------------------------------------------------