ソースを参照

feat: SPL Token fundraiser program example (#82)

* Initial Commit

* Create readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Update readme.MD

* Removed mint account from contribution. Added require check to checker

* minor changes

* added close constraint

* Update readme.MD

* Major changes performed

* Update readme.MD

* Reworked according to review

* Update fundraiser.ts

* Ran biome checks

* minor changes

* Added pnpm lock file

* updated to anchor 0.30.1

* Added nft operations example

* fixed .ts files

* minor changes

* Update readme.MD
ASCorreia 1 年間 前
コミット
82e5f16d74
33 ファイル変更3951 行追加0 行削除
  1. 21 0
      tokens/nft-operations/anchor/Anchor.toml
  2. 14 0
      tokens/nft-operations/anchor/Cargo.toml
  3. 24 0
      tokens/nft-operations/anchor/package.json
  4. 21 0
      tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml
  5. 2 0
      tokens/nft-operations/anchor/programs/mint-nft/Xargo.toml
  6. 156 0
      tokens/nft-operations/anchor/programs/mint-nft/src/contexts/create_collection.rs
  7. 156 0
      tokens/nft-operations/anchor/programs/mint-nft/src/contexts/mint_nft.rs
  8. 7 0
      tokens/nft-operations/anchor/programs/mint-nft/src/contexts/mod.rs
  9. 75 0
      tokens/nft-operations/anchor/programs/mint-nft/src/contexts/verify_collection.rs
  10. 24 0
      tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs
  11. 525 0
      tokens/nft-operations/anchor/readme.MD
  12. 138 0
      tokens/nft-operations/anchor/tests/mint-nft.ts
  13. 10 0
      tokens/nft-operations/anchor/tsconfig.json
  14. 21 0
      tokens/token-fundraiser/anchor/Anchor.toml
  15. 14 0
      tokens/token-fundraiser/anchor/Cargo.toml
  16. 20 0
      tokens/token-fundraiser/anchor/package.json
  17. 1548 0
      tokens/token-fundraiser/anchor/pnpm-lock.yaml
  18. 21 0
      tokens/token-fundraiser/anchor/programs/fundraiser/Cargo.toml
  19. 2 0
      tokens/token-fundraiser/anchor/programs/fundraiser/Xargo.toml
  20. 5 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/constants.rs
  21. 21 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/error.rs
  22. 83 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs
  23. 109 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs
  24. 62 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize.rs
  25. 9 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs
  26. 99 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs
  27. 45 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/lib.rs
  28. 7 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs
  29. 13 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs
  30. 5 0
      tokens/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs
  31. 474 0
      tokens/token-fundraiser/anchor/readme.MD
  32. 210 0
      tokens/token-fundraiser/anchor/tests/fundraiser.ts
  33. 10 0
      tokens/token-fundraiser/anchor/tsconfig.json

+ 21 - 0
tokens/nft-operations/anchor/Anchor.toml

@@ -0,0 +1,21 @@
+[toolchain]
+
+[features]
+seeds = false
+skip-lint = false
+
+[programs.localnet]
+mint_nft = "3EMcczaGi9ivdLxvvFwRbGYeEUEHpGwabXegARw4jLxa"
+
+[programs.devnet]
+mint_nft = "3EMcczaGi9ivdLxvvFwRbGYeEUEHpGwabXegARw4jLxa"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "devnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 14 - 0
tokens/nft-operations/anchor/Cargo.toml

@@ -0,0 +1,14 @@
+[workspace]
+members = [
+    "programs/*"
+]
+resolver = "2"
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1

+ 24 - 0
tokens/nft-operations/anchor/package.json

@@ -0,0 +1,24 @@
+{
+  "scripts": {
+    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.30.1",
+    "@metaplex-foundation/mpl-token-metadata": "^3.1.2",
+    "@metaplex-foundation/umi": "^0.9.0",
+    "@solana/spl-token": "^0.4.6",
+    "axios": "^1.6.7",
+    "node-fetch": "^3.3.2"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.0",
+    "@types/chai": "^4.3.0",
+    "@types/mocha": "^9.0.0",
+    "chai": "^4.3.4",
+    "mocha": "^9.0.3",
+    "prettier": "^2.6.2",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^4.3.5"
+  }
+}

+ 21 - 0
tokens/nft-operations/anchor/programs/mint-nft/Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "mint-nft"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "mint_nft"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
+
+[dependencies]
+anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
+anchor-spl = { version = "0.30.1", features = ["metadata"] }

+ 2 - 0
tokens/nft-operations/anchor/programs/mint-nft/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 156 - 0
tokens/nft-operations/anchor/programs/mint-nft/src/contexts/create_collection.rs

@@ -0,0 +1,156 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken, 
+    metadata::Metadata, 
+    token::{
+        mint_to, 
+        Mint, 
+        MintTo, 
+        Token, 
+        TokenAccount,
+    }
+};
+use anchor_spl::metadata::mpl_token_metadata::{
+    instructions::{
+        CreateMasterEditionV3Cpi, 
+        CreateMasterEditionV3CpiAccounts, 
+        CreateMasterEditionV3InstructionArgs, 
+        CreateMetadataAccountV3Cpi, 
+        CreateMetadataAccountV3CpiAccounts, 
+        CreateMetadataAccountV3InstructionArgs
+    }, 
+    types::{
+        CollectionDetails, 
+        Creator, 
+        DataV2
+    }
+};
+
+#[derive(Accounts)]
+pub struct CreateCollection<'info> {
+    #[account(mut)]
+    user: Signer<'info>,
+    #[account(
+        init,
+        payer = user,
+        mint::decimals = 0,
+        mint::authority = mint_authority,
+        mint::freeze_authority = mint_authority,
+    )]
+    mint: Account<'info, Mint>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    metadata: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    master_edition: UncheckedAccount<'info>,
+    #[account(
+        init,
+        payer = user,
+        associated_token::mint = mint,
+        associated_token::authority = user
+    )]
+    destination: Account<'info, TokenAccount>,
+    system_program: Program<'info, System>,
+    token_program: Program<'info, Token>,
+    associated_token_program: Program<'info, AssociatedToken>,
+    token_metadata_program: Program<'info, Metadata>,
+}
+
+impl<'info> CreateCollection<'info> {
+    pub fn create_collection(&mut self, bumps: &CreateCollectionBumps) -> Result<()> {
+
+        let metadata = &self.metadata.to_account_info();
+        let master_edition = &self.master_edition.to_account_info();
+        let mint = &self.mint.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let payer = &self.user.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let spl_token_program = &self.token_program.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let cpi_program = self.token_program.to_account_info();
+        let cpi_accounts = MintTo {
+            mint: self.mint.to_account_info(),
+            to: self.destination.to_account_info(),
+            authority: self.mint_authority.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
+        mint_to(cpi_ctx, 1)?;
+        msg!("Collection NFT minted!");
+
+        let creator = vec![
+            Creator {
+                address: self.mint_authority.key().clone(),
+                verified: true,
+                share: 100,
+            },
+        ];
+        
+        let metadata_account = CreateMetadataAccountV3Cpi::new(
+            spl_metadata_program, 
+            CreateMetadataAccountV3CpiAccounts {
+                metadata,
+                mint,
+                mint_authority: authority,
+                payer,
+                update_authority: (authority, true),
+                system_program,
+                rent: None,
+            },
+            CreateMetadataAccountV3InstructionArgs {
+                data: DataV2 {
+                    name: "DummyCollection".to_owned(),
+                    symbol: "DC".to_owned(),
+                    uri: "".to_owned(),
+                    seller_fee_basis_points: 0,
+                    creators: Some(creator),
+                    collection: None,
+                    uses: None,
+                },
+                is_mutable: true,
+                collection_details: Some(
+                    CollectionDetails::V1 { 
+                        size: 0 
+                    }
+                )
+            }
+        );
+        metadata_account.invoke_signed(signer_seeds)?;
+        msg!("Metadata Account created!");
+
+        let master_edition_account = CreateMasterEditionV3Cpi::new(
+            spl_metadata_program,
+            CreateMasterEditionV3CpiAccounts {
+                edition: master_edition,
+                update_authority: authority,
+                mint_authority: authority,
+                mint,
+                payer,
+                metadata,
+                token_program: spl_token_program,
+                system_program,
+                rent: None,
+            },
+            CreateMasterEditionV3InstructionArgs {
+                max_supply: Some(0),
+            }
+        );
+        master_edition_account.invoke_signed(signer_seeds)?;
+        msg!("Master Edition Account created");
+        
+        Ok(())
+    }
+}

+ 156 - 0
tokens/nft-operations/anchor/programs/mint-nft/src/contexts/mint_nft.rs

@@ -0,0 +1,156 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken, 
+    metadata::Metadata, 
+    token::{
+        mint_to,
+        Mint, 
+        MintTo, 
+        Token, 
+        TokenAccount
+    }
+};
+use anchor_spl::metadata::mpl_token_metadata::{
+    instructions::{
+        CreateMasterEditionV3Cpi, 
+        CreateMasterEditionV3CpiAccounts, 
+        CreateMasterEditionV3InstructionArgs, 
+        CreateMetadataAccountV3Cpi, 
+        CreateMetadataAccountV3CpiAccounts, 
+        CreateMetadataAccountV3InstructionArgs,
+    }, 
+    types::{
+        Collection, 
+        Creator, 
+        DataV2,
+    }
+};
+
+#[derive(Accounts)]
+pub struct MintNFT<'info> {
+    #[account(mut)]
+    pub owner: Signer<'info>,
+    #[account(
+        init,
+        payer = owner,
+        mint::decimals = 0,
+        mint::authority = mint_authority,
+        mint::freeze_authority = mint_authority,
+    )]
+    pub mint: Account<'info, Mint>,
+    #[account(
+        init,
+        payer = owner,
+        associated_token::mint = mint,
+        associated_token::authority = owner
+    )]
+    pub destination: Account<'info, TokenAccount>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    pub metadata: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    pub master_edition: UncheckedAccount<'info>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This is account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    #[account(mut)]
+    pub collection_mint: Account<'info, Mint>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub token_metadata_program: Program<'info, Metadata>,
+}
+
+impl<'info> MintNFT<'info> {
+    pub fn mint_nft(&mut self, bumps: &MintNFTBumps) -> Result<()> {
+
+        let metadata = &self.metadata.to_account_info();
+        let master_edition = &self.master_edition.to_account_info();
+        let mint = &self.mint.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let payer = &self.owner.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let spl_token_program = &self.token_program.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let cpi_program = self.token_program.to_account_info();
+        let cpi_accounts = MintTo {
+            mint: self.mint.to_account_info(),
+            to: self.destination.to_account_info(),
+            authority: self.mint_authority.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
+        mint_to(cpi_ctx, 1)?;
+        msg!("Collection NFT minted!");
+
+        let creator = vec![
+            Creator {
+                address: self.mint_authority.key(),
+                verified: true,
+                share: 100,
+            },
+        ];
+
+        let metadata_account = CreateMetadataAccountV3Cpi::new(
+            spl_metadata_program,
+            CreateMetadataAccountV3CpiAccounts {
+                metadata,
+                mint,
+                mint_authority: authority,
+                payer,
+                update_authority: (authority, true),
+                system_program,
+                rent: None,
+            }, 
+            CreateMetadataAccountV3InstructionArgs {
+                data: DataV2 {
+                    name: "Mint Test".to_string(),
+                    symbol: "YAY".to_string(),
+                    uri: "".to_string(),
+                    seller_fee_basis_points: 0,
+                    creators: Some(creator),
+                    collection: Some(Collection {
+                        verified: false,
+                        key: self.collection_mint.key(),
+                    }),
+                    uses: None
+                },
+                is_mutable: true,
+                collection_details: None,
+            }
+        );
+        metadata_account.invoke_signed(signer_seeds)?;
+
+        let master_edition_account = CreateMasterEditionV3Cpi::new(
+            spl_metadata_program,
+            CreateMasterEditionV3CpiAccounts {
+                edition: master_edition,
+                update_authority: authority,
+                mint_authority: authority,
+                mint,
+                payer,
+                metadata,
+                token_program: spl_token_program,
+                system_program,
+                rent: None,
+            },
+            CreateMasterEditionV3InstructionArgs {
+                max_supply: Some(0),
+            }
+        );
+        master_edition_account.invoke_signed(signer_seeds)?;
+
+        Ok(())
+        
+    }
+}

+ 7 - 0
tokens/nft-operations/anchor/programs/mint-nft/src/contexts/mod.rs

@@ -0,0 +1,7 @@
+pub mod mint_nft;
+pub mod create_collection;
+pub mod verify_collection;
+
+pub use mint_nft::*;
+pub use create_collection::*;
+pub use verify_collection::*;

+ 75 - 0
tokens/nft-operations/anchor/programs/mint-nft/src/contexts/verify_collection.rs

@@ -0,0 +1,75 @@
+use anchor_lang::prelude::*;
+
+use anchor_spl::metadata::mpl_token_metadata::instructions::{
+    VerifyCollectionV1Cpi,
+    VerifyCollectionV1CpiAccounts,
+};
+use anchor_spl::metadata::{
+    MasterEditionAccount, 
+    MetadataAccount,
+};
+use anchor_spl::{
+    token::Mint, 
+    metadata::Metadata, 
+};
+pub use anchor_lang::solana_program::sysvar::instructions::ID as INSTRUCTIONS_ID;
+
+#[derive(Accounts)]
+pub struct VerifyCollectionMint<'info> {
+    pub authority: Signer<'info>,
+    #[account(mut)]
+    pub metadata: Account<'info, MetadataAccount>,
+    pub mint: Account<'info, Mint>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    pub collection_mint: Account<'info, Mint>,
+    #[account(mut)]
+    pub collection_metadata: Account<'info, MetadataAccount>,
+    pub collection_master_edition: Account<'info, MasterEditionAccount>,
+    pub system_program: Program<'info, System>,
+    #[account(address = INSTRUCTIONS_ID)]
+    /// CHECK: Sysvar instruction account that is being checked with an address constraint
+    pub sysvar_instruction: UncheckedAccount<'info>,
+    pub token_metadata_program: Program<'info, Metadata>,
+}
+
+impl<'info> VerifyCollectionMint<'info> {
+    pub fn verify_collection(&mut self, bumps: &VerifyCollectionMintBumps) -> Result<()> {
+        let metadata = &self.metadata.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let collection_mint = &self.collection_mint.to_account_info();
+        let collection_metadata = &self.collection_metadata.to_account_info();
+        let collection_master_edition = &self.collection_master_edition.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let sysvar_instructions = &self.sysvar_instruction.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let verify_collection = VerifyCollectionV1Cpi::new(
+            spl_metadata_program,
+        VerifyCollectionV1CpiAccounts {
+            authority,
+            delegate_record: None,
+            metadata,
+            collection_mint,
+            collection_metadata: Some(collection_metadata),
+            collection_master_edition: Some(collection_master_edition),
+            system_program,
+            sysvar_instructions,
+        });
+        verify_collection.invoke_signed(signer_seeds)?;
+
+        msg!("Collection Verified!");
+        
+        Ok(())
+    }
+}

