@xochi/client 0.1.1 → 0.1.3
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/CHANGELOG.md +29 -0
- package/README.md +4 -3
- package/dist/cjs/machine/index.d.ts +1 -1
- package/dist/cjs/machine/index.js +2 -1
- package/dist/cjs/machine/machine.d.ts +4 -0
- package/dist/cjs/machine/machine.js +57 -31
- package/dist/cjs/machine/state.d.ts +4 -3
- package/dist/cjs/machine/state.js +1 -1
- package/dist/cjs/shared/index.d.ts +1 -0
- package/dist/cjs/shared/index.js +1 -0
- package/dist/cjs/shared/schemas.js +4 -4
- package/dist/cjs/shared/shieldedPreview.d.ts +841 -0
- package/dist/cjs/shared/shieldedPreview.js +128 -0
- package/dist/cjs/transport/client.d.ts +4 -3
- package/dist/cjs/transport/client.js +27 -22
- package/dist/esm/machine/index.d.ts +1 -1
- package/dist/esm/machine/index.js +1 -1
- package/dist/esm/machine/machine.d.ts +4 -0
- package/dist/esm/machine/machine.js +55 -29
- package/dist/esm/machine/state.d.ts +4 -3
- package/dist/esm/machine/state.js +1 -1
- package/dist/esm/shared/index.d.ts +1 -0
- package/dist/esm/shared/index.js +1 -0
- package/dist/esm/shared/schemas.js +4 -4
- package/dist/esm/shared/shieldedPreview.d.ts +841 -0
- package/dist/esm/shared/shieldedPreview.js +124 -0
- package/dist/esm/transport/client.d.ts +4 -3
- package/dist/esm/transport/client.js +27 -22
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Riddler's shielded terms preview wire (`POST /xochi/shielded/preview`,
|
|
3
|
+
* Riddler#1296): the typed request and response shapes and a decoder.
|
|
4
|
+
*
|
|
5
|
+
* There is NO transport method: the preview needs a `quote`-scoped Riddler
|
|
6
|
+
* credential, so only the Xochi Worker calls it, and the Worker proxy route is
|
|
7
|
+
* a follow-up owned by xochi#635. These shapes exist so that proxy and its
|
|
8
|
+
* consumers decode the solver's answer with the same schema the solver
|
|
9
|
+
* publishes (`packages/spec/wire/openapi.json`, `ShieldedPreviewResponse`).
|
|
10
|
+
*
|
|
11
|
+
* The preview is NOT a signing payload: `preview_digest` is the EIP-712 digest
|
|
12
|
+
* of `ShieldedPreviewV1` under the `Xochi Shielded Preview` domain, which no
|
|
13
|
+
* `ShieldedTermsV1` signature verifies against (`@xochi/spec`
|
|
14
|
+
* `conformance/shielded-terms.json`, PROPOSED). `executable` is always false.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { ethAddress } from "./schemas.js";
|
|
18
|
+
/** 32-byte hex (`0x` + 64). */
|
|
19
|
+
const bytes32 = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
|
|
20
|
+
/**
|
|
21
|
+
* A 32-byte Aztec address or field element. The decoder checks the shape only;
|
|
22
|
+
* the solver has already refused zero and values at or above the BN254 modulus.
|
|
23
|
+
* A 20-byte EVM address never fits.
|
|
24
|
+
*/
|
|
25
|
+
export const aztecValue = bytes32;
|
|
26
|
+
/** Canonical unsigned decimal string (uint256 members). */
|
|
27
|
+
const uintString = z.string().regex(/^(0|[1-9][0-9]*)$/);
|
|
28
|
+
/** Canonical positive base-unit amount. */
|
|
29
|
+
const baseUnits = z.string().regex(/^[1-9][0-9]*$/);
|
|
30
|
+
export const ShieldedNetworkIdentitySchema = z.object({
|
|
31
|
+
aztec_chain_id: z.string().regex(/^[1-9][0-9]*$/),
|
|
32
|
+
protocol_version: z.string().regex(/^[1-9][0-9]*$/),
|
|
33
|
+
rollup_address: ethAddress,
|
|
34
|
+
l1_chain_id: z.string().regex(/^[1-9][0-9]*$/),
|
|
35
|
+
genesis_or_network_digest: bytes32,
|
|
36
|
+
});
|
|
37
|
+
export const ShieldedCorridorSchema = z.object({
|
|
38
|
+
corridor_id: bytes32,
|
|
39
|
+
revision: z.number().int().min(1).max(4294967295),
|
|
40
|
+
manifest_digest: bytes32,
|
|
41
|
+
});
|
|
42
|
+
export const ShieldedFeeBreakdownSchema = z.object({
|
|
43
|
+
gas_floor: uintString,
|
|
44
|
+
solver_fee_bps: z.number().int().min(0).max(10000),
|
|
45
|
+
solver_fee: uintString,
|
|
46
|
+
price_impact_bps: z.number().int().min(0).max(10000),
|
|
47
|
+
price_impact: uintString,
|
|
48
|
+
venue_fee_bps: z.number().int().min(0).max(10000),
|
|
49
|
+
venue_fee: uintString,
|
|
50
|
+
routing_fee_bps: z.number().int().min(0).max(10000),
|
|
51
|
+
routing_fee: uintString,
|
|
52
|
+
total_bps: z.number().int().min(0).max(40000),
|
|
53
|
+
total_fee: uintString,
|
|
54
|
+
});
|
|
55
|
+
/** `ShieldedPreviewV1` in snake_case: the terms minus the per-execution members. */
|
|
56
|
+
export const ShieldedPreviewSchema = z.object({
|
|
57
|
+
protocol_version: z.literal("xochi.shielded.preview.v1"),
|
|
58
|
+
environment: z.string().min(1),
|
|
59
|
+
source: z.object({
|
|
60
|
+
chain_id: z.number().int().positive(),
|
|
61
|
+
token_address: ethAddress,
|
|
62
|
+
wallet: ethAddress,
|
|
63
|
+
collector: ethAddress,
|
|
64
|
+
amount: baseUnits,
|
|
65
|
+
}),
|
|
66
|
+
destination: z.object({
|
|
67
|
+
settlement_type: z.literal("shielded"),
|
|
68
|
+
privacy_level: z.literal("private"),
|
|
69
|
+
disclosure_profile: z.literal("recipient-private-public-amount-v1"),
|
|
70
|
+
rail: z.literal("a_tracer"),
|
|
71
|
+
network: ShieldedNetworkIdentitySchema,
|
|
72
|
+
corridor: ShieldedCorridorSchema,
|
|
73
|
+
asset_id: z.string().min(1),
|
|
74
|
+
token_address: aztecValue,
|
|
75
|
+
recipient_binding_id: z.string().min(1),
|
|
76
|
+
recipient_descriptor_digest: bytes32,
|
|
77
|
+
recipient_address: aztecValue,
|
|
78
|
+
amount: baseUnits,
|
|
79
|
+
fee_payer: aztecValue,
|
|
80
|
+
}),
|
|
81
|
+
fee_breakdown: ShieldedFeeBreakdownSchema,
|
|
82
|
+
refund_address: ethAddress,
|
|
83
|
+
refund_amount: baseUnits,
|
|
84
|
+
collect_before: z.number().int().nonnegative(),
|
|
85
|
+
settlement_service_deadline: z.number().int().nonnegative(),
|
|
86
|
+
recipient_binding_digest: bytes32,
|
|
87
|
+
policy_revision: z.number().int().nonnegative(),
|
|
88
|
+
});
|
|
89
|
+
export const ShieldedPreviewResponseSchema = z.object({
|
|
90
|
+
executable: z.literal(false),
|
|
91
|
+
preview_version: z.literal("xochi.shielded.preview.v1"),
|
|
92
|
+
preview_digest: bytes32,
|
|
93
|
+
recipient_binding_digest: bytes32,
|
|
94
|
+
domain: z.object({
|
|
95
|
+
name: z.literal("Xochi Shielded Preview"),
|
|
96
|
+
version: z.literal("1"),
|
|
97
|
+
chain_id: z.number().int().positive(),
|
|
98
|
+
salt: bytes32,
|
|
99
|
+
}),
|
|
100
|
+
preview: ShieldedPreviewSchema,
|
|
101
|
+
manifest: z.object({
|
|
102
|
+
manifest_digest: bytes32,
|
|
103
|
+
aztec_sdk_version: z.string().min(1),
|
|
104
|
+
recipient_account_class_id: aztecValue,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Decode a preview response. Throws (zod) on any shape the solver does not
|
|
109
|
+
* publish, including an executable or terms-shaped answer, and on a fee
|
|
110
|
+
* breakdown whose totals disagree: `total_fee` must be `max(gas_floor, layers)` (the gas
|
|
111
|
+
* floor is a minimum fee, as in FeePolicy) and `total_bps` the four rates.
|
|
112
|
+
*/
|
|
113
|
+
export function decodeShieldedPreview(body) {
|
|
114
|
+
const decoded = ShieldedPreviewResponseSchema.parse(body);
|
|
115
|
+
const f = decoded.preview.fee_breakdown;
|
|
116
|
+
const layers = BigInt(f.solver_fee) + BigInt(f.price_impact) + BigInt(f.venue_fee) + BigInt(f.routing_fee);
|
|
117
|
+
const gasFloor = BigInt(f.gas_floor);
|
|
118
|
+
const totalFee = layers > gasFloor ? layers : gasFloor;
|
|
119
|
+
const totalBps = f.solver_fee_bps + f.price_impact_bps + f.venue_fee_bps + f.routing_fee_bps;
|
|
120
|
+
if (BigInt(f.total_fee) !== totalFee || f.total_bps !== totalBps) {
|
|
121
|
+
throw new Error("shielded preview fee_breakdown totals disagree with max(gas_floor, layers)");
|
|
122
|
+
}
|
|
123
|
+
return decoded;
|
|
124
|
+
}
|
|
@@ -38,7 +38,7 @@ export interface RetryPolicy {
|
|
|
38
38
|
maxAttempts: number;
|
|
39
39
|
/** First exponential backoff step. */
|
|
40
40
|
baseDelayMs: number;
|
|
41
|
-
/** Clamp on
|
|
41
|
+
/** Clamp on client-generated exponential backoff delays. */
|
|
42
42
|
maxDelayMs: number;
|
|
43
43
|
}
|
|
44
44
|
export declare const DEFAULT_TIMEOUTS: TransportTimeouts;
|
|
@@ -119,8 +119,9 @@ export declare function takeApiErrorReadToken(error: unknown): string | undefine
|
|
|
119
119
|
/**
|
|
120
120
|
* Parse a Retry-After header (our API sends integer seconds) into milliseconds,
|
|
121
121
|
* clamped to [0, maxDelayMs] so a malformed or hostile value can't stall the
|
|
122
|
-
* client.
|
|
123
|
-
*
|
|
122
|
+
* client. The default safety ceiling preserves server-directed waits up to
|
|
123
|
+
* MAX_RETRY_WAIT_SEC; a custom ceiling can narrow them. Returns null when
|
|
124
|
+
* absent/unparseable so the caller falls back to its own exponential backoff.
|
|
124
125
|
*/
|
|
125
126
|
export declare function parseRetryAfterMs(headerValue: string | null, maxDelayMs?: number): number | null;
|
|
126
127
|
/**
|
|
@@ -130,11 +130,19 @@ function isNotAcceptedExecuteBody(status, body, intentId) {
|
|
|
130
130
|
* MAX_RETRY_WAIT_SEC.
|
|
131
131
|
*/
|
|
132
132
|
function notAcceptedRetryAt(headers, retryAfter) {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const wait = Number.isFinite(headerDelta) && headerDelta > retryAfter ? headerDelta : retryAfter;
|
|
133
|
+
const headerDelta = retryAfterDeltaSeconds(headers.get("Retry-After"));
|
|
134
|
+
const wait = headerDelta !== null && headerDelta > retryAfter ? headerDelta : retryAfter;
|
|
136
135
|
return Math.ceil((Date.now() + Math.min(wait, MAX_RETRY_WAIT_SEC) * 1000) / 1000);
|
|
137
136
|
}
|
|
137
|
+
/** Retry-After's delta-seconds grammar: optional HTTP whitespace around 1*DIGIT. */
|
|
138
|
+
function retryAfterDeltaSeconds(headerValue) {
|
|
139
|
+
const value = headerValue?.trim();
|
|
140
|
+
if (!value || !/^\d+$/.test(value))
|
|
141
|
+
return null;
|
|
142
|
+
const seconds = Number(value);
|
|
143
|
+
// Syntactically valid integers beyond Number's range still take the 600 s cap.
|
|
144
|
+
return Number.isFinite(seconds) ? seconds : Number.MAX_SAFE_INTEGER;
|
|
145
|
+
}
|
|
138
146
|
/**
|
|
139
147
|
* Read tokens held for the errors `execute` rejected with, keyed by the error
|
|
140
148
|
* so the token is never reachable from the error itself (a crash reporter or a
|
|
@@ -639,14 +647,13 @@ function readTokenFromPrefix(prefix, intentId) {
|
|
|
639
647
|
/**
|
|
640
648
|
* Parse a Retry-After header (our API sends integer seconds) into milliseconds,
|
|
641
649
|
* clamped to [0, maxDelayMs] so a malformed or hostile value can't stall the
|
|
642
|
-
* client.
|
|
643
|
-
*
|
|
650
|
+
* client. The default safety ceiling preserves server-directed waits up to
|
|
651
|
+
* MAX_RETRY_WAIT_SEC; a custom ceiling can narrow them. Returns null when
|
|
652
|
+
* absent/unparseable so the caller falls back to its own exponential backoff.
|
|
644
653
|
*/
|
|
645
|
-
export function parseRetryAfterMs(headerValue, maxDelayMs =
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const seconds = Number(headerValue);
|
|
649
|
-
if (!Number.isFinite(seconds) || seconds < 0)
|
|
654
|
+
export function parseRetryAfterMs(headerValue, maxDelayMs = MAX_RETRY_WAIT_SEC * 1000) {
|
|
655
|
+
const seconds = retryAfterDeltaSeconds(headerValue);
|
|
656
|
+
if (seconds === null)
|
|
650
657
|
return null;
|
|
651
658
|
return Math.min(seconds * 1000, maxDelayMs);
|
|
652
659
|
}
|
|
@@ -694,12 +701,9 @@ export function resolveRetryAfterSeconds(headers, body) {
|
|
|
694
701
|
return Math.min(target, nowSec + MAX_RETRY_WAIT_SEC);
|
|
695
702
|
}
|
|
696
703
|
function deriveRetryTarget(headers, body, nowSec) {
|
|
697
|
-
const
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
if (Number.isFinite(delta) && delta >= 0)
|
|
701
|
-
return nowSec + delta;
|
|
702
|
-
}
|
|
704
|
+
const delta = retryAfterDeltaSeconds(headers.get("Retry-After"));
|
|
705
|
+
if (delta !== null)
|
|
706
|
+
return nowSec + delta;
|
|
703
707
|
const reset = headers.get("X-RateLimit-Reset");
|
|
704
708
|
if (reset !== null) {
|
|
705
709
|
const abs = Number(reset);
|
|
@@ -1006,7 +1010,7 @@ export function createXochiClient(options) {
|
|
|
1006
1010
|
// A 503 is a transient solver blip (saturation / gas-oracle
|
|
1007
1011
|
// unavailable); pace the retry off the server's Retry-After.
|
|
1008
1012
|
if (response.status === 503) {
|
|
1009
|
-
retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After")
|
|
1013
|
+
retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After"));
|
|
1010
1014
|
}
|
|
1011
1015
|
// A cut body does not parse, so it never carries the marker.
|
|
1012
1016
|
const record = truncated ? undefined : parseJsonObject(errorBody);
|
|
@@ -1062,7 +1066,7 @@ export function createXochiClient(options) {
|
|
|
1062
1066
|
if (lastError.name === "AbortError")
|
|
1063
1067
|
break;
|
|
1064
1068
|
if (attempt < maxAttempts - 1) {
|
|
1065
|
-
const delay = retryAfterMs ?? retry.baseDelayMs * Math.pow(2, attempt);
|
|
1069
|
+
const delay = retryAfterMs ?? Math.min(retry.baseDelayMs * Math.pow(2, attempt), retry.maxDelayMs);
|
|
1066
1070
|
// A retry means the request already failed once: that is the context
|
|
1067
1071
|
// for the error below if the retries run out.
|
|
1068
1072
|
log("warn", `[xochi-client] Retry ${attempt + 1}/${maxAttempts} in ${delay}ms`);
|
|
@@ -1163,12 +1167,12 @@ export function createXochiClient(options) {
|
|
|
1163
1167
|
return cached.quote;
|
|
1164
1168
|
}
|
|
1165
1169
|
const pending = pendingQuotes.get(requestHash);
|
|
1166
|
-
if (pending
|
|
1170
|
+
if (pending) {
|
|
1167
1171
|
log("debug", "[xochi-client] Deduplicating quote request");
|
|
1168
|
-
return pending
|
|
1172
|
+
return pending;
|
|
1169
1173
|
}
|
|
1170
1174
|
const promise = fetchQuote(request);
|
|
1171
|
-
pendingQuotes.set(requestHash,
|
|
1175
|
+
pendingQuotes.set(requestHash, promise);
|
|
1172
1176
|
try {
|
|
1173
1177
|
const quote = await promise;
|
|
1174
1178
|
// Cache successful quotes -- EXCEPT pull-bearing ones. A quote carrying a
|
|
@@ -1181,7 +1185,8 @@ export function createXochiClient(options) {
|
|
|
1181
1185
|
return quote;
|
|
1182
1186
|
}
|
|
1183
1187
|
finally {
|
|
1184
|
-
pendingQuotes.
|
|
1188
|
+
if (pendingQuotes.get(requestHash) === promise)
|
|
1189
|
+
pendingQuotes.delete(requestHash);
|
|
1185
1190
|
}
|
|
1186
1191
|
},
|
|
1187
1192
|
invalidateQuote(request) {
|