Skip to content

Commit bffe729

Browse files
Harsh-H-Shahclaude
andcommitted
fix(allow-block-list-token): address review feedback on kit migration
- remove_wallet.rs: declare ab_wallet's PDA seeds (seeds = [AB_WALLET_SEED, wallet.key()]) instead of requiring the caller to pre-derive and pass the PDA directly, so getRemoveWalletInstructionAsync({ authority, wallet }) resolves it the same way getInitWalletInstructionAsync already does. Also aligns config's seeds with the CONFIG_SEED constant, matching init_wallet.rs. Regenerated the IDL/client and simplified the two frontend callers (removeWallet, processBatchWallets) accordingly. - Bump @solana/kit and @solana/program-client-core to ^7.1.0 (both the app's own deps and the generated client's peerDependencies, via a new dependencyVersions option on the codama renderVisitor call) and @solana-program/token-2022 to ^0.15.0. - cluster-data-access.tsx: drop useClusterRpc/deriveWebsocketUrl in favor of @solana/connector's useSolanaClient across every consumer, and fix addCluster's endpoint validation, which silently accepted any string - createSolanaRpc doesn't parse its endpoint eagerly despite a comment claiming otherwise. new URL(endpoint) is the actual check. - use-send-instruction.ts: adopt @solana/connector's useTransactionPreparer for blockhash + simulation-derived compute unit limit, sourcing rpc/rpcSubscriptions for the send-and-confirm step from useSolanaClient instead of the removed custom hook. (client.sendAndConfirmTransaction, suggested in review, doesn't actually exist in the installed - and latest published - @solana/connector@0.2.6, despite one JSDoc example; kept sendAndConfirmTransactionFactory from kit for that step.) - account-data-access.tsx: useSendTokens now resolves the transfer-hook's extra accounts via @solana-program/token-2022's getTransferCheckedWithTransferHookInstructionAsync (reads the mint's on-chain extra-account-metas list) instead of hardcoding this program's ab_wallet PDA convention client-side. useRequestAirdrop now uses kit's airdropFactory, which confirms the airdrop instead of returning immediately after requesting it. useTransferSol now checks signer.address against the viewed account instead of silently signing with a possibly-different connected wallet than the page's address. (useGetBalance/useGetTokenAccounts/useGetSignatures stay on a cluster-scoped RPC call, not connector's useBalance/useTokens/ useTransactions - those hooks are scoped to the connected wallet only and don't take an address, so they can't back the generic /account/[address] page, which needs to read arbitrary addresses.) - abl-token-data-access.tsx: fixed transferHookAuthority being set to mintAuthority instead of the form's own transferHookAuthority field (a legacy bug predating this migration). mintTo now uses getMintToATAInstructionPlanAsync + flattenInstructionPlan instead of manually assembling the create-ATA and mint-to instructions. - Added anchor/tests/basic.test.ts: LiteSVM-backed tests exercising the generated Kit client directly (init_config, init_wallet, the new seeds-based remove_wallet, and an authority-mismatch rejection case). This project's `anchor test` has no local-validator step to test against - its Anchor.toml [scripts] test command fully replaces Anchor's normal build+validator+deploy flow - so a real RPC connection isn't available; LiteSVM gives the TS client something real to run against without one. The old placeholder test never actually exercised anything. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4636ecd commit bffe729

13 files changed

Lines changed: 461 additions & 211 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
use anchor_lang::prelude::*;
22

3-
use crate::{ABWallet, Config};
3+
use crate::{ABWallet, Config, AB_WALLET_SEED, CONFIG_SEED};
44

55
#[derive(Accounts)]
66
pub struct RemoveWallet<'info> {
77
#[account(mut)]
88
pub authority: Signer<'info>,
99

1010
#[account(
11-
seeds = [b"config"],
11+
seeds = [CONFIG_SEED],
1212
bump = config.bump,
1313
has_one = authority,
1414
)]
1515
pub config: Box<Account<'info, Config>>,
1616

