@xochi/client 0.0.0-stage → 0.1.1

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 (125) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -2
  4. package/dist/cjs/claim.d.ts +33 -0
  5. package/dist/cjs/claim.js +44 -0
  6. package/dist/cjs/fee.d.ts +247 -0
  7. package/dist/cjs/fee.js +295 -0
  8. package/dist/cjs/index.d.ts +18 -0
  9. package/dist/cjs/index.js +33 -0
  10. package/dist/cjs/machine/claimInfo.d.ts +29 -0
  11. package/dist/cjs/machine/claimInfo.js +26 -0
  12. package/dist/cjs/machine/decisions.d.ts +452 -0
  13. package/dist/cjs/machine/decisions.js +665 -0
  14. package/dist/cjs/machine/deposit.d.ts +113 -0
  15. package/dist/cjs/machine/deposit.js +265 -0
  16. package/dist/cjs/machine/index.d.ts +23 -0
  17. package/dist/cjs/machine/index.js +53 -0
  18. package/dist/cjs/machine/machine.d.ts +409 -0
  19. package/dist/cjs/machine/machine.js +1615 -0
  20. package/dist/cjs/machine/phase.d.ts +24 -0
  21. package/dist/cjs/machine/phase.js +45 -0
  22. package/dist/cjs/machine/quoteChecks.d.ts +41 -0
  23. package/dist/cjs/machine/quoteChecks.js +82 -0
  24. package/dist/cjs/machine/receipt.d.ts +151 -0
  25. package/dist/cjs/machine/receipt.js +69 -0
  26. package/dist/cjs/machine/recipientEcho.d.ts +16 -0
  27. package/dist/cjs/machine/recipientEcho.js +27 -0
  28. package/dist/cjs/machine/solverPin.d.ts +110 -0
  29. package/dist/cjs/machine/solverPin.js +214 -0
  30. package/dist/cjs/machine/state.d.ts +167 -0
  31. package/dist/cjs/machine/state.js +31 -0
  32. package/dist/cjs/machine/transition.d.ts +15 -0
  33. package/dist/cjs/machine/transition.js +20 -0
  34. package/dist/cjs/machine/validateRequest.d.ts +15 -0
  35. package/dist/cjs/machine/validateRequest.js +127 -0
  36. package/dist/cjs/machine/view.d.ts +3 -0
  37. package/dist/cjs/machine/view.js +74 -0
  38. package/dist/cjs/machine/wire.d.ts +527 -0
  39. package/dist/cjs/machine/wire.js +280 -0
  40. package/dist/cjs/package.json +1 -0
  41. package/dist/cjs/shared/api.d.ts +9 -0
  42. package/dist/cjs/shared/api.js +5 -0
  43. package/dist/cjs/shared/chains.d.ts +243 -0
  44. package/dist/cjs/shared/chains.js +250 -0
  45. package/dist/cjs/shared/eip712.d.ts +44 -0
  46. package/dist/cjs/shared/eip712.js +64 -0
  47. package/dist/cjs/shared/index.d.ts +15 -0
  48. package/dist/cjs/shared/index.js +31 -0
  49. package/dist/cjs/shared/intentStream.d.ts +74 -0
  50. package/dist/cjs/shared/intentStream.js +59 -0
  51. package/dist/cjs/shared/intents.d.ts +88 -0
  52. package/dist/cjs/shared/intents.js +227 -0
  53. package/dist/cjs/shared/schemas.d.ts +928 -0
  54. package/dist/cjs/shared/schemas.js +524 -0
  55. package/dist/cjs/shared/stealth.d.ts +53 -0
  56. package/dist/cjs/shared/stealth.js +68 -0
  57. package/dist/cjs/shared/trading.d.ts +153 -0
  58. package/dist/cjs/shared/trading.js +185 -0
  59. package/dist/cjs/shared/types.d.ts +37 -0
  60. package/dist/cjs/shared/types.js +7 -0
  61. package/dist/cjs/shared/validators.d.ts +30 -0
  62. package/dist/cjs/shared/validators.js +79 -0
  63. package/dist/cjs/transport/client.d.ts +234 -0
  64. package/dist/cjs/transport/client.js +1456 -0
  65. package/dist/esm/claim.d.ts +33 -0
  66. package/dist/esm/claim.js +41 -0
  67. package/dist/esm/fee.d.ts +247 -0
  68. package/dist/esm/fee.js +285 -0
  69. package/dist/esm/index.d.ts +18 -0
  70. package/dist/esm/index.js +17 -0
  71. package/dist/esm/machine/claimInfo.d.ts +29 -0
  72. package/dist/esm/machine/claimInfo.js +23 -0
  73. package/dist/esm/machine/decisions.d.ts +452 -0
  74. package/dist/esm/machine/decisions.js +640 -0
  75. package/dist/esm/machine/deposit.d.ts +113 -0
  76. package/dist/esm/machine/deposit.js +260 -0
  77. package/dist/esm/machine/index.d.ts +23 -0
  78. package/dist/esm/machine/index.js +23 -0
  79. package/dist/esm/machine/machine.d.ts +409 -0
  80. package/dist/esm/machine/machine.js +1609 -0
  81. package/dist/esm/machine/phase.d.ts +24 -0
  82. package/dist/esm/machine/phase.js +42 -0
  83. package/dist/esm/machine/quoteChecks.d.ts +41 -0
  84. package/dist/esm/machine/quoteChecks.js +78 -0
  85. package/dist/esm/machine/receipt.d.ts +151 -0
  86. package/dist/esm/machine/receipt.js +64 -0
  87. package/dist/esm/machine/recipientEcho.d.ts +16 -0
  88. package/dist/esm/machine/recipientEcho.js +24 -0
  89. package/dist/esm/machine/solverPin.d.ts +110 -0
  90. package/dist/esm/machine/solverPin.js +210 -0
  91. package/dist/esm/machine/state.d.ts +167 -0
  92. package/dist/esm/machine/state.js +28 -0
  93. package/dist/esm/machine/transition.d.ts +15 -0
  94. package/dist/esm/machine/transition.js +17 -0
  95. package/dist/esm/machine/validateRequest.d.ts +15 -0
  96. package/dist/esm/machine/validateRequest.js +123 -0
  97. package/dist/esm/machine/view.d.ts +3 -0
  98. package/dist/esm/machine/view.js +71 -0
  99. package/dist/esm/machine/wire.d.ts +527 -0
  100. package/dist/esm/machine/wire.js +266 -0
  101. package/dist/esm/shared/api.d.ts +9 -0
  102. package/dist/esm/shared/api.js +4 -0
  103. package/dist/esm/shared/chains.d.ts +243 -0
  104. package/dist/esm/shared/chains.js +236 -0
  105. package/dist/esm/shared/eip712.d.ts +44 -0
  106. package/dist/esm/shared/eip712.js +61 -0
  107. package/dist/esm/shared/index.d.ts +15 -0
  108. package/dist/esm/shared/index.js +15 -0
  109. package/dist/esm/shared/intentStream.d.ts +74 -0
  110. package/dist/esm/shared/intentStream.js +55 -0
  111. package/dist/esm/shared/intents.d.ts +88 -0
  112. package/dist/esm/shared/intents.js +213 -0
  113. package/dist/esm/shared/schemas.d.ts +928 -0
  114. package/dist/esm/shared/schemas.js +521 -0
  115. package/dist/esm/shared/stealth.d.ts +53 -0
  116. package/dist/esm/shared/stealth.js +64 -0
  117. package/dist/esm/shared/trading.d.ts +153 -0
  118. package/dist/esm/shared/trading.js +182 -0
  119. package/dist/esm/shared/types.d.ts +37 -0
  120. package/dist/esm/shared/types.js +6 -0
  121. package/dist/esm/shared/validators.d.ts +30 -0
  122. package/dist/esm/shared/validators.js +74 -0
  123. package/dist/esm/transport/client.d.ts +234 -0
  124. package/dist/esm/transport/client.js +1447 -0
  125. package/package.json +61 -4
