Skip to content

Commit ca06045

Browse files
test: Lockup Tranched (#315)
* test: Lockup Tranched Add full test coverage for Lockup Tranched streams including: - createWithTimestampsLt and createWithDurationsLt test suites - cancelLt operation tests with various stream states - withdrawLt operation tests with partial/full withdrawals - Rename LL tests for clarity: cancel, renounce, withdraw - Extend test context with LT stream creation and assertion helpers - Add MAX_U64 constant for timestamp overflow validation - Update view tests (streamedAmountOf, withdrawableAmountOf) for LT - Add tranched amount/time fixtures and types * test(lockup): refactor type names and update imports - Rename TranchedAmount to TranchedAmounts for consistency - Rename TranchedTime to TranchedTimes - Import toBn helper for test utilities - Import type BN from bn.js instead of default import - Add formatting rule to Solana skill: require full-check after changes * test(lockup): rename Amount to LinearAmounts for clarity * test(lockup): improve test structure and coverage for tranched streams - Refactor cancelLt tests with clearer nesting (cold/warm streams, DEPLETED/CANCELED/SETTLED statuses) - Add tests for recipient ATA creation in withdrawLt when asset ATAs don't exist - Add Token2022 withdrawal test for tranched streams - Add test for too many tranches validation (31 tranches) - Add tests for sender ATA and token balance validation in createWithTimestampsLt - Add withdrawMax tests for tranched streams (TRANCHE_1 and END scenarios) - Add totalDuration and isCancelable params to createWithDurationsLl test helper - Clean up formatting and consolidate imports - Remove orphaned non-cancelable test that's now covered in warm stream tests * test(lockup): improve test structure for tranched and linear streams Restructure test organization to follow binary pair pattern for conditional branches. Rename TOKEN to SPL_TOKEN for clarity. Consolidate context setup and improve test readability across all Lockup and MerkleInstant modules. * test(lockup): enforce naming and grammar conventions in test suites - Rename describe blocks to match test filenames (e.g., cancelLl not cancel) - Add ERR_ prefix to all Anchor error aliases per new convention - Fix test label grammar: include articles and verbs (e.g., "when signer is not sender") - Extract conditions from it() blocks into wrapping describe() blocks - Update CLAUDE.md with testing standards and conventions * test(lockup): refactor test helpers and improve context interfaces - Improve type safety in context helper methods by adding explicit parameter types - Simplify salt parameter handling with nullish coalescing instead of isNeg() checks - Fix withdrawMax helper to accept Keypair for consistency with buildSignAndProcessTx - Update test files to use improved helper interfaces * test: replace beforeAll with beforeEach in test setup hooks * test(lockup): extract assertStreamCreation to shared utilities Moves duplicated assertStreamCreation function from createWithTimestampsLl and createWithTimestampsLt test files into utils/assertions.ts. Updates call sites to pass ctx as first argument. Removes unused helper function from collectFees.test.ts. * test(lockup): centralize and rename test helpers for consistency - Extract postCancelAssertions and postWithdrawAssertions to shared utilities - Rename UNLOCK_AMOUNTS to LINEAR_UNLOCK_AMOUNTS for clarity - Remove unused imports from test files - Fix test label grammar ("it it should" → "should", "non zero" → "non-zero") - Restructure createWithDurationsLt test hierarchy for better validation flow * test(lockup): use TranchedDurations for stream duration calculations Replace TranchedTimes-based duration calculations with pre-computed TranchedDurations constants. Simplifies context setup by eliminating repeated subtract operations. * refactor: remove unused code and rename error code - Rename TrancheAmountsDurationsMismatch to TrancheAmountsAndDurationsMismatch for clarity - Remove unused LAMPORTS_PER_SOL constant from lib/constants.ts - Remove unused StreamData.create() method (replaced by create_with_timestamps_ll) - Remove unused getMintTotalSupplyOf() test helper
1 parent dffd683 commit ca06045

38 files changed

Lines changed: 2781 additions & 938 deletions

.claude/skills/solana/SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ version: 0.1.0
1212
You are a senior Solana Anchor engineer with extensive experience using the Anchor CLI, Solana CLI, Metaplex NFTs, and
1313
Trident-based fuzz testing.
1414

15+
## Formatting Rule
16+
17+
**CRITICAL**: After making any code changes, you MUST run `just full-check` to verify that formatting and linting pass.
18+
If it fails, run `just full-write` to auto-fix formatting issues. Do NOT leave a reply with failing checks.
19+
20+
```bash
21+
# Always run after code changes
22+
just full-check
23+
24+
# If full-check fails, run this to fix formatting
25+
just full-write
26+
```
27+
1528
## Solana Core Architecture
1629

1730
**Account Model Mindset**: Programs are stateless executables operating on accounts passed to them. Program state/data

.claude/skills/solana/examples/testPattern.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
// Import patterns from actual tests:
1212
// import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors";
13-
// import { beforeAll, beforeEach, describe, it } from "vitest";
13+
// import { beforeEach, describe, it } from "vitest";
1414
// import { assertEqBn, expectToThrow } from "../../common/assertions";
1515
// import { LockupTestContext } from "../context";
1616
// import { Amount, Time } from "../utils/defaults";
@@ -20,7 +20,7 @@ let ctx: LockupTestContext;
2020
describe("withdraw", () => {
2121
// Test uninitialized program state
2222
describe("when the program is not initialized", () => {
23-
beforeAll(async () => {
23+
beforeEach(async () => {
2424
ctx = new LockupTestContext();
2525
await ctx.setUpLockup({ initProgram: false });
2626
await ctx.timeTravelTo(Time.MID_26_PERCENT);

.claude/skills/solana/references/TESTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ describe("cancel", () => {
230230
let ctx: LockupTestContext;
231231

232232
describe("when program is not initialized", () => {
233-
beforeAll(async () => {
233+
beforeEach(async () => {
234234
ctx = new LockupTestContext();
235235
await ctx.setUpLockup({ initProgram: false });
236236
});

CLAUDE.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Project Instructions
2+
3+
## Post-Session Validation
4+
5+
When a working session modifies TypeScript, Rust, Markdown, or YAML files, run `just full-check` before committing or
6+
handing off. If it fails, run `just full-write`, then re-run `just full-check` to confirm it passes. Additionally,
7+
`just tsc-check` must also pass.
8+
9+
Skip this step when changes are limited to non-code files (e.g., documentation-only conversations, config files not
10+
covered by the checker).
11+
12+
## Binary Test Pattern
13+
14+
All `describe` blocks in test files must follow a binary pair structure. Every failure-condition `describe` must have a
15+
complementary `describe` that wraps all subsequent siblings.
16+
17+
```ts
18+
// CORRECT — binary pairs
19+
describe("given X is zero", () => {
20+
it("should fail", ...);
21+
});
22+
describe("given X is not zero", () => {
23+
describe("given Y overflows", () => {
24+
it("should fail", ...);
25+
});
26+
describe("given Y does not overflow", () => {
27+
it("should succeed", ...);
28+
});
29+
});
30+
31+
// WRONG — flat siblings without complement wrappers
32+
describe("given X is zero", () => { ... });
33+
describe("given Y overflows", () => { ... });
34+
describe("given valid parameters", () => { ... });
35+
```
36+
37+
When testing model variants (LL vs LT), wrap them in a shared parent:
38+
39+
```ts
40+
describe("given a null stream", () => { /* fail */ });
41+
describe("given a valid stream", () => {
42+
describe("given a LL stream", () => { ... });
43+
describe("given a LT stream", () => { ... });
44+
});
45+
```
46+
47+
Conditions belong in `describe`, not `it`. The `it` block should only contain the outcome ("should fail", "should create
48+
the stream"). If an `it` description contains "when", "with", "given", or "if", extract the condition into a wrapping
49+
`describe`.
50+
51+
```ts
52+
// CORRECT
53+
describe("when start time equals first tranche timestamp", () => {
54+
it("should fail", async () => { ... });
55+
});
56+
57+
// WRONG — condition leaked into it()
58+
it("should fail when start time equals first tranche timestamp", async () => { ... });
59+
```
60+
61+
Follow the Rust validation order when structuring the binary tree (check the corresponding `check_*` function in
62+
`programs/lockup/src/utils/validations.rs`).
63+
64+
## Test Label Grammar
65+
66+
Use proper grammar in `describe` and `it` labels. Include verbs and articles unless doing so would make the label
67+
unreasonably long. Examples:
68+
69+
- `"when signer is not sender"` not `"when signer not sender"`
70+
- `"given a non-cancelable stream"` not `"given non cancelable stream"`
71+
- `"when deposit amount is zero"` not `"when deposit amount zero"`
72+
73+
## Test File Naming
74+
75+
The top-level `describe` in each test file must match the filename (without `.test.ts`). For example, `cancelLl.test.ts`
76+
must use `describe("cancelLl", ...)`, not `describe("cancel", ...)`.
77+
78+
## Anchor Error Aliases
79+
80+
When importing Anchor error codes from `@coral-xyz/anchor-errors`, always alias them with an `ERR_` prefix:
81+
82+
```ts
83+
// CORRECT
84+
import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ERR_ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors";
85+
86+
// WRONG — missing ERR_ prefix
87+
import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors";
88+
```

lib/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { BN } from "@coral-xyz/anchor";
22
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
3-
import { PublicKey, LAMPORTS_PER_SOL as raw_LAMPORTS_PER_SOL } from "@solana/web3.js";
3+
import { PublicKey } from "@solana/web3.js";
44

55
export const BN_1 = new BN(1);
66
export const BN_1000 = new BN(1000);
7-
export const LAMPORTS_PER_SOL = new BN(raw_LAMPORTS_PER_SOL);
7+
export const MAX_U64 = new BN("18446744073709551615"); // 2^64 - 1
88
export const REDUNDANCY_BUFFER = new BN(1_000_000); // 0.001 SOL
99
export const SABLIER_ADMIN = new PublicKey("7eJiuqfoRMNx2T83jzjEMBFNY6gx7mS5MHJ5e44f3DGC");
1010
export const SCALING_FACTOR = new BN("1000000000000000000"); // 1e18
@@ -22,6 +22,6 @@ export namespace ProgramId {
2222
"99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR",
2323
);
2424
export const MPL_CORE = new PublicKey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d");
25-
export const TOKEN = TOKEN_PROGRAM_ID;
25+
export const SPL_TOKEN = TOKEN_PROGRAM_ID;
2626
export const TOKEN_2022 = TOKEN_2022_PROGRAM_ID;
2727
}

programs/lockup/src/instructions/create_with_durations_lt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn handler(
2020
) -> Result<()> {
2121
// Validate: amounts and durations must have same length.
2222
if tranche_amounts.len() != tranche_durations.len() {
23-
return Err(ErrorCode::TrancheAmountsDurationsMismatch.into());
23+
return Err(ErrorCode::TrancheAmountsAndDurationsMismatch.into());
2424
}
2525

2626
// Get current time as stream start.

programs/lockup/src/state/lockup.rs

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -220,51 +220,6 @@ impl StreamData {
220220
Ok(())
221221
}
222222

223-
/// State update for the [`fn@crate::sablier_lockup::create_with_timestamps_ll`] instruction.
224-
#[allow(clippy::too_many_arguments)]
225-
pub fn create(
226-
&mut self,
227-
deposited_token_mint: Pubkey,
228-
bump: u8,
229-
cliff_time: u64,
230-
cliff_unlock_amount: u64,
231-
deposit_amount: u64,
232-
end_time: u64,
233-
nft_address: Pubkey,
234-
salt: u128,
235-
is_cancelable: bool,
236-
sender: Pubkey,
237-
start_time: u64,
238-
start_unlock_amount: u64,
239-
) -> Result<()> {
240-
self.bump = bump;
241-
self.amounts = Amounts {
242-
deposited: deposit_amount,
243-
refunded: 0,
244-
withdrawn: 0,
245-
};
246-
self.deposited_token_mint = deposited_token_mint;
247-
self.is_cancelable = is_cancelable;
248-
self.is_depleted = false;
249-
self.nft_address = nft_address;
250-
self.salt = salt;
251-
self.sender = sender;
252-
self.model = StreamModel::Linear {
253-
timestamps: LinearTimestamps {
254-
start: start_time,
255-
cliff: cliff_time,
256-
end: end_time,
257-
},
258-
unlock_amounts: LinearUnlockAmounts {
259-
start: start_unlock_amount,
260-
cliff: cliff_unlock_amount,
261-
},
262-
};
263-
self.was_canceled = false;
264-
265-
Ok(())
266-
}
267-
268223
/// State update for the [`fn@crate::sablier_lockup::renounce`] instruction.
269224
pub fn renounce(&mut self) -> Result<()> {
270225
self.is_cancelable = false;

programs/lockup/src/utils/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub enum ErrorCode {
5454
#[msg("Stream start time must be strictly less than the first tranche's timestamp!")]
5555
StartTimeNotLessThanFirstTranche,
5656
#[msg("Tranche amounts and durations arrays must have same length!")]
57-
TrancheAmountsDurationsMismatch,
57+
TrancheAmountsAndDurationsMismatch,
5858
#[msg("Too many tranches!")]
5959
TooManyTranches,
6060
#[msg("Tranche timestamp overflow!")]

scripts/ts/init-merkle-instant-and-create-campaign.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async function createCampaign({
3939
startTime = Campaign.START_TIME,
4040
expirationTime = Campaign.EXPIRATION_TIME,
4141
airdropTokenMint = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"), // USDC on Devnet
42-
airdropTokenProgram = ProgramId.TOKEN,
42+
airdropTokenProgram = ProgramId.SPL_TOKEN,
4343
} = {}) {
4444
// Set a higher compute unit limit so that the transaction doesn't fail
4545
const increaseCULimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 });

tests/common/anchor-bankrun.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,6 @@ export async function getATABalance(banksClient: BanksClient, ataAddress: Public
163163
return toBn(accountData.amount);
164164
}
165165

166-
export async function getMintTotalSupplyOf(
167-
banksClient: BanksClient,
168-
mintAddress: PublicKey,
169-
): Promise<BN> {
170-
const mintAccount = await banksClient.getAccount(mintAddress);
171-
if (!mintAccount) {
172-
throw new Error("The queried mint account does not exist!");
173-
}
174-
175-
const mintData = token.MintLayout.decode(mintAccount.data);
176-
return toBn(mintData.supply);
177-
}
178-
179166
export async function transfer(
180167
banksClient: BanksClient,
181168
payer: Signer,

0 commit comments

Comments
 (0)