@macalinao/zod-solana
Version:
Zod schemas for Solana types with @solana/kit integration
427 lines (360 loc) • 16.9 kB
text/typescript
import { describe, expect, test } from "bun:test";
import { createKeyPairFromPrivateKeyBytes } from "@solana/keys";
import { createKeyPairSignerFromPrivateKeyBytes } from "@solana/kit";
import { jsonUint8ArraySchema, uint8ArraySchema } from "./uint8array-schema.js";
describe("uint8ArraySchema", () => {
test("transforms valid number arrays to Uint8Array", () => {
const result = uint8ArraySchema.parse([0, 1, 2, 3, 255]);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(5);
expect(result[0]).toBe(0);
expect(result[1]).toBe(1);
expect(result[2]).toBe(2);
expect(result[3]).toBe(3);
expect(result[4]).toBe(255);
});
test("handles empty arrays", () => {
const result = uint8ArraySchema.parse([]);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(0);
});
test("handles single element arrays", () => {
const result = uint8ArraySchema.parse([42]);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(1);
expect(result[0]).toBe(42);
});
test("rejects arrays with invalid u8 values", () => {
expect(() => uint8ArraySchema.parse([0, 256])).toThrow(
"Value must be at most 255",
);
expect(() => uint8ArraySchema.parse([-1, 0])).toThrow(
"Value must be at least 0",
);
expect(() => uint8ArraySchema.parse([1.5])).toThrow(
"Value must be an integer",
);
expect(() => uint8ArraySchema.parse([0, "1", 2])).toThrow();
expect(() => uint8ArraySchema.parse([null])).toThrow();
});
test("rejects non-array inputs", () => {
expect(() => uint8ArraySchema.parse("not an array")).toThrow();
expect(() => uint8ArraySchema.parse(123)).toThrow();
expect(() => uint8ArraySchema.parse(null)).toThrow();
expect(() => uint8ArraySchema.parse(undefined)).toThrow();
});
test("works with 32-byte arrays (typical for keys)", () => {
const keyBytes = Array.from({ length: 32 }, (_, i) => i % 256);
const result = uint8ArraySchema.parse(keyBytes);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(32);
for (let i = 0; i < 32; i++) {
expect(result[i]).toBe(i % 256);
}
});
test("works with 64-byte arrays (typical for signatures)", () => {
const sigBytes = Array.from({ length: 64 }, (_, i) => (i * 3) % 256);
const result = uint8ArraySchema.parse(sigBytes);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(64);
for (let i = 0; i < 64; i++) {
expect(result[i]).toBe((i * 3) % 256);
}
});
test("handles large arrays", () => {
const largeArray = Array.from({ length: 1000 }, (_, i) => i % 256);
const result = uint8ArraySchema.parse(largeArray);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(1000);
});
});
describe("jsonUint8ArraySchema", () => {
test("parses JSON strings and transforms to Uint8Array", () => {
const result = jsonUint8ArraySchema.parse("[0, 1, 2, 255]");
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(4);
expect(result[0]).toBe(0);
expect(result[1]).toBe(1);
expect(result[2]).toBe(2);
expect(result[3]).toBe(255);
});
test("handles JSON empty arrays", () => {
const result = jsonUint8ArraySchema.parse("[]");
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(0);
});
test("handles JSON with whitespace", () => {
const result = jsonUint8ArraySchema.parse(" [ 1 , 2 , 3 ] ");
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(3);
expect(result[0]).toBe(1);
expect(result[1]).toBe(2);
expect(result[2]).toBe(3);
});
test("handles JSON with newlines and formatting", () => {
const json = `[
1,
2,
3
]`;
const result = jsonUint8ArraySchema.parse(json);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(3);
expect(Array.from(result)).toEqual([1, 2, 3]);
});
test("rejects invalid JSON", () => {
expect(() => jsonUint8ArraySchema.parse("[1, 2,")).toThrow("Invalid JSON");
expect(() => jsonUint8ArraySchema.parse("not json")).toThrow(
"Invalid JSON",
);
expect(() => jsonUint8ArraySchema.parse("")).toThrow("Invalid JSON");
expect(() => jsonUint8ArraySchema.parse('{"a": 1}')).toThrow();
expect(() => jsonUint8ArraySchema.parse("null")).toThrow();
expect(() => jsonUint8ArraySchema.parse("undefined")).toThrow();
});
test("rejects JSON with invalid u8 values", () => {
expect(() => jsonUint8ArraySchema.parse("[0, 256]")).toThrow(
"Value must be at most 255",
);
expect(() => jsonUint8ArraySchema.parse("[-1, 0]")).toThrow(
"Value must be at least 0",
);
expect(() => jsonUint8ArraySchema.parse("[1.5]")).toThrow(
"Value must be an integer",
);
expect(() => jsonUint8ArraySchema.parse('["not a number"]')).toThrow();
expect(() => jsonUint8ArraySchema.parse("[true]")).toThrow();
expect(() => jsonUint8ArraySchema.parse("[null]")).toThrow();
});
test("rejects non-string inputs", () => {
expect(() => jsonUint8ArraySchema.parse([1, 2, 3])).toThrow();
expect(() => jsonUint8ArraySchema.parse(123)).toThrow();
expect(() => jsonUint8ArraySchema.parse(null)).toThrow();
expect(() => jsonUint8ArraySchema.parse(undefined)).toThrow();
expect(() => jsonUint8ArraySchema.parse({})).toThrow();
});
test("works with JSON-encoded 32-byte arrays", () => {
const keyBytes = Array.from({ length: 32 }, (_, i) => i % 256);
const json = JSON.stringify(keyBytes);
const result = jsonUint8ArraySchema.parse(json);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(32);
for (let i = 0; i < 32; i++) {
expect(result[i]).toBe(i % 256);
}
});
test("works with JSON-encoded 64-byte arrays", () => {
const sigBytes = Array.from({ length: 64 }, (_, i) => (i * 3) % 256);
const json = JSON.stringify(sigBytes);
const result = jsonUint8ArraySchema.parse(json);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(64);
for (let i = 0; i < 64; i++) {
expect(result[i]).toBe((i * 3) % 256);
}
});
});
describe("integration with createKeyPairFromPrivateKeyBytes", () => {
test("uint8ArraySchema works with createKeyPairFromPrivateKeyBytes", async () => {
// Create a valid 32-byte array for a keypair
const secretKeyBytes = [
// Private key (32 bytes)
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
// Parse through our schema
const uint8Array = uint8ArraySchema.parse(secretKeyBytes);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(32);
// Create keypair
const keypair = await createKeyPairFromPrivateKeyBytes(uint8Array);
expect(keypair).toBeDefined();
expect(keypair.publicKey).toBeInstanceOf(CryptoKey);
expect(keypair.privateKey).toBeInstanceOf(CryptoKey);
});
test("jsonUint8ArraySchema works with createKeyPairFromPrivateKeyBytes", async () => {
// Create a valid 32-byte array for a keypair
const secretKeyBytes = [
// Private key (32 bytes)
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
// Convert to JSON string
const json = JSON.stringify(secretKeyBytes);
// Parse through our schema
const uint8Array = jsonUint8ArraySchema.parse(json);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(32);
// Create keypair
const keypair = await createKeyPairFromPrivateKeyBytes(uint8Array);
expect(keypair).toBeDefined();
expect(keypair.publicKey).toBeInstanceOf(CryptoKey);
expect(keypair.privateKey).toBeInstanceOf(CryptoKey);
});
test("validates that both schemas produce identical results", async () => {
const secretKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
const fromArray = uint8ArraySchema.parse(secretKeyBytes);
const fromJson = jsonUint8ArraySchema.parse(JSON.stringify(secretKeyBytes));
// Check arrays are identical
expect(fromArray.length).toBe(fromJson.length);
expect(Array.from(fromArray)).toEqual(Array.from(fromJson));
// Create keypairs and verify they're the same
const keypair1 = await createKeyPairFromPrivateKeyBytes(fromArray);
const keypair2 = await createKeyPairFromPrivateKeyBytes(fromJson);
// Both public keys should have the same algorithm and extractable property
expect(keypair1.publicKey.algorithm).toEqual(keypair2.publicKey.algorithm);
expect(keypair1.publicKey.extractable).toBe(keypair2.publicKey.extractable);
});
test("rejects invalid length arrays for createKeyPairFromPrivateKeyBytes", () => {
// Too short
expect(() => uint8ArraySchema.parse([1, 2, 3])).not.toThrow();
const shortArray = uint8ArraySchema.parse([1, 2, 3]);
expect(createKeyPairFromPrivateKeyBytes(shortArray)).rejects.toThrow();
// Too long
const longArray = uint8ArraySchema.parse(Array(65).fill(0));
expect(createKeyPairFromPrivateKeyBytes(longArray)).rejects.toThrow();
});
test("handles real-world use case with environment variable", async () => {
// Simulate reading from an environment variable
const envValue =
"[88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219, 255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185]";
// Parse and create keypair
const uint8Array = jsonUint8ArraySchema.parse(envValue);
const keypair = await createKeyPairFromPrivateKeyBytes(uint8Array);
expect(keypair).toBeDefined();
expect(keypair.publicKey).toBeInstanceOf(CryptoKey);
expect(keypair.privateKey).toBeInstanceOf(CryptoKey);
});
test("handles compact JSON format", async () => {
const secretKeyBytes = Array.from({ length: 32 }, (_, i) => i % 256);
const compactJson = JSON.stringify(secretKeyBytes).replace(/\s/g, ""); // Remove all whitespace
const uint8Array = jsonUint8ArraySchema.parse(compactJson);
expect(uint8Array.length).toBe(32);
const keypair = await createKeyPairFromPrivateKeyBytes(uint8Array);
expect(keypair).toBeDefined();
});
});
describe("integration with createKeyPairSignerFromBytes", () => {
test("uint8ArraySchema works with 64-byte arrays for keypair signers", () => {
// Create a 64-byte array by duplicating a 32-byte private key
// This demonstrates the schema can handle larger arrays used by createKeyPairSignerFromBytes
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
// Create 64-byte array (shows schema supports larger arrays for different signer functions)
const fake64ByteArray = [...privateKeyBytes, ...privateKeyBytes];
// Parse through our schema - this should not throw
const uint8Array = uint8ArraySchema.parse(fake64ByteArray);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(64);
// Verify the schema properly validates the length
expect(() => uint8ArraySchema.parse(fake64ByteArray)).not.toThrow();
});
test("jsonUint8ArraySchema handles 64-byte arrays from JSON", () => {
// Create a 64-byte array (using duplicated 32-byte pattern for test)
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
const fake64ByteArray = [...privateKeyBytes, ...privateKeyBytes];
// Convert to JSON string
const json = JSON.stringify(fake64ByteArray);
// Parse through our schema
const uint8Array = jsonUint8ArraySchema.parse(json);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(64);
// Verify both schemas produce identical arrays for same data
const fromArray = uint8ArraySchema.parse(fake64ByteArray);
expect(fromArray.length).toBe(uint8Array.length);
expect(Array.from(fromArray)).toEqual(Array.from(uint8Array));
});
test("schema validates different array sizes for keypair use cases", () => {
// Test that our schema can handle various sizes that might be used with signers
const sizes = [32, 64, 96]; // Common sizes for keys
for (const size of sizes) {
const testArray = Array.from({ length: size }, (_, i) => i % 256);
expect(() => uint8ArraySchema.parse(testArray)).not.toThrow();
const result = uint8ArraySchema.parse(testArray);
expect(result.length).toBe(size);
}
});
});
describe("integration with createKeyPairSignerFromPrivateKeyBytes", () => {
test("uint8ArraySchema works with createKeyPairSignerFromPrivateKeyBytes", async () => {
// Create a valid 32-byte private key
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
// Parse through our schema
const uint8Array = uint8ArraySchema.parse(privateKeyBytes);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(32);
// Create keypair signer from private key only
const signer = await createKeyPairSignerFromPrivateKeyBytes(uint8Array);
expect(signer).toBeDefined();
expect(typeof signer.address).toBe("string");
expect(signer.address.length).toBeGreaterThan(30);
});
test("jsonUint8ArraySchema works with createKeyPairSignerFromPrivateKeyBytes", async () => {
// Create a valid 32-byte private key
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
// Convert to JSON string
const json = JSON.stringify(privateKeyBytes);
// Parse through our schema
const uint8Array = jsonUint8ArraySchema.parse(json);
expect(uint8Array).toBeInstanceOf(Uint8Array);
expect(uint8Array.length).toBe(32);
// Create keypair signer from private key
const signer = await createKeyPairSignerFromPrivateKeyBytes(uint8Array);
expect(signer).toBeDefined();
expect(typeof signer.address).toBe("string");
expect(signer.address.length).toBeGreaterThan(30);
});
test("validates signers from both schemas produce same address", async () => {
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
const fromArray = uint8ArraySchema.parse(privateKeyBytes);
const fromJson = jsonUint8ArraySchema.parse(
JSON.stringify(privateKeyBytes),
);
// Create signers and verify they have the same address
const signer1 = await createKeyPairSignerFromPrivateKeyBytes(fromArray);
const signer2 = await createKeyPairSignerFromPrivateKeyBytes(fromJson);
expect(signer1.address).toBe(signer2.address);
});
test("handles environment variable use case with private key", async () => {
// Simulate reading a private key from environment variable
const envValue =
"[88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219, 255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185]";
// Parse and create signer
const uint8Array = jsonUint8ArraySchema.parse(envValue);
const signer = await createKeyPairSignerFromPrivateKeyBytes(uint8Array);
expect(signer).toBeDefined();
expect(typeof signer.address).toBe("string");
expect(signer.address.length).toBeGreaterThan(30);
});
test("validates both private key methods produce same address", async () => {
const privateKeyBytes = [
88, 100, 47, 111, 185, 7, 50, 1, 136, 247, 70, 127, 111, 51, 154, 36, 219,
255, 58, 82, 26, 166, 214, 152, 228, 160, 73, 148, 151, 15, 115, 185,
];
const uint8Array = uint8ArraySchema.parse(privateKeyBytes);
// Create using both @solana/keys and @solana/kit methods
const cryptoKeyPair = await createKeyPairFromPrivateKeyBytes(uint8Array);
const signer = await createKeyPairSignerFromPrivateKeyBytes(uint8Array);
// Both should work and create valid objects
expect(cryptoKeyPair.publicKey).toBeInstanceOf(CryptoKey);
expect(signer.address).toBeDefined();
expect(typeof signer.address).toBe("string");
});
});