sol-trade-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/README.md +390 -0
- package/dist/chunk-MMQAMIKR.mjs +3735 -0
- package/dist/chunk-NEZDFAYA.mjs +7744 -0
- package/dist/clients-VITWK7B6.mjs +1370 -0
- package/dist/index-1BK_FXsW.d.mts +2327 -0
- package/dist/index-1BK_FXsW.d.ts +2327 -0
- package/dist/index.d.mts +2659 -0
- package/dist/index.d.ts +2659 -0
- package/dist/index.js +13265 -0
- package/dist/index.mjs +562 -0
- package/dist/perf/index.d.mts +2 -0
- package/dist/perf/index.d.ts +2 -0
- package/dist/perf/index.js +3742 -0
- package/dist/perf/index.mjs +214 -0
- package/package.json +101 -0
- package/src/__tests__/complete_sdk.test.ts +354 -0
- package/src/__tests__/hotpath.test.ts +486 -0
- package/src/__tests__/nonce.test.ts +45 -0
- package/src/__tests__/sdk.test.ts +425 -0
- package/src/address-lookup/index.ts +197 -0
- package/src/cache/cache.ts +308 -0
- package/src/calc/index.ts +1058 -0
- package/src/calc/pumpfun.ts +124 -0
- package/src/common/bonding_curve.ts +272 -0
- package/src/common/compute-budget.ts +148 -0
- package/src/common/confirm-any-signature.ts +184 -0
- package/src/common/fast-timing.ts +481 -0
- package/src/common/fast_fn.ts +150 -0
- package/src/common/gas-fee-strategy.ts +253 -0
- package/src/common/map-pool.ts +23 -0
- package/src/common/nonce.ts +40 -0
- package/src/common/sdk-log.ts +460 -0
- package/src/common/seed.ts +381 -0
- package/src/common/spl-token.ts +578 -0
- package/src/common/subscription-handle.ts +644 -0
- package/src/common/trading-utils.ts +239 -0
- package/src/common/wsol-manager.ts +325 -0
- package/src/compute/compute_budget_manager.ts +187 -0
- package/src/compute/index.ts +21 -0
- package/src/constants/index.ts +96 -0
- package/src/execution/execution.ts +532 -0
- package/src/execution/index.ts +42 -0
- package/src/hotpath/executor.ts +464 -0
- package/src/hotpath/index.ts +64 -0
- package/src/hotpath/state.ts +435 -0
- package/src/index.ts +2117 -0
- package/src/instruction/bonk_builder.ts +730 -0
- package/src/instruction/index.ts +24 -0
- package/src/instruction/meteora_damm_v2_builder.ts +509 -0
- package/src/instruction/pumpfun_builder.ts +1183 -0
- package/src/instruction/pumpswap.ts +1123 -0
- package/src/instruction/raydium_amm_v4_builder.ts +692 -0
- package/src/instruction/raydium_cpmm_builder.ts +795 -0
- package/src/middleware/traits.ts +407 -0
- package/src/params/index.ts +483 -0
- package/src/perf/compiler-optimization.ts +529 -0
- package/src/perf/hardware.ts +631 -0
- package/src/perf/index.ts +9 -0
- package/src/perf/kernel-bypass.ts +656 -0
- package/src/perf/protocol.ts +682 -0
- package/src/perf/realtime.ts +592 -0
- package/src/perf/simd.ts +668 -0
- package/src/perf/syscall-bypass.ts +331 -0
- package/src/perf/ultra-low-latency.ts +505 -0
- package/src/perf/zero-copy.ts +589 -0
- package/src/pool/pool.ts +294 -0
- package/src/rpc/client.ts +345 -0
- package/src/sdk-errors.ts +13 -0
- package/src/security/index.ts +26 -0
- package/src/security/secure-key.ts +303 -0
- package/src/security/validators.ts +281 -0
- package/src/seed/pda.ts +262 -0
- package/src/serialization/index.ts +28 -0
- package/src/serialization/serialization.ts +288 -0
- package/src/swqos/clients.ts +1754 -0
- package/src/swqos/index.ts +50 -0
- package/src/swqos/providers.ts +1707 -0
- package/src/trading/core/async-executor.ts +702 -0
- package/src/trading/core/confirmation-monitor.ts +711 -0
- package/src/trading/core/index.ts +82 -0
- package/src/trading/core/retry-handler.ts +683 -0
- package/src/trading/core/transaction-pool.ts +780 -0
- package/src/trading/executor.ts +385 -0
- package/src/trading/factory.ts +282 -0
- package/src/trading/index.ts +30 -0
- package/src/types.ts +8 -0
- package/src/utils/index.ts +155 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure key storage and management for Sol Trade SDK
|
|
3
|
+
*
|
|
4
|
+
* Implements secure memory handling for private keys with:
|
|
5
|
+
* - Memory encryption at rest
|
|
6
|
+
* - Secure zeroing after use
|
|
7
|
+
* - Context manager for automatic cleanup
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Keypair, PublicKey } from '@solana/web3.js';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import * as nacl from 'tweetnacl';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown when secure key operation fails
|
|
16
|
+
*/
|
|
17
|
+
export class SecureKeyError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'SecureKeyError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error thrown when trying to access a cleared key
|
|
26
|
+
*/
|
|
27
|
+
export class KeyNotAvailableError extends SecureKeyError {
|
|
28
|
+
constructor(message: string = 'Key not available') {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'KeyNotAvailableError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Metadata about a stored key
|
|
36
|
+
*/
|
|
37
|
+
export interface KeyMetadata {
|
|
38
|
+
pubkey: string;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
lastAccessed?: number;
|
|
41
|
+
accessCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Secure storage for Solana private keys.
|
|
46
|
+
*
|
|
47
|
+
* Features:
|
|
48
|
+
* - Keys are encrypted in memory when not in use
|
|
49
|
+
* - Automatic secure zeroing of key material
|
|
50
|
+
* - Context manager support for temporary key access
|
|
51
|
+
*/
|
|
52
|
+
export class SecureKeyStorage {
|
|
53
|
+
private encryptedKey: Buffer | null = null;
|
|
54
|
+
private salt: Buffer | null = null;
|
|
55
|
+
private pubkey: string | null = null;
|
|
56
|
+
private isUnlocked: boolean = false;
|
|
57
|
+
private unlockedKey: Buffer | null = null;
|
|
58
|
+
private metadata: KeyMetadata | null = null;
|
|
59
|
+
private passwordProtected: boolean = false;
|
|
60
|
+
|
|
61
|
+
private constructor() {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create secure storage from a Keypair.
|
|
65
|
+
*/
|
|
66
|
+
static fromKeypair(keypair: Keypair, password?: string): SecureKeyStorage {
|
|
67
|
+
const storage = new SecureKeyStorage();
|
|
68
|
+
storage.pubkey = keypair.publicKey.toBase58();
|
|
69
|
+
|
|
70
|
+
// Get secret key bytes
|
|
71
|
+
const secretBytes = Buffer.from(keypair.secretKey);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (password) {
|
|
75
|
+
storage.passwordProtected = true;
|
|
76
|
+
storage.salt = crypto.randomBytes(16);
|
|
77
|
+
storage.encryptedKey = storage.encryptWithPassword(
|
|
78
|
+
secretBytes,
|
|
79
|
+
password,
|
|
80
|
+
storage.salt
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
// Simple XOR encryption with random key (better than plaintext)
|
|
84
|
+
storage.salt = crypto.randomBytes(64);
|
|
85
|
+
storage.encryptedKey = storage.xorEncrypt(secretBytes, storage.salt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
storage.metadata = {
|
|
89
|
+
pubkey: storage.pubkey,
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
accessCount: 0,
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
// Always clear the secret bytes from memory
|
|
95
|
+
storage.secureZero(secretBytes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return storage;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create secure storage from a seed.
|
|
103
|
+
*/
|
|
104
|
+
static fromSeed(seed: Uint8Array, password?: string): SecureKeyStorage {
|
|
105
|
+
if (seed.length !== 32) {
|
|
106
|
+
throw new SecureKeyError(`Seed must be 32 bytes, got ${seed.length}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const keypair = Keypair.fromSeed(seed);
|
|
110
|
+
try {
|
|
111
|
+
return SecureKeyStorage.fromKeypair(keypair, password);
|
|
112
|
+
} finally {
|
|
113
|
+
// Clear seed from memory
|
|
114
|
+
SecureKeyStorage.secureZero(Buffer.from(seed));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create secure storage from a mnemonic phrase.
|
|
120
|
+
*/
|
|
121
|
+
static fromMnemonic(mnemonic: string, password?: string): SecureKeyStorage {
|
|
122
|
+
// Simplified implementation - in production use proper BIP39 derivation
|
|
123
|
+
const seed = crypto
|
|
124
|
+
.pbkdf2Sync(mnemonic, 'mnemonic', 2048, 32, 'sha512');
|
|
125
|
+
const keypair = Keypair.fromSeed(seed);
|
|
126
|
+
try {
|
|
127
|
+
return SecureKeyStorage.fromKeypair(keypair, password);
|
|
128
|
+
} finally {
|
|
129
|
+
SecureKeyStorage.secureZero(seed);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private encryptWithPassword(data: Buffer, password: string, salt: Buffer): Buffer {
|
|
134
|
+
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
|
135
|
+
const iv = crypto.randomBytes(16);
|
|
136
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
137
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
138
|
+
const authTag = cipher.getAuthTag();
|
|
139
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private decryptWithPassword(encryptedData: Buffer, password: string, salt: Buffer): Buffer {
|
|
143
|
+
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
|
144
|
+
const iv = encryptedData.slice(0, 16);
|
|
145
|
+
const authTag = encryptedData.slice(16, 32);
|
|
146
|
+
const encrypted = encryptedData.slice(32);
|
|
147
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
148
|
+
decipher.setAuthTag(authTag);
|
|
149
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private xorEncrypt(data: Buffer, key: Buffer): Buffer {
|
|
153
|
+
const result = Buffer.alloc(data.length);
|
|
154
|
+
for (let i = 0; i < data.length; i++) {
|
|
155
|
+
const dataByte = data[i] ?? 0;
|
|
156
|
+
const keyByte = key[i % key.length] ?? 0;
|
|
157
|
+
result[i] = dataByte ^ keyByte;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Securely zero out sensitive data from memory
|
|
164
|
+
*/
|
|
165
|
+
private static secureZero(data: Buffer): void {
|
|
166
|
+
// Overwrite with zeros
|
|
167
|
+
data.fill(0);
|
|
168
|
+
// Overwrite with ones
|
|
169
|
+
data.fill(0xff);
|
|
170
|
+
// Overwrite with random
|
|
171
|
+
crypto.randomFillSync(data);
|
|
172
|
+
// Final zero
|
|
173
|
+
data.fill(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private secureZero(data: Buffer): void {
|
|
177
|
+
SecureKeyStorage.secureZero(data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Temporarily access the keypair.
|
|
182
|
+
* Key is automatically cleared after callback completes.
|
|
183
|
+
*/
|
|
184
|
+
async withKeypair<T>(
|
|
185
|
+
callback: (keypair: Keypair) => Promise<T>,
|
|
186
|
+
password?: string
|
|
187
|
+
): Promise<T> {
|
|
188
|
+
if (!this.encryptedKey) {
|
|
189
|
+
throw new KeyNotAvailableError('No key stored');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.passwordProtected && !password) {
|
|
193
|
+
throw new SecureKeyError('Password required to unlock');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let decrypted: Buffer | null = null;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Decrypt the key
|
|
200
|
+
if (this.passwordProtected && this.salt) {
|
|
201
|
+
decrypted = this.decryptWithPassword(this.encryptedKey, password!, this.salt);
|
|
202
|
+
} else if (this.salt) {
|
|
203
|
+
decrypted = this.xorEncrypt(this.encryptedKey, this.salt);
|
|
204
|
+
} else {
|
|
205
|
+
throw new SecureKeyError('Invalid storage state');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create keypair from decrypted bytes
|
|
209
|
+
const keypair = Keypair.fromSeed(decrypted.slice(0, 32));
|
|
210
|
+
|
|
211
|
+
// Update metadata
|
|
212
|
+
if (this.metadata) {
|
|
213
|
+
this.metadata.lastAccessed = Date.now();
|
|
214
|
+
this.metadata.accessCount++;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.isUnlocked = true;
|
|
218
|
+
this.unlockedKey = decrypted;
|
|
219
|
+
|
|
220
|
+
return await callback(keypair);
|
|
221
|
+
} finally {
|
|
222
|
+
// Always cleanup
|
|
223
|
+
this.isUnlocked = false;
|
|
224
|
+
if (this.unlockedKey) {
|
|
225
|
+
this.secureZero(this.unlockedKey);
|
|
226
|
+
this.unlockedKey = null;
|
|
227
|
+
}
|
|
228
|
+
if (decrypted) {
|
|
229
|
+
this.secureZero(decrypted);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Sign a message without exposing the keypair.
|
|
236
|
+
*/
|
|
237
|
+
async signMessage(message: Buffer | Uint8Array, password?: string): Promise<Buffer> {
|
|
238
|
+
return this.withKeypair(async (keypair) => {
|
|
239
|
+
const signature = nacl.sign.detached(Buffer.from(message), keypair.secretKey);
|
|
240
|
+
return Buffer.from(signature);
|
|
241
|
+
}, password);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the public key (safe to access)
|
|
246
|
+
*/
|
|
247
|
+
getPublicKey(): string {
|
|
248
|
+
return this.pubkey || '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if storage requires password
|
|
253
|
+
*/
|
|
254
|
+
get isPasswordProtected(): boolean {
|
|
255
|
+
return this.passwordProtected;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get key metadata
|
|
260
|
+
*/
|
|
261
|
+
getMetadata(): KeyMetadata | null {
|
|
262
|
+
return this.metadata;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Permanently clear all key material
|
|
267
|
+
*/
|
|
268
|
+
clear(): void {
|
|
269
|
+
if (this.encryptedKey) {
|
|
270
|
+
this.secureZero(this.encryptedKey);
|
|
271
|
+
this.encryptedKey = null;
|
|
272
|
+
}
|
|
273
|
+
if (this.salt) {
|
|
274
|
+
this.secureZero(this.salt);
|
|
275
|
+
this.salt = null;
|
|
276
|
+
}
|
|
277
|
+
if (this.unlockedKey) {
|
|
278
|
+
this.secureZero(this.unlockedKey);
|
|
279
|
+
this.unlockedKey = null;
|
|
280
|
+
}
|
|
281
|
+
this.pubkey = null;
|
|
282
|
+
this.metadata = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Convenience function for quick signing
|
|
288
|
+
*/
|
|
289
|
+
export async function signWithKeypair(
|
|
290
|
+
keypair: Keypair,
|
|
291
|
+
message: Buffer | Uint8Array,
|
|
292
|
+
clearAfter: boolean = false
|
|
293
|
+
): Promise<Buffer> {
|
|
294
|
+
const signature = nacl.sign.detached(Buffer.from(message), keypair.secretKey);
|
|
295
|
+
|
|
296
|
+
if (clearAfter) {
|
|
297
|
+
// Attempt to clear sensitive data
|
|
298
|
+
const secret = Buffer.from(keypair.secretKey);
|
|
299
|
+
SecureKeyStorage['secureZero'](secret);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return Buffer.from(signature);
|
|
303
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validators for Sol Trade SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides secure input validation for:
|
|
5
|
+
* - RPC URLs
|
|
6
|
+
* - Program IDs
|
|
7
|
+
* - Amounts
|
|
8
|
+
* - Slippage values
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PublicKey } from '@solana/web3.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when input validation fails
|
|
15
|
+
*/
|
|
16
|
+
export class ValidationError extends Error {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'ValidationError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Known legitimate Solana program IDs
|
|
25
|
+
*/
|
|
26
|
+
export const KNOWN_PROGRAM_IDS: Record<string, string[]> = {
|
|
27
|
+
// PumpFun
|
|
28
|
+
pumpfun: ['6EF8rrecthR5Dkzon8Nwu78hRvfCKopJFfWcCzNfXt3D'],
|
|
29
|
+
// PumpSwap
|
|
30
|
+
pumpswap: ['pAMMBay6oceH9fJKBRdGP4LmVn7LKwEqT7dPWn1oLKs'],
|
|
31
|
+
// Raydium
|
|
32
|
+
raydium: [
|
|
33
|
+
'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK', // CPMM
|
|
34
|
+
'675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8', // AMM V4
|
|
35
|
+
],
|
|
36
|
+
// Meteora
|
|
37
|
+
meteora: ['MERLuDFBMmsHnsBPZw2sDQZHvXFM4sPkHePSuUZnPdK'], // DAMM V2
|
|
38
|
+
// Bonk
|
|
39
|
+
bonk: ['bLGPY3zYMBUfok1bMna4jrHGG3QdhSCuLZxUx2fMMLo'],
|
|
40
|
+
// System programs
|
|
41
|
+
system: [
|
|
42
|
+
'11111111111111111111111111111111', // System Program
|
|
43
|
+
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program
|
|
44
|
+
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', // Token-2022
|
|
45
|
+
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', // Associated Token
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Base58 character set
|
|
50
|
+
const BASE58_CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
51
|
+
const BASE58_REGEX = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
|
|
52
|
+
|
|
53
|
+
// Private IP patterns
|
|
54
|
+
const PRIVATE_IP_PATTERNS = [
|
|
55
|
+
/^127\./,
|
|
56
|
+
/^10\./,
|
|
57
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
58
|
+
/^192\.168\./,
|
|
59
|
+
/^0\./,
|
|
60
|
+
/^localhost$/i,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate RPC URL format and security.
|
|
65
|
+
*/
|
|
66
|
+
export function validateRpcUrl(url: string, allowHttp: boolean = false): string {
|
|
67
|
+
if (!url) {
|
|
68
|
+
throw new ValidationError('RPC URL cannot be empty');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let parsed: URL;
|
|
72
|
+
try {
|
|
73
|
+
parsed = new URL(url);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new ValidationError(`Invalid URL format: ${url}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check scheme
|
|
79
|
+
if (!['https:', 'http:'].includes(parsed.protocol)) {
|
|
80
|
+
throw new ValidationError(`Invalid URL scheme: ${parsed.protocol}. Must be http or https`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parsed.protocol === 'http:' && !allowHttp) {
|
|
84
|
+
throw new ValidationError(
|
|
85
|
+
'HTTP RPC URLs are insecure. Use HTTPS or set allowHttp=true if you understand the risks'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check hostname
|
|
90
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
91
|
+
|
|
92
|
+
// Block localhost and private IPs in production
|
|
93
|
+
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
94
|
+
if (pattern.test(hostname)) {
|
|
95
|
+
throw new ValidationError(
|
|
96
|
+
`Private IP/localhost RPC URLs are not allowed for security: ${hostname}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check port
|
|
102
|
+
if (parsed.port) {
|
|
103
|
+
const port = parseInt(parsed.port, 10);
|
|
104
|
+
if (port < 1 || port > 65535) {
|
|
105
|
+
throw new ValidationError(`Invalid port number: ${port}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Reconstruct clean URL
|
|
110
|
+
let cleanUrl = `${parsed.protocol}//${parsed.hostname}`;
|
|
111
|
+
if (parsed.port) {
|
|
112
|
+
cleanUrl += `:${parsed.port}`;
|
|
113
|
+
}
|
|
114
|
+
if (parsed.pathname && parsed.pathname !== '/') {
|
|
115
|
+
cleanUrl += parsed.pathname;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return cleanUrl;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a Solana program ID.
|
|
123
|
+
*/
|
|
124
|
+
export function validateProgramId(programId: string, expectedProgram?: string): string {
|
|
125
|
+
if (!programId) {
|
|
126
|
+
throw new ValidationError('Program ID cannot be empty');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check base58 format
|
|
130
|
+
if (!BASE58_REGEX.test(programId)) {
|
|
131
|
+
throw new ValidationError(`Invalid base58 characters in program ID: ${programId}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check length
|
|
135
|
+
if (programId.length < 32 || programId.length > 48) {
|
|
136
|
+
throw new ValidationError(`Invalid program ID length: ${programId.length} (expected 32-48)`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Verify against known program IDs if expected
|
|
140
|
+
if (expectedProgram) {
|
|
141
|
+
const expectedIds = KNOWN_PROGRAM_IDS[expectedProgram.toLowerCase()];
|
|
142
|
+
if (expectedIds && !expectedIds.includes(programId)) {
|
|
143
|
+
throw new ValidationError(
|
|
144
|
+
`Program ID ${programId} does not match known ${expectedProgram} program IDs. ` +
|
|
145
|
+
`Expected one of: ${expectedIds.slice(0, 3).join(', ')}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return programId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate an amount value.
|
|
155
|
+
*/
|
|
156
|
+
export function validateAmount(
|
|
157
|
+
amount: number | bigint,
|
|
158
|
+
name: string = 'amount',
|
|
159
|
+
allowZero: boolean = false
|
|
160
|
+
): bigint {
|
|
161
|
+
const bigAmount = typeof amount === 'bigint' ? amount : BigInt(amount);
|
|
162
|
+
|
|
163
|
+
if (bigAmount < BigInt(0)) {
|
|
164
|
+
throw new ValidationError(`${name} cannot be negative: ${amount}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (bigAmount === BigInt(0) && !allowZero) {
|
|
168
|
+
throw new ValidationError(`${name} cannot be zero`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for reasonable upper bound
|
|
172
|
+
const maxSafe = BigInt('9223372036854775807'); // Max i64
|
|
173
|
+
if (bigAmount > maxSafe) {
|
|
174
|
+
throw new ValidationError(`${name} exceeds maximum safe value: ${amount} > ${maxSafe}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return bigAmount;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate slippage in basis points.
|
|
182
|
+
*/
|
|
183
|
+
export function validateSlippage(slippageBasisPoints: number): number {
|
|
184
|
+
if (!Number.isInteger(slippageBasisPoints)) {
|
|
185
|
+
throw new ValidationError(`Slippage must be an integer, got ${typeof slippageBasisPoints}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (slippageBasisPoints < 0) {
|
|
189
|
+
throw new ValidationError(`Slippage cannot be negative: ${slippageBasisPoints}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (slippageBasisPoints > 10000) {
|
|
193
|
+
throw new ValidationError(
|
|
194
|
+
`Slippage cannot exceed 10000 basis points (100%), got ${slippageBasisPoints}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Warn on high slippage
|
|
199
|
+
if (slippageBasisPoints > 1000) {
|
|
200
|
+
console.warn(
|
|
201
|
+
`High slippage detected: ${slippageBasisPoints} bp (${slippageBasisPoints / 100}%). ` +
|
|
202
|
+
'This may result in significant price impact.'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return slippageBasisPoints;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validate a Solana public key.
|
|
211
|
+
*/
|
|
212
|
+
export function validatePubkey(pubkey: string | PublicKey, name: string = 'pubkey'): PublicKey {
|
|
213
|
+
if (!pubkey) {
|
|
214
|
+
throw new ValidationError(`${name} cannot be empty`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const pubkeyStr = typeof pubkey === 'string' ? pubkey : pubkey.toBase58();
|
|
218
|
+
|
|
219
|
+
// Check base58 format
|
|
220
|
+
if (!BASE58_REGEX.test(pubkeyStr)) {
|
|
221
|
+
throw new ValidationError(`Invalid base58 characters in ${name}: ${pubkeyStr}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check length
|
|
225
|
+
if (pubkeyStr.length < 32 || pubkeyStr.length > 48) {
|
|
226
|
+
throw new ValidationError(`Invalid ${name} length: ${pubkeyStr.length} (expected 32-48)`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
return new PublicKey(pubkeyStr);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new ValidationError(`Invalid ${name}: ${pubkeyStr} - ${error}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validate a trading pair.
|
|
238
|
+
*/
|
|
239
|
+
export function validateMintPair(inputMint: string, outputMint: string): void {
|
|
240
|
+
validatePubkey(inputMint, 'input_mint');
|
|
241
|
+
validatePubkey(outputMint, 'output_mint');
|
|
242
|
+
|
|
243
|
+
if (inputMint === outputMint) {
|
|
244
|
+
throw new ValidationError('Input and output mint cannot be the same');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validate transaction size.
|
|
250
|
+
*/
|
|
251
|
+
export function validateTransactionSize(transactionBytes: Buffer | Uint8Array, maxSize: number = 1232): void {
|
|
252
|
+
if (!(transactionBytes instanceof Buffer) && !(transactionBytes instanceof Uint8Array)) {
|
|
253
|
+
throw new ValidationError(`Transaction must be bytes, got ${typeof transactionBytes}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (transactionBytes.length > maxSize) {
|
|
257
|
+
throw new ValidationError(
|
|
258
|
+
`Transaction size ${transactionBytes.length} exceeds maximum ${maxSize} bytes`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate a signature string.
|
|
265
|
+
*/
|
|
266
|
+
export function validateSignature(signature: string): string {
|
|
267
|
+
if (!signature) {
|
|
268
|
+
throw new ValidationError('Signature cannot be empty');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Signatures are 64 bytes = ~88 base58 chars
|
|
272
|
+
if (!BASE58_REGEX.test(signature)) {
|
|
273
|
+
throw new ValidationError(`Invalid base58 characters in signature: ${signature}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (signature.length < 80 || signature.length > 96) {
|
|
277
|
+
throw new ValidationError(`Invalid signature length: ${signature.length}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return signature;
|
|
281
|
+
}
|