17+
pub wallet: SystemAccount<'info>,
18+
1719
#[account(
1820
mut,
1921
close = authority,
22+
seeds = [AB_WALLET_SEED, wallet.key().as_ref()],
23+
bump,
2024
)]
2125
pub ab_wallet: Account<'info, ABWallet>,
2226

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,139 @@
1-
import type { Program } from '@anchor-lang/core';
2-
import * as anchor from '@anchor-lang/core';
3-
import type { AblToken } from '../target/types/abl_token';
1+
import * as path from 'node:path';
2+
import {
3+
appendTransactionMessageInstructions,
4+
createTransactionMessage,
5+
generateKeyPairSigner,
6+
lamports,
7+
pipe,
8+
setTransactionMessageFeePayerSigner,
9+
signTransactionMessageWithSigners,
10+
type Instruction,
11+
type KeyPairSigner,
12+
} from '@solana/kit';
13+
import { assert } from 'chai';
14+
import { FailedTransactionMetadata, LiteSVM } from 'litesvm';
15+
import { decodeABWallet } from '../../src/generated/accounts/aBWallet';
16+
import { decodeConfig } from '../../src/generated/accounts/config';
17+
import {
18+
getInitConfigInstructionAsync,
19+
getInitWalletInstructionAsync,
20+
getRemoveWalletInstructionAsync,
21+
} from '../../src/generated/instructions';
22+
import { findAbWalletPda, findConfigPda } from '../../src/generated/pdas';
23+
import { ABL_TOKEN_PROGRAM_ADDRESS } from '../../src/generated/programs';
424

