@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,23 @@
1
+ /**
2
+ * The stealth claim-info gate, applied at quote ingest.
3
+ */
4
+ import { STEALTH_ACCOUNT_SALT, isTrustedStealthFactory } from "../shared/index.js";
5
+ export function claimInfoError(claimInfo, chainId) {
6
+ if (!claimInfo)
7
+ return null; // Not a stealth quote; nothing to check.
8
+ if (!isTrustedStealthFactory(claimInfo.factoryAddress, chainId)) {
9
+ return {
10
+ code: "untrusted_factory",
11
+ message: "This quote settles through an account factory this app does not recognize",
12
+ };
13
+ }
14
+ // Salt is the other half of the CREATE2 input: moving it relocates the
15
+ // account exactly as changing the factory does.
16
+ if (claimInfo.salt !== undefined && claimInfo.salt !== STEALTH_ACCOUNT_SALT) {
17
+ return {
18
+ code: "unexpected_salt",
19
+ message: "This quote settles to an unexpected account slot",
20
+ };
21
+ }
22
+ return null;
23
+ }
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Intent lifecycle decision logic (pure).
3
+ *
4
+ * The fund-critical + behavior-defining decisions of the intent lifecycle, each
5
+ * a pure function of its inputs: no React, wagmi, network or clock coupling.
6
+ * `update` (machine.ts) maps each decision onto its state transitions and
7
+ * effects; these stay separately exported so each rule is assertable alone.
8
+ */
9
+ import type { ChainId, IntentStatus, SelectableChainId, SettlementType } from "../shared/index.js";
10
+ import type { QuoteRequest, QuoteResponse, IntentWebSocketMessage, ExecuteResponse } from "./wire.js";
11
+ import { type IntentState, type SignRefusal } from "./state.js";
12
+ /** Quote requests are ignored while in one of these states (mid sign/execute). */
13
+ export declare const QUOTE_BUSY_STATES: IntentStatus[];
14
+ /**
15
+ * States an explicit deposit-route allocation may start from: before any
16
+ * signature or deposit. Past them the trade has a ceremony, a swap or an
17
+ * outcome a fresh route would land on.
18
+ */
19
+ export declare const ALLOCATABLE_STATES: IntentStatus[];
20
+ /** A delivered or returned trade: a late quote reply may not reopen it. */
21
+ export declare const ENDED_STATES: IntentStatus[];
22
+ /** A pre-execution quote is reset when the wallet context changes in these states. */
23
+ export declare const WALLET_INTERRUPTIBLE_STATES: IntentStatus[];
24
+ /**
25
+ * The status poll runs while in one of these in-flight states. Each follows an
26
+ * execute or a revealed deposit address, so a `failed` read from one is Retry
27
+ * only on the Worker's nothing-moved marker (`failureMayHaveMoved`): a deposit
28
+ * route's `failed` can follow a deposit the poll never saw.
29
+ */
30
+ export declare const POLL_INFLIGHT_STATES: IntentStatus[];
31
+ /**
32
+ * Whether a `failed` after an execute (or a revealed deposit address) may have
33
+ * moved funds: every one but the Worker's nothing-moved finish (`substatus`
34
+ * `NOT_EXECUTED_SUBSTATUS`, whose sources that constant lists). The machine
35
+ * shows it as the terminal "may have moved" view (no Retry, no poll, Reset
36
+ * only); rules: README "Fund safety".
37
+ */
38
+ export declare function failureMayHaveMoved(substatus: unknown): boolean;
39
+ /**
40
+ * The latest time (unix seconds) the Worker's read finish ends this quote's
41
+ * intent `failed` with `NOT_EXECUTED_SUBSTATUS` while it is still `quoted`
42
+ * with no execute claim (never claimed, or every claim undone): the next read
43
+ * past its signed deadline plus `QUOTED_EXPIRY_GRACE_SECS` (market rows only;
44
+ * the quote sweep usually ends an undone claim earlier). The signed deadline
45
+ * is the served `eip712` message's `deadline`; the later of it and the
46
+ * quote's `expiry` is taken, so the poll never stops short of the Worker's
47
+ * clock. Either alone is enough; null only without a quote or with neither
48
+ * readable.
49
+ */
50
+ export declare function unexecutedFinishAt(quote: QuoteResponse | null): number | null;
51
+ /**
52
+ * In-flight status poll cadence. THE cadence: every client poll of
53
+ * `/api/intent/:id/status` runs on this, the machine's own and the activity
54
+ * feeds' (`useIntentStatusPoll`).
55
+ */
56
+ export declare const STATUS_POLL_MS = 4000;
57
+ /**
58
+ * Hard ceiling on in-flight polling, counted in elapsed POLL INTERVALS -- the
59
+ * only ticks that are wall clock. The arming poll happens at elapsed zero and a
60
+ * foreground snap-forward is off-interval, so neither spends this. Counting
61
+ * every poll would let a user backgrounding and foregrounding through a wallet
62
+ * deep-link burn the whole budget in seconds, while the give-up warning
63
+ * asserts an elapsed ten minutes that never happened.
64
+ *
65
+ * It is a budget for NOTHING HAPPENING, not for polling: a poll reporting
66
+ * progress refills it. Past this cap the client stops polling a stuck solver
67
+ * and leaves resolution to reconciliation/webhooks.
68
+ *
69
+ * This is the ceiling for a phase whose duration WE control. `awaiting_deposit`
70
+ * is not one: the solver sets that window (Riddler defaults to 1800s), so its
71
+ * ceiling is derived from the attested deadline instead (`pollCeilingAttempts`).
72
+ */
73
+ export declare const STATUS_POLL_FLAT_CEILING_ATTEMPTS = 150;
74
+ /**
75
+ * The flat ceiling as wall clock (10 min); see above for why the attempt count
76
+ * is the primary form and this is derived from it. `pollCeilingAttempts` takes
77
+ * the duration, because the deposit window it competes with is a duration too.
78
+ */
79
+ export declare const STATUS_POLL_FLAT_CEILING_MS: number;
80
+ /**
81
+ * Keep polling this long past the deposit deadline.
82
+ *
83
+ * The deadline is when the solver stops FILLING, not when it stops reporting. A
84
+ * transfer broadcast just inside the window can confirm outside it, and a late
85
+ * one is refunded rather than filled -- both are status changes the user is
86
+ * waiting on, and both land after the countdown reads zero.
87
+ */
88
+ export declare const DEPOSIT_POLL_GRACE_MS: number;
89
+ /**
90
+ * Absolute ceiling, whatever a deadline claims. A malformed or hostile
91
+ * `deposit_deadline` far in the future must not turn the poll into a permanent
92
+ * background request loop.
93
+ */
94
+ export declare const POLL_CEILING_MAX_MS: number;
95
+ /**
96
+ * How many polls to run before giving up, for the phase being entered.
97
+ *
98
+ * `awaiting_deposit` is the one phase whose duration the SOLVER sets: Riddler's
99
+ * deposit window defaults to 1800s. The flat ~10 minute ceiling was written for
100
+ * a wedged solver on the EVM path, and applying it here stopped the poll at a
101
+ * third of the window. A deposit that landed after minute ten never advanced the
102
+ * modal, so a swap that had actually settled sat on a spinner, and the user was
103
+ * then told their funds were coming back and to start again.
104
+ *
105
+ * `executing` lasts at least until the Worker's latest finish of an intent no
106
+ * execute holds (`unexecutedFinishAt`, passed as `unexecutedFinishSecs`) plus
107
+ * one poll: an execute that never reached the Worker (a timeout, a refused
108
+ * redirect) or that this client stopped resending reads `quoted`, which is no
109
+ * progress, until the quote sweep (usually first for an un-claimed intent) or
110
+ * the read past the signed deadline finishes it `failed` with
111
+ * `NOT_EXECUTED_SUBSTATUS` and offers Retry.
112
+ *
113
+ * Bounded below by the flat ceiling (never poll less than before), and above by
114
+ * POLL_CEILING_MAX_MS.
115
+ */
116
+ export declare function pollCeilingAttempts(status: IntentStatus, depositDeadlineSecs: number | undefined, nowMs: number, pollMs: number, flatCeilingMs: number, unexecutedFinishSecs?: number): number;
117
+ /**
118
+ * May a resumed intent (restored from the persisted activity log after a mobile
119
+ * tab eviction) be adopted into this state?
120
+ *
121
+ * Only from a clean slate. `resume` exists to restore a swap the machine has no
122
+ * memory of, so ANY live state means it is already tracking something --
123
+ * and overwriting that with a stale stored record would point the status poll,
124
+ * the modal, and the user's read of "what am I waiting on" at the wrong intent
125
+ * while a real one is mid-flight. Fails closed: unknown state means no adopt.
126
+ * See xochi#339.
127
+ */
128
+ export declare function canAdoptResumedIntent(state: Pick<IntentState, "status" | "intentId">): boolean;
129
+ /**
130
+ * The state to hold after adopting a resumed intent, or `prev` UNCHANGED when the
131
+ * adopt must be refused. The machine's `resume` message applies exactly this,
132
+ * so the whole rule is assertable on its own.
133
+ *
134
+ * Three things it has to get right, all of them fund- or privacy-relevant:
135
+ *
136
+ * - Refuse unless the hook is on a clean slate (`canAdoptResumedIntent`), so a
137
+ * stale stored record can never clobber a real trade that is mid-flight.
138
+ * - Refuse an adopt with no `owner`. This is the only intent in the app whose
139
+ * owner does not come from a quote made this session (it comes from the storage
140
+ * key of the record), so without it the adopted state would be the only
141
+ * intentId-bearing state with `quotedAddress: null` -- and the panel's
142
+ * ownership fallback (`intent.quotedAddress ?? address`) silently becomes the
143
+ * LIVE wallet, copying this swap's amounts and tx hashes into whichever account
144
+ * the user switches to next.
145
+ * - Build from `INITIAL_INTENT_STATE`, not from `prev`. A spread of `prev` would
146
+ * carry a stale quote, error or receipt hash into a swap it does not describe.
147
+ *
148
+ * `depositDeadline` is the one served fact that must come ACROSS, and it is not
149
+ * an exception to the rule above. No quote is synthesized, because a stale price
150
+ * shown as current would be a lie; a deadline does not go stale, it expires, and
151
+ * carrying it is what lets the poll say so. Omitting it left
152
+ * `pollCeilingAttempts` on its flat 10-minute ceiling while the restored deposit
153
+ * screen counted down the solver's real 30-minute window, so the poll stopped and
154
+ * announced it beside a countdown that disagreed. Null on every pull route.
155
+ *
156
+ * See xochi#339.
157
+ */
158
+ export declare function buildResumedState(prev: IntentState, intentId: string, status: IntentStatus, owner: string, depositDeadline?: number | null): IntentState;
159
+ /** Statuses that render the panel as loading. */
160
+ export declare const LOADING_STATES: IntentStatus[];
161
+ export interface BuildQuoteRequestInput {
162
+ wallet?: string;
163
+ preview?: boolean;
164
+ allocationKey?: string;
165
+ fromChainId: SelectableChainId;
166
+ toChainId: SelectableChainId;
167
+ fromToken: string;
168
+ toToken: string;
169
+ fromAmount: string;
170
+ deadline: number;
171
+ slippageBps: number;
172
+ trustScore: number;
173
+ settlementPreference: SettlementType;
174
+ recipientAddress?: string;
175
+ refundAddress?: string;
176
+ stealthMetaAddress?: {
177
+ spendingPubKey: string;
178
+ viewingPubKey: string;
179
+ chainId: ChainId;
180
+ };
181
+ }
182
+ /**
183
+ * The request fields the server reads through a normalisation: an empty
184
+ * `recipientAddress` or `refundAddress` is undefined (EVM routes settle to, and
185
+ * refund to, the connected wallet) and the stealth meta-address is sent ONLY
186
+ * on a stealth settlement. The machine's trade key reads the same values, so
187
+ * two forms that build one request are one trade.
188
+ */
189
+ export declare function normalizeTradeFields(input: Pick<BuildQuoteRequestInput, "settlementPreference"> & {
190
+ recipientAddress?: string | null;
191
+ refundAddress?: string | null;
192
+ stealthMetaAddress?: BuildQuoteRequestInput["stealthMetaAddress"] | null;
193
+ }): Pick<QuoteRequest, "recipientAddress" | "refundAddress" | "stealthMetaAddress">;
194
+ /** Build the quote request body, with the trade fields normalised. */
195
+ export declare function buildQuoteRequest(input: BuildQuoteRequestInput): QuoteRequest;
196
+ export type QuoteAccessDecision = "executable" | "preview" | "needs_wallet";
197
+ /**
198
+ * Decide whether pricing is indicative or executable.
199
+ *
200
+ * Deposit-origin pricing is always a preview until the caller explicitly asks
201
+ * to allocate a route. That keeps amount editing side-effect free even for a
202
+ * connected wallet. Private settlement still needs wallet-derived keys.
203
+ */
204
+ export declare function quoteAccessDecision(input: {
205
+ connected: boolean;
206
+ settlementPreference: SettlementType;
207
+ isDepositOrigin: boolean;
208
+ allocateDepositRoute?: boolean;
209
+ }): QuoteAccessDecision;
210
+ /**
211
+ * Replace a held preview with a wallet-bound executable quote?
212
+ *
213
+ * True exactly when a wallet is present while a preview is the quote on
214
+ * screen. The caller refreshes silently (the visible price must not flash back
215
+ * to "quoting" on connect) and force-refreshes (a preview prices at Standard
216
+ * tier and carries no signing material, so it must never be served from cache
217
+ * as an executable quote).
218
+ */
219
+ export declare function shouldUpgradePreviewQuote(input: {
220
+ connected: boolean;
221
+ hasAmount: boolean;
222
+ status: IntentStatus;
223
+ quoteIsPreview: boolean;
224
+ isDepositOrigin: boolean;
225
+ }): boolean;
226
+ /**
227
+ * The signing gate. A preview is never signable whatever else it claims: the
228
+ * server strips its EIP-712 payload, so the wallet/session boundary stays at
229
+ * execution even though the price itself was served anonymously.
230
+ */
231
+ export declare function canSignQuote(status: IntentStatus, quote: QuoteResponse | null): boolean;
232
+ /** True when the current indicative deposit price can be explicitly allocated. */
233
+ export declare function canAllocateDepositQuote(connected: boolean, isDepositOrigin: boolean, status: IntentStatus, quote: QuoteResponse | null): boolean;
234
+ /**
235
+ * The pre-expiry auto-refresh delay in ms: how long until we should re-fetch to
236
+ * replace an expiring quote (`buffer` before expiry). A non-positive result means
237
+ * the quote is already inside the buffer window and no refresh is scheduled.
238
+ */
239
+ export declare function computeRefreshDelayMs(expiryIso: string, now: number, bufferMs: number): number;
240
+ /**
241
+ * Whether the expiry timer should re-quote.
242
+ *
243
+ * Indicative pricing is allocation-free on every origin and may stay live.
244
+ * Once a deposit address exists, its `deposit_deadline` owns the lifecycle; a
245
+ * 30-second price refresh must never replace or invalidate that route.
246
+ */
247
+ export declare function shouldRefreshQuote(input: {
248
+ status: IntentStatus;
249
+ /** Backgrounded tabs never refresh: a hidden tab should not hammer anything. */
250
+ hidden: boolean;
251
+ hasAllocatedDeposit: boolean;
252
+ }): boolean;
253
+ /**
254
+ * Whether returning to a foregrounded tab should re-quote.
255
+ *
256
+ * The confirm modal owns its own refresh. An allocated deposit route is also
257
+ * immutable until its deposit deadline; all other quoted/expired prices may be
258
+ * refreshed on return.
259
+ */
260
+ export declare function shouldRefreshOnReturn(input: {
261
+ status: IntentStatus;
262
+ paused: boolean;
263
+ hasAllocatedDeposit: boolean;
264
+ }): boolean;
265
+ export type SignPreflight = {
266
+ kind: "awaitingDeposit";
267
+ } | {
268
+ kind: "noQuote";
269
+ }
270
+ /**
271
+ * Refused while the quote is still the right one to show: the user fixes the
272
+ * cause (switch chain, re-quote) without losing it. `refusal` is the typed
273
+ * reason a renderer words; `error` is the message for a step that already
274
+ * left `quoted` (the deep-link "Sign 2 of 2" tap).
275
+ */
276
+ | {
277
+ kind: "reject";
278
+ to: "quoted";
279
+ refusal: SignRefusal;
280
+ error: string;
281
+ } | {
282
+ kind: "reject";
283
+ to: "expired";
284
+ error: string;
285
+ } | {
286
+ kind: "proceed";
287
+ };
288
+ /**
289
+ * Decide whether a quote may be signed, in the exact precedence the hook
290
+ * enforces (all fail-closed, no funds move on a reject):
291
+ * 1. Deposit route (Tron origin) -> no signature; reveal the address.
292
+ * 2. No EIP-712 payload -> nothing to sign.
293
+ * 3. Wallet account changed since the quote -> re-quote (`quoted`).
294
+ * 4. Wallet not on the origin chain -> refuse (`quoted`); the domain binds it.
295
+ * 5. Wallet chain changed since the quote -> re-quote (`quoted`).
296
+ * 6. Quote already expired -> `expired`.
297
+ * 7. Otherwise proceed.
298
+ */
299
+ export declare function decideSignPreflight(input: {
300
+ hasDepositAddress: boolean;
301
+ hasEip712: boolean;
302
+ address: string | undefined;
303
+ quotedAddress: string | null;
304
+ quotedChainId: number | null;
305
+ /**
306
+ * The wallet's actual chain (`useAccount().chainId`), undefined when none is
307
+ * reported. May be a chain the app has no config for (xochi#539).
308
+ */
309
+ chainId: number | undefined;
310
+ /** The order's origin chain, which the signed EIP-712 domain binds. */
311
+ originChainId: SelectableChainId;
312
+ /** Quote expiry in ms, or 0 when the quote carries none. */
313
+ expiryMs: number;
314
+ now: number;
315
+ }): SignPreflight;
316
+ export type ExecuteDecision = {
317
+ kind: "failed";
318
+ error: string;
319
+ mayHaveMoved: boolean;
320
+ } | {
321
+ kind: "transition";
322
+ status: IntentStatus;
323
+ updates: Partial<IntentState>;
324
+ };
325
+ /**
326
+ * Interpret an /execute response, preserving the in-doubt distinction (#226):
327
+ * the worker returns `{success:false}` for BOTH a genuine failure AND a
328
+ * fail-closed downgrade (the origin pull already moved funds but the
329
+ * settlement is unconfirmed inline, so IN-DOUBT). `reconciling` is the
330
+ * server's flag when it sends one, else on whenever the server withheld
331
+ * success (`reconciling ?? !success`). The verdicts, in order:
332
+ * - `failed` with `success: false` and `reconciling` off by that rule (the
333
+ * server sent `reconciling: false`) is final: Retry only with the Worker's
334
+ * nothing-moved marker, else "may have moved" (`failureMayHaveMoved`).
335
+ * - `completed` completes with reconciling off, whatever the reply says: a
336
+ * completion leaves nothing to reconcile.
337
+ * - Any other `failed` (`success: true`, or `reconciling` on, the absent flag
338
+ * included) is in doubt: it moves to the in-flight `executing` with
339
+ * reconciling on, and the poll reads the final status.
340
+ * - Anything else advances to its status with reconciling by that rule, so a
341
+ * withheld success shows the "Still confirming" path with no Retry (an
342
+ * in-session retry of a funds-moved swap would be a double execution).
343
+ * `onExecuted` also shows an unmapped status in doubt.
344
+ */
345
+ export declare function decideExecuteResult(result: ExecuteResponse): ExecuteDecision;
346
+ export type ExecuteResend = {
347
+ kind: "resend";
348
+ delayMs: number;
349
+ } | {
350
+ kind: "give_up";
351
+ reason: "exhausted" | "no_retry_after" | "stale_retry_after" | "deadline";
352
+ };
353
+ /**
354
+ * After the server refused an execute before accepting anything, whether to
355
+ * send the SAME request again and when (rules: README "Fund safety").
356
+ * `retryAfter` and `deadline` are unix seconds, `now` the host's clock; a null
357
+ * `deadline` never resends, a `retryAfter` older than
358
+ * `RETRY_AFTER_MAX_AGE_SECS` gives up, and the wait is at least a second, so
359
+ * a slightly past `retryAfter` cannot spin. A give-up offers no Retry: the
360
+ * signed request stays executable until its deadline, so the machine shows
361
+ * "not sent yet" (`IntentState.notSent`) and polls until the Worker finishes
362
+ * the intent.
363
+ */
364
+ export declare function decideExecuteResend(input: {
365
+ resends: number;
366
+ retryAfter: number | undefined;
367
+ deadline: number | null;
368
+ now: number;
369
+ }): ExecuteResend;
370
+ /**
371
+ * The earliest deadline (unix seconds) a resent execute carries: the quote's
372
+ * expiry, the signed intent's `deadline` and the origin pull's `validBefore` /
373
+ * `deadline`. Null without a readable quote expiry, which never resends.
374
+ */
375
+ export declare function executeDeadline(quote: QuoteResponse | null): number | null;
376
+ /**
377
+ * Map an intent status message (from the poll) to the transition to apply. A
378
+ * refund is fund-safe and terminal: its reason rides `refundReason` (plain
379
+ * language), never `error`. Mirrors the poll's fail-closed handling.
380
+ */
381
+ export declare function mapStatusUpdate(msg: IntentWebSocketMessage): {
382
+ status: IntentStatus;
383
+ updates: Partial<IntentState>;
384
+ };
385
+ /**
386
+ * Read a lifecycle frame from the intent stream (`IntentStatusFrame`, the
387
+ * worker's `{ type: "status", intent_id, status, revision, ... }`, D5 5.9).
388
+ *
389
+ * Returns null for anything that is not a CURRENT status frame: a value that
390
+ * does not parse, or a frame at or below `lastRevision` (the relay replays its
391
+ * buffer to every (re)connecting subscriber and deferred publishes can land out
392
+ * of order, so a stale frame must not be read as the current status).
393
+ *
394
+ * Otherwise returns the frame's `revision`, which the caller records, and the
395
+ * status `message` for `handleStatusUpdate`, or a null message when the status
396
+ * is outside POLL_INFLIGHT_STATES (terminals included). The frame carries none
397
+ * of what the poll returns with a terminal (receipt hashes, the failure reason,
398
+ * the refund reason; `tx_hash` only on a public settlement), and a terminal
399
+ * status stops the poll. Applying one would pin the final view without its
400
+ * receipt link or real reason, so the poll delivers terminals, whole, within
401
+ * one interval. The stream accelerates the in-flight steps.
402
+ */
403
+ export declare function mapStatusFrame(value: unknown, lastRevision: number | null): {
404
+ revision: number | null;
405
+ message: IntentWebSocketMessage | null;
406
+ } | null;
407
+ /**
408
+ * The drift warning to log when the in-flight status poll gets a definitive 404
409
+ * for a DISPATCHED intent id, or null when this id has already been reported.
410
+ *
411
+ * The poll's decision on that 404 is to HOLD (never terminalize; see the status
412
+ * poll in machine.ts for why), and a hold that emits nothing is indistinguishable from a
413
+ * healthy poll: the modal spins, the poll runs to its ~10 min cap, and no console
414
+ * line says which swap it is. So the first 404 per id carries a signal, in the
415
+ * same shape CLAUDE.md already relies on for `mapRiddlerStatus` drift (a warn
416
+ * carrying the intent id, catchable in logs).
417
+ *
418
+ * `reportedId` is the last id the caller reported, so the warning is one-shot:
419
+ * the poll runs every 4s and would otherwise emit ~150 identical lines per stuck
420
+ * swap. A different id reports again -- a second swap hitting this is a second
421
+ * data point, not a repeat.
422
+ *
423
+ * Distinct from the transport-level warn in `intentAPI.getIntentStatus`, which
424
+ * fires once per id for ANY 404, including the benign one the orphan-quote sweep
425
+ * produces for a pre-dispatch row (and can be spent on that benign case by
426
+ * whichever caller polls the id first). This one names the dispatched case, which
427
+ * is the bug.
428
+ */
429
+ export declare function notFoundDriftWarning(intentId: string, reportedId: string | null): string | null;
430
+ /**
431
+ * Fields to merge from a poll whose status did NOT change, or null when the
432
+ * poll carries nothing new.
433
+ *
434
+ * The status-transition table has no self-edges, so a `settling -> settling`
435
+ * poll is an invalid move and `applyStatusTransition` discards the entire
436
+ * update, receipt hashes included. A stealth swap sits at `settling` for its
437
+ * whole life, so that edge is never valid and the fill hash the server returns
438
+ * on every 4s poll never reached the UI: the user was shown "Still confirming"
439
+ * with no transaction link while the settlement was on chain (xochi#353).
440
+ *
441
+ * Two deliberate omissions:
442
+ *
443
+ * - A field that did not ARRIVE is not applied. Blanking a hash we already hold
444
+ * because a later poll omitted it would flicker the receipt link back off.
445
+ * - `reconciling` is never merged. The poll owns that flag on its own timer, and
446
+ * re-applying `mapStatusUpdate`'s default `false` here would switch the
447
+ * "safe to close" view off again on every tick.
448
+ *
449
+ * Returns null when nothing changed, so the caller can skip the setState and
450
+ * avoid a re-render per poll.
451
+ */
452
+ export declare function mergeSameStatusReceipt(prev: Pick<IntentState, "receivingTxHash" | "settlementTxHash" | "settlementChainId" | "substatus">, msg: IntentWebSocketMessage): Partial<IntentState> | null;