Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Cargo.lock
games/world-cup/
games/gacha/pinocchio/idl/
tokens/token-2022/transfer-hook/block-list/pinocchio/sdk/
tokens/token-2022/transfer-hook/allow-block-list-token/idl/
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@
package-lock.json
pnpm-lock.yaml
yarn.lock

# Anchor IDL copied verbatim from `anchor build` output (anchor/target/idl/abl_token.json);
# the input `pnpm run generate-client` reads, never hand-edited.
/idl
# Codama-generated Kit client; already formatted by the renderer itself.
/src/generated
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Compile the program (make sure to replace your program ID):
Compile the UI:
`yarn run build`

The UI talks to the program through a Codama-generated client under `src/generated`, rebuilt by
`pnpm run generate-client` (which `dev` and `build` run for you). It reads the IDL emitted by
`anchor build` when one is present, falling back to the committed copy in `idl/`, so a program ID
change picks up automatically — commit the refreshed `idl/abl_token.json` and `src/generated` when
the change is meant to be permanent.

Serve the UI:
`yarn run dev`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
use anchor_lang::prelude::*;

use crate::{ABWallet, Config};
use crate::{ABWallet, Config, AB_WALLET_SEED, CONFIG_SEED};

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

#[account(
seeds = [b"config"],
seeds = [CONFIG_SEED],
bump = config.bump,
has_one = authority,
)]
pub config: Box<Account<'info, Config>>,

pub wallet: SystemAccount<'info>,

#[account(
mut,
close = authority,
seeds = [AB_WALLET_SEED, wallet.key().as_ref()],
bump,
)]
pub ab_wallet: Account<'info, ABWallet>,

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,135 @@
import type { Program } from '@anchor-lang/core';
import * as anchor from '@anchor-lang/core';
import type { AblToken } from '../target/types/abl_token';
import * as path from 'node:path';
import {
appendTransactionMessageInstructions,
createTransactionMessage,
generateKeyPairSigner,
lamports,
pipe,
setTransactionMessageFeePayerSigner,
signTransactionMessageWithSigners,
type Instruction,
type KeyPairSigner,
} from '@solana/kit';
import { assert } from 'chai';
import { FailedTransactionMetadata, LiteSVM } from 'litesvm';
import { decodeABWallet } from '../../src/generated/accounts/aBWallet';
import { decodeConfig } from '../../src/generated/accounts/config';
import {
getInitConfigInstructionAsync,
getInitWalletInstructionAsync,
getRemoveWalletInstructionAsync,
} from '../../src/generated/instructions';
import { findAbWalletPda, findConfigPda } from '../../src/generated/pdas';
import { ABL_TOKEN_PROGRAM_ADDRESS } from '../../src/generated/programs';

describe('abl-token', () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
// Exercises the Codama-generated Kit client - the same client the webapp uses - against a
// LiteSVM instance loaded with the built program, covering the generated instruction
// builders, PDA derivation, and account decoders. LiteSVM keeps the suite independent of a
// validator, whose ephemeral program id would not match the client's `declare_id!`.
const PROGRAM_SO = path.join(__dirname, '..', 'target', 'deploy', 'abl_token.so');

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

it('should run the program', async () => {
// Add your test here.
before(async () => {
svm = new LiteSVM();
svm.addProgramFromFile(ABL_TOKEN_PROGRAM_ADDRESS, PROGRAM_SO);
authority = await generateKeyPairSigner();
svm.airdrop(authority.address, lamports(BigInt(10_000_000_000)));
});

async function send(instructions: Instruction | Instruction[], payer: KeyPairSigner = authority) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayerSigner(payer, m),
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
m => appendTransactionMessageInstructions(Array.isArray(instructions) ? instructions : [instructions], m),
);
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const result = svm.sendTransaction(signedTransaction);
if (result instanceof FailedTransactionMetadata) {
throw new Error(`Transaction failed: ${result.toString()}`);
}
return result;
}

it('initializes the config, owned by the payer', async () => {
const ix = await getInitConfigInstructionAsync(
{ payer: authority },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
await send(ix);

const [configPda] = await findConfigPda({ programAddress: ABL_TOKEN_PROGRAM_ADDRESS });
const account = svm.getAccount(configPda);
if (!account?.exists) throw new Error('Config account not found');

const config = decodeConfig({ ...account, address: configPda });
assert.equal(config.data.authority, authority.address);
});

it('adds a wallet to the list, then removes it by wallet address alone', async () => {
const wallet = await generateKeyPairSigner();

const initIx = await getInitWalletInstructionAsync(
{ authority, wallet: wallet.address, allowed: true },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
await send(initIx);

const [abWalletPda] = await findAbWalletPda(
{ wallet: wallet.address },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
const created = svm.getAccount(abWalletPda);
if (!created?.exists) throw new Error('ab_wallet account was not created');

const decoded = decodeABWallet({ ...created, address: abWalletPda });
assert.equal(decoded.data.wallet, wallet.address);
assert.isTrue(decoded.data.allowed);

// `ab_wallet` is resolved from `wallet` by the generated client, using the seeds the
// Rust account declares.
const removeIx = await getRemoveWalletInstructionAsync(
{ authority, wallet: wallet.address },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
await send(removeIx);

const closed = svm.getAccount(abWalletPda);
assert.isTrue(!closed?.exists || closed.data.length === 0, 'ab_wallet account should be closed');
});

it('rejects removing a wallet for a caller who is not the config authority', async () => {
const wallet = await generateKeyPairSigner();
const initIx = await getInitWalletInstructionAsync(
{ authority, wallet: wallet.address, allowed: false },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
await send(initIx);

const impostor = await generateKeyPairSigner();
svm.airdrop(impostor.address, lamports(BigInt(10_000_000_000)));

const removeIx = await getRemoveWalletInstructionAsync(
{ authority: impostor, wallet: wallet.address },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);

let threw = false;
try {
await send(removeIx, impostor);
} catch {
threw = true;
}
assert.isTrue(threw, 'expected the has_one authority check to reject a non-authority caller');

const [abWalletPda] = await findAbWalletPda(
{ wallet: wallet.address },
{ programAddress: ABL_TOKEN_PROGRAM_ADDRESS },
);
const stillThere = svm.getAccount(abWalletPda);
if (!stillThere?.exists) throw new Error('ab_wallet account should still exist');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')];
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
// Codama-generated client — regenerated on every build (`pnpm run generate-client`),
// never hand-edited, so it's not worth linting.
ignores: ['src/generated/**'],
},
];

export default eslintConfig;
Loading
Loading