├── .eslintrc.cjs ├── .gitignore ├── README.md ├── components ├── Footer.jsx ├── HeaderStat.jsx └── Navbar.jsx ├── helpers ├── constants.js └── contracts.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── nft.png └── vite.svg ├── src ├── App.css ├── App.jsx ├── Home.jsx ├── assets │ └── react.svg ├── index.css └── main.jsx ├── styles ├── Home.module.css └── globals.css ├── vite.config.js └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Container, Divider, Stack, Text } from "@chakra-ui/react"; 2 | 3 | export const Footer = () => ( 4 | 5 | 6 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /components/HeaderStat.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading, Stack, Text, useBreakpointValue, useColorModeValue } from "@chakra-ui/react" 2 | 3 | export const HeaderStat = (props) => { 4 | const { label, value, ...boxProps } = props 5 | return ( 6 | 20 | 21 | 22 | {label} 23 | 24 | 30 | {value} 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Container, 3 | Box, 4 | HStack, 5 | Flex, 6 | ButtonGroup, 7 | Button, 8 | useColorModeValue, 9 | } from "@chakra-ui/react"; 10 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 11 | 12 | export const Navbar = () => ( 13 | 14 | 19 | 20 | 21 | 22 | 23 | {["Coin NFT Staking"].map((item) => ( 24 | 25 | ))} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const NFT_KEY = 2 | "0000000000000000000000000000000000000000000000000000000000000500"; 3 | -------------------------------------------------------------------------------- /helpers/contracts.js: -------------------------------------------------------------------------------- 1 | export const tokenABI = [ 2 | { 3 | inputs: [ 4 | { 5 | internalType: "uint256", 6 | name: "initialSupply", 7 | type: "uint256", 8 | }, 9 | ], 10 | stateMutability: "nonpayable", 11 | type: "constructor", 12 | }, 13 | { 14 | anonymous: false, 15 | inputs: [ 16 | { 17 | indexed: true, 18 | internalType: "address", 19 | name: "owner", 20 | type: "address", 21 | }, 22 | { 23 | indexed: true, 24 | internalType: "address", 25 | name: "spender", 26 | type: "address", 27 | }, 28 | { 29 | indexed: false, 30 | internalType: "uint256", 31 | name: "value", 32 | type: "uint256", 33 | }, 34 | ], 35 | name: "Approval", 36 | type: "event", 37 | }, 38 | { 39 | anonymous: false, 40 | inputs: [ 41 | { 42 | indexed: true, 43 | internalType: "address", 44 | name: "from", 45 | type: "address", 46 | }, 47 | { 48 | indexed: true, 49 | internalType: "address", 50 | name: "to", 51 | type: "address", 52 | }, 53 | { 54 | indexed: false, 55 | internalType: "uint256", 56 | name: "value", 57 | type: "uint256", 58 | }, 59 | ], 60 | name: "Transfer", 61 | type: "event", 62 | }, 63 | { 64 | inputs: [ 65 | { 66 | internalType: "address", 67 | name: "owner", 68 | type: "address", 69 | }, 70 | { 71 | internalType: "address", 72 | name: "spender", 73 | type: "address", 74 | }, 75 | ], 76 | name: "allowance", 77 | outputs: [ 78 | { 79 | internalType: "uint256", 80 | name: "", 81 | type: "uint256", 82 | }, 83 | ], 84 | stateMutability: "view", 85 | type: "function", 86 | }, 87 | { 88 | inputs: [ 89 | { 90 | internalType: "address", 91 | name: "spender", 92 | type: "address", 93 | }, 94 | { 95 | internalType: "uint256", 96 | name: "amount", 97 | type: "uint256", 98 | }, 99 | ], 100 | name: "approve", 101 | outputs: [ 102 | { 103 | internalType: "bool", 104 | name: "", 105 | type: "bool", 106 | }, 107 | ], 108 | stateMutability: "nonpayable", 109 | type: "function", 110 | }, 111 | { 112 | inputs: [ 113 | { 114 | internalType: "address", 115 | name: "account", 116 | type: "address", 117 | }, 118 | ], 119 | name: "balanceOf", 120 | outputs: [ 121 | { 122 | internalType: "uint256", 123 | name: "", 124 | type: "uint256", 125 | }, 126 | ], 127 | stateMutability: "view", 128 | type: "function", 129 | }, 130 | { 131 | inputs: [], 132 | name: "decimals", 133 | outputs: [ 134 | { 135 | internalType: "uint8", 136 | name: "", 137 | type: "uint8", 138 | }, 139 | ], 140 | stateMutability: "view", 141 | type: "function", 142 | }, 143 | { 144 | inputs: [ 145 | { 146 | internalType: "address", 147 | name: "spender", 148 | type: "address", 149 | }, 150 | { 151 | internalType: "uint256", 152 | name: "subtractedValue", 153 | type: "uint256", 154 | }, 155 | ], 156 | name: "decreaseAllowance", 157 | outputs: [ 158 | { 159 | internalType: "bool", 160 | name: "", 161 | type: "bool", 162 | }, 163 | ], 164 | stateMutability: "nonpayable", 165 | type: "function", 166 | }, 167 | { 168 | inputs: [ 169 | { 170 | internalType: "address", 171 | name: "spender", 172 | type: "address", 173 | }, 174 | { 175 | internalType: "uint256", 176 | name: "addedValue", 177 | type: "uint256", 178 | }, 179 | ], 180 | name: "increaseAllowance", 181 | outputs: [ 182 | { 183 | internalType: "bool", 184 | name: "", 185 | type: "bool", 186 | }, 187 | ], 188 | stateMutability: "nonpayable", 189 | type: "function", 190 | }, 191 | { 192 | inputs: [], 193 | name: "name", 194 | outputs: [ 195 | { 196 | internalType: "string", 197 | name: "", 198 | type: "string", 199 | }, 200 | ], 201 | stateMutability: "view", 202 | type: "function", 203 | }, 204 | { 205 | inputs: [], 206 | name: "symbol", 207 | outputs: [ 208 | { 209 | internalType: "string", 210 | name: "", 211 | type: "string", 212 | }, 213 | ], 214 | stateMutability: "view", 215 | type: "function", 216 | }, 217 | { 218 | inputs: [], 219 | name: "totalSupply", 220 | outputs: [ 221 | { 222 | internalType: "uint256", 223 | name: "", 224 | type: "uint256", 225 | }, 226 | ], 227 | stateMutability: "view", 228 | type: "function", 229 | }, 230 | { 231 | inputs: [ 232 | { 233 | internalType: "address", 234 | name: "to", 235 | type: "address", 236 | }, 237 | { 238 | internalType: "uint256", 239 | name: "amount", 240 | type: "uint256", 241 | }, 242 | ], 243 | name: "transfer", 244 | outputs: [ 245 | { 246 | internalType: "bool", 247 | name: "", 248 | type: "bool", 249 | }, 250 | ], 251 | stateMutability: "nonpayable", 252 | type: "function", 253 | }, 254 | { 255 | inputs: [ 256 | { 257 | internalType: "address", 258 | name: "from", 259 | type: "address", 260 | }, 261 | { 262 | internalType: "address", 263 | name: "to", 264 | type: "address", 265 | }, 266 | { 267 | internalType: "uint256", 268 | name: "amount", 269 | type: "uint256", 270 | }, 271 | ], 272 | name: "transferFrom", 273 | outputs: [ 274 | { 275 | internalType: "bool", 276 | name: "", 277 | type: "bool", 278 | }, 279 | ], 280 | stateMutability: "nonpayable", 281 | type: "function", 282 | }, 283 | ]; 284 | export const nftABI = [ 285 | { 286 | inputs: [ 287 | { 288 | internalType: "uint256", 289 | name: "_mintPrice", 290 | type: "uint256", 291 | }, 292 | ], 293 | stateMutability: "nonpayable", 294 | type: "constructor", 295 | }, 296 | { 297 | inputs: [], 298 | name: "ContractPaused", 299 | type: "error", 300 | }, 301 | { 302 | inputs: [], 303 | name: "NotOwner", 304 | type: "error", 305 | }, 306 | { 307 | anonymous: false, 308 | inputs: [ 309 | { 310 | indexed: true, 311 | internalType: "address", 312 | name: "account", 313 | type: "address", 314 | }, 315 | { 316 | indexed: true, 317 | internalType: "address", 318 | name: "operator", 319 | type: "address", 320 | }, 321 | { 322 | indexed: false, 323 | internalType: "bool", 324 | name: "approved", 325 | type: "bool", 326 | }, 327 | ], 328 | name: "ApprovalForAll", 329 | type: "event", 330 | }, 331 | { 332 | anonymous: false, 333 | inputs: [ 334 | { 335 | indexed: true, 336 | internalType: "address", 337 | name: "operator", 338 | type: "address", 339 | }, 340 | { 341 | indexed: true, 342 | internalType: "address", 343 | name: "from", 344 | type: "address", 345 | }, 346 | { 347 | indexed: true, 348 | internalType: "address", 349 | name: "to", 350 | type: "address", 351 | }, 352 | { 353 | indexed: false, 354 | internalType: "uint256[]", 355 | name: "ids", 356 | type: "uint256[]", 357 | }, 358 | { 359 | indexed: false, 360 | internalType: "uint256[]", 361 | name: "values", 362 | type: "uint256[]", 363 | }, 364 | ], 365 | name: "TransferBatch", 366 | type: "event", 367 | }, 368 | { 369 | anonymous: false, 370 | inputs: [ 371 | { 372 | indexed: true, 373 | internalType: "address", 374 | name: "operator", 375 | type: "address", 376 | }, 377 | { 378 | indexed: true, 379 | internalType: "address", 380 | name: "from", 381 | type: "address", 382 | }, 383 | { 384 | indexed: true, 385 | internalType: "address", 386 | name: "to", 387 | type: "address", 388 | }, 389 | { 390 | indexed: false, 391 | internalType: "uint256", 392 | name: "id", 393 | type: "uint256", 394 | }, 395 | { 396 | indexed: false, 397 | internalType: "uint256", 398 | name: "value", 399 | type: "uint256", 400 | }, 401 | ], 402 | name: "TransferSingle", 403 | type: "event", 404 | }, 405 | { 406 | anonymous: false, 407 | inputs: [ 408 | { 409 | indexed: false, 410 | internalType: "string", 411 | name: "value", 412 | type: "string", 413 | }, 414 | { 415 | indexed: true, 416 | internalType: "uint256", 417 | name: "id", 418 | type: "uint256", 419 | }, 420 | ], 421 | name: "URI", 422 | type: "event", 423 | }, 424 | { 425 | inputs: [], 426 | name: "_paused", 427 | outputs: [ 428 | { 429 | internalType: "bool", 430 | name: "", 431 | type: "bool", 432 | }, 433 | ], 434 | stateMutability: "view", 435 | type: "function", 436 | }, 437 | { 438 | inputs: [ 439 | { 440 | internalType: "address", 441 | name: "account", 442 | type: "address", 443 | }, 444 | { 445 | internalType: "uint256", 446 | name: "id", 447 | type: "uint256", 448 | }, 449 | ], 450 | name: "balanceOf", 451 | outputs: [ 452 | { 453 | internalType: "uint256", 454 | name: "", 455 | type: "uint256", 456 | }, 457 | ], 458 | stateMutability: "view", 459 | type: "function", 460 | }, 461 | { 462 | inputs: [ 463 | { 464 | internalType: "address[]", 465 | name: "accounts", 466 | type: "address[]", 467 | }, 468 | { 469 | internalType: "uint256[]", 470 | name: "ids", 471 | type: "uint256[]", 472 | }, 473 | ], 474 | name: "balanceOfBatch", 475 | outputs: [ 476 | { 477 | internalType: "uint256[]", 478 | name: "", 479 | type: "uint256[]", 480 | }, 481 | ], 482 | stateMutability: "view", 483 | type: "function", 484 | }, 485 | { 486 | inputs: [], 487 | name: "collectFee", 488 | outputs: [], 489 | stateMutability: "nonpayable", 490 | type: "function", 491 | }, 492 | { 493 | inputs: [], 494 | name: "getMintPrice", 495 | outputs: [ 496 | { 497 | internalType: "uint256", 498 | name: "", 499 | type: "uint256", 500 | }, 501 | ], 502 | stateMutability: "view", 503 | type: "function", 504 | }, 505 | { 506 | inputs: [ 507 | { 508 | internalType: "address", 509 | name: "account", 510 | type: "address", 511 | }, 512 | { 513 | internalType: "address", 514 | name: "operator", 515 | type: "address", 516 | }, 517 | ], 518 | name: "isApprovedForAll", 519 | outputs: [ 520 | { 521 | internalType: "bool", 522 | name: "", 523 | type: "bool", 524 | }, 525 | ], 526 | stateMutability: "view", 527 | type: "function", 528 | }, 529 | { 530 | inputs: [ 531 | { 532 | internalType: "uint256", 533 | name: "id", 534 | type: "uint256", 535 | }, 536 | { 537 | internalType: "uint256", 538 | name: "numberOfTokens", 539 | type: "uint256", 540 | }, 541 | ], 542 | name: "mint", 543 | outputs: [], 544 | stateMutability: "payable", 545 | type: "function", 546 | }, 547 | { 548 | inputs: [], 549 | name: "mintPrice", 550 | outputs: [ 551 | { 552 | internalType: "uint256", 553 | name: "", 554 | type: "uint256", 555 | }, 556 | ], 557 | stateMutability: "view", 558 | type: "function", 559 | }, 560 | { 561 | inputs: [], 562 | name: "owner", 563 | outputs: [ 564 | { 565 | internalType: "address", 566 | name: "", 567 | type: "address", 568 | }, 569 | ], 570 | stateMutability: "view", 571 | type: "function", 572 | }, 573 | { 574 | inputs: [ 575 | { 576 | internalType: "address", 577 | name: "from", 578 | type: "address", 579 | }, 580 | { 581 | internalType: "address", 582 | name: "to", 583 | type: "address", 584 | }, 585 | { 586 | internalType: "uint256[]", 587 | name: "ids", 588 | type: "uint256[]", 589 | }, 590 | { 591 | internalType: "uint256[]", 592 | name: "amounts", 593 | type: "uint256[]", 594 | }, 595 | { 596 | internalType: "bytes", 597 | name: "data", 598 | type: "bytes", 599 | }, 600 | ], 601 | name: "safeBatchTransferFrom", 602 | outputs: [], 603 | stateMutability: "nonpayable", 604 | type: "function", 605 | }, 606 | { 607 | inputs: [ 608 | { 609 | internalType: "address", 610 | name: "from", 611 | type: "address", 612 | }, 613 | { 614 | internalType: "address", 615 | name: "to", 616 | type: "address", 617 | }, 618 | { 619 | internalType: "uint256", 620 | name: "id", 621 | type: "uint256", 622 | }, 623 | { 624 | internalType: "uint256", 625 | name: "amount", 626 | type: "uint256", 627 | }, 628 | { 629 | internalType: "bytes", 630 | name: "data", 631 | type: "bytes", 632 | }, 633 | ], 634 | name: "safeTransferFrom", 635 | outputs: [], 636 | stateMutability: "nonpayable", 637 | type: "function", 638 | }, 639 | { 640 | inputs: [ 641 | { 642 | internalType: "address", 643 | name: "operator", 644 | type: "address", 645 | }, 646 | { 647 | internalType: "bool", 648 | name: "approved", 649 | type: "bool", 650 | }, 651 | ], 652 | name: "setApprovalForAll", 653 | outputs: [], 654 | stateMutability: "nonpayable", 655 | type: "function", 656 | }, 657 | { 658 | inputs: [ 659 | { 660 | internalType: "bool", 661 | name: "val", 662 | type: "bool", 663 | }, 664 | ], 665 | name: "setPaused", 666 | outputs: [], 667 | stateMutability: "nonpayable", 668 | type: "function", 669 | }, 670 | { 671 | inputs: [ 672 | { 673 | internalType: "string", 674 | name: "newuri", 675 | type: "string", 676 | }, 677 | ], 678 | name: "setURI", 679 | outputs: [], 680 | stateMutability: "nonpayable", 681 | type: "function", 682 | }, 683 | { 684 | inputs: [ 685 | { 686 | internalType: "bytes4", 687 | name: "interfaceId", 688 | type: "bytes4", 689 | }, 690 | ], 691 | name: "supportsInterface", 692 | outputs: [ 693 | { 694 | internalType: "bool", 695 | name: "", 696 | type: "bool", 697 | }, 698 | ], 699 | stateMutability: "view", 700 | type: "function", 701 | }, 702 | { 703 | inputs: [ 704 | { 705 | internalType: "address", 706 | name: "newOwner", 707 | type: "address", 708 | }, 709 | ], 710 | name: "transferOwnership", 711 | outputs: [], 712 | stateMutability: "nonpayable", 713 | type: "function", 714 | }, 715 | { 716 | inputs: [ 717 | { 718 | internalType: "uint256", 719 | name: "", 720 | type: "uint256", 721 | }, 722 | ], 723 | name: "uri", 724 | outputs: [ 725 | { 726 | internalType: "string", 727 | name: "", 728 | type: "string", 729 | }, 730 | ], 731 | stateMutability: "view", 732 | type: "function", 733 | }, 734 | ]; 735 | export const stakingABI = [ 736 | { 737 | inputs: [ 738 | { 739 | internalType: "address", 740 | name: "rewardTokenAddress", 741 | type: "address", 742 | }, 743 | { 744 | internalType: "address", 745 | name: "nftAddress", 746 | type: "address", 747 | }, 748 | { 749 | internalType: "uint256", 750 | name: "_tokenPool", 751 | type: "uint256", 752 | }, 753 | { 754 | internalType: "uint256", 755 | name: "_blockReward", 756 | type: "uint256", 757 | }, 758 | { 759 | internalType: "uint256", 760 | name: "_key", 761 | type: "uint256", 762 | }, 763 | ], 764 | stateMutability: "nonpayable", 765 | type: "constructor", 766 | }, 767 | { 768 | inputs: [], 769 | name: "ContractPaused", 770 | type: "error", 771 | }, 772 | { 773 | inputs: [], 774 | name: "NotOwner", 775 | type: "error", 776 | }, 777 | { 778 | anonymous: false, 779 | inputs: [ 780 | { 781 | indexed: false, 782 | internalType: "uint256", 783 | name: "oldID", 784 | type: "uint256", 785 | }, 786 | { 787 | indexed: false, 788 | internalType: "uint256", 789 | name: "newID", 790 | type: "uint256", 791 | }, 792 | ], 793 | name: "KeyTokenChanged", 794 | type: "event", 795 | }, 796 | { 797 | anonymous: false, 798 | inputs: [ 799 | { 800 | indexed: false, 801 | internalType: "address", 802 | name: "staker", 803 | type: "address", 804 | }, 805 | { 806 | indexed: false, 807 | internalType: "uint256", 808 | name: "amount", 809 | type: "uint256", 810 | }, 811 | ], 812 | name: "RewardsClaimed", 813 | type: "event", 814 | }, 815 | { 816 | anonymous: false, 817 | inputs: [ 818 | { 819 | indexed: true, 820 | internalType: "address", 821 | name: "staker", 822 | type: "address", 823 | }, 824 | { 825 | indexed: false, 826 | internalType: "uint256", 827 | name: "amount", 828 | type: "uint256", 829 | }, 830 | { 831 | indexed: false, 832 | internalType: "uint256", 833 | name: "totalStaked", 834 | type: "uint256", 835 | }, 836 | ], 837 | name: "Staked", 838 | type: "event", 839 | }, 840 | { 841 | anonymous: false, 842 | inputs: [ 843 | { 844 | indexed: true, 845 | internalType: "address", 846 | name: "staker", 847 | type: "address", 848 | }, 849 | { 850 | indexed: false, 851 | internalType: "uint256", 852 | name: "amount", 853 | type: "uint256", 854 | }, 855 | { 856 | indexed: false, 857 | internalType: "uint256", 858 | name: "totalStaked", 859 | type: "uint256", 860 | }, 861 | ], 862 | name: "Unstaked", 863 | type: "event", 864 | }, 865 | { 866 | inputs: [ 867 | { 868 | internalType: "uint256", 869 | name: "newBlockReward", 870 | type: "uint256", 871 | }, 872 | ], 873 | name: "changeBlockReward", 874 | outputs: [], 875 | stateMutability: "nonpayable", 876 | type: "function", 877 | }, 878 | { 879 | inputs: [ 880 | { 881 | internalType: "uint256", 882 | name: "id", 883 | type: "uint256", 884 | }, 885 | ], 886 | name: "changePoolKeyToken", 887 | outputs: [], 888 | stateMutability: "nonpayable", 889 | type: "function", 890 | }, 891 | { 892 | inputs: [], 893 | name: "claim", 894 | outputs: [], 895 | stateMutability: "nonpayable", 896 | type: "function", 897 | }, 898 | { 899 | inputs: [ 900 | { 901 | internalType: "address", 902 | name: "user", 903 | type: "address", 904 | }, 905 | ], 906 | name: "earned", 907 | outputs: [ 908 | { 909 | internalType: "uint256", 910 | name: "", 911 | type: "uint256", 912 | }, 913 | ], 914 | stateMutability: "view", 915 | type: "function", 916 | }, 917 | { 918 | inputs: [], 919 | name: "getPoolKeyToken", 920 | outputs: [ 921 | { 922 | internalType: "uint256", 923 | name: "", 924 | type: "uint256", 925 | }, 926 | ], 927 | stateMutability: "view", 928 | type: "function", 929 | }, 930 | { 931 | inputs: [ 932 | { 933 | internalType: "address", 934 | name: "", 935 | type: "address", 936 | }, 937 | { 938 | internalType: "address", 939 | name: "", 940 | type: "address", 941 | }, 942 | { 943 | internalType: "uint256[]", 944 | name: "", 945 | type: "uint256[]", 946 | }, 947 | { 948 | internalType: "uint256[]", 949 | name: "", 950 | type: "uint256[]", 951 | }, 952 | { 953 | internalType: "bytes", 954 | name: "", 955 | type: "bytes", 956 | }, 957 | ], 958 | name: "onERC1155BatchReceived", 959 | outputs: [ 960 | { 961 | internalType: "bytes4", 962 | name: "", 963 | type: "bytes4", 964 | }, 965 | ], 966 | stateMutability: "nonpayable", 967 | type: "function", 968 | }, 969 | { 970 | inputs: [ 971 | { 972 | internalType: "address", 973 | name: "", 974 | type: "address", 975 | }, 976 | { 977 | internalType: "address", 978 | name: "", 979 | type: "address", 980 | }, 981 | { 982 | internalType: "uint256", 983 | name: "", 984 | type: "uint256", 985 | }, 986 | { 987 | internalType: "uint256", 988 | name: "", 989 | type: "uint256", 990 | }, 991 | { 992 | internalType: "bytes", 993 | name: "", 994 | type: "bytes", 995 | }, 996 | ], 997 | name: "onERC1155Received", 998 | outputs: [ 999 | { 1000 | internalType: "bytes4", 1001 | name: "", 1002 | type: "bytes4", 1003 | }, 1004 | ], 1005 | stateMutability: "nonpayable", 1006 | type: "function", 1007 | }, 1008 | { 1009 | inputs: [], 1010 | name: "owner", 1011 | outputs: [ 1012 | { 1013 | internalType: "address", 1014 | name: "", 1015 | type: "address", 1016 | }, 1017 | ], 1018 | stateMutability: "view", 1019 | type: "function", 1020 | }, 1021 | { 1022 | inputs: [], 1023 | name: "paused", 1024 | outputs: [ 1025 | { 1026 | internalType: "bool", 1027 | name: "", 1028 | type: "bool", 1029 | }, 1030 | ], 1031 | stateMutability: "view", 1032 | type: "function", 1033 | }, 1034 | { 1035 | inputs: [], 1036 | name: "poolKEY", 1037 | outputs: [ 1038 | { 1039 | internalType: "uint256", 1040 | name: "", 1041 | type: "uint256", 1042 | }, 1043 | ], 1044 | stateMutability: "view", 1045 | type: "function", 1046 | }, 1047 | { 1048 | inputs: [ 1049 | { 1050 | internalType: "bool", 1051 | name: "val", 1052 | type: "bool", 1053 | }, 1054 | ], 1055 | name: "setPaused", 1056 | outputs: [], 1057 | stateMutability: "nonpayable", 1058 | type: "function", 1059 | }, 1060 | { 1061 | inputs: [ 1062 | { 1063 | internalType: "uint256", 1064 | name: "amount", 1065 | type: "uint256", 1066 | }, 1067 | { 1068 | internalType: "bytes", 1069 | name: "data", 1070 | type: "bytes", 1071 | }, 1072 | ], 1073 | name: "stake", 1074 | outputs: [], 1075 | stateMutability: "nonpayable", 1076 | type: "function", 1077 | }, 1078 | { 1079 | inputs: [ 1080 | { 1081 | internalType: "address", 1082 | name: "", 1083 | type: "address", 1084 | }, 1085 | ], 1086 | name: "stakers", 1087 | outputs: [ 1088 | { 1089 | internalType: "uint256", 1090 | name: "totalNftStaked", 1091 | type: "uint256", 1092 | }, 1093 | { 1094 | internalType: "uint256", 1095 | name: "rewardPending", 1096 | type: "uint256", 1097 | }, 1098 | { 1099 | internalType: "uint256", 1100 | name: "lastUpdateAt", 1101 | type: "uint256", 1102 | }, 1103 | ], 1104 | stateMutability: "view", 1105 | type: "function", 1106 | }, 1107 | { 1108 | inputs: [ 1109 | { 1110 | internalType: "bytes4", 1111 | name: "interfaceId", 1112 | type: "bytes4", 1113 | }, 1114 | ], 1115 | name: "supportsInterface", 1116 | outputs: [ 1117 | { 1118 | internalType: "bool", 1119 | name: "", 1120 | type: "bool", 1121 | }, 1122 | ], 1123 | stateMutability: "view", 1124 | type: "function", 1125 | }, 1126 | { 1127 | inputs: [], 1128 | name: "totalStaked", 1129 | outputs: [ 1130 | { 1131 | internalType: "uint256", 1132 | name: "", 1133 | type: "uint256", 1134 | }, 1135 | ], 1136 | stateMutability: "view", 1137 | type: "function", 1138 | }, 1139 | { 1140 | inputs: [ 1141 | { 1142 | internalType: "address", 1143 | name: "addr", 1144 | type: "address", 1145 | }, 1146 | ], 1147 | name: "totalStakedFor", 1148 | outputs: [ 1149 | { 1150 | internalType: "uint256", 1151 | name: "", 1152 | type: "uint256", 1153 | }, 1154 | ], 1155 | stateMutability: "view", 1156 | type: "function", 1157 | }, 1158 | { 1159 | inputs: [ 1160 | { 1161 | internalType: "address", 1162 | name: "newOwner", 1163 | type: "address", 1164 | }, 1165 | ], 1166 | name: "transferOwnership", 1167 | outputs: [], 1168 | stateMutability: "nonpayable", 1169 | type: "function", 1170 | }, 1171 | { 1172 | inputs: [ 1173 | { 1174 | internalType: "uint256", 1175 | name: "amount", 1176 | type: "uint256", 1177 | }, 1178 | { 1179 | internalType: "bytes", 1180 | name: "data", 1181 | type: "bytes", 1182 | }, 1183 | ], 1184 | name: "unstake", 1185 | outputs: [], 1186 | stateMutability: "nonpayable", 1187 | type: "function", 1188 | }, 1189 | ]; 1190 | export const tokenAddress = "0x3C6eeC559d7a8ECfaB88d29e042Aac4e6e0a63a6"; 1191 | export const nftAddress = "0xf046E538fff7b03940346FF07E120bbB2dD8bcc1"; 1192 | export const stakingAddress = "0x26C596e3d3eFC4a9D2F31b563CE60A4a5922da31"; 1193 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-staking-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/pro-theme": "^0.0.64", 14 | "@chakra-ui/react": "^2.1.2", 15 | "@emotion/react": "^11", 16 | "@emotion/styled": "^11", 17 | "@fontsource/inter": "^4.5.15", 18 | "@rainbow-me/rainbowkit": "^0.5.0", 19 | "dotenv": "^16.0.3", 20 | "ethers": "^5.7.0", 21 | "framer-motion": "^6", 22 | "next": "13.2.3", 23 | "prettier": "^2.8.4", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react-icons": "^4.8.0", 27 | "wagmi": "^0.6.4" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "^18.2.66", 31 | "@types/react-dom": "^18.2.22", 32 | "@vitejs/plugin-react": "^4.2.1", 33 | "eslint": "^8.57.0", 34 | "eslint-plugin-react": "^7.34.1", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.6", 37 | "vite": "^5.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/nft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptoDev88/nft-staking-app/c03967e6c3a5cf92c6ca6672084f265ddda4701a/public/nft.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider, extendTheme } from "@chakra-ui/react"; 2 | import { theme } from "@chakra-ui/pro-theme"; 3 | import "@fontsource/inter/variable.css"; 4 | import "@rainbow-me/rainbowkit/styles.css"; 5 | import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit"; 6 | import { configureChains, createClient, WagmiConfig } from "wagmi"; 7 | import { jsonRpcProvider } from "wagmi/providers/jsonRpc"; 8 | import Home from "./Home"; 9 | 10 | const celoChain = { 11 | id: 44787, 12 | name: "Celo Alfajores Testnet", 13 | network: "alfajores", 14 | nativeCurrency: { 15 | decimals: 18, 16 | name: "Celo", 17 | symbol: "CELO", 18 | }, 19 | rpcUrls: { 20 | default: "https://alfajores-forno.celo-testnet.org", 21 | }, 22 | blockExplorers: { 23 | default: { 24 | name: "CeloScan", 25 | url: "https://alfajores.celoscan.io", 26 | }, 27 | }, 28 | testnet: true, 29 | }; 30 | 31 | const { chains, provider } = configureChains( 32 | [celoChain], 33 | [ 34 | jsonRpcProvider({ 35 | rpc: (chain) => { 36 | if (chain.id !== celoChain.id) return null; 37 | return { http: chain.rpcUrls.default }; 38 | }, 39 | }), 40 | ] 41 | ); 42 | 43 | const { connectors } = getDefaultWallets({ 44 | appName: "Celo NFT Marketplace", 45 | chains, 46 | }); 47 | 48 | const wagmiClient = createClient({ 49 | autoConnect: true, 50 | connectors, 51 | provider, 52 | }); 53 | 54 | function App() { 55 | const myTheme = extendTheme( 56 | { 57 | colors: { ...theme.colors }, 58 | }, 59 | theme 60 | ); 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /src/Home.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ethers } from "ethers"; 3 | import { 4 | useSigner, 5 | useProvider, 6 | useAccount, 7 | useBalance, 8 | useContract, 9 | } from "wagmi"; 10 | 11 | import { Navbar } from "../components/Navbar"; 12 | import { HeaderStat } from "../components/HeaderStat"; 13 | import { Footer } from "../components/Footer"; 14 | 15 | import { 16 | Container, 17 | SimpleGrid, 18 | Button, 19 | Divider, 20 | Heading, 21 | InputGroup, 22 | InputRightElement, 23 | Input, 24 | Stack, 25 | Text, 26 | Box, 27 | Stat, 28 | StatLabel, 29 | useBreakpointValue, 30 | useColorModeValue, 31 | Skeleton, 32 | useToast, 33 | Image, 34 | Center, 35 | } from "@chakra-ui/react"; 36 | 37 | import { 38 | tokenABI, 39 | nftABI, 40 | stakingABI, 41 | tokenAddress, 42 | nftAddress, 43 | stakingAddress, 44 | } from "../helpers/contracts"; 45 | import { NFT_KEY } from "../helpers/constants"; 46 | 47 | const Home = () => { 48 | const toast = useToast(); 49 | 50 | const [address, setAddress] = useState(); 51 | 52 | const [totalStakedAmount, setTotalStakedAmount] = useState(); // optional: total staked in contract 53 | const [priceNFT, setPriceNFT] = useState(); 54 | 55 | //user balances 56 | const [userCeloBalance, setUserCeloBalance] = useState(); 57 | const [userStakedBalance, setUserStakedBalance] = useState(); 58 | const [userPendingRewards, setUserPendingRewards] = useState(); 59 | const [userNFTBalance, setUserNFTBalance] = useState(); 60 | const [userTokenBalance, setUserTokenBalance] = useState(); 61 | const [rewardPool, setRewardPool] = useState(); 62 | 63 | const [userAllowanceStakeNFT, setUserAllowanceStakeNFT] = useState(); 64 | 65 | // Get provider and signer from wagmi 66 | const { data: provider } = useProvider(); 67 | const { data: signer } = useSigner(); 68 | 69 | // isConnected => address 70 | const { isConnected } = useAccount(); 71 | 72 | // contracts 73 | const tokenContract = useContract({ 74 | addressOrName: tokenAddress, 75 | contractInterface: tokenABI, 76 | signerOrProvider: signer, 77 | }); 78 | 79 | const stakingContract = useContract({ 80 | addressOrName: stakingAddress, 81 | contractInterface: stakingABI, 82 | signerOrProvider: signer, 83 | }); 84 | 85 | const nftContract = useContract({ 86 | addressOrName: nftAddress, 87 | contractInterface: nftABI, 88 | signerOrProvider: signer, 89 | }); 90 | 91 | const updateUserBalances = async (address) => { 92 | const celoBalance = ethers.utils.formatEther(await signer.getBalance()); 93 | 94 | const tokenBalance = ethers.utils.formatEther( 95 | await tokenContract.balanceOf(address) 96 | ); 97 | console.log(tokenBalance); 98 | const stakedTokens = await stakingContract.totalStakedFor(address); 99 | 100 | const pendingRewards = await stakingContract.earned(address); 101 | 102 | const nft = await nftContract.balanceOf(address, NFT_KEY); 103 | const _rewardPool = ethers.utils.formatEther( 104 | await tokenContract.balanceOf(stakingContract.address) 105 | ); 106 | 107 | const allowanceStakedNFT = await nftContract.isApprovedForAll( 108 | address, 109 | stakingContract.address 110 | ); 111 | 112 | setUserCeloBalance(celoBalance); 113 | setUserTokenBalance(tokenBalance); 114 | setUserStakedBalance(stakedTokens); 115 | setUserPendingRewards(pendingRewards); 116 | setUserNFTBalance(nft); 117 | setRewardPool(_rewardPool); 118 | setUserAllowanceStakeNFT(allowanceStakedNFT); 119 | }; 120 | 121 | const loadDefaultValues = async () => { 122 | // Get signer's address 123 | const _address = await signer.getAddress(); 124 | const priceNFT = await nftContract.getMintPrice(); 125 | const stakedTokens = await stakingContract.totalStaked(); 126 | 127 | setAddress(_address); 128 | setPriceNFT(BigInt(priceNFT).toString()); 129 | setTotalStakedAmount(BigInt(stakedTokens).toString()); 130 | }; 131 | 132 | const formatNumber = (number) => { 133 | const amount = ethers.utils.formatEther(number); 134 | const calcDec = Math.pow(10, 0); 135 | 136 | return Math.trunc(amount * calcDec) / calcDec; 137 | }; 138 | 139 | const formatNumber6Dec = (number) => { 140 | const amount = ethers.utils.formatEther(number); 141 | const calcDec = Math.pow(10, 6); 142 | 143 | return Math.trunc(amount * calcDec) / calcDec; 144 | }; 145 | 146 | const getNFT = async () => { 147 | if (!isConnected) { 148 | toast({ 149 | title: "Connect Wallet", 150 | description: 151 | "Please connect your wallet and wait for the page to load.", 152 | status: "error", 153 | duration: 6000, 154 | isClosable: true, 155 | }); 156 | return; 157 | } 158 | 159 | const input = document.querySelector("#inputGetNFT"); 160 | 161 | if ( 162 | !Number.isInteger(parseInt(input.value)) || 163 | parseInt(input.value) == 0 || 164 | input.value.length == 0 165 | ) { 166 | toast({ 167 | title: "Only Numbers", 168 | description: "Please use only round numbers with no decimal places.", 169 | status: "error", 170 | duration: 6000, 171 | isClosable: true, 172 | }); 173 | return; 174 | } 175 | 176 | const inputValue = input.value * ethers.utils.formatEther(priceNFT); 177 | const formattedValue = parseInt( 178 | ethers.utils.parseEther(inputValue.toString()) 179 | ); 180 | 181 | if (userCeloBalance < inputValue) { 182 | toast({ 183 | title: "Not Enough Funds", 184 | description: "Please make sure you have enough funds to proceed.", 185 | status: "error", 186 | duration: 6000, 187 | isClosable: true, 188 | }); 189 | return; 190 | } 191 | 192 | const cost = (input.value * priceNFT).toString(); 193 | const approvePurchaseTX = await nftContract.mint(NFT_KEY, input.value, { 194 | value: cost, 195 | }); 196 | 197 | toast({ 198 | title: "Confirming Transaction", 199 | description: "Please wait until the transaction is confirmed.", 200 | status: "info", 201 | duration: 6000, 202 | isClosable: true, 203 | }); 204 | 205 | await approvePurchaseTX.wait(); 206 | 207 | if (!approvePurchaseTX) { 208 | toast({ 209 | title: "Transaction Failed", 210 | description: "Please refresh the page and try again.", 211 | status: "error", 212 | duration: 6000, 213 | isClosable: true, 214 | }); 215 | } 216 | 217 | toast({ 218 | title: "Transaction Success!", 219 | description: "You have successfully purchased the nfts.", 220 | status: "success", 221 | duration: 6000, 222 | isClosable: true, 223 | }); 224 | 225 | input.value = ""; 226 | updateUserBalances(address); 227 | }; 228 | 229 | const stakeNFT = async () => { 230 | if (!isConnected) { 231 | toast({ 232 | title: "Connect Wallet", 233 | description: 234 | "Please connect your wallet and wait for the page to load.", 235 | status: "error", 236 | duration: 6000, 237 | isClosable: true, 238 | }); 239 | return; 240 | } 241 | 242 | const input = document.querySelector("#inputStakeNFT"); 243 | 244 | if ( 245 | !Number.isInteger(parseInt(input.value)) || 246 | parseInt(input.value) == 0 || 247 | input.value.length == 0 248 | ) { 249 | toast({ 250 | title: "Only Numbers", 251 | description: "Please use only round numbers with no decimal places.", 252 | status: "error", 253 | duration: 6000, 254 | isClosable: true, 255 | }); 256 | return; 257 | } 258 | 259 | const inputValue = parseInt(input.value); 260 | 261 | if (parseInt(userNFTBalance) < inputValue) { 262 | toast({ 263 | title: "Not Enough Funds", 264 | description: "Please make sure you have enough funds to proceed.", 265 | status: "error", 266 | duration: 6000, 267 | isClosable: true, 268 | }); 269 | return; 270 | } 271 | 272 | if (userAllowanceStakeNFT == false) { 273 | toast({ 274 | title: "Approve NFTs", 275 | description: "Please approve the staking contract to use your NFTs.", 276 | status: "info", 277 | duration: 6000, 278 | isClosable: true, 279 | }); 280 | const approveTX = await nftContract.setApprovalForAll( 281 | stakingAddress, 282 | true 283 | ); 284 | 285 | toast({ 286 | title: "Approving NFTs", 287 | description: "Please wait until the NFTs are approved.", 288 | status: "info", 289 | duration: 6000, 290 | isClosable: true, 291 | }); 292 | 293 | await approveTX.wait(); 294 | 295 | if (approveTX) { 296 | toast({ 297 | title: "Approve Success!", 298 | description: "You have successfully approved the request.", 299 | status: "success", 300 | duration: 6000, 301 | isClosable: true, 302 | }); 303 | setUserAllowanceStakeNFT(true); 304 | } 305 | } 306 | 307 | toast({ 308 | title: "Confirm Transaction", 309 | description: "Please confirm the transaction in your wallet to proceed.", 310 | status: "info", 311 | duration: 6000, 312 | isClosable: true, 313 | }); 314 | 315 | const approveStakingTX = await stakingContract.stake(inputValue, "0x0000"); 316 | 317 | toast({ 318 | title: "Confirming Transaction", 319 | description: "Please wait until the transaction is confirmed.", 320 | status: "info", 321 | duration: 6000, 322 | isClosable: true, 323 | }); 324 | 325 | await approveStakingTX.wait(); 326 | 327 | if (!approveStakingTX) { 328 | toast({ 329 | title: "Transaction Failed", 330 | description: "Please refresh the page and try again.", 331 | status: "error", 332 | duration: 6000, 333 | isClosable: true, 334 | }); 335 | } 336 | 337 | toast({ 338 | title: "NFTs Staked!", 339 | description: "You have successfully staked your nfts.", 340 | status: "success", 341 | duration: 6000, 342 | isClosable: true, 343 | }); 344 | input.value = ""; 345 | updateUserBalances(address); 346 | }; 347 | 348 | const unstakeNFT = async () => { 349 | if (!isConnected || !userNFTBalance || !userStakedBalance) { 350 | toast({ 351 | title: "Connect Wallet", 352 | description: 353 | "Please connect your wallet and wait for the page to load.", 354 | status: "error", 355 | duration: 6000, 356 | isClosable: true, 357 | }); 358 | return; 359 | } 360 | 361 | const input = document.querySelector("#inputUnstakeNFT"); 362 | 363 | if ( 364 | !Number.isInteger(parseInt(input.value)) || 365 | parseInt(input.value) == 0 || 366 | input.value.length == 0 367 | ) { 368 | toast({ 369 | title: "Only Numbers", 370 | description: "Please use only round numbers with no decimal places.", 371 | status: "error", 372 | duration: 6000, 373 | isClosable: true, 374 | }); 375 | return; 376 | } 377 | 378 | const inputValue = parseInt(input.value); 379 | 380 | if (inputValue > parseInt(userStakedBalance)) { 381 | toast({ 382 | title: "Not Available", 383 | description: "You don't have that amount of NFTs staked.", 384 | status: "error", 385 | duration: 6000, 386 | isClosable: true, 387 | }); 388 | return; 389 | } 390 | 391 | toast({ 392 | title: "Confirm Transaction", 393 | description: "Please confirm the transaction in your wallet to proceed.", 394 | status: "info", 395 | duration: 6000, 396 | isClosable: true, 397 | }); 398 | 399 | const approveStakingTX = await stakingContract.unstake( 400 | inputValue, 401 | "0x0000" 402 | ); 403 | 404 | toast({ 405 | title: "Confirming Transaction", 406 | description: "Please wait until the transaction is confirmed.", 407 | status: "info", 408 | duration: 6000, 409 | isClosable: true, 410 | }); 411 | 412 | await approveStakingTX.wait(); 413 | 414 | if (!approveStakingTX) { 415 | toast({ 416 | title: "Transaction Failed", 417 | description: "Please refresh the page and try again.", 418 | status: "error", 419 | duration: 6000, 420 | isClosable: true, 421 | }); 422 | } 423 | 424 | toast({ 425 | title: "NFTs Unstaked!", 426 | description: "You have successfully unstaked the nfts.", 427 | status: "success", 428 | duration: 6000, 429 | isClosable: true, 430 | }); 431 | input.value = ""; 432 | updateUserBalances(address); 433 | }; 434 | 435 | const claimRewards = async () => { 436 | if (!isConnected) { 437 | toast({ 438 | title: "Connect Wallet", 439 | description: 440 | "Please connect your wallet and wait for the page to load.", 441 | status: "error", 442 | duration: 6000, 443 | isClosable: true, 444 | }); 445 | return; 446 | } 447 | 448 | if (userPendingRewards == 0) { 449 | toast({ 450 | title: "Not Available", 451 | description: "You don't have any pending rewards.", 452 | status: "error", 453 | duration: 6000, 454 | isClosable: true, 455 | }); 456 | return; 457 | } 458 | 459 | toast({ 460 | title: "Confirm Transaction", 461 | description: "Please confirm the transaction in your wallet to proceed.", 462 | status: "info", 463 | duration: 6000, 464 | isClosable: true, 465 | }); 466 | 467 | const approveClaimTX = await stakingContract.claim(); 468 | 469 | toast({ 470 | title: "Confirming Transaction", 471 | description: "Please wait until the transaction is confirmed.", 472 | status: "info", 473 | duration: 6000, 474 | isClosable: true, 475 | }); 476 | 477 | await approveClaimTX.wait(); 478 | 479 | if (!approveClaimTX) { 480 | toast({ 481 | title: "Transaction Failed", 482 | description: "Please refresh the page and try again.", 483 | status: "error", 484 | duration: 6000, 485 | isClosable: true, 486 | }); 487 | } 488 | 489 | toast({ 490 | title: "Rewards Claimed!", 491 | description: "You have successfully claimed your rewards.", 492 | status: "success", 493 | duration: 6000, 494 | isClosable: true, 495 | }); 496 | updateUserBalances(address); 497 | }; 498 | 499 | const getMaxNFT = async () => { 500 | if (!isConnected) { 501 | toast({ 502 | title: "Connect Wallet", 503 | description: 504 | "Please connect your wallet and wait for the page to load.", 505 | status: "error", 506 | duration: 6000, 507 | isClosable: true, 508 | }); 509 | return; 510 | } 511 | document.querySelector("#inputGetNFT").value = parseInt(userCeloBalance); 512 | }; 513 | 514 | const getMaxStakeNFT = async () => { 515 | if (!isConnected) { 516 | toast({ 517 | title: "Connect Wallet", 518 | description: 519 | "Please connect your wallet and wait for the page to load.", 520 | status: "error", 521 | duration: 6000, 522 | isClosable: true, 523 | }); 524 | return; 525 | } 526 | 527 | document.querySelector("#inputStakeNFT").value = parseInt(userNFTBalance); 528 | }; 529 | 530 | const getMaxUnstakeNFT = async () => { 531 | if (!isConnected) { 532 | toast({ 533 | title: "Connect Wallet", 534 | description: 535 | "Please connect your wallet and wait for the page to load.", 536 | status: "error", 537 | duration: 6000, 538 | isClosable: true, 539 | }); 540 | return; 541 | } 542 | 543 | document.querySelector("#inputUnstakeNFT").value = 544 | parseInt(userStakedBalance); 545 | }; 546 | 547 | useEffect(() => { 548 | if (isConnected) { 549 | loadDefaultValues(); 550 | updateUserBalances(address); 551 | } 552 | }); 553 | 554 | useEffect(() => { 555 | const interval = setInterval(async () => { 556 | if (isConnected) { 557 | const pendingRewards = await stakingContract.earned(address); 558 | setUserPendingRewards(pendingRewards); 559 | } 560 | }, 10000); 561 | return () => clearInterval(interval); 562 | }); 563 | 564 | return ( 565 |
566 | {/* 567 | Simple NFT Staking 568 | 569 | */} 570 | 571 | 572 | {isConnected && ( 573 | 574 | 575 | 579 | 580 | 584 | 585 | 586 | 587 | 591 | 592 | 593 | 594 | 598 | 599 | 600 | 0 ? rewardPool : 0} 603 | /> 604 | 605 | 606 | 607 | 608 | )} 609 | 610 | 619 | 620 | 627 |
628 | Coin NFT 636 |
637 | Get Coin NFT 638 | 645 | 646 | 652 | 653 | 656 | 657 | 658 | 666 | 667 |
668 | 669 | 670 | 671 | Stake Coin NFT 672 | 673 | 674 | Here you can stake your Coin NFTs, unstake them, and claim your 675 | rewards in $COIN tokens. 676 | 677 | 678 | 686 | 687 | 693 | 694 | 697 | 698 | 699 | 707 | 708 | 716 | 717 | 723 | 724 | 727 | 728 | 729 | 737 | 738 | 745 | 746 | Your pending $COIN token rewards 747 | 748 | 756 | 759 | {userPendingRewards 760 | ? formatNumber6Dec(userPendingRewards) 761 | : 0} 762 | 763 | 764 | 765 | 766 | 774 | 775 | 776 |
777 |
778 |
779 | 780 |
782 | ); 783 | }; 784 | 785 | export default Home; 786 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .footer__link { 32 | font-size: 0.9rem; 33 | font-weight: 600; 34 | color: var(--clr-fg); 35 | } 36 | 37 | .title a { 38 | color: #0070f3; 39 | text-decoration: none; 40 | } 41 | 42 | .title a:hover, 43 | .title a:focus, 44 | .title a:active { 45 | text-decoration: underline; 46 | } 47 | 48 | .title { 49 | margin: 0; 50 | line-height: 1.15; 51 | font-size: 4rem; 52 | } 53 | 54 | .title, 55 | .description { 56 | text-align: center; 57 | } 58 | 59 | .description { 60 | margin: 4rem 0; 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | } 81 | 82 | .card { 83 | margin: 1rem; 84 | padding: 1.5rem; 85 | text-align: left; 86 | color: inherit; 87 | text-decoration: none; 88 | border: 1px solid #eaeaea; 89 | border-radius: 10px; 90 | transition: color 0.15s ease, border-color 0.15s ease; 91 | max-width: 300px; 92 | } 93 | 94 | .card:hover, 95 | .card:focus, 96 | .card:active { 97 | color: #0070f3; 98 | border-color: #0070f3; 99 | } 100 | 101 | .card h2 { 102 | margin: 0 0 1rem 0; 103 | font-size: 1.5rem; 104 | } 105 | 106 | .card p { 107 | margin: 0; 108 | font-size: 1.25rem; 109 | line-height: 1.5; 110 | } 111 | 112 | @media (max-width: 600px) { 113 | .grid { 114 | width: 100%; 115 | flex-direction: column; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------