@@ -0,0 +1,524 @@
1
+ "use strict";
2
+ /**
3
+ * Zod Schemas
4
+ *
5
+ * The Worker's quote and execute RESPONSE wire contract (`/api/intent/quote`,
6
+ * `/api/intent/execute`), with the primitives it closes over. Types are derived
7
+ * from schemas via z.infer -- single source of truth.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.QUOTED_EXPIRY_GRACE_SECS = exports.EXECUTE_RESEND_MARGIN_SECS = exports.MAX_EXECUTE_RESENDS = exports.ExecuteResponseSchema = exports.READ_TOKEN_HEADER = exports.READ_TOKEN_PATTERN = exports.READ_TOKEN_LENGTH = exports.QuoteResponseSchema = exports.FeeDenominationSchema = exports.FeeBreakdownSchema = exports.ClaimInfoSchema = exports.SettlementOptionSchema = exports.Eip712TypedDataSchema = exports.QuotePromoSchema = exports.PROMO_TIER_BETTER_NOTE = exports.PROMO_NOT_APPLIED_NOTE = exports.PROMO_CODE_REGEX = exports.QuoteReferralSchema = exports.REFERRAL_NEUTRAL_NOTE = exports.REFERRAL_NOT_APPLIED_NOTE = exports.REFERRAL_CODE_REGEX = exports.paymentMethod = exports.settlementType = exports.recipientAddress = exports.tronAddress = exports.ethAddress = void 0;
11
+ const zod_1 = require("zod");
12
+ const validators_js_1 = require("./validators.js");
13
+ const intents_js_1 = require("./intents.js");
14
+ // ============================================================
15
+ // Primitives
16
+ // ============================================================
17
+ /** Ethereum address: 0x-prefixed, 42 hex chars. Single source: validators.ts. */
18
+ exports.ethAddress = zod_1.z.string().regex(validators_js_1.ETH_ADDRESS_REGEX);
19
+ /**
20
+ * Tron base58check address: leading `T`, 34 chars, base58 alphabet (no
21
+ * 0/O/I/l). Structural check mirroring `TRON_ADDRESS_REGEX` in validators.ts;
22
+ * kept inline here so the schema module stays free of the viem import chain.
23
+ */
24
+ exports.tronAddress = zod_1.z
25
+ .string()
26
+ .regex(/^T[1-9A-HJ-NP-Za-km-z]{33}$/, "Must be a Tron base58 address");
27
+ /**
28
+ * Recipient address for a non-EVM destination: the user's own address on the
29
+ * destination chain (EVM routes settle to the signing wallet, so it is
30
+ * optional). EVM hex20 or Tron base58.
31
+ */
32
+ exports.recipientAddress = zod_1.z.union([exports.ethAddress, exports.tronAddress]);
33
+ /** Settlement type */
34
+ exports.settlementType = zod_1.z
35
+ .enum(["public", "shielded", "stealth"])
36
+ .describe("Where the payout lands. `public` settles to the wallet address. `stealth` settles to a one-time address on Ethereum L1 (ERC-5564). `shielded` (Aztec L2 notes) is planned and not yet live: it is gated by SHIELDED_SETTLEMENT_LIVE, served as unavailable in `settlement_options`, and refused as a `settlement_preference` while that is off. Read `settlement_options` for live availability rather than assuming this enum's members are all fillable.");
37
+ /**
38
+ * Pull+gas payment method the quote's eip712 authorizes.
39
+ *
40
+ * `deposit` is the odd one out and is not a pull at all: on a non-EVM origin
41
+ * (Tron) the user sends the tokens themselves to a served `deposit_address`,
42
+ * at their own network cost, with no wallet signature anywhere in the flow.
43
+ * It is in this enum because it is what belongs in this FIELD, which answers
44
+ * "how does the origin leg get funded".
45
+ *
46
+ * Without it the wire lies: a Tron-origin quote reports `payment_method:
47
+ * "permit2"` next to `gasless: false` and a `deposit_address`, describing a
48
+ * Permit2 pull on a chain that has no Permit2. The browser papers over it
49
+ * (`resolvePaymentMethod` overrides to `deposit` whenever a deposit address is
50
+ * present), so a human never sees the wrong label; every non-browser consumer
51
+ * does: the MCP server, the OpenAPI contract, and any agent branching on this
52
+ * field to decide whether to prepare a signature.
53
+ */
54
+ exports.paymentMethod = zod_1.z.enum([
55
+ "erc3009",
56
+ "permit2",
57
+ "pimlico",
58
+ "x402",
59
+ "mpp",
60
+ "approval",
61
+ "deposit",
62
+ ]);
63
+ // ============================================================
64
+ // Referral and promo outcomes (quote response siblings of fee_breakdown)
65
+ // ============================================================
66
+ /**
67
+ * A referral code as stored (xochi#583): 3-32 of `A-Z 0-9 _ -`, starting with a
68
+ * letter or digit. Stored and compared uppercase, so a link's `?ref=summer` and
69
+ * `SUMMER` are the same code. Migration 0019 enforces the same shape.
70
+ */
71
+ exports.REFERRAL_CODE_REGEX = /^[A-Z0-9][A-Z0-9_-]{2,31}$/;
72
+ /**
73
+ * The note served when a `ref` was sent and is not applied. ONE text for every
74
+ * reason (unknown, expired, revoked, scoped to another key, a key that does not
75
+ * accept referrals, self-referral): a per-reason note would let a caller
76
+ * enumerate which codes exist.
77
+ */
78
+ exports.REFERRAL_NOT_APPLIED_NOTE = "Referral code not applied.";
79
+ /**
80
+ * The neutral referral line (xochi#603): served on a quote whose `ref` was
81
+ * applied when the request is not signed in as the quoting wallet. One fixed
82
+ * text, identical whether or not the wallet has a referral binding.
83
+ */
84
+ exports.REFERRAL_NEUTRAL_NOTE = "A referrer may earn part of the routing fee on this trade.";
85
+ exports.QuoteReferralSchema = zod_1.z
86
+ .union([
87
+ zod_1.z
88
+ .object({
89
+ code: zod_1.z
90
+ .string()
91
+ .regex(exports.REFERRAL_CODE_REGEX)
92
+ .describe("The code this trade credits, normalized uppercase."),
93
+ name: zod_1.z
94
+ .string()
95
+ .describe("Affiliate display name, rendered as `Referral: {name}` on the routing line."),
96
+ })
97
+ .strict()
98
+ .describe("Served only when the request carries a wallet-bound session (SIWE JWT or Mandate) for the quoting wallet: the code the trade will credit if executed now. The wallet's open referral binding wins over `ref`; otherwise it is the applied `ref`."),
99
+ zod_1.z
100
+ .object({
101
+ neutral: zod_1.z.literal(true),
102
+ note: zod_1.z.literal(exports.REFERRAL_NEUTRAL_NOTE),
103
+ })
104
+ .strict()
105
+ .describe("Served when `ref` was applied but the request is not signed in as the quoting wallet: a referrer may be credited, and who is not said. Identical whether or not the wallet has a referral binding."),
106
+ ])
107
+ .describe("Who earns the routing fee layer (`fee_breakdown.routing_fee`) in place of the publishable key's routing recipient. Attribution only: no amount, rate or total differs from the same quote without `ref`.");
108
+ /**
109
+ * A promo code as stored (xochi#584): 3-32 of `A-Z 0-9 _ -`, starting with a
110
+ * letter or digit. Stored and compared uppercase, so `launch10` and `LAUNCH10`
111
+ * are the same code. Migration 0020 enforces the same shape.
112
+ */
113
+ exports.PROMO_CODE_REGEX = /^[A-Z0-9][A-Z0-9_-]{2,31}$/;
114
+ /**
115
+ * The inline error served when a `promo` was sent and cannot be used. ONE text
116
+ * for every reason (unknown, malformed, expired, revoked, used up, used up by
117
+ * this wallet, not accepted under this publishable key, lookup ceiling, the
118
+ * feature off): a per-reason message would let a caller enumerate which codes
119
+ * exist.
120
+ */
121
+ exports.PROMO_NOT_APPLIED_NOTE = "This code can't be applied.";
122
+ /**
123
+ * The note served when a valid code was sent but the caller's trust tier
124
+ * already discounts at least as much (discount = max(tier, capped promo), never
125
+ * both; an Institutional wallet always lands here). The code is
126
+ * not consumed.
127
+ */
128
+ exports.PROMO_TIER_BETTER_NOTE = "Your trust tier already gives you a fee at least as low as this code.";
129
+ exports.QuotePromoSchema = zod_1.z
130
+ .object({
131
+ code: zod_1.z.string().regex(exports.PROMO_CODE_REGEX).describe("The applied code, normalized uppercase."),
132
+ discount_bps: zod_1.z
133
+ .number()
134
+ .int()
135
+ .positive()
136
+ .describe("Discount this code bought, in bps off the Standard venue + routing layers for the route's asset class. Already reflected in every amount of this quote and itemized as `fee_breakdown.discount`."),
137
+ })
138
+ .strict()
139
+ .describe("Present when the `promo` sent on the quote was applied: it beat the caller's trust-tier discount and priced this quote. One use of the code is counted when this quote is executed, not now; if the code is used up or withdrawn by then, execute refuses with `promo_unavailable` and the caller re-quotes.");
140
+ // ============================================================
141
+ // Response Schemas (worker -> client/agent wire contract)
142
+ // ============================================================
143
+ /**
144
+ * EIP-712 typed data the user signs. Internal keys follow the EIP-712
145
+ * standard (camelCase: domain.chainId, domain.verifyingContract, primaryType)
146
+ * and are part of the signed payload -- they are intentionally NOT snake_cased.
147
+ * Passed through verbatim from the solver. Mirrors the `Eip712TypedData`
148
+ * schema in public/openapi.yaml.
149
+ */
150
+ exports.Eip712TypedDataSchema = zod_1.z.object({
151
+ // Two domain shapes coexist: legacy Xochi-owned structs and token-pull
152
+ // domains carry `verifyingContract`; the Riddler XochiIntent v3 domain carries
153
+ // `salt` (keccak256(version)) and NO verifyingContract. Both fields are
154
+ // optional so either shape validates and both survive the parse verbatim -- a
155
+ // stripped `salt` or an injected `verifyingContract` would change the domain
156
+ // separator and strand the signature.
157
+ domain: zod_1.z.object({
158
+ name: zod_1.z.string(),
159
+ version: zod_1.z.string(),
160
+ chainId: zod_1.z.number().int(),
161
+ verifyingContract: exports.ethAddress.optional(),
162
+ salt: zod_1.z.string().optional(),
163
+ }),
164
+ types: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), type: zod_1.z.string() }))),
165
+ primaryType: zod_1.z.string(),
166
+ message: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
167
+ });
168
+ exports.SettlementOptionSchema = zod_1.z.object({
169
+ type: exports.settlementType,
170
+ available: zod_1.z.boolean(),
171
+ reason: zod_1.z.string().optional(),
172
+ });
173
+ exports.ClaimInfoSchema = zod_1.z.object({
174
+ factory_address: exports.ethAddress,
175
+ salt: zod_1.z.number().int(),
176
+ });
177
+ /**
178
+ * A fee layer AMOUNT in token base units: digits only, no sign, no decimal
179
+ * point, no exponent, no `0x`.
180
+ *
181
+ * A plain `z.string()` would admit `""`, `"0.5"`, `"1.2e3"` and `"-5"`. Every
182
+ * consumer immediately does `BigInt()` on these to reconcile the layers against
183
+ * `total_fee`, and `BigInt` throws on three of those four and silently returns
184
+ * `0n` on the empty string -- so the loose type turns an upstream formatting
185
+ * change into a 500 on every quote for the route (nothing wraps the
186
+ * reconciliation in a try/catch). The producer (Riddler `fee_policy.ex`,
187
+ * `calculate_layered_fee`) emits `Integer.to_string` of a non-negative integer
188
+ * for all five, so this narrowing describes what is actually served rather
189
+ * than restricting it.
190
+ *
191
+ * Deliberately NOT applied to `gas_floor.usd` or `total_usd`: those are dollar
192
+ * figures and are legitimately decimal ("0.022663").
193
+ */
194
+ const feeLayerBaseUnits = zod_1.z
195
+ .string()
196
+ .regex(/^\d+$/)
197
+ .describe("Fee layer amount in token base units (integer, no decimal point). " +
198
+ "Denominated in the token named by the response's `fee_denomination`.");
199
+ /**
200
+ * Itemized fee breakdown (economics.md Part 1). snake_case wire shape the worker
201
+ * emits and the frontend renders against. Layers are three additive bps lines on
202
+ * a never-discounted cost floor (solver + gas), plus venue and routing.
203
+ *
204
+ * The layer AMOUNTS carry no denomination of their own. The token they are
205
+ * measured in is named by `fee_denomination` on the enclosing quote response.
206
+ */
207
+ exports.FeeBreakdownSchema = zod_1.z.object({
208
+ // The SOLVER's view of the caller's tier, under which IT computed the layer
209
+ // amounts below. NOT the authority on the caller's tier: Xochi resolves that
210
+ // from D1 (`user_tiers`) and the client-supplied `trust_score` is never
211
+ // trusted. The two can legitimately disagree for a quote or two after an
212
+ // upgrade, because the solver's copy of the score lags D1's. Render the
213
+ // caller's tier from the Xochi-side value; read this one only as the label
214
+ // that explains these particular numbers.
215
+ tier: zod_1.z
216
+ .enum(["standard", "trusted", "verified", "premium", "institutional"])
217
+ .describe("The SOLVER's view of the tier, and the schedule it priced these layer " +
218
+ "amounts under. Not the authority on the caller's tier -- Xochi resolves " +
219
+ "that from its own records, and the solver's copy of the trust score can " +
220
+ "lag a fresh upgrade by a quote or two. Use it to explain these numbers, " +
221
+ "never to display the user's tier."),
222
+ asset_class: zod_1.z.enum(["stable", "volatile"]),
223
+ gas_floor: zod_1.z.object({ amount: zod_1.z.string(), usd: zod_1.z.string() }),
224
+ solver_fee_bps: zod_1.z.number(),
225
+ solver_fee: feeLayerBaseUnits,
226
+ price_impact_bps: zod_1.z.number(),
227
+ price_impact: feeLayerBaseUnits,
228
+ venue_fee_bps: zod_1.z.number(),
229
+ venue_fee: feeLayerBaseUnits,
230
+ routing_fee_bps: zod_1.z.number(),
231
+ routing_fee: feeLayerBaseUnits,
232
+ total_bps: zod_1.z.number(),
233
+ total_fee: feeLayerBaseUnits,
234
+ total_usd: zod_1.z.string(),
235
+ surplus_share_pct: zod_1.z.number(),
236
+ // Present when a promo code was in play on the quote (xochi#584): which
237
+ // discount priced it and how much it took off the Standard venue + routing
238
+ // layers. Emitted by the solver, which priced it; the worker reads `source`
239
+ // to decide whether the code was applied rather than assuming it was.
240
+ discount: zod_1.z
241
+ .object({
242
+ source: zod_1.z
243
+ .enum(["tier", "promo"])
244
+ .describe("Which discount priced this quote: the trust tier's or the promo code's. They never stack: discount = max(tier, min(promo, Institutional discount))."),
245
+ bps: zod_1.z
246
+ .number()
247
+ .int()
248
+ .nonnegative()
249
+ .describe("Basis points taken off the Standard venue + routing layers for this asset class. Never off the solver spread or the gas floor."),
250
+ amount: feeLayerBaseUnits,
251
+ })
252
+ .strict()
253
+ .optional()
254
+ .describe("The applied discount as a labelled line INSIDE `total_fee`: `venue_fee` and `routing_fee` are already net of it, so it is never subtracted again. Render it as e.g. `Promo LAUNCH10 -0.05%`. Absent when no promo code was sent."),
255
+ });
256
+ /**
257
+ * Which token the `fee_breakdown` layer amounts are measured in. See the
258
+ * `fee_denomination` field on {@link QuoteResponseSchema}.
259
+ */
260
+ exports.FeeDenominationSchema = zod_1.z.enum(["origin", "destination"]);
261
+ /**
262
+ * Canonical quote response: the Xochi worker's `/api/intent/quote` output and
263
+ * the pinned agent contract. snake_case envelope; the `eip712` value is
264
+ * verbatim EIP-712 typed data. When `can_solve` is true, `to_amount`,
265
+ * `payment_method`, and `eip712` are present. Mirrors `QuoteResponse` in
266
+ * public/openapi.yaml -- keep all three in lockstep.
267
+ */
268
+ exports.QuoteResponseSchema = zod_1.z
269
+ .object({
270
+ success: zod_1.z.boolean(),
271
+ intent_id: zod_1.z.string(),
272
+ quote_id: zod_1.z.string(),
273
+ can_solve: zod_1.z.boolean(),
274
+ /** Non-executable price exploration: never persisted and carries no signing material. */
275
+ preview: zod_1.z.boolean().optional(),
276
+ to_amount: zod_1.z.string().optional(),
277
+ min_to_amount: zod_1.z.string().optional(),
278
+ xochi_fee: zod_1.z.string().optional(),
279
+ xochi_fee_rate: zod_1.z.string().optional(),
280
+ cost_bps: zod_1.z
281
+ .number()
282
+ .nonnegative()
283
+ .optional()
284
+ .describe("All-in cost of this quote in basis points, from the same fixed-cost " +
285
+ "guard that refuses an uneconomical order, so the displayed price and " +
286
+ "the refusal threshold cannot disagree. Unlike xochi_fee_rate (a flat " +
287
+ "schedule rate) this moves with route and size: cost is " +
288
+ "max(gas floor of the destination chain, the fee rate), so it rises " +
289
+ "as the order shrinks. BOUNDED BY THE GUARD: an order whose cost " +
290
+ "exceeds the ceiling is declined with error_category " +
291
+ "'amount_too_small' and carries no cost_bps at all, so every served " +
292
+ "value sits at or below the ceiling (150 bps at MAX_COST_RATIO 0.015). " +
293
+ "Do not model a range above it. Omitted when the origin token has no " +
294
+ "dollar value on the worker and the guard could not run -- absent " +
295
+ "means unknown, never zero. Unrounded: below half a basis point the " +
296
+ "distinction between a free fill and a merely cheap one still matters, " +
297
+ "and rounding server-side destroys it."),
298
+ // Itemized layer split; optional until the worker/Riddler emit it.
299
+ fee_breakdown: exports.FeeBreakdownSchema.optional(),
300
+ // Which token `fee_breakdown`'s amounts are measured in. Carried on the wire
301
+ // rather than inferred from the MERE PRESENCE of fee_breakdown, which would
302
+ // rest on an undocumented convention in another repo (Riddler's
303
+ // fee_policy.ex computes every layer off `input_amount`): the xochi#410
304
+ // failure class, a cross-repo fact with no runtime evidence in this one.
305
+ fee_denomination: exports.FeeDenominationSchema.optional().describe("Which token the `fee_breakdown` layer AMOUNTS are denominated in. " +
306
+ "'origin' means `from_token` base units, 'destination' means `to_token` " +
307
+ "base units. Formatting a fee with the wrong token's decimals and symbol " +
308
+ "misstates it by the exchange rate, and on a USDC->USDC route (6 decimals " +
309
+ "both sides) the mistake is invisible -- which is why this is stated on " +
310
+ "the wire instead of inferred from the presence of `fee_breakdown`. " +
311
+ "PROVENANCE: the solver does not emit this field yet, so today Xochi " +
312
+ "asserts it from the solver's documented convention (every layer computed " +
313
+ "off the input amount, hence 'origin') rather than reporting a value the " +
314
+ "solver stated. It is forwarded verbatim once the solver does send one. " +
315
+ "Treat it as Xochi's best statement of the denomination, not as the fee " +
316
+ "computer's own declaration. ABSENT MEANS UNKNOWN, never a default: when " +
317
+ "there is no `fee_breakdown` the field is omitted entirely."),
318
+ // Referral attribution for the routing layer (xochi#583). Xochi-owned and
319
+ // a sibling of `fee_breakdown` rather than inside it: the breakdown is the
320
+ // solver's itemization and may be absent, while the routing line exists on
321
+ // every quote (the client derives one without a breakdown).
322
+ referral: exports.QuoteReferralSchema.optional(),
323
+ referral_note: zod_1.z
324
+ .literal(exports.REFERRAL_NOT_APPLIED_NOTE)
325
+ .optional()
326
+ .describe("Present when a `ref` was sent and not applied. Deliberately one fixed text for every reason (unknown, expired, revoked, not valid for this key, a key that does not accept referrals, self-referral), so the response does not reveal whether a code exists. The quote is otherwise identical to one sent without `ref`."),
327
+ // Promo code outcome (xochi#584). Xochi-owned siblings of `fee_breakdown`:
328
+ // the breakdown itemizes the discount (`fee_breakdown.discount`), these say
329
+ // whether the code the caller sent is the thing that priced it.
330
+ promo: exports.QuotePromoSchema.optional(),
331
+ promo_note: zod_1.z
332
+ .enum([exports.PROMO_NOT_APPLIED_NOTE, exports.PROMO_TIER_BETTER_NOTE])
333
+ .optional()
334
+ .describe("Inline message for a `promo` that did not price this quote. The quote is still served, priced as if no code had been sent. `" +
335
+ exports.PROMO_NOT_APPLIED_NOTE +
336
+ "` is deliberately one fixed text for every reason (unknown, expired, revoked, used up, this wallet's limit reached, not accepted under this publishable key), so the response does not reveal whether a code exists. `" +
337
+ exports.PROMO_TIER_BETTER_NOTE +
338
+ "` means the code is valid but the trust-tier discount is at least as large; it is not consumed."),
339
+ estimated_gas_cost: zod_1.z.string().optional(),
340
+ stream_topic: zod_1.z
341
+ .string()
342
+ .regex(/^[A-Za-z0-9_-]{16,64}$/)
343
+ .optional()
344
+ .describe("Subscribe capability for this intent's live status and fill-race " +
345
+ "stream, at `GET /api/stream/<stream_topic>` (WebSocket upgrade). " +
346
+ "TREAT IT AS A BEARER TOKEN: possession is the whole authorization, " +
347
+ "there is no second check, and the relay replays its recent event " +
348
+ "buffer to every new subscriber. It is served exactly once, in this " +
349
+ "response, and is never returned by a read endpoint. Keep it out of " +
350
+ "logs and out of URLs you share. It is deliberately NOT the " +
351
+ "`intent_id` beside it: that value is public by construction (it " +
352
+ "appears in status paths and in server logs), so a stream keyed on it " +
353
+ "would be readable by anyone who has ever seen one. Absent on an " +
354
+ "intent quoted before this field existed, which means no stream is " +
355
+ "available; fall back to polling `GET /api/intent/:id/status`, which " +
356
+ "is the durable path in every case."),
357
+ expiry: zod_1.z.string(),
358
+ gasless: zod_1.z.boolean().optional(),
359
+ gasless_fee: zod_1.z.string().optional(),
360
+ payment_method: exports.paymentMethod.optional(),
361
+ route: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
362
+ eip712: exports.Eip712TypedDataSchema.optional(),
363
+ // Origin-pull authorization: a second EIP-712 the user signs so the solver
364
+ // can collect origin funds before settling. ReceiveWithAuthorization (USDC
365
+ // domain) for erc3009, PermitWitnessTransferFrom for permit2. Absent for
366
+ // non-pulling methods and the mock path.
367
+ pull_authorization: exports.Eip712TypedDataSchema.optional(),
368
+ // Non-EVM origin (Tron): the per-intent address the user manually sends TRC-20
369
+ // to. Mutually exclusive with pull_authorization (a deposit route has no
370
+ // typed-data pull). Base58check -- never lowercased.
371
+ deposit_address: zod_1.z.string().optional(),
372
+ // Solver attestation binding the deposit_address to this intent. The client
373
+ // verifies the signature against a pinned signer BEFORE displaying the address
374
+ // (fail-closed); an absent/invalid attestation makes the route unavailable.
375
+ deposit_attestation: zod_1.z.string().optional(),
376
+ // Deposit-acceptance deadline (unix seconds) for a deposit route -- decoupled
377
+ // from the shorter quote `expiry` so a manual TRC-20 transfer has time to land.
378
+ // Drives the deposit countdown.
379
+ deposit_deadline: zod_1.z.number().int().optional(),
380
+ // Echo of the requested destination recipient (non-EVM/Tron routes). These
381
+ // routes carry no wallet signature, so recipient integrity rides on this
382
+ // authenticated channel: the client asserts the solvable quote echoes exactly
383
+ // the address it asked for and fail-closes otherwise (#113). Base58 is
384
+ // case-sensitive -- passed through verbatim, never lowercased.
385
+ recipient_address: exports.recipientAddress.optional(),
386
+ settlement_options: zod_1.z.array(exports.SettlementOptionSchema).optional(),
387
+ claim_info: exports.ClaimInfoSchema.optional(),
388
+ // Execute replay-dedup nonce the GET alias injects (absent from the POST
389
+ // envelope) so a third-party ACP agent need not do the pull-nonce bit math
390
+ // itself. Derived from the pull-authorization nonce (see deriveReplayNonce).
391
+ // Optional: the POST omits it, and the GET omits it too on a pull-less route.
392
+ nonce: zod_1.z.number().int().min(0).optional(),
393
+ error: zod_1.z.string().optional(),
394
+ // Present on the unsolvable-200 branch: `error_category` is a stable machine
395
+ // key for the reject class, `detail` the solver's raw reason. Absent on
396
+ // solvable quotes.
397
+ //
398
+ // Enumerated so the generated spec documents the vocabulary agents branch on,
399
+ // and so adding a category without declaring it fails the contract tests.
400
+ // Four are worker-side fail-closed guards; the rest come from
401
+ // `classifyDecline` (decline.ts), whose returns are a closed set of literals
402
+ // with `unknown` as the catch-all.
403
+ // Documented, deliberately NOT enumerated. The worker owns four of these,
404
+ // but the rest come from `classifyDecline` (decline.ts) over solver reasons,
405
+ // and the contract test below asserts an unlisted value still parses -- this
406
+ // side of the vocabulary is intentionally open, so a new upstream reason must
407
+ // not start failing every client that validates.
408
+ error_category: zod_1.z
409
+ .string()
410
+ .optional()
411
+ .describe("Stable machine key for a declined quote. Branch on this rather than on `error`, whose prose may change. Known values: gas_floor_unpriced (transient, retry), origin_pull_amount_mismatch, served_payout_mismatch, settlement_type_unavailable, stealth_factory_untrusted, refund_address_invalid, amount_too_large, amount_too_small, no_liquidity, route_unsupported, invalid_request (the request shape was rejected, not the route -- see `detail` for the offending field), auth, unknown. Treat an unrecognized value as a non-retryable decline."),
412
+ detail: zod_1.z.string().optional(),
413
+ // Present when `error_category` is `amount_too_small` because the route's
414
+ // FIXED settlement cost consumed too much of the order: the smallest order
415
+ // (USD) that clears the threshold at the current cost. Lets a caller retry
416
+ // with a workable amount instead of bisecting for one. Not a protocol
417
+ // constant -- it moves with the corridor's cost, so read it per quote.
418
+ minimum_amount_usd: zod_1.z
419
+ .number()
420
+ .positive()
421
+ .optional()
422
+ .describe("Smallest order in USD you should send on this route. THIS is the number to retry with; it already accounts for both the solver's floor and the fixed-cost threshold, so it is >= `min_fillable_usd`. Present on an `amount_too_small` decline. Varies with the corridor's current cost, so do not cache it."),
423
+ // The solver's own live bounds, passed through on a bounds decline. Both
424
+ // are served, so an undeclared one makes the strict schema reject a
425
+ // legitimate decline and leaves it out of the generated spec (#754).
426
+ min_fillable_usd: zod_1.z
427
+ .number()
428
+ .positive()
429
+ .optional()
430
+ .describe("The solver's live per-corridor floor: the smallest order it will fill at all, which is its own cost to fill. Reported for transparency. Do NOT retry with this figure -- an order at exactly the solver's cost delivers almost nothing. Use `minimum_amount_usd`."),
431
+ max_order_usd: zod_1.z
432
+ .number()
433
+ .positive()
434
+ .optional()
435
+ .describe("The solver's live per-trade ceiling in USD. Present on an `amount_too_large` decline."),
436
+ })
437
+ .strict();
438
+ /**
439
+ * Length of an intent read capability (`read_token` on the execute response,
440
+ * xochi#563): 32 random bytes as unpadded base64url.
441
+ */
442
+ exports.READ_TOKEN_LENGTH = 43;
443
+ /** Shape of an intent read capability: exactly `READ_TOKEN_LENGTH` base64url characters. */
444
+ exports.READ_TOKEN_PATTERN = new RegExp(`^[A-Za-z0-9_-]{${exports.READ_TOKEN_LENGTH}}$`);
445
+ /**
446
+ * The request header that carries a `read_token` on `GET /api/intent/:id` and
447
+ * `GET /api/intent/:id/status`. A header and never a query parameter: the token
448
+ * must not land in URLs, logs or referrers.
449
+ */
450
+ exports.READ_TOKEN_HEADER = "X-Xochi-Read-Token";
451
+ /** Mirrors `ExecuteResponse` in public/openapi.yaml. */
452
+ exports.ExecuteResponseSchema = zod_1.z
453
+ .object({
454
+ success: zod_1.z.boolean(),
455
+ intent_id: zod_1.z.string(),
456
+ status: zod_1.z.string(),
457
+ tx_hash: zod_1.z.string().optional(),
458
+ // Per-leg hashes for cross-chain: the origin/deposit leg is `tx_hash`, the
459
+ // destination fill is `receiving_tx_hash` (arrives later via status/WS).
460
+ bridge_tx_hash: zod_1.z.string().optional(),
461
+ receiving_tx_hash: zod_1.z.string().optional(),
462
+ // Public destination fill receipt (Riddler #632) + its chain, when a same-chain
463
+ // or fast fill resolves inline; usually arrives later via the status poll.
464
+ settlement_tx_hash: zod_1.z.string().optional(),
465
+ settlement_chain_id: zod_1.z.number().int().optional(),
466
+ // In-doubt: the solver may have moved funds but did not confirm. The intent
467
+ // stays non-terminal; surface this so the client shows "still confirming"
468
+ // rather than a success or a failure.
469
+ reconciling: zod_1.z.boolean().optional(),
470
+ note_commitment: zod_1.z.string().optional(),
471
+ stealth_address: zod_1.z.string().optional(),
472
+ ephemeral_pub_key: zod_1.z.string().optional(),
473
+ view_tag: zod_1.z.number().int().optional(),
474
+ error: zod_1.z.string().optional(),
475
+ substatus: zod_1.z
476
+ .literal(intents_js_1.NOT_EXECUTED_SUBSTATUS)
477
+ .optional()
478
+ .describe("`not_executed` on a `failed` where nothing moved, served by the execute 502 of the worker's own finishes (the resend budget's, a not-accepted refusal whose un-claim's write failed, no solver configured, a solver refusal carrying `details.funds_moved: false`) and echoed from the row by the lost-race 502 and the in-doubt 200: safe to re-quote. Absent on any other failure. Every source: docs/contracts/xochi-intent-api.md, \"The worker's not_executed marker\"."),
479
+ // Per-intent read capability (xochi#563): 32 random bytes, base64url, no
480
+ // padding. Served once, by the execute that admits the intent; present it as
481
+ // `X-Xochi-Read-Token` on `GET /api/intent/:id[/status]` to read the owner's
482
+ // view of a stealth intent; a shielded intent's token read gets the
483
+ // status-only view (SPEC.md §5.1). Older servers omit it. The one declared
484
+ // field that never fails the body: a malformed token is dropped, because this
485
+ // body reports an execute that may already have moved funds, and failing it
486
+ // would turn a settled swap into an in-doubt one over a privacy-only field.
487
+ // The describe is xochi's (xochi#565, `0317e84`) verbatim.
488
+ read_token: zod_1.z
489
+ .string()
490
+ .regex(exports.READ_TOKEN_PATTERN)
491
+ .optional()
492
+ .catch(undefined)
493
+ .describe("Per-intent read capability (xochi#563): 32 random bytes, base64url without padding. Returned to the request that claimed the intent for execution, on every answer where funds may have moved: the 200, the 502 of an unmarked solver refusal or failure this request wrote, a 502 whose status another writer decided, and the outcome unknown after the claim. Never on a `not_executed` answer, an un-claim back to `quoted` (the not-accepted 503, the in-doubt 200 with `status: \"quoted\"`) or a retried execute. Store it with the intent id and send it as the `X-Xochi-Read-Token` header on `GET /api/intent/{id}` and `/status` to get the owner's full view of a stealth intent without a wallet session. On a shielded intent it reads only the status-only view (legs_withheld), like an anonymous read; only a SIWE session for the intent's wallet reads that full record. A bearer secret: never log it or put it in a URL. The server stores only its SHA-256, so it cannot be re-issued; signing in with the intent's wallet (SIWE) reads the same view."),
494
+ })
495
+ .strict();
496
+ /**
497
+ * Automatic resends of one execute after its not-accepted answer. Mirrors
498
+ * `MAX_EXECUTE_RESENDS` in xochi's `@xochi/shared` `schemas.ts`, the budget
499
+ * the Worker enforces too: the refusal that follows this many un-claims
500
+ * finishes the intent `failed` instead.
501
+ */
502
+ exports.MAX_EXECUTE_RESENDS = 3;
503
+ /**
504
+ * A resend must reach the Worker at least this many seconds before the
505
+ * earliest deadline the signed request carries. Mirrors
506
+ * `EXECUTE_RESEND_MARGIN_SECS` in xochi's `@xochi/shared` `schemas.ts`, the
507
+ * margin the Worker applies before it un-claims.
508
+ */
509
+ exports.EXECUTE_RESEND_MARGIN_SECS = 5;
510
+ /**
511
+ * Seconds past an unexecuted intent's signed deadline before the Worker ends
512
+ * it: a `quoted` market row (never claimed, or every claim undone) that an
513
+ * intent read (`GET /api/intent/:id` or the status poll, never the stream)
514
+ * finds this far past the `deadline` the user signed is finished `failed`
515
+ * with `NOT_EXECUTED_SUBSTATUS`, and the read serves that. Slack for clock
516
+ * skew between the Worker, the database (whose clock the claim's deadline
517
+ * check uses), the solver and the client. Measured from the signed deadline;
518
+ * the quote sweep's own 60 s is measured from the Worker's 30 s quote expiry
519
+ * and usually ends a touched row first (README "Fund safety"). Mirrors
520
+ * `QUOTED_EXPIRY_GRACE_SECS` in xochi's `@xochi/shared` `schemas.ts`. The
521
+ * machine polls an execute at least this long past the quote's deadline
522
+ * (`unexecutedFinishAt`).
523
+ */
524
+ exports.QUOTED_EXPIRY_GRACE_SECS = 60;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Stealth settlement constants: the SimpleAccount factory + salt a stealth
3
+ * payout is derived against, and the per-chain allowlist that bounds which
4
+ * advertised factory may be followed.
5
+ *
6
+ * @see https://eips.ethereum.org/EIPS/eip-5564
7
+ * @see https://eips.ethereum.org/EIPS/eip-6538
8
+ */
9
+ /**
10
+ * SimpleAccountFactory (v0.8) for the counterfactual stealth destination.
11
+ * Salt is 0: one account per stealth EOA.
12
+ *
13
+ * Canonical home for a security-critical address the client and the worker
14
+ * both need; never declare a second copy.
15
+ */
16
+ export declare const SIMPLE_ACCOUNT_FACTORY = "0x13E9ed32155810FDbd067D4522C492D6f68E5944";
17
+ /** CREATE2 salt for the stealth SimpleAccount. One account per stealth EOA. */
18
+ export declare const STEALTH_ACCOUNT_SALT = 0;
19
+ /**
20
+ * Factories a stealth payout may be derived against, PER DESTINATION CHAIN.
21
+ *
22
+ * The client deliberately FOLLOWS the solver-advertised factory rather than its
23
+ * own pin (#302): if the two disagree they derive different counterfactual
24
+ * addresses and the recipient cannot find their money. That is the right call
25
+ * for parity, and it is why this list exists rather than a hard pin.
26
+ *
27
+ * Nothing else constrains WHAT may be advertised. The factory decides who owns
28
+ * the account the payout lands in, so an advertised factory that deploys an
29
+ * account owned by someone else is theft, and both sides would have agreed on
30
+ * it. This is the trust anchor the advertised value is checked against: follow
31
+ * the solver, but only into a factory this build already knows.
32
+ *
33
+ * Keyed by chain rather than a flat list because the capability matrix is
34
+ * expected to go per-chain, and a flat list would then accept a factory
35
+ * legitimate on one chain as authorization for another. Stealth is pinned to
36
+ * Ethereum L1 today (STEALTH_SETTLEMENT_CHAIN_ID), so there is exactly one
37
+ * entry; an unlisted chain has no trusted factory and fails closed.
38
+ *
39
+ * ROTATION: ship a build listing both the old and new factory for that chain,
40
+ * let the solver flip, then drop the old one. Staging it that way keeps the
41
+ * allowlist fail-closed without a window where stealth is refused.
42
+ */
43
+ export declare const TRUSTED_STEALTH_FACTORIES: Readonly<Record<number, readonly string[]>>;
44
+ /**
45
+ * Whether a factory may be used to derive a stealth payout ON THIS CHAIN.
46
+ *
47
+ * Case-insensitive: EVM addresses compare case-insensitively and the capability
48
+ * matrix preserves whatever casing the solver sent. Absent address, absent
49
+ * chain, or a chain with no listed factory all return false, because "we have
50
+ * no entry for this" and "this is fine" must not be the same answer about who
51
+ * receives someone's money.
52
+ */
53
+ export declare function isTrustedStealthFactory(address: string | undefined | null, chainId: number | undefined | null): boolean;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * Stealth settlement constants: the SimpleAccount factory + salt a stealth
4
+ * payout is derived against, and the per-chain allowlist that bounds which
5
+ * advertised factory may be followed.
6
+ *
7
+ * @see https://eips.ethereum.org/EIPS/eip-5564
8
+ * @see https://eips.ethereum.org/EIPS/eip-6538
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.TRUSTED_STEALTH_FACTORIES = exports.STEALTH_ACCOUNT_SALT = exports.SIMPLE_ACCOUNT_FACTORY = void 0;
12
+ exports.isTrustedStealthFactory = isTrustedStealthFactory;
13
+ const chains_js_1 = require("./chains.js");
14
+ /**
15
+ * SimpleAccountFactory (v0.8) for the counterfactual stealth destination.
16
+ * Salt is 0: one account per stealth EOA.
17
+ *
18
+ * Canonical home for a security-critical address the client and the worker
19
+ * both need; never declare a second copy.
20
+ */
21
+ exports.SIMPLE_ACCOUNT_FACTORY = "0x13E9ed32155810FDbd067D4522C492D6f68E5944";
22
+ /** CREATE2 salt for the stealth SimpleAccount. One account per stealth EOA. */
23
+ exports.STEALTH_ACCOUNT_SALT = 0;
24
+ /**
25
+ * Factories a stealth payout may be derived against, PER DESTINATION CHAIN.
26
+ *
27
+ * The client deliberately FOLLOWS the solver-advertised factory rather than its
28
+ * own pin (#302): if the two disagree they derive different counterfactual
29
+ * addresses and the recipient cannot find their money. That is the right call
30
+ * for parity, and it is why this list exists rather than a hard pin.
31
+ *
32
+ * Nothing else constrains WHAT may be advertised. The factory decides who owns
33
+ * the account the payout lands in, so an advertised factory that deploys an
34
+ * account owned by someone else is theft, and both sides would have agreed on
35
+ * it. This is the trust anchor the advertised value is checked against: follow
36
+ * the solver, but only into a factory this build already knows.
37
+ *
38
+ * Keyed by chain rather than a flat list because the capability matrix is
39
+ * expected to go per-chain, and a flat list would then accept a factory
40
+ * legitimate on one chain as authorization for another. Stealth is pinned to
41
+ * Ethereum L1 today (STEALTH_SETTLEMENT_CHAIN_ID), so there is exactly one
42
+ * entry; an unlisted chain has no trusted factory and fails closed.
43
+ *
44
+ * ROTATION: ship a build listing both the old and new factory for that chain,
45
+ * let the solver flip, then drop the old one. Staging it that way keeps the
46
+ * allowlist fail-closed without a window where stealth is refused.
47
+ */
48
+ exports.TRUSTED_STEALTH_FACTORIES = {
49
+ [chains_js_1.CHAIN_IDS.ethereum]: [exports.SIMPLE_ACCOUNT_FACTORY],
50
+ };
51
+ /**
52
+ * Whether a factory may be used to derive a stealth payout ON THIS CHAIN.
53
+ *
54
+ * Case-insensitive: EVM addresses compare case-insensitively and the capability
55
+ * matrix preserves whatever casing the solver sent. Absent address, absent
56
+ * chain, or a chain with no listed factory all return false, because "we have
57
+ * no entry for this" and "this is fine" must not be the same answer about who
58
+ * receives someone's money.
59
+ */
60
+ function isTrustedStealthFactory(address, chainId) {
61
+ if (!address || chainId === undefined || chainId === null)
62
+ return false;
63
+ const allowed = exports.TRUSTED_STEALTH_FACTORIES[chainId];
64
+ if (!allowed)
65
+ return false;
66
+ const normalized = address.toLowerCase();
67
+ return allowed.some((f) => f.toLowerCase() === normalized);
68
+ }