+ 24 - 0
tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs

@@ -0,0 +1,24 @@
+use anchor_lang::prelude::*;
+
+declare_id!("3EMcczaGi9ivdLxvvFwRbGYeEUEHpGwabXegARw4jLxa");
+
+pub mod contexts;
+
+pub use contexts::*;
+
+#[program]
+pub mod mint_nft {
+
+    use super::*;
+    pub fn create_collection(ctx: Context<CreateCollection>) -> Result<()> {
+        ctx.accounts.create_collection(&ctx.bumps)
+    }
+    
+    pub fn mint_nft(ctx: Context<MintNFT>) -> Result<()> {
+        ctx.accounts.mint_nft(&ctx.bumps)
+    }
+
+    pub fn verify_collection(ctx: Context<VerifyCollectionMint>) -> Result<()> {
+        ctx.accounts.verify_collection(&ctx.bumps)
+    }
+}

+ 525 - 0
tokens/nft-operations/anchor/readme.MD

@@ -0,0 +1,525 @@
+# NFT Operations
+
+This example demonstrates how to create a NFT collection, how to mint a NFT and how to verify a NFT as part of a collection.
+
+---
+
+## Create a NFT Collection:
+
+The accounts needed to create a NFT Collection are the following:
+
+```rust
+#[derive(Accounts)]
+pub struct CreateCollection<'info> {
+    #[account(mut)]
+    user: Signer<'info>,
+    #[account(
+        init,
+        payer = user,
+        mint::decimals = 0,
+        mint::authority = mint_authority,
+        mint::freeze_authority = mint_authority,
+    )]
+    mint: Account<'info, Mint>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    metadata: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    master_edition: UncheckedAccount<'info>,
+    #[account(
+        init,
+        payer = user,
+        associated_token::mint = mint,
+        associated_token::authority = user
+    )]
+    destination: Account<'info, TokenAccount>,
+    system_program: Program<'info, System>,
+    token_program: Program<'info, Token>,
+    associated_token_program: Program<'info, AssociatedToken>,
+    token_metadata_program: Program<'info, Metadata>,
+}
+```
+
+### Let's break down these accounts:
+
+- user: the account that is creating the collection NFT and the owner of the destination token account
+
+- mint: the collection NFT Mint account. We will be initializing this account with 0 decimals and giving the mint authority and freeze authority to the mint_authority account
+
+- mint_authority: the account with authority to mint tokens from the collection NFT mint account
+
+- metadata: the metadata account of the collection NFT
+
+- master_edition: the master edition account of the collection NFT
+
+- destination: the token account where the collection NFT will minted to. We will be initializing this account and verifying the correct mint and authority
+
+- system_program: Program resposible for the initialization of any new account
+
+- token_program and associated_token_program: We are creating new ATAs and minting tokens
+
+- token_metadata_program: MPL token metadata program that will be used to create the metadata and master edition accounts
+
+To note in here, that both the metadata account and the master_edition account are Unchecked Accounts. That is due to the fact that they are not initialized, and the initialization will be performed by the token_metadata_program when we perform a CPI (cross program invocation) to initialize both accounts.
+
+If we had something like:
+
+```rust
+#[derive(Accounts)]
+pub struct CreateCollection<'info> {
+    #[account(mut)]
+    metadata: Account<'info, MetadataAccount>,
+    #[account(mut)]
+    master_edition: Account<'info, MasterEditionAccount>,
+}
+```
+
+our instruction would fail because it would expect the accounts to be already initialized.
+
+However, if the account was already initialized (you'll see that while we verify collections), you should use the specific account types
+
+### We then implement some functionality for our CreateCollection context:
+
+```rust
+impl<'info> CreateCollection<'info> {
+    pub fn create_collection(&mut self, bumps: &CreateCollectionBumps) -> Result<()> {
+
+        let metadata = &self.metadata.to_account_info();
+        let master_edition = &self.master_edition.to_account_info();
+        let mint = &self.mint.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let payer = &self.user.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let spl_token_program = &self.token_program.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let cpi_program = self.token_program.to_account_info();
+        let cpi_accounts = MintTo {
+            mint: self.mint.to_account_info(),
+            to: self.destination.to_account_info(),
+            authority: self.mint_authority.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
+        mint_to(cpi_ctx, 1)?;
+        msg!("Collection NFT minted!");
+
+        let creator = vec![
+            Creator {
+                address: self.mint_authority.key().clone(),
+                verified: true,
+                share: 100,
+            },
+        ];
+        
+        let metadata_account = CreateMetadataAccountV3Cpi::new(
+            spl_metadata_program, 
+            CreateMetadataAccountV3CpiAccounts {
+                metadata,
+                mint,
+                mint_authority: authority,
+                payer,
+                update_authority: (authority, true),
+                system_program,
+                rent: None,
+            },
+            CreateMetadataAccountV3InstructionArgs {
+                data: DataV2 {
+                    name: "DummyCollection".to_owned(),
+                    symbol: "DC".to_owned(),
+                    uri: "".to_owned(),
+                    seller_fee_basis_points: 0,
+                    creators: Some(creator),
+                    collection: None,
+                    uses: None,
+                },
+                is_mutable: true,
+                collection_details: Some(
+                    CollectionDetails::V1 { 
+                        size: 0 
+                    }
+                )
+            }
+        );
+        metadata_account.invoke_signed(signer_seeds)?;
+        msg!("Metadata Account created!");
+
+        let master_edition_account = CreateMasterEditionV3Cpi::new(
+            spl_metadata_program,
+            CreateMasterEditionV3CpiAccounts {
+                edition: master_edition,
+                update_authority: authority,
+                mint_authority: authority,
+                mint,
+                payer,
+                metadata,
+                token_program: spl_token_program,
+                system_program,
+                rent: None,
+            },
+            CreateMasterEditionV3InstructionArgs {
+                max_supply: Some(0),
+            }
+        );
+        master_edition_account.invoke_signed(signer_seeds)?;
+        msg!("Master Edition Account created");
+        
+        Ok(())
+    }
+}
+```
+
+The create collection method consists of 3 steps:
+
+- Mint one token to the destination token account by performing a CPI to the Token Program
+
+- Create a metadata account for the mint account to store standardized data that can be understood by apps and marketplaces. This is achieved by performing a CPI to the Token Metadata Program. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA
+
+- Create a master edition account for the mint account by performing a CPI to the Token Metadata Program. That will ensure that the special characteristics on Non-Fungible Tokens are met. It will also transfer both the mint authority and the freeze authority to the Master Edition PDA. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA
+
+
+More information on Token Metadata can be found at https://developers.metaplex.com/token-metadata
+
+---
+
+## Mint a NFT:
+
+The accounts needed to create a NFT Collection are the following:
+
+```rust
+#[derive(Accounts)]
+pub struct MintNFT<'info> {
+    #[account(mut)]
+    pub owner: Signer<'info>,
+    #[account(
+        init,
+        payer = owner,
+        mint::decimals = 0,
+        mint::authority = mint_authority,
+        mint::freeze_authority = mint_authority,
+    )]
+    pub mint: Account<'info, Mint>,
+    #[account(
+        init,
+        payer = owner,
+        associated_token::mint = mint,
+        associated_token::authority = owner
+    )]
+    pub destination: Account<'info, TokenAccount>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    pub metadata: UncheckedAccount<'info>,
+    #[account(mut)]
+    /// CHECK: This account will be initialized by the metaplex program
+    pub master_edition: UncheckedAccount<'info>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This is account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    #[account(mut)]
+    pub collection_mint: Account<'info, Mint>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub token_metadata_program: Program<'info, Metadata>,
+}
+```
+
+### Let's break down these accounts:
+
+- owner: the account that is creating the NFT and the owner of the destination token account
+
+- mint: the collection NFT Mint account. We will be initializing this account with 0 decimals and giving the mint authority and freeze authority to the mint_authority account
+
+- destination: the token account where the collection NFT will minted to. We will be initializing this account and verifying the correct mint and authority
+
+- metadata: the metadata account of the collection NFT
+
+- master_edition: the master edition account of the collection NFT
+
+- mint_authority: the account with authority to mint tokens from the collection NFT mint account
+
+- collection_mint: the collection account that the NFT that we are minting should be part of
+
+- system_program: Program resposible for the initialization of any new account
+
+- token_program and associated_token_program: We are creating new ATAs and minting tokens
+
+- token_metadata_program: MPL token metadata program that will be used to create the metadata and master edition accounts
+
+If you take a closer look, you will see that the accounts (apart from "collection_mint") are the same.
+This is due to the fact that the a collection is basically just a regular NFT but, the "collection_details" field will be set with a CollectionDetails struct and the "collection" field under "data" set to None.
+
+On the other hand, a NFT will have "collection_details" field set to None and with a CollectionDetails and the "collection" field under "data" set to a Collection struct, containing the key of the collection it belongs to and a verified boolean (set to False, it will be automatically set to True once the NFT gets verified as part of the collection)
+
+This is actually where the "collection" account comes from. This account is used to set the the address of the Collection struct when we are creating the NFT metadata account
+
+### We then implement some functionality for our MintNFT context:
+
+```rust
+impl<'info> MintNFT<'info> {
+    pub fn mint_nft(&mut self, bumps: &MintNFTBumps) -> Result<()> {
+
+        let metadata = &self.metadata.to_account_info();
+        let master_edition = &self.master_edition.to_account_info();
+        let mint = &self.mint.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let payer = &self.owner.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let spl_token_program = &self.token_program.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let cpi_program = self.token_program.to_account_info();
+        let cpi_accounts = MintTo {
+            mint: self.mint.to_account_info(),
+            to: self.destination.to_account_info(),
+            authority: self.mint_authority.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
+        mint_to(cpi_ctx, 1)?;
+        msg!("Collection NFT minted!");
+
+        let creator = vec![
+            Creator {
+                address: self.mint_authority.key(),
+                verified: true,
+                share: 100,
+            },
+        ];
+
+        let metadata_account = CreateMetadataAccountV3Cpi::new(
+            spl_metadata_program,
+            CreateMetadataAccountV3CpiAccounts {
+                metadata,
+                mint,
+                mint_authority: authority,
+                payer,
+                update_authority: (authority, true),
+                system_program,
+                rent: None,
+            }, 
+            CreateMetadataAccountV3InstructionArgs {
+                data: DataV2 {
+                    name: "Mint Test".to_string(),
+                    symbol: "YAY".to_string(),
+                    uri: "".to_string(),
+                    seller_fee_basis_points: 0,
+                    creators: Some(creator),
+                    collection: Some(Collection {
+                        verified: false,
+                        key: self.collection_mint.key(),
+                    }),
+                    uses: None
+                },
+                is_mutable: true,
+                collection_details: None,
+            }
+        );
+        metadata_account.invoke_signed(signer_seeds)?;
+
+        let master_edition_account = CreateMasterEditionV3Cpi::new(
+            spl_metadata_program,
+            CreateMasterEditionV3CpiAccounts {
+                edition: master_edition,
+                update_authority: authority,
+                mint_authority: authority,
+                mint,
+                payer,
+                metadata,
+                token_program: spl_token_program,
+                system_program,
+                rent: None,
+            },
+            CreateMasterEditionV3InstructionArgs {
+                max_supply: Some(0),
+            }
+        );
+        master_edition_account.invoke_signed(signer_seeds)?;
+
+        Ok(())
+        
+    }
+}
+```
+
+Since a collection NFT is just a regular NFT with "special" metadata, again you can see that the same is happening as when created the Collection NFT.
+
+- Mint one token to the destination token account by performing a CPI to the Token Program
+
+- Create a metadata account for the mint account to store standardized data that can be understood by apps and marketplaces. This is achieved by performing a CPI to the Token Metadata Program. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA
+
+- Create a master edition account for the mint account by performing a CPI to the Token Metadata Program. That will ensure that the special characteristics on Non-Fungible Tokens are met. It will also transfer both the mint authority and the freeze authority to the Master Edition PDA. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA
+
+
+The difference is in the data of our metadata account.
+
+for our collection NFT, we have
+```rust
+CreateMetadataAccountV3InstructionArgs {
+    data: DataV2 {
+        name: "DummyCollection".to_owned(),
+        symbol: "DC".to_owned(),
+        uri: "".to_owned(),
+        seller_fee_basis_points: 0,
+        creators: Some(creator),
+        collection: None,
+        uses: None,
+    },
+    is_mutable: true,
+    collection_details: Some(
+        CollectionDetails::V1 { 
+            size: 0 
+        }
+    )
+}
+```
+where we set the "collection_details" field
+
+
+for our "regular" NFT we have
+```rust
+CreateMetadataAccountV3InstructionArgs {
+    data: DataV2 {
+        name: "Mint Test".to_string(),
+        symbol: "YAY".to_string(),
+        uri: "".to_string(),
+        seller_fee_basis_points: 0,
+        creators: Some(creator),
+        collection: Some(Collection {
+            verified: false,
+            key: self.collection_mint.key(),
+        }),
+        uses: None
+    },
+    is_mutable: true,
+    collection_details: None,
+}
+```
+where we set the "collection" field with the key of the collection account.
+
+Again, we set the "verified" boolean to false, since this NFT has not yet been verified as part of the desired collection
+
+---
+
+## Verify a NFT as part of a collection:
+
+The accounts needed to verify a NFT as part of a collection are the following:
+
+```rust
+#[derive(Accounts)]
+pub struct VerifyCollectionMint<'info> {
+    pub authority: Signer<'info>,
+    #[account(mut)]
+    pub metadata: Account<'info, MetadataAccount>,
+    pub mint: Account<'info, Mint>,
+    #[account(
+        seeds = [b"authority"],
+        bump,
+    )]
+    /// CHECK: This account is not initialized and is being used for signing purposes only
+    pub mint_authority: UncheckedAccount<'info>,
+    pub collection_mint: Account<'info, Mint>,
+    #[account(mut)]
+    pub collection_metadata: Account<'info, MetadataAccount>,
+    pub collection_master_edition: Account<'info, MasterEditionAccount>,
+    pub system_program: Program<'info, System>,
+    #[account(address = INSTRUCTIONS_ID)]
+    /// CHECK: Sysvar instruction account that is being checked with an address constraint
+    pub sysvar_instruction: UncheckedAccount<'info>,
+    pub token_metadata_program: Program<'info, Metadata>,
+}
+```
+
+### Let's break down these accounts:
+
+- authority: signer of the transaction. This can be used to restrict the address that can execute the verify collection method, by adding constraints
+
+- metadata: the metadata account of the NFT that we want to verify
+
+- mint: the NFT that we want to verify
+
+- mint_authority: the mint_authority of the Collection NFT
+
+- collection_mint: the mint account of the Collection NFT
+
+- collection_metadata: the metadata account of the Collection NFT
+
+- collection_master_edition: the master edition account of the Collection NFT
+
+- system_program: program resposible for the initialization of any new account
+
+- sysvar_instruction: the instructions sysvar provides access to the serialized instruction data
+for the currently-running transaction
+
+- token_metadata_program: MPL token metadata program that will be used to verify the NFT as part of the desired collection
+
+Note that the only account that need to be mutable in here, are the NFT and Colelction NFT metadata accounts.
+This is due to the fact that both will be updated. The NFT metadata account will have the "verified" boolean set to true, and the Collection NFT metadata account will have the colelction size incremented
+
+### We then implement some functionality for our VerifyCollectionMint context:
+
+```rust
+impl<'info> VerifyCollectionMint<'info> {
+    pub fn verify_collection(&mut self, bumps: &VerifyCollectionMintBumps) -> Result<()> {
+        let metadata = &self.metadata.to_account_info();
+        let authority = &self.mint_authority.to_account_info();
+        let collection_mint = &self.collection_mint.to_account_info();
+        let collection_metadata = &self.collection_metadata.to_account_info();
+        let collection_master_edition = &self.collection_master_edition.to_account_info();
+        let system_program = &self.system_program.to_account_info();
+        let sysvar_instructions = &self.sysvar_instruction.to_account_info();
+        let spl_metadata_program = &self.token_metadata_program.to_account_info();
+
+        let seeds = &[
+            &b"authority"[..], 
+            &[bumps.mint_authority]
+        ];
+        let signer_seeds = &[&seeds[..]];
+
+        let verify_collection = VerifyCollectionV1Cpi::new(
+            spl_metadata_program,
+            VerifyCollectionV1CpiAccounts {
+                authority,
+                delegate_record: None,
+                metadata,
+                collection_mint,
+                collection_metadata: Some(collection_metadata),
+                collection_master_edition: Some(collection_master_edition),
+                system_program,
+                sysvar_instructions,
+            }
+        );
+        verify_collection.invoke_signed(signer_seeds)?;
+
+        msg!("Collection Verified!");
+        
+        Ok(())
+    }
+}
+```
+
+In this "verify_collection" method, we simply create a CPI to the to the Token Metadata Program with the appropriate accounts to verify the NFT as part of a collection. Since the authority of the Collection NFT will sign that CPI, the NFT will be verified as part of the collection.
+
+---
+
+With this examples, you will be able to adjust / adapt it to your needs and create Collections, Mint NFTs, and verify NFTs as part of collections

+ 138 - 0
tokens/nft-operations/anchor/tests/mint-nft.ts

@@ -0,0 +1,138 @@
+import * as anchor from '@coral-xyz/anchor';
+import type { Program } from '@coral-xyz/anchor';
+import type NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
+import { ASSOCIATED_PROGRAM_ID } from '@coral-xyz/anchor/dist/cjs/utils/token';
+import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token';
+import { Keypair, SystemProgram } from '@solana/web3.js';
+import type { MintNft } from '../target/types/mint_nft';
+
+describe('mint-nft', () => {
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  const wallet = provider.wallet as NodeWallet;
+
+  const program = anchor.workspace.MintNft as Program<MintNft>;
+
+  const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
+
+  const mintAuthority = anchor.web3.PublicKey.findProgramAddressSync([Buffer.from('authority')], program.programId)[0];
+
+  const collectionKeypair = Keypair.generate();
+  const collectionMint = collectionKeypair.publicKey;
+
+  const mintKeypair = Keypair.generate();
+  const mint = mintKeypair.publicKey;
+
+  const getMetadata = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => {
+    return anchor.web3.PublicKey.findProgramAddressSync(
+      [Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
+      TOKEN_METADATA_PROGRAM_ID,
+    )[0];
+  };
+
+  const getMasterEdition = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => {
+    return anchor.web3.PublicKey.findProgramAddressSync(
+      [Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition')],
+      TOKEN_METADATA_PROGRAM_ID,
+    )[0];
+  };
+
+  it('Create Collection NFT', async () => {
+    console.log('\nCollection Mint Key: ', collectionMint.toBase58());
+
+    const metadata = await getMetadata(collectionMint);
+    console.log('Collection Metadata Account: ', metadata.toBase58());
+
+    const masterEdition = await getMasterEdition(collectionMint);
+    console.log('Master Edition Account: ', masterEdition.toBase58());
+
+    const destination = getAssociatedTokenAddressSync(collectionMint, wallet.publicKey);
+    console.log('Destination ATA = ', destination.toBase58());
+
+    const tx = await program.methods
+      .createCollection()
+      .accountsPartial({
+        user: wallet.publicKey,
+        mint: collectionMint,
+        mintAuthority,
+        metadata,
+        masterEdition,
+        destination,
+        systemProgram: SystemProgram.programId,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
+        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
+      })
+      .signers([collectionKeypair])
+      .rpc({
+        skipPreflight: true,
+      });
+    console.log('\nCollection NFT minted: TxID - ', tx);
+  });
+
+  it('Mint NFT', async () => {
+    console.log('\nMint', mint.toBase58());
+
+    const metadata = await getMetadata(mint);
+    console.log('Metadata', metadata.toBase58());
+
+    const masterEdition = await getMasterEdition(mint);
+    console.log('Master Edition', masterEdition.toBase58());
+
+    const destination = getAssociatedTokenAddressSync(mint, wallet.publicKey);
+    console.log('Destination', destination.toBase58());
+
+    const tx = await program.methods
+      .mintNft()
+      .accountsPartial({
+        owner: wallet.publicKey,
+        destination,
+        metadata,
+        masterEdition,
+        mint,
+        mintAuthority,
+        collectionMint,
+        systemProgram: SystemProgram.programId,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
+        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
+      })
+      .signers([mintKeypair])
+      .rpc({
+        skipPreflight: true,
+      });
+    console.log('\nNFT Minted! Your transaction signature', tx);
+  });
+
+  it('Verify Collection', async () => {
+    const mintMetadata = await getMetadata(mint);
+    console.log('\nMint Metadata', mintMetadata.toBase58());
+
+    const collectionMetadata = await getMetadata(collectionMint);
+    console.log('Collection Metadata', collectionMetadata.toBase58());
+
+    const collectionMasterEdition = await getMasterEdition(collectionMint);
+    console.log('Collection Master Edition', collectionMasterEdition.toBase58());
+
+    const tx = await program.methods
+      .verifyCollection()
+      .accountsPartial({
+        authority: wallet.publicKey,
+        metadata: mintMetadata,
+        mint,
+        mintAuthority,
+        collectionMint,
+        collectionMetadata,
+        collectionMasterEdition,
+        systemProgram: SystemProgram.programId,
+        sysvarInstruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
+        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
+      })
+      .rpc({
+        skipPreflight: true,
+      });
+    console.log('\nCollection Verified! Your transaction signature', tx);
+  });
+});

+ 10 - 0
tokens/nft-operations/anchor/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

+ 21 - 0
tokens/token-fundraiser/anchor/Anchor.toml

@@ -0,0 +1,21 @@
+[toolchain]
+
+[features]
+resolution = true
+skip-lint = false
+
+[programs.localnet]
+fundraiser = "Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC"
+
+[programs.devnet]
+fundraiser = "Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Localnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 14 - 0
tokens/token-fundraiser/anchor/Cargo.toml

@@ -0,0 +1,14 @@
+[workspace]
+members = [
+    "programs/*"
+]
+resolver = "2"
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1

+ 20 - 0
tokens/token-fundraiser/anchor/package.json

@@ -0,0 +1,20 @@
+{
+  "scripts": {
+    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.30.0",
+    "@solana/spl-token": "^0.4.6"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.0",
+    "@types/chai": "^4.3.0",
+    "@types/mocha": "^9.0.0",
+    "chai": "^4.3.4",
+    "mocha": "^9.0.3",
+    "prettier": "^2.6.2",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^4.3.5"
+  }
+}

+ 1548 - 0
tokens/token-fundraiser/anchor/pnpm-lock.yaml

@@ -0,0 +1,1548 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@coral-xyz/anchor':
+        specifier: ^0.30.0
+        version: 0.30.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      '@solana/spl-token':
+        specifier: ^0.4.6
+        version: 0.4.6(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)
+    devDependencies:
+      '@types/bn.js':
+        specifier: ^5.1.0
+        version: 5.1.5
+      '@types/chai':
+        specifier: ^4.3.0
+        version: 4.3.16
+      '@types/mocha':
+        specifier: ^9.0.0
+        version: 9.1.1
+      chai:
+        specifier: ^4.3.4
+        version: 4.4.1
+      mocha:
+        specifier: ^9.0.3
+        version: 9.2.2
+      prettier:
+        specifier: ^2.6.2
+        version: 2.8.8
+      ts-mocha:
+        specifier: ^10.0.0
+        version: 10.0.0(mocha@9.2.2)
+      typescript:
+        specifier: ^4.3.5
+        version: 4.9.5
+
+packages:
+
+  '@babel/runtime@7.24.7':
+    resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
+    engines: {node: '>=6.9.0'}
+
+  '@coral-xyz/anchor-errors@0.30.1':
+    resolution: {integrity: sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==}
+    engines: {node: '>=10'}
+
+  '@coral-xyz/anchor@0.30.1':
+    resolution: {integrity: sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ==}
+    engines: {node: '>=11'}
+
+  '@coral-xyz/borsh@0.30.1':
+    resolution: {integrity: sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@solana/web3.js': ^1.68.0
+
+  '@noble/curves@1.4.0':
+    resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==}
+
+  '@noble/hashes@1.4.0':
+    resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
+    engines: {node: '>= 16'}
+
+  '@solana/buffer-layout-utils@0.2.0':
+    resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==}
+    engines: {node: '>= 10'}
+
+  '@solana/buffer-layout@4.0.1':
+    resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
+    engines: {node: '>=5.10'}
+
+  '@solana/codecs-core@2.0.0-preview.2':
+    resolution: {integrity: sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg==}
+
+  '@solana/codecs-data-structures@2.0.0-preview.2':
+    resolution: {integrity: sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==}
+
+  '@solana/codecs-numbers@2.0.0-preview.2':
+    resolution: {integrity: sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw==}
+
+  '@solana/codecs-strings@2.0.0-preview.2':
+    resolution: {integrity: sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==}
+    peerDependencies:
+      fastestsmallesttextencoderdecoder: ^1.0.22
+
+  '@solana/codecs@2.0.0-preview.2':
+    resolution: {integrity: sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==}
+
+  '@solana/errors@2.0.0-preview.2':
+    resolution: {integrity: sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA==}
+    hasBin: true
+
+  '@solana/options@2.0.0-preview.2':
+    resolution: {integrity: sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w==}
+
+  '@solana/spl-token-group@0.0.4':
+    resolution: {integrity: sha512-7+80nrEMdUKlK37V6kOe024+T7J4nNss0F8LQ9OOPYdWCCfJmsGUzVx2W3oeizZR4IHM6N4yC9v1Xqwc3BTPWw==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.91.6
+
+  '@solana/spl-token-metadata@0.1.4':
+    resolution: {integrity: sha512-N3gZ8DlW6NWDV28+vCCDJoTqaCZiF/jDUnk3o8GRkAFzHObiR60Bs1gXHBa8zCPdvOwiG6Z3dg5pg7+RW6XNsQ==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.91.6
+
+  '@solana/spl-token@0.4.6':
+    resolution: {integrity: sha512-1nCnUqfHVtdguFciVWaY/RKcQz1IF4b31jnKgAmjU9QVN1q7dRUkTEWJZgTYIEtsULjVnC9jRqlhgGN39WbKKA==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.91.6
+
+  '@solana/spl-type-length-value@0.1.0':
+    resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==}
+    engines: {node: '>=16'}
+
+  '@solana/web3.js@1.93.0':
+    resolution: {integrity: sha512-suf4VYwWxERz4tKoPpXCRHFRNst7jmcFUaD65kII+zg9urpy5PeeqgLV6G5eWGzcVzA9tZeXOju1A1Y+0ojEVw==}
+
+  '@swc/helpers@0.5.11':
+    resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
+
+  '@types/bn.js@5.1.5':
+    resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==}
+
+  '@types/chai@4.3.16':
+    resolution: {integrity: sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==}
+
+  '@types/connect@3.4.38':
+    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+  '@types/json5@0.0.29':
+    resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+
+  '@types/mocha@9.1.1':
+    resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==}
+
+  '@types/node@12.20.55':
+    resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+
+  '@types/node@20.14.6':
+    resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==}
+
+  '@types/uuid@8.3.4':
+    resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
+
+  '@types/ws@7.4.7':
+    resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
+
+  '@types/ws@8.5.10':
+    resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
+
+  '@ungap/promise-all-settled@1.1.2':
+    resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
+
+  JSONStream@1.3.5:
+    resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
+    hasBin: true
+
+  agentkeepalive@4.5.0:
+    resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+    engines: {node: '>= 8.0.0'}
+
+  ansi-colors@4.1.1:
+    resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
+    engines: {node: '>=6'}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+    engines: {node: '>=8'}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  arrify@1.0.1:
+    resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
+    engines: {node: '>=0.10.0'}
+
+  assertion-error@1.1.0:
+    resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  base-x@3.0.9:
+    resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==}
+
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  bigint-buffer@1.1.5:
+    resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
+    engines: {node: '>= 10.0.0'}
+
+  bignumber.js@9.1.2:
+    resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
+
+  binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+    engines: {node: '>=8'}
+
+  bindings@1.5.0:
+    resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
+  bn.js@5.2.1:
+    resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==}
+
+  borsh@0.7.0:
+    resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
+
+  brace-expansion@1.1.11:
+    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  browser-stdout@1.3.1:
+    resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
+
+  bs58@4.0.1:
+    resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
+
+  buffer-from@1.1.2:
+    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+  buffer-layout@1.2.2:
+    resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==}
+    engines: {node: '>=4.5'}
+
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+  bufferutil@4.0.8:
+    resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
+    engines: {node: '>=6.14.2'}
+
+  camelcase@6.3.0:
+    resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+    engines: {node: '>=10'}
+
+  chai@4.4.1:
+    resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
+    engines: {node: '>=4'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  chalk@5.3.0:
+    resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+  check-error@1.0.3:
+    resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
+
+  chokidar@3.5.3:
+    resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+    engines: {node: '>= 8.10.0'}
+
+  cliui@7.0.4:
+    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  commander@12.1.0:
+    resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+    engines: {node: '>=18'}
+
+  commander@2.20.3:
+    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  cross-fetch@3.1.8:
+    resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
+
+  crypto-hash@1.3.0:
+    resolution: {integrity: sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==}
+    engines: {node: '>=8'}
+
+  debug@4.3.3:
+    resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  decamelize@4.0.0:
+    resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
+    engines: {node: '>=10'}
+
+  deep-eql@4.1.4:
+    resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
+    engines: {node: '>=6'}
+
+  delay@5.0.0:
+    resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
+    engines: {node: '>=10'}
+
+  diff@3.5.0:
+    resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==}
+    engines: {node: '>=0.3.1'}
+
+  diff@5.0.0:
+    resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
+    engines: {node: '>=0.3.1'}
+
+  dot-case@3.0.4:
+    resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
+
+  emoji-regex@8.0.0:
+    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+  es6-promise@4.2.8:
+    resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
+  es6-promisify@5.0.0:
+    resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
+
+  escalade@3.1.2:
+    resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+    engines: {node: '>=6'}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eventemitter3@4.0.7:
+    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
+  eventemitter3@5.0.1:
+    resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
+  eyes@0.1.8:
+    resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
+    engines: {node: '> 0.1.90'}
+
+  fast-stable-stringify@1.0.0:
+    resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
+
+  fastestsmallesttextencoderdecoder@1.0.22:
+    resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
+
+  file-uri-to-path@1.0.0:
+    resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  flat@5.0.2:
+    resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
+    hasBin: true
+
+  fs.realpath@1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  get-caller-file@2.0.5:
+    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+    engines: {node: 6.* || 8.* || >= 10.*}
+
+  get-func-name@2.0.2:
+    resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  glob@7.2.0:
+    resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
+    deprecated: Glob versions prior to v9 are no longer supported
+
+  growl@1.10.5:
+    resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==}
+    engines: {node: '>=4.x'}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  he@1.2.0:
+    resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+    hasBin: true
+
+  humanize-ms@1.2.1:
+    resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+  inflight@1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-fullwidth-code-point@3.0.0:
+    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+    engines: {node: '>=8'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  is-plain-obj@2.1.0:
+    resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+    engines: {node: '>=8'}
+
+  is-unicode-supported@0.1.0:
+    resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+    engines: {node: '>=10'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  isomorphic-ws@4.0.1:
+    resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==}
+    peerDependencies:
+      ws: '*'
+
+  jayson@4.1.0:
+    resolution: {integrity: sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  js-yaml@4.1.0:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  json-stringify-safe@5.0.1:
+    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
+  json5@1.0.2:
+    resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
+    hasBin: true
+
+  jsonparse@1.3.1:
+    resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
+    engines: {'0': node >= 0.2.0}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  log-symbols@4.1.0:
+    resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+    engines: {node: '>=10'}
+
+  loupe@2.3.7:
+    resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+
+  lower-case@2.0.2:
+    resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
+
+  make-error@1.3.6:
+    resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
+  minimatch@3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  minimatch@4.2.1:
+    resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==}
+    engines: {node: '>=10'}
+
+  minimist@1.2.8:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+  mkdirp@0.5.6:
+    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+    hasBin: true
+
+  mocha@9.2.2:
+    resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==}
+    engines: {node: '>= 12.0.0'}
+    hasBin: true
+
+  ms@2.1.2:
+    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  nanoid@3.3.1:
+    resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  no-case@3.0.4:
+    resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
+
+  node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+
+  node-gyp-build@4.8.1:
+    resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
+    hasBin: true
+
+  normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+
+  once@1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  pako@2.1.0:
+    resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  path-is-absolute@1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+    engines: {node: '>=0.10.0'}
+
+  pathval@1.1.1:
+    resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  prettier@2.8.8:
+    resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
+    engines: {node: '>=10.13.0'}
+    hasBin: true
+
+  randombytes@2.1.0:
+    resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+
+  readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+
+  regenerator-runtime@0.14.1:
+    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+  require-directory@2.1.1:
+    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+    engines: {node: '>=0.10.0'}
+
+  rpc-websockets@9.0.2:
+    resolution: {integrity: sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw==}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+  serialize-javascript@6.0.0:
+    resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
+
+  snake-case@3.0.4:
+    resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
+
+  source-map-support@0.5.21:
+    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+  source-map@0.6.1:
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+    engines: {node: '>=0.10.0'}
+
+  string-width@4.2.3:
+    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+    engines: {node: '>=8'}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+    engines: {node: '>=8'}
+
+  strip-bom@3.0.0:
+    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+    engines: {node: '>=4'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  superstruct@0.15.5:
+    resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==}
+
+  superstruct@1.0.4:
+    resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==}
+    engines: {node: '>=14.0.0'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  supports-color@8.1.1:
+    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+    engines: {node: '>=10'}
+
+  text-encoding-utf-8@1.0.2:
+    resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
+
+  through@2.3.8:
+    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  toml@3.0.0:
+    resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
+
+  tr46@0.0.3:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+  ts-mocha@10.0.0:
+    resolution: {integrity: sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==}
+    engines: {node: '>= 6.X.X'}
+    hasBin: true
+    peerDependencies:
+      mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X
+
+  ts-node@7.0.1:
+    resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==}
+    engines: {node: '>=4.2.0'}
+    hasBin: true
+
+  tsconfig-paths@3.15.0:
+    resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+
+  tslib@2.6.3:
+    resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+
+  type-detect@4.0.8:
+    resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+    engines: {node: '>=4'}
+
+  typescript@4.9.5:
+    resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
+    engines: {node: '>=4.2.0'}
+    hasBin: true
+
+  undici-types@5.26.5:
+    resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+  utf-8-validate@5.0.10:
+    resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
+    engines: {node: '>=6.14.2'}
+
+  uuid@8.3.2:
+    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+    hasBin: true
+
+  webidl-conversions@3.0.1:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+  whatwg-url@5.0.0:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  workerpool@6.2.0:
+    resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==}
+
+  wrap-ansi@7.0.0:
+    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+    engines: {node: '>=10'}
+
+  wrappy@1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+  ws@7.5.10:
+    resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  ws@8.17.1:
+    resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  y18n@5.0.8:
+    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+    engines: {node: '>=10'}
+
+  yargs-parser@20.2.4:
+    resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
+    engines: {node: '>=10'}
+
+  yargs-unparser@2.0.0:
+    resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
+    engines: {node: '>=10'}
+
+  yargs@16.2.0:
+    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+    engines: {node: '>=10'}
+
+  yn@2.0.0:
+    resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==}
+    engines: {node: '>=4'}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@babel/runtime@7.24.7':
+    dependencies:
+      regenerator-runtime: 0.14.1
+
+  '@coral-xyz/anchor-errors@0.30.1': {}
+
+  '@coral-xyz/anchor@0.30.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@coral-xyz/anchor-errors': 0.30.1
+      '@coral-xyz/borsh': 0.30.1(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      '@noble/hashes': 1.4.0
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bn.js: 5.2.1
+      bs58: 4.0.1
+      buffer-layout: 1.2.2
+      camelcase: 6.3.0
+      cross-fetch: 3.1.8
+      crypto-hash: 1.3.0
+      eventemitter3: 4.0.7
+      pako: 2.1.0
+      snake-case: 3.0.4
+      superstruct: 0.15.5
+      toml: 3.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))':
+    dependencies:
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bn.js: 5.2.1
+      buffer-layout: 1.2.2
+
+  '@noble/curves@1.4.0':
+    dependencies:
+      '@noble/hashes': 1.4.0
+
+  '@noble/hashes@1.4.0': {}
+
+  '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bigint-buffer: 1.1.5
+      bignumber.js: 9.1.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  '@solana/buffer-layout@4.0.1':
+    dependencies:
+      buffer: 6.0.3
+
+  '@solana/codecs-core@2.0.0-preview.2':
+    dependencies:
+      '@solana/errors': 2.0.0-preview.2
+
+  '@solana/codecs-data-structures@2.0.0-preview.2':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.2
+      '@solana/codecs-numbers': 2.0.0-preview.2
+      '@solana/errors': 2.0.0-preview.2
+
+  '@solana/codecs-numbers@2.0.0-preview.2':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.2
+      '@solana/errors': 2.0.0-preview.2
+
+  '@solana/codecs-strings@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.2
+      '@solana/codecs-numbers': 2.0.0-preview.2
+      '@solana/errors': 2.0.0-preview.2
+      fastestsmallesttextencoderdecoder: 1.0.22
+
+  '@solana/codecs@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.2
+      '@solana/codecs-data-structures': 2.0.0-preview.2
+      '@solana/codecs-numbers': 2.0.0-preview.2
+      '@solana/codecs-strings': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/options': 2.0.0-preview.2
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/errors@2.0.0-preview.2':
+    dependencies:
+      chalk: 5.3.0
+      commander: 12.1.0
+
+  '@solana/options@2.0.0-preview.2':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.2
+      '@solana/codecs-numbers': 2.0.0-preview.2
+
+  '@solana/spl-token-group@0.0.4(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)':
+    dependencies:
+      '@solana/codecs': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/spl-type-length-value': 0.1.0
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/spl-token-metadata@0.1.4(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)':
+    dependencies:
+      '@solana/codecs': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/spl-type-length-value': 0.1.0
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/spl-token@0.4.6(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      '@solana/spl-token-group': 0.0.4(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/spl-token-metadata': 0.1.4(@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/web3.js': 1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - fastestsmallesttextencoderdecoder
+      - utf-8-validate
+
+  '@solana/spl-type-length-value@0.1.0':
+    dependencies:
+      buffer: 6.0.3
+
+  '@solana/web3.js@1.93.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@babel/runtime': 7.24.7
+      '@noble/curves': 1.4.0
+      '@noble/hashes': 1.4.0
+      '@solana/buffer-layout': 4.0.1
+      agentkeepalive: 4.5.0
+      bigint-buffer: 1.1.5
+      bn.js: 5.2.1
+      borsh: 0.7.0
+      bs58: 4.0.1
+      buffer: 6.0.3
+      fast-stable-stringify: 1.0.0
+      jayson: 4.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      node-fetch: 2.7.0
+      rpc-websockets: 9.0.2
+      superstruct: 1.0.4
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  '@swc/helpers@0.5.11':
+    dependencies:
+      tslib: 2.6.3
+
+  '@types/bn.js@5.1.5':
+    dependencies:
+      '@types/node': 20.14.6
+
+  '@types/chai@4.3.16': {}
+
+  '@types/connect@3.4.38':
+    dependencies:
+      '@types/node': 12.20.55
+
+  '@types/json5@0.0.29':
+    optional: true
+
+  '@types/mocha@9.1.1': {}
+
+  '@types/node@12.20.55': {}
+
+  '@types/node@20.14.6':
+    dependencies:
+      undici-types: 5.26.5
+
+  '@types/uuid@8.3.4': {}
+
+  '@types/ws@7.4.7':
+    dependencies:
+      '@types/node': 12.20.55
+
+  '@types/ws@8.5.10':
+    dependencies:
+      '@types/node': 20.14.6
+
+  '@ungap/promise-all-settled@1.1.2': {}
+
+  JSONStream@1.3.5:
+    dependencies:
+      jsonparse: 1.3.1
+      through: 2.3.8
+
+  agentkeepalive@4.5.0:
+    dependencies:
+      humanize-ms: 1.2.1
+
+  ansi-colors@4.1.1: {}
+
+  ansi-regex@5.0.1: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  anymatch@3.1.3:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+
+  argparse@2.0.1: {}
+
+  arrify@1.0.1: {}
+
+  assertion-error@1.1.0: {}
+
+  balanced-match@1.0.2: {}
+
+  base-x@3.0.9:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  base64-js@1.5.1: {}
+
+  bigint-buffer@1.1.5:
+    dependencies:
+      bindings: 1.5.0
+
+  bignumber.js@9.1.2: {}
+
+  binary-extensions@2.3.0: {}
+
+  bindings@1.5.0:
+    dependencies:
+      file-uri-to-path: 1.0.0
+
+  bn.js@5.2.1: {}
+
+  borsh@0.7.0:
+    dependencies:
+      bn.js: 5.2.1
+      bs58: 4.0.1
+      text-encoding-utf-8: 1.0.2
+
+  brace-expansion@1.1.11:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  browser-stdout@1.3.1: {}
+
+  bs58@4.0.1:
+    dependencies:
+      base-x: 3.0.9
+
+  buffer-from@1.1.2: {}
+
+  buffer-layout@1.2.2: {}
+
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  bufferutil@4.0.8:
+    dependencies:
+      node-gyp-build: 4.8.1
+    optional: true
+
+  camelcase@6.3.0: {}
+
+  chai@4.4.1:
+    dependencies:
+      assertion-error: 1.1.0
+      check-error: 1.0.3
+      deep-eql: 4.1.4
+      get-func-name: 2.0.2
+      loupe: 2.3.7
+      pathval: 1.1.1
+      type-detect: 4.0.8
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  chalk@5.3.0: {}
+
+  check-error@1.0.3:
+    dependencies:
+      get-func-name: 2.0.2
+
+  chokidar@3.5.3:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  cliui@7.0.4:
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 7.0.0
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  commander@12.1.0: {}
+
+  commander@2.20.3: {}
+
+  concat-map@0.0.1: {}
+
+  cross-fetch@3.1.8:
+    dependencies:
+      node-fetch: 2.7.0
+    transitivePeerDependencies:
+      - encoding
+
+  crypto-hash@1.3.0: {}
+
+  debug@4.3.3(supports-color@8.1.1):
+    dependencies:
+      ms: 2.1.2
+    optionalDependencies:
+      supports-color: 8.1.1
+
+  decamelize@4.0.0: {}
+
+  deep-eql@4.1.4:
+    dependencies:
+      type-detect: 4.0.8
+
+  delay@5.0.0: {}
+
+  diff@3.5.0: {}
+
+  diff@5.0.0: {}
+
+  dot-case@3.0.4:
+    dependencies:
+      no-case: 3.0.4
+      tslib: 2.6.3
+
+  emoji-regex@8.0.0: {}
+
+  es6-promise@4.2.8: {}
+
+  es6-promisify@5.0.0:
+    dependencies:
+      es6-promise: 4.2.8
+
+  escalade@3.1.2: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  eventemitter3@4.0.7: {}
+
+  eventemitter3@5.0.1: {}
+
+  eyes@0.1.8: {}
+
+  fast-stable-stringify@1.0.0: {}
+
+  fastestsmallesttextencoderdecoder@1.0.22: {}
+
+  file-uri-to-path@1.0.0: {}
+
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat@5.0.2: {}
+
+  fs.realpath@1.0.0: {}
+
+  fsevents@2.3.3:
+    optional: true
+
+  get-caller-file@2.0.5: {}
+
+  get-func-name@2.0.2: {}
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob@7.2.0:
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+
+  growl@1.10.5: {}
+
+  has-flag@4.0.0: {}
+
+  he@1.2.0: {}
+
+  humanize-ms@1.2.1:
+    dependencies:
+      ms: 2.1.3
+
+  ieee754@1.2.1: {}
+
+  inflight@1.0.6:
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+
+  inherits@2.0.4: {}
+
+  is-binary-path@2.1.0:
+    dependencies:
+      binary-extensions: 2.3.0
+
+  is-extglob@2.1.1: {}
+
+  is-fullwidth-code-point@3.0.0: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
+  is-plain-obj@2.1.0: {}
+
+  is-unicode-supported@0.1.0: {}
+
+  isexe@2.0.0: {}
+
+  isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
+    dependencies:
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+
+  jayson@4.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      JSONStream: 1.3.5
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      json-stringify-safe: 5.0.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  js-yaml@4.1.0:
+    dependencies:
+      argparse: 2.0.1
+
+  json-stringify-safe@5.0.1: {}
+
+  json5@1.0.2:
+    dependencies:
+      minimist: 1.2.8
+    optional: true
+
+  jsonparse@1.3.1: {}
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  log-symbols@4.1.0:
+    dependencies:
+      chalk: 4.1.2
+      is-unicode-supported: 0.1.0
+
+  loupe@2.3.7:
+    dependencies:
+      get-func-name: 2.0.2
+
+  lower-case@2.0.2:
+    dependencies:
+      tslib: 2.6.3
+
+  make-error@1.3.6: {}
+
+  minimatch@3.1.2:
+    dependencies:
+      brace-expansion: 1.1.11
+
+  minimatch@4.2.1:
+    dependencies:
+      brace-expansion: 1.1.11
+
+  minimist@1.2.8: {}
+
+  mkdirp@0.5.6:
+    dependencies:
+      minimist: 1.2.8
+
+  mocha@9.2.2:
+    dependencies:
+      '@ungap/promise-all-settled': 1.1.2
+      ansi-colors: 4.1.1
+      browser-stdout: 1.3.1
+      chokidar: 3.5.3
+      debug: 4.3.3(supports-color@8.1.1)
+      diff: 5.0.0
+      escape-string-regexp: 4.0.0
+      find-up: 5.0.0
+      glob: 7.2.0
+      growl: 1.10.5
+      he: 1.2.0
+      js-yaml: 4.1.0
+      log-symbols: 4.1.0
+      minimatch: 4.2.1
+      ms: 2.1.3
+      nanoid: 3.3.1
+      serialize-javascript: 6.0.0
+      strip-json-comments: 3.1.1
+      supports-color: 8.1.1
+      which: 2.0.2
+      workerpool: 6.2.0
+      yargs: 16.2.0
+      yargs-parser: 20.2.4
+      yargs-unparser: 2.0.0
+
+  ms@2.1.2: {}
+
+  ms@2.1.3: {}
+
+  nanoid@3.3.1: {}
+
+  no-case@3.0.4:
+    dependencies:
+      lower-case: 2.0.2
+      tslib: 2.6.3
+
+  node-fetch@2.7.0:
+    dependencies:
+      whatwg-url: 5.0.0
+
+  node-gyp-build@4.8.1:
+    optional: true
+
+  normalize-path@3.0.0: {}
+
+  once@1.4.0:
+    dependencies:
+      wrappy: 1.0.2
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  pako@2.1.0: {}
+
+  path-exists@4.0.0: {}
+
+  path-is-absolute@1.0.1: {}
+
+  pathval@1.1.1: {}
+
+  picomatch@2.3.1: {}
+
+  prettier@2.8.8: {}
+
+  randombytes@2.1.0:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  readdirp@3.6.0:
+    dependencies:
+      picomatch: 2.3.1
+
+  regenerator-runtime@0.14.1: {}
+
+  require-directory@2.1.1: {}
+
+  rpc-websockets@9.0.2:
+    dependencies:
+      '@swc/helpers': 0.5.11
+      '@types/uuid': 8.3.4
+      '@types/ws': 8.5.10
+      buffer: 6.0.3
+      eventemitter3: 5.0.1
+      uuid: 8.3.2
+      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  safe-buffer@5.2.1: {}
+
+  serialize-javascript@6.0.0:
+    dependencies:
+      randombytes: 2.1.0
+
+  snake-case@3.0.4:
+    dependencies:
+      dot-case: 3.0.4
+      tslib: 2.6.3
+
+  source-map-support@0.5.21:
+    dependencies:
+      buffer-from: 1.1.2
+      source-map: 0.6.1
+
+  source-map@0.6.1: {}
+
+  string-width@4.2.3:
+    dependencies:
+      emoji-regex: 8.0.0
+      is-fullwidth-code-point: 3.0.0
+      strip-ansi: 6.0.1
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-bom@3.0.0:
+    optional: true
+
+  strip-json-comments@3.1.1: {}
+
+  superstruct@0.15.5: {}
+
+  superstruct@1.0.4: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  supports-color@8.1.1:
+    dependencies:
+      has-flag: 4.0.0
+
+  text-encoding-utf-8@1.0.2: {}
+
+  through@2.3.8: {}
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  toml@3.0.0: {}
+
+  tr46@0.0.3: {}
+
+  ts-mocha@10.0.0(mocha@9.2.2):
+    dependencies:
+      mocha: 9.2.2
+      ts-node: 7.0.1
+    optionalDependencies:
+      tsconfig-paths: 3.15.0
+
+  ts-node@7.0.1:
+    dependencies:
+      arrify: 1.0.1
+      buffer-from: 1.1.2
+      diff: 3.5.0
+      make-error: 1.3.6
+      minimist: 1.2.8
+      mkdirp: 0.5.6
+      source-map-support: 0.5.21
+      yn: 2.0.0
+
+  tsconfig-paths@3.15.0:
+    dependencies:
+      '@types/json5': 0.0.29
+      json5: 1.0.2
+      minimist: 1.2.8
+      strip-bom: 3.0.0
+    optional: true
+
+  tslib@2.6.3: {}
+
+  type-detect@4.0.8: {}
+
+  typescript@4.9.5: {}
+
+  undici-types@5.26.5: {}
+
+  utf-8-validate@5.0.10:
+    dependencies:
+      node-gyp-build: 4.8.1
+    optional: true
+
+  uuid@8.3.2: {}
+
+  webidl-conversions@3.0.1: {}
+
+  whatwg-url@5.0.0:
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  workerpool@6.2.0: {}
+
+  wrap-ansi@7.0.0:
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+
+  wrappy@1.0.2: {}
+
+  ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  y18n@5.0.8: {}
+
+  yargs-parser@20.2.4: {}
+
+  yargs-unparser@2.0.0:
+    dependencies:
+      camelcase: 6.3.0
+      decamelize: 4.0.0
+      flat: 5.0.2
+      is-plain-obj: 2.1.0
+
+  yargs@16.2.0:
+    dependencies:
+      cliui: 7.0.4
+      escalade: 3.1.2
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      string-width: 4.2.3
+      y18n: 5.0.8
+      yargs-parser: 20.2.4
+
+  yn@2.0.0: {}
+
+  yocto-queue@0.1.0: {}

+ 21 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "fundraiser"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "fundraiser"
+
+[features]
+default = []
+cpi = ["no-entrypoint"]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
+
+[dependencies]
+anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
+anchor-spl = "0.30.1"

+ 2 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 5 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/constants.rs

@@ -0,0 +1,5 @@
+pub const ANCHOR_DISCRIMINATOR: usize = 8;
+pub const MIN_AMOUNT_TO_RAISE: u64 = 3;
+pub const SECONDS_TO_DAYS: i64 = 86400;
+pub const MAX_CONTRIBUTION_PERCENTAGE: u64 = 10;
+pub const PERCENTAGE_SCALER: u64 = 100;

+ 21 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/error.rs

@@ -0,0 +1,21 @@
+use anchor_lang::error_code;
+
+#[error_code]
+pub enum FundraiserError {
+    #[msg("The amount to raise has not been met")]
+    TargetNotMet,
+    #[msg("The amount to raise has been achieved")]
+    TargetMet,
+    #[msg("The contribution is too big")]
+    ContributionTooBig,
+    #[msg("The contribution is too small")]
+    ContributionTooSmall,
+    #[msg("The maximum amount to contribute has been reached")]
+    MaximumContributionsReached,
+    #[msg("The fundraiser has not ended yet")]
+    FundraiserNotEnded,
+    #[msg("The fundraiser has ended")]
+    FundraiserEnded,
+    #[msg("Invalid total amount. i should be bigger than 3")]
+    InvalidAmount
+}

+ 83 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs

@@ -0,0 +1,83 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken, 
+    token::{
+        transfer, 
+        Mint, 
+        Token, 
+        TokenAccount, 
+        Transfer
+    }
+};
+
+use crate::{
+    state::Fundraiser, 
+    FundraiserError
+};
+
+#[derive(Accounts)]
+pub struct CheckContributions<'info> {
+    #[account(mut)]
+    pub maker: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        seeds = [b"fundraiser".as_ref(), maker.key().as_ref()],
+        bump = fundraiser.bump,
+        close = maker,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser,
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    #[account(
+        init_if_needed,
+        payer = maker,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = maker,
+    )]
+    pub maker_ata: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+}
+
+impl<'info> CheckContributions<'info> {
+    pub fn check_contributions(&self) -> Result<()> {
+        
+        // Check if the target amount has been met
+        require!(
+            self.vault.amount >= self.fundraiser.amount_to_raise,
+            FundraiserError::TargetNotMet
+        );
+
+        // Transfer the funds to the maker
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the vault to the maker
+        let cpi_accounts = Transfer {
+            from: self.vault.to_account_info(),
+            to: self.maker_ata.to_account_info(),
+            authority: self.fundraiser.to_account_info(),
+        };
+
+        // Signer seeds to sign the CPI on behalf of the fundraiser account
+        let signer_seeds: [&[&[u8]]; 1] = [&[
+            b"fundraiser".as_ref(),
+            self.maker.to_account_info().key.as_ref(),
+            &[self.fundraiser.bump],
+        ]];
+
+        // CPI context with signer since the fundraiser account is a PDA
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &signer_seeds);
+
+        // Transfer the funds from the vault to the maker
+        transfer(cpi_ctx, self.vault.amount)?;
+
+        Ok(())
+    }
+}

