Skip to content
Open
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
105 changes: 105 additions & 0 deletions packages/sdk/src/__test__/unit/encryptedResponses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
LatticeSecureEncryptedRequestType,
ProtocolConstants,
encryptedSecureRequest,
} from '../../protocol';
import { aes256_encrypt, checksum, getP256KeyPair } from '../../util';
import { request } from '../../shared/functions';

vi.mock('../../shared/functions', async () => {
const actual = await vi.importActual<typeof import('../../shared/functions')>(
'../../shared/functions',
);
return {
...actual,
request: vi.fn(),
};
});

const requestMock = vi.mocked(request);

const buildEncryptedResponse = ({
sharedSecret,
requestType,
status,
responsePub,
}: {
sharedSecret: Buffer;
requestType: LatticeSecureEncryptedRequestType;
status: number;
responsePub: Buffer;
}) => {
const responseDataSize =
ProtocolConstants.msgSizes.secure.data.response.encrypted[requestType];
const decrypted = Buffer.alloc(
ProtocolConstants.msgSizes.secure.data.response.encrypted.encryptedData,
);
responsePub.copy(decrypted, 0);
decrypted[responsePub.length] = status;
const checksumOffset = responsePub.length + responseDataSize;
const cs = checksum(decrypted.slice(0, checksumOffset));
decrypted.writeUInt32BE(cs, checksumOffset);
return aes256_encrypt(decrypted, sharedSecret);
};

describe('encryptedSecureRequest response sizes', () => {
const requestType = LatticeSecureEncryptedRequestType.event;
const sharedSecret = Buffer.alloc(32, 7);
const ephemeralPub = getP256KeyPair(Buffer.alloc(32, 3));
const requestData = Buffer.alloc(
ProtocolConstants.msgSizes.secure.data.request.encrypted[requestType],
);
const responseKey = getP256KeyPair(Buffer.alloc(32, 9));
const responsePub = Buffer.from(
responseKey.getPublic().encode('hex', false),
'hex',
);

beforeEach(() => {
requestMock.mockReset();
});

it('accepts compact encrypted response size', async () => {
const encryptedResponse = buildEncryptedResponse({
sharedSecret,
requestType,
status: 0,
responsePub,
});
requestMock.mockResolvedValueOnce(encryptedResponse);

const result = await encryptedSecureRequest({
data: requestData,
requestType,
sharedSecret,
ephemeralPub,
url: 'http://example.test',
});

expect(result.decryptedData[0]).toBe(0);
});

it('accepts legacy encrypted response size', async () => {
const encryptedResponse = buildEncryptedResponse({
sharedSecret,
requestType,
status: 0,
responsePub,
});
const legacyResponse = Buffer.concat([
encryptedResponse,
Buffer.alloc(encryptedResponse.length),
]);
requestMock.mockResolvedValueOnce(legacyResponse);

const result = await encryptedSecureRequest({
data: requestData,
requestType,
sharedSecret,
ephemeralPub,
url: 'http://example.test',
});

expect(result.decryptedData[0]).toBe(0);
});
});
60 changes: 60 additions & 0 deletions packages/sdk/src/__test__/unit/sendEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { __private__ as sendEventPrivate } from '../../functions/sendEvent';

const { encodeEventPayload } = sendEventPrivate;

describe('sendEvent encoding', () => {
it('encodes and pads message payload', () => {
const payload = encodeEventPayload({
eventType: 1,
eventId: '00000000-0000-0000-0000-000000000000',
message: 'hi',
});
expect(payload.length).toBe(1722);
expect(payload[0]).toBe(1);
expect(payload.slice(1, 17).every((b) => b === 0)).toBe(true);
expect(payload.readUInt16LE(17)).toBe(2);
expect(payload.slice(19, 21).toString('utf8')).toBe('hi');
expect(payload.slice(21).every((b) => b === 0)).toBe(true);
});

it('throws on empty message', () => {
expect(() =>
encodeEventPayload({
eventType: 1,
eventId: '00000000-0000-0000-0000-000000000000',
message: '',
}),
).toThrow(/must not be empty/i);
});

it('throws when message is too long', () => {
const longMsg = 'a'.repeat(1704);
expect(() =>
encodeEventPayload({
eventType: 1,
eventId: '00000000-0000-0000-0000-000000000000',
message: longMsg,
}),
).toThrow(/too long/i);
});

it('throws on invalid eventId', () => {
expect(() =>
encodeEventPayload({
eventType: 1,
eventId: 'not-a-uuid',
message: 'hi',
}),
).toThrow(/eventId/i);
});

it('throws on invalid eventType', () => {
expect(() =>
encodeEventPayload({
eventType: 256,
eventId: '00000000-0000-0000-0000-000000000000',
message: 'hi',
}),
).toThrow(/eventType/i);
});
});
214 changes: 214 additions & 0 deletions packages/sdk/src/__test__/unit/solana.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { decodeTransaction, toEd25519Bytes } from '../../calldata/solana';

