Ver Fonte

Allow-Block list token (#399)

Tiago Carvalho há 2 meses atrás
pai
commit
33e969ad5d
84 ficheiros alterados com 4850 adições e 0 exclusões
  1. 44 0
      tokens/token-2022/transfer-hook/allow-block-list-token/.gitignore
  2. 14 0
      tokens/token-2022/transfer-hook/allow-block-list-token/.prettierignore
  3. 7 0
      tokens/token-2022/transfer-hook/allow-block-list-token/.prettierrc
  4. 50 0
      tokens/token-2022/transfer-hook/allow-block-list-token/README.md
  5. 13 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/.gitignore
  6. 7 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/.prettierignore
  7. 19 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/Anchor.toml
  8. 14 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/Cargo.toml
  9. 45 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml
  10. 2 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Xargo.toml
  11. 4 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/constants.rs
  12. 16 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/errors.rs
  13. 139 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs
  14. 110 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs
  15. 32 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs
  16. 141 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs
  17. 43 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs
  18. 16 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/mod.rs
  19. 30 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs
  20. 119 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs
  21. 51 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs
  22. 48 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs
  23. 34 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs
  24. 111 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test.rs
  25. 31 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/src/abl-token-exports.ts
  26. 1 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/src/index.ts
  27. 14 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/tests/basic.test.ts
  28. 13 0
      tokens/token-2022/transfer-hook/allow-block-list-token/anchor/tsconfig.json
  29. 21 0
      tokens/token-2022/transfer-hook/allow-block-list-token/components.json
  30. 14 0
      tokens/token-2022/transfer-hook/allow-block-list-token/eslint.config.mjs
  31. 7 0
      tokens/token-2022/transfer-hook/allow-block-list-token/next.config.ts
  32. 59 0
      tokens/token-2022/transfer-hook/allow-block-list-token/package.json
  33. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/postcss.config.mjs
  34. 0 0
      tokens/token-2022/transfer-hook/allow-block-list-token/public/.gitkeep
  35. 15 0
      tokens/token-2022/transfer-hook/allow-block-list-token/scripts/start.sh
  36. 4 0
      tokens/token-2022/transfer-hook/allow-block-list-token/scripts/stop.sh
  37. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/account/[address]/page.tsx
  38. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/account/page.tsx
  39. 6 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/config/page.tsx
  40. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/create-token/page.tsx
  41. BIN
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/favicon.ico
  42. 127 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/globals.css
  43. 41 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/layout.tsx
  44. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/manage-token/[address]/page.tsx
  45. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/manage-token/page.tsx
  46. 5 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/app/page.tsx
  47. 360 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-config.tsx
  48. 330 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-data-access.tsx
  49. 34 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-feature.tsx
  50. 281 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token-detail.tsx
  51. 55 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token-input.tsx
  52. 30 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token.tsx
  53. 33 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-new-token.tsx
  54. 290 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-ui.tsx
  55. 255 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-data-access.tsx
  56. 47 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-detail-feature.tsx
  57. 22 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-list-feature.tsx
  58. 358 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-ui.tsx
  59. 13 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-alert.tsx
  60. 17 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-footer.tsx
  61. 79 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-header.tsx
  62. 23 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-hero.tsx
  63. 33 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-layout.tsx
  64. 38 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-modal.tsx
  65. 19 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-providers.tsx
  66. 117 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/cluster/cluster-data-access.tsx
  67. 72 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/cluster/cluster-ui.tsx
  68. 34 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/dashboard/dashboard-feature.tsx
  69. 31 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/react-query-provider.tsx
  70. 43 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/solana/solana-provider.tsx
  71. 8 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/theme-provider.tsx
  72. 29 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/theme-select.tsx
  73. 51 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/alert.tsx
  74. 50 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/button.tsx
  75. 54 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/card.tsx
  76. 111 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/dialog.tsx
  77. 219 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/dropdown-menu.tsx
  78. 21 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/input.tsx
  79. 21 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/label.tsx
  80. 25 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/sonner.tsx
  81. 75 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/table.tsx
  82. 33 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/components/use-transaction-toast.tsx
  83. 13 0
      tokens/token-2022/transfer-hook/allow-block-list-token/src/lib/utils.ts
  84. 29 0
      tokens/token-2022/transfer-hook/allow-block-list-token/tsconfig.json

+ 44 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/.gitignore

@@ -0,0 +1,44 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Anchor
+/anchor/target/

+ 14 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/.prettierignore

@@ -0,0 +1,14 @@
+# Add files here to ignore them from prettier formatting
+/anchor/target/debug
+/anchor/target/deploy
+/anchor/target/release
+/anchor/target/sbf-solana-solana
+/anchor/target/test-ledger
+/anchor/target/.rustc_info.json
+/dist
+/coverage
+.next
+/tmp
+package-lock.json
+pnpm-lock.yaml
+yarn.lock

+ 7 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/.prettierrc

@@ -0,0 +1,7 @@
+{
+  "arrowParens": "always",
+  "printWidth": 120,
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "all"
+}

+ 50 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/README.md

@@ -0,0 +1,50 @@
+# AllowBlockList Token
+
+An example of a allow / block list token using token extensions.
+
+## Features
+
+Allows the creation of an allow block list with a list authority.
+The allow/block list is then consumed by a transfer-hook.
+
+The list is managed by a single authority and can be used by several token mints. This enables a separation of concerns between token management and allow/block list management, ideal for scenarios where an issuer wants a 3rd party managed allow/block list or wants to share the same list across a group of assets.
+
+Initializes new tokens with several configuration options:
+- Permanent delegate
+- Allow list
+- Block list
+- Metadata
+- Authorities
+
+The issuer can configure the allow and block list with 3 distinct configurations:
+- Force Allow: requires everyone receiving tokens to be explicitly allowed in
+- Block: allows everyone to receive tokens unless explicitly blocked
+- Threshold Allow: allows everyone to receive tokens unless explicitly blocked up until a given transfer amount threshold. Transfers larger than the threshold require explicitly allow
+
+These configurations are saved in the token mint metadata.
+
+This repo includes a UI to manage the allow/block list based on the `legacy-next-tailwind-basic` template. It also allows creating new token mints on the spot with transfer-hook enabled along with token transfers given that most wallets fail to fetch transfer-hook dependencies on devnet and locally.
+
+## Setup
+
+Install dependencies:
+`yarn install`
+
+Compile the program:
+`anchor build`
+
+Compile the UI:
+`yarn run build`
+
+Serve the UI:
+`yarn run dev`
+
+### Local testing
+
+There are a couple scripts to manage the local validator and deployment.
+
+To start the local validator and deploy the program:
+`./scripts/start.sh`
+
+To stop the local validator:
+`./scripts/stop.sh`

+ 13 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/.gitignore

@@ -0,0 +1,13 @@
+.anchor
+.DS_Store
+target/debug
+target/deploy
+target/release
+target/sbf-solana-solana
+target/test-ledger
+target/.rustc_info.json
+**/*.rs.bk
+node_modules
+test-ledger
+.yarn
+ledger

+ 7 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/.prettierignore

@@ -0,0 +1,7 @@
+.anchor
+.DS_Store
+target
+node_modules
+dist
+build
+test-ledger

+ 19 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/Anchor.toml

@@ -0,0 +1,19 @@
+[toolchain]
+package_manager = "yarn"
+
+[features]
+resolution = true
+skip-lint = false
+
+[programs.localnet]
+abl-token = "LtkoMwPSKxAE714EY3V1oAEQ5LciqJcRwQQuQnzEhQQ"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "../node_modules/.bin/jest --preset ts-jest"

+ 14 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/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

+ 45 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml

@@ -0,0 +1,45 @@
+[package]
+name = "abl-token"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "abl_token"
+
+[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.31.1", features = ["interface-instructions"] }
+anchor-spl = { version = "0.31.1", features = [
+    "token_2022_extensions",
+    "token_2022",
+] }
+
+
+spl-tlv-account-resolution = "0.8.1"
+spl-transfer-hook-interface = { version = "0.8.2" }
+spl-discriminator = "0.3"
+
+[dev-dependencies]
+litesvm = "0.6.1"
+
+
+solana-instruction = "2.2.1"
+solana-keypair = "2.2.1"
+solana-native-token = "2.2.1"
+solana-pubkey = "2.2.1"
+solana-signer = "2.2.1"
+solana-system-interface = "1.0.0"
+solana-transaction = "2.2.1"
+solana-message = "2.2.1"
+solana-sdk-ids = "2.2.1"
+spl-token-2022 = { version = "8.0.1", features = ["no-entrypoint"]}

+ 2 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Xargo.toml

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

+ 4 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/constants.rs

@@ -0,0 +1,4 @@
+
+pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas";
+pub const CONFIG_SEED: &[u8] = b"config";
+pub const AB_WALLET_SEED: &[u8] = b"ab_wallet";

+ 16 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/errors.rs

@@ -0,0 +1,16 @@
+use anchor_lang::error_code;
+
+#[error_code]
+pub enum ABListError {
+    #[msg("Invalid metadata")]
+    InvalidMetadata,
+
+    #[msg("Wallet not allowed")]
+    WalletNotAllowed,
+
+    #[msg("Amount not allowed")]
+    AmountNotAllowed,
+
+    #[msg("Wallet blocked")]
+    WalletBlocked,
+}

+ 139 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/attach_to_mint.rs

@@ -0,0 +1,139 @@
+use anchor_lang::{prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer};
+use anchor_spl::{
+    token_2022::{spl_token_2022::{extension::{BaseStateWithExtensions, StateWithExtensions}, state::Mint as Mint2022}, Token2022},
+    token_interface::{spl_token_metadata_interface::state::{Field, TokenMetadata}, token_metadata_initialize, token_metadata_update_field, Mint, TokenMetadataInitialize, TokenMetadataUpdateField},
+};
+
+use spl_tlv_account_resolution::{
+     state::ExtraAccountMetaList,
+};
+use spl_transfer_hook_interface::instruction::ExecuteInstruction;
+
+use crate::{Mode, META_LIST_ACCOUNT_SEED, get_extra_account_metas, get_meta_list_size};
+
+
+#[derive(Accounts)]
+#[instruction(args: AttachToMintArgs)]
+pub struct AttachToMint<'info> {
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    pub mint_authority: Signer<'info>,
+
+    pub metadata_authority: Signer<'info>,
+
+    #[account(
+        mut,
+        mint::token_program = token_program,
+    )]
+    pub mint: Box<InterfaceAccount<'info, Mint>>,
+
+    #[account(
+        init,
+        space = get_meta_list_size()?,
+        seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],
+        bump,
+        payer = payer,
+    )]
+    /// CHECK: extra metas account
+    pub extra_metas_account: UncheckedAccount<'info>,
+
+    pub system_program: Program<'info, System>,
+
+    pub token_program: Program<'info, Token2022>,
+}
+
+impl AttachToMint<'_> {
+    pub fn attach_to_mint(&mut self, args: AttachToMintArgs) -> Result<()> {
+        let mint_info = self.mint.to_account_info();
+        let mint_data = mint_info.data.borrow();
+        let mint = StateWithExtensions::<Mint2022>::unpack(&mint_data)?;
+
+        let metadata = mint.get_variable_len_extension::<TokenMetadata>();
+        
+        if metadata.is_err() {
+            // assume metadata is not initialized, so we need to initialize it
+    
+            let cpi_accounts = TokenMetadataInitialize {
+                program_id: self.token_program.to_account_info(),
+                mint: self.mint.to_account_info(),
+                metadata: self.mint.to_account_info(), // metadata account is the mint, since data is stored in mint
+                mint_authority: self.mint_authority.to_account_info(),
+                update_authority: self.metadata_authority.to_account_info(),
+            };
+            let cpi_ctx = CpiContext::new(
+                self.token_program.to_account_info(),
+                cpi_accounts,
+            );
+            token_metadata_initialize(cpi_ctx, args.name.unwrap(), args.symbol.unwrap(), args.uri.unwrap())?;
+        }
+
+        let cpi_accounts = TokenMetadataUpdateField {
+            metadata: self.mint.to_account_info(),
+            update_authority: self.metadata_authority.to_account_info(),
+            program_id: self.token_program.to_account_info(),
+        };
+
+        let cpi_ctx = CpiContext::new(
+            self.token_program.to_account_info(),
+            cpi_accounts,
+        );
+
+        token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?;
+
+        if args.mode == Mode::Mixed {
+            let cpi_accounts = TokenMetadataUpdateField {
+                metadata: self.mint.to_account_info(),
+                update_authority: self.metadata_authority.to_account_info(),
+                program_id: self.token_program.to_account_info(),
+            };
+            let cpi_ctx = CpiContext::new(
+                self.token_program.to_account_info(),
+                cpi_accounts,
+            );
+
+            token_metadata_update_field(
+                cpi_ctx,
+                Field::Key("threshold".to_string()),
+                args.threshold.to_string(),
+            )?;
+        }
+
+        
+        let data = self.mint.to_account_info().data_len();
+        let min_balance = Rent::get()?.minimum_balance(data);
+        if min_balance > self.mint.to_account_info().get_lamports() {
+            invoke(
+                &transfer(
+                    &self.payer.key(),
+                    &self.mint.to_account_info().key(),
+                    min_balance - self.mint.to_account_info().get_lamports(),
+                ),
+                &[
+                    self.payer.to_account_info(),
+                    self.mint.to_account_info(),
+                    self.system_program.to_account_info(),
+                ],
+            )?;
+        }
+
+        // initialize the extra metas account
+        let extra_metas_account = &self.extra_metas_account;
+        let metas = get_extra_account_metas()?;
+        let mut data = extra_metas_account.try_borrow_mut_data()?;
+        ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &metas)?;
+
+        Ok(())
+        
+    }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct AttachToMintArgs {
+    pub name: Option<String>,
+    pub symbol: Option<String>,
+    pub uri: Option<String>,
+    pub mode: Mode,
+    pub threshold: u64,
+}
+

+ 110 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/change_mode.rs

@@ -0,0 +1,110 @@
+use anchor_lang::{prelude::*, solana_program::system_instruction::transfer };
+use anchor_lang::solana_program::program::invoke;
+use anchor_spl::token_interface::spl_token_metadata_interface::state::TokenMetadata;
+use anchor_spl::{
+    token_2022::{
+        spl_token_2022::extension::{BaseStateWithExtensions, StateWithExtensions},
+        spl_token_2022::state::Mint,
+        Token2022,
+        
+    },
+    token_interface::{
+        Mint as MintAccount,
+        spl_token_metadata_interface::state::Field, token_metadata_update_field,
+        TokenMetadataUpdateField,
+    },
+};
+
+use crate::Mode;
+
+#[derive(Accounts)]
+pub struct ChangeMode<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+    
+    #[account(
+        mut,
+        mint::token_program = token_program,
+    )]
+    pub mint: InterfaceAccount<'info, MintAccount>,
+
+    pub token_program: Program<'info, Token2022>,
+
+    pub system_program: Program<'info, System>,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct ChangeModeArgs {
+    pub mode: Mode,
+    pub threshold: u64,
+}
+
+impl ChangeMode<'_> {
+    pub fn change_mode(&mut self, args: ChangeModeArgs) -> Result<()> {
+        let cpi_accounts = TokenMetadataUpdateField {
+            metadata: self.mint.to_account_info(),
+            update_authority: self.authority.to_account_info(),
+            program_id: self.token_program.to_account_info(),
+        };
+        let cpi_program = self.token_program.to_account_info();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+
+        token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?;
+
+        if args.mode == Mode::Mixed || self.has_threshold()? {
+            let threshold = if args.mode == Mode::Mixed {
+                args.threshold
+            } else {
+                0
+            };
+
+            let cpi_accounts = TokenMetadataUpdateField {
+                metadata: self.mint.to_account_info(),
+                update_authority: self.authority.to_account_info(),
+                program_id: self.token_program.to_account_info(),
+            };
+            let cpi_program = self.token_program.to_account_info();
+            let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+
+            token_metadata_update_field(
+                cpi_ctx,
+                Field::Key("threshold".to_string()),
+                threshold.to_string(),
+            )?;
+        }
+
+        
+
+        let data = self.mint.to_account_info().data_len();
+        let min_balance = Rent::get()?.minimum_balance(data);
+        if min_balance > self.mint.to_account_info().get_lamports() {
+            invoke(
+                &transfer(
+                    &self.authority.key(),
+                    &self.mint.to_account_info().key(),
+                    min_balance - self.mint.to_account_info().get_lamports(),
+                ),
+                &[
+                    self.authority.to_account_info(),
+                    self.mint.to_account_info(),
+                    self.system_program.to_account_info(),
+                ],
+            )?;
+        }
+
+        Ok(())
+    }
+
+    fn has_threshold(&self) -> Result<bool> {
+        let mint_info = self.mint.to_account_info();
+        let mint_data = mint_info.data.borrow();
+        let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
+        let metadata = mint.get_variable_len_extension::<TokenMetadata>();
+        Ok(metadata.is_ok()
+            && metadata
+                .unwrap()
+                .additional_metadata
+                .iter()
+                .any(|(key, _)| key == "threshold"))
+    }
+}

+ 32 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_config.rs

@@ -0,0 +1,32 @@
+use anchor_lang::prelude::*;
+use crate::{Config, CONFIG_SEED};
+
+
+#[derive(Accounts)]
+pub struct InitConfig<'info> {
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    #[account(
+        init,
+        payer = payer,
+        space = 8 + Config::INIT_SPACE,
+        seeds = [CONFIG_SEED],
+        bump,
+    )]
+    pub config: Box<Account<'info, Config>>,
+
+    pub system_program: Program<'info, System>,
+}
+
+impl InitConfig<'_> {
+    pub fn init_config(&mut self, config_bump: u8) -> Result<()> {
+
+        self.config.set_inner(Config {
+            authority: self.payer.key(),
+            bump: config_bump,
+        });
+
+        Ok(())
+    }
+}

+ 141 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_mint.rs

@@ -0,0 +1,141 @@
+use anchor_lang::{
+    prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer,
+};
+use anchor_spl::{
+    token_2022::Token2022,
+    token_interface::{
+        spl_token_metadata_interface::state::Field, token_metadata_initialize,
+        token_metadata_update_field, Mint, TokenMetadataInitialize, TokenMetadataUpdateField,
+    },
+};
+
+use spl_tlv_account_resolution::
+    state::ExtraAccountMetaList
+;
+use spl_transfer_hook_interface::instruction::ExecuteInstruction;
+
+use crate::{get_extra_account_metas, get_meta_list_size, Mode, META_LIST_ACCOUNT_SEED};
+
+
+#[derive(Accounts)]
+#[instruction(args: InitMintArgs)]
+pub struct InitMint<'info> {
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    #[account(
+        init,
+        payer = payer,
+        mint::token_program = token_program,
+        mint::decimals = args.decimals,
+        mint::authority = payer.key(),
+        mint::freeze_authority = args.freeze_authority,
+        extensions::permanent_delegate::delegate = args.permanent_delegate,
+        extensions::transfer_hook::authority = args.transfer_hook_authority,
+        extensions::transfer_hook::program_id = crate::id(),
+        extensions::metadata_pointer::authority = payer.key(),
+        extensions::metadata_pointer::metadata_address = mint.key(),
+    )]
+    pub mint: Box<InterfaceAccount<'info, Mint>>,
+
+    #[account(
+        init,
+        space = get_meta_list_size()?,
+        seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],
+        bump,
+        payer = payer,
+    )]
+    /// CHECK: extra metas account
+    pub extra_metas_account: UncheckedAccount<'info>,
+
+    pub system_program: Program<'info, System>,
+
+    pub token_program: Program<'info, Token2022>,
+}
+
+impl InitMint<'_> {
+    pub fn init_mint(&mut self, args: InitMintArgs) -> Result<()> {
+        let cpi_accounts = TokenMetadataInitialize {
+            program_id: self.token_program.to_account_info(),
+            mint: self.mint.to_account_info(),
+            metadata: self.mint.to_account_info(), // metadata account is the mint, since data is stored in mint
+            mint_authority: self.payer.to_account_info(),
+            update_authority: self.payer.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new(
+            self.token_program.to_account_info(),
+            cpi_accounts,
+        );
+        token_metadata_initialize(cpi_ctx, args.name, args.symbol, args.uri)?;
+
+        let cpi_accounts = TokenMetadataUpdateField {
+            metadata: self.mint.to_account_info(),
+            update_authority: self.payer.to_account_info(),
+            program_id: self.token_program.to_account_info(),
+        };
+
+        let cpi_ctx = CpiContext::new(
+            self.token_program.to_account_info(),
+            cpi_accounts,
+        );
+
+        token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?;
+
+        if args.mode == Mode::Mixed {
+            let cpi_accounts = TokenMetadataUpdateField {
+                metadata: self.mint.to_account_info(),
+                update_authority: self.payer.to_account_info(),
+                program_id: self.token_program.to_account_info(),
+            };
+            let cpi_ctx = CpiContext::new(
+                self.token_program.to_account_info(),
+                cpi_accounts,
+            );
+
+            token_metadata_update_field(
+                cpi_ctx,
+                Field::Key("threshold".to_string()),
+                args.threshold.to_string(),
+            )?;
+        }
+
+        let data = self.mint.to_account_info().data_len();
+        let min_balance = Rent::get()?.minimum_balance(data);
+        if min_balance > self.mint.to_account_info().get_lamports() {
+            invoke(
+                &transfer(
+                    &self.payer.key(),
+                    &self.mint.to_account_info().key(),
+                    min_balance - self.mint.to_account_info().get_lamports(),
+                ),
+                &[
+                    self.payer.to_account_info(),
+                    self.mint.to_account_info(),
+                    self.system_program.to_account_info(),
+                ],
+            )?;
+        }
+
+        // initialize the extra metas account
+        let extra_metas_account = &self.extra_metas_account;
+        let metas = get_extra_account_metas()?;
+        let mut data = extra_metas_account.try_borrow_mut_data()?;
+        ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &metas)?;
+
+        Ok(())
+    }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct InitMintArgs {
+    pub decimals: u8,
+    pub mint_authority: Pubkey,
+    pub freeze_authority: Pubkey,
+    pub permanent_delegate: Pubkey,
+    pub transfer_hook_authority: Pubkey,
+    pub mode: Mode,
+    pub threshold: u64,
+    pub name: String,
+    pub symbol: String,
+    pub uri: String,
+}

+ 43 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/init_wallet.rs

@@ -0,0 +1,43 @@
+use anchor_lang::prelude::*;
+
+use crate::{ABWallet, Config, AB_WALLET_SEED, CONFIG_SEED};
+
+#[derive(Accounts)]
+pub struct InitWallet<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+
+    #[account(
+        seeds = [CONFIG_SEED],
+        bump = config.bump,
+        has_one = authority,
+    )]
+    pub config: Box<Account<'info, Config>>,
+
+    pub wallet: SystemAccount<'info>,
+
+    #[account(
+        init,
+        payer = authority,
+        space = 8 + ABWallet::INIT_SPACE,
+        seeds = [AB_WALLET_SEED, wallet.key().as_ref()],
+        bump,
+    )]
+    pub ab_wallet: Account<'info, ABWallet>,
+
+    pub system_program: Program<'info, System>,
+}
+
+impl InitWallet<'_> {
+    pub fn init_wallet(&mut self, args: InitWalletArgs) -> Result<()> {
+        let ab_wallet = &mut self.ab_wallet;
+        ab_wallet.wallet = self.wallet.key();
+        ab_wallet.allowed = args.allowed;
+        Ok(())
+    }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct InitWalletArgs {
+    pub allowed: bool,
+}

+ 16 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/mod.rs

@@ -0,0 +1,16 @@
+pub mod init_mint;
+pub mod init_wallet;
+pub mod tx_hook;
+pub mod remove_wallet;
+pub mod change_mode;
+pub mod init_config;
+pub mod attach_to_mint;
+
+pub use init_mint::*;
+pub use init_wallet::*;
+pub use tx_hook::*;
+pub use remove_wallet::*;
+pub use change_mode::*;
+pub use init_config::*;
+pub use attach_to_mint::*;
+

+ 30 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/remove_wallet.rs

@@ -0,0 +1,30 @@
+use anchor_lang::prelude::*;
+
+use crate::{ABWallet, Config};
+
+#[derive(Accounts)]
+pub struct RemoveWallet<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+
+    #[account(
+        seeds = [b"config"],
+        bump = config.bump,
+        has_one = authority,
+    )]
+    pub config: Box<Account<'info, Config>>,
+
+    #[account(
+        mut,
+        close = authority,
+    )]
+    pub ab_wallet: Account<'info, ABWallet>,
+
+    pub system_program: Program<'info, System>,
+}
+
+impl RemoveWallet<'_> {
+    pub fn remove_wallet(&mut self) -> Result<()> {
+        Ok(())
+    }
+}

+ 119 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs

@@ -0,0 +1,119 @@
+use std::str::FromStr;
+
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    token_2022::spl_token_2022::{
+        extension::{BaseStateWithExtensions, StateWithExtensions},
+        state::Mint,
+    },
+    token_interface::spl_token_metadata_interface::state::TokenMetadata,
+};
+
+use crate::{ABListError, ABWallet, Mode};
+
+#[derive(Accounts)]
+pub struct TxHook<'info> {
+    /// CHECK:
+    pub source_token_account: UncheckedAccount<'info>,
+    /// CHECK:
+    pub mint: UncheckedAccount<'info>,
+    /// CHECK:
+    pub destination_token_account: UncheckedAccount<'info>,
+    /// CHECK:
+    pub owner_delegate: UncheckedAccount<'info>,
+    /// CHECK:
+    pub meta_list: UncheckedAccount<'info>,
+    /// CHECK:
+    pub ab_wallet: UncheckedAccount<'info>,
+}
+
+impl TxHook<'_> {
+    pub fn tx_hook(&self, amount: u64) -> Result<()> {
+        let mint_info = self.mint.to_account_info();
+        let mint_data = mint_info.data.borrow();
+        let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
+
+        let metadata = mint.get_variable_len_extension::<TokenMetadata>()?;
+        let decoded_mode = Self::decode_metadata(&metadata)?;
+        let decoded_wallet_mode = self.decode_wallet_mode()?;
+
+        match (decoded_mode, decoded_wallet_mode) {
+            // first check the force allow modes
+            (DecodedMintMode::Allow, DecodedWalletMode::Allow) => Ok(()),
+            (DecodedMintMode::Allow, _) => Err(ABListError::WalletNotAllowed.into()),
+            // then check if the wallet is blocked
+            (_, DecodedWalletMode::Block) => Err(ABListError::WalletBlocked.into()),
+            (DecodedMintMode::Block, _) => Ok(()),
+            // lastly check the threshold mode
+            (DecodedMintMode::Threshold(threshold), DecodedWalletMode::None)
+                if amount >= threshold =>
+            {
+                Err(ABListError::AmountNotAllowed.into())
+            }
+            (DecodedMintMode::Threshold(_), _) => Ok(()),
+        }
+    }
+
+    fn decode_wallet_mode(&self) -> Result<DecodedWalletMode> {
+        if self.ab_wallet.data_is_empty() {
+            return Ok(DecodedWalletMode::None);
+        }
+
+        let wallet_data = &mut self.ab_wallet.data.borrow();
+        let wallet = ABWallet::try_deserialize(&mut &wallet_data[..])?;
+
+        if wallet.allowed {
+            Ok(DecodedWalletMode::Allow)
+        } else {
+            Ok(DecodedWalletMode::Block)
+        }
+    }
+
+    fn decode_metadata(metadata: &TokenMetadata) -> Result<DecodedMintMode> {
+        let mut mode = Mode::Allow;
+        let mut threshold = 0;
+
+        for (key, value) in metadata.additional_metadata.iter() {
+            if key == "AB" {
+                mode = Mode::from_str(value).map_err(|_| ABListError::InvalidMetadata)?;
+                if mode == Mode::Allow {
+                    return Ok(DecodedMintMode::Allow);
+                } else if mode == Mode::Block {
+                    return Ok(DecodedMintMode::Block);
+                } else if mode == Mode::Mixed && threshold > 0 {
+                    return Ok(DecodedMintMode::Threshold(threshold));
+                }
+            } else if key == "threshold" {
+                threshold = u64::from_str(value).map_err(|_| ABListError::InvalidMetadata)?;
+                if threshold > 0 {
+                    return Ok(DecodedMintMode::Threshold(threshold));
+                }
+            }
+        }
+
+        // we have early returns above, but we can reach here if metadata is meddled with
+        // which is why we have this fallback
+        // also, anchor doesn't yet support removing keys from metadata, which means that if we set threshold, we can never remove the KV pair
+        // only set it to 0
+
+        if mode == Mode::Allow {
+            return Ok(DecodedMintMode::Allow);
+        } else if mode == Mode::Block {
+            return Ok(DecodedMintMode::Block);
+        }
+
+        Ok(DecodedMintMode::Threshold(threshold))
+    }
+}
+
+enum DecodedMintMode {
+    Allow,
+    Block,
+    Threshold(u64),
+}
+
+enum DecodedWalletMode {
+    Allow,
+    Block,
+    None,
+}

+ 51 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/lib.rs

@@ -0,0 +1,51 @@
+use anchor_lang::prelude::*;
+use spl_discriminator::SplDiscriminate;
+use spl_transfer_hook_interface::instruction::ExecuteInstruction;
+
+pub mod errors;
+pub mod instructions;
+pub mod state;
+pub mod constants;
+pub mod utils;
+pub use errors::*;
+pub use instructions::*;
+pub use state::*;
+pub use constants::*;
+pub use utils::*;
+
+declare_id!("LtkoMwPSKxAE714EY3V1oAEQ5LciqJcRwQQuQnzEhQQ");
+
+#[program]
+pub mod abl_token {
+
+    use super::*;
+
+    pub fn init_mint(ctx: Context<InitMint>, args: InitMintArgs) -> Result<()> {
+        ctx.accounts.init_mint(args)
+    }
+
+    pub fn init_config(ctx: Context<InitConfig>) -> Result<()> {
+        ctx.accounts.init_config(ctx.bumps.config)
+    }
+
+    pub fn attach_to_mint(ctx: Context<AttachToMint>, args: AttachToMintArgs) -> Result<()> {
+        ctx.accounts.attach_to_mint(args)
+    }
+
+    #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)]
+    pub fn tx_hook(ctx: Context<TxHook>, amount: u64) -> Result<()> {
+        ctx.accounts.tx_hook(amount)
+    }
+
+    pub fn init_wallet(ctx: Context<InitWallet>, args: InitWalletArgs) -> Result<()> {
+        ctx.accounts.init_wallet(args)
+    }
+
+    pub fn remove_wallet(ctx: Context<RemoveWallet>) -> Result<()> {
+        ctx.accounts.remove_wallet()
+    }
+
+    pub fn change_mode(ctx: Context<ChangeMode>, args: ChangeModeArgs) -> Result<()> {
+        ctx.accounts.change_mode(args)
+    }
+}

+ 48 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/state.rs

@@ -0,0 +1,48 @@
+use std::{fmt::{self, Display}, str::FromStr};
+
+use anchor_lang::prelude::*;
+
+#[account]
+#[derive(InitSpace)]
+pub struct ABWallet {
+    pub wallet: Pubkey,
+    pub allowed: bool,
+}
+
+#[account]
+#[derive(InitSpace)]
+pub struct Config {
+    pub authority: Pubkey,
+    pub bump: u8,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, PartialEq)]
+pub enum Mode {
+    Allow,
+    Block,
+    Mixed
+}
+
+impl FromStr for Mode {
+    type Err = ();
+
+    fn from_str(s: &str) -> std::result::Result<Mode, ()> {
+        match s {
+            "Allow" => Ok(Mode::Allow),
+            "Block" => Ok(Mode::Block),
+            "Mixed" => Ok(Mode::Mixed),
+            _ => Err(()),
+        }
+    }
+}
+
+impl Display for Mode {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Mode::Allow => write!(f, "Allow"),
+            Mode::Block => write!(f, "Block"),
+            Mode::Mixed => write!(f, "Mixed"),
+        }
+    }
+}
+

+ 34 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs

@@ -0,0 +1,34 @@
+use anchor_lang::prelude::*;
+
+use spl_tlv_account_resolution::{
+    account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
+};
+
+use crate::AB_WALLET_SEED;
+
+
+
+
+pub fn get_meta_list_size() -> Result<usize> {
+    Ok(ExtraAccountMetaList::size_of(1).unwrap())
+}
+
+pub fn get_extra_account_metas() -> Result<Vec<ExtraAccountMeta>> {
+    Ok(vec![
+        // [5] ab_wallet for destination token account wallet
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: AB_WALLET_SEED.to_vec(),
+                },
+                Seed::AccountData {
+                    account_index: 2,
+                    data_index: 32,
+                    length: 32,
+                },
+            ],
+            false,
+            false,
+        )?, // [2] destination token account
+    ])
+}

+ 111 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test.rs

@@ -0,0 +1,111 @@
+use {
+    anchor_lang::ToAccountMetas, anchor_lang::InstructionData, solana_message::Message,
+    abl_token::{accounts::InitMint, accounts::InitConfig, instructions::InitMintArgs, Mode}, litesvm::LiteSVM, solana_instruction::Instruction, solana_keypair::Keypair, solana_native_token::LAMPORTS_PER_SOL, solana_pubkey::{pubkey, Pubkey}, solana_sdk_ids::system_program::ID as SYSTEM_PROGRAM_ID, solana_signer::Signer, solana_transaction::Transaction, spl_token_2022::ID as TOKEN_22_PROGRAM_ID, std::path::PathBuf
+};
+
+const PROGRAM_ID: Pubkey = abl_token::ID_CONST;
+
+fn setup() -> (LiteSVM, Keypair) {
+    let mut svm = LiteSVM::new();
+    let admin_kp = Keypair::new();
+    let admin_pk = admin_kp.pubkey();
+
+    svm.airdrop(&admin_pk, 10000 * LAMPORTS_PER_SOL).unwrap();
+
+
+    let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+    so_path.push("../../target/deploy/abl_token.so");
+
+    println!("Deploying program from {}", so_path.display());
+    
+    let bytecode = std::fs::read(so_path).unwrap();
+
+    svm.add_program(PROGRAM_ID, &bytecode);
+
+    (svm, admin_kp)
+}
+
+#[test]
+fn test() {
+
+    let (mut svm, admin_kp) = setup();
+    let admin_pk = admin_kp.pubkey();
+
+    let mint_kp = Keypair::new();
+    let mint_pk = mint_kp.pubkey();
+    let config = derive_config();
+    let meta_list = derive_meta_list(&mint_pk);
+
+    let init_cfg_ix = abl_token::instruction::InitConfig {   };
+
+    let init_cfg_accounts = InitConfig {
+        payer: admin_pk,
+        config: config,
+        system_program: SYSTEM_PROGRAM_ID,
+    };
+
+    let accs = init_cfg_accounts.to_account_metas(None);
+
+    let instruction = Instruction {
+        program_id: PROGRAM_ID,
+        accounts: accs,
+        data: init_cfg_ix.data(),
+    };
+    let msg = Message::new(&[instruction], Some(&admin_pk));
+    let tx = Transaction::new(&[&admin_kp], msg, svm.latest_blockhash());
+
+    svm.send_transaction(tx).unwrap();
+
+    let args: InitMintArgs = InitMintArgs {
+        name: "Test".to_string(),
+        symbol: "TEST".to_string(),
+        uri: "https://test.com".to_string(),
+        decimals: 6,
+        mint_authority: mint_pk,
+        freeze_authority: mint_pk,
+        permanent_delegate: mint_pk,
+        transfer_hook_authority: admin_pk,
+        mode: Mode::Mixed,
+        threshold: 100000,
+    };
+    let init_mint_ix = abl_token::instruction::InitMint {
+        args: args,
+    };
+
+    let data = init_mint_ix.data();
+
+    let init_mint_accounts = InitMint {
+        payer: admin_pk,
+        mint: mint_pk,
+        extra_metas_account: meta_list,
+        system_program: SYSTEM_PROGRAM_ID,
+        token_program: TOKEN_22_PROGRAM_ID,
+    };
+
+    let accs = init_mint_accounts.to_account_metas(None);
+
+    let instruction = Instruction {
+        program_id: PROGRAM_ID,
+        accounts: accs,
+        data: data,
+    };
+    let msg = Message::new(&[instruction], Some(&admin_pk));
+    let tx = Transaction::new(&[&admin_kp, &mint_kp], msg, svm.latest_blockhash());
+
+    let _res = svm.send_transaction(tx).unwrap();
+
+    
+
+}
+
+fn derive_config() -> Pubkey {
+    let seeds = &[b"config".as_ref()];
+    Pubkey::find_program_address(seeds, &PROGRAM_ID).0
+}
+
+fn derive_meta_list(mint: &Pubkey) -> Pubkey {
+    let seeds = &[b"extra-account-metas", mint.as_ref()];
+    Pubkey::find_program_address(seeds, &PROGRAM_ID).0
+}
+
+

+ 31 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/src/abl-token-exports.ts

@@ -0,0 +1,31 @@
+// Here we export some useful types and functions for interacting with the Anchor program.
+import { AnchorProvider, Program } from '@coral-xyz/anchor'
+import { Cluster, PublicKey } from '@solana/web3.js'
+import ABLTokenIDL from '../target/idl/abl_token.json'
+import type { AblToken } from '../target/types/abl_token'
+
+// Re-export the generated IDL and type
+export { ABLTokenIDL }
+
+// The programId is imported from the program IDL.
+export const ABL_TOKEN_PROGRAM_ID = new PublicKey(ABLTokenIDL.address)
+
+// This is a helper function to get the Basic Anchor program.
+export function getABLTokenProgram(provider: AnchorProvider, address?: PublicKey): Program<AblToken> {
+  return new Program({ ...ABLTokenIDL, address: address ? address.toBase58() : ABLTokenIDL.address } as AblToken, provider)
+}
+
+// This is a helper function to get the program ID for the Basic program depending on the cluster.
+export function getABLTokenProgramId(cluster: Cluster) {
+  switch (cluster) {
+    case 'devnet':
+    case 'testnet':
+      // This is the program ID for the Basic program on devnet and testnet.
+      return new PublicKey('6z68wfurCMYkZG51s1Et9BJEd9nJGUusjHXNt4dGbNNF')
+    case 'mainnet-beta':
+    default:
+      return ABL_TOKEN_PROGRAM_ID
+  }
+}
+
+//ABLTokenIDL.types["mode"]

+ 1 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/src/index.ts

@@ -0,0 +1 @@
+export * from './abl-token-exports'

+ 14 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/tests/basic.test.ts

@@ -0,0 +1,14 @@
+import * as anchor from '@coral-xyz/anchor'
+import { Program } from '@coral-xyz/anchor'
+import { AblToken } from '../target/types/abl_token'
+
+describe('abl-token', () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.AnchorProvider.env())
+
+  const program = anchor.workspace.ABLToken as Program<AblToken>
+
+  it('should run the program', async () => {
+    // Add your test here.
+  })
+})

+ 13 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "module": "commonjs",
+    "types": ["jest", "node"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "target": "es6",
+    "resolveJsonModule": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true
+  }
+}

+ 21 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/components.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "src/app/globals.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  },
+  "iconLibrary": "lucide"
+}

+ 14 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/eslint.config.mjs

@@ -0,0 +1,14 @@
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+import { FlatCompat } from '@eslint/eslintrc'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+})
+
+const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')]
+
+export default eslintConfig

+ 7 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/next.config.ts

@@ -0,0 +1,7 @@
+import type { NextConfig } from 'next'
+
+const nextConfig: NextConfig = {
+  /* config options here */
+}
+
+export default nextConfig