+ 109 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs

@@ -0,0 +1,109 @@
+use anchor_lang::prelude::*;
+use anchor_spl::token::{
+    Mint, 
+    transfer, 
+    Token, 
+    TokenAccount, 
+    Transfer
+};
+
+use crate::{
+    state::{
+        Contributor, 
+        Fundraiser
+    }, FundraiserError, 
+    ANCHOR_DISCRIMINATOR, 
+    MAX_CONTRIBUTION_PERCENTAGE, 
+    PERCENTAGE_SCALER, SECONDS_TO_DAYS
+};
+
+#[derive(Accounts)]
+pub struct Contribute<'info> {
+    #[account(mut)]
+    pub contributor: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        has_one = mint_to_raise,
+        seeds = [b"fundraiser".as_ref(), fundraiser.maker.as_ref()],
+        bump = fundraiser.bump,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        init_if_needed,
+        payer = contributor,
+        seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()],
+        bump,
+        space = ANCHOR_DISCRIMINATOR + Contributor::INIT_SPACE,
+    )]
+    pub contributor_account: Account<'info, Contributor>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = contributor
+    )]
+    pub contributor_ata: Account<'info, TokenAccount>,
+    #[account(
+        mut,
+        associated_token::mint = fundraiser.mint_to_raise,
+        associated_token::authority = fundraiser
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+}
+
+impl<'info> Contribute<'info> {
+    pub fn contribute(&mut self, amount: u64) -> Result<()> {
+
+        // Check if the amount to contribute meets the minimum amount required
+        require!(
+            amount >= 1_u64.pow(self.mint_to_raise.decimals as u32), 
+            FundraiserError::ContributionTooSmall
+        );
+
+        // Check if the amount to contribute is less than the maximum allowed contribution
+        require!(
+            amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER, 
+            FundraiserError::ContributionTooBig
+        );
+
+        // Check if the fundraising duration has been reached
+        let current_time = Clock::get()?.unix_timestamp;
+        require!(
+            self.fundraiser.duration <= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16,
+            crate::FundraiserError::FundraiserEnded
+        );
+
+        // Check if the maximum contributions per contributor have been reached
+        require!(
+            (self.contributor_account.amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER)
+                && (self.contributor_account.amount + amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER),
+            FundraiserError::MaximumContributionsReached
+        );
+
+        // Transfer the funds to the vault
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the contributor to the vault
+        let cpi_accounts = Transfer {
+            from: self.contributor_ata.to_account_info(),
+            to: self.vault.to_account_info(),
+            authority: self.contributor.to_account_info(),
+        };
+
+        // Crete a CPI context
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+
+        // Transfer the funds from the contributor to the vault
+        transfer(cpi_ctx, amount)?;
+
+        // Update the fundraiser and contributor accounts with the new amounts
+        self.fundraiser.current_amount += amount;
+
+        self.contributor_account.amount += amount;
+
+        Ok(())
+    }
+}

