@sip-protocol/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.mts +3640 -0
- package/dist/index.d.ts +3640 -0
- package/dist/index.js +5725 -0
- package/dist/index.mjs +5606 -0
- package/package.json +61 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/near-intents.ts +475 -0
- package/src/adapters/oneclick-client.ts +367 -0
- package/src/commitment.ts +470 -0
- package/src/crypto.ts +93 -0
- package/src/errors.ts +471 -0
- package/src/index.ts +369 -0
- package/src/intent.ts +488 -0
- package/src/privacy.ts +382 -0
- package/src/proofs/index.ts +52 -0
- package/src/proofs/interface.ts +228 -0
- package/src/proofs/mock.ts +258 -0
- package/src/proofs/noir.ts +233 -0
- package/src/sip.ts +299 -0
- package/src/solver/index.ts +25 -0
- package/src/solver/mock-solver.ts +278 -0
- package/src/stealth.ts +414 -0
- package/src/validation.ts +401 -0
- package/src/wallet/base-adapter.ts +407 -0
- package/src/wallet/errors.ts +106 -0
- package/src/wallet/ethereum/adapter.ts +655 -0
- package/src/wallet/ethereum/index.ts +48 -0
- package/src/wallet/ethereum/mock.ts +505 -0
- package/src/wallet/ethereum/types.ts +364 -0
- package/src/wallet/index.ts +116 -0
- package/src/wallet/registry.ts +207 -0
- package/src/wallet/solana/adapter.ts +533 -0
- package/src/wallet/solana/index.ts +40 -0
- package/src/wallet/solana/mock.ts +522 -0
- package/src/wallet/solana/types.ts +253 -0
- package/src/zcash/index.ts +53 -0
- package/src/zcash/rpc-client.ts +623 -0
- package/src/zcash/shielded-service.ts +641 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities for SIP Protocol SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive validation for all public API inputs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
PrivacyLevel,
|
|
9
|
+
ChainId,
|
|
10
|
+
Asset,
|
|
11
|
+
IntentInput,
|
|
12
|
+
IntentOutput,
|
|
13
|
+
CreateIntentParams,
|
|
14
|
+
HexString,
|
|
15
|
+
} from '@sip-protocol/types'
|
|
16
|
+
import { ValidationError } from './errors'
|
|
17
|
+
|
|
18
|
+
// ─── Chain IDs ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Valid chain identifiers
|
|
22
|
+
*/
|
|
23
|
+
const VALID_CHAIN_IDS: readonly ChainId[] = [
|
|
24
|
+
'solana',
|
|
25
|
+
'ethereum',
|
|
26
|
+
'near',
|
|
27
|
+
'zcash',
|
|
28
|
+
'polygon',
|
|
29
|
+
'arbitrum',
|
|
30
|
+
'optimism',
|
|
31
|
+
'base',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a string is a valid chain ID
|
|
36
|
+
*/
|
|
37
|
+
export function isValidChainId(chain: string): chain is ChainId {
|
|
38
|
+
return VALID_CHAIN_IDS.includes(chain as ChainId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Privacy Levels ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Valid privacy levels
|
|
45
|
+
*/
|
|
46
|
+
const VALID_PRIVACY_LEVELS: readonly PrivacyLevel[] = [
|
|
47
|
+
'transparent',
|
|
48
|
+
'shielded',
|
|
49
|
+
'compliant',
|
|
50
|
+
] as unknown as readonly PrivacyLevel[]
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a value is a valid privacy level
|
|
54
|
+
*/
|
|
55
|
+
export function isValidPrivacyLevel(level: unknown): level is PrivacyLevel {
|
|
56
|
+
if (typeof level !== 'string') return false
|
|
57
|
+
return ['transparent', 'shielded', 'compliant'].includes(level)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Hex Strings ───────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a string is valid hex format (with 0x prefix)
|
|
64
|
+
*/
|
|
65
|
+
export function isValidHex(value: string): value is HexString {
|
|
66
|
+
if (typeof value !== 'string') return false
|
|
67
|
+
if (!value.startsWith('0x')) return false
|
|
68
|
+
const hex = value.slice(2)
|
|
69
|
+
if (hex.length === 0) return false
|
|
70
|
+
return /^[0-9a-fA-F]+$/.test(hex)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if a hex string has a specific byte length
|
|
75
|
+
*/
|
|
76
|
+
export function isValidHexLength(value: string, byteLength: number): boolean {
|
|
77
|
+
if (!isValidHex(value)) return false
|
|
78
|
+
const hex = value.slice(2)
|
|
79
|
+
return hex.length === byteLength * 2
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Amount Validation ─────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if an amount is valid (positive bigint)
|
|
86
|
+
*/
|
|
87
|
+
export function isValidAmount(value: unknown): value is bigint {
|
|
88
|
+
return typeof value === 'bigint' && value > 0n
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if an amount is non-negative
|
|
93
|
+
*/
|
|
94
|
+
export function isNonNegativeAmount(value: unknown): value is bigint {
|
|
95
|
+
return typeof value === 'bigint' && value >= 0n
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Slippage Validation ───────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if slippage is in valid range (0 to 1, exclusive of 1)
|
|
102
|
+
*/
|
|
103
|
+
export function isValidSlippage(value: number): boolean {
|
|
104
|
+
return typeof value === 'number' && !isNaN(value) && value >= 0 && value < 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Stealth Address Validation ────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* SIP stealth meta-address format:
|
|
111
|
+
* sip:<chain>:<spendingKey>:<viewingKey>
|
|
112
|
+
* Each key is 33 bytes compressed (66 hex chars with 0x prefix)
|
|
113
|
+
*/
|
|
114
|
+
const STEALTH_META_ADDRESS_REGEX = /^sip:[a-z]+:0x[0-9a-fA-F]{66}:0x[0-9a-fA-F]{66}$/
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a string is a valid stealth meta-address
|
|
118
|
+
*/
|
|
119
|
+
export function isValidStealthMetaAddress(addr: string): boolean {
|
|
120
|
+
if (typeof addr !== 'string') return false
|
|
121
|
+
return STEALTH_META_ADDRESS_REGEX.test(addr)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if a public key is valid (compressed secp256k1: 33 bytes)
|
|
126
|
+
*/
|
|
127
|
+
export function isValidCompressedPublicKey(key: string): boolean {
|
|
128
|
+
if (!isValidHexLength(key, 33)) return false
|
|
129
|
+
// Compressed keys start with 02 or 03
|
|
130
|
+
const prefix = key.slice(2, 4)
|
|
131
|
+
return prefix === '02' || prefix === '03'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a private key is valid (32 bytes)
|
|
136
|
+
*/
|
|
137
|
+
export function isValidPrivateKey(key: string): boolean {
|
|
138
|
+
return isValidHexLength(key, 32)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Asset Validation ──────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate an asset object
|
|
145
|
+
*/
|
|
146
|
+
export function validateAsset(asset: unknown, field: string): asserts asset is Asset {
|
|
147
|
+
if (!asset || typeof asset !== 'object') {
|
|
148
|
+
throw new ValidationError('must be an object', field)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const a = asset as Partial<Asset>
|
|
152
|
+
|
|
153
|
+
if (!a.chain || !isValidChainId(a.chain)) {
|
|
154
|
+
throw new ValidationError(
|
|
155
|
+
`invalid chain '${a.chain}', must be one of: ${VALID_CHAIN_IDS.join(', ')}`,
|
|
156
|
+
`${field}.chain`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof a.symbol !== 'string' || a.symbol.length === 0) {
|
|
161
|
+
throw new ValidationError('symbol must be a non-empty string', `${field}.symbol`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (a.address !== null && !isValidHex(a.address as string)) {
|
|
165
|
+
throw new ValidationError('address must be null or valid hex string', `${field}.address`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof a.decimals !== 'number' || !Number.isInteger(a.decimals) || a.decimals < 0) {
|
|
169
|
+
throw new ValidationError('decimals must be a non-negative integer', `${field}.decimals`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Intent Input Validation ───────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate intent input
|
|
177
|
+
*/
|
|
178
|
+
export function validateIntentInput(input: unknown, field: string = 'input'): asserts input is IntentInput {
|
|
179
|
+
if (!input || typeof input !== 'object') {
|
|
180
|
+
throw new ValidationError('must be an object', field)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const i = input as Partial<IntentInput>
|
|
184
|
+
|
|
185
|
+
// Validate asset
|
|
186
|
+
validateAsset(i.asset, `${field}.asset`)
|
|
187
|
+
|
|
188
|
+
// Validate amount
|
|
189
|
+
if (!isValidAmount(i.amount)) {
|
|
190
|
+
throw new ValidationError(
|
|
191
|
+
'amount must be a positive bigint',
|
|
192
|
+
`${field}.amount`,
|
|
193
|
+
{ received: typeof i.amount, value: String(i.amount) }
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Intent Output Validation ──────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate intent output
|
|
202
|
+
*/
|
|
203
|
+
export function validateIntentOutput(output: unknown, field: string = 'output'): asserts output is IntentOutput {
|
|
204
|
+
if (!output || typeof output !== 'object') {
|
|
205
|
+
throw new ValidationError('must be an object', field)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const o = output as Partial<IntentOutput>
|
|
209
|
+
|
|
210
|
+
// Validate asset
|
|
211
|
+
validateAsset(o.asset, `${field}.asset`)
|
|
212
|
+
|
|
213
|
+
// Validate minAmount
|
|
214
|
+
if (!isNonNegativeAmount(o.minAmount)) {
|
|
215
|
+
throw new ValidationError(
|
|
216
|
+
'minAmount must be a non-negative bigint',
|
|
217
|
+
`${field}.minAmount`,
|
|
218
|
+
{ received: typeof o.minAmount, value: String(o.minAmount) }
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Validate maxSlippage
|
|
223
|
+
if (!isValidSlippage(o.maxSlippage as number)) {
|
|
224
|
+
throw new ValidationError(
|
|
225
|
+
'maxSlippage must be a number between 0 and 1 (exclusive)',
|
|
226
|
+
`${field}.maxSlippage`,
|
|
227
|
+
{ received: o.maxSlippage }
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Create Intent Params Validation ───────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Validate CreateIntentParams
|
|
236
|
+
*/
|
|
237
|
+
export function validateCreateIntentParams(params: unknown): asserts params is CreateIntentParams {
|
|
238
|
+
if (!params || typeof params !== 'object') {
|
|
239
|
+
throw new ValidationError('params must be an object')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const p = params as Partial<CreateIntentParams>
|
|
243
|
+
|
|
244
|
+
// Required: input
|
|
245
|
+
if (!p.input) {
|
|
246
|
+
throw new ValidationError('input is required', 'input')
|
|
247
|
+
}
|
|
248
|
+
validateIntentInput(p.input, 'input')
|
|
249
|
+
|
|
250
|
+
// Required: output
|
|
251
|
+
if (!p.output) {
|
|
252
|
+
throw new ValidationError('output is required', 'output')
|
|
253
|
+
}
|
|
254
|
+
validateIntentOutput(p.output, 'output')
|
|
255
|
+
|
|
256
|
+
// Required: privacy
|
|
257
|
+
if (!p.privacy) {
|
|
258
|
+
throw new ValidationError('privacy is required', 'privacy')
|
|
259
|
+
}
|
|
260
|
+
if (!isValidPrivacyLevel(p.privacy)) {
|
|
261
|
+
throw new ValidationError(
|
|
262
|
+
`invalid privacy level '${p.privacy}', must be one of: transparent, shielded, compliant`,
|
|
263
|
+
'privacy'
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Conditional: recipientMetaAddress for shielded modes
|
|
268
|
+
if ((p.privacy === 'shielded' || p.privacy === 'compliant') && p.recipientMetaAddress) {
|
|
269
|
+
if (!isValidStealthMetaAddress(p.recipientMetaAddress)) {
|
|
270
|
+
throw new ValidationError(
|
|
271
|
+
'invalid stealth meta-address format, expected: sip:<chain>:<spendingKey>:<viewingKey>',
|
|
272
|
+
'recipientMetaAddress'
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Conditional: viewingKey for compliant mode
|
|
278
|
+
if (p.privacy === 'compliant' && !p.viewingKey) {
|
|
279
|
+
throw new ValidationError(
|
|
280
|
+
'viewingKey is required for compliant mode',
|
|
281
|
+
'viewingKey'
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (p.viewingKey && !isValidHex(p.viewingKey)) {
|
|
286
|
+
throw new ValidationError(
|
|
287
|
+
'viewingKey must be a valid hex string',
|
|
288
|
+
'viewingKey'
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Optional: ttl
|
|
293
|
+
if (p.ttl !== undefined) {
|
|
294
|
+
if (typeof p.ttl !== 'number' || !Number.isInteger(p.ttl) || p.ttl <= 0) {
|
|
295
|
+
throw new ValidationError(
|
|
296
|
+
'ttl must be a positive integer (seconds)',
|
|
297
|
+
'ttl',
|
|
298
|
+
{ received: p.ttl }
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Viewing Key Validation ────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Validate a viewing key (32-byte hex string)
|
|
308
|
+
*/
|
|
309
|
+
export function validateViewingKey(key: unknown, field: string = 'viewingKey'): void {
|
|
310
|
+
if (!key || typeof key !== 'string') {
|
|
311
|
+
throw new ValidationError('must be a string', field)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!isValidHex(key)) {
|
|
315
|
+
throw new ValidationError('must be a valid hex string with 0x prefix', field)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!isValidHexLength(key, 32)) {
|
|
319
|
+
throw new ValidationError('must be 32 bytes (64 hex characters)', field)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── Scalar/Point Validation for Cryptographic Operations ──────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* secp256k1 curve order (n)
|
|
327
|
+
*/
|
|
328
|
+
const SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check if a bigint is a valid scalar (1 <= x < n)
|
|
332
|
+
*/
|
|
333
|
+
export function isValidScalar(value: bigint): boolean {
|
|
334
|
+
return value > 0n && value < SECP256K1_ORDER
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Validate a scalar value
|
|
339
|
+
*/
|
|
340
|
+
export function validateScalar(value: unknown, field: string): asserts value is bigint {
|
|
341
|
+
if (typeof value !== 'bigint') {
|
|
342
|
+
throw new ValidationError('must be a bigint', field)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!isValidScalar(value)) {
|
|
346
|
+
throw new ValidationError(
|
|
347
|
+
'must be in range (0, curve order)',
|
|
348
|
+
field,
|
|
349
|
+
{ curveOrder: SECP256K1_ORDER.toString(16) }
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── Timestamp Validation ──────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Validate a timestamp (positive integer, not in the past)
|
|
358
|
+
*/
|
|
359
|
+
export function validateTimestamp(
|
|
360
|
+
value: unknown,
|
|
361
|
+
field: string,
|
|
362
|
+
options: { allowPast?: boolean } = {}
|
|
363
|
+
): asserts value is number {
|
|
364
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
365
|
+
throw new ValidationError('must be a non-negative integer', field)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!options.allowPast && value < Date.now()) {
|
|
369
|
+
throw new ValidationError('must not be in the past', field)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Composite Validators ──────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validate multiple conditions and collect all errors
|
|
377
|
+
*/
|
|
378
|
+
export function validateAll(validators: Array<() => void>): void {
|
|
379
|
+
const errors: ValidationError[] = []
|
|
380
|
+
|
|
381
|
+
for (const validator of validators) {
|
|
382
|
+
try {
|
|
383
|
+
validator()
|
|
384
|
+
} catch (e) {
|
|
385
|
+
if (e instanceof ValidationError) {
|
|
386
|
+
errors.push(e)
|
|
387
|
+
} else {
|
|
388
|
+
throw e
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (errors.length === 1) {
|
|
394
|
+
throw errors[0]
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (errors.length > 1) {
|
|
398
|
+
const messages = errors.map(e => e.message).join('; ')
|
|
399
|
+
throw new ValidationError(`Multiple validation errors: ${messages}`)
|
|
400
|
+
}
|
|
401
|
+
}
|