@usearete/sdk 0.2.0 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +118 -0
  2. package/dist/index.cjs +7393 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +802 -129
  5. package/dist/index.esm.js +3447 -526
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/ssr/handlers.cjs +794 -0
  8. package/dist/ssr/handlers.cjs.map +1 -0
  9. package/dist/ssr/handlers.esm.js +523 -4
  10. package/dist/ssr/handlers.esm.js.map +1 -1
  11. package/dist/ssr/index.cjs +795 -0
  12. package/dist/ssr/index.cjs.map +1 -0
  13. package/dist/ssr/index.esm.js +523 -4
  14. package/dist/ssr/index.esm.js.map +1 -1
  15. package/dist/ssr/nextjs-app.cjs +859 -0
  16. package/dist/ssr/nextjs-app.cjs.map +1 -0
  17. package/dist/ssr/nextjs-app.esm.js +523 -4
  18. package/dist/ssr/nextjs-app.esm.js.map +1 -1
  19. package/dist/ssr/tanstack-start.cjs +881 -0
  20. package/dist/ssr/tanstack-start.cjs.map +1 -0
  21. package/dist/ssr/tanstack-start.esm.js +523 -4
  22. package/dist/ssr/tanstack-start.esm.js.map +1 -1
  23. package/dist/ssr/vite.cjs +887 -0
  24. package/dist/ssr/vite.cjs.map +1 -0
  25. package/dist/ssr/vite.esm.js +523 -4
  26. package/dist/ssr/vite.esm.js.map +1 -1
  27. package/package.json +10 -8
  28. package/dist/index.js +0 -4448
  29. package/dist/index.js.map +0 -1
  30. package/dist/ssr/handlers.js +0 -294
  31. package/dist/ssr/handlers.js.map +0 -1
  32. package/dist/ssr/index.js +0 -295
  33. package/dist/ssr/index.js.map +0 -1
  34. package/dist/ssr/nextjs-app.js +0 -359
  35. package/dist/ssr/nextjs-app.js.map +0 -1
  36. package/dist/ssr/tanstack-start.js +0 -381
  37. package/dist/ssr/tanstack-start.js.map +0 -1
  38. package/dist/ssr/vite.js +0 -387
  39. package/dist/ssr/vite.js.map +0 -1
