@arcanetech/privacy-sdk-relay 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,881 @@
1
+ // src/json-safe.ts
2
+ function jsonSafeClone(value) {
3
+ return JSON.parse(JSON.stringify(value, jsonSafeReplacer));
4
+ }
5
+ function jsonSafeReplacer(_key, value) {
6
+ if (typeof value === "bigint") {
7
+ return value.toString();
8
+ }
9
+ if (value instanceof Uint8Array) {
10
+ return { type: "Buffer", data: [...value] };
11
+ }
12
+ return value;
13
+ }
14
+
15
+ // src/relay-config.ts
16
+ function resolveRelayOrigin(origin) {
17
+ const trimmed = origin?.trim().replace(/\/$/u, "");
18
+ if (!trimmed) {
19
+ return void 0;
20
+ }
21
+ return trimmed;
22
+ }
23
+ function isRelayConfigured(config) {
24
+ return Boolean(resolveRelayOrigin(config?.origin));
25
+ }
26
+ function requireRelayOrigin(origin) {
27
+ const resolved = resolveRelayOrigin(origin);
28
+ if (!resolved) {
29
+ throw new Error("Relay API origin is not configured.");
30
+ }
31
+ return resolved;
32
+ }
33
+ function requireRelayApi(ports) {
34
+ if (!ports.relayApi) {
35
+ throw new Error("Relay API is not configured.");
36
+ }
37
+ return ports.relayApi;
38
+ }
39
+
40
+ // src/reasons.ts
41
+ var RELAY_PUBLIC_REASON = {
42
+ invalidPackage: "invalid_package",
43
+ unsupportedSignalShape: "unsupported_signal_shape",
44
+ positivePublicDeposit: "positive_public_deposit",
45
+ zkConfigMissing: "zk_config_missing",
46
+ zkConfigDeprecated: "zk_config_deprecated",
47
+ zkConfigIncompatible: "zk_config_incompatible",
48
+ invalidProof: "invalid_proof",
49
+ kytRejected: "kyt_rejected",
50
+ kytAuthorizationMismatch: "kyt_authorization_mismatch",
51
+ escrowRelayUnconfigured: "escrow_relay_unconfigured",
52
+ escrowAuthorizationMismatch: "escrow_authorization_mismatch",
53
+ nullifiersSpent: "nullifiers_spent",
54
+ simulationFailed: "simulation_failed",
55
+ resourceFeeExceeded: "resource_fee_exceeded",
56
+ resourceLimitExceeded: "resource_limit_exceeded",
57
+ sendFailed: "send_failed",
58
+ transactionFailed: "transaction_failed",
59
+ infrastructureFailed: "infrastructure_failed"
60
+ };
61
+ var RELAY_HTTP_REASON = {
62
+ payloadTooLarge: "payload_too_large",
63
+ rateLimited: "rate_limited",
64
+ unsupportedPool: "unsupported_pool",
65
+ queueAtCapacity: "queue_at_capacity",
66
+ lowBalance: "low_balance",
67
+ conflictingPayload: "conflicting_payload",
68
+ retryNotAllowed: "retry_not_allowed",
69
+ notFound: "not_found",
70
+ relayerUnavailable: "relayer_unavailable",
71
+ invalidPackage: "invalid_package",
72
+ unsupportedSignalShape: "unsupported_signal_shape"
73
+ };
74
+ var REJECTION_REASONS = /* @__PURE__ */ new Set([
75
+ RELAY_PUBLIC_REASON.invalidPackage,
76
+ RELAY_PUBLIC_REASON.unsupportedSignalShape,
77
+ RELAY_PUBLIC_REASON.positivePublicDeposit,
78
+ RELAY_PUBLIC_REASON.zkConfigMissing,
79
+ RELAY_PUBLIC_REASON.zkConfigDeprecated,
80
+ RELAY_PUBLIC_REASON.zkConfigIncompatible,
81
+ RELAY_PUBLIC_REASON.invalidProof,
82
+ RELAY_PUBLIC_REASON.kytRejected,
83
+ RELAY_PUBLIC_REASON.kytAuthorizationMismatch,
84
+ RELAY_PUBLIC_REASON.escrowRelayUnconfigured,
85
+ RELAY_PUBLIC_REASON.escrowAuthorizationMismatch,
86
+ RELAY_PUBLIC_REASON.nullifiersSpent,
87
+ RELAY_PUBLIC_REASON.simulationFailed,
88
+ RELAY_PUBLIC_REASON.resourceFeeExceeded,
89
+ RELAY_PUBLIC_REASON.resourceLimitExceeded
90
+ ]);
91
+ var RETRYABLE_FAILURE_REASONS = /* @__PURE__ */ new Set([
92
+ RELAY_PUBLIC_REASON.sendFailed,
93
+ RELAY_PUBLIC_REASON.transactionFailed,
94
+ RELAY_PUBLIC_REASON.infrastructureFailed
95
+ ]);
96
+ var RELAY_INFRASTRUCTURE_HTTP_REASONS = /* @__PURE__ */ new Set([
97
+ RELAY_HTTP_REASON.rateLimited,
98
+ RELAY_HTTP_REASON.queueAtCapacity,
99
+ RELAY_HTTP_REASON.lowBalance,
100
+ RELAY_HTTP_REASON.relayerUnavailable,
101
+ RELAY_HTTP_REASON.payloadTooLarge
102
+ ]);
103
+ function isRetryableFailureReason(reason) {
104
+ if (!reason) {
105
+ return false;
106
+ }
107
+ return RETRYABLE_FAILURE_REASONS.has(reason);
108
+ }
109
+ function isRejectionReason(reason) {
110
+ return REJECTION_REASONS.has(reason);
111
+ }
112
+
113
+ // src/relay-api-error.ts
114
+ var RelayApiError = class extends Error {
115
+ reason;
116
+ httpStatus;
117
+ constructor(input) {
118
+ super(input.reason);
119
+ this.name = "RelayApiError";
120
+ this.reason = input.reason;
121
+ this.httpStatus = input.httpStatus;
122
+ }
123
+ };
124
+ function isInfrastructureRelayFailure(error) {
125
+ return RELAY_INFRASTRUCTURE_HTTP_REASONS.has(error.reason);
126
+ }
127
+ function isRelayApiError(error) {
128
+ return error instanceof RelayApiError;
129
+ }
130
+
131
+ // src/relay-api.ts
132
+ var HTTP_OK = 200;
133
+ var HTTP_ACCEPTED = 202;
134
+ function asRecord(value) {
135
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
136
+ return value;
137
+ }
138
+ return {};
139
+ }
140
+ function readReason(payload, fallback) {
141
+ const record = asRecord(payload);
142
+ return typeof record.reason === "string" && record.reason.trim() ? record.reason : fallback;
143
+ }
144
+ async function parseJson(response) {
145
+ try {
146
+ return await response.json();
147
+ } catch {
148
+ return {};
149
+ }
150
+ }
151
+ async function requestJson(input) {
152
+ const init = {
153
+ method: input.method,
154
+ headers: { Accept: "application/json" }
155
+ };
156
+ if (input.body) {
157
+ init.headers = {
158
+ Accept: "application/json",
159
+ "Content-Type": "application/json"
160
+ };
161
+ init.body = JSON.stringify(input.body);
162
+ }
163
+ const response = await input.fetch(input.url, init);
164
+ const payload = await parseJson(response);
165
+ return { httpStatus: response.status, payload };
166
+ }
167
+ function requireOkStatus(input) {
168
+ if (input.allowed.includes(input.httpStatus)) {
169
+ return asRecord(input.payload);
170
+ }
171
+ throw new RelayApiError({
172
+ reason: readReason(input.payload, RELAY_PUBLIC_REASON.infrastructureFailed),
173
+ httpStatus: input.httpStatus
174
+ });
175
+ }
176
+ function mapAccepted(record) {
177
+ return {
178
+ relayRequestId: String(record.relayRequestId ?? ""),
179
+ status: String(record.status ?? "accepted"),
180
+ createdAt: String(record.createdAt ?? ""),
181
+ statusUrl: String(record.statusUrl ?? "")
182
+ };
183
+ }
184
+ function mapStatus(record) {
185
+ const status = {
186
+ relayRequestId: String(record.relayRequestId ?? ""),
187
+ status: String(record.status ?? ""),
188
+ createdAt: String(record.createdAt ?? ""),
189
+ updatedAt: String(record.updatedAt ?? ""),
190
+ retryAllowed: record.retryAllowed === true
191
+ };
192
+ if (typeof record.attemptNumber === "number") {
193
+ status.attemptNumber = record.attemptNumber;
194
+ }
195
+ if (typeof record.transactionHash === "string") {
196
+ status.transactionHash = record.transactionHash;
197
+ }
198
+ if (typeof record.publicReason === "string") {
199
+ status.publicReason = record.publicReason;
200
+ }
201
+ return status;
202
+ }
203
+ function resolveFetcher(input) {
204
+ return input.fetch ?? globalThis.fetch.bind(globalThis);
205
+ }
206
+ function createRelayApi(input) {
207
+ const origin = requireRelayOrigin(input.origin);
208
+ const fetchImpl = resolveFetcher(input);
209
+ return {
210
+ async createRequest(body) {
211
+ const result = await requestJson({
212
+ fetch: fetchImpl,
213
+ url: `${origin}/relay-requests`,
214
+ method: "POST",
215
+ body
216
+ });
217
+ return mapAccepted(
218
+ requireOkStatus({
219
+ httpStatus: result.httpStatus,
220
+ payload: result.payload,
221
+ allowed: [HTTP_ACCEPTED]
222
+ })
223
+ );
224
+ },
225
+ async readStatus(relayRequestId) {
226
+ const result = await requestJson({
227
+ fetch: fetchImpl,
228
+ url: `${origin}/relay-requests/${relayRequestId}`,
229
+ method: "GET"
230
+ });
231
+ return mapStatus(
232
+ requireOkStatus({
233
+ httpStatus: result.httpStatus,
234
+ payload: result.payload,
235
+ allowed: [HTTP_OK]
236
+ })
237
+ );
238
+ },
239
+ async retryAttempt(relayRequestId) {
240
+ const result = await requestJson({
241
+ fetch: fetchImpl,
242
+ url: `${origin}/relay-requests/${relayRequestId}/attempts`,
243
+ method: "POST"
244
+ });
245
+ return mapStatus(
246
+ requireOkStatus({
247
+ httpStatus: result.httpStatus,
248
+ payload: result.payload,
249
+ allowed: [HTTP_OK]
250
+ })
251
+ );
252
+ }
253
+ };
254
+ }
255
+
256
+ // src/types.ts
257
+ var SUBMISSION_PATH = {
258
+ direct: "direct",
259
+ relay: "relay"
260
+ };
261
+ var RELAY_STATUS = {
262
+ accepted: "accepted",
263
+ validating: "validating",
264
+ queued: "queued",
265
+ submitted: "submitted",
266
+ reconciling: "reconciling",
267
+ succeeded: "succeeded",
268
+ rejected: "rejected",
269
+ failed: "failed"
270
+ };
271
+ var PENDING_OPERATION_PHASE = {
272
+ prepared: "prepared",
273
+ relayAccepted: "relay_accepted",
274
+ admissionFailed: "admission_failed",
275
+ succeeded: "succeeded",
276
+ rejected: "rejected",
277
+ failed: "failed",
278
+ settlementTimedOut: "settlement_timed_out"
279
+ };
280
+
281
+ // src/fallback-policy.ts
282
+ var BLOCKED_DIRECT_STATUSES = /* @__PURE__ */ new Set([
283
+ RELAY_STATUS.submitted,
284
+ RELAY_STATUS.reconciling,
285
+ RELAY_STATUS.rejected,
286
+ RELAY_STATUS.succeeded
287
+ ]);
288
+ function canOfferDirectSubmission(operation) {
289
+ if (operation.escrowSend) {
290
+ return false;
291
+ }
292
+ if (operation.finalized) {
293
+ return false;
294
+ }
295
+ if (operation.relayStatus && BLOCKED_DIRECT_STATUSES.has(operation.relayStatus)) {
296
+ return false;
297
+ }
298
+ if (operation.phase === PENDING_OPERATION_PHASE.rejected) {
299
+ return false;
300
+ }
301
+ if (operation.phase === PENDING_OPERATION_PHASE.failed) {
302
+ return false;
303
+ }
304
+ if (operation.relayRequestId) {
305
+ return false;
306
+ }
307
+ return operation.phase === PENDING_OPERATION_PHASE.prepared || operation.phase === PENDING_OPERATION_PHASE.admissionFailed;
308
+ }
309
+ function canRetryRelayAttempt(operation) {
310
+ if (operation.finalized || !operation.relayRequestId) {
311
+ return false;
312
+ }
313
+ return operation.phase === PENDING_OPERATION_PHASE.failed && operation.retryAllowed;
314
+ }
315
+
316
+ // src/is-unfinalized-relay-operation.ts
317
+ function isUnfinalizedRelayOperation(operation) {
318
+ return Boolean(operation.relayRequestId) && !operation.finalized;
319
+ }
320
+
321
+ // src/complete-succeeded-operation.ts
322
+ var inFlightCompletions = /* @__PURE__ */ new Map();
323
+ function toSubmitResult(operation, extras) {
324
+ const result = {
325
+ outcome: operation.phase,
326
+ operation,
327
+ fallbackAllowed: canOfferDirectSubmission(operation),
328
+ retryAllowed: canRetryRelayAttempt(operation)
329
+ };
330
+ if (extras?.txId) {
331
+ result.txId = extras.txId;
332
+ }
333
+ if (extras?.coinDeliveryWarning) {
334
+ result.coinDeliveryWarning = extras.coinDeliveryWarning;
335
+ }
336
+ return result;
337
+ }
338
+ async function requireStoredOperation(input) {
339
+ const operation = await input.ports.store.read({
340
+ walletPublicKey: input.walletPublicKey,
341
+ operationId: input.operationId
342
+ });
343
+ if (!operation) {
344
+ throw new Error("Pending private operation was not found.");
345
+ }
346
+ return operation;
347
+ }
348
+ function successKey(operation, txId) {
349
+ return `${operation.walletPublicKey}:${operation.id}:${txId}`;
350
+ }
351
+ function withSucceededPhase(operation, txId) {
352
+ return {
353
+ ...operation,
354
+ phase: PENDING_OPERATION_PHASE.succeeded,
355
+ relayStatus: "succeeded",
356
+ transactionHash: txId,
357
+ retryAllowed: false
358
+ };
359
+ }
360
+ function isAlreadyCompleted(operation) {
361
+ return operation.finalized && operation.deliveriesDrained && operation.transactionPersisted;
362
+ }
363
+ async function applySuccessSideEffects(input) {
364
+ let current = input.operation;
365
+ let warning;
366
+ if (!current.finalized) {
367
+ await input.ports.finalizeLocalState({
368
+ operation: current,
369
+ txId: input.txId
370
+ });
371
+ current = { ...current, finalized: true };
372
+ await input.ports.store.save(current);
373
+ }
374
+ if (!current.deliveriesDrained) {
375
+ warning = await input.ports.drainDeliveries({
376
+ operation: current,
377
+ txId: input.txId
378
+ });
379
+ current = { ...current, deliveriesDrained: true };
380
+ await input.ports.store.save(current);
381
+ }
382
+ if (!current.transactionPersisted) {
383
+ input.ports.persistUserTransaction({
384
+ operation: current,
385
+ txId: input.txId
386
+ });
387
+ current = { ...current, transactionPersisted: true };
388
+ await input.ports.store.save(current);
389
+ }
390
+ return warning === void 0 ? { operation: current } : { operation: current, warning };
391
+ }
392
+ async function applySucceededCompletion(input) {
393
+ const applied = await applySuccessSideEffects(input);
394
+ await input.ports.store.save(applied.operation);
395
+ return toSubmitResult(applied.operation, {
396
+ txId: input.txId,
397
+ ...applied.warning ? { coinDeliveryWarning: applied.warning } : {}
398
+ });
399
+ }
400
+ async function completeSucceededOperation(input) {
401
+ const stored = await input.ports.store.read({
402
+ walletPublicKey: input.operation.walletPublicKey,
403
+ operationId: input.operation.id
404
+ });
405
+ const succeeded = withSucceededPhase(stored ?? input.operation, input.txId);
406
+ const key = successKey(succeeded, input.txId);
407
+ if (isAlreadyCompleted(succeeded)) {
408
+ await input.ports.store.save(succeeded);
409
+ return toSubmitResult(succeeded, { txId: input.txId });
410
+ }
411
+ const existing = inFlightCompletions.get(key);
412
+ if (existing) {
413
+ return existing;
414
+ }
415
+ const completion = applySucceededCompletion({
416
+ ports: input.ports,
417
+ operation: succeeded,
418
+ txId: input.txId
419
+ });
420
+ inFlightCompletions.set(key, completion);
421
+ try {
422
+ return await completion;
423
+ } finally {
424
+ inFlightCompletions.delete(key);
425
+ }
426
+ }
427
+
428
+ // src/submit-prepared-private-operation.ts
429
+ function buildPreparedOperation(input) {
430
+ return {
431
+ id: input.id,
432
+ walletPublicKey: input.walletPublicKey,
433
+ phase: PENDING_OPERATION_PHASE.prepared,
434
+ retryAllowed: false,
435
+ display: input.display,
436
+ snapshot: input.snapshot,
437
+ deliveryOutbox: input.deliveryOutbox,
438
+ relayPackage: input.relayPackage,
439
+ finalized: false,
440
+ deliveriesDrained: false,
441
+ transactionPersisted: false,
442
+ ...input.escrowSend ? { escrowSend: true } : {}
443
+ };
444
+ }
445
+ async function submitDirectPath(input) {
446
+ const receipt = await input.ports.submitDirect(input.operation);
447
+ return completeSucceededOperation({
448
+ ports: input.ports,
449
+ operation: input.operation,
450
+ txId: receipt.txId
451
+ });
452
+ }
453
+ async function saveAdmissionFailure(input) {
454
+ const failed = {
455
+ ...input.operation,
456
+ phase: PENDING_OPERATION_PHASE.admissionFailed,
457
+ publicReason: input.reason,
458
+ retryAllowed: false
459
+ };
460
+ await input.ports.store.save(failed);
461
+ return toSubmitResult(failed);
462
+ }
463
+ async function saveRejectedWithoutRequest(input) {
464
+ const rejected = {
465
+ ...input.operation,
466
+ phase: PENDING_OPERATION_PHASE.rejected,
467
+ publicReason: input.reason,
468
+ retryAllowed: false
469
+ };
470
+ await input.ports.store.save(rejected);
471
+ return toSubmitResult(rejected);
472
+ }
473
+ async function createRelayRequest(input) {
474
+ const relayApi = requireRelayApi(input.ports);
475
+ await input.ports.store.save(input.operation);
476
+ try {
477
+ const accepted = await relayApi.createRequest(input.operation.relayPackage);
478
+ const stored = {
479
+ ...input.operation,
480
+ phase: PENDING_OPERATION_PHASE.relayAccepted,
481
+ relayRequestId: accepted.relayRequestId,
482
+ relayStatus: RELAY_STATUS.accepted,
483
+ retryAllowed: false
484
+ };
485
+ await input.ports.store.save(stored);
486
+ return toSubmitResult(stored);
487
+ } catch (error) {
488
+ const reason = error instanceof RelayApiError ? error.reason : RELAY_PUBLIC_REASON.infrastructureFailed;
489
+ if (error instanceof RelayApiError && !isInfrastructureRelayFailure(error)) {
490
+ return saveRejectedWithoutRequest({
491
+ ports: input.ports,
492
+ operation: input.operation,
493
+ reason
494
+ });
495
+ }
496
+ return saveAdmissionFailure({
497
+ ports: input.ports,
498
+ operation: input.operation,
499
+ reason
500
+ });
501
+ }
502
+ }
503
+ function shouldSubmitDirect(input) {
504
+ if (input.operation.escrowSend) {
505
+ return false;
506
+ }
507
+ return input.operation.submissionPath === SUBMISSION_PATH.direct || !isRelayConfigured(input.ports.relayConfig);
508
+ }
509
+ async function submitPreparedPrivateOperation(input) {
510
+ const prepared = buildPreparedOperation(input.operation);
511
+ if (input.operation.escrowSend && !isRelayConfigured(input.ports.relayConfig)) {
512
+ return saveRejectedWithoutRequest({
513
+ ports: input.ports,
514
+ operation: prepared,
515
+ reason: RELAY_PUBLIC_REASON.escrowRelayUnconfigured
516
+ });
517
+ }
518
+ if (shouldSubmitDirect(input)) {
519
+ return submitDirectPath({
520
+ ports: input.ports,
521
+ operation: prepared
522
+ });
523
+ }
524
+ return createRelayRequest({
525
+ ports: input.ports,
526
+ operation: prepared
527
+ });
528
+ }
529
+
530
+ // src/poll-pending-operation.ts
531
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
532
+ RELAY_STATUS.succeeded,
533
+ RELAY_STATUS.rejected,
534
+ RELAY_STATUS.failed
535
+ ]);
536
+ function isRelayStatus(value) {
537
+ return Object.values(RELAY_STATUS).includes(value);
538
+ }
539
+ function applyStatus(operation, status) {
540
+ const next = {
541
+ ...operation,
542
+ retryAllowed: status.retryAllowed
543
+ };
544
+ if (isRelayStatus(status.status)) {
545
+ next.relayStatus = status.status;
546
+ }
547
+ if (status.publicReason) {
548
+ next.publicReason = status.publicReason;
549
+ }
550
+ if (status.transactionHash) {
551
+ next.transactionHash = status.transactionHash;
552
+ }
553
+ if (status.status === RELAY_STATUS.rejected) {
554
+ next.phase = PENDING_OPERATION_PHASE.rejected;
555
+ next.retryAllowed = false;
556
+ }
557
+ if (status.status === RELAY_STATUS.failed) {
558
+ next.phase = PENDING_OPERATION_PHASE.failed;
559
+ }
560
+ return next;
561
+ }
562
+ async function pollPendingOperation(input) {
563
+ const operation = await requireStoredOperation(input);
564
+ if (!operation.relayRequestId) {
565
+ return toSubmitResult(operation);
566
+ }
567
+ const status = await requireRelayApi(input.ports).readStatus(
568
+ operation.relayRequestId
569
+ );
570
+ const updated = applyStatus(operation, status);
571
+ await input.ports.store.save(updated);
572
+ if (status.status === RELAY_STATUS.succeeded && status.transactionHash) {
573
+ return completeSucceededOperation({
574
+ ports: input.ports,
575
+ operation: updated,
576
+ txId: status.transactionHash
577
+ });
578
+ }
579
+ return toSubmitResult(updated, {
580
+ ...updated.transactionHash ? { txId: updated.transactionHash } : {}
581
+ });
582
+ }
583
+ function isTerminalRelayStatus(status) {
584
+ return Boolean(status && TERMINAL_STATUSES.has(status));
585
+ }
586
+
587
+ // src/retry-and-resume.ts
588
+ function withoutPublicReason(operation) {
589
+ const next = { ...operation };
590
+ delete next.publicReason;
591
+ return next;
592
+ }
593
+ async function retryFailedRelayAttempt(input) {
594
+ const operation = await requireStoredOperation(input);
595
+ if (!canRetryRelayAttempt(operation) || !operation.relayRequestId) {
596
+ throw new Error("Relay retry is not allowed for this operation.");
597
+ }
598
+ await requireRelayApi(input.ports).retryAttempt(operation.relayRequestId);
599
+ await input.ports.store.save({
600
+ ...withoutPublicReason(operation),
601
+ phase: PENDING_OPERATION_PHASE.relayAccepted,
602
+ relayStatus: RELAY_STATUS.accepted,
603
+ retryAllowed: false
604
+ });
605
+ return pollPendingOperation(input);
606
+ }
607
+ async function resumePendingOperations(input) {
608
+ const listed = await input.ports.store.list(input.walletPublicKey);
609
+ const pending = listed.filter((operation) => isUnfinalizedRelayOperation(operation));
610
+ const results = [];
611
+ for (const operation of pending) {
612
+ results.push(
613
+ await pollPendingOperation({
614
+ ports: input.ports,
615
+ walletPublicKey: input.walletPublicKey,
616
+ operationId: operation.id
617
+ })
618
+ );
619
+ }
620
+ return results;
621
+ }
622
+
623
+ // src/submit-direct-fallback.ts
624
+ async function submitDirectFallback(input) {
625
+ const operation = await requireStoredOperation(input);
626
+ if (!input.consent || !canOfferDirectSubmission(operation)) {
627
+ throw new Error("Direct submission fallback is not allowed.");
628
+ }
629
+ const receipt = await input.ports.submitDirect(operation);
630
+ return completeSucceededOperation({
631
+ ports: input.ports,
632
+ operation,
633
+ txId: receipt.txId
634
+ });
635
+ }
636
+
637
+ // src/await-relay-settlement.ts
638
+ var DEFAULT_SETTLEMENT_POLL_INTERVAL_MS = 2e3;
639
+ var DEFAULT_SETTLEMENT_MAX_ATTEMPTS = 15;
640
+ var DEFAULT_SETTLEMENT_BACKOFF_MULTIPLIER = 2;
641
+ var DEFAULT_SETTLEMENT_MAX_INTERVAL_MS = 3e4;
642
+ function delay(ms) {
643
+ return new Promise((resolve) => {
644
+ setTimeout(resolve, ms);
645
+ });
646
+ }
647
+ function isSettled(result) {
648
+ return result.outcome === PENDING_OPERATION_PHASE.succeeded || result.outcome === PENDING_OPERATION_PHASE.rejected || result.outcome === PENDING_OPERATION_PHASE.failed || result.outcome === PENDING_OPERATION_PHASE.admissionFailed || result.outcome === PENDING_OPERATION_PHASE.settlementTimedOut || isTerminalRelayStatus(result.operation.relayStatus);
649
+ }
650
+ function nextPollDelayMs(input) {
651
+ return Math.min(
652
+ input.pollIntervalMs * input.backoffMultiplier ** input.attemptIndex,
653
+ input.maxPollIntervalMs
654
+ );
655
+ }
656
+ async function markSettlementTimedOut(input) {
657
+ const timedOut = {
658
+ ...input.operation,
659
+ phase: PENDING_OPERATION_PHASE.settlementTimedOut
660
+ };
661
+ await input.ports.store.save(timedOut);
662
+ return toSubmitResult(timedOut);
663
+ }
664
+ async function pollUntilBound(input) {
665
+ let result = await pollPendingOperation(input);
666
+ let attemptIndex = 0;
667
+ while (!isSettled(result) && attemptIndex + 1 < input.maxAttempts) {
668
+ await input.delay(
669
+ nextPollDelayMs({
670
+ attemptIndex,
671
+ pollIntervalMs: input.pollIntervalMs,
672
+ backoffMultiplier: input.backoffMultiplier,
673
+ maxPollIntervalMs: input.maxPollIntervalMs
674
+ })
675
+ );
676
+ result = await pollPendingOperation(input);
677
+ attemptIndex += 1;
678
+ }
679
+ return result;
680
+ }
681
+ function resolvePollBounds(input) {
682
+ return {
683
+ ports: input.ports,
684
+ walletPublicKey: input.walletPublicKey,
685
+ operationId: input.operationId,
686
+ pollIntervalMs: input.pollIntervalMs ?? DEFAULT_SETTLEMENT_POLL_INTERVAL_MS,
687
+ maxAttempts: input.maxAttempts ?? DEFAULT_SETTLEMENT_MAX_ATTEMPTS,
688
+ backoffMultiplier: input.backoffMultiplier ?? DEFAULT_SETTLEMENT_BACKOFF_MULTIPLIER,
689
+ maxPollIntervalMs: input.maxPollIntervalMs ?? DEFAULT_SETTLEMENT_MAX_INTERVAL_MS,
690
+ delay: input.delay ?? delay
691
+ };
692
+ }
693
+ async function awaitRelaySettlement(input) {
694
+ const result = await pollUntilBound(resolvePollBounds(input));
695
+ if (isSettled(result)) {
696
+ return result;
697
+ }
698
+ return markSettlementTimedOut({
699
+ ports: input.ports,
700
+ operation: result.operation
701
+ });
702
+ }
703
+ async function submitAndAwaitPrivateOperation(input) {
704
+ const submitted = await submitPreparedPrivateOperation(input);
705
+ if (submitted.outcome !== PENDING_OPERATION_PHASE.relayAccepted) {
706
+ return submitted;
707
+ }
708
+ return awaitRelaySettlement({
709
+ ports: input.ports,
710
+ walletPublicKey: input.operation.walletPublicKey,
711
+ operationId: input.operation.id,
712
+ ...input.pollIntervalMs === void 0 ? {} : { pollIntervalMs: input.pollIntervalMs },
713
+ ...input.maxAttempts === void 0 ? {} : { maxAttempts: input.maxAttempts }
714
+ });
715
+ }
716
+
717
+ // src/protocol-relay-client-error.ts
718
+ var ProtocolRelayClientError = class extends Error {
719
+ outcome;
720
+ fallbackAllowed;
721
+ retryAllowed;
722
+ operationId;
723
+ publicReason;
724
+ constructor(result) {
725
+ super(result.operation.publicReason ?? result.outcome);
726
+ this.name = "ProtocolRelayClientError";
727
+ this.outcome = result.outcome;
728
+ this.fallbackAllowed = result.fallbackAllowed;
729
+ this.retryAllowed = result.retryAllowed;
730
+ this.operationId = result.operation.id;
731
+ if (result.operation.publicReason) {
732
+ this.publicReason = result.operation.publicReason;
733
+ }
734
+ }
735
+ };
736
+ function throwIfRelayUnsuccessful(result) {
737
+ if (result.outcome === PENDING_OPERATION_PHASE.succeeded && result.txId) {
738
+ return {
739
+ txId: result.txId,
740
+ ...result.coinDeliveryWarning ? { coinDeliveryWarning: result.coinDeliveryWarning } : {}
741
+ };
742
+ }
743
+ throw new ProtocolRelayClientError(result);
744
+ }
745
+
746
+ // src/serialize.ts
747
+ var RELAY_PACKAGE_VERSION_V1 = 1;
748
+ function nonceToString(value) {
749
+ return typeof value === "string" ? value : value.toString();
750
+ }
751
+ function copyKeyVersionHints(hints) {
752
+ return hints.map((value) => typeof value === "number" ? value : void 0);
753
+ }
754
+ function serializeRelayPackage(source) {
755
+ const body = {
756
+ version: source.version,
757
+ poolSelector: source.poolSelector,
758
+ zkConfigNonce: nonceToString(source.zkConfigNonce),
759
+ proofBytes: source.proofBytes,
760
+ publicSignals: source.publicSignals,
761
+ applicationIdHints: [...source.applicationIdHints]
762
+ };
763
+ if (source.ciphertextBytes !== void 0) {
764
+ body.ciphertextBytes = source.ciphertextBytes;
765
+ }
766
+ if (source.outputNoteEphemeralScalars !== void 0) {
767
+ body.outputNoteEphemeralScalars = [...source.outputNoteEphemeralScalars];
768
+ }
769
+ if (source.escrowAuthorization !== void 0) {
770
+ body.escrowAuthorization = source.escrowAuthorization;
771
+ }
772
+ if (source.keyVersionHints) {
773
+ body.keyVersionHints = copyKeyVersionHints(source.keyVersionHints);
774
+ }
775
+ return body;
776
+ }
777
+ function readNonce(value) {
778
+ if (typeof value === "string" || typeof value === "number") {
779
+ return value.toString();
780
+ }
781
+ if (typeof value === "bigint") {
782
+ return value.toString();
783
+ }
784
+ return void 0;
785
+ }
786
+ function readHints(value) {
787
+ if (!Array.isArray(value)) {
788
+ return void 0;
789
+ }
790
+ return value.map((entry) => typeof entry === "number" ? entry : void 0);
791
+ }
792
+ function readStringArray(value) {
793
+ if (!Array.isArray(value) || value.length === 0) {
794
+ return void 0;
795
+ }
796
+ if (!value.every((entry) => typeof entry === "string")) {
797
+ return void 0;
798
+ }
799
+ return value;
800
+ }
801
+ function readApplicationIdHints(value) {
802
+ return readStringArray(value);
803
+ }
804
+ function readWireRecord(payload) {
805
+ if (typeof payload !== "object" || payload === null) {
806
+ return void 0;
807
+ }
808
+ return payload;
809
+ }
810
+ function deserializeRelayPackage(payload) {
811
+ const record = readWireRecord(payload);
812
+ if (!record || record.version !== RELAY_PACKAGE_VERSION_V1 || typeof record.proofBytes !== "string") {
813
+ return void 0;
814
+ }
815
+ if (typeof record.poolSelector !== "string" || typeof record.publicSignals !== "string") {
816
+ return void 0;
817
+ }
818
+ const zkConfigNonce = readNonce(record.zkConfigNonce);
819
+ const applicationIdHints = readApplicationIdHints(record.applicationIdHints);
820
+ if (!zkConfigNonce || !applicationIdHints) {
821
+ return void 0;
822
+ }
823
+ const body = {
824
+ version: RELAY_PACKAGE_VERSION_V1,
825
+ poolSelector: record.poolSelector,
826
+ zkConfigNonce,
827
+ proofBytes: record.proofBytes,
828
+ publicSignals: record.publicSignals,
829
+ applicationIdHints
830
+ };
831
+ if (typeof record.ciphertextBytes === "string") {
832
+ body.ciphertextBytes = record.ciphertextBytes;
833
+ }
834
+ const outputNoteEphemeralScalars = readStringArray(record.outputNoteEphemeralScalars);
835
+ if (outputNoteEphemeralScalars) {
836
+ body.outputNoteEphemeralScalars = outputNoteEphemeralScalars;
837
+ }
838
+ if (typeof record.escrowAuthorization === "string") {
839
+ body.escrowAuthorization = record.escrowAuthorization;
840
+ }
841
+ const keyVersionHints = readHints(record.keyVersionHints);
842
+ if (keyVersionHints) {
843
+ body.keyVersionHints = keyVersionHints;
844
+ }
845
+ return body;
846
+ }
847
+ export {
848
+ DEFAULT_SETTLEMENT_MAX_ATTEMPTS,
849
+ DEFAULT_SETTLEMENT_POLL_INTERVAL_MS,
850
+ PENDING_OPERATION_PHASE,
851
+ ProtocolRelayClientError,
852
+ RELAY_HTTP_REASON,
853
+ RELAY_PACKAGE_VERSION_V1,
854
+ RELAY_PUBLIC_REASON,
855
+ RELAY_STATUS,
856
+ RelayApiError,
857
+ SUBMISSION_PATH,
858
+ awaitRelaySettlement,
859
+ canOfferDirectSubmission,
860
+ canRetryRelayAttempt,
861
+ createRelayApi,
862
+ deserializeRelayPackage,
863
+ isInfrastructureRelayFailure,
864
+ isRejectionReason,
865
+ isRelayApiError,
866
+ isRelayConfigured,
867
+ isRetryableFailureReason,
868
+ isTerminalRelayStatus,
869
+ isUnfinalizedRelayOperation,
870
+ jsonSafeClone,
871
+ pollPendingOperation,
872
+ resolveRelayOrigin,
873
+ resumePendingOperations,
874
+ retryFailedRelayAttempt,
875
+ serializeRelayPackage,
876
+ submitAndAwaitPrivateOperation,
877
+ submitDirectFallback,
878
+ submitPreparedPrivateOperation,
879
+ throwIfRelayUnsuccessful
880
+ };
881
+ //# sourceMappingURL=index.js.map