+ 62 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize.rs

@@ -0,0 +1,62 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken, 
+    token::{
+        Mint, 
+        Token, 
+        TokenAccount
+    }
+};
+
+use crate::{
+    state::Fundraiser, FundraiserError, ANCHOR_DISCRIMINATOR, MIN_AMOUNT_TO_RAISE
+};
+
+#[derive(Accounts)]
+pub struct Initialize<'info> {
+    #[account(mut)]
+    pub maker: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        init,
+        payer = maker,
+        seeds = [b"fundraiser", maker.key().as_ref()],
+        bump,
+        space = ANCHOR_DISCRIMINATOR + Fundraiser::INIT_SPACE,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        init,
+        payer = maker,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser,
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+}
+
+impl<'info> Initialize<'info> {
+    pub fn initialize(&mut self, amount: u64, duration: u16, bumps: &InitializeBumps) -> Result<()> {
+
+        // Check if the amount to raise meets the minimum amount required
+        require!(
+            amount >= MIN_AMOUNT_TO_RAISE.pow(self.mint_to_raise.decimals as u32),
+            FundraiserError::InvalidAmount
+        );
+
+        // Initialize the fundraiser account
+        self.fundraiser.set_inner(Fundraiser {
+            maker: self.maker.key(),
+            mint_to_raise: self.mint_to_raise.key(),
+            amount_to_raise: amount,
+            current_amount: 0,
+            time_started: Clock::get()?.unix_timestamp,
+            duration,
+            bump: bumps.fundraiser
+        });
+        
+        Ok(())
+    }
+}

