diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/project-8-token-vesting/README copy.md b/project-8-token-vesting/README copy.md deleted file mode 100644 index dea6a11..0000000 --- a/project-8-token-vesting/README copy.md +++ /dev/null @@ -1,96 +0,0 @@ -# token-vesting - -This project is generated with the [create-solana-dapp](https://github.com/solana-developers/create-solana-dapp) generator. - -## Getting Started - -### Prerequisites - -- Node v18.18.0 or higher - -- Rust v1.77.2 or higher -- Anchor CLI 0.30.0 or higher -- Solana CLI 1.18.9 or higher - -### Installation - -#### Clone the repo - -```shell -git clone -cd -``` - -#### Install Dependencies - -```shell -npm install -``` - -#### Start the web app - -``` -npm run dev -``` - -## Apps - -### anchor - -This is a Solana program written in Rust using the Anchor framework. - -#### Commands - -You can use any normal anchor commands. Either move to the `anchor` directory and run the `anchor` command or prefix the command with `npm run`, eg: `npm run anchor`. - -#### Sync the program id: - -Running this command will create a new keypair in the `anchor/target/deploy` directory and save the address to the Anchor config file and update the `declare_id!` macro in the `./src/lib.rs` file of the program. - -You will manually need to update the constant in `anchor/lib/vesting-exports.ts` to match the new program id. - -```shell -npm run anchor keys sync -``` - -#### Build the program: - -```shell -npm run anchor-build -``` - -#### Start the test validator with the program deployed: - -```shell -npm run anchor-localnet -``` - -#### Run the tests - -```shell -npm run anchor-test -``` - -#### Deploy to Devnet - -```shell -npm run anchor deploy --provider.cluster devnet -``` - -### web - -This is a React app that uses the Anchor generated client to interact with the Solana program. - -#### Commands - -Start the web app - -```shell -npm run dev -``` - -Build the web app - -```shell -npm run build -``` diff --git a/project-8-token-vesting/README.md b/project-8-token-vesting/README.md index 9092b4d..cd6ecd1 100644 --- a/project-8-token-vesting/README.md +++ b/project-8-token-vesting/README.md @@ -13,6 +13,15 @@ Create Vesting Account: Set up a vesting account for a company, specifying the c Create Employee Vesting: Establish a vesting schedule for an employee, including start and end times, total amount, and a cliff period. Claim Tokens: Allows employees to claim their vested tokens after the cliff period, based on the time elapsed and the amount vested. +## Program Updates +- Introduced the initial_fund_amount parameter in the create_vesting_account function, ensuring that the vesting treasury is pre-funded at the time of account creation. +- Implemented token transfer logic to move the initial fund from the signer's associated token account to the vesting treasury. + +## Fronend Updates +- Significantly enhanced the UI for a modern and fully responsive experience, including a redesigned navbar. +- Added a mint token feature, allowing users to mint new tokens smoothly. +- Integrated a claim token functionality, enabling users to withdraw vested tokens efficiently. + ## Program Functions - `create_vesting_account`: Initializes a vesting account for a company and initializes a vesting token account to hold the entire token allocation. diff --git a/project-8-token-vesting/anchor/.eslintrc.json b/project-8-token-vesting/anchor/.eslintrc.json deleted file mode 100644 index 0121389..0000000 --- a/project-8-token-vesting/anchor/.eslintrc.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": ["../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.json"], - "parser": "jsonc-eslint-parser", - "rules": { - "@nx/dependency-checks": [ - "error", - { - "ignoredFiles": ["{projectRoot}/rollup.config.{js,ts,mjs,mts}"] - } - ] - } - } - ] -} diff --git a/project-8-token-vesting/anchor/.gitignore b/project-8-token-vesting/anchor/.gitignore new file mode 100644 index 0000000..22e78bc --- /dev/null +++ b/project-8-token-vesting/anchor/.gitignore @@ -0,0 +1,12 @@ +.anchor +.DS_Store +target/debug +target/deploy +target/release +target/sbf-solana-solana +target/test-ledger +target/.rustc_info.json +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/project-8-token-vesting/anchor/.prettierignore b/project-8-token-vesting/anchor/.prettierignore new file mode 100644 index 0000000..4142583 --- /dev/null +++ b/project-8-token-vesting/anchor/.prettierignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/project-8-token-vesting/anchor/.swcrc b/project-8-token-vesting/anchor/.swcrc deleted file mode 100644 index 28e88ec..0000000 --- a/project-8-token-vesting/anchor/.swcrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "transform": { - "decoratorMetadata": true, - "legacyDecorator": true - }, - "keepClassNames": true, - "externalHelpers": true, - "loose": true - }, - "module": { - "type": "es6" - }, - "sourceMaps": true, - "exclude": [ - "jest.config.ts", - ".*\\.spec.tsx?$", - ".*\\.test.tsx?$", - "./src/jest-setup.ts$", - "./**/jest-setup.ts$", - ".*.js$" - ] -} diff --git a/project-8-token-vesting/anchor/Anchor.toml b/project-8-token-vesting/anchor/Anchor.toml index 0d6bfbd..4a8bcb4 100644 --- a/project-8-token-vesting/anchor/Anchor.toml +++ b/project-8-token-vesting/anchor/Anchor.toml @@ -1,21 +1,22 @@ [toolchain] +anchor_version = "0.30.1" [features] resolution = true skip-lint = false -[programs.localnet] -vesting = "GFdLg11UBR8ZeePW43ZyD1gY4z4UQ96LPa22YBgnn4z8" +[programs.devnet] +vesting = "BnRyuRkBiWQ6D3Tsu5n4AuzzHjSbFHBcXKs63e51p1ft" [registry] -url = "https://api.apr.dev" +url = "https://api.devnet.solana.com" [provider] -cluster = "Localnet" +cluster = "Devnet" wallet = "~/.config/solana/id.json" [scripts] -test = "../node_modules/.bin/nx run anchor:jest" +test = "../node_modules/.bin/jest --preset ts-jest" [test] startup_wait = 5000 diff --git a/project-8-token-vesting/anchor/README.md b/project-8-token-vesting/anchor/README.md deleted file mode 100644 index f845a14..0000000 --- a/project-8-token-vesting/anchor/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# anchor - -This library was generated with [Nx](https://nx.dev). - -## Building - -Run `nx build anchor` to build the library. - -## Running unit tests - -Run `nx test anchor` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/project-8-token-vesting/anchor/jest.config.ts b/project-8-token-vesting/anchor/jest.config.ts deleted file mode 100644 index fd64796..0000000 --- a/project-8-token-vesting/anchor/jest.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable */ -import { readFileSync } from 'fs'; - -// Reading the SWC compilation config and remove the "exclude" -// for the test files to be compiled by SWC -const { exclude: _, ...swcJestConfig } = JSON.parse( - readFileSync(`${__dirname}/.swcrc`, 'utf-8') -); - -// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. -// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" -if (swcJestConfig.swcrc === undefined) { - swcJestConfig.swcrc = false; -} - -// Uncomment if using global setup/teardown files being transformed via swc -// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries -// jest needs EsModule Interop to find the default exported setup/teardown functions -// swcJestConfig.module.noInterop = false; - -export default { - displayName: 'anchor', - preset: '../jest.preset.js', - transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - testEnvironment: '', - coverageDirectory: '../coverage/anchor', -}; diff --git a/project-8-token-vesting/anchor/migrations/deploy.ts b/project-8-token-vesting/anchor/migrations/deploy.ts index 221cf4f..e914965 100644 --- a/project-8-token-vesting/anchor/migrations/deploy.ts +++ b/project-8-token-vesting/anchor/migrations/deploy.ts @@ -2,9 +2,10 @@ // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. -import * as anchor from '@coral-xyz/anchor'; +const anchor = require("@coral-xyz/anchor"); +import { AnchorProvider } from '@coral-xyz/anchor' -module.exports = async function (provider) { +module.exports = async function (provider: AnchorProvider) { // Configure client to use the provider. anchor.setProvider(provider); diff --git a/project-8-token-vesting/anchor/package.json b/project-8-token-vesting/anchor/package.json deleted file mode 100644 index 622c9d6..0000000 --- a/project-8-token-vesting/anchor/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@token-vesting/anchor", - "version": "0.0.1", - "dependencies": { - "@coral-xyz/anchor": "^0.30.1", - "@solana/spl-token": "^0.4.8", - "@solana/web3.js": "1.94.0", - "anchor-bankrun": "^0.4.0", - "solana-bankrun": "^0.2.0", - "spl-token-bankrun": "0.2.5" - }, - "main": "./index.cjs", - "module": "./index.js", - "private": true -} diff --git a/project-8-token-vesting/anchor/programs/vesting/Cargo.toml b/project-8-token-vesting/anchor/programs/dapptokenvesting/Cargo.toml similarity index 55% rename from project-8-token-vesting/anchor/programs/vesting/Cargo.toml rename to project-8-token-vesting/anchor/programs/dapptokenvesting/Cargo.toml index 23b069f..1789181 100644 --- a/project-8-token-vesting/anchor/programs/vesting/Cargo.toml +++ b/project-8-token-vesting/anchor/programs/dapptokenvesting/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "vesting" +name = "dapptokenvesting" version = "0.1.0" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "vesting" +name = "dapptokenvesting" [features] +default = [] +cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build","anchor-spl/idl-build"] [dependencies] -anchor-lang = { version="0.30.1", features=["init-if-needed"] } +anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } anchor-spl = "0.30.1" -solana-program = "1.18.17" \ No newline at end of file +solana-program = "1.18.17" diff --git a/project-8-token-vesting/anchor/programs/vesting/Xargo.toml b/project-8-token-vesting/anchor/programs/dapptokenvesting/Xargo.toml similarity index 100% rename from project-8-token-vesting/anchor/programs/vesting/Xargo.toml rename to project-8-token-vesting/anchor/programs/dapptokenvesting/Xargo.toml diff --git a/project-8-token-vesting/anchor/programs/vesting/src/lib.rs b/project-8-token-vesting/anchor/programs/dapptokenvesting/src/lib.rs similarity index 73% rename from project-8-token-vesting/anchor/programs/vesting/src/lib.rs rename to project-8-token-vesting/anchor/programs/dapptokenvesting/src/lib.rs index 1aee746..c93ef60 100644 --- a/project-8-token-vesting/anchor/programs/vesting/src/lib.rs +++ b/project-8-token-vesting/anchor/programs/dapptokenvesting/src/lib.rs @@ -1,15 +1,16 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token_interface::{ self, Mint, TokenAccount, TokenInterface, TransferChecked }; +use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked}; -declare_id!("GFdLg11UBR8ZeePW43ZyD1gY4z4UQ96LPa22YBgnn4z8"); +declare_id!("BnRyuRkBiWQ6D3Tsu5n4AuzzHjSbFHBcXKs63e51p1ft"); #[program] pub mod vesting { use super::*; pub fn create_vesting_account( ctx: Context, - company_name: String + company_name: String, + initial_fund_amount: u64, ) -> Result<()> { *ctx.accounts.vesting_account = VestingAccount { owner: ctx.accounts.signer.key(), @@ -20,6 +21,24 @@ pub mod vesting { bump: ctx.bumps.vesting_account, }; + let cpi_accounts = TransferChecked { + from: ctx + .accounts + .signer_associated_token_account + .to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + to: ctx.accounts.treasury_token_account.to_account_info(), + authority: ctx.accounts.signer.to_account_info(), + }; + + let cpi_program = ctx.accounts.token_program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + + token_interface::transfer_checked( + cpi_ctx, + initial_fund_amount, + ctx.accounts.mint.decimals, + )?; Ok(()) } @@ -28,7 +47,7 @@ pub mod vesting { start_time: i64, end_time: i64, total_amount: i64, - cliff_time: i64 + cliff_time: i64, ) -> Result<()> { *ctx.accounts.employee_account = EmployeeAccount { beneficiary: ctx.accounts.beneficiary.key(), @@ -46,49 +65,61 @@ pub mod vesting { pub fn claim_tokens(ctx: Context, _company_name: String) -> Result<()> { let employee_account = &mut ctx.accounts.employee_account; + let now = Clock::get()?.unix_timestamp; - - // Check if the current time is before the cliff time + if now < employee_account.cliff_time { return Err(ErrorCode::ClaimNotAvailableYet.into()); } - // Calculate the vested amount + let time_since_start = now.saturating_sub(employee_account.start_time); - let total_vesting_time = employee_account.end_time.saturating_sub( - employee_account.start_time - ); + let total_vesting_time = employee_account + .end_time + .saturating_sub(employee_account.start_time); + let vested_amount = if now >= employee_account.end_time { employee_account.total_amount } else { (employee_account.total_amount * time_since_start) / total_vesting_time }; - - //Calculate the amount that can be withdrawn + let claimable_amount = vested_amount.saturating_sub(employee_account.total_withdrawn); - // Check if there is anything left to claim - if claimable_amount == 0 { + + if claimable_amount <= 0 { return Err(ErrorCode::NothingToClaim.into()); } + + let decimals = ctx.accounts.mint.decimals; + if claimable_amount < 0 { + return Err(ErrorCode::InvalidClaimAmount.into()); + } + + let claimable_amount_scaled = (claimable_amount as u64) + .checked_mul(10_u64.pow(decimals as u32)) + .ok_or(ErrorCode::InvalidClaimAmount)?; + let transfer_cpi_accounts = TransferChecked { from: ctx.accounts.treasury_token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.employee_token_account.to_account_info(), authority: ctx.accounts.treasury_token_account.to_account_info(), }; + let cpi_program = ctx.accounts.token_program.to_account_info(); - let signer_seeds: &[&[&[u8]]] = &[ - &[ - b"vesting_treasury", - ctx.accounts.vesting_account.company_name.as_ref(), - &[ctx.accounts.vesting_account.treasury_bump], - ], - ]; - let cpi_context = CpiContext::new(cpi_program, transfer_cpi_accounts).with_signer( - signer_seeds - ); - let decimals = ctx.accounts.mint.decimals; - token_interface::transfer_checked(cpi_context, claimable_amount as u64, decimals)?; + + let signer_seeds: &[&[&[u8]]] = &[&[ + b"vesting_treasury", + ctx.accounts.vesting_account.company_name.as_ref(), + &[ctx.accounts.vesting_account.treasury_bump], + ]]; + + let cpi_context = + CpiContext::new(cpi_program, transfer_cpi_accounts).with_signer(signer_seeds); + + token_interface::transfer_checked(cpi_context, claimable_amount_scaled, decimals)?; + employee_account.total_withdrawn += claimable_amount; + Ok(()) } } @@ -98,6 +129,12 @@ pub mod vesting { pub struct CreateVestingAccount<'info> { #[account(mut)] pub signer: Signer<'info>, + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = signer + )] + pub signer_associated_token_account: InterfaceAccount<'info, TokenAccount>, #[account( init, space = 8 + VestingAccount::INIT_SPACE, @@ -202,8 +239,10 @@ pub struct EmployeeAccount { #[error_code] pub enum ErrorCode { - #[msg("Claiming is not available yet.")] + #[msg("Claiming is not available yet. ")] ClaimNotAvailableYet, #[msg("There is nothing to claim.")] NothingToClaim, + #[msg("Claimed Amount is invalid. ")] + InvalidClaimAmount, } diff --git a/project-8-token-vesting/anchor/project.json b/project-8-token-vesting/anchor/project.json deleted file mode 100644 index edf5256..0000000 --- a/project-8-token-vesting/anchor/project.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "anchor", - "$schema": "../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "anchor/src", - "projectType": "library", - "tags": [], - "targets": { - "build": { - "executor": "@nx/rollup:rollup", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/anchor", - "main": "anchor/src/index.ts", - "tsConfig": "anchor/tsconfig.lib.json", - "assets": [], - "project": "anchor/package.json", - "compiler": "swc", - "format": ["cjs", "esm"] - } - }, - "lint": { - "executor": "@nx/eslint:lint" - }, - "test": { - "executor": "nx:run-commands", - "options": { - "cwd": "anchor", - "commands": ["anchor test"], - "parallel": false - } - }, - "anchor": { - "executor": "nx:run-commands", - "options": { - "cwd": "anchor", - "commands": ["anchor"], - "parallel": false - } - }, - "localnet": { - "executor": "nx:run-commands", - "options": { - "cwd": "anchor", - "commands": ["anchor localnet"], - "parallel": false - } - }, - "jest": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "anchor/jest.config.ts" - } - } - } -} diff --git a/project-8-token-vesting/anchor/src/dapptokenvesting-exports.ts b/project-8-token-vesting/anchor/src/dapptokenvesting-exports.ts new file mode 100644 index 0000000..517a0d7 --- /dev/null +++ b/project-8-token-vesting/anchor/src/dapptokenvesting-exports.ts @@ -0,0 +1,31 @@ +// Here we export some useful types and functions for interacting with the Anchor program. +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { Cluster, PublicKey } from "@solana/web3.js"; +import DapptokenvestingIDL from "../target/idl/vesting.json"; +import type { Vesting as Dapptokenvesting } from "../target/types/vesting"; + +// Re-export the generated IDL and type +export { Dapptokenvesting, DapptokenvestingIDL }; + +// The programId is imported from the program IDL. +export const DAPPTOKENVESTING_PROGRAM_ID = new PublicKey( + DapptokenvestingIDL.address +); + +// This is a helper function to get the Dapptokenvesting Anchor program. +export function getDapptokenvestingProgram(provider: AnchorProvider) { + return new Program(DapptokenvestingIDL as Dapptokenvesting, provider); +} + +// This is a helper function to get the program ID for the Dapptokenvesting program depending on the cluster. +export function getDapptokenvestingProgramId(cluster: Cluster) { + switch (cluster) { + case "devnet": + case "testnet": + // This is the program ID for the Dapptokenvesting program on devnet and testnet. + return new PublicKey("BnRyuRkBiWQ6D3Tsu5n4AuzzHjSbFHBcXKs63e51p1ft"); + case "mainnet-beta": + default: + return DAPPTOKENVESTING_PROGRAM_ID; + } +} diff --git a/project-8-token-vesting/anchor/src/index.ts b/project-8-token-vesting/anchor/src/index.ts index 44d1f46..1e38c91 100644 --- a/project-8-token-vesting/anchor/src/index.ts +++ b/project-8-token-vesting/anchor/src/index.ts @@ -1,3 +1,3 @@ // This file was generated by preset-anchor. Programs are exported from this file. -export * from './vesting-exports'; +export * from './dapptokenvesting-exports' diff --git a/project-8-token-vesting/anchor/src/vesting-exports.ts b/project-8-token-vesting/anchor/src/vesting-exports.ts deleted file mode 100644 index b33f423..0000000 --- a/project-8-token-vesting/anchor/src/vesting-exports.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Here we export some useful types and functions for interacting with the Anchor program. -import { AnchorProvider, Program } from '@coral-xyz/anchor'; -import { Cluster, PublicKey } from '@solana/web3.js'; -import VestingIDL from '../target/idl/vesting.json'; -import type { Vesting } from '../target/types/vesting'; - -// Re-export the generated IDL and type -export { Vesting, VestingIDL }; - -// The programId is imported from the program IDL. -export const VESTING_PROGRAM_ID = new PublicKey(VestingIDL.address); - -// This is a helper function to get the Vesting Anchor program. -export function getVestingProgram(provider: AnchorProvider) { - return new Program(VestingIDL as Vesting, provider); -} - -// This is a helper function to get the program ID for the Vesting program depending on the cluster. -export function getVestingProgramId(cluster: Cluster) { - switch (cluster) { - case 'devnet': - case 'testnet': - // This is the program ID for the Vesting program on devnet and testnet. - return new PublicKey('2vKg76rA1Ho27YD4uuc2Z2FCwRTySxdyHup1JjsXS6dp'); - case 'mainnet-beta': - default: - return VESTING_PROGRAM_ID; - } -} diff --git a/project-8-token-vesting/anchor/tests/bankrun.spec.ts b/project-8-token-vesting/anchor/tests/bankrun.spec.ts deleted file mode 100644 index 82ee749..0000000 --- a/project-8-token-vesting/anchor/tests/bankrun.spec.ts +++ /dev/null @@ -1,172 +0,0 @@ -// No imports needed: web3, anchor, pg and more are globally available -import * as anchor from "@coral-xyz/anchor"; -import { BankrunProvider } from "anchor-bankrun"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { BN, Program } from "@coral-xyz/anchor"; - -import { - startAnchor, - Clock, - BanksClient, - ProgramTestContext, -} from "solana-bankrun"; - -import { createMint, mintTo } from "spl-token-bankrun"; -import { PublicKey, Keypair } from "@solana/web3.js"; -import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; - -import IDL from "../target/idl/vesting.json"; -import { Vesting } from "../target/types/vesting"; -import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; - -describe("Vesting Smart Contract Tests", () => { - const companyName = "Company"; - let beneficiary: Keypair; - let vestingAccountKey: PublicKey; - let treasuryTokenAccount: PublicKey; - let employeeAccount: PublicKey; - let provider: BankrunProvider; - let program: Program; - let banksClient: BanksClient; - let employer: Keypair; - let mint: PublicKey; - let beneficiaryProvider: BankrunProvider; - let program2: Program; - let context: ProgramTestContext; - - beforeAll(async () => { - beneficiary = new anchor.web3.Keypair(); - - // set up bankrun - context = await startAnchor( - "", - [{ name: "vesting", programId: new PublicKey(IDL.address) }], - [ - { - address: beneficiary.publicKey, - info: { - lamports: 1_000_000_000, - data: Buffer.alloc(0), - owner: SYSTEM_PROGRAM_ID, - executable: false, - }, - }, - ] - ); - provider = new BankrunProvider(context); - - anchor.setProvider(provider); - - program = new Program(IDL as Vesting, provider); - - banksClient = context.banksClient; - - employer = provider.wallet.payer; - - // Create a new mint - // @ts-ignore - mint = await createMint(banksClient, employer, employer.publicKey, null, 2); - - // Generate a new keypair for the beneficiary - beneficiaryProvider = new BankrunProvider(context); - beneficiaryProvider.wallet = new NodeWallet(beneficiary); - - program2 = new Program(IDL as Vesting, beneficiaryProvider); - - // Derive PDAs - [vestingAccountKey] = PublicKey.findProgramAddressSync( - [Buffer.from(companyName)], - program.programId - ); - - [treasuryTokenAccount] = PublicKey.findProgramAddressSync( - [Buffer.from("vesting_treasury"), Buffer.from(companyName)], - program.programId - ); - - [employeeAccount] = PublicKey.findProgramAddressSync( - [ - Buffer.from("employee_vesting"), - beneficiary.publicKey.toBuffer(), - vestingAccountKey.toBuffer(), - ], - program.programId - ); - }); - - it("should create a vesting account", async () => { - const tx = await program.methods - .createVestingAccount(companyName) - .accounts({ - signer: employer.publicKey, - mint, - tokenProgram: TOKEN_PROGRAM_ID, - }) - .rpc({ commitment: "confirmed" }); - - const vestingAccountData = await program.account.vestingAccount.fetch( - vestingAccountKey, - "confirmed" - ); - console.log( - "Vesting Account Data:", - JSON.stringify(vestingAccountData, null, 2) - ); - - console.log("Create Vesting Account Transaction Signature:", tx); - }); - - it("should fund the treasury token account", async () => { - const amount = 10_000 * 10 ** 9; - const mintTx = await mintTo( - // @ts-ignores - banksClient, - employer, - mint, - treasuryTokenAccount, - employer, - amount - ); - - console.log("Mint to Treasury Transaction Signature:", mintTx); - }); - - it("should create an employee vesting account", async () => { - const tx2 = await program.methods - .createEmployeeVesting(new BN(0), new BN(100), new BN(100), new BN(0)) - .accounts({ - beneficiary: beneficiary.publicKey, - vestingAccount: vestingAccountKey, - }) - .rpc({ commitment: "confirmed", skipPreflight: true }); - - console.log("Create Employee Account Transaction Signature:", tx2); - console.log("Employee account", employeeAccount.toBase58()); - }); - - it("should claim tokens", async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - - const currentClock = await banksClient.getClock(); - context.setClock( - new Clock( - currentClock.slot, - currentClock.epochStartTimestamp, - currentClock.epoch, - currentClock.leaderScheduleEpoch, - 1000n - ) - ); - - console.log("Employee account", employeeAccount.toBase58()); - - const tx3 = await program2.methods - .claimTokens(companyName) - .accounts({ - tokenProgram: TOKEN_PROGRAM_ID, - }) - .rpc({ commitment: "confirmed" }); - - console.log("Claim Tokens transaction signature", tx3); - }); -}); diff --git a/project-8-token-vesting/anchor/tests/dapptokenvesting.spec.ts b/project-8-token-vesting/anchor/tests/dapptokenvesting.spec.ts new file mode 100644 index 0000000..c177b06 --- /dev/null +++ b/project-8-token-vesting/anchor/tests/dapptokenvesting.spec.ts @@ -0,0 +1,76 @@ +import * as anchor from '@coral-xyz/anchor' +import {Program} from '@coral-xyz/anchor' +import {Keypair} from '@solana/web3.js' +import {Dapptokenvesting} from '../target/types/dapptokenvesting' + +describe('dapptokenvesting', () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env() + anchor.setProvider(provider) + const payer = provider.wallet as anchor.Wallet + + const program = anchor.workspace.Dapptokenvesting as Program + + const dapptokenvestingKeypair = Keypair.generate() + + it('Initialize Dapptokenvesting', async () => { + await program.methods + .initialize() + .accounts({ + dapptokenvesting: dapptokenvestingKeypair.publicKey, + payer: payer.publicKey, + }) + .signers([dapptokenvestingKeypair]) + .rpc() + + const currentCount = await program.account.dapptokenvesting.fetch(dapptokenvestingKeypair.publicKey) + + expect(currentCount.count).toEqual(0) + }) + + it('Increment Dapptokenvesting', async () => { + await program.methods.increment().accounts({ dapptokenvesting: dapptokenvestingKeypair.publicKey }).rpc() + + const currentCount = await program.account.dapptokenvesting.fetch(dapptokenvestingKeypair.publicKey) + + expect(currentCount.count).toEqual(1) + }) + + it('Increment Dapptokenvesting Again', async () => { + await program.methods.increment().accounts({ dapptokenvesting: dapptokenvestingKeypair.publicKey }).rpc() + + const currentCount = await program.account.dapptokenvesting.fetch(dapptokenvestingKeypair.publicKey) + + expect(currentCount.count).toEqual(2) + }) + + it('Decrement Dapptokenvesting', async () => { + await program.methods.decrement().accounts({ dapptokenvesting: dapptokenvestingKeypair.publicKey }).rpc() + + const currentCount = await program.account.dapptokenvesting.fetch(dapptokenvestingKeypair.publicKey) + + expect(currentCount.count).toEqual(1) + }) + + it('Set dapptokenvesting value', async () => { + await program.methods.set(42).accounts({ dapptokenvesting: dapptokenvestingKeypair.publicKey }).rpc() + + const currentCount = await program.account.dapptokenvesting.fetch(dapptokenvestingKeypair.publicKey) + + expect(currentCount.count).toEqual(42) + }) + + it('Set close the dapptokenvesting account', async () => { + await program.methods + .close() + .accounts({ + payer: payer.publicKey, + dapptokenvesting: dapptokenvestingKeypair.publicKey, + }) + .rpc() + + // The account should no longer exist, returning null. + const userAccount = await program.account.dapptokenvesting.fetchNullable(dapptokenvestingKeypair.publicKey) + expect(userAccount).toBeNull() + }) +}) diff --git a/project-8-token-vesting/anchor/tests/fixtures/vesting.so b/project-8-token-vesting/anchor/tests/fixtures/vesting.so deleted file mode 100755 index 80ac024..0000000 Binary files a/project-8-token-vesting/anchor/tests/fixtures/vesting.so and /dev/null differ diff --git a/project-8-token-vesting/anchor/tsconfig.json b/project-8-token-vesting/anchor/tsconfig.json index 150e738..825c613 100644 --- a/project-8-token-vesting/anchor/tsconfig.json +++ b/project-8-token-vesting/anchor/tsconfig.json @@ -1,16 +1,36 @@ { - "extends": "../tsconfig.base.json", "compilerOptions": { - "module": "commonjs" - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["jest", "node"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@project/anchor": ["anchor/src"], + "@/*": ["./src/*"] } - ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.spec.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"] } diff --git a/project-8-token-vesting/anchor/tsconfig.lib.json b/project-8-token-vesting/anchor/tsconfig.lib.json deleted file mode 100644 index ed7d7ef..0000000 --- a/project-8-token-vesting/anchor/tsconfig.lib.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../dist/out-tsc", - "declaration": true, - "types": ["node"], - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/project-8-token-vesting/anchor/tsconfig.spec.json b/project-8-token-vesting/anchor/tsconfig.spec.json deleted file mode 100644 index 663c878..0000000 --- a/project-8-token-vesting/anchor/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/project-8-token-vesting/dist/web/.nx-helpers/compiled.js b/project-8-token-vesting/dist/web/.nx-helpers/compiled.js new file mode 100644 index 0000000..1e04b98 --- /dev/null +++ b/project-8-token-vesting/dist/web/.nx-helpers/compiled.js @@ -0,0 +1,6 @@ + + const withNx = require('./with-nx'); + module.exports = withNx; + module.exports.withNx = withNx; + module.exports.composePlugins = require('./compose-plugins').composePlugins; + \ No newline at end of file diff --git a/project-8-token-vesting/dist/web/.nx-helpers/compose-plugins.js b/project-8-token-vesting/dist/web/.nx-helpers/compose-plugins.js new file mode 100644 index 0000000..9fd40f8 --- /dev/null +++ b/project-8-token-vesting/dist/web/.nx-helpers/compose-plugins.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.composePlugins = void 0; +function composePlugins(...plugins) { + return function (baseConfig) { + return async function combined(phase, context) { + let config = baseConfig; + for (const plugin of plugins) { + const fn = await plugin; + const configOrFn = fn(config); + if (typeof configOrFn === 'function') { + config = await configOrFn(phase, context); + } + else { + config = configOrFn; + } + } + return config; + }; + }; +} +exports.composePlugins = composePlugins; diff --git a/project-8-token-vesting/dist/web/.nx-helpers/with-nx.js b/project-8-token-vesting/dist/web/.nx-helpers/with-nx.js new file mode 100644 index 0000000..2cfece5 --- /dev/null +++ b/project-8-token-vesting/dist/web/.nx-helpers/with-nx.js @@ -0,0 +1,303 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withNx = exports.forNextVersion = exports.getAliasForProject = exports.getNextConfig = void 0; +function regexEqual(x, y) { + return (x instanceof RegExp && + y instanceof RegExp && + x.source === y.source && + x.global === y.global && + x.ignoreCase === y.ignoreCase && + x.multiline === y.multiline); +} +/** + * Do not remove or rename this function. Production builds inline `with-nx.js` file with a replacement + * To this function that hard-codes the libsDir. + */ +function getWithNxContext() { +return { +workspaceRoot: '/home/kavit/web3/full-stack-projects/solana-developers/developer-bootcamp-2024/project-8-token-vesting', +libsDir: 'libs' +} +} +function getNxContext(graph, target) { + const { parseTargetString, workspaceRoot } = require('@nx/devkit'); + const projectNode = graph.nodes[target.project]; + const targetConfig = projectNode.data.targets[target.target]; + const targetOptions = targetConfig.options; + if (target.configuration) { + Object.assign(targetOptions, targetConfig.configurations[target.configuration]); + } + const partialExecutorContext = { + projectName: target.project, + targetName: target.target, + projectGraph: graph, + configurationName: target.configuration, + root: workspaceRoot, + }; + if (targetOptions.devServerTarget) { + // Executors such as @nx/cypress:cypress define the devServerTarget option. + return getNxContext(graph, parseTargetString(targetOptions.devServerTarget, partialExecutorContext)); + } + else if (targetOptions.buildTarget) { + // Executors such as @nx/next:server or @nx/next:export define the buildTarget option. + return getNxContext(graph, parseTargetString(targetOptions.buildTarget, partialExecutorContext)); + } + // Default case, return info for current target. + // This could be a build using @nx/next:build or run-commands without using our executors. + return { + node: graph.nodes[target.project], + options: targetOptions, + projectName: target.project, + targetName: target.target, + configurationName: target.configuration, + }; +} +/** + * Try to read output dir from project, and default to '.next' if executing outside of Nx (e.g. dist is added to a docker image). + */ +function withNx(_nextConfig = {}, context = getWithNxContext()) { + return async (phase) => { + const { PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER } = await Promise.resolve().then(() => require('next/constants')); + // Two scenarios where we want to skip graph creation: + // 1. Running production server means the build is already done so we just need to start the Next.js server. + // 2. During graph creation (i.e. create nodes), we won't have a graph to read, and it is not needed anyway since it's a build-time concern. + // + // NOTE: Avoid any `require(...)` or `import(...)` statements here. Development dependencies are not available at production runtime. + if (PHASE_PRODUCTION_SERVER === phase || global.NX_GRAPH_CREATION) { + const { nx, ...validNextConfig } = _nextConfig; + return { + distDir: '.next', + ...validNextConfig, + }; + } + else { + const { createProjectGraphAsync, joinPathFragments, offsetFromRoot, workspaceRoot, } = require('@nx/devkit'); + let graph; + try { + graph = await createProjectGraphAsync(); + } + catch (e) { + throw new Error('Could not create project graph. Please ensure that your workspace is valid.', { cause: e }); + } + const originalTarget = { + project: process.env.NX_TASK_TARGET_PROJECT, + target: process.env.NX_TASK_TARGET_TARGET, + configuration: process.env.NX_TASK_TARGET_CONFIGURATION, + }; + const { node: projectNode, options, projectName: project, } = getNxContext(graph, originalTarget); + const projectDirectory = projectNode.data.root; + // Get next config + const nextConfig = getNextConfig(_nextConfig, context); + // For Next.js 13.1 and greater, make sure workspace libs are transpiled. + forNextVersion('>=13.1.0', () => { + if (!graph.dependencies[project]) + return; + const { readTsConfigPaths } = require('@nx/js'); + const { findAllProjectNodeDependencies, } = require('nx/src/utils/project-graph-utils'); + const paths = readTsConfigPaths(); + const deps = findAllProjectNodeDependencies(project); + nextConfig.transpilePackages ??= []; + for (const dep of deps) { + const alias = getAliasForProject(graph.nodes[dep], paths); + if (alias) { + nextConfig.transpilePackages.push(alias); + } + } + }); + // process.env.NX_NEXT_OUTPUT_PATH is set when running @nx/next:build + options.outputPath = + process.env.NX_NEXT_OUTPUT_PATH || options.outputPath; + // outputPath may be undefined if using run-commands or other executors other than @nx/next:build. + // In this case, the user should set distDir in their next.config.js. + if (options.outputPath && phase !== PHASE_DEVELOPMENT_SERVER) { + const outputDir = `${offsetFromRoot(projectDirectory)}${options.outputPath}`; + // If running dev-server, we should keep `.next` inside project directory since Turbopack expects this. + // See: https://github.com/nrwl/nx/issues/19365 + nextConfig.distDir = + nextConfig.distDir && nextConfig.distDir !== '.next' + ? joinPathFragments(outputDir, nextConfig.distDir) + : joinPathFragments(outputDir, '.next'); + } + // If we are running a static serve of the Next.js app, we need to change the output to 'export' and the distDir to 'out'. + if (process.env.NX_SERVE_STATIC_BUILD_RUNNING === 'true') { + nextConfig.output = 'export'; + nextConfig.distDir = 'out'; + } + const userWebpackConfig = nextConfig.webpack; + const { createWebpackConfig } = require('@nx/next/src/utils/config'); + nextConfig.webpack = (a, b) => createWebpackConfig(workspaceRoot, projectDirectory, options.fileReplacements, options.assets)(userWebpackConfig ? userWebpackConfig(a, b) : a, b); + return nextConfig; + } + }; +} +exports.withNx = withNx; +function getNextConfig(nextConfig = {}, context = getWithNxContext()) { + // If `next-compose-plugins` is used, the context argument is invalid. + if (!context.libsDir || !context.workspaceRoot) { + context = getWithNxContext(); + } + const userWebpack = nextConfig.webpack || ((x) => x); + const { nx, ...validNextConfig } = nextConfig; + return { + eslint: { + ignoreDuringBuilds: true, + ...(validNextConfig.eslint ?? {}), + }, + ...validNextConfig, + webpack: (config, options) => { + /* + * Update babel to support our monorepo setup. + * The 'upward' mode allows the root babel.config.json and per-project .babelrc files to be picked up. + */ + if (nx?.babelUpwardRootMode) { + options.defaultLoaders.babel.options.babelrc = true; + options.defaultLoaders.babel.options.rootMode = 'upward'; + } + /* + * Modify the Next.js webpack config to allow workspace libs to use css modules. + * Note: This would be easier if Next.js exposes css-loader and sass-loader on `defaultLoaders`. + */ + // Include workspace libs in css/sass loaders + const includes = [ + require('path').join(context.workspaceRoot, context.libsDir), + ]; + const nextCssLoaders = config.module.rules.find((rule) => typeof rule.oneOf === 'object'); + // webpack config is not as expected + if (!nextCssLoaders) + return config; + /* + * 1. Modify css loader to enable module support for workspace libs + */ + const nextCssLoader = nextCssLoaders.oneOf.find((rule) => rule.sideEffects === false && regexEqual(rule.test, /\.module\.css$/)); + // Might not be found if Next.js webpack config changes in the future + if (nextCssLoader && nextCssLoader.issuer) { + nextCssLoader.issuer.or = nextCssLoader.issuer.and + ? nextCssLoader.issuer.and.concat(includes) + : includes; + delete nextCssLoader.issuer.and; + } + /* + * 2. Modify sass loader to enable module support for workspace libs + */ + const nextSassLoader = nextCssLoaders.oneOf.find((rule) => rule.sideEffects === false && + regexEqual(rule.test, /\.module\.(scss|sass)$/)); + // Might not be found if Next.js webpack config changes in the future + if (nextSassLoader && nextSassLoader.issuer) { + nextSassLoader.issuer.or = nextSassLoader.issuer.and + ? nextSassLoader.issuer.and.concat(includes) + : includes; + delete nextSassLoader.issuer.and; + } + /* + * 3. Modify error loader to ignore css modules used by workspace libs + */ + const nextErrorCssModuleLoader = nextCssLoaders.oneOf.find((rule) => rule.use && + rule.use.loader === 'error-loader' && + rule.use.options && + (rule.use.options.reason === + 'CSS Modules \u001b[1mcannot\u001b[22m be imported from within \u001b[1mnode_modules\u001b[22m.\n' + + 'Read more: https://err.sh/next.js/css-modules-npm' || + rule.use.options.reason === + 'CSS Modules cannot be imported from within node_modules.\nRead more: https://err.sh/next.js/css-modules-npm')); + // Might not be found if Next.js webpack config changes in the future + if (nextErrorCssModuleLoader) { + nextErrorCssModuleLoader.exclude = includes; + } + /** + * 4. Modify css loader to allow global css from node_modules to be imported from workspace libs + */ + const nextGlobalCssLoader = nextCssLoaders.oneOf.find((rule) => rule.include?.and?.find((include) => regexEqual(include, /node_modules/))); + // Might not be found if Next.js webpack config changes in the future + if (nextGlobalCssLoader && nextGlobalCssLoader.issuer) { + nextGlobalCssLoader.issuer.or = nextGlobalCssLoader.issuer.and + ? nextGlobalCssLoader.issuer.and.concat(includes) + : includes; + delete nextGlobalCssLoader.issuer.and; + } + /** + * 5. Add SVGR support if option is on. + */ + // Default SVGR support to be on for projects. + if (nx?.svgr !== false) { + // TODO(v20): Remove file-loader and use `?react` querystring to differentiate between asset and SVGR. + // It should be: + // use: [{ + // test: /\.svg$/i, + // type: 'asset', + // resourceQuery: /react/, // *.svg?react + // }, + // { + // test: /\.svg$/i, + // issuer: /\.[jt]sx?$/, + // resourceQuery: { not: [/react/] }, // exclude react component if *.svg?react + // use: ['@svgr/webpack'], + // }], + // See: + // - SVGR: https://react-svgr.com/docs/webpack/#use-svgr-and-asset-svg-in-the-same-project + // - Vite: https://www.npmjs.com/package/vite-plugin-svgr + // - Rsbuild: https://github.com/web-infra-dev/rsbuild/pull/1783 + // Note: We also need a migration for any projects that are using SVGR to convert + // `import { ReactComponent as X } from './x.svg` to + // `import X from './x.svg?react'; + config.module.rules.push({ + test: /\.svg$/, + issuer: { not: /\.(css|scss|sass)$/ }, + resourceQuery: { + not: [ + /__next_metadata__/, + /__next_metadata_route__/, + /__next_metadata_image_meta__/, + ], + }, + use: [ + { + loader: require.resolve('@svgr/webpack'), + options: { + svgo: false, + titleProp: true, + ref: true, + }, + }, + { + loader: require.resolve('file-loader'), + options: { + // Next.js hard-codes assets to load from "static/media". + // See: https://github.com/vercel/next.js/blob/53d017d/packages/next/src/build/webpack-config.ts#L1993 + name: 'static/media/[name].[hash].[ext]', + }, + }, + ], + }); + } + return userWebpack(config, options); + }, + }; +} +exports.getNextConfig = getNextConfig; +function getAliasForProject(node, paths) { + // Match workspace libs to their alias in tsconfig paths. + for (const [alias, lookup] of Object.entries(paths ?? {})) { + const lookupContainsDepNode = lookup.some((lookupPath) => lookupPath.startsWith(node?.data?.root) || + lookupPath.startsWith('./' + node?.data?.root)); + if (lookupContainsDepNode) { + return alias; + } + } + return null; +} +exports.getAliasForProject = getAliasForProject; +// Runs a function if the Next.js version satisfies the range. +function forNextVersion(range, fn) { + const semver = require('semver'); + const nextJsVersion = require('next/package.json').version; + if (semver.satisfies(nextJsVersion, range)) { + fn(); + } +} +exports.forNextVersion = forNextVersion; +// Support for older generated code: `const withNx = require('@nx/next/plugins/with-nx');` +module.exports = withNx; +// Support for newer generated code: `const { withNx } = require(...);` +module.exports.withNx = withNx; +module.exports.getNextConfig = getNextConfig; +module.exports.getAliasForProject = getAliasForProject; diff --git a/project-8-token-vesting/dist/web/next.config.js b/project-8-token-vesting/dist/web/next.config.js new file mode 100644 index 0000000..419dfb5 --- /dev/null +++ b/project-8-token-vesting/dist/web/next.config.js @@ -0,0 +1,30 @@ +//@ts-check + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { composePlugins, withNx } = require('./.nx-helpers/compiled.js'); + +/** + * @type {import('./.nx-helpers/compiled.js').WithNxOptions} + **/ +const nextConfig = { + webpack: (config) => { + config.externals = [ + ...(config.externals || []), + 'bigint', + 'node-gyp-build', + ]; + return config; + }, + nx: { + // Set this to true if you would like to use SVGR + // See: https://github.com/gregberge/svgr + svgr: false, + }, +}; + +const plugins = [ + // Add more Next.js plugins to this list if needed. + withNx, +]; + +module.exports = composePlugins(...plugins)(nextConfig); diff --git a/project-8-token-vesting/dist/web/package.json b/project-8-token-vesting/dist/web/package.json new file mode 100644 index 0000000..03c2c03 --- /dev/null +++ b/project-8-token-vesting/dist/web/package.json @@ -0,0 +1,8 @@ +{ + "name": "web", + "version": "0.0.1", + "scripts": { + "start": "next start" + }, + "dependencies": {} +} diff --git a/project-8-token-vesting/dist/web/public/.gitkeep b/project-8-token-vesting/dist/web/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/project-8-token-vesting/dist/web/public/logo.png b/project-8-token-vesting/dist/web/public/logo.png new file mode 100644 index 0000000..4676bea Binary files /dev/null and b/project-8-token-vesting/dist/web/public/logo.png differ diff --git a/project-8-token-vesting/package.json b/project-8-token-vesting/package.json index 674658a..2e6dd3e 100644 --- a/project-8-token-vesting/package.json +++ b/project-8-token-vesting/package.json @@ -9,6 +9,7 @@ "anchor-test": "nx run anchor:anchor test", "feature": "nx generate @solana-developers/preset-react:feature", "build": "nx build web", + "start": "nx serve web", "dev": "nx serve web" }, "private": true, @@ -19,7 +20,6 @@ "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-adapter-react": "^0.15.35", "@solana/wallet-adapter-react-ui": "^0.9.35", - "@solana/web3.js": "1.94.0", "@tabler/icons-react": "3.5.0", "@tailwindcss/typography": "0.5.13", "@tanstack/react-query": "5.40.0", @@ -30,9 +30,12 @@ "encoding": "0.1.13", "jotai": "2.8.3", "next": "14.0.4", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-flatpickr": "^3.10.13", "react-hot-toast": "2.4.1", + "react-icons": "^5.4.0", + "react-perfect-scrollbar": "^1.5.8", "tslib": "^2.3.0" }, "devDependencies": { @@ -48,10 +51,12 @@ "@swc/core": "~1.3.85", "@swc/helpers": "~0.5.2", "@swc/jest": "0.2.20", + "@types/bn.js": "^5.1.6", "@types/jest": "^29.4.0", "@types/node": "18.16.9", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "@types/react-flatpickr": "^3.8.11", "@typescript-eslint/eslint-plugin": "^7.3.0", "@typescript-eslint/parser": "^7.3.0", "autoprefixer": "10.4.13", diff --git a/project-8-token-vesting/web/app/account/[address]/page.tsx b/project-8-token-vesting/web/app/account/[address]/page.tsx index aea13b8..5bb6688 100644 --- a/project-8-token-vesting/web/app/account/[address]/page.tsx +++ b/project-8-token-vesting/web/app/account/[address]/page.tsx @@ -1,5 +1,5 @@ -import AccountDetailFeature from '@/components/account/account-detail-feature'; +import AccountDetailFeature from '@/components/account/account-detail-feature' export default function Page() { - return ; + return } diff --git a/project-8-token-vesting/web/app/account/page.tsx b/project-8-token-vesting/web/app/account/page.tsx index d937df6..1b0e276 100644 --- a/project-8-token-vesting/web/app/account/page.tsx +++ b/project-8-token-vesting/web/app/account/page.tsx @@ -1,5 +1,5 @@ -import AccountListFeature from '@/components/account/account-list-feature'; +import AccountListFeature from '@/components/account/account-list-feature' export default function Page() { - return ; + return } diff --git a/project-8-token-vesting/web/app/api/hello/route.ts b/project-8-token-vesting/web/app/api/hello/route.ts index de70bac..e74df4e 100644 --- a/project-8-token-vesting/web/app/api/hello/route.ts +++ b/project-8-token-vesting/web/app/api/hello/route.ts @@ -1,3 +1,3 @@ export async function GET(request: Request) { - return new Response('Hello, from API!'); + return new Response('Hello, from API!') } diff --git a/project-8-token-vesting/web/app/clusters/page.tsx b/project-8-token-vesting/web/app/clusters/page.tsx index 5ac2a9a..1f3ca0e 100644 --- a/project-8-token-vesting/web/app/clusters/page.tsx +++ b/project-8-token-vesting/web/app/clusters/page.tsx @@ -1,5 +1,5 @@ -import ClusterFeature from '@/components/cluster/cluster-feature'; +import ClusterFeature from '@/components/cluster/cluster-feature' export default function Page() { - return ; + return } diff --git a/project-8-token-vesting/web/app/createMint/page.tsx b/project-8-token-vesting/web/app/createMint/page.tsx new file mode 100644 index 0000000..2e75799 --- /dev/null +++ b/project-8-token-vesting/web/app/createMint/page.tsx @@ -0,0 +1,5 @@ +import TokenMemeFeature from "@/components/mintMeme/meme-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/app/createVesting/page.tsx b/project-8-token-vesting/web/app/createVesting/page.tsx new file mode 100644 index 0000000..44a23be --- /dev/null +++ b/project-8-token-vesting/web/app/createVesting/page.tsx @@ -0,0 +1,5 @@ +import VestingdappFeature from "@/components/vesting/vestingdapp-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/app/dapptokenvesting/page.tsx b/project-8-token-vesting/web/app/dapptokenvesting/page.tsx new file mode 100644 index 0000000..44a23be --- /dev/null +++ b/project-8-token-vesting/web/app/dapptokenvesting/page.tsx @@ -0,0 +1,5 @@ +import VestingdappFeature from "@/components/vesting/vestingdapp-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/app/employeetoken/page.tsx b/project-8-token-vesting/web/app/employeetoken/page.tsx new file mode 100644 index 0000000..b7caca6 --- /dev/null +++ b/project-8-token-vesting/web/app/employeetoken/page.tsx @@ -0,0 +1,6 @@ +import EmployeeFeature from "@/components/employee/employee-feature"; +import TokenMemeFeature from "@/components/mintMeme/meme-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/public/favicon.ico b/project-8-token-vesting/web/app/favicon.ico similarity index 100% rename from project-8-token-vesting/web/public/favicon.ico rename to project-8-token-vesting/web/app/favicon.ico diff --git a/project-8-token-vesting/web/app/global.css b/project-8-token-vesting/web/app/global.css deleted file mode 100644 index 390a546..0000000 --- a/project-8-token-vesting/web/app/global.css +++ /dev/null @@ -1,19 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -html, -body { - height: 100%; -} - -.wallet-adapter-button-trigger { - background: rgb(100, 26, 230) !important; - border-radius: 8px !important; - padding-left: 16px !important; - padding-right: 16px !important; -} -.wallet-adapter-dropdown-list, -.wallet-adapter-button { - font-family: inherit !important; -} diff --git a/project-8-token-vesting/web/app/globals.css b/project-8-token-vesting/web/app/globals.css new file mode 100644 index 0000000..e2610a5 --- /dev/null +++ b/project-8-token-vesting/web/app/globals.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + font-size: 16px; + overflow-x: hidden; + -webkit-text-size-adjust: 100%; +} + +.wallet-adapter-button-trigger { + background: rgb(100, 26, 230) !important; + border-radius: 8px !important; + padding-left: 16px !important; + padding-right: 16px !important; +} +.wallet-adapter-dropdown-list, +.wallet-adapter-button { + font-family: inherit !important; +} +.ps__rail-x, +.ps__rail-y { + background-color: transparent !important; + z-index: 1; +} + +.ps__thumb-x { + background-color: #888; + height: 10px !important; + border-radius: 5px; + min-width: 30px !important; +} + +.ps__thumb-y { + background-color: #888; + width: 10px !important; + border-radius: 5px; + min-height: 30px !important; +} + +.ps__thumb-x:hover, +.ps__thumb-y:hover { + background-color: #555; +} diff --git a/project-8-token-vesting/web/app/layout.tsx b/project-8-token-vesting/web/app/layout.tsx index 739922f..19dddba 100644 --- a/project-8-token-vesting/web/app/layout.tsx +++ b/project-8-token-vesting/web/app/layout.tsx @@ -1,18 +1,25 @@ -import './global.css'; -import { UiLayout } from '@/components/ui/ui-layout'; -import { ClusterProvider } from '@/components/cluster/cluster-data-access'; -import { SolanaProvider } from '@/components/solana/solana-provider'; -import { ReactQueryProvider } from './react-query-provider'; +import "./globals.css"; +import { ClusterProvider } from "@/components/cluster/cluster-data-access"; +import { SolanaProvider } from "@/components/solana/solana-provider"; +import { UiLayout } from "@/components/ui/ui-layout"; +import { ReactQueryProvider } from "./react-query-provider"; export const metadata = { - title: 'token-vesting', - description: 'Generated by create-solana-dapp', + title: "dapp-token-vesting", + description: "Generated by create-solana-dapp", }; const links: { label: string; path: string }[] = [ - { label: 'Account', path: '/account' }, - { label: 'Clusters', path: '/clusters' }, - { label: 'Vesting Program', path: '/vesting' }, + { label: "Account", path: "/account" }, + { label: "Clusters", path: "/clusters" }, +]; +const companyLinks: { label: string; path: string }[] = [ + { label: "Token Program", path: "/mint" }, + { label: "Vesting Program", path: "/dapptokenvesting" }, + { label: "Vested Employees", path: "/vestedemployees" }, +]; +const employeeLinks: { label: string; path: string }[] = [ + { label: "Claim Token", path: "/employeetoken" }, ]; export default function RootLayout({ @@ -22,11 +29,21 @@ export default function RootLayout({ }) { return ( + - {children} + + {children} + diff --git a/project-8-token-vesting/web/app/mint/page.tsx b/project-8-token-vesting/web/app/mint/page.tsx new file mode 100644 index 0000000..2e75799 --- /dev/null +++ b/project-8-token-vesting/web/app/mint/page.tsx @@ -0,0 +1,5 @@ +import TokenMemeFeature from "@/components/mintMeme/meme-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/app/page.module.css b/project-8-token-vesting/web/app/page.module.css deleted file mode 100644 index 8a13e21..0000000 --- a/project-8-token-vesting/web/app/page.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.page { -} diff --git a/project-8-token-vesting/web/app/page.tsx b/project-8-token-vesting/web/app/page.tsx index 3926721..cf7d07d 100644 --- a/project-8-token-vesting/web/app/page.tsx +++ b/project-8-token-vesting/web/app/page.tsx @@ -1,5 +1,5 @@ -import DashboardFeature from '@/components/dashboard/dashboard-feature'; +import AccountListFeature from "@/components/account/account-list-feature"; export default function Page() { - return ; + return ; } diff --git a/project-8-token-vesting/web/app/react-query-provider.tsx b/project-8-token-vesting/web/app/react-query-provider.tsx index 537b89c..e12cf8d 100644 --- a/project-8-token-vesting/web/app/react-query-provider.tsx +++ b/project-8-token-vesting/web/app/react-query-provider.tsx @@ -1,15 +1,15 @@ -'use client'; +'use client' -import React, { ReactNode, useState } from 'react'; -import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import React, { ReactNode, useState } from 'react' +import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' +import { QueryClientProvider, QueryClient } from '@tanstack/react-query' export function ReactQueryProvider({ children }: { children: ReactNode }) { - const [client] = useState(new QueryClient()); + const [client] = useState(new QueryClient()) return ( {children} - ); + ) } diff --git a/project-8-token-vesting/web/app/vestedemployees/page.tsx b/project-8-token-vesting/web/app/vestedemployees/page.tsx new file mode 100644 index 0000000..44a23be --- /dev/null +++ b/project-8-token-vesting/web/app/vestedemployees/page.tsx @@ -0,0 +1,5 @@ +import VestingdappFeature from "@/components/vesting/vestingdapp-feature"; + +export default function Page() { + return ; +} diff --git a/project-8-token-vesting/web/app/vesting/page.tsx b/project-8-token-vesting/web/app/vesting/page.tsx deleted file mode 100644 index bc538b4..0000000 --- a/project-8-token-vesting/web/app/vesting/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import VestingFeature from '@/components/vesting/vesting-feature'; - -export default function Page() { - return ; -} diff --git a/project-8-token-vesting/web/components/account/account-data-access.tsx b/project-8-token-vesting/web/components/account/account-data-access.tsx index 53cdb8e..ff1c342 100644 --- a/project-8-token-vesting/web/components/account/account-data-access.tsx +++ b/project-8-token-vesting/web/components/account/account-data-access.tsx @@ -1,7 +1,7 @@ -'use client'; +"use client"; -import { useConnection, useWallet } from '@solana/wallet-adapter-react'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { Connection, LAMPORTS_PER_SOL, @@ -10,16 +10,16 @@ import { TransactionMessage, TransactionSignature, VersionedTransaction, -} from '@solana/web3.js'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import toast from 'react-hot-toast'; -import { useTransactionToast } from '../ui/ui-layout'; +} from "@solana/web3.js"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTransactionToast } from "../ui/ui-layout"; export function useGetBalance({ address }: { address: PublicKey }) { const { connection } = useConnection(); return useQuery({ - queryKey: ['get-balance', { endpoint: connection.rpcEndpoint, address }], + queryKey: ["get-balance", { endpoint: connection.rpcEndpoint, address }], queryFn: () => connection.getBalance(address), }); } @@ -28,8 +28,8 @@ export function useGetSignatures({ address }: { address: PublicKey }) { const { connection } = useConnection(); return useQuery({ - queryKey: ['get-signatures', { endpoint: connection.rpcEndpoint, address }], - queryFn: () => connection.getConfirmedSignaturesForAddress2(address), + queryKey: ["get-signatures", { endpoint: connection.rpcEndpoint, address }], + queryFn: () => connection.getSignaturesForAddress(address), }); } @@ -38,7 +38,7 @@ export function useGetTokenAccounts({ address }: { address: PublicKey }) { return useQuery({ queryKey: [ - 'get-token-accounts', + "get-token-accounts", { endpoint: connection.rpcEndpoint, address }, ], queryFn: async () => { @@ -52,6 +52,7 @@ export function useGetTokenAccounts({ address }: { address: PublicKey }) { ]); return [...tokenAccounts.value, ...token2022Accounts.value]; }, + refetchOnWindowFocus: false, }); } @@ -63,11 +64,11 @@ export function useTransferSol({ address }: { address: PublicKey }) { return useMutation({ mutationKey: [ - 'transfer-sol', + "transfer-sol", { endpoint: connection.rpcEndpoint, address }, ], mutationFn: async (input: { destination: PublicKey; amount: number }) => { - let signature: TransactionSignature = ''; + let signature: TransactionSignature = ""; try { const { transaction, latestBlockhash } = await createTransaction({ publicKey: address, @@ -76,19 +77,16 @@ export function useTransferSol({ address }: { address: PublicKey }) { connection, }); - // Send transaction and await for signature signature = await wallet.sendTransaction(transaction, connection); - // Send transaction and await for signature await connection.confirmTransaction( { signature, ...latestBlockhash }, - 'confirmed' + "confirmed" ); - console.log(signature); return signature; } catch (error: unknown) { - console.log('error', `Transaction failed! ${error}`, signature); + console.log("error", `Transaction failed! ${error}`, signature); return; } @@ -100,19 +98,19 @@ export function useTransferSol({ address }: { address: PublicKey }) { return Promise.all([ client.invalidateQueries({ queryKey: [ - 'get-balance', + "get-balance", { endpoint: connection.rpcEndpoint, address }, ], }), client.invalidateQueries({ queryKey: [ - 'get-signatures', + "get-signatures", { endpoint: connection.rpcEndpoint, address }, ], }), ]); }, - onError: (error) => { + onError: (error: unknown) => { toast.error(`Transaction failed! ${error}`); }, }); @@ -124,7 +122,7 @@ export function useRequestAirdrop({ address }: { address: PublicKey }) { const client = useQueryClient(); return useMutation({ - mutationKey: ['airdrop', { endpoint: connection.rpcEndpoint, address }], + mutationKey: ["airdrop", { endpoint: connection.rpcEndpoint, address }], mutationFn: async (amount: number = 1) => { const [latestBlockhash, signature] = await Promise.all([ connection.getLatestBlockhash(), @@ -133,7 +131,7 @@ export function useRequestAirdrop({ address }: { address: PublicKey }) { await connection.confirmTransaction( { signature, ...latestBlockhash }, - 'confirmed' + "confirmed" ); return signature; }, @@ -142,13 +140,13 @@ export function useRequestAirdrop({ address }: { address: PublicKey }) { return Promise.all([ client.invalidateQueries({ queryKey: [ - 'get-balance', + "get-balance", { endpoint: connection.rpcEndpoint, address }, ], }), client.invalidateQueries({ queryKey: [ - 'get-signatures', + "get-signatures", { endpoint: connection.rpcEndpoint, address }, ], }), diff --git a/project-8-token-vesting/web/components/account/account-detail-feature.tsx b/project-8-token-vesting/web/components/account/account-detail-feature.tsx index cc7a2f7..113af33 100644 --- a/project-8-token-vesting/web/components/account/account-detail-feature.tsx +++ b/project-8-token-vesting/web/components/account/account-detail-feature.tsx @@ -1,33 +1,28 @@ -'use client'; +'use client' -import { PublicKey } from '@solana/web3.js'; -import { useMemo } from 'react'; +import { PublicKey } from '@solana/web3.js' +import { useMemo } from 'react' -import { useParams } from 'next/navigation'; +import { useParams } from 'next/navigation' -import { ExplorerLink } from '../cluster/cluster-ui'; -import { AppHero, ellipsify } from '../ui/ui-layout'; -import { - AccountBalance, - AccountButtons, - AccountTokens, - AccountTransactions, -} from './account-ui'; +import { ExplorerLink } from '../cluster/cluster-ui' +import { AppHero, ellipsify } from '../ui/ui-layout' +import { AccountBalance, AccountButtons, AccountTokens, AccountTransactions } from './account-ui' export default function AccountDetailFeature() { - const params = useParams(); + const params = useParams() const address = useMemo(() => { if (!params.address) { - return; + return } try { - return new PublicKey(params.address); + return new PublicKey(params.address) } catch (e) { - console.log(`Invalid public key`, e); + console.log(`Invalid public key`, e) } - }, [params]); + }, [params]) if (!address) { - return
Error loading account
; + return
Error loading account
} return ( @@ -36,10 +31,7 @@ export default function AccountDetailFeature() { title={} subtitle={
- +
} > @@ -52,5 +44,5 @@ export default function AccountDetailFeature() { - ); + ) } diff --git a/project-8-token-vesting/web/components/account/account-list-feature.tsx b/project-8-token-vesting/web/components/account/account-list-feature.tsx index fe4006e..3443c76 100644 --- a/project-8-token-vesting/web/components/account/account-list-feature.tsx +++ b/project-8-token-vesting/web/components/account/account-list-feature.tsx @@ -1,15 +1,15 @@ -'use client'; +'use client' -import { useWallet } from '@solana/wallet-adapter-react'; -import { WalletButton } from '../solana/solana-provider'; +import { useWallet } from '@solana/wallet-adapter-react' -import { redirect } from 'next/navigation'; +import { redirect } from 'next/navigation' +import { WalletButton } from '../solana/solana-provider' export default function AccountListFeature() { - const { publicKey } = useWallet(); + const { publicKey } = useWallet() if (publicKey) { - return redirect(`/account/${publicKey.toString()}`); + return redirect(`/account/${publicKey.toString()}`) } return ( @@ -18,5 +18,5 @@ export default function AccountListFeature() { - ); + ) } diff --git a/project-8-token-vesting/web/components/account/account-ui.tsx b/project-8-token-vesting/web/components/account/account-ui.tsx index e04bcc0..1ab14aa 100644 --- a/project-8-token-vesting/web/components/account/account-ui.tsx +++ b/project-8-token-vesting/web/components/account/account-ui.tsx @@ -1,20 +1,22 @@ -'use client'; +"use client"; -import { useWallet } from '@solana/wallet-adapter-react'; -import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; -import { IconRefresh } from '@tabler/icons-react'; -import { useQueryClient } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; -import { AppModal, ellipsify } from '../ui/ui-layout'; -import { useCluster } from '../cluster/cluster-data-access'; -import { ExplorerLink } from '../cluster/cluster-ui'; +import { useWallet } from "@solana/wallet-adapter-react"; +import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { IconRefresh } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { AppModal, ellipsify } from "../ui/ui-layout"; +import { useCluster } from "../cluster/cluster-data-access"; +import { ExplorerLink } from "../cluster/cluster-ui"; import { useGetBalance, useGetSignatures, useGetTokenAccounts, useRequestAirdrop, useTransferSol, -} from './account-data-access'; +} from "./account-data-access"; +import PerfectScrollbar from "react-perfect-scrollbar"; +import "react-perfect-scrollbar/dist/css/styles.css"; export function AccountBalance({ address }: { address: PublicKey }) { const query = useGetBalance({ address }); @@ -22,10 +24,10 @@ export function AccountBalance({ address }: { address: PublicKey }) { return (

query.refetch()} > - {query.data ? : '...'} SOL + {query.data ? : "..."} SOL

); @@ -53,9 +55,9 @@ export function AccountBalanceCheck({ address }: { address: PublicKey }) { is not found on this cluster. + + + )} + + + + )} )} @@ -255,61 +261,65 @@ export function AccountTransactions({ address }: { address: PublicKey }) { {query.data.length === 0 ? (
No transactions found.
) : ( - - - - - - - - - - - {items?.map((item) => ( - - - - - - - ))} - {(query.data?.length ?? 0) > 5 && ( - - - - )} - -
SignatureSlotBlock TimeStatus
- - - - - {new Date((item.blockTime ?? 0) * 1000).toISOString()} - - {item.err ? ( -
- Failed -
- ) : ( -
Success
- )} -
- -
+
+ + + + + + + + + + + + {items?.map((item) => ( + + + + + + + ))} + {(query.data?.length ?? 0) > 5 && ( + + + + )} + +
SignatureSlotBlock TimeStatus
+ + + + + {new Date((item.blockTime ?? 0) * 1000).toISOString()} + + {item.err ? ( +
+ Failed +
+ ) : ( +
Success
+ )} +
+ +
+
+
)} )} @@ -350,7 +360,7 @@ function ModalAirdrop({ address: PublicKey; }) { const mutation = useRequestAirdrop({ address }); - const [amount, setAmount] = useState('2'); + const [amount, setAmount] = useState("2"); return ( Wallet not connected; diff --git a/project-8-token-vesting/web/components/cluster/cluster-data-access.tsx b/project-8-token-vesting/web/components/cluster/cluster-data-access.tsx index 358b6c0..6d9aa87 100644 --- a/project-8-token-vesting/web/components/cluster/cluster-data-access.tsx +++ b/project-8-token-vesting/web/components/cluster/cluster-data-access.tsx @@ -1,16 +1,16 @@ -'use client'; +'use client' -import { clusterApiUrl, Connection } from '@solana/web3.js'; -import { atom, useAtomValue, useSetAtom } from 'jotai'; -import { atomWithStorage } from 'jotai/utils'; -import { createContext, ReactNode, useContext } from 'react'; -import toast from 'react-hot-toast'; +import { clusterApiUrl, Connection } from '@solana/web3.js' +import { atom, useAtomValue, useSetAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { createContext, ReactNode, useContext } from 'react' +import toast from 'react-hot-toast' export interface Cluster { - name: string; - endpoint: string; - network?: ClusterNetwork; - active?: boolean; + name: string + endpoint: string + network?: ClusterNetwork + active?: boolean } export enum ClusterNetwork { @@ -35,92 +35,83 @@ export const defaultClusters: Cluster[] = [ endpoint: clusterApiUrl('testnet'), network: ClusterNetwork.Testnet, }, -]; +] -const clusterAtom = atomWithStorage( - 'solana-cluster', - defaultClusters[0] -); -const clustersAtom = atomWithStorage( - 'solana-clusters', - defaultClusters -); +const clusterAtom = atomWithStorage('solana-cluster', defaultClusters[0]) +const clustersAtom = atomWithStorage('solana-clusters', defaultClusters) const activeClustersAtom = atom((get) => { - const clusters = get(clustersAtom); - const cluster = get(clusterAtom); + const clusters = get(clustersAtom) + const cluster = get(clusterAtom) return clusters.map((item) => ({ ...item, active: item.name === cluster.name, - })); -}); + })) +}) const activeClusterAtom = atom((get) => { - const clusters = get(activeClustersAtom); + const clusters = get(activeClustersAtom) - return clusters.find((item) => item.active) || clusters[0]; -}); + return clusters.find((item) => item.active) || clusters[0] +}) export interface ClusterProviderContext { - cluster: Cluster; - clusters: Cluster[]; - addCluster: (cluster: Cluster) => void; - deleteCluster: (cluster: Cluster) => void; - setCluster: (cluster: Cluster) => void; - getExplorerUrl(path: string): string; + cluster: Cluster + clusters: Cluster[] + addCluster: (cluster: Cluster) => void + deleteCluster: (cluster: Cluster) => void + setCluster: (cluster: Cluster) => void + getExplorerUrl(path: string): string } -const Context = createContext( - {} as ClusterProviderContext -); +const Context = createContext({} as ClusterProviderContext) export function ClusterProvider({ children }: { children: ReactNode }) { - const cluster = useAtomValue(activeClusterAtom); - const clusters = useAtomValue(activeClustersAtom); - const setCluster = useSetAtom(clusterAtom); - const setClusters = useSetAtom(clustersAtom); + const cluster = useAtomValue(activeClusterAtom) + const clusters = useAtomValue(activeClustersAtom) + const setCluster = useSetAtom(clusterAtom) + const setClusters = useSetAtom(clustersAtom) const value: ClusterProviderContext = { cluster, clusters: clusters.sort((a, b) => (a.name > b.name ? 1 : -1)), addCluster: (cluster: Cluster) => { try { - new Connection(cluster.endpoint); - setClusters([...clusters, cluster]); + new Connection(cluster.endpoint) + setClusters([...clusters, cluster]) } catch (err) { - toast.error(`${err}`); + toast.error(`${err}`) } }, deleteCluster: (cluster: Cluster) => { - setClusters(clusters.filter((item) => item.name !== cluster.name)); + setClusters(clusters.filter((item) => item.name !== cluster.name)) }, setCluster: (cluster: Cluster) => setCluster(cluster), - getExplorerUrl: (path: string) => - `https://explorer.solana.com/${path}${getClusterUrlParam(cluster)}`, - }; - return {children}; + getExplorerUrl: (path: string) => `https://explorer.solana.com/${path}${getClusterUrlParam(cluster)}`, + } + return {children} } export function useCluster() { - return useContext(Context); + return useContext(Context) } function getClusterUrlParam(cluster: Cluster): string { - let suffix = ''; + let suffix = '' switch (cluster.network) { case ClusterNetwork.Devnet: - suffix = 'devnet'; - break; + suffix = 'devnet' + break case ClusterNetwork.Mainnet: - suffix = ''; - break; + suffix = '' + break case ClusterNetwork.Testnet: - suffix = 'testnet'; - break; + suffix = 'testnet' + break default: - suffix = `custom&customUrl=${encodeURIComponent(cluster.endpoint)}`; - break; + suffix = `custom&customUrl=${encodeURIComponent(cluster.endpoint)}` + break } - return suffix.length ? `?cluster=${suffix}` : ''; + return suffix.length ? `?cluster=${suffix}` : '' } diff --git a/project-8-token-vesting/web/components/cluster/cluster-feature.tsx b/project-8-token-vesting/web/components/cluster/cluster-feature.tsx index abbc95c..3ccf3b8 100644 --- a/project-8-token-vesting/web/components/cluster/cluster-feature.tsx +++ b/project-8-token-vesting/web/components/cluster/cluster-feature.tsx @@ -1,31 +1,22 @@ -'use client'; +'use client' -import { useState } from 'react'; -import { AppHero } from '../ui/ui-layout'; -import { ClusterUiModal } from './cluster-ui'; -import { ClusterUiTable } from './cluster-ui'; +import { useState } from 'react' +import { AppHero } from '../ui/ui-layout' +import { ClusterUiModal } from './cluster-ui' +import { ClusterUiTable } from './cluster-ui' export default function ClusterFeature() { - const [showModal, setShowModal] = useState(false); + const [showModal, setShowModal] = useState(false) return (
- - setShowModal(false)} - /> -
- ); + ) } diff --git a/project-8-token-vesting/web/components/cluster/cluster-ui.tsx b/project-8-token-vesting/web/components/cluster/cluster-ui.tsx index 43b349f..a785ae3 100644 --- a/project-8-token-vesting/web/components/cluster/cluster-ui.tsx +++ b/project-8-token-vesting/web/components/cluster/cluster-ui.tsx @@ -1,12 +1,13 @@ -'use client'; +"use client"; -import { useConnection } from '@solana/wallet-adapter-react'; -import { IconTrash } from '@tabler/icons-react'; -import { useQuery } from '@tanstack/react-query'; -import { ReactNode, useState } from 'react'; -import { AppModal } from '../ui/ui-layout'; -import { ClusterNetwork, useCluster } from './cluster-data-access'; -import { Connection } from '@solana/web3.js'; +import { useConnection } from "@solana/wallet-adapter-react"; +import { IconTrash } from "@tabler/icons-react"; +import { useQuery } from "@tanstack/react-query"; +import React, { ReactNode, useState } from "react"; +import { AppModal } from "../ui/ui-layout"; +import { ClusterNetwork, useCluster } from "./cluster-data-access"; +import { Connection } from "@solana/web3.js"; +import { SetStateAction } from "jotai"; export function ExplorerLink({ path, @@ -35,7 +36,7 @@ export function ClusterChecker({ children }: { children: ReactNode }) { const { connection } = useConnection(); const query = useQuery({ - queryKey: ['version', { cluster, endpoint: connection.rpcEndpoint }], + queryKey: ["version", { cluster, endpoint: connection.rpcEndpoint }], queryFn: () => connection.getVersion(), retry: 1, }); @@ -48,10 +49,7 @@ export function ClusterChecker({ children }: { children: ReactNode }) { Error connecting to cluster {cluster.name} - @@ -60,7 +58,11 @@ export function ClusterChecker({ children }: { children: ReactNode }) { return children; } -export function ClusterUiSelect() { +export function ClusterUiSelect({ + setShowMenu, +}: { + setShowMenu: React.Dispatch>; +}) { const { clusters, setCluster, cluster } = useCluster(); return (
@@ -69,15 +71,18 @@ export function ClusterUiSelect() {
    {clusters.map((item) => (
  • @@ -96,13 +101,13 @@ export function ClusterUiModal({ show: boolean; }) { const { addCluster } = useCluster(); - const [name, setName] = useState(''); + const [name, setName] = useState(""); const [network, setNetwork] = useState(); - const [endpoint, setEndpoint] = useState(''); + const [endpoint, setEndpoint] = useState(""); return ( { @@ -112,10 +117,10 @@ export function ClusterUiModal({ addCluster({ name, network, endpoint }); hideModal(); } else { - console.log('Invalid cluster name'); + console.log("Invalid cluster name"); } } catch { - console.log('Invalid cluster endpoint'); + console.log("Invalid cluster endpoint"); } }} submitLabel="Save" @@ -161,7 +166,7 @@ export function ClusterUiTable() { {clusters.map((item) => ( - +
    @@ -179,7 +184,7 @@ export function ClusterUiTable() {
    - Network: {item.network ?? 'custom'} + Network: {item.network ?? "custom"}
    {item.endpoint} @@ -188,9 +193,9 @@ export function ClusterUiTable() {
- ); + ) } diff --git a/project-8-token-vesting/web/components/employee/employee-data-access.tsx b/project-8-token-vesting/web/components/employee/employee-data-access.tsx new file mode 100644 index 0000000..713f8d3 --- /dev/null +++ b/project-8-token-vesting/web/components/employee/employee-data-access.tsx @@ -0,0 +1,161 @@ +import { PublicKey, SystemProgram, Transaction } from "@solana/web3.js"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useCommonProgram } from "../common/common-data-access"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddress, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import toast from "react-hot-toast"; +import { useTransactionToast } from "../ui/ui-layout"; +import { useRouter } from "next/navigation"; + +export function useEmployee(walletKey: PublicKey) { + const { cluster, program } = useCommonProgram(); + const wallet = useWallet(); + const { connection } = useConnection(); + const router = useRouter(); + + const transactionToast = useTransactionToast(); + + const calculateClaimableTokens = ( + startTime: number, + endTime: number, + cliffTime: number, + totalAmount: number, + totalWithdrawn: number, + currentTime: number + ): number => { + if (currentTime < cliffTime) { + return 0; + } + + const elapsedTime = Math.min(currentTime, endTime) - startTime; + const vestingDuration = endTime - startTime; + + if (vestingDuration <= 0) { + throw new Error( + "Invalid vesting schedule: endTime must be greater than startTime." + ); + } + + const vestableAmount = (totalAmount * elapsedTime) / vestingDuration; + const claimableTokens = Math.max(0, vestableAmount - totalWithdrawn); + + return claimableTokens; + }; + + const employeeAccounts = useQuery({ + queryKey: ["employeeTokenVestingAccounts", "all", { cluster }], + queryFn: async () => { + const queryResponse = await program.account.employeeAccount.all([ + { + memcmp: { + offset: 8, + bytes: walletKey.toBase58(), + }, + }, + ]); + const vestingAccKeys = queryResponse.map( + (acc) => acc.account.vestingAccount + ); + const relatedVestingAccounts = await Promise.all( + vestingAccKeys.map((va) => program.account.vestingAccount.fetch(va)) + ); + + const data = queryResponse.map((empAcc, index) => { + const vestingDetail = relatedVestingAccounts[index]; + return { + pda: empAcc.publicKey.toString(), + beneficiary: empAcc.account.beneficiary.toString(), + startTime: empAcc.account.startTime.toNumber(), + endTime: empAcc.account.endTime.toNumber(), + totalAmount: empAcc.account.totalAmount.toNumber(), + totalWithdrawn: empAcc.account.totalWithdrawn.toNumber(), + cliffTime: empAcc.account.cliffTime.toNumber(), + vestingAccount: empAcc.account.vestingAccount.toString(), + companyName: vestingDetail.companyName, + treasuryTokenAccount: vestingDetail.treasuryTokenAccount.toString(), + token: vestingDetail.mint.toString(), + }; + }); + return data; + }, + }); + + const claimTokens = useMutation({ + mutationKey: ["claim-tokens"], + mutationFn: async ({ + companyName, + vestingAccountPubkey, + employeeAccountPubkey, + mintPubkey, + treasuryTokenAccountPubkey, + }: { + companyName: string; + vestingAccountPubkey: PublicKey; + employeeAccountPubkey: PublicKey; + mintPubkey: PublicKey; + treasuryTokenAccountPubkey: PublicKey; + }) => { + if (!walletKey) { + throw new Error("Wallet or program is not initialized."); + } + + const ata = await getAssociatedTokenAddress( + mintPubkey, + walletKey, + false, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const { blockhash } = await connection.getLatestBlockhash(); + const transaction = new Transaction({ + recentBlockhash: blockhash, + feePayer: walletKey, + }); + + const instruction = await program.methods + .claimTokens(companyName) + .accounts({ + beneficiary: walletKey, + employeeAccount: employeeAccountPubkey, + vestingAccount: vestingAccountPubkey, + mint: mintPubkey, + treasuryTokenAccount: treasuryTokenAccountPubkey, + employeeTokenAccount: ata, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + } as any) + .instruction(); + transaction.add(instruction); + + if (!wallet.signTransaction) { + toast.error( + "The wallet adapter does not support signing transactions." + ); + throw new Error( + "The wallet adapter does not support signing transactions." + ); + } + const signedTransaction = await wallet.signTransaction(transaction); + const signature = await connection.sendRawTransaction( + signedTransaction.serialize(), + { + skipPreflight: false, + } + ); + transactionToast(signature); + router.push("/employeetoken"); + return signature; + }, + onError: (error: Error) => { + toast.error(`Claim tokens failed: ${error.message}`); + }, + }); + + return { employeeAccounts, calculateClaimableTokens, claimTokens }; +} diff --git a/project-8-token-vesting/web/components/employee/employee-feature.tsx b/project-8-token-vesting/web/components/employee/employee-feature.tsx new file mode 100644 index 0000000..4ad7b1b --- /dev/null +++ b/project-8-token-vesting/web/components/employee/employee-feature.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useWallet } from "@solana/wallet-adapter-react"; +import { WalletButton } from "../solana/solana-provider"; +import { EmployeeUi } from "./employee-ui"; + +export default function EmployeeFeature() { + const { publicKey } = useWallet(); + + return publicKey ? ( +
+ +
+ ) : ( +
+
+
+ +
+
+
+ ); +} diff --git a/project-8-token-vesting/web/components/employee/employee-types.ts b/project-8-token-vesting/web/components/employee/employee-types.ts new file mode 100644 index 0000000..0010a02 --- /dev/null +++ b/project-8-token-vesting/web/components/employee/employee-types.ts @@ -0,0 +1,36 @@ +import { PublicKey } from "@solana/web3.js"; + +export interface ITokenAccount { + mintAddress: string; + tokenAccount: string; + tokenAmount: number; +} +export interface IClaimableTokens { + startTime: number; + endTime: number; + cliffTime: number; + totalAmount: number; + totalWithdrawn: number; + currentTime: number; +} +export interface IEmployeeVesting { + pda: string; + beneficiary: string; + token: string; + startTime: number; + endTime: number; + cliffTime: number; + totalAmount: number; + totalWithdrawn: number; + treasuryTokenAccount: string; + vestingAccount: string; + companyName: string; +} + +export interface IClaimTokens { + companyName: string; + vestingAccountPubkey: PublicKey; + employeeAccountPubkey: PublicKey; + mintPubkey: PublicKey; + treasuryTokenAccountPubkey: PublicKey; +} diff --git a/project-8-token-vesting/web/components/employee/employee-ui.tsx b/project-8-token-vesting/web/components/employee/employee-ui.tsx new file mode 100644 index 0000000..60a58ac --- /dev/null +++ b/project-8-token-vesting/web/components/employee/employee-ui.tsx @@ -0,0 +1,341 @@ +"use client"; + +import PerfectScrollbar from "react-perfect-scrollbar"; +import "react-perfect-scrollbar/dist/css/styles.css"; +import { PublicKey } from "@solana/web3.js"; +import { useEmployee } from "./employee-data-access"; +import { IoMdRefresh } from "react-icons/io"; +import { useEffect, useState } from "react"; +import { IClaimTokens, IEmployeeVesting } from "./employee-types"; +import Loader from "../common/common-loader"; +import { useConnection } from "@solana/wallet-adapter-react"; +import { QueryObserverResult, RefetchOptions } from "@tanstack/react-query"; + +export function EmployeeUi({ publicKey }: { publicKey: PublicKey }) { + const { + employeeAccounts: employeeAccountsQuery, + calculateClaimableTokens, + claimTokens, + } = useEmployee(publicKey); + + if (employeeAccountsQuery.isLoading) { + return ( +
+ +
+ ); + } + if ( + !employeeAccountsQuery.isLoading && + (!employeeAccountsQuery.data || employeeAccountsQuery.data.length === 0) + ) { + return ( +
+ + Your haven't added to any token vesting program. + +
+ ); + } + return ( +
+ {employeeAccountsQuery.isLoading ? ( +
+ +
+ ) : employeeAccountsQuery.error ? ( + + Error occured whicle fetching your token vesting accounts. + + ) : employeeAccountsQuery.data ? ( +
+ +
+ ) : ( +
+

No accounts

+ No accounts found. +
+ )} +
+ ); +} + +function EmployeeProgramCard({ + employeeAccounts, + refetch, + claimableTokens, + claimTokens, +}: { + employeeAccounts: IEmployeeVesting[]; + refetch: ( + options?: RefetchOptions | undefined + ) => Promise>; + claimableTokens: ( + startTime: number, + endTime: number, + cliffTime: number, + totalAmount: number, + totalWithdrawn: number, + currentTime: number + ) => number; + claimTokens: ({ + companyName, + vestingAccountPubkey, + employeeAccountPubkey, + mintPubkey, + treasuryTokenAccountPubkey, + }: IClaimTokens) => Promise; +}) { + const { connection } = useConnection(); + const [claimableTokenMap, setClaimableTokenMap] = useState<{ + [key: string]: string; + }>({}); + const [claimTokenLoading, setClaimTokenLoading] = useState<{ + [key: string]: boolean; + }>({}); + const [claimingState, setClaimingState] = useState(false); + useEffect(() => { + if (!claimingState) { + const initialClaimableTokens: { [key: string]: string } = {}; + const initialClaimLoading: { [key: string]: boolean } = {}; + employeeAccounts.forEach(async (employeeAccount) => { + setClaimTokenLoading({ [employeeAccount.pda]: false }); + const currentClaimableTokens = await claimableTokens( + employeeAccount.startTime, + employeeAccount.endTime, + employeeAccount.cliffTime, + employeeAccount.totalAmount, + employeeAccount.totalWithdrawn, + Math.floor(Date.now() / 1000) + ).toFixed(2); + initialClaimableTokens[employeeAccount.pda] = currentClaimableTokens; + initialClaimLoading[employeeAccount.pda] = false; + }); + setClaimableTokenMap(initialClaimableTokens); + setClaimTokenLoading(initialClaimLoading); + } + }, [employeeAccounts, claimableTokens, claimingState]); + + const refreshClaimableTokens = ( + pda: string, + startTime: number, + endTime: number, + cliffTime: number, + totalAmount: number, + totalWithdrawn: number, + currentTime: number + ) => { + const currentClaimableTokens = claimableTokens( + startTime, + endTime, + cliffTime, + totalAmount, + totalWithdrawn, + currentTime + ).toFixed(2); + setClaimableTokenMap((prev) => ({ + ...prev, + [pda]: currentClaimableTokens, + })); + }; + + return ( +
+
+
+

+ Employees Vesting Account List +

+
+ {employeeAccounts.length > 0 ? ( +
+ +
+ {employeeAccounts.map((employeeAccount) => ( +
+
+

+ Account +

+

+ {employeeAccount.pda} +

+
+
+

+ Wallet Address +

+

+ {employeeAccount.beneficiary} +

+
+
+

+ Token +

+

+ {employeeAccount.token} +

+
+
+
+

+ Start +

+

+ {new Date( + employeeAccount.startTime * 1000 + ).toLocaleString()} +

+
+
+

+ Cliff +

+

+ {new Date( + employeeAccount.cliffTime * 1000 + ).toLocaleString()} +

+
+
+
+
+

+ End +

+

+ {new Date( + employeeAccount.endTime * 1000 + ).toLocaleString()} +

+
+
+

+ Total +

+

+ {employeeAccount.totalAmount} +

+
+
+
+

+ Already Withdrawn : +

+

+ {employeeAccount.totalWithdrawn} +

+
+
+

+ Claimable Token : +

+

+ {claimableTokenMap[employeeAccount.pda]} + + refreshClaimableTokens( + employeeAccount.pda, + employeeAccount.startTime, + employeeAccount.endTime, + employeeAccount.cliffTime, + employeeAccount.totalAmount, + employeeAccount.totalWithdrawn, + Math.floor(Date.now() / 1000) + ) + } + className="text-md cursor-pointer md:scale-110 hover:scale-125 active:scale-95" + > + + +

+
+ +
+ ))} +
+
+
+ ) : ( +

+ No employee vesting accounts found. +

+ )} +
+
+ ); +} diff --git a/project-8-token-vesting/web/components/mintMeme/meme-data-access.tsx b/project-8-token-vesting/web/components/mintMeme/meme-data-access.tsx new file mode 100644 index 0000000..2575491 --- /dev/null +++ b/project-8-token-vesting/web/components/mintMeme/meme-data-access.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { WalletContextState } from "@solana/wallet-adapter-react"; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import toast from "react-hot-toast"; +import { + createAssociatedTokenAccountInstruction, + createInitializeMintInstruction, + createMintToInstruction, + getAssociatedTokenAddress, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { useMutation } from "@tanstack/react-query"; +import { useTransactionToast } from "../ui/ui-layout"; +import { useRouter } from "next/navigation"; + +export const useCreateMintAndTokenAccount = () => { + const transactionToast = useTransactionToast(); + const createMintAndTokenAccount = useMutation< + { signature: string; mint: PublicKey; associatedTokenAccount: PublicKey }, + Error, + { + connection: Connection; + walletAdapter: WalletContextState; + tokenAmount: number | bigint | string; + } + >({ + mutationKey: ["mintToken", "create"], + mutationFn: async ({ connection, walletAdapter, tokenAmount }) => { + if (!walletAdapter || !walletAdapter.connected) { + throw new Error("Wallet not connected. Please connect your wallet."); + } + + const walletPublicKey = walletAdapter.publicKey; + if (!walletPublicKey) { + throw new Error("Wallet public key not available."); + } + + const mint = Keypair.generate(); + const associatedTokenAccount = await getAssociatedTokenAddress( + mint.publicKey, + walletPublicKey + ); + + const { blockhash } = await connection.getLatestBlockhash(); + const transaction = new Transaction({ + recentBlockhash: blockhash, + feePayer: walletPublicKey, + }); + + const mintRent = await connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + const decimals = 9; + const mintAmount = BigInt(tokenAmount) * BigInt(10 ** decimals); + + transaction.add( + SystemProgram.createAccount({ + fromPubkey: walletPublicKey, + newAccountPubkey: mint.publicKey, + lamports: mintRent, + space: MINT_SIZE, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMintInstruction( + mint.publicKey, + decimals, + walletPublicKey, + walletPublicKey + ), + createAssociatedTokenAccountInstruction( + walletPublicKey, + associatedTokenAccount, + walletPublicKey, + mint.publicKey + ), + createMintToInstruction( + mint.publicKey, + associatedTokenAccount, + walletPublicKey, + mintAmount + ) + ); + + transaction.partialSign(mint); + + if (!walletAdapter.signTransaction) { + toast.error( + "The wallet adapter does not support signing transactions." + ); + throw new Error( + "The wallet adapter does not support signing transactions." + ); + } + const signedTransaction = await walletAdapter.signTransaction( + transaction + ); + const signature = await connection.sendRawTransaction( + signedTransaction.serialize(), + { + skipPreflight: false, + } + ); + return { signature, mint: mint.publicKey, associatedTokenAccount }; + }, + onSuccess: async ({ signature }) => { + transactionToast(signature); + }, + onError: (error) => { + toast.error(`Failed to create mint token: ${error.message}`); + }, + }); + return { createMintAndTokenAccount }; +}; diff --git a/project-8-token-vesting/web/components/mintMeme/meme-feature.tsx b/project-8-token-vesting/web/components/mintMeme/meme-feature.tsx new file mode 100644 index 0000000..fd386f1 --- /dev/null +++ b/project-8-token-vesting/web/components/mintMeme/meme-feature.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useWallet } from "@solana/wallet-adapter-react"; +import { WalletButton } from "../solana/solana-provider"; +import { AppHero, ellipsify } from "../ui/ui-layout"; +import { ExplorerLink } from "../cluster/cluster-ui"; +import { MemeTokenCreate, MemeTokenList } from "./meme-ui"; +import { useCommonProgram } from "../common/common-data-access"; +import { usePathname } from "next/navigation"; + +export default function TokenMemeFeature() { + const { publicKey } = useWallet(); + const { programId } = useCommonProgram(); + const pathName = usePathname(); + + return publicKey ? ( +
+ {pathName === "/createMint" && ( + +

+ +

+ +
+ )} + {pathName === "/mint" && } +
+ ) : ( +
+
+
+ +
+
+
+ ); +} diff --git a/project-8-token-vesting/web/components/mintMeme/meme-types.ts b/project-8-token-vesting/web/components/mintMeme/meme-types.ts new file mode 100644 index 0000000..317d22e --- /dev/null +++ b/project-8-token-vesting/web/components/mintMeme/meme-types.ts @@ -0,0 +1,5 @@ +export interface ITokenAccount { + mintAddress: string; + tokenAccount: string; + tokenAmount: number; +} diff --git a/project-8-token-vesting/web/components/mintMeme/meme-ui.tsx b/project-8-token-vesting/web/components/mintMeme/meme-ui.tsx new file mode 100644 index 0000000..2540cba --- /dev/null +++ b/project-8-token-vesting/web/components/mintMeme/meme-ui.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useCreateMintAndTokenAccount } from "./meme-data-access"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import Link from "next/link"; +import PerfectScrollbar from "react-perfect-scrollbar"; +import "react-perfect-scrollbar/dist/css/styles.css"; +import { useRouter } from "next/navigation"; +import Loader from "../common/common-loader"; +import { useGetTokenAccounts } from "../account/account-data-access"; +import { AccountInfo, ParsedAccountData, PublicKey } from "@solana/web3.js"; +import { ExplorerLink } from "../cluster/cluster-ui"; + +export function MemeTokenCreate({ publicKey }: { publicKey: PublicKey }) { + const wallet = useWallet(); + const { connection } = useConnection(); + const router = useRouter(); + const [tokenAmount, setTokenAmount] = useState(""); + const { createMintAndTokenAccount } = useCreateMintAndTokenAccount(); + const [isSignatureConfirming, setIsSignatureConfirming] = + useState(false); + + return ( +
+ setTokenAmount(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring focus:ring-blue-300" + /> + +
+ ); +} + +export function MemeTokenList({ publicKey }: { publicKey: PublicKey }) { + const query = useGetTokenAccounts({ address: publicKey }); + const tokenAccounts = useMemo(() => { + return query.data; + }, [query.data]); + if (tokenAccounts?.length === 0) { + return ( +
+ + Create New Token Program + + Your connected wallet doesnot have any token accounts. +
+ ); + } + return ( +
+ {query.isPending ? ( +
+ +
+ ) : tokenAccounts && tokenAccounts?.length > 0 ? ( + + ) : ( +
+

No accounts

+ No accounts found. Create one above to get started. +
+ )} +
+ ); +} + +function MemeTokenCard({ + account: tokenAccounts, +}: { + account: { pubkey: PublicKey; account: AccountInfo }[]; +}) { + const wallet = useWallet(); + if (!wallet.publicKey) { + return; + } + return ( +
+ {tokenAccounts.length === 0 && ( + + Create New Token Program + + )} +
+
+

+ Your Token Accounts List +

+ + + Add New Token + +
+ {tokenAccounts.length > 0 ? ( +
+ + + + + + + + + + + + {tokenAccounts.map(({ account, pubkey }) => ( + + + + + + ))} + +
+ Token Account + + Mint Address + + Token Amount +
+ + + + + {account.data.parsed.info.tokenAmount.uiAmount} +
+
+
+ ) : ( +

No token accounts found.

+ )} +
+
+ ); +} diff --git a/project-8-token-vesting/web/components/solana/solana-provider.tsx b/project-8-token-vesting/web/components/solana/solana-provider.tsx index e756401..5c73985 100644 --- a/project-8-token-vesting/web/components/solana/solana-provider.tsx +++ b/project-8-token-vesting/web/components/solana/solana-provider.tsx @@ -1,25 +1,27 @@ -'use client'; +"use client"; -import dynamic from 'next/dynamic'; -import { AnchorProvider } from '@coral-xyz/anchor'; -import { WalletError } from '@solana/wallet-adapter-base'; +import dynamic from "next/dynamic"; +import { AnchorProvider } from "@coral-xyz/anchor"; +import { WalletError } from "@solana/wallet-adapter-base"; import { AnchorWallet, useConnection, useWallet, ConnectionProvider, WalletProvider, -} from '@solana/wallet-adapter-react'; -import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; -import { ReactNode, useCallback, useMemo } from 'react'; -import { useCluster } from '../cluster/cluster-data-access'; +} from "@solana/wallet-adapter-react"; +import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; +import { ReactNode, useCallback, useMemo } from "react"; +import { useCluster } from "../cluster/cluster-data-access"; -require('@solana/wallet-adapter-react-ui/styles.css'); +require("@solana/wallet-adapter-react-ui/styles.css"); export const WalletButton = dynamic( async () => - (await import('@solana/wallet-adapter-react-ui')).WalletMultiButton, - { ssr: false } + (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton, + { + ssr: false, + } ); export function SolanaProvider({ children }: { children: ReactNode }) { @@ -43,6 +45,6 @@ export function useAnchorProvider() { const wallet = useWallet(); return new AnchorProvider(connection, wallet as AnchorWallet, { - commitment: 'confirmed', + commitment: "confirmed", }); } diff --git a/project-8-token-vesting/web/components/ui/navbar-link-list.tsx b/project-8-token-vesting/web/components/ui/navbar-link-list.tsx new file mode 100644 index 0000000..2aba306 --- /dev/null +++ b/project-8-token-vesting/web/components/ui/navbar-link-list.tsx @@ -0,0 +1,119 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import React, { useEffect } from "react"; +import { WalletButton } from "../solana/solana-provider"; +import { ClusterUiSelect } from "../cluster/cluster-ui"; +import { SetStateAction } from "jotai"; + +const NavbarLinkList = ({ + links, + companyLinks, + employeeLinks, + showMenu, + setShowMenu, +}: { + links: { label: string; path: string }[]; + companyLinks: { label: string; path: string }[]; + employeeLinks: { label: string; path: string }[]; + showMenu: boolean; + setShowMenu: React.Dispatch>; +}) => { + const pathname = usePathname(); + useEffect(() => { + const handleResize = () => { + if (window.innerWidth >= 724) { + setShowMenu(false); + } + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + return ( +
    + {links.map(({ label, path }) => ( +
  • setShowMenu(false)} className=""> + + {label} + +
  • + ))} +
  • e.stopPropagation()}> +
    +
    Company
    +
      + {companyLinks.map(({ label, path }) => ( +
    • + setShowMenu(false)} + href={path} + className={`btn btn-sm ${ + pathname.startsWith(path) ? "btn-primary" : "btn-ghost" + }`} + > + {label} + +
    • + ))} +
    +
    +
  • +
  • e.stopPropagation()}> +
    +
    Employee
    +
      + {employeeLinks.map(({ label, path }) => ( +
    • + setShowMenu(false)} + href={path} + className={`btn btn-sm ${ + pathname.startsWith(path) ? "btn-primary" : "btn-ghost" + }`} + > + {label} + +
    • + ))} +
    +
    +
  • +
  • +
  • +
      +
    • e.stopPropagation()}> + +
    • +
    • { + e.stopPropagation(); + }} + > + +
    • +
    +
  • +
+ ); +}; + +export default NavbarLinkList; diff --git a/project-8-token-vesting/web/components/ui/ui-layout.tsx b/project-8-token-vesting/web/components/ui/ui-layout.tsx index e869268..6e84e6b 100644 --- a/project-8-token-vesting/web/components/ui/ui-layout.tsx +++ b/project-8-token-vesting/web/components/ui/ui-layout.tsx @@ -1,52 +1,48 @@ -'use client'; +"use client"; -import { WalletButton } from '../solana/solana-provider'; -import * as React from 'react'; -import { ReactNode, Suspense, useEffect, useRef } from 'react'; +import Link from "next/link"; +import * as React from "react"; +import { ReactNode, Suspense, useEffect, useRef } from "react"; +import toast, { Toaster } from "react-hot-toast"; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; - -import { AccountChecker } from '../account/account-ui'; -import { - ClusterChecker, - ClusterUiSelect, - ExplorerLink, -} from '../cluster/cluster-ui'; -import toast, { Toaster } from 'react-hot-toast'; +import { AccountChecker } from "../account/account-ui"; +import { ClusterChecker, ExplorerLink } from "../cluster/cluster-ui"; +import { TiThMenu } from "react-icons/ti"; +import NavbarLinkList from "./navbar-link-list"; export function UiLayout({ children, links, + companyLinks, + employeeLinks, }: { children: ReactNode; links: { label: string; path: string }[]; + companyLinks: { label: string; path: string }[]; + employeeLinks: { label: string; path: string }[]; }) { - const pathname = usePathname(); - + const [showMenu, setShowMenu] = React.useState(false); return ( -
+
-
+
- Logo + Logo -
    - {links.map(({ label, path }) => ( -
  • - - {label} - -
  • - ))} -
-
-
- - +
setShowMenu((prev) => !prev)} + className="md:hidden" + > + +
+ +
@@ -64,17 +60,17 @@ export function UiLayout({
-