+ 59 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/package.json

@@ -0,0 +1,59 @@
+{
+  "name": "legacy-next-tailwind-basic",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "anchor": "cd anchor && anchor",
+    "anchor-build": "cd anchor && anchor build",
+    "anchor-localnet": "cd anchor && anchor localnet",
+    "anchor-test": "cd anchor && anchor test",
+    "build": "next build",
+    "ci": "npm run build && npm run lint && npm run format:check",
+    "dev": "next dev --turbopack",
+    "format": "prettier --write .",
+    "format:check": "prettier --check .",
+    "lint": "next lint",
+    "start": "next start"
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.31.1",
+    "@radix-ui/react-dialog": "^1.1.11",
+    "@radix-ui/react-dropdown-menu": "^2.1.12",
+    "@radix-ui/react-label": "^2.1.4",
+    "@radix-ui/react-slot": "^1.2.0",
+    "@solana/spl-token": "0.4.13",
+    "@solana/wallet-adapter-base": "0.9.26",
+    "@solana/wallet-adapter-react": "0.15.38",
+    "@solana/wallet-adapter-react-ui": "0.9.38",
+    "@solana/web3.js": "1.98.2",
+    "@tanstack/react-query": "^5.74.7",
+    "bigint-buffer": "^1.1.5",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "jotai": "^2.12.3",
+    "lucide-react": "^0.503.0",
+    "next": "15.3.1",
+    "next-themes": "^0.4.6",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "sonner": "^2.0.3",
+    "tailwind-merge": "^3.2.0",
+    "tw-animate-css": "^1.2.8"
+  },
+  "devDependencies": {
+    "@eslint/eslintrc": "^3.3.1",
+    "@tailwindcss/postcss": "^4.1.4",
+    "@types/bn.js": "^5.1.6",
+    "@types/jest": "^29.5.14",
+    "@types/node": "^22.15.3",
+    "@types/react": "^19.1.2",
+    "@types/react-dom": "^19.1.2",
+    "eslint": "^9.25.1",
+    "eslint-config-next": "15.3.1",
+    "jest": "^29.7.0",
+    "prettier": "^3.5.3",
+    "tailwindcss": "^4.1.4",
+    "ts-jest": "^29.3.2",
+    "typescript": "^5.8.3"
+  }
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/postcss.config.mjs