5-
describe('abl-token', () => {
6-
// Configure the client to use the local cluster.
7-
anchor.setProvider(anchor.AnchorProvider.env());
25+
// The Codama-generated Kit client is what the webapp actually talks to, so these tests
26+
// exercise it directly against a LiteSVM instance loaded with the built program - proving
27+
// the generated instruction builders, PDA derivation, and account decoders are wired up
28+
// correctly, which the Rust-side unit/litesvm tests (which never touch the TS client) don't
29+
// cover. There's no local validator available in this project's `anchor test` flow (the
30+
// custom [scripts] test command replaces Anchor's normal build+validator+deploy pipeline
31+
// entirely), so a real RPC connection isn't an option here.
32+
const PROGRAM_SO = path.join(__dirname, '..', 'target', 'deploy', 'abl_token.so');
833

9-
const _program = anchor.workspace.ABLToken as Program<AblToken>;
34+
describe('abl-token (Kit client, via LiteSVM)', () => {
35+
let svm: LiteSVM;
36+
let authority: KeyPairSigner;
1037

11-
it('should run the program', async () => {
12-
// Add your test here.
38+
before(async () => {
39+
svm = new LiteSVM();
40+
svm.addProgramFromFile(ABL_TOKEN_PROGRAM_ADDRESS, PROGRAM_SO);
41+
authority = await generateKeyPairSigner();
42+
svm.airdrop(authority.address, lamports(BigInt(10_000_000_000)));
43+
});
44+
45+
async function send(instructions: Instruction | Instruction[], payer: KeyPairSigner = authority) {
46+
const transactionMessage = pipe(
47+
createTransactionMessage({ version: 0 }),
48+
m => setTransactionMessageFeePayerSigner(payer, m),
49+
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
50+
m => appendTransactionMessageInstructions(Array.isArray(instructions) ? instructions : [instructions], m),
51+
);
52+
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
53+
const result = svm.sendTransaction(signedTransaction);
54+
if (result instanceof FailedTransactionMetadata) {
55+
throw new Error(`Transaction failed: ${result.toString()}`);
56+
}
57+
return result;
58+
}
59+
60+
it('initializes the config, owned by the payer', async () => {
61+
const ix = await getInitConfigInstructionAsync(
62+
{ payer: authority },
63+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
64+
);
65+
await send(ix);
66+
67+
const [configPda] = await findConfigPda({ programAddress: ABL_TOKEN_PROGRAM_ADDRESS });
68+
const account = svm.getAccount(configPda);
69+
if (!account?.exists) throw new Error('Config account not found');
70+
71+
const config = decodeConfig({ ...account, address: configPda });
72+
assert.equal(config.data.authority, authority.address);
73+
});
74+
75+
it('adds a wallet to the list, then removes it by wallet address alone', async () => {
76+
const wallet = await generateKeyPairSigner();
77+
78+
const initIx = await getInitWalletInstructionAsync(
79+
{ authority, wallet: wallet.address, allowed: true },
80+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
81+
);
82+
await send(initIx);
83+
84+
const [abWalletPda] = await findAbWalletPda(
85+
{ wallet: wallet.address },
86+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
87+
);
88+
const created = svm.getAccount(abWalletPda);
89+
if (!created?.exists) throw new Error('ab_wallet account was not created');
90+
91+
const decoded = decodeABWallet({ ...created, address: abWalletPda });
92+
assert.equal(decoded.data.wallet, wallet.address);
93+
assert.isTrue(decoded.data.allowed);
94+
95+
// `getRemoveWalletInstructionAsync` used to require the caller to pre-derive and pass
96+
// the `ab_wallet` PDA by hand; now that the Rust account declares its own seeds, it
97+
// resolves `ab_wallet` from `wallet` the same way `getInitWalletInstructionAsync` does.
98+
const removeIx = await getRemoveWalletInstructionAsync(
99+
{ authority, wallet: wallet.address },
100+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
101+
);
102+
await send(removeIx);
103+
104+
const closed = svm.getAccount(abWalletPda);
105+
assert.isTrue(!closed?.exists || closed.data.length === 0, 'ab_wallet account should be closed');
106+
});
107+
108+
it('rejects removing a wallet for a caller who is not the config authority', async () => {
109+
const wallet = await generateKeyPairSigner();
110+
const initIx = await getInitWalletInstructionAsync(
111+
{ authority, wallet: wallet.address, allowed: false },
112+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
113+
);
114+
await send(initIx);
115+
116+
const impostor = await generateKeyPairSigner();
117+
svm.airdrop(impostor.address, lamports(BigInt(10_000_000_000)));
118+
119+
const removeIx = await getRemoveWalletInstructionAsync(
120+
{ authority: impostor, wallet: wallet.address },
121+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
122+
);
123+
124+
let threw = false;
125+
try {
126+
await send(removeIx, impostor);
127+
} catch {
128+
threw = true;
129+
}
130+
assert.isTrue(threw, 'expected the has_one authority check to reject a non-authority caller');
131+
132+
const [abWalletPda] = await findAbWalletPda(
133+
{ wallet: wallet.address },
134+
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
135+
);
136+
const stillThere = svm.getAccount(abWalletPda);
137+
if (!stillThere?.exists) throw new Error('ab_wallet account should still exist');
13138
});
14139
});

tokens/token-2022/transfer-hook/allow-block-list-token/idl/abl_token.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,34 @@
364364
]
365365
}
366366
},
367+
{
368+
"name": "wallet"
369+
},
367370
{
368371
"name": "ab_wallet",
369-
"writable": true
372+
"writable": true,
373+
"pda": {
374+
"seeds": [
375+
{
376+
"kind": "const",
377+
"value": [
378+
97,
379+
98,
380+
95,
381+
119,
382+
97,
383+
108,
384+
108,
385+
101,
386+
116
387+
]
388+
},
389+
{
390+
"kind": "account",
391+
"path": "wallet"
392+
}
393+
]
394+
}
370395
},
371396
{
372397
"name": "system_program",

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
"@radix-ui/react-label": "^2.1.7",
2727
"@radix-ui/react-slot": "^1.2.3",
2828
"@solana-program/system": "^0.13.0",
29-
"@solana-program/token-2022": "^0.14.1",
29+
"@solana-program/token-2022": "^0.15.0",
3030
"@solana/connector": "^0.2.6",
31-
"@solana/kit": "^7.0.0",
32-
"@solana/program-client-core": "^7.0.0",
31+
"@solana/instruction-plans": "^7.1.0",
32+
"@solana/kit": "^7.1.0",
33+
"@solana/program-client-core": "^7.1.0",
3334
"@solana/web3.js": "^1.98.4",
3435
"@tanstack/react-query": "^5.82.0",
3536
"class-variance-authority": "^0.7.1",
@@ -58,6 +59,7 @@
5859
"codama": "^1.10.0",
5960
"eslint": "^9.25.1",
6061
"eslint-config-next": "15.3.1",
62+
"litesvm": "^1.3.0",
6163
"mocha": "^11.7.5",
6264
"prettier": "^3.5.3",
6365
"tailwindcss": "^4.1.4",

0 commit comments

Comments
 (0)