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
5 changes: 5 additions & 0 deletions .changeset/clear-taxis-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@macalinao/zod-solana": patch
---

Add BPS schema
7 changes: 7 additions & 0 deletions .changeset/fluffy-dingos-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@macalinao/token-utils": patch
"@macalinao/zod-solana": patch
"@macalinao/quarry": patch
---

Eslint 9.38 force
52 changes: 51 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"apps/*"
],
"catalog": {
"eslint": "^9.38.0",
"eslint": "=9.38.0",
"typescript": "^5.9.3",
"@macalinao/eslint-config": "^7.0.1",
"@macalinao/eslint-config-react": "^6.0.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/quarry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
"@macalinao/tsconfig": "catalog:",
"@solana/kit": "catalog:",
"@types/bun": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"eslint": "catalog:"
},
"peerDependencies": {
"@solana/kit": "catalog:"
Expand Down
3 changes: 2 additions & 1 deletion packages/token-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"@macalinao/tsconfig": "catalog:",
"@solana/kit": "catalog:",
"@types/bun": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"eslint": "catalog:"
},
"peerDependencies": {
"@solana/kit": "catalog:"
Expand Down
247 changes: 247 additions & 0 deletions packages/zod-solana/src/bps-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { describe, expect, it } from "bun:test";
import { bpsSchema } from "./bps-schema.js";

describe("bpsSchema", () => {
describe("valid values", () => {
it("should accept 0 (0%)", () => {
const result = bpsSchema.safeParse(0);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(0);
}
});

it("should accept 1 (0.01%)", () => {
const result = bpsSchema.safeParse(1);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(1);
}
});

it("should accept 50 (0.5%)", () => {
const result = bpsSchema.safeParse(50);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(50);
}
});

it("should accept 100 (1%)", () => {
const result = bpsSchema.safeParse(100);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(100);
}
});

it("should accept 500 (5%)", () => {
const result = bpsSchema.safeParse(500);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(500);
}
});

it("should accept 1000 (10%)", () => {
const result = bpsSchema.safeParse(1000);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(1000);
}
});

it("should accept 5000 (50%)", () => {
const result = bpsSchema.safeParse(5000);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(5000);
}
});

it("should accept 10000 (100%)", () => {
const result = bpsSchema.safeParse(10000);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(10000);
}
});

it("should accept various valid values", () => {
const validValues = [10, 25, 75, 150, 300, 750, 2500, 7500, 9999];
for (const value of validValues) {
const result = bpsSchema.safeParse(value);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(value);
}
}
});
});

describe("invalid values", () => {
it("should reject -1", () => {
const result = bpsSchema.safeParse(-1);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain(">=0");
}
});

it("should reject -100", () => {
const result = bpsSchema.safeParse(-100);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain(">=0");
}
});

it("should reject 10001 (max + 1)", () => {
const result = bpsSchema.safeParse(10001);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain("<=10000");
}
});

it("should reject 50000", () => {
const result = bpsSchema.safeParse(50000);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain("<=10000");
}
});

it("should reject 0.5 (non-integer)", () => {
const result = bpsSchema.safeParse(0.5);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain("expected int");
}
});

it("should reject 100.5 (non-integer)", () => {
const result = bpsSchema.safeParse(100.5);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain("expected int");
}
});

it("should reject 9999.99 (non-integer)", () => {
const result = bpsSchema.safeParse(9999.99);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain("expected int");
}
});

it("should reject NaN", () => {
const result = bpsSchema.safeParse(Number.NaN);
expect(result.success).toBe(false);
});

it("should reject Infinity", () => {
const result = bpsSchema.safeParse(Number.POSITIVE_INFINITY);
expect(result.success).toBe(false);
});

it("should reject -Infinity", () => {
const result = bpsSchema.safeParse(Number.NEGATIVE_INFINITY);
expect(result.success).toBe(false);
});

it("should reject string values", () => {
const result = bpsSchema.safeParse("500");
expect(result.success).toBe(false);
});

it("should reject null", () => {
const result = bpsSchema.safeParse(null);
expect(result.success).toBe(false);
});

it("should reject undefined", () => {
const result = bpsSchema.safeParse(undefined);
expect(result.success).toBe(false);
});

it("should reject objects", () => {
const result = bpsSchema.safeParse({ value: 500 });
expect(result.success).toBe(false);
});

it("should reject arrays", () => {
const result = bpsSchema.safeParse([500]);
expect(result.success).toBe(false);
});

it("should reject boolean values", () => {
const result = bpsSchema.safeParse(true);
expect(result.success).toBe(false);
});
});

describe("edge cases", () => {
it("should handle type coercion correctly", () => {
const result = bpsSchema.safeParse(10000.0);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(10000);
}
});

it("should reject very small negative decimals", () => {
const result = bpsSchema.safeParse(-0.1);
expect(result.success).toBe(false);
});

it("should reject very large numbers", () => {
const result = bpsSchema.safeParse(Number.MAX_SAFE_INTEGER);
expect(result.success).toBe(false);
});

it("should handle scientific notation within range", () => {
const result = bpsSchema.safeParse(1e4); // 10000
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(10000);
}
});

it("should reject scientific notation out of range", () => {
const result = bpsSchema.safeParse(1e5); // 100000
expect(result.success).toBe(false);
});
});

describe("basis points specific tests", () => {
it("should accept common percentage values in basis points", () => {
const commonPercentages = [
{ bps: 1, percentage: "0.01%" },
{ bps: 25, percentage: "0.25%" },
{ bps: 50, percentage: "0.5%" },
{ bps: 100, percentage: "1%" },
{ bps: 250, percentage: "2.5%" },
{ bps: 500, percentage: "5%" },
{ bps: 1000, percentage: "10%" },
{ bps: 2500, percentage: "25%" },
{ bps: 5000, percentage: "50%" },
{ bps: 7500, percentage: "75%" },
{ bps: 10000, percentage: "100%" },
];

for (const { bps } of commonPercentages) {
const result = bpsSchema.safeParse(bps);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(bps);
}
}
});

it("should have correct description", () => {
expect(bpsSchema.description).toBe("Basis points (0-10000)");
});
});
});
11 changes: 11 additions & 0 deletions packages/zod-solana/src/bps-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as z from "zod";

/**
* Zod schema for basis points (0-10000)
*/
export const bpsSchema: z.ZodNumber = z
.number()
.int()
.min(0)
.max(10000)
.describe("Basis points (0-10000)");
1 change: 1 addition & 0 deletions packages/zod-solana/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { addressSchema } from "./address-schema.js";
export { bpsSchema } from "./bps-schema.js";
export { U8_MAX, U16_MAX, U32_MAX, U64_MAX } from "./constants.js";
export {
type TokenMetadata,
Expand Down