+ 9 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs

@@ -0,0 +1,9 @@
+pub mod initialize;
+pub mod contribute;
+pub mod checker;
+pub mod refund;
+
+pub use initialize::*;
+pub use contribute::*;
+pub use checker::*;
+pub use refund::*;

+ 99 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs

@@ -0,0 +1,99 @@
+use anchor_lang::prelude::*;
+use anchor_spl::token::{
+    transfer, 
+    Mint, 
+    Token, 
+    TokenAccount, 
+    Transfer
+};
+
+use crate::{
+    state::{
+        Contributor, 
+        Fundraiser
+    }, 
+    SECONDS_TO_DAYS
+};
+
+#[derive(Accounts)]
+pub struct Refund<'info> {
+    #[account(mut)]
+    pub contributor: Signer<'info>,
+    pub maker: SystemAccount<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        has_one = mint_to_raise,
+        seeds = [b"fundraiser", maker.key().as_ref()],
+        bump = fundraiser.bump,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        mut,
+        seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()],
+        bump,
+        close = contributor,
+    )]
+    pub contributor_account: Account<'info, Contributor>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = contributor
+    )]
+    pub contributor_ata: Account<'info, TokenAccount>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+}
+
+impl<'info> Refund<'info> {
+    pub fn refund(&mut self) -> Result<()> {
+
+        // Check if the fundraising duration has been reached
+        let current_time = Clock::get()?.unix_timestamp;
+ 
+        require!(
+            self.fundraiser.duration >= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16,
+            crate::FundraiserError::FundraiserNotEnded
+        );
+
+        require!(
+            self.vault.amount < self.fundraiser.amount_to_raise,
+            crate::FundraiserError::TargetMet
+        );
+
+        // Transfer the funds back to the contributor
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the vault to the contributor
+        let cpi_accounts = Transfer {
+            from: self.vault.to_account_info(),
+            to: self.contributor_ata.to_account_info(),
+            authority: self.fundraiser.to_account_info(),
+        };
+
+        // Signer seeds to sign the CPI on behalf of the fundraiser account
+        let signer_seeds: [&[&[u8]]; 1] = [&[
+            b"fundraiser".as_ref(),
+            self.maker.to_account_info().key.as_ref(),
+            &[self.fundraiser.bump],
+        ]];
+
+        // CPI context with signer since the fundraiser account is a PDA
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &signer_seeds);
+
+        // Transfer the funds from the vault to the contributor
+        transfer(cpi_ctx, self.contributor_account.amount)?;
+
+        // Update the fundraiser state by reducing the amount contributed
+        self.fundraiser.current_amount -= self.contributor_account.amount;
+
+        Ok(())
+    }
+}

