From 17eca806be405bb731d70ba6f3d810bf0c2c4906 Mon Sep 17 00:00:00 2001 From: Abhishek Khaparde Date: Tue, 24 Mar 2026 20:05:38 +0530 Subject: [PATCH] feat: create TypeScript SDK package structure and core utilities Co-authored-by: aider (deepseek/deepseek-chat) --- sdk/.eslintrc.js | 25 +++++ sdk/.prettierrc | 8 ++ sdk/README.md | 117 +++++++++++++++++++ sdk/jest.config.js | 23 ++++ sdk/package.json | 56 ++++++++++ sdk/src/__tests__/utils.test.ts | 192 ++++++++++++++++++++++++++++++++ sdk/src/constants.ts | 28 +++++ sdk/src/index.ts | 9 ++ sdk/src/types.ts | 50 +++++++++ sdk/src/utils/crypto.ts | 95 ++++++++++++++++ sdk/src/utils/encoding.ts | 73 ++++++++++++ sdk/src/utils/index.ts | 7 ++ sdk/src/utils/validation.ts | 108 ++++++++++++++++++ sdk/tsconfig.json | 32 ++++++ 14 files changed, 823 insertions(+) create mode 100644 sdk/.eslintrc.js create mode 100644 sdk/.prettierrc create mode 100644 sdk/README.md create mode 100644 sdk/jest.config.js create mode 100644 sdk/package.json create mode 100644 sdk/src/__tests__/utils.test.ts create mode 100644 sdk/src/constants.ts create mode 100644 sdk/src/index.ts create mode 100644 sdk/src/types.ts create mode 100644 sdk/src/utils/crypto.ts create mode 100644 sdk/src/utils/encoding.ts create mode 100644 sdk/src/utils/index.ts create mode 100644 sdk/src/utils/validation.ts create mode 100644 sdk/tsconfig.json diff --git a/sdk/.eslintrc.js b/sdk/.eslintrc.js new file mode 100644 index 0000000..39cab3c --- /dev/null +++ b/sdk/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + env: { + node: true, + jest: true, + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'curly': ['error', 'all'], + 'eqeqeq': ['error', 'always'], + 'no-throw-literal': 'error', + }, +}; diff --git a/sdk/.prettierrc b/sdk/.prettierrc new file mode 100644 index 0000000..29b9d1f --- /dev/null +++ b/sdk/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..6e93466 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,117 @@ +# @privacylayer/sdk + +TypeScript SDK for PrivacyLayer - Zero-knowledge shielded pool on Stellar Soroban + +## Installation + +```bash +npm install @privacylayer/sdk +``` + +## Quick Start + +```typescript +import { utils, Denomination } from '@privacylayer/sdk'; + +// Generate a new note +const note = utils.generateNote(Denomination.TEN); +console.log('Generated note:', note); + +// Validate the note +const errors = utils.validateNote(note); +if (errors.length === 0) { + console.log('Note is valid'); +} + +// Convert between formats +const hex = 'deadbeef'; +const buffer = utils.hexToBuffer(hex); +const backToHex = utils.bufferToHex(buffer); +console.log('Conversion successful:', backToHex === hex); +``` + +## Features + +- **Note Generation**: Create privacy-preserving notes for deposits +- **Cryptographic Utilities**: Field element operations and validation +- **Encoding/Decoding**: Hex, base64, and buffer conversions +- **Validation**: Address, amount, and network configuration validation +- **Type Safety**: Full TypeScript support with comprehensive type definitions + +## API Reference + +### Core Types + +```typescript +import { Note, Denomination, DepositReceipt, NetworkConfig } from '@privacylayer/sdk'; + +// Note structure +const note: Note = { + nullifier: 'hex-string', + secret: 'hex-string', + commitment: 'hex-string', + denomination: Denomination.TEN +}; +``` + +### Utilities + +The SDK provides utility functions in three categories: + +1. **Crypto**: Cryptographic operations and field element handling +2. **Encoding**: Data format conversions +3. **Validation**: Input validation and sanitization + +```typescript +import { utils } from '@privacylayer/sdk'; + +// Generate random field element +const fieldElement = utils.randomFieldElement(); + +// Validate Stellar address +const isValid = utils.isValidStellarAddress('G...'); + +// Convert hex to base64 +const base64 = utils.hexToBase64('deadbeef'); +``` + +## Development + +### Setup + +```bash +cd sdk +npm install +``` + +### Build + +```bash +npm run build +``` + +### Test + +```bash +npm test +``` + +### Lint + +```bash +npm run lint +``` + +### Format Code + +```bash +npm run format +``` + +## Contributing + +Contributions are welcome! Please see the main repository's [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## License + +MIT diff --git a/sdk/jest.config.js b/sdk/jest.config.js new file mode 100644 index 0000000..8040706 --- /dev/null +++ b/sdk/jest.config.js @@ -0,0 +1,23 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.ts'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/__tests__/**', + '!src/index.ts' + ], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + coverageDirectory: 'coverage', + verbose: true +}; diff --git a/sdk/package.json b/sdk/package.json new file mode 100644 index 0000000..435bfa6 --- /dev/null +++ b/sdk/package.json @@ -0,0 +1,56 @@ +{ + "name": "@privacylayer/sdk", + "version": "0.1.0", + "description": "TypeScript SDK for PrivacyLayer - Zero-knowledge shielded pool on Stellar Soroban", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "jest", + "lint": "eslint src --ext .ts", + "format": "prettier --write src/**/*.ts", + "prepare": "npm run build", + "prepublishOnly": "npm test && npm run lint" + }, + "keywords": [ + "stellar", + "soroban", + "zk", + "zero-knowledge", + "privacy", + "cryptography" + ], + "author": "PrivacyLayer Contributors", + "license": "MIT", + "dependencies": { + "@stellar/stellar-sdk": "^12.0.0", + "bignumber.js": "^9.1.0", + "buffer": "^6.0.3" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.50.0", + "jest": "^29.7.0", + "prettier": "^3.0.0", + "ts-jest": "^29.1.0", + "typescript": "^5.2.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/ANAVHEOBA/PrivacyLayer.git", + "directory": "sdk" + }, + "bugs": { + "url": "https://github.com/ANAVHEOBA/PrivacyLayer/issues" + }, + "homepage": "https://github.com/ANAVHEOBA/PrivacyLayer#readme" +} diff --git a/sdk/src/__tests__/utils.test.ts b/sdk/src/__tests__/utils.test.ts new file mode 100644 index 0000000..50d277e --- /dev/null +++ b/sdk/src/__tests__/utils.test.ts @@ -0,0 +1,192 @@ +/** + * Unit tests for utility functions + */ + +import { + randomFieldElement, + isValidFieldElement, + generateNote, + isValidBytes32, + hexToBuffer, + bufferToHex, + bytesToBigInt, + bigIntToHex +} from '../utils/crypto'; +import { + hexToUint8Array, + uint8ArrayToHex, + base64ToHex, + hexToBase64, + isHex, + padHex +} from '../utils/encoding'; +import { + isValidStellarAddress, + isValidContractId, + isValidAmount, + isValidDenomination, + validateNote, + validateNetworkConfig +} from '../utils/validation'; +import { Denomination } from '../types'; +import { FIELD_SIZE } from '../constants'; + +describe('Crypto utilities', () => { + test('randomFieldElement generates valid field element', () => { + const element = randomFieldElement(); + expect(isValidFieldElement(element)).toBe(true); + }); + + test('isValidFieldElement validates correctly', () => { + // Valid field element (within range) + const validHex = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + expect(isValidFieldElement(validHex)).toBe(true); + + // Invalid: zero + const zeroHex = '0x0000000000000000000000000000000000000000000000000000000000000000'; + expect(isValidFieldElement(zeroHex)).toBe(false); + + // Invalid: too large (>= FIELD_SIZE) + const tooLarge = FIELD_SIZE.toString(16); + expect(isValidFieldElement(tooLarge)).toBe(false); + }); + + test('generateNote creates valid note structure', () => { + const note = generateNote(Denomination.TEN); + expect(note.nullifier).toMatch(/^[0-9a-f]{64}$/); + expect(note.secret).toMatch(/^[0-9a-f]{64}$/); + expect(note.commitment).toMatch(/^[0-9a-f]{64}$/); + }); + + test('isValidBytes32 validates 32-byte hex strings', () => { + expect(isValidBytes32('0x' + 'a'.repeat(64))).toBe(true); + expect(isValidBytes32('a'.repeat(64))).toBe(true); + expect(isValidBytes32('a'.repeat(63))).toBe(false); + expect(isValidBytes32('g'.repeat(64))).toBe(false); // Invalid hex char + }); + + test('hexToBuffer and bufferToHex work correctly', () => { + const originalHex = 'abcdef0123456789'; + const buffer = hexToBuffer(originalHex); + const convertedHex = bufferToHex(buffer); + expect(convertedHex).toBe(originalHex); + + const withPrefix = bufferToHex(buffer, true); + expect(withPrefix).toBe(`0x${originalHex}`); + }); + + test('bytesToBigInt and bigIntToHex work correctly', () => { + const hex = 'deadbeef'; + const buffer = hexToBuffer(hex); + const bigInt = bytesToBigInt(buffer); + expect(bigInt).toBe(BigInt(`0x${hex}`)); + + const convertedHex = bigIntToHex(bigInt); + expect(convertedHex).toBe(hex.padStart(64, '0')); + }); +}); + +describe('Encoding utilities', () => { + test('hexToUint8Array and uint8ArrayToHex work correctly', () => { + const hex = 'deadbeef'; + const array = hexToUint8Array(hex); + expect(array.length).toBe(4); + expect(array[0]).toBe(0xde); + expect(array[1]).toBe(0xad); + + const convertedHex = uint8ArrayToHex(array); + expect(convertedHex).toBe(hex); + }); + + test('base64ToHex and hexToBase64 work correctly', () => { + const originalHex = 'deadbeef'; + const base64 = hexToBase64(originalHex); + expect(base64).toBe('3q2+7w=='); + + const convertedHex = base64ToHex(base64); + expect(convertedHex).toBe(originalHex); + }); + + test('isHex validates hex strings', () => { + expect(isHex('abcdef')).toBe(true); + expect(isHex('0xabcdef')).toBe(true); + expect(isHex('ghijkl')).toBe(false); + expect(isHex('')).toBe(false); + }); + + test('padHex pads correctly', () => { + expect(padHex('ab', 4)).toBe('000000ab'); + expect(padHex('0xab', 4, true)).toBe('0x000000ab'); + }); +}); + +describe('Validation utilities', () => { + test('isValidStellarAddress validates Stellar addresses', () => { + // Valid address + expect(isValidStellarAddress('GABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ')).toBe(true); + // Invalid length + expect(isValidStellarAddress('GABCD')).toBe(false); + // Doesn't start with G + expect(isValidStellarAddress('ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ')).toBe(false); + }); + + test('isValidContractId validates contract IDs', () => { + // Valid contract ID + expect(isValidContractId('CABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ')).toBe(true); + // Invalid length + expect(isValidContractId('CABCD')).toBe(false); + // Doesn't start with C + expect(isValidContractId('GABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ')).toBe(false); + }); + + test('isValidAmount validates amounts', () => { + expect(isValidAmount('100')).toBe(true); + expect(isValidAmount(100)).toBe(true); + expect(isValidAmount(100n)).toBe(true); + expect(isValidAmount('0')).toBe(false); + expect(isValidAmount('-100')).toBe(false); + expect(isValidAmount('abc')).toBe(false); + }); + + test('isValidDenomination validates denominations', () => { + expect(isValidDenomination(Denomination.TEN)).toBe(true); + expect(isValidDenomination(Denomination.HUNDRED)).toBe(true); + expect(isValidDenomination(Denomination.THOUSAND)).toBe(true); + expect(isValidDenomination(Denomination.TEN_THOUSAND)).toBe(true); + expect(isValidDenomination(999)).toBe(false); + }); + + test('validateNote validates note structure', () => { + const validNote = { + nullifier: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + secret: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + commitment: 'a'.repeat(64), + denomination: Denomination.TEN + }; + expect(validateNote(validNote)).toEqual([]); + + const invalidNote = { + nullifier: '0', + secret: '0', + commitment: 'a'.repeat(63), + denomination: 999 + }; + expect(validateNote(invalidNote).length).toBeGreaterThan(0); + }); + + test('validateNetworkConfig validates network config', () => { + const validConfig = { + rpcUrl: 'https://test.stellar.org', + networkPassphrase: 'Test Network', + contractId: 'CABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ' + }; + expect(validateNetworkConfig(validConfig)).toEqual([]); + + const invalidConfig = { + rpcUrl: '', + networkPassphrase: '', + contractId: 'INVALID' + }; + expect(validateNetworkConfig(invalidConfig).length).toBeGreaterThan(0); + }); +}); diff --git a/sdk/src/constants.ts b/sdk/src/constants.ts new file mode 100644 index 0000000..625c76e --- /dev/null +++ b/sdk/src/constants.ts @@ -0,0 +1,28 @@ +/** + * Network configurations and constants + */ + +export const NETWORKS = { + testnet: { + rpcUrl: 'https://soroban-testnet.stellar.org', + networkPassphrase: 'Test SDF Network ; September 2015', + contractId: '' // To be filled after deployment + }, + futurenet: { + rpcUrl: 'https://rpc-futurenet.stellar.org', + networkPassphrase: 'Test SDF Future Network ; October 2022', + contractId: '' + }, + standalone: { + rpcUrl: 'http://localhost:8000', + networkPassphrase: 'Standalone Network ; February 2017', + contractId: '' + } +} as const; + +export const MERKLE_TREE_DEPTH = 20; +export const FIELD_SIZE = BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'); + +export const ZERO_BYTES_32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +export const DEFAULT_NETWORK = 'testnet' as const; diff --git a/sdk/src/index.ts b/sdk/src/index.ts new file mode 100644 index 0000000..133d98a --- /dev/null +++ b/sdk/src/index.ts @@ -0,0 +1,9 @@ +/** + * PrivacyLayer TypeScript SDK + * + * Main entry point for the SDK + */ + +export * from './types'; +export * from './constants'; +export * as utils from './utils'; diff --git a/sdk/src/types.ts b/sdk/src/types.ts new file mode 100644 index 0000000..652a756 --- /dev/null +++ b/sdk/src/types.ts @@ -0,0 +1,50 @@ +/** + * Core types for PrivacyLayer SDK + */ + +export interface Note { + nullifier: string; // Hex string + secret: string; // Hex string + commitment: string; // Hex string + denomination: Denomination; +} + +export enum Denomination { + TEN = 10, + HUNDRED = 100, + THOUSAND = 1000, + TEN_THOUSAND = 10000 +} + +export interface DepositReceipt { + commitment: string; + leafIndex: number; + transactionHash: string; + timestamp: number; +} + +export interface NetworkConfig { + rpcUrl: string; + networkPassphrase: string; + contractId: string; +} + +export interface WithdrawParams { + note: Note; + recipient: string; + relayer?: string; + fee?: bigint; + merkleProof: string[]; + merkleRoot: string; +} + +export interface DepositParams { + note: Note; + sender: string; +} + +export interface MerkleTreeState { + root: string; + nextIndex: number; + leaves: string[]; +} diff --git a/sdk/src/utils/crypto.ts b/sdk/src/utils/crypto.ts new file mode 100644 index 0000000..463f8bc --- /dev/null +++ b/sdk/src/utils/crypto.ts @@ -0,0 +1,95 @@ +/** + * Cryptographic utilities for PrivacyLayer SDK + */ + +import { randomBytes } from 'crypto'; +import { FIELD_SIZE, ZERO_BYTES_32 } from '../constants'; + +/** + * Generate a random field element (0 < element < FIELD_SIZE) + * Returns hex string without 0x prefix + */ +export function randomFieldElement(): string { + const byteLength = 32; + let bytes: Buffer; + + do { + bytes = randomBytes(byteLength); + } while (bytesToBigInt(bytes) >= FIELD_SIZE || bytesToBigInt(bytes) === BigInt(0)); + + return bytes.toString('hex'); +} + +/** + * Convert hex string to Buffer + */ +export function hexToBuffer(hex: string): Buffer { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return Buffer.from(cleanHex, 'hex'); +} + +/** + * Convert Buffer to hex string + */ +export function bufferToHex(buffer: Buffer, withPrefix: boolean = false): string { + const hex = buffer.toString('hex'); + return withPrefix ? `0x${hex}` : hex; +} + +/** + * Convert bytes to BigInt + */ +export function bytesToBigInt(bytes: Buffer): bigint { + return BigInt(`0x${bytes.toString('hex')}`); +} + +/** + * Convert BigInt to hex string + */ +export function bigIntToHex(num: bigint, withPrefix: boolean = false): string { + const hex = num.toString(16).padStart(64, '0'); + return withPrefix ? `0x${hex}` : hex; +} + +/** + * Check if a hex string represents a valid field element + */ +export function isValidFieldElement(hex: string): boolean { + try { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + if (!/^[0-9a-fA-F]{64}$/.test(cleanHex)) { + return false; + } + const value = BigInt(`0x${cleanHex}`); + return value > BigInt(0) && value < FIELD_SIZE; + } catch { + return false; + } +} + +/** + * Generate a random note (nullifier and secret) + */ +export function generateNote(denomination: number): { nullifier: string; secret: string; commitment: string } { + const nullifier = randomFieldElement(); + const secret = randomFieldElement(); + + // In a real implementation, this would compute Poseidon2(nullifier || secret) + // For now, we'll create a mock commitment by hashing the concatenation + const combined = `${nullifier}${secret}`; + const mockHash = Buffer.from(combined).toString('hex').slice(0, 64); + + return { + nullifier, + secret, + commitment: mockHash + }; +} + +/** + * Check if a string is a valid 32-byte hex string + */ +export function isValidBytes32(hex: string): boolean { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return /^[0-9a-fA-F]{64}$/.test(cleanHex); +} diff --git a/sdk/src/utils/encoding.ts b/sdk/src/utils/encoding.ts new file mode 100644 index 0000000..703324e --- /dev/null +++ b/sdk/src/utils/encoding.ts @@ -0,0 +1,73 @@ +/** + * Encoding and decoding utilities + */ + +import { Buffer } from 'buffer'; + +/** + * Convert hex string to Uint8Array + */ +export function hexToUint8Array(hex: string): Uint8Array { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + const matches = cleanHex.match(/.{1,2}/g) || []; + return new Uint8Array(matches.map(byte => parseInt(byte, 16))); +} + +/** + * Convert Uint8Array to hex string + */ +export function uint8ArrayToHex(bytes: Uint8Array, withPrefix: boolean = false): string { + const hex = Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + return withPrefix ? `0x${hex}` : hex; +} + +/** + * Convert base64 string to hex + */ +export function base64ToHex(base64: string): string { + const bytes = Buffer.from(base64, 'base64'); + return bytes.toString('hex'); +} + +/** + * Convert hex to base64 string + */ +export function hexToBase64(hex: string): string { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + const bytes = Buffer.from(cleanHex, 'hex'); + return bytes.toString('base64'); +} + +/** + * Convert string to hex representation + */ +export function stringToHex(str: string): string { + return Buffer.from(str, 'utf8').toString('hex'); +} + +/** + * Convert hex to string + */ +export function hexToString(hex: string): string { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return Buffer.from(cleanHex, 'hex').toString('utf8'); +} + +/** + * Pad hex string to specified byte length + */ +export function padHex(hex: string, byteLength: number, withPrefix: boolean = false): string { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + const padded = cleanHex.padStart(byteLength * 2, '0'); + return withPrefix ? `0x${padded}` : padded; +} + +/** + * Check if string is valid hex + */ +export function isHex(value: string): boolean { + const cleanValue = value.startsWith('0x') ? value.slice(2) : value; + return /^[0-9a-fA-F]+$/.test(cleanValue); +} diff --git a/sdk/src/utils/index.ts b/sdk/src/utils/index.ts new file mode 100644 index 0000000..aadbfc4 --- /dev/null +++ b/sdk/src/utils/index.ts @@ -0,0 +1,7 @@ +/** + * Utilities index file + */ + +export * from './crypto'; +export * from './encoding'; +export * from './validation'; diff --git a/sdk/src/utils/validation.ts b/sdk/src/utils/validation.ts new file mode 100644 index 0000000..52116c9 --- /dev/null +++ b/sdk/src/utils/validation.ts @@ -0,0 +1,108 @@ +/** + * Validation utilities + */ + +import { isValidFieldElement, isValidBytes32 } from './crypto'; +import { Denomination } from '../types'; + +/** + * Validate Stellar address + */ +export function isValidStellarAddress(address: string): boolean { + // Stellar addresses start with G and are 56 characters long + const pattern = /^G[A-Z0-9]{55}$/; + return pattern.test(address); +} + +/** + * Validate contract ID (Soroban contract address) + */ +export function isValidContractId(contractId: string): boolean { + // Contract IDs start with C and are 56 characters long + const pattern = /^C[A-Z0-9]{55}$/; + return pattern.test(contractId); +} + +/** + * Validate amount is positive integer + */ +export function isValidAmount(amount: bigint | number | string): boolean { + try { + const value = BigInt(amount); + return value > 0n; + } catch { + return false; + } +} + +/** + * Validate denomination + */ +export function isValidDenomination(denomination: number): boolean { + return Object.values(Denomination).includes(denomination as Denomination); +} + +/** + * Validate hex string length + */ +export function validateHexLength(hex: string, expectedBytes: number): boolean { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return cleanHex.length === expectedBytes * 2; +} + +/** + * Validate note structure + */ +export function validateNote(note: { + nullifier: string; + secret: string; + commitment: string; + denomination: number; +}): string[] { + const errors: string[] = []; + + if (!isValidFieldElement(note.nullifier)) { + errors.push('Invalid nullifier field element'); + } + if (!isValidFieldElement(note.secret)) { + errors.push('Invalid secret field element'); + } + if (!isValidBytes32(note.commitment)) { + errors.push('Invalid commitment format (must be 32 bytes hex)'); + } + if (!isValidDenomination(note.denomination)) { + errors.push('Invalid denomination'); + } + + return errors; +} + +/** + * Validate network configuration + */ +export function validateNetworkConfig(config: { + rpcUrl: string; + networkPassphrase: string; + contractId: string; +}): string[] { + const errors: string[] = []; + + if (!config.rpcUrl || typeof config.rpcUrl !== 'string') { + errors.push('Invalid rpcUrl'); + } + if (!config.networkPassphrase || typeof config.networkPassphrase !== 'string') { + errors.push('Invalid networkPassphrase'); + } + if (config.contractId && !isValidContractId(config.contractId)) { + errors.push('Invalid contractId'); + } + + return errors; +} + +/** + * Validate fee doesn't exceed amount + */ +export function validateFee(fee: bigint, amount: bigint): boolean { + return fee >= 0n && fee <= amount; +} diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json new file mode 100644 index 0000000..698cdbb --- /dev/null +++ b/sdk/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/__tests__/*" + ] +}