Skip to content

Commit 8345e82

Browse files
authored
Merge pull request #28 from macalinao/igm/zod-solana-encoders
Add more Zod validators for types commonly used in Solana
2 parents e04d10e + fd31dc7 commit 8345e82

18 files changed

+1775
-0
lines changed

.changeset/cute-crabs-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@macalinao/zod-solana": minor
3+
---
4+
5+
Add uint8array support

.changeset/empty-lions-say.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@macalinao/zod-solana": minor
3+
---
4+
5+
Add unsigned int schemas

bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
"devDependencies": {
189189
"@macalinao/eslint-config": "catalog:",
190190
"@macalinao/tsconfig": "catalog:",
191+
"@solana/keys": "^2.0.0",
191192
"@solana/kit": "catalog:",
192193
"@types/bun": "catalog:",
193194
"eslint": "catalog:",

packages/zod-solana/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"devDependencies": {
4949
"@macalinao/eslint-config": "catalog:",
5050
"@macalinao/tsconfig": "catalog:",
51+
"@solana/keys": "^2.0.0",
5152
"@solana/kit": "catalog:",
5253
"@types/bun": "catalog:",
5354
"eslint": "catalog:",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Maximum value for an unsigned 8-bit integer (2^8 - 1).
3+
* Range: 0 to 255
4+
*/
5+
export const U8_MAX = 255;
6+
7+
/**
8+
* Maximum value for an unsigned 16-bit integer (2^16 - 1).
9+
* Range: 0 to 65,535
10+
*/
11+
export const U16_MAX = 65535;
12+
13+
/**
14+
* Maximum value for an unsigned 32-bit integer (2^32 - 1).
15+
* Range: 0 to 4,294,967,295
16+
*/
17+
export const U32_MAX = 4294967295;
18+
19+
/**
20+
* Maximum value for an unsigned 64-bit integer (2^64 - 1).
21+
* Range: 0 to 18,446,744,073,709,551,615
22+
* Note: This is a BigInt value to preserve precision beyond JavaScript's Number.MAX_SAFE_INTEGER.
23+
*/
24+
export const U64_MAX = 18446744073709551615n;

packages/zod-solana/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
export { addressSchema } from "./address-schema.js";
2+
export { U8_MAX, U16_MAX, U32_MAX, U64_MAX } from "./constants.js";
23
export {
34
type TokenMetadata,
45
tokenMetadataSchema,
56
} from "./token-metadata-schema.js";
7+
export { u8Schema } from "./u8-schema.js";
8+
export { u16Schema } from "./u16-schema.js";
9+
export { u32Schema } from "./u32-schema.js";
10+
export { u64Schema } from "./u64-schema.js";
11+
export { u64StringSchema } from "./u64-string-schema.js";
12+
export { jsonUint8ArraySchema, uint8ArraySchema } from "./uint8array-schema.js";
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { U16_MAX } from "./constants.js";
3+
import { u16Schema } from "./u16-schema.js";
4+
5+
describe("u16Schema", () => {
6+
describe("valid values", () => {
7+
it("should accept 0", () => {
8+
const result = u16Schema.safeParse(0);
9+
expect(result.success).toBe(true);
10+
if (result.success) {
11+
expect(result.data).toBe(0);
12+
}
13+
});
14+
15+
it("should accept 1", () => {
16+
const result = u16Schema.safeParse(1);
17+
expect(result.success).toBe(true);
18+
if (result.success) {
19+
expect(result.data).toBe(1);
20+
}
21+
});
22+
23+
it("should accept 256 (u8 boundary)", () => {
24+
const result = u16Schema.safeParse(256);
25+
expect(result.success).toBe(true);
26+
if (result.success) {
27+
expect(result.data).toBe(256);
28+
}
29+
});
30+
31+
it("should accept 32767 (middle value)", () => {
32+
const result = u16Schema.safeParse(32767);
33+
expect(result.success).toBe(true);
34+
if (result.success) {
35+
expect(result.data).toBe(32767);
36+
}
37+
});
38+
39+
it("should accept 65535 (U16_MAX)", () => {
40+
const result = u16Schema.safeParse(65535);
41+
expect(result.success).toBe(true);
42+
if (result.success) {
43+
expect(result.data).toBe(65535);
44+
}
45+
});
46+
47+
it("should accept U16_MAX constant", () => {
48+
const result = u16Schema.safeParse(U16_MAX);
49+
expect(result.success).toBe(true);
50+
if (result.success) {
51+
expect(result.data).toBe(U16_MAX);
52+
}
53+
});
54+
55+
it("should accept various valid values", () => {
56+
const validValues = [10, 100, 1000, 10000, 50000, 60000];
57+
for (const value of validValues) {
58+
const result = u16Schema.safeParse(value);
59+
expect(result.success).toBe(true);
60+
if (result.success) {
61+
expect(result.data).toBe(value);
62+
}
63+
}
64+
});
65+
});
66+
67+
describe("invalid values", () => {
68+
it("should reject -1", () => {
69+
const result = u16Schema.safeParse(-1);
70+
expect(result.success).toBe(false);
71+
if (!result.success) {
72+
expect(result.error.issues[0]?.message).toContain("at least 0");
73+
}
74+
});
75+
76+
it("should reject -1000", () => {
77+
const result = u16Schema.safeParse(-1000);
78+
expect(result.success).toBe(false);
79+
if (!result.success) {
80+
expect(result.error.issues[0]?.message).toContain("at least 0");
81+
}
82+
});
83+
84+
it("should reject 65536 (U16_MAX + 1)", () => {
85+
const result = u16Schema.safeParse(65536);
86+
expect(result.success).toBe(false);
87+
if (!result.success) {
88+
expect(result.error.issues[0]?.message).toContain("at most 65535");
89+
}
90+
});
91+
92+
it("should reject 100000", () => {
93+
const result = u16Schema.safeParse(100000);
94+
expect(result.success).toBe(false);
95+
if (!result.success) {
96+
expect(result.error.issues[0]?.message).toContain("at most 65535");
97+
}
98+
});
99+
100+
it("should reject 0.5 (non-integer)", () => {
101+
const result = u16Schema.safeParse(0.5);
102+
expect(result.success).toBe(false);
103+
if (!result.success) {
104+
expect(result.error.issues[0]?.message).toContain("integer");
105+
}
106+
});
107+
108+
it("should reject 32767.5 (non-integer)", () => {
109+
const result = u16Schema.safeParse(32767.5);
110+
expect(result.success).toBe(false);
111+
if (!result.success) {
112+
expect(result.error.issues[0]?.message).toContain("integer");
113+
}
114+
});
115+
116+
it("should reject 999.999 (non-integer)", () => {
117+
const result = u16Schema.safeParse(999.999);
118+
expect(result.success).toBe(false);
119+
if (!result.success) {
120+
expect(result.error.issues[0]?.message).toContain("integer");
121+
}
122+
});
123+
124+
it("should reject NaN", () => {
125+
const result = u16Schema.safeParse(Number.NaN);
126+
expect(result.success).toBe(false);
127+
});
128+
129+
it("should reject Infinity", () => {
130+
const result = u16Schema.safeParse(Number.POSITIVE_INFINITY);
131+
expect(result.success).toBe(false);
132+
});
133+
134+
it("should reject -Infinity", () => {
135+
const result = u16Schema.safeParse(Number.NEGATIVE_INFINITY);
136+
expect(result.success).toBe(false);
137+
});
138+
139+
it("should reject string values", () => {
140+
const result = u16Schema.safeParse("1000");
141+
expect(result.success).toBe(false);
142+
});
143+
144+
it("should reject null", () => {
145+
const result = u16Schema.safeParse(null);
146+
expect(result.success).toBe(false);
147+
});
148+
149+
it("should reject undefined", () => {
150+
const result = u16Schema.safeParse(undefined);
151+
expect(result.success).toBe(false);
152+
});
153+
154+
it("should reject objects", () => {
155+
const result = u16Schema.safeParse({ value: 1000 });
156+
expect(result.success).toBe(false);
157+
});
158+
159+
it("should reject arrays", () => {
160+
const result = u16Schema.safeParse([1000]);
161+
expect(result.success).toBe(false);
162+
});
163+
164+
it("should reject boolean values", () => {
165+
const result = u16Schema.safeParse(true);
166+
expect(result.success).toBe(false);
167+
});
168+
});
169+
170+
describe("edge cases", () => {
171+
it("should handle type coercion correctly", () => {
172+
const result = u16Schema.safeParse(65535.0);
173+
expect(result.success).toBe(true);
174+
if (result.success) {
175+
expect(result.data).toBe(65535);
176+
}
177+
});
178+
179+
it("should reject very small negative decimals", () => {
180+
const result = u16Schema.safeParse(-0.1);
181+
expect(result.success).toBe(false);
182+
});
183+
184+
it("should reject very large numbers", () => {
185+
const result = u16Schema.safeParse(Number.MAX_SAFE_INTEGER);
186+
expect(result.success).toBe(false);
187+
});
188+
189+
it("should handle scientific notation within range", () => {
190+
const result = u16Schema.safeParse(1e4); // 10000
191+
expect(result.success).toBe(true);
192+
if (result.success) {
193+
expect(result.data).toBe(10000);
194+
}
195+
});
196+
197+
it("should reject scientific notation out of range", () => {
198+
const result = u16Schema.safeParse(1e6); // 1000000
199+
expect(result.success).toBe(false);
200+
});
201+
});
202+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { z } from "zod";
2+
import { U16_MAX } from "./constants.js";
3+
4+
/**
5+
* A Zod schema for u16 values.
6+
* Validates that a number is between 0 and 65535 (inclusive).
7+
*/
8+
export const u16Schema = z
9+
.number()
10+
.int("Value must be an integer")
11+
.min(0, "Value must be at least 0")
12+
.max(U16_MAX, `Value must be at most ${String(U16_MAX)}`);

0 commit comments

Comments
 (0)