+ 45 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/lib.rs

@@ -0,0 +1,45 @@
+use anchor_lang::prelude::*;
+
+declare_id!("Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC");
+
+mod state;
+mod instructions;
+mod error;
+mod constants;
+
+use instructions::*;
+use error::*;
+pub use constants::*;
+
+#[program]
+pub mod fundraiser {
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>, amount: u64, duration: u16) -> Result<()> {
+
+        ctx.accounts.initialize(amount, duration, &ctx.bumps)?;
+
+        Ok(())
+    }
+
+    pub fn contribute(ctx: Context<Contribute>, amount: u64) -> Result<()> {
+
+        ctx.accounts.contribute(amount)?;
+
+        Ok(())
+    }
+
+    pub fn check_contributions(ctx: Context<CheckContributions>) -> Result<()> {
+
+        ctx.accounts.check_contributions()?;
+
+        Ok(())
+    }
+
+    pub fn refund(ctx: Context<Refund>) -> Result<()> {
+
+        ctx.accounts.refund()?;
+
+        Ok(())
+    }
+}

+ 7 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs

@@ -0,0 +1,7 @@
+use anchor_lang::prelude::*;
+
+#[account]
+#[derive(InitSpace)]
+pub struct Contributor {
+    pub amount: u64,
+}

+ 13 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs

@@ -0,0 +1,13 @@
+use anchor_lang::prelude::*;
+
+#[account]
+#[derive(InitSpace)]
+pub struct Fundraiser {
+    pub maker: Pubkey,
+    pub mint_to_raise: Pubkey,
+    pub amount_to_raise: u64,
+    pub current_amount: u64,
+    pub time_started: i64,
+    pub duration: u16,
+    pub bump: u8,
+}

+ 5 - 0
tokens/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs

@@ -0,0 +1,5 @@
+pub mod fundraiser;
+pub mod contributor;
+
+pub use fundraiser::*;
+pub use contributor::*;

+ 474 - 0
tokens/token-fundraiser/anchor/readme.MD

@@ -0,0 +1,474 @@
+# Token Fundraiser
+
+This example demonstrates how to create a fundraiser for SPL Tokens.
+
+In this example, a user will be able to create a fundraiser account, where he will be specify the mint he wants to collect and the fundraising target.
+
+---
+
+## Let's walk through the architecture:
+
+A fundraising account consists of:
+
+```rust
+#[account]
+#[derive(InitSpace)]
+pub struct Fundraiser {
+    pub maker: Pubkey,
+    pub mint_to_raise: Pubkey,
+    pub amount_to_raise: u64,
+    pub current_amount: u64,
+    pub time_started: i64,
+    pub duration: u8,
+    pub bump: u8,
+}
+```
+
+### In this state account, we will store:
+
+- maker: the person who is starting the fundraising
+
+- mint_to_raise: the mint that the maker wants to receive
+
+- amount_to_raise: the target amount that the maker is trying to raise
+
+- current_amount: the total amount currently donated
+
+- time_started: the time when the account was created
+
+- duration: the timeframe to collect all the contributions (in days) 
+
+- bump: since our Fundraiser account will be a PDA (Program Derived Address), we will store the bump of the account
+
+We use InitSpace derive macro to implement the space triat that will calculate the amount of space that our account will use on-chain (without taking the anchor discriminator into consideration)
+
+---
+
+### The user will be able to create new Fundraiser accounts. For that, we create the following context:
+
+```rust
+#[derive(Accounts)]
+pub struct Initialize<'info> {
+    #[account(mut)]
+    pub maker: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        init,
+        payer = maker,
+        seeds = [b"fundraiser", maker.key().as_ref()],
+        bump,
+        space = ANCHOR_DISCRIMINATOR + Fundraiser::INIT_SPACE,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        init,
+        payer = maker,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser,
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+}
+```
+
+Let´s have a closer look at the accounts that we are passing in this context:
+
+- maker: will be the person starting the fundraising. He will be a signer of the transaction, and we mark his account as mutable as we will be deducting lamports from this account
+
+- mint_to_raise: The mint that the user wants to receive. This will be a Mint Account, that we will use to store the mint address
+
+- fundraiser: will be the state account that we will initialize and the maker will be paying for the initialization of the account.
+We derive the Fundraiser PDA from the byte representation of the word "fundraiser" and the reference of the maker public key. Anchor will calculate the canonical bump (the first bump that throws that address out of the ed25519 eliptic curve) and save it for us in a struct
+
+- vault: We will initialize a vault (ATA) to receive the contributions. This account will be derived from the mint that the user wants to receive, and the fundraiser account that we are just creating
+
+- system_program: Program resposible for the initialization of any new account
+
+- token_program and associated_token_program: We are creating new ATAs
+
+### We then implement some functionality for our Initialize context:
+
+```rust
+impl<'info> Initialize<'info> {
+    pub fn initialize(&mut self, amount: u64, duration: u8, bumps: &InitializeBumps) -> Result<()> {
+
+        // Check if the amount to raise meets the minimum amount required
+        require!(
+            amount > MIN_AMOUNT_TO_RAISE.pow(self.mint_to_raise.decimals as u32),
+            FundraiserError::InvalidAmount
+        );
+
+        // Initialize the fundraiser account
+        self.fundraiser.set_inner(Fundraiser {
+            maker: self.maker.key(),
+            mint_to_raise: self.mint_to_raise.key(),
+            amount_to_raise: amount,
+            current_amount: 0,
+            time_started: Clock::get()?.unix_timestamp,
+            duration,
+            bump: bumps.fundraiser
+        });
+        
+        Ok(())
+    }
+}
+```
+
+In here, we basically just set the data of our Fundraiser account if the amount to raise is bigger than 3 (minimum amount)
+
+---
+
+### Users will be able to contribute to a fundraising
+
+A contribution account consists of:
+
+```rust
+#[account]
+#[derive(InitSpace)]
+pub struct Contributor {
+    pub amount: u64,
+}
+```rust
+
+In this account we will only store the total amount contributed by a specific contributor
+
+#[derive(Accounts)]
+pub struct Contribute<'info> {
+    #[account(mut)]
+    pub contributor: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        has_one = mint_to_raise,
+        seeds = [b"fundraiser".as_ref(), fundraiser.maker.as_ref()],
+        bump = fundraiser.bump,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        init_if_needed,
+        payer = contributor,
+        seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()],
+        bump,
+        space = ANCHOR_DISCRIMINATOR + Contributor::INIT_SPACE,
+    )]
+    pub contributor_account: Account<'info, Contributor>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = contributor
+    )]
+    pub contributor_ata: Account<'info, TokenAccount>,
+    #[account(
+        mut,
+        associated_token::mint = fundraiser.mint_to_raise,
+        associated_token::authority = fundraiser
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+}
+```
+
+In this context, we are passing all the accounts needed to contribute to a fundraising campaign:
+
+- contributor: The address of the person that is contributing
+
+- mint_to_raise: the mint that the maker is expecting to receive as contributions
+
+- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA
+
+- contributor account: We initialize (if needed) a contributor account that will store the total amount that a specific contributor has contributed with so far
+
+- contributor_ata: The ata where we will be transfering tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and contributor address), and we mark it as mutable since we will be deducting tokens from that account
+
+- vault: The ata where we will be depositing tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and Fundraiser account), and we mark it as mutable since we will be depositing tokens in that account
+
+- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens
+
+### We then implement some functionality for our Contribute context:
+
+```rust
+impl<'info> Contribute<'info> {
+    pub fn contribute(&mut self, amount: u64) -> Result<()> {
+
+        // Check if the amount to contribute meets the minimum amount required
+        require!(
+            amount > 1_u8.pow(self.mint_to_raise.decimals as u32) as u64, 
+            FundraiserError::ContributionTooSmall
+        );
+
+        // Check if the amount to contribute is less than the maximum allowed contribution
+        require!(
+            amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER, 
+            FundraiserError::ContributionTooBig
+        );
+
+        // Check if the maximum contributions per contributor have been reached
+        require!(
+            (self.contributor_account.amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER)
+                && (self.contributor_account.amount + amount <= (self.fundraiser.amount_to_raise * MAX_CONTRIBUTION_PERCENTAGE) / PERCENTAGE_SCALER),
+            FundraiserError::MaximumContributionsReached
+        );
+
+        // Check if the fundraising duration has been reached
+        let current_time = Clock::get()?.unix_timestamp;
+        require!(
+            self.fundraiser.duration <= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u8,
+            crate::FundraiserError::FundraisingEnded
+        );
+
+        // Transfer the funds to the vault
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the contributor to the vault
+        let cpi_accounts = Transfer {
+            from: self.contributor_ata.to_account_info(),
+            to: self.vault.to_account_info(),
+            authority: self.contributor.to_account_info(),
+        };
+
+        // Crete a CPI context
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+
+        // Transfer the funds from the contributor to the vault
+        transfer(cpi_ctx, amount)?;
+
+        // Update the fundraiser and contributor accounts with the new amounts
+        self.fundraiser.current_amount += amount;
+
+        self.contributor_account.amount += amount;
+
+        Ok(())
+    }
+}
+```
+In here, we make some checks:
+- We check that the user is depositing at least one token
+
+- We Check that the user is not contributing with more than 10% of the target amount
+
+- We check that the total contributions of the user do not exceed a total of 10% of the target amount
+
+- We check that the fundraising duration has not elapsed
+
+After, we create a CPI to the token program, to transfer a certain amount of SPL tokens from the Contributor ATA to the vault.
+We pass the authority of the account where the tokens are being deducted from (In this case is the contributor, as he is the authority of the contributor ata).
+
+Lastly, we update our state acounts with the right amounts
+
+---
+
+### User will be able to claim the tokens once the fundraising target has been reached
+
+```rust
+#[derive(Accounts)]
+pub struct CheckContributions<'info> {
+    #[account(mut)]
+    pub maker: Signer<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        seeds = [b"fundraiser".as_ref(), maker.key().as_ref()],
+        bump = fundraiser.bump,
+        close = maker,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser,
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    #[account(
+        init_if_needed,
+        payer = maker,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = maker,
+    )]
+    pub maker_ata: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+}
+```
+
+In this context, we are passing all the accounts needed for a user to claim the raised tokens:
+
+- maker: The address of the person raising the the funds. We mark it as mutable since the maker will be paying for initialization fees and will receive lamports from rent back
+
+- mint_to_raise: the mint that the maker is expecting to receive as contributions
+
+- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA
+
+- vault: The ata where we will be transfering tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and fundraiser account), and we mark it as mutable since we will be deducting tokens from that account
+
+- maker_ata: The ata where we will be depositing tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and maker account), and we mark it as mutable since we will be depositing tokens in that account.
+In case we need to initialize this ATA, the maker will be paying for the initialization fees
+
+- system_program and associated_token_program: Since we are initializing new ATAs
+
+- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens
+
+### We then implement some functionality for our Contribute context:
+
+```rust
+impl<'info> CheckContributions<'info> {
+    pub fn check_contributions(&self) -> Result<()> {
+        
+        // Check if the target amount has been met
+        require!(
+            self.vault.amount >= self.fundraiser.amount_to_raise,
+            FundraiserError::TargetNotMet
+        );
+
+        // Transfer the funds to the maker
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the vault to the maker
+        let cpi_accounts = Transfer {
+            from: self.vault.to_account_info(),
+            to: self.maker_ata.to_account_info(),
+            authority: self.fundraiser.to_account_info(),
+        };
+
+        // Signer seeds to sign the CPI on behalf of the fundraiser account
+        let signer_seeds: [&[&[u8]]; 1] = [&[
+            b"fundraiser".as_ref(),
+            self.maker.to_account_info().key.as_ref(),
+            &[self.fundraiser.bump],
+        ]];
+
+        // CPI context with signer since the fundraiser account is a PDA
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &signer_seeds);
+
+        // Transfer the funds from the vault to the maker
+        transfer(cpi_ctx, self.vault.amount)?;
+
+        Ok(())
+    }
+}
+```
+
+In this implementation, we check if the amount of tokens in the vault is equal or bigger then the fundraising campaign target.
+If it is, then we perform a CPI to the token program to transfer the funds from the vault to the maker ATA. Since the vault is an ATA, we need to create our CPI context with a signer and use the seeds and bump from the PDA (We are signing with our program on behalf of that PDA).
+
+Finally, we close our Fundraiser account and send the lamports from the rent back to the maker (done with the "close" constraint int the Fundraiser account).
+
+
+---
+
+### Users will be able to refund their contributions, if the duration of the fundraising has elapsed and the target has not reached
+
+```rust
+#[derive(Accounts)]
+pub struct Refund<'info> {
+    #[account(mut)]
+    pub contributor: Signer<'info>,
+    pub maker: SystemAccount<'info>,
+    pub mint_to_raise: Account<'info, Mint>,
+    #[account(
+        mut,
+        has_one = mint_to_raise,
+        seeds = [b"fundraiser", maker.key().as_ref()],
+        bump = fundraiser.bump,
+    )]
+    pub fundraiser: Account<'info, Fundraiser>,
+    #[account(
+        mut,
+        seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()],
+        bump,
+        close = contributor,
+    )]
+    pub contributor_account: Account<'info, Contributor>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = contributor
+    )]
+    pub contributor_ata: Account<'info, TokenAccount>,
+    #[account(
+        mut,
+        associated_token::mint = mint_to_raise,
+        associated_token::authority = fundraiser
+    )]
+    pub vault: Account<'info, TokenAccount>,
+    pub token_program: Program<'info, Token>,
+    pub system_program: Program<'info, System>,
+}
+```
+
+In this context, we are passing all the accounts needed for a contributor to refund their tokens:
+
+- contributor: The address of the person that is contributing
+
+- maker: The address of the person raising the the funds.
+
+- mint_to_raise: the mint that the maker is expecting to receive as contributions
+
+- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA
+
+- contributor account: An initialized Contributor account that will store the total amount that a specific contributor has contributed with so far
+
+- contributor_ata: The ata where we will be transfering tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and contributor address), and we mark it as mutable since we will be depositing tokens to that account
+
+- vault: The ata where we will be withdrawing tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and Fundraiser account), and we mark it as mutable since we will be withdrawing tokens from that account
+
+- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens
+
+### We then implement some functionality for our Refund context:
+
+```rust
+impl<'info> Refund<'info> {
+    pub fn refund(&mut self) -> Result<()> {
+
+        // Check if the fundraising duration has been reached
+        let current_time = Clock::get()?.unix_timestamp;
+ 
+        require!(
+            self.fundraiser.duration <= ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u8,
+            crate::FundraiserError::FundraiserNotEnded
+        );
+
+        require!(
+            self.vault.amount < self.fundraiser.amount_to_raise,
+            crate::FundraiserError::TargetMet
+        );
+
+        // Transfer the funds back to the contributor
+        // CPI to the token program to transfer the funds
+        let cpi_program = self.token_program.to_account_info();
+
+        // Transfer the funds from the vault to the contributor
+        let cpi_accounts = Transfer {
+            from: self.vault.to_account_info(),
+            to: self.contributor_ata.to_account_info(),
+            authority: self.fundraiser.to_account_info(),
+        };
+
+        // Signer seeds to sign the CPI on behalf of the fundraiser account
+        let signer_seeds: [&[&[u8]]; 1] = [&[
+            b"fundraiser".as_ref(),
+            self.maker.to_account_info().key.as_ref(),
+            &[self.fundraiser.bump],
+        ]];
+
+        // CPI context with signer since the fundraiser account is a PDA
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &signer_seeds);
+
+        // Transfer the funds from the vault to the contributor
+        transfer(cpi_ctx, self.contributor_account.amount)?;
+
+        // Update the fundraiser state by reducing the amount contributed
+        self.fundraiser.current_amount -= self.contributor_account.amount;
+
+        Ok(())
+    }
+}
+```
+
+In here, we will check if the fundrasing has already met the target and if it passed the duration time.
+After doing the proper checks, we transfer the donated funds from the vault back to the contributor

+ 210 - 0
tokens/token-fundraiser/anchor/tests/fundraiser.ts

@@ -0,0 +1,210 @@
+import * as anchor from '@coral-xyz/anchor';
+import type { Program } from '@coral-xyz/anchor';
+import type NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  TOKEN_PROGRAM_ID,
+  createMint,
+  getAssociatedTokenAddressSync,
+  getOrCreateAssociatedTokenAccount,
+  mintTo,
+} from '@solana/spl-token';
+import type { Fundraiser } from '../target/types/fundraiser';
+
+describe('fundraiser', () => {
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.Fundraiser as Program<Fundraiser>;
+
+  const maker = anchor.web3.Keypair.generate();
+
+  let mint: anchor.web3.PublicKey;
+
+  let contributorATA: anchor.web3.PublicKey;
+
+  let makerATA: anchor.web3.PublicKey;
+
+  const wallet = provider.wallet as NodeWallet;
+
+  const fundraiser = anchor.web3.PublicKey.findProgramAddressSync([Buffer.from('fundraiser'), maker.publicKey.toBuffer()], program.programId)[0];
+
+  const contributor = anchor.web3.PublicKey.findProgramAddressSync(
+    [Buffer.from('contributor'), fundraiser.toBuffer(), provider.publicKey.toBuffer()],
+    program.programId,
+  )[0];
+
+  const confirm = async (signature: string): Promise<string> => {
+    const block = await provider.connection.getLatestBlockhash();
+    await provider.connection.confirmTransaction({
+      signature,
+      ...block,
+    });
+    return signature;
+  };
+
+  it('Test Preparation', async () => {
+    const airdrop = await provider.connection.requestAirdrop(maker.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL).then(confirm);
+    console.log('\nAirdropped 1 SOL to maker', airdrop);
+
+    mint = await createMint(provider.connection, wallet.payer, provider.publicKey, provider.publicKey, 6);
+    console.log('Mint created', mint.toBase58());
+
+    contributorATA = (await getOrCreateAssociatedTokenAccount(provider.connection, wallet.payer, mint, wallet.publicKey)).address;
+
+    makerATA = (await getOrCreateAssociatedTokenAccount(provider.connection, wallet.payer, mint, maker.publicKey)).address;
+
+    const mintTx = await mintTo(provider.connection, wallet.payer, mint, contributorATA, provider.publicKey, 1_000_000_0);
+    console.log('Minted 10 tokens to contributor', mintTx);
+  });
+
+  it('Initialize Fundaraiser', async () => {
+    const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+    const tx = await program.methods
+      .initialize(new anchor.BN(30000000), 0)
+      .accountsPartial({
+        maker: maker.publicKey,
+        fundraiser,
+        mintToRaise: mint,
+        vault,
+        systemProgram: anchor.web3.SystemProgram.programId,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
+      })
+      .signers([maker])
+      .rpc()
+      .then(confirm);
+
+    console.log('\nInitialized fundraiser Account');
+    console.log('Your transaction signature', tx);
+  });
+
+  it('Contribute to Fundraiser', async () => {
+    const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+    const tx = await program.methods
+      .contribute(new anchor.BN(1000000))
+      .accountsPartial({
+        contributor: provider.publicKey,
+        fundraiser,
+        contributorAccount: contributor,
+        contributorAta: contributorATA,
+        vault,
+        tokenProgram: TOKEN_PROGRAM_ID,
+      })
+      .rpc()
+      .then(confirm);
+
+    console.log('\nContributed to fundraiser', tx);
+    console.log('Your transaction signature', tx);
+    console.log('Vault balance', (await provider.connection.getTokenAccountBalance(vault)).value.amount);
+
+    const contributorAccount = await program.account.contributor.fetch(contributor);
+    console.log('Contributor balance', contributorAccount.amount.toString());
+  });
+  it('Contribute to Fundraiser', async () => {
+    const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+    const tx = await program.methods
+      .contribute(new anchor.BN(1000000))
+      .accountsPartial({
+        contributor: provider.publicKey,
+        fundraiser,
+        contributorAccount: contributor,
+        contributorAta: contributorATA,
+        vault,
+        tokenProgram: TOKEN_PROGRAM_ID,
+      })
+      .rpc()
+      .then(confirm);
+
+    console.log('\nContributed to fundraiser', tx);
+    console.log('Your transaction signature', tx);
+    console.log('Vault balance', (await provider.connection.getTokenAccountBalance(vault)).value.amount);
+
+    const contributorAccount = await program.account.contributor.fetch(contributor);
+    console.log('Contributor balance', contributorAccount.amount.toString());
+  });
+
+  it('Contribute to Fundraiser - Robustness Test', async () => {
+    try {
+      const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+      const tx = await program.methods
+        .contribute(new anchor.BN(2000000))
+        .accountsPartial({
+          contributor: provider.publicKey,
+          fundraiser,
+          contributorAccount: contributor,
+          contributorAta: contributorATA,
+          vault,
+          tokenProgram: TOKEN_PROGRAM_ID,
+        })
+        .rpc()
+        .then(confirm);
+
+      console.log('\nContributed to fundraiser', tx);
+      console.log('Your transaction signature', tx);
+      console.log('Vault balance', (await provider.connection.getTokenAccountBalance(vault)).value.amount);
+    } catch (error) {
+      console.log('\nError contributing to fundraiser');
+      console.log(error.msg);
+    }
+  });
+
+  it('Check contributions - Robustness Test', async () => {
+    try {
+      const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+      const tx = await program.methods
+        .checkContributions()
+        .accountsPartial({
+          maker: maker.publicKey,
+          mintToRaise: mint,
+          fundraiser,
+          makerAta: makerATA,
+          vault,
+          tokenProgram: TOKEN_PROGRAM_ID,
+        })
+        .signers([maker])
+        .rpc()
+        .then(confirm);
+
+      console.log('\nChecked contributions');
+      console.log('Your transaction signature', tx);
+      console.log('Vault balance', (await provider.connection.getTokenAccountBalance(vault)).value.amount);
+    } catch (error) {
+      console.log('\nError checking contributions');
+      console.log(error.msg);
+    }
+  });
+
+  it('Refund Contributions', async () => {
+    const vault = getAssociatedTokenAddressSync(mint, fundraiser, true);
+
+    const contributorAccount = await program.account.contributor.fetch(contributor);
+    console.log('\nContributor balance', contributorAccount.amount.toString());
+
+    const tx = await program.methods
+      .refund()
+      .accountsPartial({
+        contributor: provider.publicKey,
+        maker: maker.publicKey,
+        mintToRaise: mint,
+        fundraiser,
+        contributorAccount: contributor,
+        contributorAta: contributorATA,
+        vault,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        systemProgram: anchor.web3.SystemProgram.programId,
+      })
+      .rpc()
+      .then(confirm);
+
+    console.log('\nRefunded contributions', tx);
+    console.log('Your transaction signature', tx);
+    console.log('Vault balance', (await provider.connection.getTokenAccountBalance(vault)).value.amount);
+  });
+});

+ 10 - 0
tokens/token-fundraiser/anchor/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}