describe('Solana utilities', () => {
describe('decodeTransaction', () => {
// Generate a valid base64 transaction of minimum valid size (100 bytes)
const createValidBase64Tx = (length: number): string => {
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = i % 256;
}
return Buffer.from(bytes).toString('base64');
};

test('should decode valid base64 transaction within size range', () => {
const validTx = createValidBase64Tx(200);
const result = decodeTransaction(validTx);

expect(result.encoding).toBe('base64');
expect(result.bytes).toBeInstanceOf(Uint8Array);
expect(result.bytes.length).toBe(200);
});

test('should decode transaction at minimum valid size (100 bytes)', () => {
const validTx = createValidBase64Tx(100);
const result = decodeTransaction(validTx);

expect(result.encoding).toBe('base64');
expect(result.bytes.length).toBe(100);
});

test('should decode transaction at maximum valid size (1232 bytes)', () => {
const validTx = createValidBase64Tx(1232);
const result = decodeTransaction(validTx);

expect(result.encoding).toBe('base64');
expect(result.bytes.length).toBe(1232);
});

test('should reject transaction below minimum size (99 bytes)', () => {
const tooSmall = createValidBase64Tx(99);
expect(() => decodeTransaction(tooSmall)).toThrow(
/outside valid Solana range/,
);
});

test('should reject transaction above maximum size (1233 bytes)', () => {
const tooLarge = createValidBase64Tx(1233);
expect(() => decodeTransaction(tooLarge)).toThrow(
/outside valid Solana range/,
);
});

test('should reject base58-encoded string (common Solana RPC format)', () => {
// This is a base58 string (uses only base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz)
// Note: base58 excludes 0, O, I, l which are in base64
// This should NOT silently decode as garbage base64
const base58String = '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi';

expect(() => decodeTransaction(base58String)).toThrow(/not valid base64/);
});

test('should reject base58 transaction that would decode to valid length if treated as base64', () => {
// Create a string with length not divisible by 4 (invalid base64 padding)
// This simulates a base58 string that would fail the round-trip check
const fakeBase58 = `${'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijk123456789'.repeat(5)}ABC`;

expect(() => decodeTransaction(fakeBase58)).toThrow(/not valid base64/);
});

test('should reject strings with invalid base64 characters', () => {
// Contains characters not in base64 alphabet ($ and @)
const invalidChars = 'SGVsbG8$V29ybGQ@';
expect(() => decodeTransaction(invalidChars)).toThrow(/not valid base64/);
});

test('should reject strings with wrong padding', () => {
// Valid base64 should have proper = padding
const wrongPadding = 'SGVsbG8gV29ybGQ';
expect(() => decodeTransaction(wrongPadding)).toThrow(/not valid base64/);
});

test('should reject empty string', () => {
expect(() => decodeTransaction('')).toThrow();
});
});

describe('toEd25519Bytes', () => {
test('should return 32-byte array for valid 32-byte Uint8Array', () => {
const validKey = new Uint8Array(32).fill(42);
const result = toEd25519Bytes(validKey);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
expect(result?.[0]).toBe(42);
});

test('should truncate 64-byte Uint8Array to 32 bytes', () => {
const longKey = new Uint8Array(64);
for (let i = 0; i < 64; i++) {
longKey[i] = i;
}
const result = toEd25519Bytes(longKey);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
expect(result?.[31]).toBe(31);
});

test('should return null for Uint8Array shorter than 32 bytes', () => {
const shortKey = new Uint8Array(16).fill(1);
const result = toEd25519Bytes(shortKey);

expect(result).toBeNull();
});

test('should return null for 31-byte Uint8Array (off by one)', () => {
const almostValid = new Uint8Array(31).fill(1);
const result = toEd25519Bytes(almostValid);

expect(result).toBeNull();
});

test('should return null for empty Uint8Array', () => {
const empty = new Uint8Array(0);
const result = toEd25519Bytes(empty);

expect(result).toBeNull();
});

test('should return 32-byte array for valid 32-byte Buffer', () => {
const validBuffer = Buffer.alloc(32, 0xab);
const result = toEd25519Bytes(validBuffer);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
expect(result?.[0]).toBe(0xab);
});

test('should truncate longer Buffer to 32 bytes', () => {
const longBuffer = Buffer.alloc(48, 0xcd);
const result = toEd25519Bytes(longBuffer);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
});

test('should return null for Buffer shorter than 32 bytes', () => {
const shortBuffer = Buffer.alloc(20, 0xef);
const result = toEd25519Bytes(shortBuffer);

expect(result).toBeNull();
});

test('should parse valid 64-character hex string (32 bytes)', () => {
const hex64 = 'a'.repeat(64);
const result = toEd25519Bytes(hex64);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
expect(result?.[0]).toBe(0xaa);
});

test('should parse valid 0x-prefixed hex string', () => {
const hex = `0x${'b'.repeat(64)}`;
const result = toEd25519Bytes(hex);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
expect(result?.[0]).toBe(0xbb);
});

test('should truncate longer hex string to 32 bytes', () => {
const longHex = `0x${'c'.repeat(128)}`;
const result = toEd25519Bytes(longHex);

expect(result).not.toBeNull();
expect(result?.length).toBe(32);
});

test('should return null for hex string shorter than 64 chars (32 bytes)', () => {
const shortHex = 'd'.repeat(62);
const result = toEd25519Bytes(shortHex);

expect(result).toBeNull();
});

test('should return null for non-hex string', () => {
const notHex = 'not-a-valid-hex-string-at-all-xyz';
const result = toEd25519Bytes(notHex);

expect(result).toBeNull();
});

test('should return null for null input', () => {
const result = toEd25519Bytes(null);
expect(result).toBeNull();
});

test('should return null for undefined input', () => {
const result = toEd25519Bytes(undefined);
expect(result).toBeNull();
});

test('should return null for number input', () => {
const result = toEd25519Bytes(12345);
expect(result).toBeNull();
});

test('should return null for object input', () => {
const result = toEd25519Bytes({ length: 32 });
expect(result).toBeNull();
});
});
});
Loading
Loading