@@ -0,0 +1,5 @@
+const config = {
+  plugins: ['@tailwindcss/postcss'],
+}
+
+export default config

+ 0 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/public/.gitkeep


+ 15 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/scripts/start.sh

@@ -0,0 +1,15 @@
+#! /bin/bash
+
+# get root directory
+ROOT_DIR=$(cd $(dirname $0)/.. && pwd)
+
+ROOT_ANCHOR_DIR=$ROOT_DIR/anchor
+
+cd $ROOT_ANCHOR_DIR
+
+solana-test-validator --reset --ledger $ROOT_ANCHOR_DIR/test-ledger --quiet &
+
+# wait for validator to start
+sleep 5
+
+anchor deploy --provider.cluster localnet

+ 4 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/scripts/stop.sh

@@ -0,0 +1,4 @@
+#! /bin/bash
+
+#
+ps ax | grep solana-test-validator | grep -v grep | awk '{print $1}' | xargs kill -9

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/account/[address]/page.tsx

@@ -0,0 +1,5 @@
+import AccountDetailFeature from '@/components/account/account-detail-feature'
+
+export default function Page() {
+  return <AccountDetailFeature />
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/account/page.tsx

@@ -0,0 +1,5 @@
+import AccountListFeature from '@/components/account/account-list-feature'
+
+export default function Page() {
+  return <AccountListFeature />
+}

+ 6 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/config/page.tsx

@@ -0,0 +1,6 @@
+
+import AblTokenConfig from '@/components/abl-token/abl-token-config'
+
+export default function Page() {
+  return <AblTokenConfig />
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/create-token/page.tsx

@@ -0,0 +1,5 @@
+import AblTokenFeature from '@/components/abl-token/abl-token-feature'
+
+export default function Page() {
+  return <AblTokenFeature />
+}

BIN
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/favicon.ico


+ 127 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/globals.css

@@ -0,0 +1,127 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-sidebar-ring: var(--sidebar-ring);
+  --color-sidebar-border: var(--sidebar-border);
+  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+  --color-sidebar-accent: var(--sidebar-accent);
+  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+  --color-sidebar-primary: var(--sidebar-primary);
+  --color-sidebar-foreground: var(--sidebar-foreground);
+  --color-sidebar: var(--sidebar);
+  --color-chart-5: var(--chart-5);
+  --color-chart-4: var(--chart-4);
+  --color-chart-3: var(--chart-3);
+  --color-chart-2: var(--chart-2);
+  --color-chart-1: var(--chart-1);
+  --color-ring: var(--ring);
+  --color-input: var(--input);
+  --color-border: var(--border);
+  --color-destructive: var(--destructive);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-accent: var(--accent);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-muted: var(--muted);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-secondary: var(--secondary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-primary: var(--primary);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-popover: var(--popover);
+  --color-card-foreground: var(--card-foreground);
+  --color-card: var(--card);
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+  --radius: 0.625rem;
+  --background: oklch(1 0 0);
+  --foreground: oklch(0.145 0 0);
+  --card: oklch(1 0 0);
+  --card-foreground: oklch(0.145 0 0);
+  --popover: oklch(1 0 0);
+  --popover-foreground: oklch(0.145 0 0);
+  --primary: oklch(0.205 0 0);
+  --primary-foreground: oklch(0.985 0 0);
+  --secondary: oklch(0.97 0 0);
+  --secondary-foreground: oklch(0.205 0 0);
+  --muted: oklch(0.97 0 0);
+  --muted-foreground: oklch(0.556 0 0);
+  --accent: oklch(0.97 0 0);
+  --accent-foreground: oklch(0.205 0 0);
+  --destructive: oklch(0.577 0.245 27.325);
+  --border: oklch(0.922 0 0);
+  --input: oklch(0.922 0 0);
+  --ring: oklch(0.708 0 0);
+  --chart-1: oklch(0.646 0.222 41.116);
+  --chart-2: oklch(0.6 0.118 184.704);
+  --chart-3: oklch(0.398 0.07 227.392);
+  --chart-4: oklch(0.828 0.189 84.429);
+  --chart-5: oklch(0.769 0.188 70.08);
+  --sidebar: oklch(0.985 0 0);
+  --sidebar-foreground: oklch(0.145 0 0);
+  --sidebar-primary: oklch(0.205 0 0);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.97 0 0);
+  --sidebar-accent-foreground: oklch(0.205 0 0);
+  --sidebar-border: oklch(0.922 0 0);
+  --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+  --background: oklch(0.145 0 0);
+  --foreground: oklch(0.985 0 0);
+  --card: oklch(0.205 0 0);
+  --card-foreground: oklch(0.985 0 0);
+  --popover: oklch(0.205 0 0);
+  --popover-foreground: oklch(0.985 0 0);
+  --primary: oklch(0.922 0 0);
+  --primary-foreground: oklch(0.205 0 0);
+  --secondary: oklch(0.269 0 0);
+  --secondary-foreground: oklch(0.985 0 0);
+  --muted: oklch(0.269 0 0);
+  --muted-foreground: oklch(0.708 0 0);
+  --accent: oklch(0.269 0 0);
+  --accent-foreground: oklch(0.985 0 0);
+  --destructive: oklch(0.704 0.191 22.216);
+  --border: oklch(1 0 0 / 10%);
+  --input: oklch(1 0 0 / 15%);
+  --ring: oklch(0.556 0 0);
+  --chart-1: oklch(0.488 0.243 264.376);
+  --chart-2: oklch(0.696 0.17 162.48);
+  --chart-3: oklch(0.769 0.188 70.08);
+  --chart-4: oklch(0.627 0.265 303.9);
+  --chart-5: oklch(0.645 0.246 16.439);
+  --sidebar: oklch(0.205 0 0);
+  --sidebar-foreground: oklch(0.985 0 0);
+  --sidebar-primary: oklch(0.488 0.243 264.376);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.269 0 0);
+  --sidebar-accent-foreground: oklch(0.985 0 0);
+  --sidebar-border: oklch(1 0 0 / 10%);
+  --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+
+  body {
+    @apply bg-background text-foreground;
+  }
+}
+
+.wallet-adapter-button-trigger {
+  height: auto !important;
+  @apply !border !bg-background !shadow-xs hover:!bg-accent !text-accent-foreground hover:!text-accent-foreground dark:!bg-input/30 !border-input/10 dark:!border-input dark:hover:!bg-input/50;
+  @apply !px-2 !py-[6px] !rounded-md !text-sm !font-semibold !shadow-sm !transition-all;
+}

+ 41 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/layout.tsx

