@xochi/client 0.0.0-stage → 0.1.2
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 +64 -0
- package/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/cjs/claim.d.ts +33 -0
- package/dist/cjs/claim.js +44 -0
- package/dist/cjs/fee.d.ts +247 -0
- package/dist/cjs/fee.js +295 -0
- package/dist/cjs/index.d.ts +18 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/machine/claimInfo.d.ts +29 -0
- package/dist/cjs/machine/claimInfo.js +26 -0
- package/dist/cjs/machine/decisions.d.ts +452 -0
- package/dist/cjs/machine/decisions.js +665 -0
- package/dist/cjs/machine/deposit.d.ts +113 -0
- package/dist/cjs/machine/deposit.js +265 -0
- package/dist/cjs/machine/index.d.ts +23 -0
- package/dist/cjs/machine/index.js +53 -0
- package/dist/cjs/machine/machine.d.ts +411 -0
- package/dist/cjs/machine/machine.js +1627 -0
- package/dist/cjs/machine/phase.d.ts +24 -0
- package/dist/cjs/machine/phase.js +45 -0
- package/dist/cjs/machine/quoteChecks.d.ts +41 -0
- package/dist/cjs/machine/quoteChecks.js +82 -0
- package/dist/cjs/machine/receipt.d.ts +151 -0
- package/dist/cjs/machine/receipt.js +69 -0
- package/dist/cjs/machine/recipientEcho.d.ts +16 -0
- package/dist/cjs/machine/recipientEcho.js +27 -0
- package/dist/cjs/machine/solverPin.d.ts +110 -0
- package/dist/cjs/machine/solverPin.js +214 -0
- package/dist/cjs/machine/state.d.ts +167 -0
- package/dist/cjs/machine/state.js +31 -0
- package/dist/cjs/machine/transition.d.ts +15 -0
- package/dist/cjs/machine/transition.js +20 -0
- package/dist/cjs/machine/validateRequest.d.ts +15 -0
- package/dist/cjs/machine/validateRequest.js +127 -0
- package/dist/cjs/machine/view.d.ts +3 -0
- package/dist/cjs/machine/view.js +74 -0
- package/dist/cjs/machine/wire.d.ts +527 -0
- package/dist/cjs/machine/wire.js +280 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/shared/api.d.ts +9 -0
- package/dist/cjs/shared/api.js +5 -0
- package/dist/cjs/shared/chains.d.ts +243 -0
- package/dist/cjs/shared/chains.js +250 -0
- package/dist/cjs/shared/eip712.d.ts +44 -0
- package/dist/cjs/shared/eip712.js +64 -0
- package/dist/cjs/shared/index.d.ts +16 -0
- package/dist/cjs/shared/index.js +32 -0
- package/dist/cjs/shared/intentStream.d.ts +74 -0
- package/dist/cjs/shared/intentStream.js +59 -0
- package/dist/cjs/shared/intents.d.ts +88 -0
- package/dist/cjs/shared/intents.js +227 -0
- package/dist/cjs/shared/schemas.d.ts +928 -0
- package/dist/cjs/shared/schemas.js +524 -0
- package/dist/cjs/shared/shieldedPreview.d.ts +841 -0
- package/dist/cjs/shared/shieldedPreview.js +128 -0
- package/dist/cjs/shared/stealth.d.ts +53 -0
- package/dist/cjs/shared/stealth.js +68 -0
- package/dist/cjs/shared/trading.d.ts +153 -0
- package/dist/cjs/shared/trading.js +185 -0
- package/dist/cjs/shared/types.d.ts +37 -0
- package/dist/cjs/shared/types.js +7 -0
- package/dist/cjs/shared/validators.d.ts +30 -0
- package/dist/cjs/shared/validators.js +79 -0
- package/dist/cjs/transport/client.d.ts +234 -0
- package/dist/cjs/transport/client.js +1456 -0
- package/dist/esm/claim.d.ts +33 -0
- package/dist/esm/claim.js +41 -0
- package/dist/esm/fee.d.ts +247 -0
- package/dist/esm/fee.js +285 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/machine/claimInfo.d.ts +29 -0
- package/dist/esm/machine/claimInfo.js +23 -0
- package/dist/esm/machine/decisions.d.ts +452 -0
- package/dist/esm/machine/decisions.js +640 -0
- package/dist/esm/machine/deposit.d.ts +113 -0
- package/dist/esm/machine/deposit.js +260 -0
- package/dist/esm/machine/index.d.ts +23 -0
- package/dist/esm/machine/index.js +23 -0
- package/dist/esm/machine/machine.d.ts +411 -0
- package/dist/esm/machine/machine.js +1621 -0
- package/dist/esm/machine/phase.d.ts +24 -0
- package/dist/esm/machine/phase.js +42 -0
- package/dist/esm/machine/quoteChecks.d.ts +41 -0
- package/dist/esm/machine/quoteChecks.js +78 -0
- package/dist/esm/machine/receipt.d.ts +151 -0
- package/dist/esm/machine/receipt.js +64 -0
- package/dist/esm/machine/recipientEcho.d.ts +16 -0
- package/dist/esm/machine/recipientEcho.js +24 -0
- package/dist/esm/machine/solverPin.d.ts +110 -0
- package/dist/esm/machine/solverPin.js +210 -0
- package/dist/esm/machine/state.d.ts +167 -0
- package/dist/esm/machine/state.js +28 -0
- package/dist/esm/machine/transition.d.ts +15 -0
- package/dist/esm/machine/transition.js +17 -0
- package/dist/esm/machine/validateRequest.d.ts +15 -0
- package/dist/esm/machine/validateRequest.js +123 -0
- package/dist/esm/machine/view.d.ts +3 -0
- package/dist/esm/machine/view.js +71 -0
- package/dist/esm/machine/wire.d.ts +527 -0
- package/dist/esm/machine/wire.js +266 -0
- package/dist/esm/shared/api.d.ts +9 -0
- package/dist/esm/shared/api.js +4 -0
- package/dist/esm/shared/chains.d.ts +243 -0
- package/dist/esm/shared/chains.js +236 -0
- package/dist/esm/shared/eip712.d.ts +44 -0
- package/dist/esm/shared/eip712.js +61 -0
- package/dist/esm/shared/index.d.ts +16 -0
- package/dist/esm/shared/index.js +16 -0
- package/dist/esm/shared/intentStream.d.ts +74 -0
- package/dist/esm/shared/intentStream.js +55 -0
- package/dist/esm/shared/intents.d.ts +88 -0
- package/dist/esm/shared/intents.js +213 -0
- package/dist/esm/shared/schemas.d.ts +928 -0
- package/dist/esm/shared/schemas.js +521 -0
- package/dist/esm/shared/shieldedPreview.d.ts +841 -0
- package/dist/esm/shared/shieldedPreview.js +124 -0
- package/dist/esm/shared/stealth.d.ts +53 -0
- package/dist/esm/shared/stealth.js +64 -0
- package/dist/esm/shared/trading.d.ts +153 -0
- package/dist/esm/shared/trading.js +182 -0
- package/dist/esm/shared/types.d.ts +37 -0
- package/dist/esm/shared/types.js +6 -0
- package/dist/esm/shared/validators.d.ts +30 -0
- package/dist/esm/shared/validators.js +74 -0
- package/dist/esm/transport/client.d.ts +234 -0
- package/dist/esm/transport/client.js +1447 -0
- package/package.json +61 -4
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Intent lifecycle decision logic (pure).
|
|
4
|
+
*
|
|
5
|
+
* The fund-critical + behavior-defining decisions of the intent lifecycle, each
|
|
6
|
+
* a pure function of its inputs: no React, wagmi, network or clock coupling.
|
|
7
|
+
* `update` (machine.ts) maps each decision onto its state transitions and
|
|
8
|
+
* effects; these stay separately exported so each rule is assertable alone.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.LOADING_STATES = exports.POLL_CEILING_MAX_MS = exports.DEPOSIT_POLL_GRACE_MS = exports.STATUS_POLL_FLAT_CEILING_MS = exports.STATUS_POLL_FLAT_CEILING_ATTEMPTS = exports.STATUS_POLL_MS = exports.POLL_INFLIGHT_STATES = exports.WALLET_INTERRUPTIBLE_STATES = exports.ENDED_STATES = exports.ALLOCATABLE_STATES = exports.QUOTE_BUSY_STATES = void 0;
|
|
12
|
+
exports.failureMayHaveMoved = failureMayHaveMoved;
|
|
13
|
+
exports.unexecutedFinishAt = unexecutedFinishAt;
|
|
14
|
+
exports.pollCeilingAttempts = pollCeilingAttempts;
|
|
15
|
+
exports.canAdoptResumedIntent = canAdoptResumedIntent;
|
|
16
|
+
exports.buildResumedState = buildResumedState;
|
|
17
|
+
exports.normalizeTradeFields = normalizeTradeFields;
|
|
18
|
+
exports.buildQuoteRequest = buildQuoteRequest;
|
|
19
|
+
exports.quoteAccessDecision = quoteAccessDecision;
|
|
20
|
+
exports.shouldUpgradePreviewQuote = shouldUpgradePreviewQuote;
|
|
21
|
+
exports.canSignQuote = canSignQuote;
|
|
22
|
+
exports.canAllocateDepositQuote = canAllocateDepositQuote;
|
|
23
|
+
exports.computeRefreshDelayMs = computeRefreshDelayMs;
|
|
24
|
+
exports.shouldRefreshQuote = shouldRefreshQuote;
|
|
25
|
+
exports.shouldRefreshOnReturn = shouldRefreshOnReturn;
|
|
26
|
+
exports.decideSignPreflight = decideSignPreflight;
|
|
27
|
+
exports.decideExecuteResult = decideExecuteResult;
|
|
28
|
+
exports.decideExecuteResend = decideExecuteResend;
|
|
29
|
+
exports.executeDeadline = executeDeadline;
|
|
30
|
+
exports.mapStatusUpdate = mapStatusUpdate;
|
|
31
|
+
exports.mapStatusFrame = mapStatusFrame;
|
|
32
|
+
exports.notFoundDriftWarning = notFoundDriftWarning;
|
|
33
|
+
exports.mergeSameStatusReceipt = mergeSameStatusReceipt;
|
|
34
|
+
const index_js_1 = require("../shared/index.js");
|
|
35
|
+
const state_js_1 = require("./state.js");
|
|
36
|
+
// ============================================================
|
|
37
|
+
// Lifecycle state sets
|
|
38
|
+
// ============================================================
|
|
39
|
+
/** Quote requests are ignored while in one of these states (mid sign/execute). */
|
|
40
|
+
exports.QUOTE_BUSY_STATES = [
|
|
41
|
+
"signing",
|
|
42
|
+
"awaiting_deposit",
|
|
43
|
+
"executing",
|
|
44
|
+
"bridging",
|
|
45
|
+
"settling",
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* States an explicit deposit-route allocation may start from: before any
|
|
49
|
+
* signature or deposit. Past them the trade has a ceremony, a swap or an
|
|
50
|
+
* outcome a fresh route would land on.
|
|
51
|
+
*/
|
|
52
|
+
exports.ALLOCATABLE_STATES = ["idle", "pending", "quoting", "quoted"];
|
|
53
|
+
/** A delivered or returned trade: a late quote reply may not reopen it. */
|
|
54
|
+
exports.ENDED_STATES = ["completed", "refunded"];
|
|
55
|
+
/** A pre-execution quote is reset when the wallet context changes in these states. */
|
|
56
|
+
exports.WALLET_INTERRUPTIBLE_STATES = [
|
|
57
|
+
"quoted",
|
|
58
|
+
"signing",
|
|
59
|
+
"expired",
|
|
60
|
+
"failed",
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* The status poll runs while in one of these in-flight states. Each follows an
|
|
64
|
+
* execute or a revealed deposit address, so a `failed` read from one is Retry
|
|
65
|
+
* only on the Worker's nothing-moved marker (`failureMayHaveMoved`): a deposit
|
|
66
|
+
* route's `failed` can follow a deposit the poll never saw.
|
|
67
|
+
*/
|
|
68
|
+
exports.POLL_INFLIGHT_STATES = [
|
|
69
|
+
"awaiting_deposit",
|
|
70
|
+
"executing",
|
|
71
|
+
"bridging",
|
|
72
|
+
"settling",
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Whether a `failed` after an execute (or a revealed deposit address) may have
|
|
76
|
+
* moved funds: every one but the Worker's nothing-moved finish (`substatus`
|
|
77
|
+
* `NOT_EXECUTED_SUBSTATUS`, whose sources that constant lists). The machine
|
|
78
|
+
* shows it as the terminal "may have moved" view (no Retry, no poll, Reset
|
|
79
|
+
* only); rules: README "Fund safety".
|
|
80
|
+
*/
|
|
81
|
+
function failureMayHaveMoved(substatus) {
|
|
82
|
+
return substatus !== index_js_1.NOT_EXECUTED_SUBSTATUS;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The latest time (unix seconds) the Worker's read finish ends this quote's
|
|
86
|
+
* intent `failed` with `NOT_EXECUTED_SUBSTATUS` while it is still `quoted`
|
|
87
|
+
* with no execute claim (never claimed, or every claim undone): the next read
|
|
88
|
+
* past its signed deadline plus `QUOTED_EXPIRY_GRACE_SECS` (market rows only;
|
|
89
|
+
* the quote sweep usually ends an undone claim earlier). The signed deadline
|
|
90
|
+
* is the served `eip712` message's `deadline`; the later of it and the
|
|
91
|
+
* quote's `expiry` is taken, so the poll never stops short of the Worker's
|
|
92
|
+
* clock. Either alone is enough; null only without a quote or with neither
|
|
93
|
+
* readable.
|
|
94
|
+
*/
|
|
95
|
+
function unexecutedFinishAt(quote) {
|
|
96
|
+
if (!quote)
|
|
97
|
+
return null;
|
|
98
|
+
const expiryMs = new Date(quote.expiry).getTime();
|
|
99
|
+
const raw = quote.eip712Data?.message?.deadline;
|
|
100
|
+
const signed = typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : raw;
|
|
101
|
+
const candidates = [
|
|
102
|
+
Number.isFinite(expiryMs) ? Math.floor(expiryMs / 1000) : NaN,
|
|
103
|
+
typeof signed === "number" ? signed : NaN,
|
|
104
|
+
].filter((v) => Number.isFinite(v));
|
|
105
|
+
return candidates.length > 0 ? Math.max(...candidates) + index_js_1.QUOTED_EXPIRY_GRACE_SECS : null;
|
|
106
|
+
}
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Status poll cadence and ceilings
|
|
109
|
+
// ============================================================
|
|
110
|
+
/**
|
|
111
|
+
* In-flight status poll cadence. THE cadence: every client poll of
|
|
112
|
+
* `/api/intent/:id/status` runs on this, the machine's own and the activity
|
|
113
|
+
* feeds' (`useIntentStatusPoll`).
|
|
114
|
+
*/
|
|
115
|
+
exports.STATUS_POLL_MS = 4000;
|
|
116
|
+
/**
|
|
117
|
+
* Hard ceiling on in-flight polling, counted in elapsed POLL INTERVALS -- the
|
|
118
|
+
* only ticks that are wall clock. The arming poll happens at elapsed zero and a
|
|
119
|
+
* foreground snap-forward is off-interval, so neither spends this. Counting
|
|
120
|
+
* every poll would let a user backgrounding and foregrounding through a wallet
|
|
121
|
+
* deep-link burn the whole budget in seconds, while the give-up warning
|
|
122
|
+
* asserts an elapsed ten minutes that never happened.
|
|
123
|
+
*
|
|
124
|
+
* It is a budget for NOTHING HAPPENING, not for polling: a poll reporting
|
|
125
|
+
* progress refills it. Past this cap the client stops polling a stuck solver
|
|
126
|
+
* and leaves resolution to reconciliation/webhooks.
|
|
127
|
+
*
|
|
128
|
+
* This is the ceiling for a phase whose duration WE control. `awaiting_deposit`
|
|
129
|
+
* is not one: the solver sets that window (Riddler defaults to 1800s), so its
|
|
130
|
+
* ceiling is derived from the attested deadline instead (`pollCeilingAttempts`).
|
|
131
|
+
*/
|
|
132
|
+
exports.STATUS_POLL_FLAT_CEILING_ATTEMPTS = 150;
|
|
133
|
+
/**
|
|
134
|
+
* The flat ceiling as wall clock (10 min); see above for why the attempt count
|
|
135
|
+
* is the primary form and this is derived from it. `pollCeilingAttempts` takes
|
|
136
|
+
* the duration, because the deposit window it competes with is a duration too.
|
|
137
|
+
*/
|
|
138
|
+
exports.STATUS_POLL_FLAT_CEILING_MS = exports.STATUS_POLL_FLAT_CEILING_ATTEMPTS * exports.STATUS_POLL_MS;
|
|
139
|
+
/**
|
|
140
|
+
* Keep polling this long past the deposit deadline.
|
|
141
|
+
*
|
|
142
|
+
* The deadline is when the solver stops FILLING, not when it stops reporting. A
|
|
143
|
+
* transfer broadcast just inside the window can confirm outside it, and a late
|
|
144
|
+
* one is refunded rather than filled -- both are status changes the user is
|
|
145
|
+
* waiting on, and both land after the countdown reads zero.
|
|
146
|
+
*/
|
|
147
|
+
exports.DEPOSIT_POLL_GRACE_MS = 10 * 60_000;
|
|
148
|
+
/**
|
|
149
|
+
* Absolute ceiling, whatever a deadline claims. A malformed or hostile
|
|
150
|
+
* `deposit_deadline` far in the future must not turn the poll into a permanent
|
|
151
|
+
* background request loop.
|
|
152
|
+
*/
|
|
153
|
+
exports.POLL_CEILING_MAX_MS = 2 * 60 * 60_000;
|
|
154
|
+
/**
|
|
155
|
+
* How many polls to run before giving up, for the phase being entered.
|
|
156
|
+
*
|
|
157
|
+
* `awaiting_deposit` is the one phase whose duration the SOLVER sets: Riddler's
|
|
158
|
+
* deposit window defaults to 1800s. The flat ~10 minute ceiling was written for
|
|
159
|
+
* a wedged solver on the EVM path, and applying it here stopped the poll at a
|
|
160
|
+
* third of the window. A deposit that landed after minute ten never advanced the
|
|
161
|
+
* modal, so a swap that had actually settled sat on a spinner, and the user was
|
|
162
|
+
* then told their funds were coming back and to start again.
|
|
163
|
+
*
|
|
164
|
+
* `executing` lasts at least until the Worker's latest finish of an intent no
|
|
165
|
+
* execute holds (`unexecutedFinishAt`, passed as `unexecutedFinishSecs`) plus
|
|
166
|
+
* one poll: an execute that never reached the Worker (a timeout, a refused
|
|
167
|
+
* redirect) or that this client stopped resending reads `quoted`, which is no
|
|
168
|
+
* progress, until the quote sweep (usually first for an un-claimed intent) or
|
|
169
|
+
* the read past the signed deadline finishes it `failed` with
|
|
170
|
+
* `NOT_EXECUTED_SUBSTATUS` and offers Retry.
|
|
171
|
+
*
|
|
172
|
+
* Bounded below by the flat ceiling (never poll less than before), and above by
|
|
173
|
+
* POLL_CEILING_MAX_MS.
|
|
174
|
+
*/
|
|
175
|
+
function pollCeilingAttempts(status, depositDeadlineSecs, nowMs, pollMs, flatCeilingMs, unexecutedFinishSecs) {
|
|
176
|
+
const flat = Math.ceil(flatCeilingMs / pollMs);
|
|
177
|
+
let windowMs = NaN;
|
|
178
|
+
if (status === "awaiting_deposit" && depositDeadlineSecs) {
|
|
179
|
+
windowMs = depositDeadlineSecs * 1000 - nowMs + exports.DEPOSIT_POLL_GRACE_MS;
|
|
180
|
+
}
|
|
181
|
+
else if (status === "executing" && unexecutedFinishSecs) {
|
|
182
|
+
windowMs = unexecutedFinishSecs * 1000 - nowMs + pollMs;
|
|
183
|
+
}
|
|
184
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0)
|
|
185
|
+
return flat;
|
|
186
|
+
return Math.max(flat, Math.ceil(Math.min(windowMs, exports.POLL_CEILING_MAX_MS) / pollMs));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* May a resumed intent (restored from the persisted activity log after a mobile
|
|
190
|
+
* tab eviction) be adopted into this state?
|
|
191
|
+
*
|
|
192
|
+
* Only from a clean slate. `resume` exists to restore a swap the machine has no
|
|
193
|
+
* memory of, so ANY live state means it is already tracking something --
|
|
194
|
+
* and overwriting that with a stale stored record would point the status poll,
|
|
195
|
+
* the modal, and the user's read of "what am I waiting on" at the wrong intent
|
|
196
|
+
* while a real one is mid-flight. Fails closed: unknown state means no adopt.
|
|
197
|
+
* See xochi#339.
|
|
198
|
+
*/
|
|
199
|
+
function canAdoptResumedIntent(state) {
|
|
200
|
+
return state.status === "idle" && !state.intentId;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The state to hold after adopting a resumed intent, or `prev` UNCHANGED when the
|
|
204
|
+
* adopt must be refused. The machine's `resume` message applies exactly this,
|
|
205
|
+
* so the whole rule is assertable on its own.
|
|
206
|
+
*
|
|
207
|
+
* Three things it has to get right, all of them fund- or privacy-relevant:
|
|
208
|
+
*
|
|
209
|
+
* - Refuse unless the hook is on a clean slate (`canAdoptResumedIntent`), so a
|
|
210
|
+
* stale stored record can never clobber a real trade that is mid-flight.
|
|
211
|
+
* - Refuse an adopt with no `owner`. This is the only intent in the app whose
|
|
212
|
+
* owner does not come from a quote made this session (it comes from the storage
|
|
213
|
+
* key of the record), so without it the adopted state would be the only
|
|
214
|
+
* intentId-bearing state with `quotedAddress: null` -- and the panel's
|
|
215
|
+
* ownership fallback (`intent.quotedAddress ?? address`) silently becomes the
|
|
216
|
+
* LIVE wallet, copying this swap's amounts and tx hashes into whichever account
|
|
217
|
+
* the user switches to next.
|
|
218
|
+
* - Build from `INITIAL_INTENT_STATE`, not from `prev`. A spread of `prev` would
|
|
219
|
+
* carry a stale quote, error or receipt hash into a swap it does not describe.
|
|
220
|
+
*
|
|
221
|
+
* `depositDeadline` is the one served fact that must come ACROSS, and it is not
|
|
222
|
+
* an exception to the rule above. No quote is synthesized, because a stale price
|
|
223
|
+
* shown as current would be a lie; a deadline does not go stale, it expires, and
|
|
224
|
+
* carrying it is what lets the poll say so. Omitting it left
|
|
225
|
+
* `pollCeilingAttempts` on its flat 10-minute ceiling while the restored deposit
|
|
226
|
+
* screen counted down the solver's real 30-minute window, so the poll stopped and
|
|
227
|
+
* announced it beside a countdown that disagreed. Null on every pull route.
|
|
228
|
+
*
|
|
229
|
+
* See xochi#339.
|
|
230
|
+
*/
|
|
231
|
+
function buildResumedState(prev, intentId, status, owner, depositDeadline = null) {
|
|
232
|
+
if (!intentId || !owner || !canAdoptResumedIntent(prev))
|
|
233
|
+
return prev;
|
|
234
|
+
return { ...state_js_1.INITIAL_INTENT_STATE, intentId, status, quotedAddress: owner, depositDeadline };
|
|
235
|
+
}
|
|
236
|
+
/** Statuses that render the panel as loading. */
|
|
237
|
+
exports.LOADING_STATES = [
|
|
238
|
+
"quoting",
|
|
239
|
+
"signing",
|
|
240
|
+
"executing",
|
|
241
|
+
"bridging",
|
|
242
|
+
"settling",
|
|
243
|
+
];
|
|
244
|
+
/**
|
|
245
|
+
* The request fields the server reads through a normalisation: an empty
|
|
246
|
+
* `recipientAddress` or `refundAddress` is undefined (EVM routes settle to, and
|
|
247
|
+
* refund to, the connected wallet) and the stealth meta-address is sent ONLY
|
|
248
|
+
* on a stealth settlement. The machine's trade key reads the same values, so
|
|
249
|
+
* two forms that build one request are one trade.
|
|
250
|
+
*/
|
|
251
|
+
function normalizeTradeFields(input) {
|
|
252
|
+
return {
|
|
253
|
+
// Trimmed HERE, at the single point the request is built, because the
|
|
254
|
+
// deposit attestation rebuilds both of these from the same panel state and
|
|
255
|
+
// compares bytes. Sending " 0xabc" while rebuilding "0xabc" is a message
|
|
256
|
+
// that cannot recover, reported to the user as a route being unavailable.
|
|
257
|
+
// One deal, one encoding.
|
|
258
|
+
recipientAddress: input.recipientAddress?.trim() || undefined,
|
|
259
|
+
refundAddress: input.refundAddress?.trim() || undefined,
|
|
260
|
+
stealthMetaAddress: input.settlementPreference === "stealth"
|
|
261
|
+
? (input.stealthMetaAddress ?? undefined)
|
|
262
|
+
: undefined,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Build the quote request body, with the trade fields normalised. */
|
|
266
|
+
function buildQuoteRequest(input) {
|
|
267
|
+
return {
|
|
268
|
+
wallet: input.wallet,
|
|
269
|
+
preview: input.preview,
|
|
270
|
+
allocationKey: input.allocationKey,
|
|
271
|
+
fromChainId: input.fromChainId,
|
|
272
|
+
toChainId: input.toChainId,
|
|
273
|
+
fromToken: input.fromToken,
|
|
274
|
+
toToken: input.toToken,
|
|
275
|
+
fromAmount: input.fromAmount,
|
|
276
|
+
deadline: input.deadline,
|
|
277
|
+
slippageBps: input.slippageBps,
|
|
278
|
+
trustScore: input.trustScore,
|
|
279
|
+
settlementPreference: input.settlementPreference,
|
|
280
|
+
...normalizeTradeFields(input),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Decide whether pricing is indicative or executable.
|
|
285
|
+
*
|
|
286
|
+
* Deposit-origin pricing is always a preview until the caller explicitly asks
|
|
287
|
+
* to allocate a route. That keeps amount editing side-effect free even for a
|
|
288
|
+
* connected wallet. Private settlement still needs wallet-derived keys.
|
|
289
|
+
*/
|
|
290
|
+
function quoteAccessDecision(input) {
|
|
291
|
+
if (input.settlementPreference !== "public") {
|
|
292
|
+
return input.connected ? "executable" : "needs_wallet";
|
|
293
|
+
}
|
|
294
|
+
if (input.isDepositOrigin) {
|
|
295
|
+
if (!input.allocateDepositRoute)
|
|
296
|
+
return "preview";
|
|
297
|
+
return input.connected ? "executable" : "needs_wallet";
|
|
298
|
+
}
|
|
299
|
+
return input.connected ? "executable" : "preview";
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Replace a held preview with a wallet-bound executable quote?
|
|
303
|
+
*
|
|
304
|
+
* True exactly when a wallet is present while a preview is the quote on
|
|
305
|
+
* screen. The caller refreshes silently (the visible price must not flash back
|
|
306
|
+
* to "quoting" on connect) and force-refreshes (a preview prices at Standard
|
|
307
|
+
* tier and carries no signing material, so it must never be served from cache
|
|
308
|
+
* as an executable quote).
|
|
309
|
+
*/
|
|
310
|
+
function shouldUpgradePreviewQuote(input) {
|
|
311
|
+
return (!input.isDepositOrigin &&
|
|
312
|
+
input.connected &&
|
|
313
|
+
input.hasAmount &&
|
|
314
|
+
input.status === "quoted" &&
|
|
315
|
+
input.quoteIsPreview);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The signing gate. A preview is never signable whatever else it claims: the
|
|
319
|
+
* server strips its EIP-712 payload, so the wallet/session boundary stays at
|
|
320
|
+
* execution even though the price itself was served anonymously.
|
|
321
|
+
*/
|
|
322
|
+
function canSignQuote(status, quote) {
|
|
323
|
+
return status === "quoted" && quote !== null && quote.canSolve && !quote.preview;
|
|
324
|
+
}
|
|
325
|
+
/** True when the current indicative deposit price can be explicitly allocated. */
|
|
326
|
+
function canAllocateDepositQuote(connected, isDepositOrigin, status, quote) {
|
|
327
|
+
return (connected &&
|
|
328
|
+
isDepositOrigin &&
|
|
329
|
+
status === "quoted" &&
|
|
330
|
+
quote !== null &&
|
|
331
|
+
quote.canSolve &&
|
|
332
|
+
quote.preview === true);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The pre-expiry auto-refresh delay in ms: how long until we should re-fetch to
|
|
336
|
+
* replace an expiring quote (`buffer` before expiry). A non-positive result means
|
|
337
|
+
* the quote is already inside the buffer window and no refresh is scheduled.
|
|
338
|
+
*/
|
|
339
|
+
function computeRefreshDelayMs(expiryIso, now, bufferMs) {
|
|
340
|
+
return new Date(expiryIso).getTime() - now - bufferMs;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Whether the expiry timer should re-quote.
|
|
344
|
+
*
|
|
345
|
+
* Indicative pricing is allocation-free on every origin and may stay live.
|
|
346
|
+
* Once a deposit address exists, its `deposit_deadline` owns the lifecycle; a
|
|
347
|
+
* 30-second price refresh must never replace or invalidate that route.
|
|
348
|
+
*/
|
|
349
|
+
function shouldRefreshQuote(input) {
|
|
350
|
+
if (input.status !== "quoted")
|
|
351
|
+
return false;
|
|
352
|
+
if (input.hidden)
|
|
353
|
+
return false;
|
|
354
|
+
return !input.hasAllocatedDeposit;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Whether returning to a foregrounded tab should re-quote.
|
|
358
|
+
*
|
|
359
|
+
* The confirm modal owns its own refresh. An allocated deposit route is also
|
|
360
|
+
* immutable until its deposit deadline; all other quoted/expired prices may be
|
|
361
|
+
* refreshed on return.
|
|
362
|
+
*/
|
|
363
|
+
function shouldRefreshOnReturn(input) {
|
|
364
|
+
if (input.paused || input.hasAllocatedDeposit)
|
|
365
|
+
return false;
|
|
366
|
+
return input.status === "quoted" || input.status === "expired";
|
|
367
|
+
}
|
|
368
|
+
const WALLET_CHANGED = {
|
|
369
|
+
kind: "reject",
|
|
370
|
+
to: "quoted",
|
|
371
|
+
refusal: { kind: "wallet_changed" },
|
|
372
|
+
error: "The wallet or network changed. Get a fresh quote.",
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Decide whether a quote may be signed, in the exact precedence the hook
|
|
376
|
+
* enforces (all fail-closed, no funds move on a reject):
|
|
377
|
+
* 1. Deposit route (Tron origin) -> no signature; reveal the address.
|
|
378
|
+
* 2. No EIP-712 payload -> nothing to sign.
|
|
379
|
+
* 3. Wallet account changed since the quote -> re-quote (`quoted`).
|
|
380
|
+
* 4. Wallet not on the origin chain -> refuse (`quoted`); the domain binds it.
|
|
381
|
+
* 5. Wallet chain changed since the quote -> re-quote (`quoted`).
|
|
382
|
+
* 6. Quote already expired -> `expired`.
|
|
383
|
+
* 7. Otherwise proceed.
|
|
384
|
+
*/
|
|
385
|
+
function decideSignPreflight(input) {
|
|
386
|
+
if (input.hasDepositAddress)
|
|
387
|
+
return { kind: "awaitingDeposit" };
|
|
388
|
+
if (!input.hasEip712)
|
|
389
|
+
return { kind: "noQuote" };
|
|
390
|
+
const accountChanged = !input.address ||
|
|
391
|
+
(input.quotedAddress != null &&
|
|
392
|
+
input.address.toLowerCase() !== input.quotedAddress.toLowerCase());
|
|
393
|
+
if (accountChanged)
|
|
394
|
+
return WALLET_CHANGED;
|
|
395
|
+
// A signature from any other chain is one the solver rejects, and on a chain
|
|
396
|
+
// the wallet cannot serve it fails as an opaque wallet error. The panel
|
|
397
|
+
// prompts the switch before this is reachable; this is the backstop.
|
|
398
|
+
if (input.chainId !== input.originChainId) {
|
|
399
|
+
return {
|
|
400
|
+
kind: "reject",
|
|
401
|
+
to: "quoted",
|
|
402
|
+
refusal: { kind: "wrong_chain", chainId: input.originChainId },
|
|
403
|
+
error: `Switch the wallet to ${index_js_1.EXTENDED_CHAIN_NAMES[input.originChainId]} to sign.`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const chainChanged = input.quotedChainId != null && input.chainId !== input.quotedChainId;
|
|
407
|
+
if (chainChanged)
|
|
408
|
+
return WALLET_CHANGED;
|
|
409
|
+
if (input.expiryMs && input.now >= input.expiryMs) {
|
|
410
|
+
return { kind: "reject", to: "expired", error: "Quote expired" };
|
|
411
|
+
}
|
|
412
|
+
return { kind: "proceed" };
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Interpret an /execute response, preserving the in-doubt distinction (#226):
|
|
416
|
+
* the worker returns `{success:false}` for BOTH a genuine failure AND a
|
|
417
|
+
* fail-closed downgrade (the origin pull already moved funds but the
|
|
418
|
+
* settlement is unconfirmed inline, so IN-DOUBT). `reconciling` is the
|
|
419
|
+
* server's flag when it sends one, else on whenever the server withheld
|
|
420
|
+
* success (`reconciling ?? !success`). The verdicts, in order:
|
|
421
|
+
* - `failed` with `success: false` and `reconciling` off by that rule (the
|
|
422
|
+
* server sent `reconciling: false`) is final: Retry only with the Worker's
|
|
423
|
+
* nothing-moved marker, else "may have moved" (`failureMayHaveMoved`).
|
|
424
|
+
* - `completed` completes with reconciling off, whatever the reply says: a
|
|
425
|
+
* completion leaves nothing to reconcile.
|
|
426
|
+
* - Any other `failed` (`success: true`, or `reconciling` on, the absent flag
|
|
427
|
+
* included) is in doubt: it moves to the in-flight `executing` with
|
|
428
|
+
* reconciling on, and the poll reads the final status.
|
|
429
|
+
* - Anything else advances to its status with reconciling by that rule, so a
|
|
430
|
+
* withheld success shows the "Still confirming" path with no Retry (an
|
|
431
|
+
* in-session retry of a funds-moved swap would be a double execution).
|
|
432
|
+
* `onExecuted` also shows an unmapped status in doubt.
|
|
433
|
+
*/
|
|
434
|
+
function decideExecuteResult(result) {
|
|
435
|
+
const reconciling = result.reconciling ?? !result.success;
|
|
436
|
+
if (result.status === "failed" && !result.success && !reconciling) {
|
|
437
|
+
return {
|
|
438
|
+
kind: "failed",
|
|
439
|
+
error: result.error || "Execution failed",
|
|
440
|
+
mayHaveMoved: failureMayHaveMoved(result.substatus),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const base = {
|
|
444
|
+
txHash: result.txHash || null,
|
|
445
|
+
receivingTxHash: result.receivingTxHash || null,
|
|
446
|
+
settlementTxHash: result.settlementTxHash || null,
|
|
447
|
+
settlementChainId: result.settlementChainId ?? null,
|
|
448
|
+
};
|
|
449
|
+
if (result.status === "completed") {
|
|
450
|
+
return { kind: "transition", status: "completed", updates: { ...base, reconciling: false } };
|
|
451
|
+
}
|
|
452
|
+
if (result.status === "failed") {
|
|
453
|
+
return { kind: "transition", status: "executing", updates: { ...base, reconciling: true } };
|
|
454
|
+
}
|
|
455
|
+
return { kind: "transition", status: result.status, updates: { ...base, reconciling } };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* How far before `now` a not-accepted `retryAfter` may lie and still be read
|
|
459
|
+
* as the absolute target it must be. The transport rounds the target up from
|
|
460
|
+
* the moment it read the refusal, so a real one is never older than the
|
|
461
|
+
* report's dispatch delay; a minute is past any quote's life (30 s), so no
|
|
462
|
+
* resend is lost to it. An older value is a delta passed where the absolute
|
|
463
|
+
* target belongs (`retry_after: 7` read as 1970), which would resend at once
|
|
464
|
+
* and ignore the server's wait.
|
|
465
|
+
*/
|
|
466
|
+
const RETRY_AFTER_MAX_AGE_SECS = 60;
|
|
467
|
+
/**
|
|
468
|
+
* After the server refused an execute before accepting anything, whether to
|
|
469
|
+
* send the SAME request again and when (rules: README "Fund safety").
|
|
470
|
+
* `retryAfter` and `deadline` are unix seconds, `now` the host's clock; a null
|
|
471
|
+
* `deadline` never resends, a `retryAfter` older than
|
|
472
|
+
* `RETRY_AFTER_MAX_AGE_SECS` gives up, and the wait is at least a second, so
|
|
473
|
+
* a slightly past `retryAfter` cannot spin. A give-up offers no Retry: the
|
|
474
|
+
* signed request stays executable until its deadline, so the machine shows
|
|
475
|
+
* "not sent yet" (`IntentState.notSent`) and polls until the Worker finishes
|
|
476
|
+
* the intent.
|
|
477
|
+
*/
|
|
478
|
+
function decideExecuteResend(input) {
|
|
479
|
+
if (input.resends >= index_js_1.MAX_EXECUTE_RESENDS)
|
|
480
|
+
return { kind: "give_up", reason: "exhausted" };
|
|
481
|
+
const { retryAfter, deadline } = input;
|
|
482
|
+
if (retryAfter === undefined || !Number.isFinite(retryAfter)) {
|
|
483
|
+
return { kind: "give_up", reason: "no_retry_after" };
|
|
484
|
+
}
|
|
485
|
+
if (retryAfter * 1000 < input.now - RETRY_AFTER_MAX_AGE_SECS * 1000) {
|
|
486
|
+
return { kind: "give_up", reason: "stale_retry_after" };
|
|
487
|
+
}
|
|
488
|
+
const sendAt = Math.max(retryAfter * 1000, input.now + 1000);
|
|
489
|
+
if (deadline === null || sendAt > (deadline - index_js_1.EXECUTE_RESEND_MARGIN_SECS) * 1000) {
|
|
490
|
+
return { kind: "give_up", reason: "deadline" };
|
|
491
|
+
}
|
|
492
|
+
return { kind: "resend", delayMs: sendAt - input.now };
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The earliest deadline (unix seconds) a resent execute carries: the quote's
|
|
496
|
+
* expiry, the signed intent's `deadline` and the origin pull's `validBefore` /
|
|
497
|
+
* `deadline`. Null without a readable quote expiry, which never resends.
|
|
498
|
+
*/
|
|
499
|
+
function executeDeadline(quote) {
|
|
500
|
+
const expiryMs = quote ? new Date(quote.expiry).getTime() : NaN;
|
|
501
|
+
if (!quote || !Number.isFinite(expiryMs))
|
|
502
|
+
return null;
|
|
503
|
+
const signed = [
|
|
504
|
+
quote.eip712Data?.message?.deadline,
|
|
505
|
+
quote.pullAuthorization?.message?.validBefore,
|
|
506
|
+
quote.pullAuthorization?.message?.deadline,
|
|
507
|
+
].flatMap((v) => {
|
|
508
|
+
const n = typeof v === "string" && /^\d+$/.test(v) ? Number(v) : v;
|
|
509
|
+
return typeof n === "number" && Number.isFinite(n) ? [n] : [];
|
|
510
|
+
});
|
|
511
|
+
return Math.min(Math.floor(expiryMs / 1000), ...signed);
|
|
512
|
+
}
|
|
513
|
+
// ============================================================
|
|
514
|
+
// Status update (poll / message -> transition)
|
|
515
|
+
// ============================================================
|
|
516
|
+
/**
|
|
517
|
+
* Map an intent status message (from the poll) to the transition to apply. A
|
|
518
|
+
* refund is fund-safe and terminal: its reason rides `refundReason` (plain
|
|
519
|
+
* language), never `error`. Mirrors the poll's fail-closed handling.
|
|
520
|
+
*/
|
|
521
|
+
function mapStatusUpdate(msg) {
|
|
522
|
+
if (msg.status === "completed") {
|
|
523
|
+
return {
|
|
524
|
+
status: "completed",
|
|
525
|
+
updates: {
|
|
526
|
+
txHash: msg.txHash || null,
|
|
527
|
+
receivingTxHash: msg.receivingTxHash || null,
|
|
528
|
+
settlementTxHash: msg.settlementTxHash || null,
|
|
529
|
+
settlementChainId: msg.settlementChainId ?? null,
|
|
530
|
+
reconciling: false,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (msg.status === "refunded") {
|
|
535
|
+
return {
|
|
536
|
+
status: "refunded",
|
|
537
|
+
updates: {
|
|
538
|
+
error: null,
|
|
539
|
+
reconciling: false,
|
|
540
|
+
refundReason: (0, index_js_1.getRefundReasonLabel)(msg.refundReason),
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (msg.status === "failed") {
|
|
545
|
+
return {
|
|
546
|
+
status: "failed",
|
|
547
|
+
updates: { error: msg.error || "Intent failed", reconciling: false },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
status: msg.status,
|
|
552
|
+
updates: {
|
|
553
|
+
receivingTxHash: msg.receivingTxHash || null,
|
|
554
|
+
settlementTxHash: msg.settlementTxHash || null,
|
|
555
|
+
settlementChainId: msg.settlementChainId ?? null,
|
|
556
|
+
substatus: msg.substatus ?? null,
|
|
557
|
+
reconciling: msg.reconciling ?? false,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Read a lifecycle frame from the intent stream (`IntentStatusFrame`, the
|
|
563
|
+
* worker's `{ type: "status", intent_id, status, revision, ... }`, D5 5.9).
|
|
564
|
+
*
|
|
565
|
+
* Returns null for anything that is not a CURRENT status frame: a value that
|
|
566
|
+
* does not parse, or a frame at or below `lastRevision` (the relay replays its
|
|
567
|
+
* buffer to every (re)connecting subscriber and deferred publishes can land out
|
|
568
|
+
* of order, so a stale frame must not be read as the current status).
|
|
569
|
+
*
|
|
570
|
+
* Otherwise returns the frame's `revision`, which the caller records, and the
|
|
571
|
+
* status `message` for `handleStatusUpdate`, or a null message when the status
|
|
572
|
+
* is outside POLL_INFLIGHT_STATES (terminals included). The frame carries none
|
|
573
|
+
* of what the poll returns with a terminal (receipt hashes, the failure reason,
|
|
574
|
+
* the refund reason; `tx_hash` only on a public settlement), and a terminal
|
|
575
|
+
* status stops the poll. Applying one would pin the final view without its
|
|
576
|
+
* receipt link or real reason, so the poll delivers terminals, whole, within
|
|
577
|
+
* one interval. The stream accelerates the in-flight steps.
|
|
578
|
+
*/
|
|
579
|
+
function mapStatusFrame(value, lastRevision) {
|
|
580
|
+
const parsed = index_js_1.IntentStatusFrameSchema.safeParse(value);
|
|
581
|
+
if (!parsed.success)
|
|
582
|
+
return null;
|
|
583
|
+
const frame = parsed.data;
|
|
584
|
+
if (frame.revision !== null && lastRevision !== null && frame.revision <= lastRevision) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (!exports.POLL_INFLIGHT_STATES.includes(frame.status)) {
|
|
588
|
+
return { revision: frame.revision, message: null };
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
revision: frame.revision,
|
|
592
|
+
message: {
|
|
593
|
+
type: "intent_status",
|
|
594
|
+
intentId: frame.intent_id,
|
|
595
|
+
status: frame.status,
|
|
596
|
+
txHash: frame.tx_hash,
|
|
597
|
+
timestamp: new Date(frame.ts).toISOString(),
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The drift warning to log when the in-flight status poll gets a definitive 404
|
|
603
|
+
* for a DISPATCHED intent id, or null when this id has already been reported.
|
|
604
|
+
*
|
|
605
|
+
* The poll's decision on that 404 is to HOLD (never terminalize; see the status
|
|
606
|
+
* poll in machine.ts for why), and a hold that emits nothing is indistinguishable from a
|
|
607
|
+
* healthy poll: the modal spins, the poll runs to its ~10 min cap, and no console
|
|
608
|
+
* line says which swap it is. So the first 404 per id carries a signal, in the
|
|
609
|
+
* same shape CLAUDE.md already relies on for `mapRiddlerStatus` drift (a warn
|
|
610
|
+
* carrying the intent id, catchable in logs).
|
|
611
|
+
*
|
|
612
|
+
* `reportedId` is the last id the caller reported, so the warning is one-shot:
|
|
613
|
+
* the poll runs every 4s and would otherwise emit ~150 identical lines per stuck
|
|
614
|
+
* swap. A different id reports again -- a second swap hitting this is a second
|
|
615
|
+
* data point, not a repeat.
|
|
616
|
+
*
|
|
617
|
+
* Distinct from the transport-level warn in `intentAPI.getIntentStatus`, which
|
|
618
|
+
* fires once per id for ANY 404, including the benign one the orphan-quote sweep
|
|
619
|
+
* produces for a pre-dispatch row (and can be spent on that benign case by
|
|
620
|
+
* whichever caller polls the id first). This one names the dispatched case, which
|
|
621
|
+
* is the bug.
|
|
622
|
+
*/
|
|
623
|
+
function notFoundDriftWarning(intentId, reportedId) {
|
|
624
|
+
if (!intentId || intentId === reportedId)
|
|
625
|
+
return null;
|
|
626
|
+
return `[useIntent] Status poll 404 for dispatched intent ${intentId}: holding, not failing. The worker has no row for a swap that was already dispatched; leaving the outcome to reconciliation.`;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Fields to merge from a poll whose status did NOT change, or null when the
|
|
630
|
+
* poll carries nothing new.
|
|
631
|
+
*
|
|
632
|
+
* The status-transition table has no self-edges, so a `settling -> settling`
|
|
633
|
+
* poll is an invalid move and `applyStatusTransition` discards the entire
|
|
634
|
+
* update, receipt hashes included. A stealth swap sits at `settling` for its
|
|
635
|
+
* whole life, so that edge is never valid and the fill hash the server returns
|
|
636
|
+
* on every 4s poll never reached the UI: the user was shown "Still confirming"
|
|
637
|
+
* with no transaction link while the settlement was on chain (xochi#353).
|
|
638
|
+
*
|
|
639
|
+
* Two deliberate omissions:
|
|
640
|
+
*
|
|
641
|
+
* - A field that did not ARRIVE is not applied. Blanking a hash we already hold
|
|
642
|
+
* because a later poll omitted it would flicker the receipt link back off.
|
|
643
|
+
* - `reconciling` is never merged. The poll owns that flag on its own timer, and
|
|
644
|
+
* re-applying `mapStatusUpdate`'s default `false` here would switch the
|
|
645
|
+
* "safe to close" view off again on every tick.
|
|
646
|
+
*
|
|
647
|
+
* Returns null when nothing changed, so the caller can skip the setState and
|
|
648
|
+
* avoid a re-render per poll.
|
|
649
|
+
*/
|
|
650
|
+
function mergeSameStatusReceipt(prev, msg) {
|
|
651
|
+
const updates = {};
|
|
652
|
+
if (msg.substatus && msg.substatus !== prev.substatus) {
|
|
653
|
+
updates.substatus = msg.substatus;
|
|
654
|
+
}
|
|
655
|
+
if (msg.receivingTxHash && msg.receivingTxHash !== prev.receivingTxHash) {
|
|
656
|
+
updates.receivingTxHash = msg.receivingTxHash;
|
|
657
|
+
}
|
|
658
|
+
if (msg.settlementTxHash && msg.settlementTxHash !== prev.settlementTxHash) {
|
|
659
|
+
updates.settlementTxHash = msg.settlementTxHash;
|
|
660
|
+
}
|
|
661
|
+
if (msg.settlementChainId && msg.settlementChainId !== prev.settlementChainId) {
|
|
662
|
+
updates.settlementChainId = msg.settlementChainId;
|
|
663
|
+
}
|
|
664
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
665
|
+
}
|