package/dist/index.js DELETED
@@ -1,4448 +0,0 @@
1
- 'use strict';
2
-
3
- var pako = require('pako');
4
- var ed25519 = require('@noble/ed25519');
5
-
6
- const DEFAULT_MAX_ENTRIES_PER_VIEW = 10000;
7
- const DEFAULT_CONFIG = {
8
- reconnectIntervals: [1000, 2000, 4000, 8000, 16000],
9
- maxReconnectAttempts: 5,
10
- maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
11
- };
12
- /**
13
- * Determines if the error indicates the client should fetch a new token
14
- */
15
- function shouldRefreshToken(code) {
16
- return [
17
- 'TOKEN_EXPIRED',
18
- 'TOKEN_INVALID_SIGNATURE',
19
- 'TOKEN_INVALID_FORMAT',
20
- 'TOKEN_INVALID_ISSUER',
21
- 'TOKEN_INVALID_AUDIENCE',
22
- 'TOKEN_KEY_NOT_FOUND',
23
- ].includes(code);
24
- }
25
- class AreteError extends Error {
26
- constructor(message, code, details) {
27
- super(message);
28
- this.code = code;
29
- this.details = details;
30
- this.name = 'AreteError';
31
- }
32
- }
33
- /**
34
- * Parse a kebab-case error code string (from X-Error-Code header) to AuthErrorCode
35
- */
36
- function parseErrorCode(errorCode) {
37
- const codeMap = {
38
- 'token-missing': 'TOKEN_MISSING',
39
- 'token-expired': 'TOKEN_EXPIRED',
40
- 'token-invalid-signature': 'TOKEN_INVALID_SIGNATURE',
41
- 'token-invalid-format': 'TOKEN_INVALID_FORMAT',
42
- 'token-invalid-issuer': 'TOKEN_INVALID_ISSUER',
43
- 'token-invalid-audience': 'TOKEN_INVALID_AUDIENCE',
44
- 'token-missing-claim': 'TOKEN_MISSING_CLAIM',
45
- 'token-key-not-found': 'TOKEN_KEY_NOT_FOUND',
46
- 'origin-mismatch': 'ORIGIN_MISMATCH',
47
- 'origin-required': 'ORIGIN_REQUIRED',
48
- 'origin-not-allowed': 'ORIGIN_NOT_ALLOWED',
49
- 'rate-limit-exceeded': 'RATE_LIMIT_EXCEEDED',
50
- 'websocket-session-rate-limit-exceeded': 'WEBSOCKET_SESSION_RATE_LIMIT_EXCEEDED',
51
- 'connection-limit-exceeded': 'CONNECTION_LIMIT_EXCEEDED',
52
- 'subscription-limit-exceeded': 'SUBSCRIPTION_LIMIT_EXCEEDED',
53
- 'snapshot-limit-exceeded': 'SNAPSHOT_LIMIT_EXCEEDED',
54
- 'egress-limit-exceeded': 'EGRESS_LIMIT_EXCEEDED',
55
- 'invalid-static-token': 'INVALID_STATIC_TOKEN',
56
- 'internal-error': 'INTERNAL_ERROR',
57
- 'auth-required': 'AUTH_REQUIRED',
58
- 'missing-authorization-header': 'MISSING_AUTHORIZATION_HEADER',
59
- 'invalid-authorization-format': 'INVALID_AUTHORIZATION_FORMAT',
60
- 'invalid-api-key': 'INVALID_API_KEY',
61
- 'expired-api-key': 'EXPIRED_API_KEY',
62
- 'user-not-found': 'USER_NOT_FOUND',
63
- 'secret-key-required': 'SECRET_KEY_REQUIRED',
64
- 'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
65
- 'quota-exceeded': 'QUOTA_EXCEEDED',
66
- };
67
- return codeMap[errorCode.toLowerCase()] || 'INTERNAL_ERROR';
68
- }
69
-
70
- const GZIP_MAGIC_0 = 0x1f;
71
- const GZIP_MAGIC_1 = 0x8b;
72
- function isGzipData(data) {
73
- return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
74
- }
75
- function isSnapshotFrame(frame) {
76
- return frame.op === 'snapshot';
77
- }
78
- function isSubscribedFrame(frame) {
79
- return frame.op === 'subscribed';
80
- }
81
- function isEntityFrame(frame) {
82
- return ['create', 'upsert', 'patch', 'delete'].includes(frame.op);
83
- }
84
- function parseFrame(data) {
85
- if (typeof data === 'string') {
86
- return JSON.parse(data);
87
- }
88
- const bytes = new Uint8Array(data);
89
- if (isGzipData(bytes)) {
90
- const decompressed = pako.inflate(bytes);
91
- const jsonString = new TextDecoder().decode(decompressed);
92
- return JSON.parse(jsonString);
93
- }
94
- const jsonString = new TextDecoder('utf-8').decode(data);
95
- return JSON.parse(jsonString);
96
- }
97
- async function parseFrameFromBlob(blob) {
98
- const arrayBuffer = await blob.arrayBuffer();
99
- return parseFrame(arrayBuffer);
100
- }
101
- function isValidFrame(frame) {
102
- if (typeof frame !== 'object' || frame === null) {
103
- return false;
104
- }
105
- const f = frame;
106
- if (typeof f['entity'] !== 'string' ||
107
- typeof f['op'] !== 'string' ||
108
- typeof f['mode'] !== 'string' ||
109
- !['state', 'append', 'list'].includes(f['mode'])) {
110
- return false;
111
- }
112
- if (f['op'] === 'snapshot') {
113
- return Array.isArray(f['data']);
114
- }
115
- return (typeof f['key'] === 'string' &&
116
- ['create', 'upsert', 'patch', 'delete'].includes(f['op']));
117
- }
118
-
119
- const TOKEN_REFRESH_BUFFER_SECONDS = 60;
120
- const MIN_REFRESH_DELAY_MS = 1000;
121
- const DEFAULT_QUERY_PARAMETER = 'hs_token';
122
- const DEFAULT_HOSTED_TOKEN_ENDPOINT = 'https://api.arete.run/ws/sessions';
123
- const HOSTED_WEBSOCKET_SUFFIX = '.stack.arete.run';
124
- function normalizeTokenResult(result) {
125
- if (typeof result === 'string') {
126
- return { token: result };
127
- }
128
- return result;
129
- }
130
- function decodeBase64Url(value) {
131
- const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
132
- const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
133
- if (typeof atob === 'function') {
134
- return atob(padded);
135
- }
136
- const bufferCtor = globalThis.Buffer;
137
- if (bufferCtor) {
138
- return bufferCtor.from(padded, 'base64').toString('utf-8');
139
- }
140
- return undefined;
141
- }
142
- function parseJwtExpiry(token) {
143
- const parts = token.split('.');
144
- if (parts.length !== 3) {
145
- return undefined;
146
- }
147
- const payload = decodeBase64Url(parts[1] ?? '');
148
- if (!payload) {
149
- return undefined;
150
- }
151
- try {
152
- const decoded = JSON.parse(payload);
153
- return typeof decoded.exp === 'number' ? decoded.exp : undefined;
154
- }
155
- catch {
156
- return undefined;
157
- }
158
- }
159
- function normalizeExpiryTimestamp(expiresAt, expires_at) {
160
- return expiresAt ?? expires_at;
161
- }
162
- function isRefreshAuthResponseMessage(value) {
163
- if (typeof value !== 'object' || value === null) {
164
- return false;
165
- }
166
- const candidate = value;
167
- return typeof candidate['success'] === 'boolean'
168
- && !('op' in candidate)
169
- && !('entity' in candidate)
170
- && !('mode' in candidate);
171
- }
172
- function isSocketIssueMessage(value) {
173
- if (typeof value !== 'object' || value === null) {
174
- return false;
175
- }
176
- const candidate = value;
177
- return candidate['type'] === 'error'
178
- && typeof candidate['message'] === 'string'
179
- && typeof candidate['code'] === 'string'
180
- && typeof candidate['retryable'] === 'boolean'
181
- && typeof candidate['fatal'] === 'boolean';
182
- }
183
- function isHostedAreteWebsocketUrl(websocketUrl) {
184
- try {
185
- return new URL(websocketUrl).hostname.toLowerCase().endsWith(HOSTED_WEBSOCKET_SUFFIX);
186
- }
187
- catch {
188
- return false;
189
- }
190
- }
191
- /**
192
- * Historical sentinel accepted for back-compat: connecting with this URL is
193
- * treated the same as passing no WebSocket URL at all (HTTP-only mode).
194
- */
195
- const DISABLED_WEBSOCKET_URL = 'ws://127.0.0.1/__arete_disabled__';
196
- class ConnectionManager {
197
- constructor(config) {
198
- this.ws = null;
199
- this.reconnectAttempts = 0;
200
- this.reconnectTimeout = null;
201
- this.pingInterval = null;
202
- this.tokenRefreshTimeout = null;
203
- this.tokenRefreshInFlight = null;
204
- this.currentState = 'disconnected';
205
- this.subscriptionQueue = [];
206
- this.activeSubscriptions = new Set();
207
- this.frameHandlers = new Set();
208
- this.stateHandlers = new Set();
209
- this.socketIssueHandlers = new Set();
210
- this.reconnectForTokenRefresh = false;
211
- const websocketUrl = config.websocketUrl && config.websocketUrl !== DISABLED_WEBSOCKET_URL
212
- ? config.websocketUrl
213
- : null;
214
- this.websocketUrl = websocketUrl;
215
- this.hostedAreteUrl = websocketUrl !== null && isHostedAreteWebsocketUrl(websocketUrl);
216
- this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
217
- this.maxReconnectAttempts =
218
- config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
219
- this.authConfig = config.auth;
220
- if (config.initialSubscriptions) {
221
- this.subscriptionQueue.push(...config.initialSubscriptions);
222
- }
223
- }
224
- getTokenEndpoint() {
225
- if (this.authConfig?.tokenEndpoint) {
226
- return this.authConfig.tokenEndpoint;
227
- }
228
- // Require publishableKey for hosted token endpoint
229
- if (this.hostedAreteUrl && this.authConfig?.publishableKey) {
230
- return DEFAULT_HOSTED_TOKEN_ENDPOINT;
231
- }
232
- return undefined;
233
- }
234
- getAuthStrategy() {
235
- if (this.authConfig?.token) {
236
- return { kind: 'static-token', token: this.authConfig.token };
237
- }
238
- if (this.authConfig?.getToken) {
239
- return { kind: 'token-provider', getToken: this.authConfig.getToken };
240
- }
241
- const tokenEndpoint = this.getTokenEndpoint();
242
- if (tokenEndpoint) {
243
- return { kind: 'token-endpoint', endpoint: tokenEndpoint };
244
- }
245
- return { kind: 'none' };
246
- }
247
- hasRefreshableAuth() {
248
- const strategy = this.getAuthStrategy();
249
- return strategy.kind === 'token-provider' || strategy.kind === 'token-endpoint';
250
- }
251
- updateTokenState(result) {
252
- const normalized = normalizeTokenResult(result);
253
- if (!normalized.token) {
254
- throw new AreteError('Authentication provider returned an empty token', 'TOKEN_INVALID');
255
- }
256
- this.currentToken = normalized.token;
257
- this.tokenExpiry = normalizeExpiryTimestamp(normalized.expiresAt, normalized.expires_at)
258
- ?? parseJwtExpiry(normalized.token);
259
- if (this.isTokenExpired()) {
260
- throw new AreteError('Authentication token is expired', 'TOKEN_EXPIRED');
261
- }
262
- return normalized.token;
263
- }
264
- clearTokenState() {
265
- this.currentToken = undefined;
266
- this.tokenExpiry = undefined;
267
- }
268
- async getOrRefreshToken(forceRefresh = false) {
269
- if (!forceRefresh && this.currentToken && !this.isTokenExpired()) {
270
- return this.currentToken;
271
- }
272
- const strategy = this.getAuthStrategy();
273
- // For hosted Arete URLs, auth is required - fail early with clear message
274
- if (strategy.kind === 'none' && this.hostedAreteUrl) {
275
- throw new AreteError('Arete authentication required. Please provide auth.publishableKey to AreteProvider. ' +
276
- 'Get your key from https://arete.run/dashboard', 'AUTH_REQUIRED');
277
- }
278
- switch (strategy.kind) {
279
- case 'static-token':
280
- return this.updateTokenState(strategy.token);
281
- case 'token-provider':
282
- try {
283
- return this.updateTokenState(await strategy.getToken());
284
- }
285
- catch (error) {
286
- if (error instanceof AreteError) {
287
- throw error;
288
- }
289
- throw new AreteError('Failed to get authentication token', 'AUTH_REQUIRED', error);
290
- }
291
- case 'token-endpoint':
292
- try {
293
- return this.updateTokenState(await this.fetchTokenFromEndpoint(strategy.endpoint));
294
- }
295
- catch (error) {
296
- if (error instanceof AreteError) {
297
- throw error;
298
- }
299
- throw new AreteError('Failed to fetch authentication token from endpoint', 'AUTH_REQUIRED', error);
300
- }
301
- case 'none':
302
- return undefined;
303
- }
304
- }
305
- createTokenEndpointRequestBody() {
306
- return {
307
- websocket_url: this.websocketUrl ?? '',
308
- };
309
- }
310
- async fetchTokenFromEndpoint(tokenEndpoint) {
311
- const response = await fetch(tokenEndpoint, {
312
- method: 'POST',
313
- headers: {
314
- ...(this.authConfig?.publishableKey
315
- ? { Authorization: `Bearer ${this.authConfig.publishableKey}` }
316
- : {}),
317
- ...(this.authConfig?.tokenEndpointHeaders ?? {}),
318
- 'Content-Type': 'application/json',
319
- },
320
- credentials: this.authConfig?.tokenEndpointCredentials,
321
- body: JSON.stringify(this.createTokenEndpointRequestBody()),
322
- });
323
- if (!response.ok) {
324
- const rawError = await response.text();
325
- let parsedError;
326
- if (rawError) {
327
- try {
328
- parsedError = JSON.parse(rawError);
329
- }
330
- catch {
331
- parsedError = undefined;
332
- }
333
- }
334
- const wireErrorCode = response.headers.get('X-Error-Code')
335
- ?? (typeof parsedError?.code === 'string' ? parsedError.code : null);
336
- const errorCode = wireErrorCode
337
- ? parseErrorCode(wireErrorCode)
338
- : response.status === 429
339
- ? 'QUOTA_EXCEEDED'
340
- : 'AUTH_REQUIRED';
341
- const errorMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
342
- ? parsedError.error
343
- : rawError || response.statusText || 'Authentication request failed';
344
- throw new AreteError(`Token endpoint returned ${response.status}: ${errorMessage}`, errorCode, {
345
- status: response.status,
346
- wireErrorCode,
347
- responseBody: rawError || null,
348
- });
349
- }
350
- const data = (await response.json());
351
- if (!data.token) {
352
- throw new AreteError('Token endpoint did not return a token', 'TOKEN_INVALID');
353
- }
354
- return data;
355
- }
356
- isTokenExpired() {
357
- if (!this.tokenExpiry) {
358
- return false;
359
- }
360
- return Date.now() >= (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000;
361
- }
362
- scheduleTokenRefresh() {
363
- this.clearTokenRefreshTimeout();
364
- if (!this.hasRefreshableAuth() || !this.tokenExpiry) {
365
- return;
366
- }
367
- const refreshAtMs = Math.max(Date.now() + MIN_REFRESH_DELAY_MS, (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000);
368
- const delayMs = Math.max(MIN_REFRESH_DELAY_MS, refreshAtMs - Date.now());
369
- this.tokenRefreshTimeout = setTimeout(() => {
370
- void this.refreshTokenInBackground();
371
- }, delayMs);
372
- }
373
- clearTokenRefreshTimeout() {
374
- if (this.tokenRefreshTimeout) {
375
- clearTimeout(this.tokenRefreshTimeout);
376
- this.tokenRefreshTimeout = null;
377
- }
378
- }
379
- async refreshTokenInBackground() {
380
- if (!this.hasRefreshableAuth()) {
381
- return;
382
- }
383
- if (this.tokenRefreshInFlight) {
384
- return this.tokenRefreshInFlight;
385
- }
386
- this.tokenRefreshInFlight = (async () => {
387
- const previousToken = this.currentToken;
388
- try {
389
- await this.getOrRefreshToken(true);
390
- if (previousToken &&
391
- this.currentToken &&
392
- this.currentToken !== previousToken &&
393
- this.ws?.readyState === WebSocket.OPEN) {
394
- // Try in-band auth refresh first
395
- const refreshed = await this.sendInBandAuthRefresh(this.currentToken);
396
- if (!refreshed) {
397
- // Fall back to reconnecting if in-band refresh failed
398
- this.rotateConnectionForTokenRefresh();
399
- }
400
- }
401
- this.scheduleTokenRefresh();
402
- }
403
- catch {
404
- this.scheduleTokenRefresh();
405
- }
406
- finally {
407
- this.tokenRefreshInFlight = null;
408
- }
409
- })();
410
- return this.tokenRefreshInFlight;
411
- }
412
- async sendInBandAuthRefresh(token) {
413
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
414
- return false;
415
- }
416
- try {
417
- const message = JSON.stringify({
418
- type: 'refresh_auth',
419
- token: token,
420
- });
421
- this.ws.send(message);
422
- return true;
423
- }
424
- catch (error) {
425
- console.warn('Failed to send in-band auth refresh:', error);
426
- return false;
427
- }
428
- }
429
- handleRefreshAuthResponse(message) {
430
- if (message.success) {
431
- const expiresAt = normalizeExpiryTimestamp(message.expiresAt, message.expires_at);
432
- if (typeof expiresAt === 'number') {
433
- this.tokenExpiry = expiresAt;
434
- }
435
- this.scheduleTokenRefresh();
436
- return true;
437
- }
438
- const errorCode = message.error ? parseErrorCode(message.error) : 'INTERNAL_ERROR';
439
- if (shouldRefreshToken(errorCode)) {
440
- this.clearTokenState();
441
- }
442
- this.rotateConnectionForTokenRefresh();
443
- return true;
444
- }
445
- handleSocketIssueMessage(message) {
446
- this.notifySocketIssue(message);
447
- if (message.fatal) {
448
- this.updateState('error', message.message);
449
- }
450
- return true;
451
- }
452
- rotateConnectionForTokenRefresh() {
453
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.reconnectForTokenRefresh) {
454
- return;
455
- }
456
- this.reconnectForTokenRefresh = true;
457
- this.updateState('reconnecting');
458
- this.ws.close(1000, 'token refresh');
459
- }
460
- buildAuthUrl(token) {
461
- const websocketUrl = this.requireWebsocketUrl();
462
- if (this.authConfig?.tokenTransport === 'bearer') {
463
- return websocketUrl;
464
- }
465
- if (!token) {
466
- return websocketUrl;
467
- }
468
- const separator = websocketUrl.includes('?') ? '&' : '?';
469
- return `${websocketUrl}${separator}${DEFAULT_QUERY_PARAMETER}=${encodeURIComponent(token)}`;
470
- }
471
- requireWebsocketUrl() {
472
- if (this.websocketUrl === null) {
473
- throw new AreteError('WebSocket transport is disabled (client was connected with transport: "http"); views and subscriptions are unavailable', 'WEBSOCKET_DISABLED');
474
- }
475
- return this.websocketUrl;
476
- }
477
- createWebSocket(url, token) {
478
- if (this.authConfig?.tokenTransport === 'bearer') {
479
- const init = token
480
- ? { headers: { Authorization: `Bearer ${token}` } }
481
- : undefined;
482
- if (this.authConfig.websocketFactory) {
483
- return this.authConfig.websocketFactory(url, init);
484
- }
485
- throw new AreteError('auth.tokenTransport="bearer" requires auth.websocketFactory in this environment', 'INVALID_CONFIG');
486
- }
487
- if (this.authConfig?.websocketFactory) {
488
- return this.authConfig.websocketFactory(url);
489
- }
490
- return new WebSocket(url);
491
- }
492
- getState() {
493
- return this.currentState;
494
- }
495
- async getHttpAuthToken(forceRefresh = false) {
496
- return this.getOrRefreshToken(forceRefresh);
497
- }
498
- clearHttpAuthToken() {
499
- this.clearTokenState();
500
- }
501
- onFrame(handler) {
502
- this.frameHandlers.add(handler);
503
- return () => {
504
- this.frameHandlers.delete(handler);
505
- };
506
- }
507
- onStateChange(handler) {
508
- this.stateHandlers.add(handler);
509
- return () => {
510
- this.stateHandlers.delete(handler);
511
- };
512
- }
513
- onSocketIssue(handler) {
514
- this.socketIssueHandlers.add(handler);
515
- return () => {
516
- this.socketIssueHandlers.delete(handler);
517
- };
518
- }
519
- notifySocketIssue(message) {
520
- const issue = {
521
- error: message.error,
522
- message: message.message,
523
- code: parseErrorCode(message.code),
524
- retryable: message.retryable,
525
- retryAfter: message.retry_after,
526
- suggestedAction: message.suggested_action,
527
- docsUrl: message.docs_url,
528
- fatal: message.fatal,
529
- };
530
- for (const handler of this.socketIssueHandlers) {
531
- handler(issue);
532
- }
533
- return issue;
534
- }
535
- async connect() {
536
- this.requireWebsocketUrl();
537
- if (this.ws?.readyState === WebSocket.OPEN ||
538
- this.ws?.readyState === WebSocket.CONNECTING ||
539
- this.currentState === 'connecting') {
540
- return;
541
- }
542
- this.updateState('connecting');
543
- let token;
544
- try {
545
- token = await this.getOrRefreshToken();
546
- }
547
- catch (error) {
548
- this.updateState('error', error instanceof Error ? error.message : 'Failed to get token');
549
- throw error;
550
- }
551
- const wsUrl = this.buildAuthUrl(token);
552
- return new Promise((resolve, reject) => {
553
- let settled = false;
554
- const finish = (fn) => {
555
- if (settled) {
556
- return;
557
- }
558
- settled = true;
559
- fn();
560
- };
561
- try {
562
- this.ws = this.createWebSocket(wsUrl, token);
563
- this.ws.onopen = () => {
564
- this.reconnectAttempts = 0;
565
- this.updateState('connected');
566
- this.startPingInterval();
567
- this.scheduleTokenRefresh();
568
- this.resubscribeActive();
569
- this.flushSubscriptionQueue();
570
- finish(() => resolve());
571
- };
572
- this.ws.onmessage = async (event) => {
573
- try {
574
- let frame;
575
- if (event.data instanceof ArrayBuffer) {
576
- frame = parseFrame(event.data);
577
- }
578
- else if (event.data instanceof Blob) {
579
- frame = await parseFrameFromBlob(event.data);
580
- }
581
- else if (typeof event.data === 'string') {
582
- const parsed = JSON.parse(event.data);
583
- if (isRefreshAuthResponseMessage(parsed)) {
584
- this.handleRefreshAuthResponse(parsed);
585
- return;
586
- }
587
- if (isSocketIssueMessage(parsed)) {
588
- this.handleSocketIssueMessage(parsed);
589
- return;
590
- }
591
- frame = parseFrame(JSON.stringify(parsed));
592
- }
593
- else {
594
- throw new AreteError(`Unsupported message type: ${typeof event.data}`, 'PARSE_ERROR');
595
- }
596
- this.notifyFrameHandlers(frame);
597
- }
598
- catch {
599
- this.updateState('error', 'Failed to parse frame from server');
600
- }
601
- };
602
- this.ws.onerror = () => {
603
- const error = new AreteError('WebSocket connection error', 'CONNECTION_ERROR');
604
- const wasConnecting = this.currentState === 'connecting';
605
- this.updateState('error', error.message);
606
- if (wasConnecting) {
607
- finish(() => reject(error));
608
- }
609
- };
610
- this.ws.onclose = (event) => {
611
- this.stopPingInterval();
612
- this.clearTokenRefreshTimeout();
613
- this.ws = null;
614
- if (!settled) {
615
- const detail = event.reason
616
- ? `${event.code}: ${event.reason}`
617
- : `code ${event.code}`;
618
- const errorMessage = `WebSocket closed before open (${detail})`;
619
- this.updateState('error', errorMessage);
620
- finish(() => reject(new AreteError(errorMessage, 'CONNECTION_ERROR')));
621
- return;
622
- }
623
- if (this.reconnectForTokenRefresh) {
624
- this.reconnectForTokenRefresh = false;
625
- void this.connect().catch(() => {
626
- this.handleReconnect();
627
- });
628
- return;
629
- }
630
- // Parse close reason for error codes (e.g., "token-expired: Token has expired")
631
- const closeReason = event.reason || '';
632
- const errorCodeMatch = closeReason.match(/^([\w-]+):/);
633
- const errorCode = errorCodeMatch ? parseErrorCode(errorCodeMatch[1]) : null;
634
- // Check for auth errors that require token refresh
635
- if (event.code === 1008 || errorCode) {
636
- const isAuthError = errorCode
637
- ? shouldRefreshToken(errorCode)
638
- : /expired|invalid|token/i.test(closeReason);
639
- if (isAuthError) {
640
- this.clearTokenState();
641
- // Try to reconnect immediately with a fresh token
642
- void this.connect().catch(() => {
643
- this.handleReconnect();
644
- });
645
- return;
646
- }
647
- // Check for rate limit errors
648
- const isRateLimit = errorCode === 'RATE_LIMIT_EXCEEDED' ||
649
- errorCode === 'CONNECTION_LIMIT_EXCEEDED' ||
650
- /rate.?limit|quota|limit.?exceeded/i.test(closeReason);
651
- if (isRateLimit) {
652
- this.updateState('error', `Rate limit exceeded: ${closeReason}`);
653
- // Don't auto-reconnect on rate limits, let user handle it
654
- return;
655
- }
656
- }
657
- if (this.currentState !== 'disconnected') {
658
- this.handleReconnect();
659
- }
660
- };
661
- }
662
- catch (error) {
663
- const hsError = new AreteError('Failed to create WebSocket connection', 'CONNECTION_ERROR', error);
664
- this.updateState('error', hsError.message);
665
- reject(hsError);
666
- }
667
- });
668
- }
669
- disconnect() {
670
- this.clearReconnectTimeout();
671
- this.stopPingInterval();
672
- this.clearTokenRefreshTimeout();
673
- this.reconnectForTokenRefresh = false;
674
- this.updateState('disconnected');
675
- if (this.ws) {
676
- this.ws.close();
677
- this.ws = null;
678
- }
679
- }
680
- subscribe(subscription) {
681
- this.requireWebsocketUrl();
682
- const subKey = this.makeSubKey(subscription);
683
- if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
684
- if (this.activeSubscriptions.has(subKey)) {
685
- return;
686
- }
687
- const subMsg = { type: 'subscribe', ...subscription };
688
- this.ws.send(JSON.stringify(subMsg));
689
- this.activeSubscriptions.add(subKey);
690
- }
691
- else {
692
- const alreadyQueued = this.subscriptionQueue.some((queuedSubscription) => this.makeSubKey(queuedSubscription) === subKey);
693
- if (!alreadyQueued) {
694
- this.subscriptionQueue.push(subscription);
695
- }
696
- }
697
- }
698
- unsubscribe(view, key) {
699
- const subscription = { view, key };
700
- const subKey = this.makeSubKey(subscription);
701
- if (this.activeSubscriptions.has(subKey)) {
702
- this.activeSubscriptions.delete(subKey);
703
- if (this.ws?.readyState === WebSocket.OPEN) {
704
- const unsubMsg = { type: 'unsubscribe', view, key };
705
- this.ws.send(JSON.stringify(unsubMsg));
706
- }
707
- }
708
- }
709
- isConnected() {
710
- return this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN;
711
- }
712
- makeSubKey(subscription) {
713
- return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
714
- }
715
- flushSubscriptionQueue() {
716
- while (this.subscriptionQueue.length > 0) {
717
- const subscription = this.subscriptionQueue.shift();
718
- if (subscription) {
719
- this.subscribe(subscription);
720
- }
721
- }
722
- }
723
- resubscribeActive() {
724
- for (const subKey of this.activeSubscriptions) {
725
- const [view, key, partition] = subKey.split(':');
726
- const subscription = {
727
- view: view ?? '',
728
- key: key === '*' ? undefined : key,
729
- partition: partition || undefined,
730
- };
731
- if (this.ws?.readyState === WebSocket.OPEN) {
732
- const subMsg = { type: 'subscribe', ...subscription };
733
- this.ws.send(JSON.stringify(subMsg));
734
- }
735
- }
736
- }
737
- updateState(state, error) {
738
- this.currentState = state;
739
- for (const handler of this.stateHandlers) {
740
- handler(state, error);
741
- }
742
- }
743
- notifyFrameHandlers(frame) {
744
- for (const handler of this.frameHandlers) {
745
- handler(frame);
746
- }
747
- }
748
- handleReconnect() {
749
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
750
- this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
751
- return;
752
- }
753
- this.updateState('reconnecting');
754
- const attemptIndex = Math.min(this.reconnectAttempts, this.reconnectIntervals.length - 1);
755
- const delay = this.reconnectIntervals[attemptIndex] ?? 1000;
756
- this.reconnectAttempts++;
757
- this.reconnectTimeout = setTimeout(() => {
758
- this.connect().catch(() => {
759
- /* retry handled by onclose */
760
- });
761
- }, delay);
762
- }
763
- clearReconnectTimeout() {
764
- if (this.reconnectTimeout) {
765
- clearTimeout(this.reconnectTimeout);
766
- this.reconnectTimeout = null;
767
- }
768
- }
769
- startPingInterval() {
770
- this.stopPingInterval();
771
- this.pingInterval = setInterval(() => {
772
- if (this.ws?.readyState === WebSocket.OPEN) {
773
- this.ws.send('{"type":"ping"}');
774
- }
775
- }, 15000);
776
- }
777
- stopPingInterval() {
778
- if (this.pingInterval) {
779
- clearInterval(this.pingInterval);
780
- this.pingInterval = null;
781
- }
782
- }
783
- }
784
-
785
- const INTERNAL_SEQ_FIELD = '__seq';
786
- function isObject(item) {
787
- return item !== null && typeof item === 'object' && !Array.isArray(item);
788
- }
789
- function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
790
- if (!isObject(target) || !isObject(source)) {
791
- return source;
792
- }
793
- const result = { ...target };
794
- for (const key in source) {
795
- const sourceValue = source[key];
796
- if (sourceValue === undefined) {
797
- continue;
798
- }
799
- const targetValue = result[key];
800
- const fieldPath = currentPath ? `${currentPath}.${key}` : key;
801
- if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
802
- if (appendPaths.includes(fieldPath)) {
803
- result[key] = [...targetValue, ...sourceValue];
804
- }
805
- else {
806
- result[key] = sourceValue;
807
- }
808
- }
809
- else if (isObject(sourceValue) && isObject(targetValue)) {
810
- result[key] = deepMergeWithAppend(targetValue, sourceValue, appendPaths, fieldPath);
811
- }
812
- else {
813
- result[key] = sourceValue;
814
- }
815
- }
816
- return result;
817
- }
818
- function stripUndefinedProperties(value) {
819
- if (Array.isArray(value)) {
820
- return value.map(item => stripUndefinedProperties(item));
821
- }
822
- if (!isObject(value)) {
823
- return value;
824
- }
825
- const result = {};
826
- for (const [key, nestedValue] of Object.entries(value)) {
827
- if (nestedValue === undefined) {
828
- continue;
829
- }
830
- result[key] = stripUndefinedProperties(nestedValue);
831
- }
832
- return result;
833
- }
834
- function toCamelCaseSegment(value) {
835
- if (value === '_seq') {
836
- return INTERNAL_SEQ_FIELD;
837
- }
838
- const pascal = value
839
- .split(/[_.-]/)
840
- .filter(segment => segment.length > 0)
841
- .map(segment => segment[0].toUpperCase() + segment.slice(1))
842
- .join('');
843
- if (pascal.length === 0) {
844
- return value;
845
- }
846
- return pascal[0].toLowerCase() + pascal.slice(1);
847
- }
848
- class FrameProcessor {
849
- constructor(storage, config = {}) {
850
- this.pendingUpdates = [];
851
- this.flushTimer = null;
852
- this.isProcessing = false;
853
- this.storage = storage;
854
- this.maxEntriesPerView = config.maxEntriesPerView === undefined
855
- ? DEFAULT_MAX_ENTRIES_PER_VIEW
856
- : config.maxEntriesPerView;
857
- this.flushIntervalMs = config.flushIntervalMs ?? 0;
858
- this.schemas = config.schemas;
859
- this.patchSchemas = config.patchSchemas;
860
- }
861
- getSchema(viewPath, patch = false) {
862
- const schemas = patch ? this.patchSchemas : this.schemas;
863
- if (!schemas)
864
- return null;
865
- const entityName = viewPath.split('/')[0];
866
- if (typeof entityName !== 'string' || entityName.length === 0)
867
- return null;
868
- const entityKey = entityName;
869
- return schemas[entityKey] ?? null;
870
- }
871
- normalizeEntity(viewPath, data, patch = false) {
872
- const schema = this.getSchema(viewPath, patch)
873
- ?? (!patch ? null : this.getSchema(viewPath));
874
- if (!schema)
875
- return data;
876
- const result = schema.safeParse(data);
877
- if (!result.success) {
878
- console.warn('[Arete] Frame validation failed:', {
879
- view: viewPath,
880
- error: result.error,
881
- });
882
- return null;
883
- }
884
- return stripUndefinedProperties(result.data);
885
- }
886
- hasSchema(viewPath) {
887
- return this.getSchema(viewPath) !== null;
888
- }
889
- normalizePathSegment(viewPath, segment) {
890
- if (!this.hasSchema(viewPath)) {
891
- return segment;
892
- }
893
- return toCamelCaseSegment(segment);
894
- }
895
- normalizePath(viewPath, path) {
896
- if (!this.hasSchema(viewPath)) {
897
- return path;
898
- }
899
- return path
900
- .split('.')
901
- .filter(segment => segment.length > 0)
902
- .map(segment => this.normalizePathSegment(viewPath, segment))
903
- .join('.');
904
- }
905
- normalizeSortConfig(viewPath, sort) {
906
- if (!this.hasSchema(viewPath)) {
907
- return sort;
908
- }
909
- return {
910
- ...sort,
911
- field: sort.field.map(segment => this.normalizePathSegment(viewPath, segment)),
912
- };
913
- }
914
- normalizeAppendPaths(viewPath, append) {
915
- if (!append || append.length === 0 || !this.hasSchema(viewPath)) {
916
- return append ?? [];
917
- }
918
- return append.map(path => this.normalizePath(viewPath, path));
919
- }
920
- extractSeq(data) {
921
- if (!isObject(data)) {
922
- return undefined;
923
- }
924
- const seq = data._seq;
925
- if (typeof seq === 'string') {
926
- return seq;
927
- }
928
- if (typeof seq === 'number' && Number.isFinite(seq)) {
929
- return String(seq);
930
- }
931
- return undefined;
932
- }
933
- getInternalSeq(data) {
934
- if (!isObject(data)) {
935
- return undefined;
936
- }
937
- const seq = data[INTERNAL_SEQ_FIELD];
938
- return typeof seq === 'string' ? seq : undefined;
939
- }
940
- attachInternalSeq(viewPath, data, seq) {
941
- if (!seq || !this.hasSchema(viewPath) || !isObject(data)) {
942
- return data;
943
- }
944
- Object.defineProperty(data, INTERNAL_SEQ_FIELD, {
945
- value: seq,
946
- enumerable: false,
947
- configurable: true,
948
- writable: true,
949
- });
950
- return data;
951
- }
952
- handleFrame(frame) {
953
- if (this.flushIntervalMs === 0) {
954
- this.processFrame(frame);
955
- return;
956
- }
957
- this.pendingUpdates.push({ frame });
958
- this.scheduleFlush();
959
- }
960
- /**
961
- * Immediately flush all pending updates.
962
- * Useful for ensuring all updates are processed before reading state.
963
- */
964
- flush() {
965
- if (this.flushTimer !== null) {
966
- clearTimeout(this.flushTimer);
967
- this.flushTimer = null;
968
- }
969
- this.flushPendingUpdates();
970
- }
971
- /**
972
- * Clean up any pending timers. Call when disposing the processor.
973
- */
974
- dispose() {
975
- if (this.flushTimer !== null) {
976
- clearTimeout(this.flushTimer);
977
- this.flushTimer = null;
978
- }
979
- this.pendingUpdates = [];
980
- }
981
- scheduleFlush() {
982
- if (this.flushTimer !== null) {
983
- return;
984
- }
985
- this.flushTimer = setTimeout(() => {
986
- this.flushTimer = null;
987
- this.flushPendingUpdates();
988
- }, this.flushIntervalMs);
989
- }
990
- flushPendingUpdates() {
991
- if (this.isProcessing || this.pendingUpdates.length === 0) {
992
- return;
993
- }
994
- this.isProcessing = true;
995
- const batch = this.pendingUpdates;
996
- this.pendingUpdates = [];
997
- const viewsToEnforce = new Set();
998
- for (const { frame } of batch) {
999
- const viewPath = this.processFrameWithoutEnforce(frame);
1000
- if (viewPath) {
1001
- viewsToEnforce.add(viewPath);
1002
- }
1003
- }
1004
- viewsToEnforce.forEach((viewPath) => {
1005
- this.enforceMaxEntries(viewPath);
1006
- });
1007
- this.isProcessing = false;
1008
- }
1009
- processFrame(frame) {
1010
- if (isSubscribedFrame(frame)) {
1011
- this.handleSubscribedFrame(frame);
1012
- }
1013
- else if (isSnapshotFrame(frame)) {
1014
- this.handleSnapshotFrame(frame);
1015
- }
1016
- else {
1017
- this.handleEntityFrame(frame);
1018
- }
1019
- }
1020
- processFrameWithoutEnforce(frame) {
1021
- if (isSubscribedFrame(frame)) {
1022
- this.handleSubscribedFrame(frame);
1023
- return null;
1024
- }
1025
- else if (isSnapshotFrame(frame)) {
1026
- this.handleSnapshotFrameWithoutEnforce(frame);
1027
- return frame.entity;
1028
- }
1029
- else {
1030
- this.handleEntityFrameWithoutEnforce(frame);
1031
- return frame.entity;
1032
- }
1033
- }
1034
- handleSubscribedFrame(frame) {
1035
- if (this.storage.setViewConfig && frame.sort) {
1036
- this.storage.setViewConfig(frame.view, {
1037
- sort: this.normalizeSortConfig(frame.view, frame.sort),
1038
- });
1039
- }
1040
- }
1041
- handleSnapshotFrame(frame) {
1042
- this.handleSnapshotFrameWithoutEnforce(frame);
1043
- this.enforceMaxEntries(frame.entity);
1044
- }
1045
- handleSnapshotFrameWithoutEnforce(frame) {
1046
- const viewPath = frame.entity;
1047
- for (const entity of frame.data) {
1048
- const normalized = this.normalizeEntity(viewPath, entity.data);
1049
- if (normalized === null) {
1050
- continue;
1051
- }
1052
- const nextValue = this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data));
1053
- const previousValue = this.storage.get(viewPath, entity.key);
1054
- this.storage.set(viewPath, entity.key, nextValue);
1055
- this.storage.notifyUpdate(viewPath, entity.key, {
1056
- type: 'upsert',
1057
- key: entity.key,
1058
- data: nextValue,
1059
- });
1060
- this.emitRichUpdate(viewPath, entity.key, previousValue, nextValue, 'upsert');
1061
- }
1062
- }
1063
- handleEntityFrame(frame) {
1064
- this.handleEntityFrameWithoutEnforce(frame);
1065
- this.enforceMaxEntries(frame.entity);
1066
- }
1067
- handleEntityFrameWithoutEnforce(frame) {
1068
- const viewPath = frame.entity;
1069
- const previousValue = this.storage.get(viewPath, frame.key);
1070
- switch (frame.op) {
1071
- case 'create':
1072
- case 'upsert':
1073
- {
1074
- const normalized = this.normalizeEntity(viewPath, frame.data);
1075
- if (normalized === null) {
1076
- break;
1077
- }
1078
- const nextValue = this.attachInternalSeq(viewPath, normalized, frame.seq ?? this.extractSeq(frame.data));
1079
- this.storage.set(viewPath, frame.key, nextValue);
1080
- this.storage.notifyUpdate(viewPath, frame.key, {
1081
- type: 'upsert',
1082
- key: frame.key,
1083
- data: nextValue,
1084
- });
1085
- this.emitRichUpdate(viewPath, frame.key, previousValue, nextValue, frame.op);
1086
- break;
1087
- }
1088
- case 'patch': {
1089
- const existing = this.storage.get(viewPath, frame.key);
1090
- const normalizedPatch = this.normalizeEntity(viewPath, frame.data, true);
1091
- if (normalizedPatch === null) {
1092
- break;
1093
- }
1094
- const appendPaths = this.normalizeAppendPaths(viewPath, frame.append);
1095
- const merged = existing
1096
- ? deepMergeWithAppend(existing, normalizedPatch, appendPaths)
1097
- : normalizedPatch;
1098
- const nextValue = this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing));
1099
- this.storage.set(viewPath, frame.key, nextValue);
1100
- this.storage.notifyUpdate(viewPath, frame.key, {
1101
- type: 'patch',
1102
- key: frame.key,
1103
- data: normalizedPatch,
1104
- });
1105
- this.emitRichUpdate(viewPath, frame.key, previousValue, nextValue, 'patch', normalizedPatch);
1106
- break;
1107
- }
1108
- case 'delete':
1109
- this.storage.delete(viewPath, frame.key);
1110
- this.storage.notifyUpdate(viewPath, frame.key, {
1111
- type: 'delete',
1112
- key: frame.key,
1113
- });
1114
- if (previousValue !== null) {
1115
- const richUpdate = { type: 'deleted', key: frame.key, lastKnown: previousValue };
1116
- this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
1117
- }
1118
- break;
1119
- }
1120
- }
1121
- emitRichUpdate(viewPath, key, before, after, _op, patch) {
1122
- const richUpdate = before === null
1123
- ? { type: 'created', key, data: after }
1124
- : { type: 'updated', key, before, after, patch };
1125
- this.storage.notifyRichUpdate(viewPath, key, richUpdate);
1126
- }
1127
- enforceMaxEntries(viewPath) {
1128
- if (this.maxEntriesPerView === null)
1129
- return;
1130
- if (!this.storage.evictOldest)
1131
- return;
1132
- while (this.storage.size(viewPath) > this.maxEntriesPerView) {
1133
- this.storage.evictOldest(viewPath);
1134
- }
1135
- }
1136
- }
1137
-
1138
- class ViewData {
1139
- constructor() {
1140
- this.entities = new Map();
1141
- this.accessOrder = [];
1142
- }
1143
- get(key) {
1144
- return this.entities.get(key);
1145
- }
1146
- set(key, value) {
1147
- if (!this.entities.has(key)) {
1148
- this.accessOrder.push(key);
1149
- }
1150
- else {
1151
- this.touch(key);
1152
- }
1153
- this.entities.set(key, value);
1154
- }
1155
- delete(key) {
1156
- const idx = this.accessOrder.indexOf(key);
1157
- if (idx !== -1) {
1158
- this.accessOrder.splice(idx, 1);
1159
- }
1160
- return this.entities.delete(key);
1161
- }
1162
- has(key) {
1163
- return this.entities.has(key);
1164
- }
1165
- values() {
1166
- return this.entities.values();
1167
- }
1168
- keys() {
1169
- return this.entities.keys();
1170
- }
1171
- get size() {
1172
- return this.entities.size;
1173
- }
1174
- touch(key) {
1175
- const idx = this.accessOrder.indexOf(key);
1176
- if (idx !== -1) {
1177
- this.accessOrder.splice(idx, 1);
1178
- this.accessOrder.push(key);
1179
- }
1180
- }
1181
- evictOldest() {
1182
- const oldest = this.accessOrder.shift();
1183
- if (oldest !== undefined) {
1184
- this.entities.delete(oldest);
1185
- }
1186
- return oldest;
1187
- }
1188
- clear() {
1189
- this.entities.clear();
1190
- this.accessOrder = [];
1191
- }
1192
- }
1193
- class MemoryAdapter {
1194
- constructor(_config = {}) {
1195
- this.views = new Map();
1196
- this.updateCallbacks = new Set();
1197
- this.richUpdateCallbacks = new Set();
1198
- }
1199
- get(viewPath, key) {
1200
- const view = this.views.get(viewPath);
1201
- if (!view)
1202
- return null;
1203
- const value = view.get(key);
1204
- return value !== undefined ? value : null;
1205
- }
1206
- getAll(viewPath) {
1207
- const view = this.views.get(viewPath);
1208
- if (!view)
1209
- return [];
1210
- return Array.from(view.values());
1211
- }
1212
- getAllSync(viewPath) {
1213
- const view = this.views.get(viewPath);
1214
- if (!view)
1215
- return undefined;
1216
- return Array.from(view.values());
1217
- }
1218
- getSync(viewPath, key) {
1219
- const view = this.views.get(viewPath);
1220
- if (!view)
1221
- return undefined;
1222
- const value = view.get(key);
1223
- return value !== undefined ? value : null;
1224
- }
1225
- has(viewPath, key) {
1226
- return this.views.get(viewPath)?.has(key) ?? false;
1227
- }
1228
- keys(viewPath) {
1229
- const view = this.views.get(viewPath);
1230
- if (!view)
1231
- return [];
1232
- return Array.from(view.keys());
1233
- }
1234
- size(viewPath) {
1235
- return this.views.get(viewPath)?.size ?? 0;
1236
- }
1237
- set(viewPath, key, data) {
1238
- let view = this.views.get(viewPath);
1239
- if (!view) {
1240
- view = new ViewData();
1241
- this.views.set(viewPath, view);
1242
- }
1243
- view.set(key, data);
1244
- }
1245
- delete(viewPath, key) {
1246
- this.views.get(viewPath)?.delete(key);
1247
- }
1248
- clear(viewPath) {
1249
- if (viewPath) {
1250
- this.views.get(viewPath)?.clear();
1251
- this.views.delete(viewPath);
1252
- }
1253
- else {
1254
- this.views.clear();
1255
- }
1256
- }
1257
- evictOldest(viewPath) {
1258
- return this.views.get(viewPath)?.evictOldest();
1259
- }
1260
- onUpdate(callback) {
1261
- this.updateCallbacks.add(callback);
1262
- return () => this.updateCallbacks.delete(callback);
1263
- }
1264
- onRichUpdate(callback) {
1265
- this.richUpdateCallbacks.add(callback);
1266
- return () => this.richUpdateCallbacks.delete(callback);
1267
- }
1268
- notifyUpdate(viewPath, key, update) {
1269
- for (const callback of this.updateCallbacks) {
1270
- callback(viewPath, key, update);
1271
- }
1272
- }
1273
- notifyRichUpdate(viewPath, key, update) {
1274
- for (const callback of this.richUpdateCallbacks) {
1275
- callback(viewPath, key, update);
1276
- }
1277
- }
1278
- }
1279
-
1280
- function getNestedValue(obj, path) {
1281
- let current = obj;
1282
- for (const segment of path) {
1283
- if (current === null || current === undefined)
1284
- return undefined;
1285
- if (typeof current !== 'object')
1286
- return undefined;
1287
- current = current[segment];
1288
- }
1289
- return current;
1290
- }
1291
- function compareSortValues(a, b) {
1292
- if (a === b)
1293
- return 0;
1294
- if (a === undefined || a === null)
1295
- return -1;
1296
- if (b === undefined || b === null)
1297
- return 1;
1298
- if (typeof a === 'bigint' && typeof b === 'bigint') {
1299
- return a < b ? -1 : 1;
1300
- }
1301
- if (typeof a === 'number' && typeof b === 'number') {
1302
- return a - b;
1303
- }
1304
- if (typeof a === 'bigint' && typeof b === 'number' && Number.isInteger(b)) {
1305
- return a < BigInt(b) ? -1 : 1;
1306
- }
1307
- if (typeof a === 'number' && typeof b === 'bigint' && Number.isInteger(a)) {
1308
- return BigInt(a) < b ? -1 : 1;
1309
- }
1310
- if (typeof a === 'string' && typeof b === 'string') {
1311
- return a.localeCompare(b);
1312
- }
1313
- if (typeof a === 'boolean' && typeof b === 'boolean') {
1314
- return (a ? 1 : 0) - (b ? 1 : 0);
1315
- }
1316
- return String(a).localeCompare(String(b));
1317
- }
1318
- class SortedStorageDecorator {
1319
- constructor(inner) {
1320
- this.sortConfigs = new Map();
1321
- this.sortedKeysMap = new Map();
1322
- this.inner = inner;
1323
- }
1324
- get(viewPath, key) {
1325
- return this.inner.get(viewPath, key);
1326
- }
1327
- getAll(viewPath) {
1328
- const sortedKeys = this.sortedKeysMap.get(viewPath);
1329
- if (sortedKeys && sortedKeys.length > 0) {
1330
- return sortedKeys
1331
- .map(k => this.inner.get(viewPath, k))
1332
- .filter((v) => v !== null);
1333
- }
1334
- return this.inner.getAll(viewPath);
1335
- }
1336
- getAllSync(viewPath) {
1337
- const sortedKeys = this.sortedKeysMap.get(viewPath);
1338
- if (sortedKeys && sortedKeys.length > 0) {
1339
- return sortedKeys
1340
- .map(k => this.inner.getSync(viewPath, k))
1341
- .filter((v) => v !== null && v !== undefined);
1342
- }
1343
- return this.inner.getAllSync(viewPath);
1344
- }
1345
- getSync(viewPath, key) {
1346
- return this.inner.getSync(viewPath, key);
1347
- }
1348
- has(viewPath, key) {
1349
- return this.inner.has(viewPath, key);
1350
- }
1351
- keys(viewPath) {
1352
- const sortedKeys = this.sortedKeysMap.get(viewPath);
1353
- if (sortedKeys)
1354
- return [...sortedKeys];
1355
- return this.inner.keys(viewPath);
1356
- }
1357
- size(viewPath) {
1358
- return this.inner.size(viewPath);
1359
- }
1360
- set(viewPath, key, data) {
1361
- this.inner.set(viewPath, key, data);
1362
- const sortConfig = this.sortConfigs.get(viewPath);
1363
- if (sortConfig) {
1364
- this.updateSortedPosition(viewPath, key, data, sortConfig);
1365
- }
1366
- }
1367
- delete(viewPath, key) {
1368
- const sortedKeys = this.sortedKeysMap.get(viewPath);
1369
- if (sortedKeys) {
1370
- const idx = sortedKeys.indexOf(key);
1371
- if (idx !== -1) {
1372
- sortedKeys.splice(idx, 1);
1373
- }
1374
- }
1375
- this.inner.delete(viewPath, key);
1376
- }
1377
- clear(viewPath) {
1378
- if (viewPath) {
1379
- this.sortedKeysMap.delete(viewPath);
1380
- this.sortConfigs.delete(viewPath);
1381
- }
1382
- else {
1383
- this.sortedKeysMap.clear();
1384
- this.sortConfigs.clear();
1385
- }
1386
- this.inner.clear(viewPath);
1387
- }
1388
- evictOldest(viewPath) {
1389
- const sortedKeys = this.sortedKeysMap.get(viewPath);
1390
- if (sortedKeys && sortedKeys.length > 0) {
1391
- const oldest = sortedKeys.pop();
1392
- this.inner.delete(viewPath, oldest);
1393
- return oldest;
1394
- }
1395
- return this.inner.evictOldest?.(viewPath);
1396
- }
1397
- setViewConfig(viewPath, config) {
1398
- if (config.sort && !this.sortConfigs.has(viewPath)) {
1399
- this.sortConfigs.set(viewPath, config.sort);
1400
- this.rebuildSortedKeys(viewPath, config.sort);
1401
- }
1402
- this.inner.setViewConfig?.(viewPath, config);
1403
- }
1404
- getViewConfig(viewPath) {
1405
- const sortConfig = this.sortConfigs.get(viewPath);
1406
- if (sortConfig)
1407
- return { sort: sortConfig };
1408
- return this.inner.getViewConfig?.(viewPath);
1409
- }
1410
- onUpdate(callback) {
1411
- return this.inner.onUpdate(callback);
1412
- }
1413
- onRichUpdate(callback) {
1414
- return this.inner.onRichUpdate(callback);
1415
- }
1416
- notifyUpdate(viewPath, key, update) {
1417
- this.inner.notifyUpdate(viewPath, key, update);
1418
- }
1419
- notifyRichUpdate(viewPath, key, update) {
1420
- this.inner.notifyRichUpdate(viewPath, key, update);
1421
- }
1422
- updateSortedPosition(viewPath, key, data, sortConfig) {
1423
- let sortedKeys = this.sortedKeysMap.get(viewPath);
1424
- if (!sortedKeys) {
1425
- sortedKeys = [];
1426
- this.sortedKeysMap.set(viewPath, sortedKeys);
1427
- }
1428
- const existingIdx = sortedKeys.indexOf(key);
1429
- if (existingIdx !== -1) {
1430
- sortedKeys.splice(existingIdx, 1);
1431
- }
1432
- const insertIdx = this.binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, key, data);
1433
- sortedKeys.splice(insertIdx, 0, key);
1434
- }
1435
- binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, newKey, newValue) {
1436
- const newSortValue = getNestedValue(newValue, sortConfig.field);
1437
- const isDesc = sortConfig.order === 'desc';
1438
- let low = 0;
1439
- let high = sortedKeys.length;
1440
- while (low < high) {
1441
- const mid = Math.floor((low + high) / 2);
1442
- const midKey = sortedKeys[mid];
1443
- const midEntity = this.inner.get(viewPath, midKey);
1444
- const midValue = getNestedValue(midEntity, sortConfig.field);
1445
- let cmp = compareSortValues(newSortValue, midValue);
1446
- if (isDesc)
1447
- cmp = -cmp;
1448
- if (cmp === 0) {
1449
- cmp = newKey.localeCompare(midKey);
1450
- }
1451
- if (cmp < 0) {
1452
- high = mid;
1453
- }
1454
- else {
1455
- low = mid + 1;
1456
- }
1457
- }
1458
- return low;
1459
- }
1460
- rebuildSortedKeys(viewPath, sortConfig) {
1461
- const allKeys = this.inner.keys(viewPath);
1462
- if (allKeys.length === 0)
1463
- return;
1464
- const isDesc = sortConfig.order === 'desc';
1465
- const entries = allKeys.map(k => [k, this.inner.get(viewPath, k)]);
1466
- entries.sort((a, b) => {
1467
- const aValue = getNestedValue(a[1], sortConfig.field);
1468
- const bValue = getNestedValue(b[1], sortConfig.field);
1469
- let cmp = compareSortValues(aValue, bValue);
1470
- if (isDesc)
1471
- cmp = -cmp;
1472
- if (cmp === 0) {
1473
- cmp = a[0].localeCompare(b[0]);
1474
- }
1475
- return cmp;
1476
- });
1477
- this.sortedKeysMap.set(viewPath, entries.map(([k]) => k));
1478
- }
1479
- }
1480
-
1481
- class SubscriptionRegistry {
1482
- constructor(connection) {
1483
- this.subscriptions = new Map();
1484
- this.connection = connection;
1485
- }
1486
- subscribe(subscription) {
1487
- const subKey = this.makeSubKey(subscription);
1488
- const existing = this.subscriptions.get(subKey);
1489
- if (existing) {
1490
- existing.refCount++;
1491
- }
1492
- else {
1493
- this.subscriptions.set(subKey, {
1494
- subscription,
1495
- refCount: 1,
1496
- });
1497
- this.connection.subscribe(subscription);
1498
- }
1499
- return () => this.unsubscribe(subscription);
1500
- }
1501
- unsubscribe(subscription) {
1502
- const subKey = this.makeSubKey(subscription);
1503
- const existing = this.subscriptions.get(subKey);
1504
- if (existing) {
1505
- existing.refCount--;
1506
- if (existing.refCount <= 0) {
1507
- this.subscriptions.delete(subKey);
1508
- this.connection.unsubscribe(subscription.view, subscription.key);
1509
- }
1510
- }
1511
- }
1512
- getRefCount(subscription) {
1513
- const subKey = this.makeSubKey(subscription);
1514
- return this.subscriptions.get(subKey)?.refCount ?? 0;
1515
- }
1516
- getActiveSubscriptions() {
1517
- return Array.from(this.subscriptions.values()).map((t) => t.subscription);
1518
- }
1519
- clear() {
1520
- for (const { subscription } of this.subscriptions.values()) {
1521
- this.connection.unsubscribe(subscription.view, subscription.key);
1522
- }
1523
- this.subscriptions.clear();
1524
- }
1525
- makeSubKey(subscription) {
1526
- const filters = subscription.filters ? JSON.stringify(subscription.filters) : '{}';
1527
- return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}:${filters}`;
1528
- }
1529
- }
1530
-
1531
- const MAX_QUEUE_SIZE = 1000;
1532
- function createUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
1533
- return {
1534
- [Symbol.asyncIterator]() {
1535
- const queue = [];
1536
- let waitingResolve = null;
1537
- let unsubscribeStorage = null;
1538
- let unsubscribeRegistry = null;
1539
- let done = false;
1540
- const handler = (viewPath, key, update) => {
1541
- if (viewPath !== subscription.view)
1542
- return;
1543
- if (keyFilter !== undefined && key !== keyFilter)
1544
- return;
1545
- const typedUpdate = update;
1546
- if (waitingResolve) {
1547
- const resolve = waitingResolve;
1548
- waitingResolve = null;
1549
- resolve({ value: typedUpdate, done: false });
1550
- }
1551
- else {
1552
- if (queue.length >= MAX_QUEUE_SIZE) {
1553
- queue.shift();
1554
- }
1555
- queue.push({
1556
- update: typedUpdate,
1557
- resolve: () => { },
1558
- });
1559
- }
1560
- };
1561
- const start = () => {
1562
- unsubscribeStorage = storage.onUpdate(handler);
1563
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1564
- };
1565
- const cleanup = () => {
1566
- done = true;
1567
- unsubscribeStorage?.();
1568
- unsubscribeRegistry?.();
1569
- };
1570
- start();
1571
- return {
1572
- async next() {
1573
- if (done) {
1574
- return { value: undefined, done: true };
1575
- }
1576
- const queued = queue.shift();
1577
- if (queued) {
1578
- return { value: queued.update, done: false };
1579
- }
1580
- return new Promise((resolve) => {
1581
- waitingResolve = resolve;
1582
- });
1583
- },
1584
- async return() {
1585
- cleanup();
1586
- return { value: undefined, done: true };
1587
- },
1588
- async throw(error) {
1589
- cleanup();
1590
- throw error;
1591
- },
1592
- };
1593
- },
1594
- };
1595
- }
1596
- function createEntityStream(storage, subscriptionRegistry, subscription, options, keyFilter) {
1597
- const schema = options?.schema;
1598
- return {
1599
- [Symbol.asyncIterator]() {
1600
- const queue = [];
1601
- let waitingResolve = null;
1602
- let unsubscribeStorage = null;
1603
- let unsubscribeRegistry = null;
1604
- let done = false;
1605
- const handler = (viewPath, key, update) => {
1606
- if (viewPath !== subscription.view)
1607
- return;
1608
- if (keyFilter !== undefined && key !== keyFilter)
1609
- return;
1610
- if (update.type === 'deleted')
1611
- return;
1612
- const entity = (update.type === 'created' ? update.data : update.after);
1613
- let output;
1614
- if (schema) {
1615
- const parsed = schema.safeParse(entity);
1616
- if (!parsed.success) {
1617
- return;
1618
- }
1619
- output = parsed.data;
1620
- }
1621
- else {
1622
- output = entity;
1623
- }
1624
- if (waitingResolve) {
1625
- const resolve = waitingResolve;
1626
- waitingResolve = null;
1627
- resolve({ value: output, done: false });
1628
- }
1629
- else {
1630
- if (queue.length >= MAX_QUEUE_SIZE) {
1631
- queue.shift();
1632
- }
1633
- queue.push(output);
1634
- }
1635
- };
1636
- const start = () => {
1637
- unsubscribeStorage = storage.onRichUpdate(handler);
1638
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1639
- };
1640
- const cleanup = () => {
1641
- done = true;
1642
- unsubscribeStorage?.();
1643
- unsubscribeRegistry?.();
1644
- };
1645
- start();
1646
- const iterator = {
1647
- async next() {
1648
- if (done) {
1649
- return { value: undefined, done: true };
1650
- }
1651
- const queued = queue.shift();
1652
- if (queued) {
1653
- return { value: queued, done: false };
1654
- }
1655
- return new Promise((resolve) => {
1656
- waitingResolve = resolve;
1657
- });
1658
- },
1659
- async return() {
1660
- cleanup();
1661
- return { value: undefined, done: true };
1662
- },
1663
- async throw(error) {
1664
- cleanup();
1665
- throw error;
1666
- },
1667
- };
1668
- return iterator;
1669
- },
1670
- };
1671
- }
1672
- function createRichUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
1673
- return {
1674
- [Symbol.asyncIterator]() {
1675
- const queue = [];
1676
- let waitingResolve = null;
1677
- let unsubscribeStorage = null;
1678
- let unsubscribeRegistry = null;
1679
- let done = false;
1680
- const handler = (viewPath, key, update) => {
1681
- if (viewPath !== subscription.view)
1682
- return;
1683
- if (keyFilter !== undefined && key !== keyFilter)
1684
- return;
1685
- const typedUpdate = update;
1686
- if (waitingResolve) {
1687
- const resolve = waitingResolve;
1688
- waitingResolve = null;
1689
- resolve({ value: typedUpdate, done: false });
1690
- }
1691
- else {
1692
- if (queue.length >= MAX_QUEUE_SIZE) {
1693
- queue.shift();
1694
- }
1695
- queue.push({
1696
- update: typedUpdate,
1697
- resolve: () => { },
1698
- });
1699
- }
1700
- };
1701
- const start = () => {
1702
- unsubscribeStorage = storage.onRichUpdate(handler);
1703
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1704
- };
1705
- const cleanup = () => {
1706
- done = true;
1707
- unsubscribeStorage?.();
1708
- unsubscribeRegistry?.();
1709
- };
1710
- start();
1711
- return {
1712
- async next() {
1713
- if (done) {
1714
- return { value: undefined, done: true };
1715
- }
1716
- const queued = queue.shift();
1717
- if (queued) {
1718
- return { value: queued.update, done: false };
1719
- }
1720
- return new Promise((resolve) => {
1721
- waitingResolve = resolve;
1722
- });
1723
- },
1724
- async return() {
1725
- cleanup();
1726
- return { value: undefined, done: true };
1727
- },
1728
- async throw(error) {
1729
- cleanup();
1730
- throw error;
1731
- },
1732
- };
1733
- },
1734
- };
1735
- }
1736
-
1737
- function createTypedStateView(viewDef, storage, subscriptionRegistry) {
1738
- return {
1739
- use(key, options) {
1740
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1741
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, options, key);
1742
- },
1743
- watch(key, options) {
1744
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1745
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
1746
- },
1747
- watchRich(key, options) {
1748
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1749
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
1750
- },
1751
- async get(key) {
1752
- return storage.get(viewDef.view, key);
1753
- },
1754
- getSync(key) {
1755
- return storage.getSync(viewDef.view, key);
1756
- },
1757
- };
1758
- }
1759
- function createTypedListView(viewDef, storage, subscriptionRegistry) {
1760
- return {
1761
- use(options) {
1762
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1763
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions }, options);
1764
- },
1765
- watch(options) {
1766
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1767
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
1768
- },
1769
- watchRich(options) {
1770
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1771
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
1772
- },
1773
- async get() {
1774
- return storage.getAll(viewDef.view);
1775
- },
1776
- getSync() {
1777
- return storage.getAllSync(viewDef.view);
1778
- },
1779
- };
1780
- }
1781
- function createTypedViews(stack, storage, subscriptionRegistry) {
1782
- const views = {};
1783
- for (const [entityName, viewGroup] of Object.entries(stack.views)) {
1784
- const group = viewGroup;
1785
- const typedGroup = {};
1786
- for (const [viewName, viewDef] of Object.entries(group)) {
1787
- if (viewDef.mode === 'state') {
1788
- typedGroup[viewName] = createTypedStateView(viewDef, storage, subscriptionRegistry);
1789
- }
1790
- else if (viewDef.mode === 'list') {
1791
- typedGroup[viewName] = createTypedListView(viewDef, storage, subscriptionRegistry);
1792
- }
1793
- }
1794
- views[entityName] = typedGroup;
1795
- }
1796
- return views;
1797
- }
1798
-
1799
- class ReadRequestError extends Error {
1800
- constructor(input) {
1801
- super(`Read request to '${input.path}' failed (${input.status}): ${input.body}`);
1802
- this.name = 'ReadRequestError';
1803
- this.status = input.status;
1804
- this.path = input.path;
1805
- this.body = input.body;
1806
- this.serverErrorCode = input.serverErrorCode;
1807
- }
1808
- }
1809
- function getServerErrorCode(response, body) {
1810
- const headerCode = response.headers.get('X-Error-Code');
1811
- if (headerCode) {
1812
- return headerCode;
1813
- }
1814
- try {
1815
- const parsed = JSON.parse(body);
1816
- return typeof parsed.code === 'string' ? parsed.code : undefined;
1817
- }
1818
- catch {
1819
- return undefined;
1820
- }
1821
- }
1822
- async function parseReadResponse(response, path) {
1823
- if (!response.ok) {
1824
- const body = await response.text();
1825
- throw new ReadRequestError({
1826
- status: response.status,
1827
- path,
1828
- body,
1829
- serverErrorCode: getServerErrorCode(response, body),
1830
- });
1831
- }
1832
- return response.json();
1833
- }
1834
- function programAccountRead(input) {
1835
- return {
1836
- account: input.account,
1837
- path: input.path,
1838
- schema: input.schema,
1839
- };
1840
- }
1841
- function programQuery(input) {
1842
- return {
1843
- name: input.name,
1844
- path: input.path,
1845
- method: input.method,
1846
- schema: input.schema,
1847
- };
1848
- }
1849
- function stackQuery(input) {
1850
- return {
1851
- name: input.name,
1852
- path: input.path,
1853
- method: input.method,
1854
- schema: input.schema,
1855
- };
1856
- }
1857
-
1858
- function joinUrl(baseUrl, path) {
1859
- return `${baseUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
1860
- }
1861
- function decodeBase64(encoded) {
1862
- if (typeof atob === 'function') {
1863
- const binary = atob(encoded);
1864
- const bytes = new Uint8Array(binary.length);
1865
- for (let i = 0; i < binary.length; i++) {
1866
- bytes[i] = binary.charCodeAt(i);
1867
- }
1868
- return bytes;
1869
- }
1870
- const bufferCtor = globalThis.Buffer;
1871
- if (bufferCtor) {
1872
- return new Uint8Array(bufferCtor.from(encoded, 'base64'));
1873
- }
1874
- throw new Error('No base64 decoder available in this environment');
1875
- }
1876
- function deriveHttpEndpoint(wsUrl) {
1877
- try {
1878
- const parsed = new URL(wsUrl);
1879
- if (parsed.protocol === 'ws:') {
1880
- parsed.protocol = 'http:';
1881
- }
1882
- else if (parsed.protocol === 'wss:') {
1883
- parsed.protocol = 'https:';
1884
- }
1885
- return parsed.toString().replace(/\/$/, '');
1886
- }
1887
- catch {
1888
- return wsUrl.replace(/^wss?:/i, (protocol) => protocol.toLowerCase() === 'wss:' ? 'https:' : 'http:');
1889
- }
1890
- }
1891
- function createChainClient(httpBaseUrl, fetchImpl) {
1892
- return {
1893
- async exists(address) {
1894
- const path = `/chain/exists/${encodeURIComponent(address)}`;
1895
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1896
- const body = await parseReadResponse(response, path);
1897
- return body.exists;
1898
- },
1899
- async lamports(address) {
1900
- const path = `/chain/lamports/${encodeURIComponent(address)}`;
1901
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1902
- const body = await parseReadResponse(response, path);
1903
- return body.lamports;
1904
- },
1905
- async minimumBalanceForRentExemption(space) {
1906
- const path = `/chain/rent-exemption/${encodeURIComponent(String(space))}`;
1907
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1908
- const body = await parseReadResponse(response, path);
1909
- return body.lamports;
1910
- },
1911
- async clock() {
1912
- const path = '/chain/clock';
1913
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1914
- return parseReadResponse(response, path);
1915
- },
1916
- async account(address) {
1917
- const path = `/chain/accounts/${encodeURIComponent(address)}`;
1918
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1919
- const body = await parseReadResponse(response, path);
1920
- if (!body) {
1921
- return null;
1922
- }
1923
- return {
1924
- address: body.address,
1925
- ownerProgram: body.ownerProgram,
1926
- lamports: body.lamports,
1927
- executable: body.executable,
1928
- data: decodeBase64(body.data),
1929
- };
1930
- },
1931
- async mint(address) {
1932
- const path = `/chain/mints/${encodeURIComponent(address)}`;
1933
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1934
- return parseReadResponse(response, path);
1935
- },
1936
- async tokenAccount(address) {
1937
- const path = `/chain/token-accounts/${encodeURIComponent(address)}`;
1938
- const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1939
- return parseReadResponse(response, path);
1940
- },
1941
- async balance(input) {
1942
- const path = '/chain/balances';
1943
- const response = await fetchImpl(joinUrl(httpBaseUrl, path), {
1944
- method: 'POST',
1945
- headers: { 'content-type': 'application/json' },
1946
- body: JSON.stringify(input),
1947
- });
1948
- return parseReadResponse(response, path);
1949
- },
1950
- };
1951
- }
1952
-
1953
- /**
1954
- * PDA (Program Derived Address) derivation utilities.
1955
- *
1956
- * Implements Solana's PDA derivation algorithm without depending on @solana/web3.js.
1957
- */
1958
- // Base58 alphabet (Bitcoin/Solana style)
1959
- const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
1960
- /**
1961
- * Decode base58 string to Uint8Array.
1962
- */
1963
- function decodeBase58(str) {
1964
- if (str.length === 0) {
1965
- return new Uint8Array(0);
1966
- }
1967
- // Big-endian byte accumulator (stored little-endian here, reversed at the
1968
- // end). Must start empty: a leading `[0]` produces a spurious extra byte for
1969
- // all-zero values such as the System Program ("111...1").
1970
- const bytes = [];
1971
- for (const char of str) {
1972
- const value = BASE58_ALPHABET.indexOf(char);
1973
- if (value === -1) {
1974
- throw new Error('Invalid base58 character: ' + char);
1975
- }
1976
- let carry = value;
1977
- for (let i = 0; i < bytes.length; i++) {
1978
- carry += (bytes[i] ?? 0) * 58;
1979
- bytes[i] = carry & 0xff;
1980
- carry >>= 8;
1981
- }
1982
- while (carry > 0) {
1983
- bytes.push(carry & 0xff);
1984
- carry >>= 8;
1985
- }
1986
- }
1987
- // Add leading zeros for each leading '1' in input
1988
- for (const char of str) {
1989
- if (char !== '1')
1990
- break;
1991
- bytes.push(0);
1992
- }
1993
- return new Uint8Array(bytes.reverse());
1994
- }
1995
- /**
1996
- * Encode Uint8Array to base58 string.
1997
- */
1998
- function encodeBase58(bytes) {
1999
- if (bytes.length === 0) {
2000
- return '';
2001
- }
2002
- // Must start empty for the same reason as `decodeBase58`: a leading `[0]`
2003
- // yields an extra '1' character when encoding all-zero inputs.
2004
- const digits = [];
2005
- for (const byte of bytes) {
2006
- let carry = byte;
2007
- for (let i = 0; i < digits.length; i++) {
2008
- carry += (digits[i] ?? 0) << 8;
2009
- digits[i] = carry % 58;
2010
- carry = (carry / 58) | 0;
2011
- }
2012
- while (carry > 0) {
2013
- digits.push(carry % 58);
2014
- carry = (carry / 58) | 0;
2015
- }
2016
- }
2017
- // Add leading zeros for each leading 0 byte in input
2018
- for (const byte of bytes) {
2019
- if (byte !== 0)
2020
- break;
2021
- digits.push(0);
2022
- }
2023
- return digits.reverse().map(d => BASE58_ALPHABET[d]).join('');
2024
- }
2025
- // SHA-256 round constants (first 32 bits of the fractional parts of the cube
2026
- // roots of the first 64 primes).
2027
- const SHA256_K = new Uint32Array([
2028
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
2029
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
2030
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
2031
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
2032
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
2033
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
2034
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
2035
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
2036
- ]);
2037
- /**
2038
- * Dependency-free, synchronous SHA-256.
2039
- *
2040
- * Used so PDA derivation works identically in browsers, Node (both CJS and
2041
- * ESM), and bundlers without relying on `require('crypto')` or async WebCrypto.
2042
- */
2043
- function sha256Pure(data) {
2044
- const h = new Uint32Array([
2045
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
2046
- 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
2047
- ]);
2048
- const bitLen = data.length * 8;
2049
- // Pad: append 0x80, then zeros, until length ≡ 56 (mod 64), then 8-byte length.
2050
- const paddedLen = ((data.length + 8) >> 6) * 64 + 64;
2051
- const msg = new Uint8Array(paddedLen);
2052
- msg.set(data);
2053
- msg[data.length] = 0x80;
2054
- // 64-bit big-endian bit length (high 32 bits assumed 0 for our seed sizes).
2055
- const dv = new DataView(msg.buffer);
2056
- dv.setUint32(paddedLen - 4, bitLen >>> 0, false);
2057
- dv.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000), false);
2058
- const w = new Uint32Array(64);
2059
- for (let offset = 0; offset < paddedLen; offset += 64) {
2060
- for (let i = 0; i < 16; i++) {
2061
- w[i] = dv.getUint32(offset + i * 4, false);
2062
- }
2063
- for (let i = 16; i < 64; i++) {
2064
- const w15 = w[i - 15];
2065
- const w2 = w[i - 2];
2066
- const s0 = ((w15 >>> 7) | (w15 << 25)) ^ ((w15 >>> 18) | (w15 << 14)) ^ (w15 >>> 3);
2067
- const s1 = ((w2 >>> 17) | (w2 << 15)) ^ ((w2 >>> 19) | (w2 << 13)) ^ (w2 >>> 10);
2068
- w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
2069
- }
2070
- let a = h[0], b = h[1], c = h[2], d = h[3];
2071
- let e = h[4], f = h[5], g = h[6], hh = h[7];
2072
- for (let i = 0; i < 64; i++) {
2073
- const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
2074
- const ch = (e & f) ^ (~e & g);
2075
- const t1 = (hh + S1 + ch + SHA256_K[i] + w[i]) >>> 0;
2076
- const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
2077
- const maj = (a & b) ^ (a & c) ^ (b & c);
2078
- const t2 = (S0 + maj) >>> 0;
2079
- hh = g;
2080
- g = f;
2081
- f = e;
2082
- e = (d + t1) >>> 0;
2083
- d = c;
2084
- c = b;
2085
- b = a;
2086
- a = (t1 + t2) >>> 0;
2087
- }
2088
- h[0] = (h[0] + a) >>> 0;
2089
- h[1] = (h[1] + b) >>> 0;
2090
- h[2] = (h[2] + c) >>> 0;
2091
- h[3] = (h[3] + d) >>> 0;
2092
- h[4] = (h[4] + e) >>> 0;
2093
- h[5] = (h[5] + f) >>> 0;
2094
- h[6] = (h[6] + g) >>> 0;
2095
- h[7] = (h[7] + hh) >>> 0;
2096
- }
2097
- const out = new Uint8Array(32);
2098
- const outView = new DataView(out.buffer);
2099
- for (let i = 0; i < 8; i++) {
2100
- outView.setUint32(i * 4, h[i], false);
2101
- }
2102
- return out;
2103
- }
2104
- /**
2105
- * SHA-256 hash function (synchronous).
2106
- */
2107
- function sha256Sync(data) {
2108
- return sha256Pure(data);
2109
- }
2110
- /**
2111
- * SHA-256 hash function (async). Uses WebCrypto when available for speed,
2112
- * otherwise falls back to the pure implementation.
2113
- */
2114
- async function sha256Async(data) {
2115
- if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
2116
- const copy = new Uint8Array(data);
2117
- const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', copy);
2118
- return new Uint8Array(hashBuffer);
2119
- }
2120
- return sha256Pure(data);
2121
- }
2122
- /**
2123
- * Check if a 32-byte value is a valid point on the ed25519 curve.
2124
- *
2125
- * A valid PDA must be OFF the curve (it must NOT correspond to a real
2126
- * ed25519 public key, so that no private key can ever sign for it).
2127
- *
2128
- * We determine on-curve status by attempting to decompress the candidate
2129
- * as a compressed Edwards point. If decompression succeeds the value lies
2130
- * on the curve; if it throws, the value is off-curve and is a valid PDA.
2131
- * This matches the behaviour of `PublicKey.isOnCurve` in @solana/web3.js.
2132
- */
2133
- function isOnCurve(publicKey) {
2134
- if (publicKey.length !== 32) {
2135
- return false;
2136
- }
2137
- try {
2138
- ed25519.Point.fromHex(publicKey);
2139
- return true;
2140
- }
2141
- catch {
2142
- return false;
2143
- }
2144
- }
2145
- /**
2146
- * PDA marker bytes appended to seeds before hashing.
2147
- */
2148
- const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
2149
- /**
2150
- * Build the hash input buffer for PDA derivation.
2151
- */
2152
- function buildPdaBuffer(seeds, programIdBytes, bump) {
2153
- const totalLength = seeds.reduce((sum, s) => sum + s.length, 0)
2154
- + 1 // bump
2155
- + 32 // programId
2156
- + PDA_MARKER.length;
2157
- const buffer = new Uint8Array(totalLength);
2158
- let offset = 0;
2159
- // Copy seeds
2160
- for (const seed of seeds) {
2161
- buffer.set(seed, offset);
2162
- offset += seed.length;
2163
- }
2164
- // Add bump seed
2165
- buffer[offset++] = bump;
2166
- // Add program ID
2167
- buffer.set(programIdBytes, offset);
2168
- offset += 32;
2169
- // Add PDA marker
2170
- buffer.set(PDA_MARKER, offset);
2171
- return buffer;
2172
- }
2173
- /**
2174
- * Validate seeds before PDA derivation.
2175
- */
2176
- function validateSeeds(seeds) {
2177
- if (seeds.length > 16) {
2178
- throw new Error('Maximum of 16 seeds allowed');
2179
- }
2180
- for (let i = 0; i < seeds.length; i++) {
2181
- const seed = seeds[i];
2182
- if (seed && seed.length > 32) {
2183
- throw new Error('Seed ' + i + ' exceeds maximum length of 32 bytes');
2184
- }
2185
- }
2186
- }
2187
- /**
2188
- * Derives a Program-Derived Address (PDA) from seeds and program ID.
2189
- *
2190
- * Algorithm:
2191
- * 1. For bump = 255 down to 0:
2192
- * a. Concatenate: seeds + [bump] + programId + "ProgramDerivedAddress"
2193
- * b. SHA-256 hash the concatenation
2194
- * c. If result is off the ed25519 curve, return it
2195
- * 2. If no valid PDA found after 256 attempts, throw error
2196
- *
2197
- * @param seeds - Array of seed buffers (max 32 bytes each, max 16 seeds)
2198
- * @param programId - The program ID (base58 string)
2199
- * @returns Tuple of [derivedAddress (base58), bumpSeed]
2200
- */
2201
- async function findProgramAddress(seeds, programId) {
2202
- validateSeeds(seeds);
2203
- const programIdBytes = decodeBase58(programId);
2204
- if (programIdBytes.length !== 32) {
2205
- throw new Error('Program ID must be 32 bytes');
2206
- }
2207
- // Try bump seeds from 255 down to 0
2208
- for (let bump = 255; bump >= 0; bump--) {
2209
- const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
2210
- const hash = await sha256Async(buffer);
2211
- if (!isOnCurve(hash)) {
2212
- return [encodeBase58(hash), bump];
2213
- }
2214
- }
2215
- throw new Error('Unable to find a valid PDA');
2216
- }
2217
- /**
2218
- * Synchronous version of findProgramAddress.
2219
- * Uses synchronous SHA-256 (Node.js crypto module).
2220
- */
2221
- function findProgramAddressSync(seeds, programId) {
2222
- validateSeeds(seeds);
2223
- const programIdBytes = decodeBase58(programId);
2224
- if (programIdBytes.length !== 32) {
2225
- throw new Error('Program ID must be 32 bytes');
2226
- }
2227
- // Try bump seeds from 255 down to 0
2228
- for (let bump = 255; bump >= 0; bump--) {
2229
- const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
2230
- const hash = sha256Sync(buffer);
2231
- if (!isOnCurve(hash)) {
2232
- return [encodeBase58(hash), bump];
2233
- }
2234
- }
2235
- throw new Error('Unable to find a valid PDA');
2236
- }
2237
- /**
2238
- * Creates a seed buffer from various input types.
2239
- *
2240
- * @param value - The value to convert to a seed
2241
- * @returns Uint8Array suitable for PDA derivation
2242
- */
2243
- function createSeed(value) {
2244
- if (value instanceof Uint8Array) {
2245
- return value;
2246
- }
2247
- if (typeof value === 'string') {
2248
- return new TextEncoder().encode(value);
2249
- }
2250
- if (typeof value === 'bigint') {
2251
- // Convert bigint to 8-byte buffer (u64 little-endian)
2252
- const buffer = new Uint8Array(8);
2253
- let n = value;
2254
- for (let i = 0; i < 8; i++) {
2255
- buffer[i] = Number(n & BigInt(0xff));
2256
- n >>= BigInt(8);
2257
- }
2258
- return buffer;
2259
- }
2260
- if (typeof value === 'number') {
2261
- // Assume u64
2262
- return createSeed(BigInt(value));
2263
- }
2264
- throw new Error('Cannot create seed from value');
2265
- }
2266
- /**
2267
- * Creates a public key seed from a base58-encoded address.
2268
- *
2269
- * @param address - Base58-encoded public key
2270
- * @returns 32-byte Uint8Array
2271
- */
2272
- function createPublicKeySeed(address) {
2273
- const decoded = decodeBase58(address);
2274
- if (decoded.length !== 32) {
2275
- throw new Error('Invalid public key length: expected 32, got ' + decoded.length);
2276
- }
2277
- return decoded;
2278
- }
2279
-
2280
- /**
2281
- * Typed serialization of PDA seed values.
2282
- *
2283
- * Shared by the standalone PDA DSL (`pda-dsl.ts`) and instruction account
2284
- * resolution (`account-resolver.ts`). When a seed carries a declared type
2285
- * (from the IDL or the `pdas!` registry), encoding is exact: pubkeys are
2286
- * base58-decoded to 32 bytes, integers are little-endian at the declared
2287
- * width. Without a type, legacy heuristics apply for backward compatibility.
2288
- */
2289
- /**
2290
- * Normalizes the type-name variants that IDLs and codegen produce
2291
- * ("Pubkey", "publicKey", "solana_pubkey::Pubkey", "String", ...) to a
2292
- * canonical seed type. Returns undefined for types that cannot be a seed.
2293
- */
2294
- function normalizeSeedType(argType) {
2295
- if (!argType)
2296
- return undefined;
2297
- // Strip any path qualifier (e.g. solana_pubkey::Pubkey).
2298
- const parts = argType.split('::');
2299
- const t = (parts[parts.length - 1] ?? argType).trim();
2300
- if (/^[ui](8|16|32|64|128)$/.test(t)) {
2301
- return t;
2302
- }
2303
- switch (t) {
2304
- case 'pubkey':
2305
- case 'Pubkey':
2306
- case 'publicKey':
2307
- case 'PublicKey':
2308
- return 'pubkey';
2309
- case 'string':
2310
- case 'String':
2311
- case 'str':
2312
- return 'string';
2313
- default:
2314
- return undefined;
2315
- }
2316
- }
2317
- /**
2318
- * Serializes a PDA seed value.
2319
- *
2320
- * With a recognized `argType`, encoding is strict and width-exact; an
2321
- * incompatible value throws rather than deriving a wrong address. Without
2322
- * one, the legacy heuristics apply: raw bytes pass through, 43/44-character
2323
- * strings are tried as base58, other strings are utf-8, numbers are 8-byte
2324
- * little-endian u64.
2325
- */
2326
- function serializeSeedValue(value, argType) {
2327
- if (value instanceof Uint8Array) {
2328
- return value;
2329
- }
2330
- const t = normalizeSeedType(argType);
2331
- if (t === 'pubkey') {
2332
- if (typeof value !== 'string') {
2333
- throw new Error(`Pubkey seed requires a base58 string, got ${typeof value}`);
2334
- }
2335
- const decoded = decodeBase58(value);
2336
- if (decoded.length !== 32) {
2337
- throw new Error(`Pubkey seed '${value}' decoded to ${decoded.length} bytes, expected 32`);
2338
- }
2339
- return decoded;
2340
- }
2341
- if (t === 'string') {
2342
- if (typeof value !== 'string') {
2343
- throw new Error(`String seed requires a string value, got ${typeof value}`);
2344
- }
2345
- return new TextEncoder().encode(value);
2346
- }
2347
- if (t !== undefined) {
2348
- // Numeric type at a declared width.
2349
- if (typeof value !== 'bigint' && typeof value !== 'number') {
2350
- throw new Error(`Numeric seed of type ${t} requires a number/bigint, got ${typeof value}`);
2351
- }
2352
- const bits = parseInt(t.slice(1), 10);
2353
- return serializeNumber(value, bits / 8, t.startsWith('i'));
2354
- }
2355
- // --- Untyped: legacy heuristics. ---
2356
- if (typeof value === 'string') {
2357
- if (value.length === 43 || value.length === 44) {
2358
- try {
2359
- return decodeBase58(value);
2360
- }
2361
- catch {
2362
- return new TextEncoder().encode(value);
2363
- }
2364
- }
2365
- return new TextEncoder().encode(value);
2366
- }
2367
- if (typeof value === 'bigint' || typeof value === 'number') {
2368
- return serializeNumber(value, 8, true);
2369
- }
2370
- throw new Error(`Cannot serialize value for PDA seed: ${typeof value}`);
2371
- }
2372
- /**
2373
- * Little-endian two's-complement encoding at a fixed byte width, with an
2374
- * overflow check so out-of-range values fail instead of silently truncating.
2375
- */
2376
- function serializeNumber(value, size, signed) {
2377
- const buffer = new Uint8Array(size);
2378
- let n = typeof value === 'bigint' ? value : BigInt(value);
2379
- const original = n;
2380
- for (let i = 0; i < size; i++) {
2381
- buffer[i] = Number(n & BigInt(0xff));
2382
- n >>= BigInt(8);
2383
- }
2384
- const fits = signed ? n === BigInt(0) || n === BigInt(-1) : n === BigInt(0);
2385
- if (!fits) {
2386
- throw new Error(`Seed value ${original} does not fit in ${size * 8} bits`);
2387
- }
2388
- return buffer;
2389
- }
2390
-
2391
- function getValueByPath(source, path) {
2392
- if (!source) {
2393
- return undefined;
2394
- }
2395
- if (Object.prototype.hasOwnProperty.call(source, path)) {
2396
- return source[path];
2397
- }
2398
- let current = source;
2399
- for (const segment of path.split('.')) {
2400
- if (current === null || typeof current !== 'object') {
2401
- return undefined;
2402
- }
2403
- current = current[segment];
2404
- }
2405
- return current;
2406
- }
2407
-
2408
- /**
2409
- * Topologically sort accounts so that dependencies (accountRef) are resolved first.
2410
- * Non-PDA accounts come first, then PDAs in dependency order.
2411
- */
2412
- function sortAccountsByDependency(accountMetas) {
2413
- // Separate non-PDA and PDA accounts
2414
- const nonPda = [];
2415
- const pda = [];
2416
- for (const meta of accountMetas) {
2417
- if (meta.category === 'pda') {
2418
- pda.push(meta);
2419
- }
2420
- else {
2421
- nonPda.push(meta);
2422
- }
2423
- }
2424
- // Build dependency graph for PDAs
2425
- const pdaDeps = new Map();
2426
- for (const meta of pda) {
2427
- const deps = new Set();
2428
- if (meta.pdaConfig) {
2429
- for (const seed of meta.pdaConfig.seeds) {
2430
- if (seed.type === 'accountRef') {
2431
- deps.add(seed.accountName);
2432
- }
2433
- }
2434
- }
2435
- pdaDeps.set(meta.name, deps);
2436
- }
2437
- // Topological sort PDAs
2438
- const sortedPda = [];
2439
- const visited = new Set();
2440
- const visiting = new Set();
2441
- function visit(name) {
2442
- if (visited.has(name))
2443
- return;
2444
- if (visiting.has(name)) {
2445
- throw new Error('Circular dependency in PDA accounts: ' + name);
2446
- }
2447
- const meta = pda.find(m => m.name === name);
2448
- if (!meta)
2449
- return; // Not a PDA, skip
2450
- visiting.add(name);
2451
- const deps = pdaDeps.get(name) || new Set();
2452
- for (const dep of deps) {
2453
- // Only visit if dep is also a PDA
2454
- if (pda.some(m => m.name === dep)) {
2455
- visit(dep);
2456
- }
2457
- }
2458
- visiting.delete(name);
2459
- visited.add(name);
2460
- sortedPda.push(meta);
2461
- }
2462
- for (const meta of pda) {
2463
- visit(meta.name);
2464
- }
2465
- return [...nonPda, ...sortedPda];
2466
- }
2467
- /**
2468
- * Resolves instruction accounts by categorizing and deriving addresses.
2469
- *
2470
- * Resolution order:
2471
- * 1. Non-PDA accounts (signer, known, userProvided) are resolved first
2472
- * 2. PDA accounts are resolved in dependency order (accounts they reference come first)
2473
- *
2474
- * @param accountMetas - Account metadata from the instruction definition
2475
- * @param args - Instruction arguments (used for PDA derivation with argRef seeds)
2476
- * @param options - Resolution options including wallet, user-provided accounts, and programId
2477
- * @returns Resolved accounts and any missing required accounts
2478
- */
2479
- function resolveAccounts(accountMetas, args, options) {
2480
- // Sort accounts by dependency
2481
- const sorted = sortAccountsByDependency(accountMetas);
2482
- // Track resolved accounts for PDA accountRef lookups
2483
- const resolvedMap = {};
2484
- const missing = [];
2485
- for (const meta of sorted) {
2486
- const resolvedAccount = resolveSingleAccount(meta, args, options, resolvedMap);
2487
- if (resolvedAccount) {
2488
- resolvedMap[meta.name] = resolvedAccount;
2489
- }
2490
- else if (!meta.isOptional) {
2491
- missing.push(meta.name);
2492
- }
2493
- }
2494
- // Return accounts in original order (as defined in accountMetas).
2495
- //
2496
- // Omitted optional accounts that precede a resolved account cannot simply
2497
- // be dropped — that would shift every later account into the wrong slot.
2498
- // Anchor's convention is to pass the program ID as a placeholder; trailing
2499
- // omitted optionals are dropped as usual.
2500
- const lastResolvedIndex = accountMetas.reduce((last, meta, index) => (resolvedMap[meta.name] ? index : last), -1);
2501
- const orderedAccounts = [];
2502
- for (let i = 0; i < accountMetas.length; i++) {
2503
- const meta = accountMetas[i];
2504
- const resolved = resolvedMap[meta.name];
2505
- if (resolved) {
2506
- orderedAccounts.push(resolved);
2507
- }
2508
- else if (meta.isOptional && i < lastResolvedIndex) {
2509
- if (!options.programId) {
2510
- throw new Error('Omitted optional account "' + meta.name + '" precedes other accounts and needs ' +
2511
- 'the program ID as a placeholder, but no programId was provided in options.');
2512
- }
2513
- orderedAccounts.push({
2514
- name: meta.name,
2515
- address: options.programId,
2516
- isSigner: false,
2517
- isWritable: false,
2518
- });
2519
- }
2520
- }
2521
- return {
2522
- accounts: orderedAccounts,
2523
- missingUserAccounts: missing,
2524
- };
2525
- }
2526
- function resolveSingleAccount(meta, args, options, resolvedMap) {
2527
- switch (meta.category) {
2528
- case 'signer':
2529
- return resolveSignerAccount(meta, options.accounts, options.wallet);
2530
- case 'known':
2531
- return resolveKnownAccount(meta);
2532
- case 'pda':
2533
- return resolvePdaAccount(meta, args, resolvedMap, options.programId, options.resolve);
2534
- case 'userProvided':
2535
- return resolveUserProvidedAccount(meta, options.accounts);
2536
- default:
2537
- return null;
2538
- }
2539
- }
2540
- function resolveSignerAccount(meta, accounts, wallet) {
2541
- const address = accounts?.[meta.name] ?? wallet?.publicKey;
2542
- if (!address) {
2543
- return null;
2544
- }
2545
- return {
2546
- name: meta.name,
2547
- address,
2548
- isSigner: true,
2549
- isWritable: meta.isWritable,
2550
- };
2551
- }
2552
- function resolveKnownAccount(meta) {
2553
- if (!meta.knownAddress) {
2554
- return null;
2555
- }
2556
- return {
2557
- name: meta.name,
2558
- address: meta.knownAddress,
2559
- isSigner: meta.isSigner,
2560
- isWritable: meta.isWritable,
2561
- };
2562
- }
2563
- function resolvePdaAccount(meta, args, resolvedMap, programId, resolve) {
2564
- if (!meta.pdaConfig) {
2565
- return null;
2566
- }
2567
- // Determine which program to derive against
2568
- const pdaProgramId = meta.pdaConfig.programId || programId;
2569
- if (!pdaProgramId) {
2570
- throw new Error('Cannot derive PDA for "' + meta.name + '": no programId specified. ' +
2571
- 'Either set pdaConfig.programId or pass programId in options.');
2572
- }
2573
- // Build seeds array
2574
- const seeds = [];
2575
- for (const seed of meta.pdaConfig.seeds) {
2576
- switch (seed.type) {
2577
- case 'literal':
2578
- seeds.push(createSeed(seed.value));
2579
- break;
2580
- case 'bytes':
2581
- seeds.push(Uint8Array.from(seed.value));
2582
- break;
2583
- case 'argRef': {
2584
- const argValue = getValueByPath(args, seed.argName) ?? getValueByPath(resolve, seed.argName);
2585
- if (argValue === undefined) {
2586
- throw new Error('PDA seed references missing argument: ' + seed.argName +
2587
- ' (for account "' + meta.name + '")');
2588
- }
2589
- seeds.push(serializeSeedValue(argValue, seed.argType));
2590
- break;
2591
- }
2592
- case 'accountRef': {
2593
- const refAccount = resolvedMap[seed.accountName];
2594
- if (!refAccount) {
2595
- throw new Error('PDA seed references unresolved account: ' + seed.accountName +
2596
- ' (for account "' + meta.name + '")');
2597
- }
2598
- // Account addresses are 32 bytes
2599
- seeds.push(decodeBase58(refAccount.address));
2600
- break;
2601
- }
2602
- default:
2603
- throw new Error('Unknown seed type');
2604
- }
2605
- }
2606
- // Derive the PDA
2607
- const [derivedAddress] = findProgramAddressSync(seeds, pdaProgramId);
2608
- return {
2609
- name: meta.name,
2610
- address: derivedAddress,
2611
- isSigner: meta.isSigner,
2612
- isWritable: meta.isWritable,
2613
- };
2614
- }
2615
- function resolveUserProvidedAccount(meta, accounts) {
2616
- const address = accounts?.[meta.name];
2617
- if (!address) {
2618
- return null;
2619
- }
2620
- return {
2621
- name: meta.name,
2622
- address,
2623
- isSigner: meta.isSigner,
2624
- isWritable: meta.isWritable,
2625
- };
2626
- }
2627
- /**
2628
- * Validates that all required accounts are present.
2629
- *
2630
- * @param result - Account resolution result
2631
- * @throws Error if any required accounts are missing
2632
- */
2633
- function validateAccountResolution(result) {
2634
- if (result.missingUserAccounts.length > 0) {
2635
- throw new Error('Missing required accounts: ' + result.missingUserAccounts.join(', '));
2636
- }
2637
- }
2638
-
2639
- /**
2640
- * Borsh-compatible instruction data serializer.
2641
- *
2642
- * This module handles serializing instruction arguments into the binary format
2643
- * expected by Solana programs using Borsh serialization.
2644
- */
2645
- /**
2646
- * Serializes instruction arguments into a Buffer using Borsh encoding.
2647
- *
2648
- * @param discriminator - The 8-byte instruction discriminator
2649
- * @param args - Arguments to serialize
2650
- * @param schema - Schema defining argument types
2651
- * @returns Serialized instruction data
2652
- */
2653
- function serializeInstructionData(discriminator, args, schema) {
2654
- const buffers = [Buffer.from(discriminator)];
2655
- for (const field of schema) {
2656
- const value = args[field.name];
2657
- // Option fields treat undefined as None; everything else must be present
2658
- // (silently encoding zeros for a missing arg corrupts instruction data).
2659
- if (value === undefined && !(typeof field.type === 'object' && 'option' in field.type)) {
2660
- throw new Error(`Missing required argument "${field.name}" (type ${JSON.stringify(field.type)})`);
2661
- }
2662
- const serialized = serializeValue(value, field.type);
2663
- buffers.push(serialized);
2664
- }
2665
- return Buffer.concat(buffers);
2666
- }
2667
- function serializeValue(value, type) {
2668
- if (typeof type === 'string') {
2669
- return serializePrimitive(value, type);
2670
- }
2671
- if ('vec' in type) {
2672
- return serializeVec(value, type.vec);
2673
- }
2674
- if ('option' in type) {
2675
- return serializeOption(value, type.option);
2676
- }
2677
- if ('array' in type) {
2678
- return serializeArray(value, type.array[0], type.array[1]);
2679
- }
2680
- if ('hashMap' in type) {
2681
- return serializeHashMap(value, type.hashMap[0], type.hashMap[1]);
2682
- }
2683
- if ('struct' in type) {
2684
- return serializeStruct(value, type.struct);
2685
- }
2686
- if ('enum' in type) {
2687
- return serializeEnum(value, type.enum);
2688
- }
2689
- throw new Error(`Unknown type: ${JSON.stringify(type)}`);
2690
- }
2691
- function serializeStruct(value, fields) {
2692
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
2693
- throw new Error(`Struct value must be a plain object, got ${typeof value}`);
2694
- }
2695
- const obj = value;
2696
- const buffers = [];
2697
- for (const field of fields) {
2698
- const fieldValue = obj[field.name];
2699
- if (fieldValue === undefined &&
2700
- !(typeof field.type === 'object' && 'option' in field.type)) {
2701
- throw new Error(`Missing required struct field "${field.name}"`);
2702
- }
2703
- buffers.push(serializeValue(fieldValue, field.type));
2704
- }
2705
- return Buffer.concat(buffers);
2706
- }
2707
- function serializeHashMap(value, keyType, valueType) {
2708
- if (keyType !== 'string') {
2709
- throw new Error(`Instruction hashMap keys must use the 'string' schema, got ${JSON.stringify(keyType)}`);
2710
- }
2711
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
2712
- throw new Error(`HashMap value must be a plain object, got ${typeof value}`);
2713
- }
2714
- const entries = Object.entries(value).sort(([left], [right]) =>
2715
- // Rust/Borsh sorts String keys by their UTF-8 bytes before serializing.
2716
- Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')));
2717
- const len = Buffer.alloc(4);
2718
- len.writeUInt32LE(entries.length, 0);
2719
- const entryBuffers = [];
2720
- for (const [key, entryValue] of entries) {
2721
- entryBuffers.push(serializePrimitive(key, 'string'));
2722
- entryBuffers.push(serializeValue(entryValue, valueType));
2723
- }
2724
- return Buffer.concat([len, ...entryBuffers]);
2725
- }
2726
- function variantName(variant) {
2727
- return typeof variant === 'string' ? variant : variant.name;
2728
- }
2729
- function serializeEnum(value, variants) {
2730
- // Numeric value: a bare variant index (fieldless variants only).
2731
- if (typeof value === 'number') {
2732
- if (!Number.isInteger(value) || value < 0 || value >= variants.length) {
2733
- throw new Error(`Enum variant index ${value} out of range (0..${variants.length - 1})`);
2734
- }
2735
- const variant = variants[value];
2736
- if (typeof variant !== 'string') {
2737
- throw new Error(`Enum variant "${variantName(variant)}" carries data; pass { ${variantName(variant)}: ... } instead of an index`);
2738
- }
2739
- return Buffer.from([value]);
2740
- }
2741
- // String value: a fieldless variant by name.
2742
- if (typeof value === 'string') {
2743
- const index = variants.findIndex((v) => variantName(v) === value);
2744
- if (index === -1) {
2745
- throw new Error(`Unknown enum variant "${value}". Expected one of: ${variants.map(variantName).join(', ')}`);
2746
- }
2747
- const variant = variants[index];
2748
- if (typeof variant !== 'string') {
2749
- throw new Error(`Enum variant "${value}" carries data; pass { ${value}: ... } instead of a bare name`);
2750
- }
2751
- return Buffer.from([index]);
2752
- }
2753
- // Object value: { variantName: payload } for data-carrying variants.
2754
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
2755
- const keys = Object.keys(value);
2756
- if (keys.length !== 1) {
2757
- throw new Error(`Enum value must be a single-key object ({ variantName: payload }), got keys [${keys.join(', ')}]`);
2758
- }
2759
- const key = keys[0];
2760
- const payload = value[key];
2761
- const index = variants.findIndex((v) => variantName(v) === key);
2762
- if (index === -1) {
2763
- throw new Error(`Unknown enum variant "${key}". Expected one of: ${variants.map(variantName).join(', ')}`);
2764
- }
2765
- const variant = variants[index];
2766
- if (typeof variant === 'string') {
2767
- throw new Error(`Enum variant "${key}" is fieldless; pass '${key}' instead of an object`);
2768
- }
2769
- const prefix = Buffer.from([index]);
2770
- if ('fields' in variant) {
2771
- return Buffer.concat([prefix, serializeStruct(payload, variant.fields)]);
2772
- }
2773
- const tuple = payload;
2774
- if (!Array.isArray(tuple) || tuple.length !== variant.tuple.length) {
2775
- throw new Error(`Enum variant "${key}" expects a tuple of length ${variant.tuple.length}`);
2776
- }
2777
- const elements = variant.tuple.map((elementType, i) => serializeValue(tuple[i], elementType));
2778
- return Buffer.concat([prefix, ...elements]);
2779
- }
2780
- throw new Error(`Cannot serialize enum from value of type ${typeof value}`);
2781
- }
2782
- function serializePrimitive(value, type) {
2783
- switch (type) {
2784
- case 'u8':
2785
- return Buffer.from([value]);
2786
- case 'u16':
2787
- const u16 = Buffer.alloc(2);
2788
- u16.writeUInt16LE(value, 0);
2789
- return u16;
2790
- case 'u32':
2791
- const u32 = Buffer.alloc(4);
2792
- u32.writeUInt32LE(value, 0);
2793
- return u32;
2794
- case 'u64':
2795
- const u64 = Buffer.alloc(8);
2796
- u64.writeBigUInt64LE(BigInt(value), 0);
2797
- return u64;
2798
- case 'u128':
2799
- // u128 is 16 bytes, little-endian
2800
- const u128 = Buffer.alloc(16);
2801
- const bigU128 = BigInt(value);
2802
- u128.writeBigUInt64LE(bigU128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0);
2803
- u128.writeBigUInt64LE(bigU128 >> BigInt(64), 8);
2804
- return u128;
2805
- case 'i8':
2806
- return Buffer.from([value]);
2807
- case 'i16':
2808
- const i16 = Buffer.alloc(2);
2809
- i16.writeInt16LE(value, 0);
2810
- return i16;
2811
- case 'i32':
2812
- const i32 = Buffer.alloc(4);
2813
- i32.writeInt32LE(value, 0);
2814
- return i32;
2815
- case 'i64':
2816
- const i64 = Buffer.alloc(8);
2817
- i64.writeBigInt64LE(BigInt(value), 0);
2818
- return i64;
2819
- case 'i128':
2820
- const i128 = Buffer.alloc(16);
2821
- const bigI128 = BigInt(value);
2822
- // The masked low limb is always non-negative, so it must be written
2823
- // unsigned; the arithmetic-shifted high limb carries the sign.
2824
- i128.writeBigUInt64LE(bigI128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0);
2825
- i128.writeBigInt64LE(bigI128 >> BigInt(64), 8);
2826
- return i128;
2827
- case 'f32': {
2828
- const f32 = Buffer.alloc(4);
2829
- f32.writeFloatLE(value, 0);
2830
- return f32;
2831
- }
2832
- case 'f64': {
2833
- const f64 = Buffer.alloc(8);
2834
- f64.writeDoubleLE(value, 0);
2835
- return f64;
2836
- }
2837
- case 'bool':
2838
- return Buffer.from([value ? 1 : 0]);
2839
- case 'bytes': {
2840
- // Borsh bytes: u32 LE length prefix + raw bytes.
2841
- const raw = value instanceof Uint8Array
2842
- ? value
2843
- : Array.isArray(value)
2844
- ? Uint8Array.from(value)
2845
- : null;
2846
- if (raw === null) {
2847
- throw new Error(`Cannot serialize bytes from value of type ${typeof value}`);
2848
- }
2849
- const lenPrefix = Buffer.alloc(4);
2850
- lenPrefix.writeUInt32LE(raw.length, 0);
2851
- return Buffer.concat([lenPrefix, Buffer.from(raw)]);
2852
- }
2853
- case 'string':
2854
- const str = value;
2855
- const strBytes = Buffer.from(str, 'utf-8');
2856
- const strLen = Buffer.alloc(4);
2857
- strLen.writeUInt32LE(strBytes.length, 0);
2858
- return Buffer.concat([strLen, strBytes]);
2859
- case 'pubkey': {
2860
- // Public key is 32 bytes. Accept base58 strings or raw 32-byte buffers.
2861
- if (value instanceof Uint8Array) {
2862
- if (value.length !== 32) {
2863
- throw new Error(`Invalid pubkey byte length: expected 32, got ${value.length}`);
2864
- }
2865
- return Buffer.from(value);
2866
- }
2867
- if (typeof value === 'string') {
2868
- const decoded = decodeBase58(value);
2869
- if (decoded.length !== 32) {
2870
- throw new Error(`Invalid pubkey: '${value}' decoded to ${decoded.length} bytes, expected 32`);
2871
- }
2872
- return Buffer.from(decoded);
2873
- }
2874
- throw new Error(`Cannot serialize pubkey from value of type ${typeof value}`);
2875
- }
2876
- default:
2877
- throw new Error(`Unknown primitive type: ${type}`);
2878
- }
2879
- }
2880
- function serializeVec(values, elementType) {
2881
- const len = Buffer.alloc(4);
2882
- len.writeUInt32LE(values.length, 0);
2883
- const elementBuffers = values.map(v => serializeValue(v, elementType));
2884
- return Buffer.concat([len, ...elementBuffers]);
2885
- }
2886
- function serializeOption(value, innerType) {
2887
- if (value === null || value === undefined) {
2888
- return Buffer.from([0]); // None
2889
- }
2890
- const inner = serializeValue(value, innerType);
2891
- return Buffer.concat([Buffer.from([1]), inner]); // Some
2892
- }
2893
- function serializeArray(values, elementType, length) {
2894
- if (values.length !== length) {
2895
- throw new Error(`Array length mismatch: expected ${length}, got ${values.length}`);
2896
- }
2897
- const elementBuffers = values.map(v => serializeValue(v, elementType));
2898
- return Buffer.concat(elementBuffers);
2899
- }
2900
-
2901
- /**
2902
- * Parses and handles instruction errors.
2903
- */
2904
- /**
2905
- * Parses an error returned from a Solana transaction.
2906
- *
2907
- * @param error - The error from the transaction
2908
- * @param errorMetadata - Error definitions from the IDL
2909
- * @returns Parsed program error or null if not a program error
2910
- */
2911
- function parseInstructionError(error, errorMetadata) {
2912
- if (!error) {
2913
- return null;
2914
- }
2915
- const errorCode = extractErrorCode(error);
2916
- if (errorCode === null) {
2917
- return null;
2918
- }
2919
- const metadata = errorMetadata.find(e => e.code === errorCode);
2920
- if (metadata) {
2921
- return {
2922
- code: metadata.code,
2923
- name: metadata.name,
2924
- message: metadata.msg,
2925
- };
2926
- }
2927
- return {
2928
- code: errorCode,
2929
- name: `CustomError${errorCode}`,
2930
- message: `Unknown error with code ${errorCode}`,
2931
- };
2932
- }
2933
- function extractErrorCode(error) {
2934
- if (typeof error !== 'object' || error === null) {
2935
- return null;
2936
- }
2937
- const errorObj = error;
2938
- // Check for InstructionError format
2939
- if (errorObj.InstructionError) {
2940
- const instructionError = errorObj.InstructionError;
2941
- if (instructionError[1]?.Custom !== undefined) {
2942
- return instructionError[1].Custom;
2943
- }
2944
- }
2945
- // Check for direct code
2946
- if (typeof errorObj.code === 'number') {
2947
- return errorObj.code;
2948
- }
2949
- return null;
2950
- }
2951
- /**
2952
- * Formats an error for display.
2953
- *
2954
- * @param error - The program error
2955
- * @returns Human-readable error message
2956
- */
2957
- function formatProgramError(error) {
2958
- return `${error.name} (${error.code}): ${error.message}`;
2959
- }
2960
- /**
2961
- * Error thrown when an instruction fails to send and the underlying failure
2962
- * could be parsed against the handler's IDL error definitions.
2963
- */
2964
- class InstructionError extends Error {
2965
- constructor(message, programError, cause) {
2966
- super(message);
2967
- this.name = 'InstructionError';
2968
- this.programError = programError;
2969
- this.cause = cause;
2970
- }
2971
- }
2972
-
2973
- /**
2974
- * Creates a data-driven instruction handler.
2975
- *
2976
- * The returned handler implements `build()` generically: it serializes args
2977
- * via the schema-driven serializer and constructs the account key list from
2978
- * the resolved, ordered accounts. No imperative per-instruction code is
2979
- * required, which keeps generated SDKs tiny and puts all serialization logic
2980
- * in one tested place.
2981
- */
2982
- function createInstructionHandler(config) {
2983
- const discriminator = config.discriminator instanceof Uint8Array
2984
- ? config.discriminator
2985
- : Uint8Array.from(config.discriminator);
2986
- const argNames = config.args.map((a) => a.name);
2987
- const errors = config.errors ?? [];
2988
- return {
2989
- programId: config.programId,
2990
- accounts: config.accounts,
2991
- errors,
2992
- argNames,
2993
- build(args, resolved) {
2994
- const data = serializeInstructionData(discriminator, args, config.args);
2995
- return {
2996
- programId: config.programId,
2997
- keys: resolved.map((r) => ({
2998
- pubkey: r.address,
2999
- isSigner: r.isSigner,
3000
- isWritable: r.isWritable,
3001
- })),
3002
- data,
3003
- };
3004
- },
3005
- };
3006
- }
3007
- /**
3008
- * Splits a merged params object into serialized args and account overrides.
3009
- *
3010
- * Keys matching a declared argument name are args; keys matching a declared
3011
- * account name (with a string value) are account address overrides. This
3012
- * applies to signer slots too, allowing explicit signer addresses to override
3013
- * the wallet fallback. Anything else throws — a typo'd key silently dropped
3014
- * here would otherwise change the built instruction. `options.accounts` on
3015
- * {@link BuildOptions} remains an unvalidated escape hatch for advanced callers.
3016
- */
3017
- function splitParams(handler, params) {
3018
- const argNameSet = new Set(handler.argNames);
3019
- const accountNameSet = new Set(handler.accounts.map((a) => a.name));
3020
- const args = {};
3021
- const accountOverrides = {};
3022
- let resolve;
3023
- for (const [key, value] of Object.entries(params)) {
3024
- if (argNameSet.has(key)) {
3025
- args[key] = value;
3026
- }
3027
- else if (key === 'resolve' && !accountNameSet.has(key)) {
3028
- if (value === undefined) {
3029
- continue;
3030
- }
3031
- if (value === null || Array.isArray(value) || typeof value !== 'object') {
3032
- throw new Error('Parameter "resolve" must be an object when provided');
3033
- }
3034
- resolve = value;
3035
- }
3036
- else if (accountNameSet.has(key)) {
3037
- if (typeof value !== 'string') {
3038
- // Non-string values are not valid account addresses.
3039
- throw new Error(`Parameter "${key}" is not a known argument and is not a base58 account address`);
3040
- }
3041
- accountOverrides[key] = value;
3042
- }
3043
- else {
3044
- throw new Error(`Unknown parameter "${key}". Expected one of args [${[...argNameSet].join(', ')}] ` +
3045
- `or accounts [${[...accountNameSet].join(', ')}]`);
3046
- }
3047
- }
3048
- return { args, accountOverrides, resolve };
3049
- }
3050
- /**
3051
- * Builds a {@link BuiltInstruction} from a handler and a merged params object.
3052
- *
3053
- * This is a pure function: it performs no network access. It is the unit of
3054
- * composition for batching (`wallet.signAndSend([a, b, c])`).
3055
- */
3056
- function buildInstruction(handler, params, options = {}) {
3057
- const { args, accountOverrides, resolve } = splitParams(handler, params);
3058
- const resolutionOptions = {
3059
- accounts: { ...accountOverrides, ...options.accounts },
3060
- resolve,
3061
- wallet: options.wallet,
3062
- programId: handler.programId,
3063
- };
3064
- const resolution = resolveAccounts(handler.accounts, args, resolutionOptions);
3065
- validateAccountResolution(resolution);
3066
- const instruction = handler.build(args, resolution.accounts);
3067
- if (options.remainingAccounts?.length) {
3068
- instruction.keys.push(...options.remainingAccounts);
3069
- }
3070
- return instruction;
3071
- }
3072
- /**
3073
- * Builds, signs, and sends an instruction via the wallet adapter.
3074
- *
3075
- * The core SDK does not touch RPC: the adapter owns blockhash, compilation,
3076
- * signing, sending, and confirmation. On failure, program errors are parsed
3077
- * against the handler's IDL error definitions and surfaced as an
3078
- * {@link InstructionError}.
3079
- */
3080
- async function executeInstruction(handler, params, options = {}) {
3081
- const instruction = buildInstruction(handler, params, options);
3082
- if (!options.wallet) {
3083
- throw new Error('Wallet required to sign and send transaction');
3084
- }
3085
- const sendOptions = {
3086
- ...options.send,
3087
- };
3088
- if (options.confirmationLevel !== undefined) {
3089
- sendOptions.confirmationLevel = options.confirmationLevel;
3090
- }
3091
- try {
3092
- const result = await options.wallet.signAndSend([instruction], sendOptions);
3093
- return { signature: result.signature, slot: result.slot };
3094
- }
3095
- catch (err) {
3096
- const programError = parseInstructionError(err, handler.errors);
3097
- if (programError) {
3098
- throw new InstructionError(`${programError.name} (${programError.code}): ${programError.message}`, programError, err);
3099
- }
3100
- throw err;
3101
- }
3102
- }
3103
- /**
3104
- * Creates an instruction executor bound to a specific wallet.
3105
- */
3106
- function createInstructionExecutor(wallet) {
3107
- return {
3108
- execute: async (handler, params, options) => {
3109
- return executeInstruction(handler, params, { ...options, wallet });
3110
- },
3111
- build: (handler, params, options) => {
3112
- return buildInstruction(handler, params, { ...options, wallet });
3113
- },
3114
- };
3115
- }
3116
-
3117
- function literal(value) {
3118
- return { type: 'literal', value };
3119
- }
3120
- function account(name) {
3121
- return { type: 'accountRef', accountName: name };
3122
- }
3123
- function arg(name, type) {
3124
- return { type: 'argRef', argName: name, argType: type };
3125
- }
3126
- function bytes(value) {
3127
- return { type: 'bytes', value };
3128
- }
3129
- function resolveSeeds(seeds, context) {
3130
- return seeds.map((seed) => {
3131
- switch (seed.type) {
3132
- case 'literal':
3133
- return new TextEncoder().encode(seed.value);
3134
- case 'bytes':
3135
- return seed.value;
3136
- case 'argRef': {
3137
- const value = getValueByPath(context.args, seed.argName) ?? getValueByPath(context.resolve, seed.argName);
3138
- if (value === undefined) {
3139
- throw new Error(`Missing arg for PDA seed: ${seed.argName}`);
3140
- }
3141
- return serializeSeedValue(value, seed.argType);
3142
- }
3143
- case 'accountRef': {
3144
- const address = context.accounts?.[seed.accountName];
3145
- if (!address) {
3146
- throw new Error(`Missing account for PDA seed: ${seed.accountName}`);
3147
- }
3148
- return decodeBase58(address);
3149
- }
3150
- }
3151
- });
3152
- }
3153
- function pda(programId, ...seeds) {
3154
- return {
3155
- seeds,
3156
- programId,
3157
- program(newProgramId) {
3158
- return pda(newProgramId, ...seeds);
3159
- },
3160
- async derive(context) {
3161
- const resolvedSeeds = resolveSeeds(this.seeds, context);
3162
- const pid = context.programId ?? this.programId;
3163
- const [address] = await findProgramAddress(resolvedSeeds, pid);
3164
- return address;
3165
- },
3166
- deriveSync(context) {
3167
- const resolvedSeeds = resolveSeeds(this.seeds, context);
3168
- const pid = context.programId ?? this.programId;
3169
- const [address] = findProgramAddressSync(resolvedSeeds, pid);
3170
- return address;
3171
- },
3172
- };
3173
- }
3174
- function createProgramPdas(pdas) {
3175
- return pdas;
3176
- }
3177
-
3178
- const STACK_RUNTIME_EXTENSIONS = '__areteStackRuntimeExtensions';
3179
- const PROGRAM_OPERATION_EXTENSIONS = '__areteProgramOperationExtensions';
3180
- function defineProgramExtensions() {
3181
- return (extensions) => extensions;
3182
- }
3183
- function mergeNamespace(base, extension) {
3184
- if (base && typeof base === 'object' && !Array.isArray(base)
3185
- && extension && typeof extension === 'object' && !Array.isArray(extension)) {
3186
- const merged = { ...base };
3187
- for (const [key, value] of Object.entries(extension)) {
3188
- merged[key] = key in merged ? mergeNamespace(merged[key], value) : value;
3189
- }
3190
- return merged;
3191
- }
3192
- return extension;
3193
- }
3194
- function mergeOperations(base, extension) {
3195
- return {
3196
- instructions: mergeNamespace(base?.instructions, extension?.instructions),
3197
- transactions: mergeNamespace(base?.transactions, extension?.transactions),
3198
- flows: mergeNamespace(base?.flows, extension?.flows),
3199
- };
3200
- }
3201
- function extendProgram(program, extensions) {
3202
- const base = program;
3203
- const extended = { ...program };
3204
- for (const key of ['pdas', 'accounts', 'queries', 'addresses', 'constants', 'defaults', 'math']) {
3205
- const extensionValue = extensions[key];
3206
- if (extensionValue !== undefined) {
3207
- extended[key] = mergeNamespace(base[key], extensionValue);
3208
- }
3209
- }
3210
- if (extensions.raw !== undefined) {
3211
- extended.rawInstructions = mergeNamespace(base.rawInstructions, extensions.raw);
3212
- }
3213
- const baseFactory = base[PROGRAM_OPERATION_EXTENSIONS]?.createOperations;
3214
- const extensionFactory = extensions.createOperations;
3215
- if (baseFactory || extensionFactory) {
3216
- Object.defineProperty(extended, PROGRAM_OPERATION_EXTENSIONS, {
3217
- value: {
3218
- createOperations(context) {
3219
- const baseOperations = baseFactory?.(context);
3220
- if (baseOperations) {
3221
- const connectedProgram = context.program;
3222
- connectedProgram.instructions = mergeNamespace(connectedProgram.instructions, baseOperations.instructions);
3223
- connectedProgram.transactions = mergeNamespace(connectedProgram.transactions, baseOperations.transactions);
3224
- connectedProgram.flows = mergeNamespace(connectedProgram.flows, baseOperations.flows);
3225
- }
3226
- return mergeOperations(baseOperations, extensionFactory?.(context));
3227
- },
3228
- },
3229
- enumerable: false,
3230
- configurable: false,
3231
- writable: false,
3232
- });
3233
- }
3234
- return extended;
3235
- }
3236
- function extendPrograms(programs, extensions) {
3237
- const merged = { ...programs };
3238
- for (const [name, program] of Object.entries(programs)) {
3239
- const extension = extensions[name];
3240
- merged[name] = extension
3241
- ? extendProgram(program, extension)
3242
- : program;
3243
- }
3244
- return merged;
3245
- }
3246
- function getProgramRuntimeExtensions(program) {
3247
- return program[PROGRAM_OPERATION_EXTENSIONS];
3248
- }
3249
- function defineStackExtensions() {
3250
- return (extensions) => extensions;
3251
- }
3252
- function extendStack(stack, extensions) {
3253
- const base = stack;
3254
- const extended = { ...stack };
3255
- for (const key of ['addresses', 'constants', 'defaults', 'math']) {
3256
- if (extensions[key] !== undefined) {
3257
- extended[key] = mergeNamespace(base[key], extensions[key]);
3258
- }
3259
- }
3260
- const baseRuntime = base[STACK_RUNTIME_EXTENSIONS];
3261
- if (baseRuntime || extensions.createRead || extensions.createFlows) {
3262
- Object.defineProperty(extended, STACK_RUNTIME_EXTENSIONS, {
3263
- value: {
3264
- createRead: baseRuntime?.createRead && extensions.createRead
3265
- ? (client) => mergeNamespace(baseRuntime.createRead(client), extensions.createRead(client))
3266
- : extensions.createRead ?? baseRuntime?.createRead,
3267
- createFlows: baseRuntime?.createFlows && extensions.createFlows
3268
- ? (client) => mergeNamespace(baseRuntime.createFlows(client), extensions.createFlows(client))
3269
- : extensions.createFlows ?? baseRuntime?.createFlows,
3270
- },
3271
- enumerable: false,
3272
- configurable: false,
3273
- writable: false,
3274
- });
3275
- }
3276
- return extended;
3277
- }
3278
- function getStackRuntimeExtensions(stack) {
3279
- return stack[STACK_RUNTIME_EXTENSIONS];
3280
- }
3281
- function defineClientField(target, key, value) {
3282
- if (value === undefined || key in target) {
3283
- return;
3284
- }
3285
- Object.defineProperty(target, key, {
3286
- value,
3287
- enumerable: true,
3288
- configurable: true,
3289
- writable: false,
3290
- });
3291
- }
3292
- function applyConnectedStackExtensions(client, stack) {
3293
- const extendedStack = stack;
3294
- defineClientField(client, 'addresses', extendedStack.addresses);
3295
- defineClientField(client, 'constants', extendedStack.constants);
3296
- defineClientField(client, 'defaults', extendedStack.defaults);
3297
- defineClientField(client, 'math', extendedStack.math);
3298
- const runtime = getStackRuntimeExtensions(stack);
3299
- defineClientField(client, 'flows', runtime?.createFlows?.(client));
3300
- defineClientField(client, 'read', runtime?.createRead?.(client));
3301
- return client;
3302
- }
3303
-
3304
- function isPreparedInstruction(instruction) {
3305
- return 'kind' in instruction
3306
- && instruction.kind === 'instruction'
3307
- && 'instruction' in instruction;
3308
- }
3309
- function nonEmpty(values, label) {
3310
- if (values.length === 0) {
3311
- throw new Error(`${label} must contain at least one item`);
3312
- }
3313
- return [...values];
3314
- }
3315
- function dedupe(values) {
3316
- return [...new Set(values)];
3317
- }
3318
- function inferSignerAddresses(instructions) {
3319
- return dedupe(instructions.flatMap((instruction) => instruction.keys.filter((key) => key.isSigner).map((key) => key.pubkey)));
3320
- }
3321
- function createPreparedTransactionBody(input) {
3322
- const instructions = nonEmpty(input.instructions, `Transaction '${input.name}'`);
3323
- return {
3324
- name: input.name,
3325
- instructions,
3326
- requiredSignerAddresses: dedupe(input.requiredSignerAddresses ?? inferSignerAddresses(instructions)),
3327
- errors: [...(input.errors ?? [])],
3328
- };
3329
- }
3330
- function createPlan(name, artifacts, transactions) {
3331
- return {
3332
- name,
3333
- artifacts,
3334
- transactions: nonEmpty(transactions, `Flow '${name}'`),
3335
- };
3336
- }
3337
- function createPreparedInstruction(input) {
3338
- const transaction = createPreparedTransactionBody({
3339
- name: input.name,
3340
- instructions: [input.instruction],
3341
- requiredSignerAddresses: input.requiredSignerAddresses,
3342
- errors: input.errors,
3343
- });
3344
- return {
3345
- kind: 'instruction',
3346
- name: input.name,
3347
- instruction: input.instruction,
3348
- transaction,
3349
- plan: createPlan(input.name, input.artifacts, [transaction]),
3350
- artifacts: input.artifacts,
3351
- };
3352
- }
3353
- function createPreparedTransaction(input) {
3354
- const hasInstructions = input.instructions !== undefined;
3355
- const hasOperations = input.operations !== undefined;
3356
- if (hasInstructions === hasOperations) {
3357
- throw new Error(`Transaction '${input.name}' must provide exactly one of instructions or operations`);
3358
- }
3359
- const transactionParts = hasOperations
3360
- ? input.operations.map((operation) => operation.transaction)
3361
- : input.instructions.map((instruction) => isPreparedInstruction(instruction)
3362
- ? instruction.transaction
3363
- : createPreparedTransactionBody({
3364
- name: input.name,
3365
- instructions: [instruction],
3366
- }));
3367
- const transaction = createPreparedTransactionBody({
3368
- name: input.name,
3369
- instructions: transactionParts.flatMap((part) => part.instructions),
3370
- requiredSignerAddresses: input.requiredSignerAddresses
3371
- ?? transactionParts.flatMap((part) => part.requiredSignerAddresses),
3372
- errors: input.errors ?? transactionParts.flatMap((part) => part.errors),
3373
- });
3374
- return {
3375
- kind: 'transaction',
3376
- name: input.name,
3377
- transaction,
3378
- plan: createPlan(input.name, input.artifacts, [transaction]),
3379
- artifacts: input.artifacts,
3380
- };
3381
- }
3382
- function createPreparedFlow(input) {
3383
- const transactions = input.transactions.map((transaction) => createPreparedTransactionBody(transaction));
3384
- return {
3385
- kind: 'flow',
3386
- name: input.name,
3387
- plan: createPlan(input.name, input.artifacts, transactions),
3388
- artifacts: input.artifacts,
3389
- };
3390
- }
3391
- function prependTransactionInstructions(transaction, instructions) {
3392
- return createPreparedTransactionBody({
3393
- ...transaction,
3394
- instructions: [...instructions, ...transaction.instructions],
3395
- requiredSignerAddresses: [
3396
- ...inferSignerAddresses(instructions),
3397
- ...transaction.requiredSignerAddresses,
3398
- ],
3399
- });
3400
- }
3401
- function appendTransactionInstructions(transaction, instructions) {
3402
- return createPreparedTransactionBody({
3403
- ...transaction,
3404
- instructions: [...transaction.instructions, ...instructions],
3405
- requiredSignerAddresses: [
3406
- ...transaction.requiredSignerAddresses,
3407
- ...inferSignerAddresses(instructions),
3408
- ],
3409
- });
3410
- }
3411
- function appendFlowTransactions(flow, transactions) {
3412
- return createPreparedFlow({
3413
- name: flow.name,
3414
- artifacts: flow.artifacts,
3415
- transactions: [...flow.plan.transactions, ...transactions],
3416
- });
3417
- }
3418
- function prependFlowTransactionInstructions(flow, transactionIndex, instructions) {
3419
- const transaction = flow.plan.transactions[transactionIndex];
3420
- if (!transaction) {
3421
- throw new Error(`Flow '${flow.name}' has no transaction at index ${transactionIndex}`);
3422
- }
3423
- const transactions = [...flow.plan.transactions];
3424
- transactions[transactionIndex] = prependTransactionInstructions(transaction, instructions);
3425
- return createPreparedFlow({
3426
- name: flow.name,
3427
- artifacts: flow.artifacts,
3428
- transactions,
3429
- });
3430
- }
3431
- function inferSignerAddress(value) {
3432
- if (typeof value === 'string' && value.length > 0) {
3433
- return value;
3434
- }
3435
- if (!value || typeof value !== 'object') {
3436
- return null;
3437
- }
3438
- const candidate = value;
3439
- if (typeof candidate.address === 'string' && candidate.address.length > 0) {
3440
- return candidate.address;
3441
- }
3442
- if (typeof candidate.publicKey === 'string' && candidate.publicKey.length > 0) {
3443
- return candidate.publicKey;
3444
- }
3445
- const publicKey = candidate.publicKey;
3446
- return typeof publicKey?.toBase58 === 'function' ? publicKey.toBase58() : null;
3447
- }
3448
- function validateTransactionSigners(transaction, host, options) {
3449
- const signerAddresses = (options.signers ?? []).map(inferSignerAddress);
3450
- const available = new Set(options.availableSignerAddresses ?? []);
3451
- for (const address of options.signerRegistry?.addresses() ?? []) {
3452
- available.add(address);
3453
- }
3454
- const wallet = options.wallet ?? host.wallet;
3455
- for (const address of wallet?.signerAddresses ?? []) {
3456
- available.add(address);
3457
- }
3458
- const walletAddress = wallet?.publicKey ?? host.publicKey;
3459
- if (walletAddress) {
3460
- available.add(walletAddress);
3461
- }
3462
- for (const address of signerAddresses) {
3463
- if (address) {
3464
- available.add(address);
3465
- }
3466
- }
3467
- const missing = transaction.requiredSignerAddresses.filter((address) => !available.has(address));
3468
- if (missing.length > 0) {
3469
- throw new Error(`Missing signer(s) for ${transaction.name}: ${missing.join(', ')}`);
3470
- }
3471
- }
3472
- class OperationExecutionError extends Error {
3473
- constructor(input) {
3474
- super(`Operation '${input.operation.name}' failed at transaction ${input.failedTransactionIndex + 1} (${input.failedTransaction.name})`);
3475
- this.name = 'OperationExecutionError';
3476
- this.operation = input.operation;
3477
- this.failedTransaction = input.failedTransaction;
3478
- this.failedTransactionIndex = input.failedTransactionIndex;
3479
- this.completedReceipts = [...input.completedReceipts];
3480
- this.cause = input.cause;
3481
- }
3482
- }
3483
- async function executePreparedOperation(host, operation, options = {}) {
3484
- const receipts = [];
3485
- const signers = [
3486
- ...new Set([
3487
- ...(options.signerRegistry?.values() ?? []),
3488
- ...(options.signers ?? []),
3489
- ]),
3490
- ];
3491
- for (const [transactionIndex, transaction] of operation.plan.transactions.entries()) {
3492
- try {
3493
- validateTransactionSigners(transaction, host, options);
3494
- await options.onTransactionStart?.({ operation, transaction, transactionIndex });
3495
- const result = await host.transaction(transaction.instructions, {
3496
- wallet: options.wallet,
3497
- send: options.send,
3498
- errors: [...transaction.errors],
3499
- signers: signers.length > 0 ? signers : undefined,
3500
- });
3501
- const receipt = {
3502
- transactionIndex,
3503
- transactionName: transaction.name,
3504
- signature: result.signature,
3505
- slot: result.slot,
3506
- };
3507
- receipts.push(receipt);
3508
- await options.onTransactionSuccess?.({
3509
- operation,
3510
- transaction,
3511
- transactionIndex,
3512
- receipt,
3513
- });
3514
- }
3515
- catch (cause) {
3516
- throw new OperationExecutionError({
3517
- operation,
3518
- failedTransaction: transaction,
3519
- failedTransactionIndex: transactionIndex,
3520
- completedReceipts: receipts,
3521
- cause,
3522
- });
3523
- }
3524
- }
3525
- if (operation.kind === 'flow') {
3526
- return {
3527
- kind: 'flow',
3528
- operationName: operation.name,
3529
- artifacts: operation.artifacts,
3530
- signatures: nonEmpty(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
3531
- transactions: nonEmpty(receipts, `Operation '${operation.name}' receipts`),
3532
- };
3533
- }
3534
- return {
3535
- kind: operation.kind,
3536
- operationName: operation.name,
3537
- artifacts: operation.artifacts,
3538
- signatures: [receipts[0].signature],
3539
- transaction: receipts[0],
3540
- };
3541
- }
3542
- function convertToJsonValue(value, ancestors) {
3543
- if (value === null || typeof value === 'string' || typeof value === 'boolean') {
3544
- return value;
3545
- }
3546
- if (typeof value === 'number') {
3547
- return Number.isFinite(value) ? value : null;
3548
- }
3549
- if (typeof value === 'bigint') {
3550
- return value.toString();
3551
- }
3552
- if (value === undefined
3553
- || typeof value === 'function'
3554
- || typeof value === 'symbol') {
3555
- return undefined;
3556
- }
3557
- if (typeof value !== 'object') {
3558
- return String(value);
3559
- }
3560
- if (ancestors.has(value)) {
3561
- throw new Error('Cannot convert a circular value to JSON');
3562
- }
3563
- ancestors.add(value);
3564
- try {
3565
- if (ArrayBuffer.isView(value)) {
3566
- return Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
3567
- }
3568
- if (value instanceof ArrayBuffer) {
3569
- return Array.from(new Uint8Array(value));
3570
- }
3571
- if (Array.isArray(value)) {
3572
- return value.map((entry) => convertToJsonValue(entry, ancestors) ?? null);
3573
- }
3574
- if (value instanceof Set) {
3575
- return [...value].map((entry) => convertToJsonValue(entry, ancestors) ?? null);
3576
- }
3577
- if (value instanceof Map) {
3578
- const result = {};
3579
- for (const [key, entry] of value) {
3580
- const converted = convertToJsonValue(entry, ancestors);
3581
- if (converted !== undefined) {
3582
- result[String(key)] = converted;
3583
- }
3584
- }
3585
- return result;
3586
- }
3587
- const toJSON = value.toJSON;
3588
- if (typeof toJSON === 'function') {
3589
- return convertToJsonValue(toJSON.call(value), ancestors) ?? null;
3590
- }
3591
- const result = {};
3592
- for (const [key, entry] of Object.entries(value)) {
3593
- const converted = convertToJsonValue(entry, ancestors);
3594
- if (converted !== undefined) {
3595
- result[key] = converted;
3596
- }
3597
- }
3598
- return result;
3599
- }
3600
- finally {
3601
- ancestors.delete(value);
3602
- }
3603
- }
3604
- function toJsonValue(value) {
3605
- return convertToJsonValue(value, new Set()) ?? null;
3606
- }
3607
- function describePreparedOperation(operation) {
3608
- return {
3609
- kind: operation.kind,
3610
- name: operation.name,
3611
- artifacts: toJsonValue(operation.artifacts),
3612
- transactions: operation.plan.transactions.map((transaction) => ({
3613
- name: transaction.name,
3614
- requiredSignerAddresses: transaction.requiredSignerAddresses,
3615
- errors: transaction.errors,
3616
- instructions: transaction.instructions.map((instruction) => ({
3617
- programId: instruction.programId,
3618
- keys: instruction.keys,
3619
- data: Array.from(instruction.data),
3620
- })),
3621
- })),
3622
- };
3623
- }
3624
- function formatPreparedOperation(operation) {
3625
- const lines = [
3626
- `${operation.kind}: ${operation.name}`,
3627
- `Transactions: ${operation.plan.transactions.length}`,
3628
- ];
3629
- for (const [index, transaction] of operation.plan.transactions.entries()) {
3630
- lines.push(` ${index + 1}. ${transaction.name} (${transaction.instructions.length} instruction${transaction.instructions.length === 1 ? '' : 's'})`);
3631
- if (transaction.requiredSignerAddresses.length > 0) {
3632
- lines.push(` Signers: ${transaction.requiredSignerAddresses.join(', ')}`);
3633
- }
3634
- }
3635
- return lines.join('\n');
3636
- }
3637
-
3638
- function mergeAttachedPrograms(stack, attachedPrograms) {
3639
- const merged = { ...(attachedPrograms ?? {}) };
3640
- for (const [name, definition] of Object.entries(stack.programs ?? {})) {
3641
- if (name in merged) {
3642
- console.warn(`Ignoring attached program '${name}' for stack '${stack.name}' because the stack already defines that key`);
3643
- }
3644
- merged[name] = definition;
3645
- }
3646
- return merged;
3647
- }
3648
- function normalizeProgramAccountWireKeys(value) {
3649
- if (Array.isArray(value)) {
3650
- return value.map(normalizeProgramAccountWireKeys);
3651
- }
3652
- if (!value || typeof value !== 'object') {
3653
- return value;
3654
- }
3655
- return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
3656
- key.replace(/[A-Z]/g, (letter, index) => `${index === 0 ? '' : '_'}${letter.toLowerCase()}`),
3657
- normalizeProgramAccountWireKeys(nestedValue),
3658
- ]));
3659
- }
3660
- function parseProgramAccountValue(definition, value) {
3661
- const schema = definition.schema;
3662
- if (!schema) {
3663
- return value;
3664
- }
3665
- const parsed = schema.safeParse(value);
3666
- if (parsed.success) {
3667
- return parsed.data;
3668
- }
3669
- const normalized = schema.safeParse(normalizeProgramAccountWireKeys(value));
3670
- if (normalized.success) {
3671
- return normalized.data;
3672
- }
3673
- throw new Error(`Program account read '${definition.account}' failed schema validation`);
3674
- }
3675
- function cloneStackWithPrograms(stack, programs) {
3676
- const cloned = Object.create(Object.getPrototypeOf(stack), Object.getOwnPropertyDescriptors(stack));
3677
- cloned.programs = programs;
3678
- return cloned;
3679
- }
3680
- function withPrograms(stack, attachedPrograms) {
3681
- return cloneStackWithPrograms(stack, mergeAttachedPrograms(stack, attachedPrograms));
3682
- }
3683
- let Arete$1 = class Arete {
3684
- constructor(url, httpBaseUrl, options) {
3685
- this.stack = options.stack;
3686
- this._wallet = options.wallet;
3687
- this.executionDefaults = options.execution;
3688
- this.httpBaseUrl = httpBaseUrl;
3689
- this.fetchImpl = options.fetch ?? this.resolveFetchImpl();
3690
- this.storage = new SortedStorageDecorator(options.storage ?? new MemoryAdapter());
3691
- this.processor = new FrameProcessor(this.storage, {
3692
- maxEntriesPerView: options.maxEntriesPerView,
3693
- flushIntervalMs: options.flushIntervalMs,
3694
- schemas: this.stack.schemas,
3695
- patchSchemas: this.stack.patchSchemas,
3696
- });
3697
- this.connection = new ConnectionManager({
3698
- websocketUrl: url,
3699
- reconnectIntervals: options.reconnectIntervals,
3700
- maxReconnectAttempts: options.maxReconnectAttempts,
3701
- auth: options.auth,
3702
- });
3703
- this.subscriptionRegistry = new SubscriptionRegistry(this.connection);
3704
- this.connection.onFrame((frame) => {
3705
- this.processor.handleFrame(frame);
3706
- });
3707
- this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
3708
- this._queries = this.buildQueries();
3709
- this._chain = createChainClient(this.httpBaseUrl, this.authenticatedFetch.bind(this));
3710
- this._programs = this.buildPrograms();
3711
- }
3712
- resolveFetchImpl() {
3713
- if (typeof globalThis.fetch !== 'function') {
3714
- throw new AreteError('A fetch implementation is required for HTTP point reads (provide ConnectOptions.fetch or use an environment with global fetch)', 'INVALID_CONFIG');
3715
- }
3716
- return globalThis.fetch.bind(globalThis);
3717
- }
3718
- buildQueries() {
3719
- const queries = {};
3720
- for (const [name, definition] of Object.entries(this.stack.queries ?? {})) {
3721
- queries[name] = this.createQueryExecutor(definition);
3722
- }
3723
- return queries;
3724
- }
3725
- buildPrograms() {
3726
- const bases = {};
3727
- for (const [name, definition] of Object.entries(this.stack.programs ?? {})) {
3728
- const instructions = {};
3729
- for (const [instructionName, handler] of Object.entries(definition.rawInstructions ?? {})) {
3730
- instructions[instructionName] = this.createTypedInstruction(handler);
3731
- }
3732
- const accounts = {};
3733
- for (const [accountName, accountDefinition] of Object.entries(definition.accounts ?? {})) {
3734
- accounts[accountName] = this.createAccountReader(accountDefinition);
3735
- }
3736
- const queries = {};
3737
- for (const [queryName, queryDefinition] of Object.entries(definition.queries ?? {})) {
3738
- queries[queryName] = this.createQueryExecutor(queryDefinition);
3739
- }
3740
- bases[name] = {
3741
- name: definition.name,
3742
- programId: definition.programId,
3743
- schemas: definition.schemas,
3744
- pdas: definition.pdas ?? {},
3745
- accounts,
3746
- queries,
3747
- raw: instructions,
3748
- addresses: definition.addresses ?? {},
3749
- constants: definition.constants ?? {},
3750
- defaults: definition.defaults ?? {},
3751
- math: definition.math ?? {},
3752
- };
3753
- }
3754
- const programs = {};
3755
- const client = this;
3756
- for (const [name, definition] of Object.entries(this.stack.programs ?? {})) {
3757
- const base = bases[name];
3758
- const connectedProgram = {
3759
- ...base,
3760
- instructions: {},
3761
- transactions: {},
3762
- flows: {},
3763
- };
3764
- const runtime = getProgramRuntimeExtensions(definition);
3765
- const operations = runtime?.createOperations({
3766
- chain: this._chain,
3767
- get wallet() {
3768
- return client._wallet;
3769
- },
3770
- program: connectedProgram,
3771
- });
3772
- connectedProgram.instructions = operations?.instructions ?? {};
3773
- connectedProgram.transactions = operations?.transactions ?? {};
3774
- connectedProgram.flows = operations?.flows ?? {};
3775
- programs[name] = connectedProgram;
3776
- }
3777
- return programs;
3778
- }
3779
- createTypedInstruction(handler) {
3780
- return {
3781
- build: (params, options) => buildInstruction(handler, params, this.withWallet(options)),
3782
- };
3783
- }
3784
- createAccountReader(definition) {
3785
- return {
3786
- fetch: async (address) => {
3787
- const result = await this.readJson(`${definition.path}/${encodeURIComponent(address)}`);
3788
- return result === null ? null : parseProgramAccountValue(definition, result);
3789
- },
3790
- fetchMany: async (addresses) => {
3791
- const result = await this.readJson(definition.path, {
3792
- method: 'POST',
3793
- body: { addresses },
3794
- });
3795
- return result.map((value) => value === null ? null : parseProgramAccountValue(definition, value));
3796
- },
3797
- exists: async (address) => {
3798
- const result = await this.readJson(`${definition.path}/${encodeURIComponent(address)}/exists`);
3799
- return result.exists;
3800
- },
3801
- };
3802
- }
3803
- createQueryExecutor(definition) {
3804
- return async (params) => {
3805
- const result = await this.readJson(definition.path, {
3806
- method: definition.method ?? 'POST',
3807
- body: params,
3808
- });
3809
- if (!definition.schema) {
3810
- return result;
3811
- }
3812
- const parsed = definition.schema.safeParse(result);
3813
- if (!parsed.success) {
3814
- throw new Error(`Query '${definition.name}' failed schema validation`);
3815
- }
3816
- return parsed.data;
3817
- };
3818
- }
3819
- async readJson(path, options) {
3820
- const response = await this.authenticatedFetch(this.resolveReadUrl(path), {
3821
- method: options?.method ?? 'GET',
3822
- headers: options?.body === undefined ? undefined : { 'content-type': 'application/json' },
3823
- body: options?.body === undefined ? undefined : JSON.stringify(options.body),
3824
- });
3825
- return parseReadResponse(response, path);
3826
- }
3827
- async authenticatedFetch(input, init) {
3828
- const attempt = async (forceRefresh = false) => {
3829
- const token = await this.connection.getHttpAuthToken(forceRefresh);
3830
- const headers = new Headers(init?.headers ?? undefined);
3831
- if (token) {
3832
- headers.set('authorization', `Bearer ${token}`);
3833
- }
3834
- return this.fetchImpl(input, {
3835
- ...init,
3836
- headers,
3837
- });
3838
- };
3839
- let response = await attempt(false);
3840
- if (!response.ok) {
3841
- const wireErrorCode = response.headers.get('X-Error-Code');
3842
- const errorCode = wireErrorCode ? parseErrorCode(wireErrorCode) : undefined;
3843
- if (errorCode && shouldRefreshToken(errorCode)) {
3844
- this.connection.clearHttpAuthToken();
3845
- response = await attempt(true);
3846
- }
3847
- }
3848
- return response;
3849
- }
3850
- resolveReadUrl(path) {
3851
- return `${this.httpBaseUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
3852
- }
3853
- /** Merge the client's default wallet into call options (call options win). */
3854
- withWallet(options) {
3855
- const merged = { ...(options ?? {}) };
3856
- if (!merged.wallet && this._wallet) {
3857
- merged.wallet = this._wallet;
3858
- }
3859
- return merged;
3860
- }
3861
- static async connect(stack, options) {
3862
- const requestedUrl = options?.url ?? stack.endpoints.ws;
3863
- const autoReconnect = options?.autoReconnect !== false;
3864
- const httpOnly = options?.transport === 'http' || (!requestedUrl && !autoReconnect);
3865
- const url = httpOnly ? null : requestedUrl;
3866
- const httpUrl = options?.httpUrl
3867
- ?? stack.endpoints.http
3868
- ?? (requestedUrl ? deriveHttpEndpoint(requestedUrl) : undefined);
3869
- if (!httpOnly && !url) {
3870
- throw new AreteError('WebSocket URL is required (provide url option or define endpoints.ws in stack)', 'INVALID_CONFIG');
3871
- }
3872
- if (!httpUrl) {
3873
- throw new AreteError('HTTP endpoint is required for transport: "http" (provide httpUrl option or define endpoints.http in stack)', 'INVALID_CONFIG');
3874
- }
3875
- const attachedPrograms = options?.programs;
3876
- const effectiveStack = withPrograms(stack, attachedPrograms);
3877
- const internalOptions = {
3878
- stack: effectiveStack,
3879
- httpUrl,
3880
- storage: options?.storage,
3881
- maxEntriesPerView: options?.maxEntriesPerView,
3882
- flushIntervalMs: options?.flushIntervalMs,
3883
- autoReconnect: options?.autoReconnect,
3884
- reconnectIntervals: options?.reconnectIntervals,
3885
- maxReconnectAttempts: options?.maxReconnectAttempts,
3886
- validateFrames: options?.validateFrames,
3887
- auth: options?.auth,
3888
- wallet: options?.wallet,
3889
- fetch: options?.fetch,
3890
- execution: options?.execution,
3891
- };
3892
- const client = new Arete(url, httpUrl, internalOptions);
3893
- if (!httpOnly && autoReconnect) {
3894
- await client.connection.connect();
3895
- }
3896
- return applyConnectedStackExtensions(client, effectiveStack);
3897
- }
3898
- get views() {
3899
- return this._views;
3900
- }
3901
- get queries() {
3902
- return this._queries;
3903
- }
3904
- get programs() {
3905
- return this._programs;
3906
- }
3907
- get chain() {
3908
- return this._chain;
3909
- }
3910
- /** The default wallet adapter, if one was configured. */
3911
- get wallet() {
3912
- return this._wallet;
3913
- }
3914
- /** The connected wallet address, if a default wallet was configured. */
3915
- get publicKey() {
3916
- return this._wallet?.publicKey;
3917
- }
3918
- /**
3919
- * Set (or clear) the default wallet adapter used for instruction execution.
3920
- * Useful for connecting/disconnecting a wallet after the client is created.
3921
- */
3922
- setWallet(wallet) {
3923
- this._wallet = wallet;
3924
- }
3925
- /**
3926
- * Sign and send a batch of pre-built instructions as a single transaction.
3927
- *
3928
- * Build instructions with `client.programs.<program>.raw.<name>.build(params)`
3929
- * and compose them here. RPC/compilation/confirmation are owned by the adapter.
3930
- *
3931
- * On failure, the error is parsed against `options.errors` when given,
3932
- * otherwise against error metadata aggregated from all the stack's handlers
3933
- * (deduped by code, first-wins — if the stack bundles programs with
3934
- * overlapping error codes, pass `options.errors` or use the per-instruction
3935
- * call path for precise attribution).
3936
- */
3937
- async transaction(instructions, options) {
3938
- const wallet = options?.wallet ?? this._wallet;
3939
- if (!wallet) {
3940
- throw new Error('Wallet required to sign and send transaction');
3941
- }
3942
- try {
3943
- const sendOptions = {
3944
- ...(options?.send ?? {}),
3945
- };
3946
- if (options?.signers !== undefined) {
3947
- sendOptions.signers = options.signers;
3948
- }
3949
- const result = await wallet.signAndSend(instructions, sendOptions);
3950
- return { signature: result.signature, slot: result.slot };
3951
- }
3952
- catch (err) {
3953
- const programError = parseInstructionError(err, options?.errors ?? this.aggregateErrors());
3954
- if (programError) {
3955
- throw new InstructionError(`${programError.name} (${programError.code}): ${programError.message}`, programError, err);
3956
- }
3957
- throw err;
3958
- }
3959
- }
3960
- execute(prepared, options) {
3961
- const defaults = this.executionDefaults;
3962
- return executePreparedOperation(this, prepared, {
3963
- wallet: options?.wallet ?? defaults?.wallet,
3964
- send: defaults?.send || options?.send
3965
- ? { ...(defaults?.send ?? {}), ...(options?.send ?? {}) }
3966
- : undefined,
3967
- signers: options?.signers ?? defaults?.signers,
3968
- signerRegistry: options?.signerRegistry ?? defaults?.signerRegistry,
3969
- availableSignerAddresses: options?.availableSignerAddresses ?? defaults?.availableSignerAddresses,
3970
- onTransactionStart: options?.onTransactionStart ?? defaults?.onTransactionStart,
3971
- onTransactionSuccess: options?.onTransactionSuccess ?? defaults?.onTransactionSuccess,
3972
- });
3973
- }
3974
- /** Error metadata from every handler in the stack, deduped by code. */
3975
- aggregateErrors() {
3976
- if (!this._aggregatedErrors) {
3977
- const all = [];
3978
- const seen = new Set();
3979
- const collect = (handler) => {
3980
- for (const error of handler.errors ?? []) {
3981
- if (!seen.has(error.code)) {
3982
- seen.add(error.code);
3983
- all.push(error);
3984
- }
3985
- }
3986
- };
3987
- for (const program of Object.values(this.stack.programs ?? {})) {
3988
- for (const handler of Object.values(program.rawInstructions ?? {})) {
3989
- collect(handler);
3990
- }
3991
- }
3992
- this._aggregatedErrors = all;
3993
- }
3994
- return this._aggregatedErrors;
3995
- }
3996
- get connectionState() {
3997
- return this.connection.getState();
3998
- }
3999
- get stackName() {
4000
- return this.stack.name;
4001
- }
4002
- get store() {
4003
- return this.storage;
4004
- }
4005
- onConnectionStateChange(callback) {
4006
- return this.connection.onStateChange(callback);
4007
- }
4008
- onFrame(callback) {
4009
- return this.connection.onFrame(callback);
4010
- }
4011
- onSocketIssue(callback) {
4012
- return this.connection.onSocketIssue(callback);
4013
- }
4014
- async connect() {
4015
- await this.connection.connect();
4016
- }
4017
- disconnect() {
4018
- this.subscriptionRegistry.clear();
4019
- this.connection.disconnect();
4020
- }
4021
- isConnected() {
4022
- return this.connection.isConnected();
4023
- }
4024
- clearStore() {
4025
- this.storage.clear();
4026
- }
4027
- getStore() {
4028
- return this.storage;
4029
- }
4030
- getConnection() {
4031
- return this.connection;
4032
- }
4033
- getSubscriptionRegistry() {
4034
- return this.subscriptionRegistry;
4035
- }
4036
- };
4037
-
4038
- function createSignerRegistry(entries = []) {
4039
- const signers = new Map();
4040
- for (const [address, signer] of entries) {
4041
- if (!address) {
4042
- throw new Error('Signer registry addresses must not be empty');
4043
- }
4044
- signers.set(address, signer);
4045
- }
4046
- return {
4047
- register(address, signer) {
4048
- if (!address) {
4049
- throw new Error('Signer registry addresses must not be empty');
4050
- }
4051
- signers.set(address, signer);
4052
- },
4053
- unregister(address) {
4054
- return signers.delete(address);
4055
- },
4056
- get(address) {
4057
- return signers.get(address);
4058
- },
4059
- has(address) {
4060
- return signers.has(address);
4061
- },
4062
- addresses() {
4063
- return [...signers.keys()];
4064
- },
4065
- values() {
4066
- return [...signers.values()];
4067
- },
4068
- entries() {
4069
- return [...signers.entries()];
4070
- },
4071
- clear() {
4072
- signers.clear();
4073
- },
4074
- };
4075
- }
4076
-
4077
- function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
4078
- const transport = member?.transport ?? options?.transport ?? (forceHttpOnly ? 'http' : 'ws');
4079
- // Resolution order: per-member override → the stack's own endpoints →
4080
- // session-wide fallback endpoints.
4081
- const url = member?.url ?? (stack.endpoints.ws || options?.endpoints?.ws);
4082
- const httpUrl = member?.httpUrl ?? stack.endpoints.http ?? options?.endpoints?.http;
4083
- return {
4084
- url,
4085
- httpUrl,
4086
- transport,
4087
- auth: member?.auth ?? options?.auth,
4088
- storage: member?.storage,
4089
- autoReconnect: member?.autoReconnect,
4090
- wallet: options?.wallet,
4091
- fetch: options?.fetch,
4092
- };
4093
- }
4094
- function programAsStack(name, program) {
4095
- return {
4096
- name,
4097
- endpoints: { ws: '' },
4098
- views: {},
4099
- programs: { [name]: program },
4100
- };
4101
- }
4102
- async function createSession(definition, options) {
4103
- const stackEntries = Object.entries(definition.stacks ?? {});
4104
- const programEntries = Object.entries(definition.programs ?? {});
4105
- if (stackEntries.length === 0 && programEntries.length === 0) {
4106
- throw new AreteError('createSession requires at least one stack or program member', 'INVALID_CONFIG');
4107
- }
4108
- let wallet = options?.wallet;
4109
- const signerRegistry = options?.signerRegistry ?? createSignerRegistry();
4110
- const connectedStacks = await Promise.all(stackEntries.map(async ([key, stack]) => {
4111
- const memberOptions = options?.stacks?.[key];
4112
- const effectiveStack = withPrograms(stack, memberOptions?.programs);
4113
- const connectOptions = resolveMemberConnectOptions(effectiveStack, memberOptions, options, false);
4114
- const client = await Arete$1.connect(effectiveStack, connectOptions);
4115
- return [key, client];
4116
- }));
4117
- const connectedPrograms = await Promise.all(programEntries.map(async ([key, program]) => {
4118
- const syntheticStack = programAsStack(key, program);
4119
- const connectOptions = resolveMemberConnectOptions(syntheticStack, options?.programs?.[key], options, true);
4120
- const client = await Arete$1.connect(syntheticStack, connectOptions);
4121
- return [key, client];
4122
- }));
4123
- const memberClients = [
4124
- ...connectedStacks.map(([, client]) => client),
4125
- ...connectedPrograms.map(([, client]) => client),
4126
- ];
4127
- const executionHost = memberClients[0];
4128
- const stacks = Object.fromEntries(connectedStacks);
4129
- const explicitProgramKeys = new Set(connectedPrograms.map(([key]) => key));
4130
- const programOwners = new Map();
4131
- const connectedProgramEntries = connectedPrograms.map(([key, client]) => [
4132
- key,
4133
- client.programs[key],
4134
- ]);
4135
- const promotedPrograms = Object.fromEntries(connectedProgramEntries);
4136
- for (const [stackKey, client] of connectedStacks) {
4137
- for (const [programKey, program] of Object.entries(client.programs)) {
4138
- if (explicitProgramKeys.has(programKey)) {
4139
- continue;
4140
- }
4141
- const existingOwner = programOwners.get(programKey);
4142
- if (existingOwner) {
4143
- console.warn(`Program '${programKey}' is bundled by stacks '${existingOwner.stack}'` +
4144
- ` (${existingOwner.programId ?? 'unknown program ID'}) and '${stackKey}'` +
4145
- ` (${program.programId ?? 'unknown program ID'}); session.programs.${programKey}` +
4146
- ` uses '${existingOwner.stack}' because it was connected first`);
4147
- continue;
4148
- }
4149
- promotedPrograms[programKey] = program;
4150
- programOwners.set(programKey, { stack: stackKey, programId: program.programId });
4151
- }
4152
- }
4153
- const programs = promotedPrograms;
4154
- const chain = options?.chain ?? (options?.endpoints?.http !== undefined
4155
- ? createChainClient(options.endpoints.http, (options?.fetch ?? globalThis.fetch?.bind(globalThis)))
4156
- : executionHost.chain);
4157
- const session = {
4158
- stacks,
4159
- programs,
4160
- get wallet() {
4161
- return wallet;
4162
- },
4163
- signerRegistry,
4164
- chain,
4165
- transaction(instructions, transactionOptions) {
4166
- const defaults = options?.execution;
4167
- const configuredSigners = transactionOptions?.signers ?? defaults?.signers;
4168
- const signers = [...new Set([...signerRegistry.values(), ...(configuredSigners ?? [])])];
4169
- return executionHost.transaction(instructions, {
4170
- wallet: transactionOptions?.wallet ?? defaults?.wallet,
4171
- send: defaults?.send || transactionOptions?.send
4172
- ? { ...(defaults?.send ?? {}), ...(transactionOptions?.send ?? {}) }
4173
- : undefined,
4174
- errors: transactionOptions?.errors,
4175
- signers: signers.length > 0 ? signers : undefined,
4176
- });
4177
- },
4178
- execute(prepared, executionOptions) {
4179
- const defaults = options?.execution;
4180
- const configuredSigners = executionOptions?.signers ?? defaults?.signers;
4181
- return executionHost.execute(prepared, {
4182
- wallet: executionOptions?.wallet ?? defaults?.wallet,
4183
- send: defaults?.send || executionOptions?.send
4184
- ? { ...(defaults?.send ?? {}), ...(executionOptions?.send ?? {}) }
4185
- : undefined,
4186
- signers: configuredSigners,
4187
- signerRegistry,
4188
- availableSignerAddresses: executionOptions?.availableSignerAddresses ?? defaults?.availableSignerAddresses,
4189
- onTransactionStart: executionOptions?.onTransactionStart ?? defaults?.onTransactionStart,
4190
- onTransactionSuccess: executionOptions?.onTransactionSuccess ?? defaults?.onTransactionSuccess,
4191
- });
4192
- },
4193
- setWallet(nextWallet) {
4194
- wallet = nextWallet;
4195
- for (const client of memberClients) {
4196
- client.setWallet(nextWallet);
4197
- }
4198
- },
4199
- close() {
4200
- for (const client of memberClients) {
4201
- client.disconnect();
4202
- }
4203
- },
4204
- };
4205
- return session;
4206
- }
4207
-
4208
- function instructionOperation(prepare) {
4209
- return {
4210
- kind: 'instruction',
4211
- prepare: async (input) => prepare(input),
4212
- };
4213
- }
4214
- function transactionOperation(prepare) {
4215
- return {
4216
- kind: 'transaction',
4217
- prepare: async (input) => prepare(input),
4218
- };
4219
- }
4220
- function flowOperation(prepare) {
4221
- return {
4222
- kind: 'flow',
4223
- prepare: async (input) => prepare(input),
4224
- };
4225
- }
4226
-
4227
- function chainAccountLoader(chain) {
4228
- return {
4229
- async getAccount(address) {
4230
- const account = await chain.account(address);
4231
- return account ? { data: account.data } : null;
4232
- },
4233
- };
4234
- }
4235
-
4236
- const SPL_TOKEN_PROGRAM_ADDRESS = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
4237
- const TOKEN_2022_PROGRAM_ADDRESS = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
4238
- const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
4239
- const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111';
4240
- async function resolveTokenProgramAddress(chain, mint, override) {
4241
- if (override) {
4242
- return override;
4243
- }
4244
- const mintAccount = await chain.mint(mint);
4245
- if (!mintAccount) {
4246
- throw new Error(`Mint account not found while resolving token program: ${mint}`);
4247
- }
4248
- if (mintAccount.ownerProgram !== SPL_TOKEN_PROGRAM_ADDRESS &&
4249
- mintAccount.ownerProgram !== TOKEN_2022_PROGRAM_ADDRESS) {
4250
- throw new Error(`Mint ${mint} is owned by unsupported token program ${mintAccount.ownerProgram}`);
4251
- }
4252
- return mintAccount.ownerProgram;
4253
- }
4254
- function deriveAssociatedTokenAccount(input) {
4255
- const [address] = findProgramAddressSync([
4256
- createPublicKeySeed(input.owner),
4257
- createPublicKeySeed(input.tokenProgram ?? SPL_TOKEN_PROGRAM_ADDRESS),
4258
- createPublicKeySeed(input.mint),
4259
- ], ASSOCIATED_TOKEN_PROGRAM_ADDRESS);
4260
- return address;
4261
- }
4262
-
4263
- /** Convert a UI amount ("1.5") to raw base units using string math (no float precision loss). */
4264
- function parseUiAmountToRaw(value, decimals) {
4265
- const trimmed = String(value).trim();
4266
- if (!/^\d+(?:\.\d+)?$/.test(trimmed)) {
4267
- throw new Error(`Invalid UI amount: ${value}`);
4268
- }
4269
- const [wholePart, fractionPart = ''] = trimmed.split('.');
4270
- if (fractionPart.length > decimals) {
4271
- const excess = fractionPart.slice(decimals);
4272
- if (/[1-9]/.test(excess)) {
4273
- throw new Error(`UI amount ${value} has more fractional digits than the mint's ${decimals} decimals`);
4274
- }
4275
- }
4276
- const fraction = fractionPart.padEnd(decimals, '0');
4277
- const whole = BigInt(wholePart || '0') * 10n ** BigInt(decimals);
4278
- const fractional = BigInt(fraction.slice(0, decimals) || '0');
4279
- return whole + fractional;
4280
- }
4281
- /** Format raw base units as a UI decimal string (inverse of {@link parseUiAmountToRaw}). */
4282
- function formatRawToUi(raw, decimals) {
4283
- const value = BigInt(raw);
4284
- const negative = value < 0n;
4285
- const magnitude = negative ? -value : value;
4286
- const scale = 10n ** BigInt(decimals);
4287
- const whole = magnitude / scale;
4288
- const fraction = magnitude % scale;
4289
- const sign = negative ? '-' : '';
4290
- if (decimals === 0 || fraction === 0n) {
4291
- return `${sign}${whole.toString()}`;
4292
- }
4293
- const fractionText = fraction.toString().padStart(decimals, '0').replace(/0+$/, '');
4294
- return `${sign}${whole.toString()}.${fractionText}`;
4295
- }
4296
- /** Resolve an {@link AmountInput} to raw base units with known decimals. */
4297
- function toRawAmount(amount, decimals) {
4298
- if (typeof amount === 'bigint') {
4299
- return amount;
4300
- }
4301
- if ('raw' in amount) {
4302
- return BigInt(amount.raw);
4303
- }
4304
- return parseUiAmountToRaw(amount.ui, decimals);
4305
- }
4306
- /** Fetch a mint's decimals via the chain read endpoint, throwing when unavailable. */
4307
- async function getMintDecimals(chain, mint) {
4308
- const account = await chain.mint(mint);
4309
- if (!account || account.decimals == null) {
4310
- throw new Error(`Mint ${mint} is missing decimals on the configured read endpoint.`);
4311
- }
4312
- return account.decimals;
4313
- }
4314
- /**
4315
- * Resolve an {@link AmountInput} to raw base units, fetching the mint's
4316
- * decimals only when they are unknown and actually needed (a bare bigint or
4317
- * `{raw}` input with explicit `decimals` never touches the network).
4318
- */
4319
- async function resolveAmount(chain, input) {
4320
- const needsDecimalsForParse = typeof input.amount !== 'bigint' && 'ui' in input.amount;
4321
- if (!needsDecimalsForParse) {
4322
- const raw = typeof input.amount === 'bigint'
4323
- ? input.amount
4324
- : BigInt(input.amount.raw);
4325
- const decimals = input.decimals ?? await getMintDecimals(chain, input.mint);
4326
- return { raw, decimals };
4327
- }
4328
- const decimals = input.decimals ?? await getMintDecimals(chain, input.mint);
4329
- return { raw: toRawAmount(input.amount, decimals), decimals };
4330
- }
4331
- /**
4332
- * Resolve an {@link AmountInput} to raw base units without forcing a decimals
4333
- * fetch when the input is already expressed in raw units.
4334
- */
4335
- async function resolveAmountToRaw(chain, input) {
4336
- if (typeof input.amount === 'bigint') {
4337
- return input.amount;
4338
- }
4339
- if ('raw' in input.amount) {
4340
- return BigInt(input.amount.raw);
4341
- }
4342
- const decimals = input.decimals ?? await getMintDecimals(chain, input.mint);
4343
- return toRawAmount(input.amount, decimals);
4344
- }
4345
- /** Resolve a named set of {@link AmountInput} values to raw base units. */
4346
- async function resolveAmountsToRaw(chain, inputs) {
4347
- const entries = await Promise.all(Object.entries(inputs).map(async ([name, input]) => {
4348
- const raw = await resolveAmountToRaw(chain, input);
4349
- return [name, raw];
4350
- }));
4351
- return Object.fromEntries(entries);
4352
- }
4353
-
4354
- const Arete = Object.assign(Arete$1, { session: createSession });
4355
-
4356
- exports.ASSOCIATED_TOKEN_PROGRAM_ADDRESS = ASSOCIATED_TOKEN_PROGRAM_ADDRESS;
4357
- exports.Arete = Arete;
4358
- exports.AreteError = AreteError;
4359
- exports.ConnectionManager = ConnectionManager;
4360
- exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
4361
- exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
4362
- exports.FrameProcessor = FrameProcessor;
4363
- exports.InstructionError = InstructionError;
4364
- exports.MemoryAdapter = MemoryAdapter;
4365
- exports.OperationExecutionError = OperationExecutionError;
4366
- exports.PROGRAM_OPERATION_EXTENSIONS = PROGRAM_OPERATION_EXTENSIONS;
4367
- exports.ReadRequestError = ReadRequestError;
4368
- exports.SPL_TOKEN_PROGRAM_ADDRESS = SPL_TOKEN_PROGRAM_ADDRESS;
4369
- exports.STACK_RUNTIME_EXTENSIONS = STACK_RUNTIME_EXTENSIONS;
4370
- exports.SYSTEM_PROGRAM_ADDRESS = SYSTEM_PROGRAM_ADDRESS;
4371
- exports.SubscriptionRegistry = SubscriptionRegistry;
4372
- exports.TOKEN_2022_PROGRAM_ADDRESS = TOKEN_2022_PROGRAM_ADDRESS;
4373
- exports.account = account;
4374
- exports.appendFlowTransactions = appendFlowTransactions;
4375
- exports.appendTransactionInstructions = appendTransactionInstructions;
4376
- exports.applyConnectedStackExtensions = applyConnectedStackExtensions;
4377
- exports.arg = arg;
4378
- exports.buildInstruction = buildInstruction;
4379
- exports.bytes = bytes;
4380
- exports.chainAccountLoader = chainAccountLoader;
4381
- exports.createChainClient = createChainClient;
4382
- exports.createEntityStream = createEntityStream;
4383
- exports.createInstructionExecutor = createInstructionExecutor;
4384
- exports.createInstructionHandler = createInstructionHandler;
4385
- exports.createPreparedFlow = createPreparedFlow;
4386
- exports.createPreparedInstruction = createPreparedInstruction;
4387
- exports.createPreparedTransaction = createPreparedTransaction;
4388
- exports.createPreparedTransactionBody = createPreparedTransactionBody;
4389
- exports.createProgramPdas = createProgramPdas;
4390
- exports.createPublicKeySeed = createPublicKeySeed;
4391
- exports.createRichUpdateStream = createRichUpdateStream;
4392
- exports.createSeed = createSeed;
4393
- exports.createSession = createSession;
4394
- exports.createSignerRegistry = createSignerRegistry;
4395
- exports.createTypedListView = createTypedListView;
4396
- exports.createTypedStateView = createTypedStateView;
4397
- exports.createTypedViews = createTypedViews;
4398
- exports.createUpdateStream = createUpdateStream;
4399
- exports.decodeBase58 = decodeBase58;
4400
- exports.defineProgramExtensions = defineProgramExtensions;
4401
- exports.defineStackExtensions = defineStackExtensions;
4402
- exports.deriveAssociatedTokenAccount = deriveAssociatedTokenAccount;
4403
- exports.deriveHttpEndpoint = deriveHttpEndpoint;
4404
- exports.derivePda = findProgramAddress;
4405
- exports.describePreparedOperation = describePreparedOperation;
4406
- exports.encodeBase58 = encodeBase58;
4407
- exports.executeInstruction = executeInstruction;
4408
- exports.executePreparedOperation = executePreparedOperation;
4409
- exports.extendProgram = extendProgram;
4410
- exports.extendPrograms = extendPrograms;
4411
- exports.extendStack = extendStack;
4412
- exports.findProgramAddress = findProgramAddress;
4413
- exports.findProgramAddressSync = findProgramAddressSync;
4414
- exports.flowOperation = flowOperation;
4415
- exports.formatPreparedOperation = formatPreparedOperation;
4416
- exports.formatProgramError = formatProgramError;
4417
- exports.formatRawToUi = formatRawToUi;
4418
- exports.getMintDecimals = getMintDecimals;
4419
- exports.getProgramRuntimeExtensions = getProgramRuntimeExtensions;
4420
- exports.getStackRuntimeExtensions = getStackRuntimeExtensions;
4421
- exports.instructionOperation = instructionOperation;
4422
- exports.isEntityFrame = isEntityFrame;
4423
- exports.isSnapshotFrame = isSnapshotFrame;
4424
- exports.isSubscribedFrame = isSubscribedFrame;
4425
- exports.isValidFrame = isValidFrame;
4426
- exports.literal = literal;
4427
- exports.parseFrame = parseFrame;
4428
- exports.parseFrameFromBlob = parseFrameFromBlob;
4429
- exports.parseInstructionError = parseInstructionError;
4430
- exports.parseUiAmountToRaw = parseUiAmountToRaw;
4431
- exports.pda = pda;
4432
- exports.prependFlowTransactionInstructions = prependFlowTransactionInstructions;
4433
- exports.prependTransactionInstructions = prependTransactionInstructions;
4434
- exports.programAccountRead = programAccountRead;
4435
- exports.programQuery = programQuery;
4436
- exports.resolveAccounts = resolveAccounts;
4437
- exports.resolveAmount = resolveAmount;
4438
- exports.resolveAmountToRaw = resolveAmountToRaw;
4439
- exports.resolveAmountsToRaw = resolveAmountsToRaw;
4440
- exports.resolveTokenProgramAddress = resolveTokenProgramAddress;
4441
- exports.serializeInstructionData = serializeInstructionData;
4442
- exports.stackQuery = stackQuery;
4443
- exports.toJsonValue = toJsonValue;
4444
- exports.toRawAmount = toRawAmount;
4445
- exports.transactionOperation = transactionOperation;
4446
- exports.validateAccountResolution = validateAccountResolution;
4447
- exports.withPrograms = withPrograms;
4448
- //# sourceMappingURL=index.js.map