@@ -0,0 +1,41 @@
+import type { Metadata } from 'next'
+import './globals.css'
+import { AppProviders } from '@/components/app-providers'
+import { AppLayout } from '@/components/app-layout'
+import React from 'react'
+
+export const metadata: Metadata = {
+  title: 'ABL Token',
+  description: 'ABL Token',
+}
+
+const links: { label: string; path: string }[] = [
+  // More links...
+  { label: 'Home', path: '/' },
+  { label: 'Account', path: '/account' },
+  { label: 'Config', path: '/config' },
+  { label: 'Create New Token', path: '/create-token' },
+  { label: 'Manage Token', path: '/manage-token' },
+]
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+  return (
+    <html lang="en" suppressHydrationWarning>
+      <body className={`antialiased`}>
+        <AppProviders>
+          <AppLayout links={links}>{children}</AppLayout>
+        </AppProviders>
+      </body>
+    </html>
+  )
+}
+// Patch BigInt so we can log it using JSON.stringify without any errors
+declare global {
+  interface BigInt {
+    toJSON(): string
+  }
+}
+
+BigInt.prototype.toJSON = function () {
+  return this.toString()
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/manage-token/[address]/page.tsx

@@ -0,0 +1,5 @@
+import AblTokenManageTokenDetail from '@/components/abl-token/abl-token-manage-token-detail'
+
+export default function Page() {
+  return <AblTokenManageTokenDetail />
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/manage-token/page.tsx

@@ -0,0 +1,5 @@
+import AblTokenManageToken from '@/components/abl-token/abl-token-manage-token'
+
+export default function Page() {
+  return <AblTokenManageToken />
+}

+ 5 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/app/page.tsx

@@ -0,0 +1,5 @@
+import { DashboardFeature } from '@/components/dashboard/dashboard-feature'
+
+export default function Home() {
+  return <DashboardFeature />
+}

+ 360 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-config.tsx

@@ -0,0 +1,360 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import { WalletButton } from '../solana/solana-provider'
+import { useAblTokenProgram } from './abl-token-data-access'
+import { AblTokenCreate, AblTokenProgram } from './abl-token-ui'
+import { AppHero } from '../app-hero'
+import { ellipsify } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import React from 'react'
+import { PublicKey } from '@solana/web3.js'
+
+export default function AblTokenConfig() {
+  const { publicKey } = useWallet()
+  const { programId, getConfig, getAbWallets } = useAblTokenProgram()
+  const [lastUpdate, setLastUpdate] = React.useState(0)
+
+  const config = getConfig.data;
+  let abWallets = getAbWallets.data;
+
+  const handleWalletListUpdate = React.useCallback(async () => {
+    await getAbWallets.refetch();
+    abWallets = getAbWallets.data;
+    setLastUpdate(Date.now())
+  }, [])
+
+  return publicKey ? (
+    <div>
+      <AppHero title="ABL Token Config" subtitle={''}>
+        <p className="mb-6">
+          <ExplorerLink path={`account/${programId}`} label={ellipsify(programId.toString())} />
+        </p>
+        {config ? (
+          <>
+            <div className="mb-16">
+              <AblTokenConfigList abWallets={abWallets} />
+            </div>
+            <div className="mb-16">
+              {config.authority.equals(publicKey) ? (
+                <AblTokenConfigListChange onWalletListUpdate={handleWalletListUpdate} />
+              ) : (
+                <div className="text-destructive font-semibold">
+                  UNAUTHORIZED: Only the config authority can modify the wallet list
+                </div>
+              )}
+            </div>
+          </>
+        ) : (
+          <AblTokenConfigCreate />
+        )}
+      </AppHero>
+    </div>
+  ) : (
+    <div className="max-w-4xl mx-auto">
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton className="btn btn-primary" />
+        </div>
+      </div>
+    </div>
+  )
+}
+
+
+export function AblTokenConfigCreate() {
+  const { initConfig, getConfig } = useAblTokenProgram()
+  const { publicKey } = useWallet()
+
+  const handleCreate = async () => {
+    if (!publicKey) return;
+    try {
+      await initConfig.mutateAsync();
+      // Refresh the config list
+      getConfig.refetch();
+    } catch (err) {
+      console.error('Failed to create config:', err);
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      <h2 className="text-2xl font-bold">Create ABL Token Config</h2>
+      <p className="text-gray-600">
+        Initialize the ABL Token configuration. This will set up the necessary accounts for managing allow/block lists.
+      </p>
+      <Button 
+        onClick={handleCreate}
+        disabled={initConfig.isPending}
+      >
+        {initConfig.isPending ? 'Creating...' : 'Create Config'}
+      </Button>
+    </div>
+  );
+}
+
+export function AblTokenConfigList({ abWallets }: { abWallets: any[] | undefined }) {
+  return (
+    <div className="space-y-4">
+      <h2 className="text-2xl font-bold">ABL Token Config List</h2>
+      {abWallets && abWallets.length > 0 ? (
+        <div className="overflow-x-auto">
+          <table className="table w-full">
+            <thead>
+              <tr>
+                <th>Wallet Address</th>
+                <th>Status</th>
+              </tr>
+            </thead>
+            <tbody>
+              {abWallets.map((wallet) => (
+                <tr key={wallet.publicKey.toString()}>
+                  <td className="font-mono">{wallet.account.wallet.toString()}</td>
+                  <td>
+                    <span className={`badge ${wallet.account.allowed ? 'badge-success' : 'badge-error'}`}>
+                      {wallet.account.allowed ? 'Allowed' : 'Blocked'}
+                    </span>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      ) : (
+        <p>No wallets configured yet.</p>
+      )}
+    </div>
+  );
+}
+
+interface WalletChange {
+  address: string;
+  mode: 'allow' | 'block' | 'remove';
+  status?: 'pending' | 'success' | 'error';
+  error?: string;
+}
+
+export function AblTokenConfigListChange({ onWalletListUpdate }: { onWalletListUpdate: () => void }) {
+  const { getAbWallets, processBatchWallets } = useAblTokenProgram()
+  const [isEditing, setIsEditing] = React.useState(false)
+  const [walletChanges, setWalletChanges] = React.useState<WalletChange[]>([])
+  const [isProcessing, setIsProcessing] = React.useState(false)
+  const existingWallets = React.useMemo(() => {
+    const wallets = getAbWallets.data || []
+    return new Map(wallets.map(w => [w.account.wallet.toString(), w.account.allowed]))
+  }, [getAbWallets.data])
+
+  const handleDragOver = (e: React.DragEvent) => {
+    e.preventDefault()
+  }
+
+  const handleDrop = async (e: React.DragEvent) => {
+    e.preventDefault()
+    const file = e.dataTransfer.files[0]
+    if (file && file.type === 'text/csv') {
+      const text = await file.text()
+      const rows = text.split('\n')
+      
+      // Create a Set of existing wallet addresses for deduplication
+      const existingAddresses = new Set([
+        ...Array.from(existingWallets.keys()),
+        ...walletChanges.map(w => w.address)
+      ])
+      
+      const parsed: WalletChange[] = rows
+        .filter(row => row.trim())
+        .map(row => {
+          const [address, mode] = row.split(',').map(field => field.trim())
+          return {
+            address,
+            mode: mode.toLowerCase() as 'allow' | 'block' | 'remove'
+          }
+        })
+        .filter(entry => {
+          try {
+            new PublicKey(entry.address)
+            return ['allow', 'block', 'remove'].includes(entry.mode)
+          } catch {
+            return false
+          }
+        })
+        .filter(entry => {
+          // Only allow 'remove' for existing wallets
+          if (entry.mode === 'remove') {
+            return existingWallets.has(entry.address)
+          }
+          return true
+        })
+        // Deduplicate entries, keeping the last occurrence of each address
+        .reduce((acc, entry) => {
+          const existingIndex = acc.findIndex(w => w.address === entry.address)
+          if (existingIndex >= 0) {
+            acc[existingIndex] = entry
+          } else {
+            acc.push(entry)
+          }
+          return acc
+        }, [] as WalletChange[])
+        // Filter out entries that already exist in the current state
+        .filter(entry => !existingAddresses.has(entry.address))
+
+      if (parsed.length > 0) {
+        setWalletChanges(prev => [...prev, ...parsed])
+        setIsEditing(true)
+      }
+    }
+  }
+
+  const handleAddWallet = () => {
+    setWalletChanges(prev => [...prev, { address: '', mode: 'allow' }])
+    setIsEditing(true)
+  }
+
+  const handleUpdateWallet = (index: number, field: keyof WalletChange, value: string) => {
+    setWalletChanges(prev => prev.map((wallet, i) => 
+      i === index ? { ...wallet, [field]: value } : wallet
+    ))
+  }
+
+  const handleRemoveWallet = (index: number) => {
+    setWalletChanges(prev => prev.filter((_, i) => i !== index))
+  }
+
+  const processWallets = async () => {
+    setIsProcessing(true)
+    const batchSize = 10
+    const batches = []
+    
+    for (let i = 0; i < walletChanges.length; i += batchSize) {
+      batches.push(walletChanges.slice(i, i + batchSize))
+    }
+
+    for (const batch of batches) {
+      try {
+        await processBatchWallets.mutateAsync({
+          wallets: batch.map(w => ({
+            wallet: new PublicKey(w.address),
+            mode: w.mode
+          }))
+        })
+        
+        // Mark batch as successful
+        setWalletChanges(prev => prev.map(wallet => 
+          batch.some(b => b.address === wallet.address) 
+            ? { ...wallet, status: 'success' }
+            : wallet
+        ))
+      } catch (error: unknown) {
+        const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
+        // Mark batch as failed
+        setWalletChanges(prev => prev.map(wallet => 
+          batch.some(b => b.address === wallet.address) 
+            ? { ...wallet, status: 'error', error: errorMessage }
+            : wallet
+        ))
+      }
+    }
+
+    // Refresh wallet list and clear successful changes
+    await getAbWallets.refetch()
+    setWalletChanges(prev => prev.filter(w => w.status !== 'success'))
+    setIsProcessing(false)
+    // Notify parent component to update the wallet list
+    onWalletListUpdate()
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h2 className="text-2xl font-bold">Edit Wallet List</h2>
+        <div className="space-x-2">
+          <Button onClick={handleAddWallet} disabled={isProcessing}>
+            Add Wallet
+          </Button>
+          {isEditing && (
+            <Button 
+              onClick={processWallets} 
+              disabled={isProcessing || walletChanges.length === 0}
+            >
+              {isProcessing ? 'Processing...' : 'Apply Changes'}
+            </Button>
+          )}
+        </div>
+      </div>
+
+      <div 
+        onDragOver={handleDragOver}
+        onDrop={handleDrop}
+        className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary cursor-pointer mb-4"
+      >
+        Drop CSV file here (address,mode)
+        <p className="text-sm text-gray-500 mt-2">
+          Mode can be: allow, block, or remove (remove only works for existing wallets)
+        </p>
+      </div>
+
+      {walletChanges.length > 0 && (
+        <div className="overflow-x-auto">
+          <table className="table w-full">
+            <thead>
+              <tr>
+                <th>Wallet Address</th>
+                <th>Mode</th>
+                <th>Status</th>
+                <th>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              {walletChanges.map((wallet, index) => (
+                <tr key={index}>
+                  <td>
+                    <input
+                      type="text"
+                      className="input input-bordered w-full"
+                      value={wallet.address}
+                      onChange={e => handleUpdateWallet(index, 'address', e.target.value)}
+                      placeholder="Wallet address"
+                      disabled={isProcessing}
+                    />
+                  </td>
+                  <td>
+                    <select
+                      className="select select-bordered w-full"
+                      value={wallet.mode}
+                      onChange={e => handleUpdateWallet(index, 'mode', e.target.value as 'allow' | 'block' | 'remove')}
+                      disabled={isProcessing}
+                    >
+                      <option value="allow">Allow</option>
+                      <option value="block">Block</option>
+                      {existingWallets.has(wallet.address) && (
+                        <option value="remove">Remove</option>
+                      )}
+                    </select>
+                  </td>
+                  <td>
+                    {wallet.status === 'success' && (
+                      <span className="badge badge-success">✓</span>
+                    )}
+                    {wallet.status === 'error' && (
+                      <span className="badge badge-error" title={wallet.error}>✗</span>
+                    )}
+                  </td>
+                  <td>
+                    <Button
+                      onClick={() => handleRemoveWallet(index)}
+                      disabled={isProcessing}
+                      variant="ghost"
+                    >
+                      Remove
+                    </Button>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      )}
+    </div>
+  )
+}

+ 330 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-data-access.tsx

@@ -0,0 +1,330 @@
+'use client'
+
+import { getABLTokenProgram, getABLTokenProgramId } from '@project/anchor'
+import { useConnection } from '@solana/wallet-adapter-react'
+import { Cluster, Keypair, PublicKey, Transaction } from '@solana/web3.js'
+import { useMutation, useQuery } from '@tanstack/react-query'
+import { useMemo } from 'react'
+import { useCluster } from '../cluster/cluster-data-access'
+import { useAnchorProvider } from '../solana/solana-provider'
+import { useTransactionToast } from '../use-transaction-toast'
+import { toast } from 'sonner'
+import { BN } from '@coral-xyz/anchor'
+import { amountToUiAmount, createAssociatedTokenAccountIdempotentInstruction, createAssociatedTokenAccountIdempotentInstructionWithDerivation, createMintToCheckedInstruction, decodeMintToCheckedInstruction, getAssociatedTokenAddressSync, getMint, getPermanentDelegate, getTokenMetadata, getTransferHook, mintToChecked, mintToCheckedInstructionData, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'
+
+
+export function useHasTransferHookEnabled(mint: PublicKey) {
+  const { connection } = useConnection()
+  const provider = useAnchorProvider()
+  const { cluster } = useCluster()
+  const programId = useMemo(() => getABLTokenProgramId(cluster.network as Cluster), [cluster])
+  const program = useMemo(() => getABLTokenProgram(provider, programId), [provider, programId])
+  return useQuery({
+    queryKey: ['has-transfer-hook', { cluster }],
+    queryFn: async () => {
+      const mintInfo = await getMint(
+        connection,
+        mint,
+        "confirmed",
+        TOKEN_2022_PROGRAM_ID,
+        );
+      const transferHook = getTransferHook(mintInfo);
+      return transferHook !== null && programId.equals(transferHook.programId);
+    },
+  })
+}
+export function useAblTokenProgram() {
+  const { connection } = useConnection()
+  const { cluster } = useCluster()
+  const transactionToast = useTransactionToast()
+  const provider = useAnchorProvider()
+  const programId = useMemo(() => getABLTokenProgramId(cluster.network as Cluster), [cluster])
+  const program = useMemo(() => getABLTokenProgram(provider, programId), [provider, programId])
+
+  const getProgramAccount = useQuery({
+    queryKey: ['get-program-account', { cluster }],
+    queryFn: () => connection.getParsedAccountInfo(programId),
+  })
+
+  const initToken = useMutation({
+    mutationKey: ['abl-token', 'init-token', { cluster }],
+    mutationFn: (args: {
+      mintAuthority: PublicKey,
+      freezeAuthority: PublicKey,
+      permanentDelegate: PublicKey,
+      transferHookAuthority: PublicKey,
+      mode: string,
+      threshold: BN,
+      name: string,
+      symbol: string,
+      uri: string,
+      decimals: number,
+    }) => {
+      const modeEnum = args.mode === 'allow' ? { allow: {} } : args.mode === 'block' ? { block: {}} : { mixed: {}};
+      const mint = Keypair.generate();
+
+      return program.methods.initMint({
+        decimals: args.decimals,
+        mintAuthority: args.mintAuthority,
+        freezeAuthority: args.freezeAuthority,
+        permanentDelegate: args.permanentDelegate,
+        transferHookAuthority: args.mintAuthority,
+        mode: modeEnum,
+        threshold: args.threshold,
+        name: args.name,
+        symbol: args.symbol,
+        uri: args.uri,
+      }).accounts({
+        mint: mint.publicKey,
+      }).signers([mint]).rpc().then((signature) => ({ signature, mintAddress: mint.publicKey }))
+    },
+    onSuccess: ({ signature, mintAddress }) => {
+      transactionToast(signature)
+      window.location.href = `/manage-token/${mintAddress.toString()}`
+    },
+    onError: () => toast.error('Failed to initialize token'),
+  })
+
+  const attachToExistingToken = useMutation({
+    mutationKey: ['abl-token', 'attach-to-existing-token', { cluster }],
+    mutationFn: (args: {
+      mint: PublicKey,
+      mode: string,
+      threshold: BN,
+      name: string | null,
+      symbol: string | null,
+      uri: string | null,
+    }) => {
+      const modeEnum = args.mode === 'allow' ? { allow: {} } : args.mode === 'block' ? { block: {}} : { mixed: {}};
+
+      return program.methods.attachToMint({
+        mode: modeEnum,
+        threshold: args.threshold,
+        name: args.name,
+        symbol: args.symbol,
+        uri: args.uri,
+      }).accounts({
+        mint: args.mint,
+      }).rpc()
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to initialize token'),
+  })
+
+  const changeMode = useMutation({
+    mutationKey: ['abl-token', 'change-mode', { cluster }],
+    mutationFn: (args: {
+      mode: string,
+      threshold: BN,
+      mint: PublicKey,
+    }) => {
+      const modeEnum = args.mode === 'allow' ? { allow: {} } : args.mode === 'block' ? { block: {}} : { mixed: {}}
+      return program.methods.changeMode({
+        mode: modeEnum,
+        threshold: args.threshold,
+      }).accounts({
+        mint: args.mint,
+      }).rpc()
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to run program'),
+  })
+
+  const initWallet = useMutation({
+    mutationKey: ['abl-token', 'change-mode', { cluster }],
+    mutationFn: (args: {
+      wallet: PublicKey,
+      allowed: boolean,
+    }) => {
+      return program.methods.initWallet({
+        allowed: args.allowed,
+      }).accounts({
+        wallet: args.wallet,
+      }).rpc()
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to run program'),
+  })
+
+  const processBatchWallets = useMutation({
+    mutationKey: ['abl-token', 'process-batch-wallets', { cluster }],
+    mutationFn: async (args: {
+      wallets: {wallet: PublicKey, mode: "allow" | "block" | "remove"}[],
+    }) => {
+      const instructions = await Promise.all(args.wallets.map((wallet) => {
+        if (wallet.mode === "remove") {
+          const [abWalletPda] = PublicKey.findProgramAddressSync(
+            [
+              Buffer.from('ab_wallet'),
+              wallet.wallet.toBuffer(),
+            ],
+            program.programId
+          );
+          return program.methods.removeWallet().accounts({
+            abWallet: abWalletPda,
+          }).instruction()
+        }
+        return program.methods.initWallet({
+          allowed: wallet.mode === "allow",
+        }).accounts({
+          wallet: wallet.wallet,
+        }).instruction()
+      }));
+      
+      const transaction = new Transaction();
+      transaction.add(...instructions);
+      transaction.feePayer = provider.wallet.publicKey;
+      transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
+      //transaction.sign(provider.wallet);
+
+      let signedTx = await provider.wallet.signTransaction(transaction);
+
+      return connection.sendRawTransaction(signedTx.serialize());
+
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to run program'),
+  })
+  
+
+  const removeWallet = useMutation({
+    mutationKey: ['abl-token', 'change-mode', { cluster }],
+    mutationFn: (args: {
+      wallet: PublicKey,
+    }) => {
+      const [abWalletPda] = PublicKey.findProgramAddressSync(
+        [
+          Buffer.from('ab_wallet'),
+          args.wallet.toBuffer(),
+        ],
+        program.programId
+      );
+      return program.methods.removeWallet().accounts({
+        abWallet: abWalletPda,
+      }).rpc()
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to run program'),
+  })
+
+
+  const initConfig = useMutation({
+    mutationKey: ['abl-token', 'init-config', { cluster }],
+    mutationFn: () => {
+      return program.methods.initConfig().rpc()
+    },
+  })
+
+  const getConfig = useQuery({
+    queryKey: ['get-config', { cluster }],
+    queryFn: () => {
+      const [configPda] = PublicKey.findProgramAddressSync(
+        [Buffer.from('config')],
+        program.programId
+      );
+      return program.account.config.fetch(configPda)
+    },
+  })
+  
+  const getAbWallets = useQuery({
+    queryKey: ['get-ab-wallets', { cluster }],
+    queryFn: () => {
+      return program.account.abWallet.all()
+    },
+  })
+
+  const getToken = (mint: PublicKey) => useQuery({
+    queryKey: ['get-token', { endpoint: connection.rpcEndpoint, mint }],
+    queryFn: async () => {
+      const mintInfo = await getMint(
+        connection,
+        mint,
+        "confirmed",
+        TOKEN_2022_PROGRAM_ID,
+      );
+
+      const metadata = await getTokenMetadata(
+        connection,
+        mint,
+        "confirmed",
+        TOKEN_2022_PROGRAM_ID,
+      );
+
+      const permanentDelegate = await getPermanentDelegate(mintInfo);
+
+      return {
+        name: metadata?.name,
+        symbol: metadata?.symbol,
+        uri: metadata?.uri,
+        decimals: mintInfo.decimals,
+        mintAuthority: mintInfo.mintAuthority,
+        freezeAuthority: mintInfo.freezeAuthority,
+        permanentDelegate: permanentDelegate,
+      }
+    },
+  })
+/*
+  const getBalance = useQuery({
+    queryKey: ['get-balance', { cluster }],
+    queryFn: () => {
+      getbal
+    },
+  })*/
+
+  const mintTo = useMutation({
+    mutationKey: ['abl-token', 'mint-to', { cluster }],
+    mutationFn: async (args: {
+      mint: PublicKey,
+      amount: BN,
+      recipient: PublicKey,
+    }) => {
+      const mintInfo = await getMint(
+        connection,
+        args.mint,
+        "confirmed",
+        TOKEN_2022_PROGRAM_ID,
+      );
+      const ata = getAssociatedTokenAddressSync(args.mint, args.recipient, true, TOKEN_2022_PROGRAM_ID);
+      
+      const ix = createAssociatedTokenAccountIdempotentInstruction(provider.publicKey, ata, args.recipient, args.mint, TOKEN_2022_PROGRAM_ID);
+      const ix2 = createMintToCheckedInstruction(args.mint, ata, provider.publicKey, args.amount.toNumber(), mintInfo.decimals, undefined, TOKEN_2022_PROGRAM_ID);
+      const tx = new Transaction();
+      tx.add(ix, ix2);
+      tx.feePayer = provider.wallet.publicKey;
+      tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
+      let signedTx = await provider.wallet.signTransaction(tx);
+      return connection.sendRawTransaction(signedTx.serialize())
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: () => toast.error('Failed to run program'),
+  })
+
+  return {
+    program,
+    programId,
+    getProgramAccount,
+    initToken,
+    changeMode,
+    initWallet,
+    removeWallet,
+    initConfig,
+    getConfig,
+    getAbWallets,
+    getToken,
+    processBatchWallets,
+    mintTo,
+    attachToExistingToken,
+  }
+}

+ 34 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-feature.tsx

@@ -0,0 +1,34 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import { WalletButton } from '../solana/solana-provider'
+import { useAblTokenProgram } from './abl-token-data-access'
+import { AblTokenCreate, AblTokenProgram } from './abl-token-ui'
+import { AppHero } from '../app-hero'
+import { ellipsify } from '@/lib/utils'
+
+export default function AblTokenFeature() {
+  const { publicKey } = useWallet()
+  const { programId } = useAblTokenProgram()
+
+  return publicKey ? (
+    <div>
+      <AppHero title="ABL Token" subtitle={'Run the program by clicking the "Run program" button.'}>
+        <p className="mb-6">
+          <ExplorerLink path={`account/${programId}`} label={ellipsify(programId.toString())} />
+        </p>
+        <AblTokenCreate />
+      </AppHero>
+      <AblTokenProgram />
+    </div>
+  ) : (
+    <div className="max-w-4xl mx-auto">
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton className="btn btn-primary" />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 281 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token-detail.tsx

@@ -0,0 +1,281 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { WalletButton } from '../solana/solana-provider'
+import { useParams } from 'next/navigation'
+import React from 'react'
+import { useAblTokenProgram, useHasTransferHookEnabled } from './abl-token-data-access'
+import { PublicKey } from '@solana/web3.js'
+import { PermanentDelegate } from '@solana/spl-token'
+import { BN } from '@coral-xyz/anchor'
+import { Button } from '@/components/ui/button'
+
+interface TokenInfo {
+  address: string;
+  name: string | undefined;
+  symbol: string | undefined;
+  uri: string | undefined;
+  decimals: number;
+  supply: number;
+  mintAuthority: PublicKey | null;
+  freezeAuthority: PublicKey | null;
+  permanentDelegate: PermanentDelegate | null;
+}
+
+function TokenInfo({ tokenInfo }: { tokenInfo: TokenInfo | null }) {
+  return (
+    <div className="bg-base-200 p-6 rounded-lg">
+      <h2 className="text-2xl font-bold mb-4">Token Information</h2>
+      {tokenInfo ? (
+        <div className="grid grid-cols-2 gap-4">
+          <div>Address: {tokenInfo.address}</div>
+          <div>Name: {tokenInfo.name}</div>
+          <div>Symbol: {tokenInfo.symbol}</div>
+          <div>Decimals: {tokenInfo.decimals}</div>
+          <div>URI: {tokenInfo.uri}</div>
+          <div>Supply: {tokenInfo.supply}</div>
+          <div>Mint Authority: {tokenInfo.mintAuthority?.toString()}</div>
+          <div>Freeze Authority: {tokenInfo.freezeAuthority?.toString()}</div>
+          <div>Permanent Delegate: {tokenInfo.permanentDelegate?.delegate.toString()}</div>
+        </div>
+      ) : (
+        <p>No token information available.</p>
+      )}
+    </div>
+  )
+}
+
+function TokenManagement({ tokenInfo }: { tokenInfo: TokenInfo }) {
+  const { publicKey } = useWallet()
+  const { changeMode, mintTo, attachToExistingToken } = useAblTokenProgram()
+  const [mode, setMode] = React.useState<'allow' | 'block' | 'mixed'>('allow')
+  const [threshold, setThreshold] = React.useState('100000')
+  const [destinationWallet, setDestinationWallet] = React.useState('')
+  const hasTransferHookEnabled = useHasTransferHookEnabled(new PublicKey(tokenInfo.address))
+  const [name, setName] = React.useState<string>('')
+  const [symbol, setSymbol] = React.useState<string>('')
+  const [uri, setUri] = React.useState<string>('')
+
+
+  const handleApplyChanges = async () => {
+    if (!publicKey || !tokenInfo) return;
+
+    try {
+      await changeMode.mutateAsync({
+        mode,
+        threshold: new BN(threshold),
+        mint: new PublicKey(tokenInfo.address),
+      });
+    } catch (err) {
+      console.error('Failed to apply changes:', err);
+    }
+  };
+
+  const setTransferHook = async () => {
+    if (!publicKey || !tokenInfo) return;
+
+    try {
+      await attachToExistingToken.mutateAsync({
+        mint: new PublicKey(tokenInfo.address),
+        mode,
+        threshold: new BN(threshold),
+        name,
+        symbol,
+        uri,
+      });
+    } catch (err) {
+      console.error('Failed to set transfer hook:', err);
+    }
+  };
+
+  const [mintAmount, setMintAmount] = React.useState('0')
+
+  const handleMint = async () => {
+    if (!publicKey || !tokenInfo) return;
+
+    try {
+      await mintTo.mutateAsync({
+        mint: new PublicKey(tokenInfo.address),
+        amount: new BN(mintAmount),
+        recipient: publicKey,
+      });
+      console.log('Minted successfully');
+    } catch (err) {
+      console.error('Failed to mint tokens:', err);
+    }
+  };
+
+  return (
+    <div className="bg-base-200 p-6 rounded-lg">
+      <h2 className="text-2xl font-bold mb-4">Token Management</h2>
+      <div className="space-y-4">
+        <div>
+          {hasTransferHookEnabled.data ? (
+            <div>
+              <label className="block mb-2">Mode</label>
+              <div className="flex gap-4">
+                <label>
+                  <input
+                    type="radio"
+                    checked={mode === 'allow'}
+                    onChange={() => setMode('allow')}
+                    name="mode"
+                  /> Allow
+                </label>
+                <label>
+                  <input
+                    type="radio"
+                    checked={mode === 'block'}
+                    onChange={() => setMode('block')}
+                    name="mode"
+                  /> Block
+                </label>
+                <label>
+                  <input
+                    type="radio"
+                    checked={mode === 'mixed'}
+                    onChange={() => setMode('mixed')}
+                    name="mode"
+                  /> Mixed
+                </label>
+              </div>
+
+              {mode === 'mixed' && (
+              <div>
+                <label className="block mb-2">Threshold Amount</label>
+                <input
+                  type="number"
+                  className="w-full p-2 border rounded"
+                  value={threshold}
+                  onChange={e => setThreshold(e.target.value)}
+                  min="0"
+                />
+              </div>
+              )}
+
+              <div className="mt-4">
+                <Button onClick={handleApplyChanges}>
+                  Apply Changes
+                </Button>
+              </div>
+              
+            </div>
+          ) : (
+            <div>
+            <div className="space-y-4">
+              <div>
+                <label className="block mb-2">Name (Optional)</label>
+                <input
+                  type="text"
+                  className="w-full p-2 border rounded"
+                  value={name}
+                  onChange={e => setName(e.target.value)}
+                  placeholder="Enter token name"
+                />
+              </div>
+              <div>
+                <label className="block mb-2">Symbol (Optional)</label>
+                <input
+                  type="text"
+                  className="w-full p-2 border rounded"
+                  value={symbol}
+                  onChange={e => setSymbol(e.target.value)}
+                  placeholder="Enter token symbol"
+                />
+              </div>
+              <div>
+                <label className="block mb-2">URI (Optional)</label>
+                <input
+                  type="text"
+                  className="w-full p-2 border rounded"
+                  value={uri}
+                  onChange={e => setUri(e.target.value)}
+                  placeholder="Enter token URI"
+                />
+              </div>
+            </div>
+            <div className="mt-4">
+              <Button onClick={setTransferHook}>
+                Set Transfer hook
+              </Button>
+            </div>
+            </div>
+          )}
+        </div>
+
+        <div className="mt-8">
+          <h3 className="text-xl font-bold mb-2">Mint New Tokens</h3>
+          <div className="space-y-4">
+            <div>
+              <label className="block mb-2">Destination Wallet</label>
+              <input
+                type="text"
+                className="w-full p-2 border rounded"
+                value={destinationWallet}
+                onChange={e => setDestinationWallet(e.target.value)}
+                placeholder="Enter destination wallet address"
+              />
+            </div>
+            <div className="flex items-center gap-4">
+              <input
+                type="number"
+                className="w-full p-2 border rounded"
+                value={mintAmount}
+                onChange={e => setMintAmount(e.target.value)}
+                min="0"
+                placeholder="Amount to mint"
+              />
+              <Button onClick={handleMint}>
+                Mint Tokens
+              </Button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default function ManageTokenDetail() {
+  const { publicKey } = useWallet()
+  const { getToken } = useAblTokenProgram()
+  const params = useParams()
+  const tokenAddress = params?.address as string
+
+  const tokenQuery = getToken(new PublicKey(tokenAddress));
+
+  const tokenInfo = React.useMemo(() => {
+    if (!tokenQuery?.data || !tokenAddress) return null;
+    return {
+      ...tokenQuery.data,
+      address: tokenAddress,
+      supply: 0, // TODO: Get supply from token account
+    };
+  }, [tokenQuery?.data, tokenAddress]);
+
+  if (!publicKey) {
+    return (
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton />
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div className="space-y-8">
+      {tokenQuery?.isLoading ? (
+        <p>Loading token information...</p>
+      ) : tokenQuery?.isError ? (
+        <p>Error loading token information. Please check the token address.</p>
+      ) : (
+        <>
+          <TokenInfo tokenInfo={tokenInfo} />
+          {tokenInfo && <TokenManagement tokenInfo={tokenInfo}/>}
+          
+        </>
+      )}
+    </div>
+  )
+}

+ 55 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token-input.tsx

@@ -0,0 +1,55 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { WalletButton } from '../solana/solana-provider'
+
+import { redirect } from 'next/navigation'
+import React from 'react'
+import { Button } from '@/components/ui/button'
+
+export default function ManageTokenInput() {
+  const { publicKey } = useWallet()
+  const [tokenAddress, setTokenAddress] = React.useState('')
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault()
+    if (tokenAddress) {
+      redirect(`/manage-token/${tokenAddress.toString()}`)
+    }
+  }
+
+  if (!publicKey) {
+    return (
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton />
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div className="hero py-[64px]">
+      <div className="hero-content">
+        <form onSubmit={handleSubmit} className="w-full max-w-md">
+          <div className="space-y-4">
+            <label className="block">
+              Token Address
+              <input
+                type="text"
+                className="w-full p-2 border rounded"
+                value={tokenAddress}
+                onChange={e => setTokenAddress(e.target.value)}
+                placeholder="Enter token address"
+                required
+              />
+            </label>
+            <Button type="submit">
+              Manage Token
+            </Button>
+          </div>
+        </form>
+      </div>
+    </div>
+  )
+}

+ 30 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-manage-token.tsx

@@ -0,0 +1,30 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import { WalletButton } from '../solana/solana-provider'
+import { useAblTokenProgram } from './abl-token-data-access'
+import { AblTokenCreate, AblTokenProgram } from './abl-token-ui'
+import { AppHero } from '../app-hero'
+import { ellipsify } from '@/lib/utils'
+import ManageTokenInput from './abl-token-manage-token-input'
+export default function AblTokenFeature() {
+  const { publicKey } = useWallet()
+  const { programId } = useAblTokenProgram()
+
+  return publicKey ? (
+    <div>
+      <AppHero title="Manage Token">
+        <ManageTokenInput />
+      </AppHero>
+    </div>
+  ) : (
+    <div className="max-w-4xl mx-auto">
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton className="btn btn-primary" />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 33 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-new-token.tsx

@@ -0,0 +1,33 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import { WalletButton } from '../solana/solana-provider'
+import { useAblTokenProgram } from './abl-token-data-access'
+import { AblTokenCreate, AblTokenProgram } from './abl-token-ui'
+import { AppHero } from '../app-hero'
+import { ellipsify } from '@/lib/utils'
+
+export default function AblTokenFeature() {
+  const { publicKey } = useWallet()
+  const { programId } = useAblTokenProgram()
+
+  return publicKey ? (
+    <div>
+      <AppHero title="Create New Token">
+        <p className="mb-6">
+          <ExplorerLink path={`account/${programId}`} label={ellipsify(programId.toString())} />
+        </p>
+        <AblTokenCreate />
+      </AppHero>
+    </div>
+  ) : (
+    <div className="max-w-4xl mx-auto">
+      <div className="hero py-[64px]">
+        <div className="hero-content text-center">
+          <WalletButton className="btn btn-primary" />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 290 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/abl-token/abl-token-ui.tsx

@@ -0,0 +1,290 @@
+'use client'
+
+import { PublicKey } from '@solana/web3.js'
+import { useAblTokenProgram } from './abl-token-data-access'
+import { Button } from '@/components/ui/button'
+import { BN } from '@coral-xyz/anchor'
+import React from 'react'
+import { useWallet } from '@solana/wallet-adapter-react'
+
+export function AblTokenCreate() {
+  
+  const { publicKey } = useWallet()
+  const { initToken } = useAblTokenProgram()
+  const [mode, setMode] = React.useState<'allow' | 'block' | 'threshold'>('allow')
+  const [threshold, setThreshold] = React.useState('100000')
+  const [formData, setFormData] = React.useState({
+    mintAuthority: publicKey ? publicKey.toString() : '',
+    freezeAuthority: publicKey ? publicKey.toString() : '',
+    permanentDelegate: publicKey ? publicKey.toString() : '',
+    transferHookAuthority: publicKey ? publicKey.toString() : '',
+    name: '',
+    symbol: '',
+    uri: '',
+    decimals: '6'
+  })
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault()
+    try {
+      initToken.mutateAsync({
+        decimals: parseInt(formData.decimals),
+        mintAuthority: new PublicKey(formData.mintAuthority),
+        freezeAuthority: new PublicKey(formData.freezeAuthority),
+        permanentDelegate: new PublicKey(formData.permanentDelegate),
+        transferHookAuthority: new PublicKey(formData.transferHookAuthority),
+        mode,
+        threshold: new BN(threshold),
+        name: formData.name,
+        symbol: formData.symbol,
+        uri: formData.uri
+      })
+    } catch (err) {
+      console.error('Invalid form data:', err)
+    }
+  }
+
+  return (
+    <form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto">
+      <div className="space-y-2">
+        <label className="block">
+          Mint Authority*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.mintAuthority}
+            onChange={e => setFormData({...formData, mintAuthority: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Freeze Authority*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.freezeAuthority}
+            onChange={e => setFormData({...formData, freezeAuthority: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Permanent Delegate*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.permanentDelegate}
+            onChange={e => setFormData({...formData, permanentDelegate: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Transfer Hook Authority*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.transferHookAuthority}
+            onChange={e => setFormData({...formData, transferHookAuthority: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Name*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.name}
+            onChange={e => setFormData({...formData, name: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Symbol*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.symbol}
+            onChange={e => setFormData({...formData, symbol: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          URI*
+          <input
+            type="text"
+            className="w-full p-2 border rounded"
+            value={formData.uri}
+            onChange={e => setFormData({...formData, uri: e.target.value})}
+            required
+          />
+        </label>
+
+        <label className="block">
+          Decimals*
+          <input
+            type="number"
+            className="w-full p-2 border rounded"
+            value={formData.decimals}
+            onChange={e => setFormData({...formData, decimals: e.target.value})}
+            required
+            min="0"
+            max="9"
+          />
+        </label>
+
+        <div className="space-y-2">
+          <label className="block">Mode*</label>
+          <div className="flex gap-4">
+            <label>
+              <input
+                type="radio"
+                checked={mode === 'allow'}
+                onChange={() => setMode('allow')}
+                name="mode"
+              /> Allow
+            </label>
+            <label>
+              <input
+                type="radio"
+                checked={mode === 'block'}
+                onChange={() => setMode('block')}
+                name="mode"
+              /> Block
+            </label>
+            <label>
+              <input
+                type="radio"
+                checked={mode === 'threshold'}
+                onChange={() => setMode('threshold')}
+                name="mode"
+              /> Threshold
+            </label>
+          </div>
+        </div>
+
+        {mode === 'threshold' && (
+          <label className="block">
+            Threshold Amount
+            <input
+              type="number"
+              className="w-full p-2 border rounded"
+              value={threshold}
+              onChange={e => setThreshold(e.target.value)}
+              min="0"
+            />
+          </label>
+        )}
+      </div>
+
+      <Button type="submit" disabled={initToken.isPending}>
+        Create Token {initToken.isPending && '...'}
+      </Button>
+    </form>
+  )
+}
+
+export function AblTokenProgram() {
+  const { getProgramAccount } = useAblTokenProgram()
+
+  if (getProgramAccount.isLoading) {
+    return <span className="loading loading-spinner loading-lg"></span>
+  }
+  if (!getProgramAccount.data?.value) {
+    return (
+      <div className="alert alert-info flex justify-center">
+        <span>Program account not found. Make sure you have deployed the program and are on the correct cluster.</span>
+      </div>
+    )
+  }
+  return (
+    <div className={'space-y-6'}>
+      <pre>{JSON.stringify(getProgramAccount.data.value, null, 2)}</pre>
+    </div>
+  )
+}
+
+interface WalletEntry {
+  address: string;
+  mode: 'allow' | 'block';
+}
+
+export function AblTokenWalletTable() {
+  const [wallets, setWallets] = React.useState<WalletEntry[]>([]);
+
+  const handleDragOver = (e: React.DragEvent) => {
+    e.preventDefault();
+  };
+
+  const handleDrop = async (e: React.DragEvent) => {
+    e.preventDefault();
+
+    const file = e.dataTransfer.files[0];
+    if (file && file.type === 'text/csv') {
+      const text = await file.text();
+      const rows = text.split('\n');
+      
+      const parsed: WalletEntry[] = rows
+        .filter(row => row.trim()) // Skip empty rows
+        .map(row => {
+          const [address, mode] = row.split(',').map(field => field.trim());
+          return {
+            address,
+            mode: mode.toLowerCase() as 'allow' | 'block'
+          };
+        })
+        .filter(entry => {
+          // Basic validation
+          try {
+            new PublicKey(entry.address);
+            return ['allow', 'block'].includes(entry.mode);
+          } catch {
+            return false;
+          }
+        });
+
+      setWallets(parsed);
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      <div 
+        onDragOver={handleDragOver}
+        onDrop={handleDrop}
+        className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary cursor-pointer"
+      >
+        Drop CSV file here (address,mode)
+      </div>
+
+      {wallets.length > 0 && (
+        <div className="overflow-x-auto">
+          <table className="table w-full">
+            <thead>
+              <tr>
+                <th>Address</th>
+                <th>Mode</th>
+              </tr>
+            </thead>
+            <tbody>
+              {wallets.map((wallet, index) => (
+                <tr key={index}>
+                  <td className="font-mono">{wallet.address}</td>
+                  <td>
+                    <span className={`badge ${wallet.mode === 'allow' ? 'badge-success' : 'badge-error'}`}>
+                      {wallet.mode}
+                    </span>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      )}
+    </div>
+  );
+}

+ 255 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-data-access.tsx

@@ -0,0 +1,255 @@
+'use client'
+
+import { createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, createTransferCheckedWithTransferHookInstruction, getAssociatedTokenAddressSync, getExtraAccountMetaAddress, getExtraAccountMetas, getMint, getTransferHook, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'
+import { useConnection, useWallet } from '@solana/wallet-adapter-react'
+import {
+  Connection,
+  LAMPORTS_PER_SOL,
+  PublicKey,
+  SendTransactionError,
+  SystemProgram,
+  Transaction,
+  TransactionMessage,
+  TransactionSignature,
+  VersionedTransaction,
+} from '@solana/web3.js'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { useTransactionErrorToast, useTransactionToast } from '../use-transaction-toast'
+import { useAnchorProvider } from '../solana/solana-provider'
+import { toast } from 'sonner'
+import { Buffer } from "buffer"
+
+export function useGetBalance({ address }: { address: PublicKey }) {
+  const { connection } = useConnection()
+
+  return useQuery({
+    queryKey: ['get-balance', { endpoint: connection.rpcEndpoint, address }],
+    queryFn: () => connection.getBalance(address),
+  })
+}
+
+export function useGetSignatures({ address }: { address: PublicKey }) {
+  const { connection } = useConnection()
+
+  return useQuery({
+    queryKey: ['get-signatures', { endpoint: connection.rpcEndpoint, address }],
+    queryFn: () => connection.getSignaturesForAddress(address),
+  })
+}
+
+export function useSendTokens() {
+  const { connection } = useConnection()
+  const { publicKey } = useWallet()
+  const transactionToast = useTransactionToast()
+  const provider = useAnchorProvider()
+  const transactionErrorToast = useTransactionErrorToast()
+
+  return useMutation({
+    mutationFn: async (args: {
+      mint: PublicKey,
+      destination: PublicKey,
+      amount: number,
+    }) => {
+      if (!publicKey) throw new Error('No public key found');
+      const { mint, destination, amount } = args;
+      const mintInfo = await getMint(connection, mint, 'confirmed', TOKEN_2022_PROGRAM_ID);
+      const ataDestination = getAssociatedTokenAddressSync(mint, destination, true, TOKEN_2022_PROGRAM_ID);
+      const ataSource = getAssociatedTokenAddressSync(mint, publicKey, true, TOKEN_2022_PROGRAM_ID);
+      const ix = createAssociatedTokenAccountIdempotentInstruction(publicKey, ataDestination, destination, mint, TOKEN_2022_PROGRAM_ID);
+      const bi = BigInt(amount);
+      const decimals = mintInfo.decimals;
+      console.log("BI: ", bi);
+      console.log("AMOUNT: ", amount);
+      console.log("DECIMALS: ", decimals);
+      const buf = Buffer.alloc(10);
+      console.dir(buf);
+  
+      buf.writeBigUInt64LE(bi, 0);
+      console.log(buf);
+      const ix3 = await createTransferCheckedInstruction(ataSource, mint, ataDestination, publicKey, bi, decimals, undefined, TOKEN_2022_PROGRAM_ID);
+
+      const transferHook = getTransferHook(mintInfo);
+      if (!transferHook) throw new Error('bad token');
+      const extraMetas = getExtraAccountMetaAddress(mint, transferHook.programId);
+
+      const seeds = [Buffer.from('ab_wallet'), destination.toBuffer()];
+      const abWallet = PublicKey.findProgramAddressSync(seeds, transferHook.programId)[0];
+
+      ix3.keys.push({ pubkey: abWallet, isSigner: false, isWritable: false });
+      ix3.keys.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false });
+      ix3.keys.push({ pubkey: extraMetas, isSigner: false, isWritable: false });
+
+      console.log("tx-hook: ", transferHook.programId.toString());
+      console.log("extra-metas: ", extraMetas.toString());
+      console.log("ab-wallet: ", abWallet.toString());
+      console.log("KEYS: ", ix3.keys);
+      
+      const validateStateAccount = await connection.getAccountInfo(extraMetas, 'confirmed');
+      if (!validateStateAccount) throw new Error('validate-state-account not found');
+      const validateStateData = getExtraAccountMetas(validateStateAccount);
+      console.log("validate-state-data: ", validateStateData);
+    
+      //const ix2 = await createTransferCheckedWithTransferHookInstruction(connection, ataSource, mint, ataDestination, publicKey, bi, decimals, undefined, 'confirmed', TOKEN_2022_PROGRAM_ID);
+      
+      const transaction = new Transaction();
+      transaction.add(ix, ix3);
+      transaction.feePayer = provider.wallet.publicKey;
+      transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
+
+      let signedTx = await provider.wallet.signTransaction(transaction);
+
+      return connection.sendRawTransaction(signedTx.serialize());
+    },
+    onSuccess: (signature) => {
+      transactionToast(signature)
+    },
+    onError: (error) => { transactionErrorToast(error, connection) },
+  })
+}
+
+export function useGetTokenAccounts({ address }: { address: PublicKey }) {
+  const { connection } = useConnection()
+
+  return useQuery({
+    queryKey: ['get-token-accounts', { endpoint: connection.rpcEndpoint, address }],
+    queryFn: async () => {
+      const [tokenAccounts, token2022Accounts] = await Promise.all([
+        connection.getParsedTokenAccountsByOwner(address, {
+          programId: TOKEN_PROGRAM_ID,
+        }),
+        connection.getParsedTokenAccountsByOwner(address, {
+          programId: TOKEN_2022_PROGRAM_ID,
+        }),
+      ])
+      return [...tokenAccounts.value, ...token2022Accounts.value]
+    },
+  })
+}
+
+export function useTransferSol({ address }: { address: PublicKey }) {
+  const { connection } = useConnection()
+  // const transactionToast = useTransactionToast()
+  const wallet = useWallet()
+  const client = useQueryClient()
+
+  return useMutation({
+    mutationKey: ['transfer-sol', { endpoint: connection.rpcEndpoint, address }],
+    mutationFn: async (input: { destination: PublicKey; amount: number }) => {
+      let signature: TransactionSignature = ''
+      try {
+        const { transaction, latestBlockhash } = await createTransaction({
+          publicKey: address,
+          destination: input.destination,
+          amount: input.amount,
+          connection,
+        })
+
+        // Send transaction and await for signature
+        signature = await wallet.sendTransaction(transaction, connection)
+
+        // Send transaction and await for signature
+        await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed')
+
+        console.log(signature)
+        return signature
+      } catch (error: unknown) {
+        console.log('error', `Transaction failed! ${error}`, signature)
+
+        return
+      }
+    },
+    onSuccess: (signature) => {
+      if (signature) {
+        // TODO: Add back Toast
+        // transactionToast(signature)
+        console.log('Transaction sent', signature)
+      }
+      return Promise.all([
+        client.invalidateQueries({
+          queryKey: ['get-balance', { endpoint: connection.rpcEndpoint, address }],
+        }),
+        client.invalidateQueries({
+          queryKey: ['get-signatures', { endpoint: connection.rpcEndpoint, address }],
+        }),
+      ])
+    },
+    onError: (error) => {
+      // TODO: Add Toast
+      console.error(`Transaction failed! ${error}`)
+    },
+  })
+}
+
+export function useRequestAirdrop({ address }: { address: PublicKey }) {
+  const { connection } = useConnection()
+  // const transactionToast = useTransactionToast()
+  const client = useQueryClient()
+
+  return useMutation({
+    mutationKey: ['airdrop', { endpoint: connection.rpcEndpoint, address }],
+    mutationFn: async (amount: number = 1) => {
+      const [latestBlockhash, signature] = await Promise.all([
+        connection.getLatestBlockhash(),
+        connection.requestAirdrop(address, amount * LAMPORTS_PER_SOL),
+      ])
+
+      await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed')
+      return signature
+    },
+    onSuccess: (signature) => {
+      // TODO: Add back Toast
+      // transactionToast(signature)
+      console.log('Airdrop sent', signature)
+      return Promise.all([
+        client.invalidateQueries({
+          queryKey: ['get-balance', { endpoint: connection.rpcEndpoint, address }],
+        }),
+        client.invalidateQueries({
+          queryKey: ['get-signatures', { endpoint: connection.rpcEndpoint, address }],
+        }),
+      ])
+    },
+  })
+}
+
+async function createTransaction({
+  publicKey,
+  destination,
+  amount,
+  connection,
+}: {
+  publicKey: PublicKey
+  destination: PublicKey
+  amount: number
+  connection: Connection
+}): Promise<{
+  transaction: VersionedTransaction
+  latestBlockhash: { blockhash: string; lastValidBlockHeight: number }
+}> {
+  // Get the latest blockhash to use in our transaction
+  const latestBlockhash = await connection.getLatestBlockhash()
+
+  // Create instructions to send, in this case a simple transfer
+  const instructions = [
+    SystemProgram.transfer({
+      fromPubkey: publicKey,
+      toPubkey: destination,
+      lamports: amount * LAMPORTS_PER_SOL,
+    }),
+  ]
+
+  // Create a new TransactionMessage with version and compile it to legacy
+  const messageLegacy = new TransactionMessage({
+    payerKey: publicKey,
+    recentBlockhash: latestBlockhash.blockhash,
+    instructions,
+  }).compileToLegacyMessage()
+
+  // Create a new VersionedTransaction which supports legacy and v0
+  const transaction = new VersionedTransaction(messageLegacy)
+
+  return {
+    transaction,
+    latestBlockhash,
+  }
+}

+ 47 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-detail-feature.tsx

@@ -0,0 +1,47 @@
+'use client'
+
+import { PublicKey } from '@solana/web3.js'
+import { useMemo } from 'react'
+import { useParams } from 'next/navigation'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import { AccountBalance, AccountButtons, AccountTokens, AccountTransactions } from './account-ui'
+import { AppHero } from '../app-hero'
+import { ellipsify } from '@/lib/utils'
+
+export default function AccountDetailFeature() {
+  const params = useParams()
+  const address = useMemo(() => {
+    if (!params.address) {
+      return
+    }
+    try {
+      return new PublicKey(params.address)
+    } catch (e) {
+      console.log(`Invalid public key`, e)
+    }
+  }, [params])
+  if (!address) {
+    return <div>Error loading account</div>
+  }
+
+  return (
+    <div>
+      <AppHero
+        title={<AccountBalance address={address} />}
+        subtitle={
+          <div className="my-4">
+            <ExplorerLink path={`account/${address}`} label={ellipsify(address.toString())} />
+          </div>
+        }
+      >
+        <div className="my-4">
+          <AccountButtons address={address} />
+        </div>
+      </AppHero>
+      <div className="space-y-8">
+        <AccountTokens address={address} />
+        <AccountTransactions address={address} />
+      </div>
+    </div>
+  )
+}

+ 22 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-list-feature.tsx

@@ -0,0 +1,22 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { WalletButton } from '../solana/solana-provider'
+
+import { redirect } from 'next/navigation'
+
+export default function AccountListFeature() {
+  const { publicKey } = useWallet()
+
+  if (publicKey) {
+    return redirect(`/account/${publicKey.toString()}`)
+  }
+
+  return (
+    <div className="hero py-[64px]">
+      <div className="hero-content text-center">
+        <WalletButton />
+      </div>
+    </div>
+  )
+}

+ 358 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-ui.tsx

@@ -0,0 +1,358 @@
+'use client'
+
+import { useWallet } from '@solana/wallet-adapter-react'
+import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'
+import { RefreshCw } from 'lucide-react'
+import { useQueryClient } from '@tanstack/react-query'
+import { useMemo, useState } from 'react'
+
+import { useCluster } from '../cluster/cluster-data-access'
+import { ExplorerLink } from '../cluster/cluster-ui'
+import {
+  useGetBalance,
+  useGetSignatures,
+  useGetTokenAccounts,
+  useRequestAirdrop,
+  useSendTokens,
+  useTransferSol,
+} from './account-data-access'
+import { ellipsify } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import { AppAlert } from '@/components/app-alert'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
+import { AppModal } from '@/components/app-modal'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+
+export function AccountBalance({ address }: { address: PublicKey }) {
+  const query = useGetBalance({ address })
+
+  return (
+    <h1 className="text-5xl font-bold cursor-pointer" onClick={() => query.refetch()}>
+      {query.data ? <BalanceSol balance={query.data} /> : '...'} SOL
+    </h1>
+  )
+}
+
+export function AccountChecker() {
+  const { publicKey } = useWallet()
+  if (!publicKey) {
+    return null
+  }
+  return <AccountBalanceCheck address={publicKey} />
+}
+
+export function AccountBalanceCheck({ address }: { address: PublicKey }) {
+  const { cluster } = useCluster()
+  const mutation = useRequestAirdrop({ address })
+  const query = useGetBalance({ address })
+
+  if (query.isLoading) {
+    return null
+  }
+  if (query.isError || !query.data) {
+    return (
+      <AppAlert
+        action={
+          <Button variant="outline" onClick={() => mutation.mutateAsync(1).catch((err) => console.log(err))}>
+            Request Airdrop
+          </Button>
+        }
+      >
+        You are connected to <strong>{cluster.name}</strong> but your account is not found on this cluster.
+      </AppAlert>
+    )
+  }
+  return null
+}
+
+export function AccountButtons({ address }: { address: PublicKey }) {
+  const { cluster } = useCluster()
+  return (
+    <div>
+      <div className="space-x-2">
+        {cluster.network?.includes('mainnet') ? null : <ModalAirdrop address={address} />}
+        <ModalSend address={address} />
+        <ModalReceive address={address} />
+      </div>
+    </div>
+  )
+}
+
+export function AccountTokens({ address }: { address: PublicKey }) {
+  const [showAll, setShowAll] = useState(false)
+  const query = useGetTokenAccounts({ address })
+  const client = useQueryClient()
+  const sendTokens = useSendTokens()
+  const items = useMemo(() => {
+    if (showAll) return query.data
+    return query.data?.slice(0, 5)
+  }, [query.data, showAll])
+
+  return (
+    <div className="space-y-2">
+      <div className="justify-between">
+        <div className="flex justify-between">
+          <h2 className="text-2xl font-bold">Token Accounts</h2>
+          <div className="space-x-2">
+            {query.isLoading ? (
+              <span className="loading loading-spinner"></span>
+            ) : (
+              <Button
+                variant="outline"
+                onClick={async () => {
+                  await query.refetch()
+                  await client.invalidateQueries({
+                    queryKey: ['getTokenAccountBalance'],
+                  })
+                }}
+              >
+                <RefreshCw size={16} />
+              </Button>
+            )}
+          </div>
+        </div>
+      </div>
+      {query.isError && <pre className="alert alert-error">Error: {query.error?.message.toString()}</pre>}
+      {query.isSuccess && (
+        <div>
+          {query.data.length === 0 ? (
+            <div>No token accounts found.</div>
+          ) : (
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>Public Key</TableHead>
+                  <TableHead>Mint</TableHead>
+                  <TableHead className="text-right">Balance</TableHead>
+                  <TableHead></TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {items?.map(({ account, pubkey }) => (
+                  <TableRow key={pubkey.toString()}>
+                    <TableCell>
+                      <div className="flex space-x-2">
+                        <span className="font-mono">
+                          <ExplorerLink label={ellipsify(pubkey.toString())} path={`account/${pubkey.toString()}`} />
+                        </span>
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex space-x-2">
+                        <span className="font-mono">
+                          <ExplorerLink
+                            label={ellipsify(account.data.parsed.info.mint)}
+                            path={`account/${account.data.parsed.info.mint.toString()}`}
+                          />
+                        </span>
+                      </div>
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <span className="font-mono">{account.data.parsed.info.tokenAmount.uiAmount}</span>
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <span className="font-mono">
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => {
+                          const amount = window.prompt("Enter amount to send:")
+                          const destination = window.prompt("Enter destination wallet address:")
+                          if (amount && destination) {
+                            try {
+                              sendTokens.mutateAsync({ mint: new PublicKey(account.data.parsed.info.mint), destination: new PublicKey(destination), amount: parseFloat(amount) });
+                              // TODO: Implement token transfer logic
+                              console.log(`Sending ${amount} tokens to ${destination}`)
+                            } catch (err) {
+                              console.error("Failed to send tokens:", err)
+                            }
+                          }
+                        }}
+                      >
+                        Send
+                      </Button>
+                      </span>
+                    </TableCell>
+                  </TableRow>
+                ))}
+
+                {(query.data?.length ?? 0) > 5 && (
+                  <TableRow>
+                    <TableCell colSpan={4} className="text-center">
+                      <Button variant="outline" onClick={() => setShowAll(!showAll)}>
+                        {showAll ? 'Show Less' : 'Show All'}
+                      </Button>
+                    </TableCell>
+                  </TableRow>
+                )}
+              </TableBody>
+            </Table>
+          )}
+        </div>
+      )}
+    </div>
+  )
+}
+
+export function AccountTransactions({ address }: { address: PublicKey }) {
+  const query = useGetSignatures({ address })
+  const [showAll, setShowAll] = useState(false)
+
+  const items = useMemo(() => {
+    if (showAll) return query.data
+    return query.data?.slice(0, 5)
+  }, [query.data, showAll])
+
+  return (
+    <div className="space-y-2">
+      <div className="flex justify-between">
+        <h2 className="text-2xl font-bold">Transaction History</h2>
+        <div className="space-x-2">
+          {query.isLoading ? (
+            <span className="loading loading-spinner"></span>
+          ) : (
+            <Button variant="outline" onClick={() => query.refetch()}>
+              <RefreshCw size={16} />
+            </Button>
+          )}
+        </div>
+      </div>
+      {query.isError && <pre className="alert alert-error">Error: {query.error?.message.toString()}</pre>}
+      {query.isSuccess && (
+        <div>
+          {query.data.length === 0 ? (
+            <div>No transactions found.</div>
+          ) : (
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>Signature</TableHead>
+                  <TableHead className="text-right">Slot</TableHead>
+                  <TableHead>Block Time</TableHead>
+                  <TableHead className="text-right">Status</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {items?.map((item) => (
+                  <TableRow key={item.signature}>
+                    <TableHead className="font-mono">
+                      <ExplorerLink path={`tx/${item.signature}`} label={ellipsify(item.signature, 8)} />
+                    </TableHead>
+                    <TableCell className="font-mono text-right">
+                      <ExplorerLink path={`block/${item.slot}`} label={item.slot.toString()} />
+                    </TableCell>
+                    <TableCell>{new Date((item.blockTime ?? 0) * 1000).toISOString()}</TableCell>
+                    <TableCell className="text-right">
+                      {item.err ? (
+                        <span className="text-red-500" title={item.err.toString()}>
+                          Failed
+                        </span>
+                      ) : (
+                        <span className="text-green-500">Success</span>
+                      )}
+                    </TableCell>
+                  </TableRow>
+                ))}
+                {(query.data?.length ?? 0) > 5 && (
+                  <TableRow>
+                    <TableCell colSpan={4} className="text-center">
+                      <Button variant="outline" onClick={() => setShowAll(!showAll)}>
+                        {showAll ? 'Show Less' : 'Show All'}
+                      </Button>
+                    </TableCell>
+                  </TableRow>
+                )}
+              </TableBody>
+            </Table>
+          )}
+        </div>
+      )}
+    </div>
+  )
+}
+
+function BalanceSol({ balance }: { balance: number }) {
+  return <span>{Math.round((balance / LAMPORTS_PER_SOL) * 100000) / 100000}</span>
+}
+
+function ModalReceive({ address }: { address: PublicKey }) {
+  return (
+    <AppModal title="Receive">
+      <p>Receive assets by sending them to your public key:</p>
+      <code>{address.toString()}</code>
+    </AppModal>
+  )
+}
+
+function ModalAirdrop({ address }: { address: PublicKey }) {
+  const mutation = useRequestAirdrop({ address })
+  const [amount, setAmount] = useState('2')
+
+  return (
+    <AppModal
+      title="Airdrop"
+      submitDisabled={!amount || mutation.isPending}
+      submitLabel="Request Airdrop"
+      submit={() => mutation.mutateAsync(parseFloat(amount))}
+    >
+      <Label htmlFor="amount">Amount</Label>
+      <Input
+        disabled={mutation.isPending}
+        id="amount"
+        min="1"
+        onChange={(e) => setAmount(e.target.value)}
+        placeholder="Amount"
+        step="any"
+        type="number"
+        value={amount}
+      />
+    </AppModal>
+  )
+}
+
+function ModalSend({ address }: { address: PublicKey }) {
+  const wallet = useWallet()
+  const mutation = useTransferSol({ address })
+  const [destination, setDestination] = useState('')
+  const [amount, setAmount] = useState('1')
+
+  if (!address || !wallet.sendTransaction) {
+    return <div>Wallet not connected</div>
+  }
+
+  return (
+    <AppModal
+      title="Send"
+      submitDisabled={!destination || !amount || mutation.isPending}
+      submitLabel="Send"
+      submit={() => {
+        mutation.mutateAsync({
+          destination: new PublicKey(destination),
+          amount: parseFloat(amount),
+        })
+      }}
+    >
+      <Label htmlFor="destination">Destination</Label>
+      <Input
+        disabled={mutation.isPending}
+        id="destination"
+        onChange={(e) => setDestination(e.target.value)}
+        placeholder="Destination"
+        type="text"
+        value={destination}
+      />
+      <Label htmlFor="amount">Amount</Label>
+      <Input
+        disabled={mutation.isPending}
+        id="amount"
+        min="1"
+        onChange={(e) => setAmount(e.target.value)}
+        placeholder="Amount"
+        step="any"
+        type="number"
+        value={amount}
+      />
+    </AppModal>
+  )
+}

+ 13 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-alert.tsx

@@ -0,0 +1,13 @@
+import { AlertCircle } from 'lucide-react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { ReactNode } from 'react'
+
+export function AppAlert({ action, children }: { action: ReactNode; children: ReactNode }) {
+  return (
+    <Alert variant="warning">
+      <AlertCircle className="h-4 w-4" />
+      <AlertTitle>{children}</AlertTitle>
+      <AlertDescription className="flex justify-end">{action}</AlertDescription>
+    </Alert>
+  )
+}

+ 17 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-footer.tsx

@@ -0,0 +1,17 @@
+import React from 'react'
+
+export function AppFooter() {
+  return (
+    <footer className="text-center p-2 bg-neutral-100 dark:bg-neutral-900 dark:text-neutral-400 text-xs">
+      Generated by{' '}
+      <a
+        className="link hover:text-neutral-500 dark:hover:text-white"
+        href="https://github.com/solana-developers/create-solana-dapp"
+        target="_blank"
+        rel="noopener noreferrer"
+      >
+        create-solana-dapp
+      </a>
+    </footer>
+  )
+}

+ 79 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-header.tsx

@@ -0,0 +1,79 @@
+'use client'
+import { usePathname } from 'next/navigation'
+import { useState } from 'react'
+import Link from 'next/link'
+import { Button } from '@/components/ui/button'
+import { Menu, X } from 'lucide-react'
+import { ThemeSelect } from '@/components/theme-select'
+import { ClusterUiSelect } from './cluster/cluster-ui'
+import { WalletButton } from '@/components/solana/solana-provider'
+
+export function AppHeader({ links = [] }: { links: { label: string; path: string }[] }) {
+  const pathname = usePathname()
+  const [showMenu, setShowMenu] = useState(false)
+
+  function isActive(path: string) {
+    return path === '/' ? pathname === '/' : pathname.startsWith(path)
+  }
+
+  return (
+    <header className="relative z-50 px-4 py-2 bg-neutral-100 dark:bg-neutral-900 dark:text-neutral-400">
+      <div className="mx-auto flex justify-between items-center">
+        <div className="flex items-baseline gap-4">
+          <Link className="text-xl hover:text-neutral-500 dark:hover:text-white" href="/">
+            <span>ABL Token</span>
+          </Link>
+          <div className="hidden md:flex items-center">
+            <ul className="flex gap-4 flex-nowrap items-center">
+              {links.map(({ label, path }) => (
+                <li key={path}>
+                  <Link
+                    className={`hover:text-neutral-500 dark:hover:text-white ${isActive(path) ? 'text-neutral-500 dark:text-white' : ''}`}
+                    href={path}
+                  >
+                    {label}
+                  </Link>
+                </li>
+              ))}
+            </ul>
+          </div>
+        </div>
+
+        <Button variant="ghost" size="icon" className="md:hidden" onClick={() => setShowMenu(!showMenu)}>
+          {showMenu ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
+        </Button>
+
+        <div className="hidden md:flex items-center gap-4">
+          <WalletButton />
+          <ClusterUiSelect />
+          <ThemeSelect />
+        </div>
+
+        {showMenu && (
+          <div className="md:hidden fixed inset-x-0 top-[52px] bottom-0 bg-neutral-100/95 dark:bg-neutral-900/95 backdrop-blur-sm">
+            <div className="flex flex-col p-4 gap-4 border-t dark:border-neutral-800">
+              <ul className="flex flex-col gap-4">
+                {links.map(({ label, path }) => (
+                  <li key={path}>
+                    <Link
+                      className={`hover:text-neutral-500 dark:hover:text-white block text-lg py-2  ${isActive(path) ? 'text-neutral-500 dark:text-white' : ''} `}
+                      href={path}
+                      onClick={() => setShowMenu(false)}
+                    >
+                      {label}
+                    </Link>
+                  </li>
+                ))}
+              </ul>
+              <div className="flex flex-col gap-4">
+                <WalletButton />
+                <ClusterUiSelect />
+                <ThemeSelect />
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+    </header>
+  )
+}

+ 23 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-hero.tsx

@@ -0,0 +1,23 @@
+import React from 'react'
+
+export function AppHero({
+  children,
+  subtitle,
+  title,
+}: {
+  children?: React.ReactNode
+  subtitle?: React.ReactNode
+  title?: React.ReactNode
+}) {
+  return (
+    <div className="flex flex-row justify-center py-[16px] md:py-[64px]">
+      <div className="text-center">
+        <div className="max-w-2xl">
+          {typeof title === 'string' ? <h1 className="text-5xl font-bold">{title}</h1> : title}
+          {typeof subtitle === 'string' ? <p className="pt-4 md:py-6">{subtitle}</p> : subtitle}
+          {children}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 33 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-layout.tsx

@@ -0,0 +1,33 @@
+'use client'
+
+import { ThemeProvider } from './theme-provider'
+import { Toaster } from './ui/sonner'
+import { AppHeader } from '@/components/app-header'
+import React from 'react'
+import { AppFooter } from '@/components/app-footer'
+import { ClusterChecker } from '@/components/cluster/cluster-ui'
+import { AccountChecker } from '@/components/account/account-ui'
+
+export function AppLayout({
+  children,
+  links,
+}: {
+  children: React.ReactNode
+  links: { label: string; path: string }[]
+}) {
+  return (
+    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
+      <div className="flex flex-col min-h-screen">
+        <AppHeader links={links} />
+        <main className="flex-grow container mx-auto p-4">
+          <ClusterChecker>
+            <AccountChecker />
+          </ClusterChecker>
+          {children}
+        </main>
+        <AppFooter />
+      </div>
+      <Toaster />
+    </ThemeProvider>
+  )
+}

+ 38 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-modal.tsx

@@ -0,0 +1,38 @@
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
+import { ReactNode } from 'react'
+
+export function AppModal({
+  children,
+  title,
+  submit,
+  submitDisabled,
+  submitLabel,
+}: {
+  children: ReactNode
+  title: string
+  submit?: () => void
+  submitDisabled?: boolean
+  submitLabel?: string
+}) {
+  return (
+    <Dialog>
+      <DialogTrigger asChild>
+        <Button variant="outline">{title}</Button>
+      </DialogTrigger>
+      <DialogContent className="sm:max-w-[525px]">
+        <DialogHeader>
+          <DialogTitle>{title}</DialogTitle>
+        </DialogHeader>
+        <div className="grid gap-4 py-4">{children}</div>
+        <DialogFooter>
+          {submit ? (
+            <Button type="submit" onClick={submit} disabled={submitDisabled}>
+              {submitLabel || 'Save'}
+            </Button>
+          ) : null}
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  )
+}

+ 19 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/app-providers.tsx

@@ -0,0 +1,19 @@
+'use client'
+
+import { ThemeProvider } from '@/components/theme-provider'
+import { ReactQueryProvider } from './react-query-provider'
+import { ClusterProvider } from '@/components/cluster/cluster-data-access'
+import { SolanaProvider } from '@/components/solana/solana-provider'
+import React from 'react'
+
+export function AppProviders({ children }: Readonly<{ children: React.ReactNode }>) {
+  return (
+    <ReactQueryProvider>
+      <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
+        <ClusterProvider>
+          <SolanaProvider>{children}</SolanaProvider>
+        </ClusterProvider>
+      </ThemeProvider>
+    </ReactQueryProvider>
+  )
+}

+ 117 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/cluster/cluster-data-access.tsx

@@ -0,0 +1,117 @@
+'use client'
+
+import { clusterApiUrl, Connection } from '@solana/web3.js'
+import { atom, useAtomValue, useSetAtom } from 'jotai'
+import { atomWithStorage } from 'jotai/utils'
+import { createContext, ReactNode, useContext } from 'react'
+
+export interface SolanaCluster {
+  name: string
+  endpoint: string
+  network?: ClusterNetwork
+  active?: boolean
+}
+
+export enum ClusterNetwork {
+  Mainnet = 'mainnet-beta',
+  Testnet = 'testnet',
+  Devnet = 'devnet',
+  Custom = 'custom',
+}
+
+// By default, we don't configure the mainnet-beta cluster
+// The endpoint provided by clusterApiUrl('mainnet-beta') does not allow access from the browser due to CORS restrictions
+// To use the mainnet-beta cluster, provide a custom endpoint
+export const defaultClusters: SolanaCluster[] = [
+  {
+    name: 'devnet',
+    endpoint: clusterApiUrl('devnet'),
+    network: ClusterNetwork.Devnet,
+  },
+  { name: 'local', endpoint: 'http://localhost:8899' },
+  {
+    name: 'testnet',
+    endpoint: clusterApiUrl('testnet'),
+    network: ClusterNetwork.Testnet,
+  },
+]
+
+const clusterAtom = atomWithStorage<SolanaCluster>('solana-cluster', defaultClusters[0])
+const clustersAtom = atomWithStorage<SolanaCluster[]>('solana-clusters', defaultClusters)
+
+const activeClustersAtom = atom<SolanaCluster[]>((get) => {
+  const clusters = get(clustersAtom)
+  const cluster = get(clusterAtom)
+  return clusters.map((item) => ({
+    ...item,
+    active: item.name === cluster.name,
+  }))
+})
+
+const activeClusterAtom = atom<SolanaCluster>((get) => {
+  const clusters = get(activeClustersAtom)
+
+  return clusters.find((item) => item.active) || clusters[0]
+})
+
+export interface ClusterProviderContext {
+  cluster: SolanaCluster
+  clusters: SolanaCluster[]
+  addCluster: (cluster: SolanaCluster) => void
+  deleteCluster: (cluster: SolanaCluster) => void
+  setCluster: (cluster: SolanaCluster) => void
+
+  getExplorerUrl(path: string): string
+}
+
+const Context = createContext<ClusterProviderContext>({} as ClusterProviderContext)
+
+export function ClusterProvider({ children }: { children: ReactNode }) {
+  const cluster = useAtomValue(activeClusterAtom)
+  const clusters = useAtomValue(activeClustersAtom)
+  const setCluster = useSetAtom(clusterAtom)
+  const setClusters = useSetAtom(clustersAtom)
+
+  const value: ClusterProviderContext = {
+    cluster,
+    clusters: clusters.sort((a, b) => (a.name > b.name ? 1 : -1)),
+    addCluster: (cluster: SolanaCluster) => {
+      try {
+        new Connection(cluster.endpoint)
+        setClusters([...clusters, cluster])
+      } catch (err) {
+        console.error(`${err}`)
+      }
+    },
+    deleteCluster: (cluster: SolanaCluster) => {
+      setClusters(clusters.filter((item) => item.name !== cluster.name))
+    },
+    setCluster: (cluster: SolanaCluster) => setCluster(cluster),
+    getExplorerUrl: (path: string) => `https://explorer.solana.com/${path}${getClusterUrlParam(cluster)}`,
+  }
+  return <Context.Provider value={value}>{children}</Context.Provider>
+}
+
+export function useCluster() {
+  return useContext(Context)
+}
+
+function getClusterUrlParam(cluster: SolanaCluster): string {
+  let suffix = ''
+  switch (cluster.network) {
+    case ClusterNetwork.Devnet:
+      suffix = 'devnet'
+      break
+    case ClusterNetwork.Mainnet:
+      suffix = ''
+      break
+    case ClusterNetwork.Testnet:
+      suffix = 'testnet'
+      break
+    default:
+      suffix = `custom&customUrl=${encodeURIComponent(cluster.endpoint)}`
+      break
+  }
+
+  return suffix.length ? `?cluster=${suffix}` : ''
+}

+ 72 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/cluster/cluster-ui.tsx

@@ -0,0 +1,72 @@
+'use client'
+
+import { useConnection } from '@solana/wallet-adapter-react'
+
+import { useQuery } from '@tanstack/react-query'
+import * as React from 'react'
+import { ReactNode } from 'react'
+
+import { useCluster } from './cluster-data-access'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
+import { Button } from '@/components/ui/button'
+import { AppAlert } from '@/components/app-alert'
+
+export function ExplorerLink({ path, label, className }: { path: string; label: string; className?: string }) {
+  const { getExplorerUrl } = useCluster()
+  return (
+    <a
+      href={getExplorerUrl(path)}
+      target="_blank"
+      rel="noopener noreferrer"
+      className={className ? className : `link font-mono`}
+    >
+      {label}
+    </a>
+  )
+}
+
+export function ClusterChecker({ children }: { children: ReactNode }) {
+  const { cluster } = useCluster()
+  const { connection } = useConnection()
+
+  const query = useQuery({
+    queryKey: ['version', { cluster, endpoint: connection.rpcEndpoint }],
+    queryFn: () => connection.getVersion(),
+    retry: 1,
+  })
+  if (query.isLoading) {
+    return null
+  }
+  if (query.isError || !query.data) {
+    return (
+      <AppAlert
+        action={
+          <Button variant="outline" onClick={() => query.refetch()}>
+            Refresh
+          </Button>
+        }
+      >
+        Error connecting to cluster <span className="font-bold">{cluster.name}</span>.
+      </AppAlert>
+    )
+  }
+  return children
+}
+
+export function ClusterUiSelect() {
+  const { clusters, setCluster, cluster } = useCluster()
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button variant="outline">{cluster.name}</Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end">
+        {clusters.map((item) => (
+          <DropdownMenuItem key={item.name} onClick={() => setCluster(item)}>
+            {item.name}
+          </DropdownMenuItem>
+        ))}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 34 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/dashboard/dashboard-feature.tsx

@@ -0,0 +1,34 @@
+import { AppHero } from '@/components/app-hero'
+
+const links: { label: string; href: string }[] = [
+  { label: 'Solana Docs', href: 'https://docs.solana.com/' },
+  { label: 'Solana Faucet', href: 'https://faucet.solana.com/' },
+  { label: 'Solana Cookbook', href: 'https://solana.com/developers/cookbook/' },
+  { label: 'Solana Stack Overflow', href: 'https://solana.stackexchange.com/' },
+  { label: 'Solana Developers GitHub', href: 'https://github.com/solana-developers/' },
+]
+
+export function DashboardFeature() {
+  return (
+    <div>
+      <AppHero title="gm" subtitle="Say hi to your new Solana app." />
+      <div className="max-w-xl mx-auto py-6 sm:px-6 lg:px-8 text-center">
+        <div className="space-y-2">
+          <p>Here are some helpful links to get you started.</p>
+          {links.map((link, index) => (
+            <div key={index}>
+              <a
+                href={link.href}
+                className="hover:text-gray-500 dark:hover:text-gray-300"
+                target="_blank"
+                rel="noopener noreferrer"
+              >
+                {link.label}
+              </a>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 31 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/react-query-provider.tsx

@@ -0,0 +1,31 @@
+// Taken from https://tanstack.com/query/5/docs/framework/react/guides/advanced-ssr
+'use client'
+
+import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'
+
+function makeQueryClient() {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        staleTime: 60 * 1000,
+      },
+    },
+  })
+}
+
+let browserQueryClient: QueryClient | undefined = undefined
+
+function getQueryClient() {
+  if (isServer) {
+    return makeQueryClient()
+  } else {
+    if (!browserQueryClient) browserQueryClient = makeQueryClient()
+    return browserQueryClient
+  }
+}
+
+export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
+  const queryClient = getQueryClient()
+
+  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+}

+ 43 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/solana/solana-provider.tsx

@@ -0,0 +1,43 @@
+'use client'
+
+import { WalletError } from '@solana/wallet-adapter-base'
+import {
+  AnchorWallet,
+  ConnectionProvider,
+  useConnection,
+  useWallet,
+  WalletProvider,
+} from '@solana/wallet-adapter-react'
+import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'
+import dynamic from 'next/dynamic'
+import { ReactNode, useCallback, useMemo } from 'react'
+import { useCluster } from '../cluster/cluster-data-access'
+import '@solana/wallet-adapter-react-ui/styles.css'
+import { AnchorProvider } from '@coral-xyz/anchor'
+
+export const WalletButton = dynamic(async () => (await import('@solana/wallet-adapter-react-ui')).WalletMultiButton, {
+  ssr: false,
+})
+
+export function SolanaProvider({ children }: { children: ReactNode }) {
+  const { cluster } = useCluster()
+  const endpoint = useMemo(() => cluster.endpoint, [cluster])
+  const onError = useCallback((error: WalletError) => {
+    console.error(error)
+  }, [])
+
+  return (
+    <ConnectionProvider endpoint={endpoint}>
+      <WalletProvider wallets={[]} onError={onError} autoConnect={true}>
+        <WalletModalProvider>{children}</WalletModalProvider>
+      </WalletProvider>
+    </ConnectionProvider>
+  )
+}
+
+export function useAnchorProvider() {
+  const { connection } = useConnection()
+  const wallet = useWallet()
+
+  return new AnchorProvider(connection, wallet as AnchorWallet, { commitment: 'confirmed' })
+}

+ 8 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/theme-provider.tsx

@@ -0,0 +1,8 @@
+'use client'
+
+import * as React from 'react'
+import { ThemeProvider as NextThemesProvider } from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
+  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
+}

+ 29 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/theme-select.tsx

@@ -0,0 +1,29 @@
+'use client'
+
+import * as React from 'react'
+import { Moon, Sun } from 'lucide-react'
+import { useTheme } from 'next-themes'
+
+import { Button } from '@/components/ui/button'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
+
+export function ThemeSelect() {
+  const { setTheme } = useTheme()
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button variant="outline" size="icon">
+          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
+          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
+          <span className="sr-only">Toggle theme</span>
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end">
+        <DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
+        <DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
+        <DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 51 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/alert.tsx

@@ -0,0 +1,51 @@
+import * as React from 'react'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const alertVariants = cva(
+  'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+  {
+    variants: {
+      variant: {
+        default: 'bg-card text-card-foreground',
+        destructive:
+          'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
+        warning:
+          'text-yellow-500 bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-yellow-500/90 border-yellow-500 dark:bg-yellow-900/10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+)
+
+function Alert({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
+  return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-title"
+      className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-description"
+      className={cn(
+        'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Alert, AlertTitle, AlertDescription }

+ 50 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/button.tsx

@@ -0,0 +1,50 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+        destructive:
+          'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+        outline:
+          'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+        secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+        ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+        link: 'text-primary underline-offset-4 hover:underline',
+      },
+      size: {
+        default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+        sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+        icon: 'size-9',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  },
+)
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<'button'> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean
+  }) {
+  const Comp = asChild ? Slot : 'button'
+
+  return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
+}
+
+export { Button, buttonVariants }

+ 54 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/card.tsx

@@ -0,0 +1,54 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Card({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card"
+      className={cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', className)}
+      {...props}
+    />
+  )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-header"
+      className={cn(
+        '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return <div data-slot="card-title" className={cn('leading-none font-semibold', className)} {...props} />
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+  return <div data-slot="card-description" className={cn('text-muted-foreground text-sm', className)} {...props} />
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-action"
+      className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
+      {...props}
+    />
+  )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return <div data-slot="card-content" className={cn('px-6', className)} {...props} />
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return <div data-slot="card-footer" className={cn('flex items-center px-6 [.border-t]:pt-6', className)} {...props} />
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }

+ 111 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/dialog.tsx

@@ -0,0 +1,111 @@
+'use client'
+
+import * as React from 'react'
+import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { XIcon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />
+}
+
+function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
+}
+
+function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
+}
+
+function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
+}
+
+function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+        <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
+          <XIcon />
+          <span className="sr-only">Close</span>
+        </DialogPrimitive.Close>
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
+      {...props}
+    />
+  )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
+      {...props}
+    />
+  )
+}
+
+function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn('text-lg leading-none font-semibold', className)}
+      {...props}
+    />
+  )
+}
+
+function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn('text-muted-foreground text-sm', className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+}

+ 219 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/dropdown-menu.tsx

@@ -0,0 +1,219 @@
+'use client'
+
+import * as React from 'react'
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
+}
+
+function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+  return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+}
+
+function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+  return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
+}
+
+function DropdownMenuContent({
+  className,
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+  return (
+    <DropdownMenuPrimitive.Portal>
+      <DropdownMenuPrimitive.Content
+        data-slot="dropdown-menu-content"
+        sideOffset={sideOffset}
+        className={cn(
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
+          className,
+        )}
+        {...props}
+      />
+    </DropdownMenuPrimitive.Portal>
+  )
+}
+
+function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+  return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = 'default',
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+  inset?: boolean
+  variant?: 'default' | 'destructive'
+}) {
+  return (
+    <DropdownMenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+  return (
+    <DropdownMenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.CheckboxItem>
+  )
+}
+
+function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+  return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+  return (
+    <DropdownMenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.RadioItem>
+  )
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.Label
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+  return (
+    <DropdownMenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn('bg-border -mx-1 my-1 h-px', className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.SubTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto size-4" />
+    </DropdownMenuPrimitive.SubTrigger>
+  )
+}
+
+function DropdownMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+  return (
+    <DropdownMenuPrimitive.SubContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+}

+ 21 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/input.tsx

@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+        'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
+        'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Input }

+ 21 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/label.tsx

@@ -0,0 +1,21 @@
+'use client'
+
+import * as React from 'react'
+import * as LabelPrimitive from '@radix-ui/react-label'
+
+import { cn } from '@/lib/utils'
+
+function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  return (
+    <LabelPrimitive.Root
+      data-slot="label"
+      className={cn(
+        'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Label }

+ 25 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/sonner.tsx

@@ -0,0 +1,25 @@
+'use client'
+
+import { useTheme } from 'next-themes'
+import { Toaster as Sonner, ToasterProps } from 'sonner'
+
+const Toaster = ({ ...props }: ToasterProps) => {
+  const { theme = 'system' } = useTheme()
+
+  return (
+    <Sonner
+      theme={theme as ToasterProps['theme']}
+      className="toaster group"
+      style={
+        {
+          '--normal-bg': 'var(--popover)',
+          '--normal-text': 'var(--popover-foreground)',
+          '--normal-border': 'var(--border)',
+        } as React.CSSProperties
+      }
+      {...props}
+    />
+  )
+}
+
+export { Toaster }

+ 75 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/ui/table.tsx

@@ -0,0 +1,75 @@
+'use client'
+
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Table({ className, ...props }: React.ComponentProps<'table'>) {
+  return (
+    <div data-slot="table-container" className="relative w-full overflow-x-auto">
+      <table data-slot="table" className={cn('w-full caption-bottom text-sm', className)} {...props} />
+    </div>
+  )
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
+  return <thead data-slot="table-header" className={cn('[&_tr]:border-b', className)} {...props} />
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
+  return <tbody data-slot="table-body" className={cn('[&_tr:last-child]:border-0', className)} {...props} />
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
+  return (
+    <tfoot
+      data-slot="table-footer"
+      className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
+      {...props}
+    />
+  )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
+  return (
+    <tr
+      data-slot="table-row"
+      className={cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', className)}
+      {...props}
+    />
+  )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
+  return (
+    <th
+      data-slot="table-head"
+      className={cn(
+        'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
+  return (
+    <td
+      data-slot="table-cell"
+      className={cn(
+        'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
+        className,
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
+  return (
+    <caption data-slot="table-caption" className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
+  )
+}
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

+ 33 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/use-transaction-toast.tsx

@@ -0,0 +1,33 @@
+import { toast } from 'sonner'
+import { ExplorerLink } from './cluster/cluster-ui'
+import { Connection, SendTransactionError } from '@solana/web3.js'
+
+export function useTransactionToast() {
+  return (signature: string) => {
+    toast('Transaction sent', {
+      description: <ExplorerLink path={`tx/${signature}`} label="View Transaction" />,
+    })
+  }
+}
+
+export function useTransactionErrorToast() {
+  return async (error: Error, connection: Connection) => {
+    const logs = await (error as SendTransactionError).getLogs(connection);
+    const anchorError = logs.find((l) => l.startsWith("Program log: AnchorError occurred"));
+    if (anchorError) {
+      if (anchorError.includes("WalletBlocked")) {
+        toast.error(`Destination wallet is blocked from receiving funds.`)
+      } else if (anchorError.includes("WalletNotAllowed")) {
+        toast.error(`Destination wallet is not allowed to receive funds.`)
+      } else if (anchorError.includes("AmountNotAllowed")) {
+        toast.error(`Destination wallet is not authorized to receive this amount.`)
+      } else {
+        console.log("ERROR: ", error)
+        toast.error(`Failed to run program: ${error}`)
+      }
+    } else {
+      console.log("ERROR: ", error)
+      toast.error(`Failed to run program: ${error}`)
+    }
+  }
+}

+ 13 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/src/lib/utils.ts

@@ -0,0 +1,13 @@
+import { type ClassValue, clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs))
+}
+
+export function ellipsify(str = '', len = 4, delimiter = '..') {
+  const strLen = str.length
+  const limit = len * 2 + delimiter.length
+
+  return strLen >= limit ? str.substring(0, len) + delimiter + str.substring(strLen - len, strLen) : str
+}

+ 29 - 0
tokens/token-2022/transfer-hook/allow-block-list-token/tsconfig.json

@@ -0,0 +1,29 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "baseUrl": ".",
+    "paths": {
+      "@project/anchor": ["anchor/src"],
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}