@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -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 +411 -0
  19. package/dist/cjs/machine/machine.js +1627 -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 +16 -0
  48. package/dist/cjs/shared/index.js +32 -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/shieldedPreview.d.ts +841 -0
  56. package/dist/cjs/shared/shieldedPreview.js +128 -0
  57. package/dist/cjs/shared/stealth.d.ts +53 -0
  58. package/dist/cjs/shared/stealth.js +68 -0
  59. package/dist/cjs/shared/trading.d.ts +153 -0
  60. package/dist/cjs/shared/trading.js +185 -0
  61. package/dist/cjs/shared/types.d.ts +37 -0
  62. package/dist/cjs/shared/types.js +7 -0
  63. package/dist/cjs/shared/validators.d.ts +30 -0
  64. package/dist/cjs/shared/validators.js +79 -0
  65. package/dist/cjs/transport/client.d.ts +234 -0
  66. package/dist/cjs/transport/client.js +1456 -0
  67. package/dist/esm/claim.d.ts +33 -0
  68. package/dist/esm/claim.js +41 -0
  69. package/dist/esm/fee.d.ts +247 -0
  70. package/dist/esm/fee.js +285 -0
  71. package/dist/esm/index.d.ts +18 -0
  72. package/dist/esm/index.js +17 -0
  73. package/dist/esm/machine/claimInfo.d.ts +29 -0
  74. package/dist/esm/machine/claimInfo.js +23 -0
  75. package/dist/esm/machine/decisions.d.ts +452 -0
  76. package/dist/esm/machine/decisions.js +640 -0
  77. package/dist/esm/machine/deposit.d.ts +113 -0
  78. package/dist/esm/machine/deposit.js +260 -0
  79. package/dist/esm/machine/index.d.ts +23 -0
  80. package/dist/esm/machine/index.js +23 -0
  81. package/dist/esm/machine/machine.d.ts +411 -0
  82. package/dist/esm/machine/machine.js +1621 -0
  83. package/dist/esm/machine/phase.d.ts +24 -0
  84. package/dist/esm/machine/phase.js +42 -0
  85. package/dist/esm/machine/quoteChecks.d.ts +41 -0
  86. package/dist/esm/machine/quoteChecks.js +78 -0
  87. package/dist/esm/machine/receipt.d.ts +151 -0
  88. package/dist/esm/machine/receipt.js +64 -0
  89. package/dist/esm/machine/recipientEcho.d.ts +16 -0
  90. package/dist/esm/machine/recipientEcho.js +24 -0
  91. package/dist/esm/machine/solverPin.d.ts +110 -0
  92. package/dist/esm/machine/solverPin.js +210 -0
  93. package/dist/esm/machine/state.d.ts +167 -0
  94. package/dist/esm/machine/state.js +28 -0
  95. package/dist/esm/machine/transition.d.ts +15 -0
  96. package/dist/esm/machine/transition.js +17 -0
  97. package/dist/esm/machine/validateRequest.d.ts +15 -0
  98. package/dist/esm/machine/validateRequest.js +123 -0
  99. package/dist/esm/machine/view.d.ts +3 -0
  100. package/dist/esm/machine/view.js +71 -0
  101. package/dist/esm/machine/wire.d.ts +527 -0
  102. package/dist/esm/machine/wire.js +266 -0
  103. package/dist/esm/shared/api.d.ts +9 -0
  104. package/dist/esm/shared/api.js +4 -0
  105. package/dist/esm/shared/chains.d.ts +243 -0
  106. package/dist/esm/shared/chains.js +236 -0
  107. package/dist/esm/shared/eip712.d.ts +44 -0
  108. package/dist/esm/shared/eip712.js +61 -0
  109. package/dist/esm/shared/index.d.ts +16 -0
  110. package/dist/esm/shared/index.js +16 -0
  111. package/dist/esm/shared/intentStream.d.ts +74 -0
  112. package/dist/esm/shared/intentStream.js +55 -0
  113. package/dist/esm/shared/intents.d.ts +88 -0
  114. package/dist/esm/shared/intents.js +213 -0
  115. package/dist/esm/shared/schemas.d.ts +928 -0
  116. package/dist/esm/shared/schemas.js +521 -0
  117. package/dist/esm/shared/shieldedPreview.d.ts +841 -0
  118. package/dist/esm/shared/shieldedPreview.js +124 -0
  119. package/dist/esm/shared/stealth.d.ts +53 -0
  120. package/dist/esm/shared/stealth.js +64 -0
  121. package/dist/esm/shared/trading.d.ts +153 -0
  122. package/dist/esm/shared/trading.js +182 -0
  123. package/dist/esm/shared/types.d.ts +37 -0
  124. package/dist/esm/shared/types.js +6 -0
  125. package/dist/esm/shared/validators.d.ts +30 -0
  126. package/dist/esm/shared/validators.js +74 -0
  127. package/dist/esm/transport/client.d.ts +234 -0
  128. package/dist/esm/transport/client.js +1447 -0
  129. package/package.json +61 -4
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * Fail-closed pin for origin-pull authorizations: binds both WHERE the funds go
4
+ * and HOW MUCH, before the wallet signs.
5
+ *
6
+ * Two axes can move funds on the origin leg. The recipient (pull `to` for
7
+ * ERC-3009, `spender` for Permit2) decides who collects. The amount (`value` for
8
+ * ERC-3009, `permitted.amount` for Permit2) decides how much. The user's
9
+ * signature is the on-chain authority for both (ERC-3009 moves exactly `value`,
10
+ * Permit2 caps the pull at `permitted.amount`), so a pull whose amount exceeds
11
+ * the intended input can overdraw the wallet even to a correctly-pinned
12
+ * recipient. This pin binds the recipient to an operator allowlist AND the amount
13
+ * to the intended input, refusing to sign on either mismatch. The trust anchors
14
+ * are the client's own config and the user's own quoted amount, neither of which
15
+ * the quote server can influence.
16
+ *
17
+ * Defense in depth: the Xochi worker independently refuses to serve a quote whose
18
+ * pull amount does not equal `from_amount` (packages/worker-lib), and the
19
+ * on-chain token / Permit2 contract verifies the signature over the amount
20
+ * actually pulled. So a compromised quote server cannot overdraw even if this
21
+ * client check is bypassed.
22
+ *
23
+ * Permit2 has no on-chain recipient guard (the spender chooses the recipient at
24
+ * call time), so its recipient is ALWAYS fail-closed: an unpinned permit2 pull is
25
+ * rejected. ERC-3009 binds `to` in the signed digest and the token enforces
26
+ * `msg.sender == to`, so an unpinned `to` stays bounded (accepted unless a hard
27
+ * pin is requested). Once an allowlist is configured, both methods enforce it.
28
+ *
29
+ * Mirrors the raxol agent-side pin (`solver_allowed?/2`). See GitHub #333
30
+ * (recipient pin) and the #170 origin-leg amount binding.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.validateSolverPin = validateSolverPin;
34
+ exports.validatePayoutPin = validatePayoutPin;
35
+ const index_js_1 = require("../shared/index.js");
36
+ const ERC3009_TYPE = "ReceiveWithAuthorization";
37
+ const PERMIT2_TYPE = "PermitWitnessTransferFrom";
38
+ /**
39
+ * Validate a served origin-pull authorization against the solver pin. Returns
40
+ * `{ ok: true }` when the wallet may sign, or `{ ok: false, field, reason }` to
41
+ * abort before signing.
42
+ *
43
+ * `expectedAmount` is the intended input amount (origin-token smallest units, the
44
+ * user's own quoted `from_amount`). The pull is rejected unless its collected
45
+ * amount equals it, so a served pull cannot overdraw beyond what the user meant
46
+ * to spend even when the recipient is correctly pinned.
47
+ */
48
+ function validateSolverPin(pull, config, expectedAmount) {
49
+ // message values are `unknown` off the wire; checkRecipient guards the field.
50
+ const message = pull.message;
51
+ let recipient;
52
+ switch (pull.primaryType) {
53
+ case ERC3009_TYPE:
54
+ recipient = checkRecipient("pull_to", message.to, "erc3009", config.erc3009Allowlist, config);
55
+ break;
56
+ case PERMIT2_TYPE:
57
+ recipient = checkRecipient("pull_spender", message.spender, "permit2", config.permit2Allowlist, config);
58
+ break;
59
+ default:
60
+ return {
61
+ ok: false,
62
+ field: "pull_type",
63
+ reason: `unsupported origin-pull authorization type: ${pull.primaryType}`,
64
+ };
65
+ }
66
+ if (!recipient.ok)
67
+ return recipient;
68
+ return checkAmount(pull, expectedAmount);
69
+ }
70
+ /**
71
+ * Bind the DESTINATION leg of the intent payload to what the client rendered,
72
+ * before the wallet signs it.
73
+ *
74
+ * `validateSolverPin` above protects the origin leg: how much leaves the wallet
75
+ * and who collects it. This is the mirror for the other end -- how much comes
76
+ * back and who receives it -- and it exists because those two fields are the only
77
+ * ones a user cannot verify by reading a wallet prompt. A payload whose
78
+ * `toAmount` is lower than the number on screen, or whose `recipient` is an
79
+ * address the user never chose, produces a wallet prompt that looks entirely
80
+ * normal.
81
+ *
82
+ * The trust anchor is the same as the origin pin's and that is the whole point:
83
+ * the expectation comes from the client's own rendered quote, never from another
84
+ * field of the same served response. The Xochi worker independently refuses to
85
+ * serve a quote whose `eip712.message.toAmount` disagrees with the `to_amount` it
86
+ * advertises (`servedPayoutMismatch`, packages/worker-lib), so against a healthy
87
+ * backend this check never fires. It covers what that one structurally cannot:
88
+ * anything that reaches the payload after the worker has vouched for it, or a
89
+ * worker that is itself compromised. A firing check here is an incident, not a
90
+ * tuning problem.
91
+ *
92
+ * Fail-closed on a missing or unreadable field: never sign a payout we cannot
93
+ * read.
94
+ */
95
+ function validatePayoutPin(intent, expected) {
96
+ const message = intent.message;
97
+ const servedAmount = message.toAmount;
98
+ if (typeof servedAmount !== "string" && typeof servedAmount !== "number") {
99
+ return {
100
+ ok: false,
101
+ field: "payout_amount",
102
+ reason: "intent payout amount is missing or unparseable",
103
+ };
104
+ }
105
+ if (!sameAmount(servedAmount, expected.toAmount)) {
106
+ return {
107
+ ok: false,
108
+ field: "payout_amount",
109
+ reason: `intent payout ${String(servedAmount)} does not match the quoted output ${expected.toAmount}`,
110
+ };
111
+ }
112
+ // Fail closed rather than skip: not knowing the expected recipient is exactly
113
+ // the case where signing blind is worst.
114
+ if (!expected.recipient) {
115
+ return {
116
+ ok: false,
117
+ field: "payout_recipient",
118
+ reason: "no expected recipient to check the payout against",
119
+ };
120
+ }
121
+ const servedRecipient = message.recipient;
122
+ if (typeof servedRecipient !== "string" || servedRecipient === "") {
123
+ return {
124
+ ok: false,
125
+ field: "payout_recipient",
126
+ reason: "intent payout recipient is missing or unparseable",
127
+ };
128
+ }
129
+ if (!sameRecipient(servedRecipient, expected.recipient)) {
130
+ return {
131
+ ok: false,
132
+ field: "payout_recipient",
133
+ reason: `intent payout recipient ${servedRecipient} is not the address this quote was for`,
134
+ };
135
+ }
136
+ return { ok: true };
137
+ }
138
+ /**
139
+ * Compare two smallest-unit amounts by value, not by spelling.
140
+ *
141
+ * A string compare would reject `"0998000"` or a numeric `998000` against
142
+ * `"998000"` even though both mean the same payout, and this check fails closed:
143
+ * a false negative blocks a legitimate swap at the wallet prompt. BigInt is also
144
+ * required for correctness at scale, since an 18-decimal payout is past
145
+ * `Number.MAX_SAFE_INTEGER`. Falls back to exact string equality only when a
146
+ * value will not parse as an integer, so an unexpected format still has to match
147
+ * verbatim rather than being waved through.
148
+ */
149
+ function sameAmount(served, expected) {
150
+ try {
151
+ return BigInt(served) === BigInt(expected);
152
+ }
153
+ catch {
154
+ return String(served) === expected;
155
+ }
156
+ }
157
+ /**
158
+ * Compare two recipients per address family. An EVM (`0x`-hex) pair compares
159
+ * case-insensitively; anything else (base58 Tron/Solana) is case-sensitive and
160
+ * compares verbatim, because lowercasing a base58 address changes it. Mirrors
161
+ * the worker-side rule and Riddler's own `token_value/1`.
162
+ */
163
+ function sameRecipient(a, b) {
164
+ return a.startsWith("0x") && b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
165
+ }
166
+ /**
167
+ * Bind the pull's collected amount to the intended input. Fail-closed: a
168
+ * missing/unparseable amount is rejected (never sign an authorization whose value
169
+ * we cannot read), as is any amount that does not match exactly. Riddler builds
170
+ * the pull value straight from `from_amount` with no fee or buffer, so exact
171
+ * equality is correct.
172
+ */
173
+ function checkAmount(pull, expectedAmount) {
174
+ const amount = (0, index_js_1.extractPullAmount)(pull);
175
+ if (amount === null) {
176
+ return {
177
+ ok: false,
178
+ field: "pull_amount",
179
+ reason: "origin-pull amount is missing or unparseable",
180
+ };
181
+ }
182
+ if (amount !== expectedAmount) {
183
+ return {
184
+ ok: false,
185
+ field: "pull_amount",
186
+ reason: `origin-pull amount ${amount.toString()} does not match the intended input ${expectedAmount.toString()}`,
187
+ };
188
+ }
189
+ return { ok: true };
190
+ }
191
+ function checkRecipient(field, recipient, method, allowlist, config) {
192
+ const addr = typeof recipient === "string" ? recipient.toLowerCase() : null;
193
+ if (allowlist.length === 0) {
194
+ // No pin configured for this method. ERC-3009 is bounded by the signed `to`
195
+ // + on-chain msg.sender == to, so accept unless a hard pin is demanded.
196
+ // Permit2 has no such guard, so it must always be pinned.
197
+ if (method === "erc3009" && !config.requirePin)
198
+ return { ok: true };
199
+ return {
200
+ ok: false,
201
+ field,
202
+ reason: method === "permit2"
203
+ ? "permit2 origin pull requires a pinned spender address"
204
+ : "solver pin required but no allowlist is configured",
205
+ };
206
+ }
207
+ if (addr !== null && allowlist.includes(addr))
208
+ return { ok: true };
209
+ return {
210
+ ok: false,
211
+ field,
212
+ reason: `origin-pull ${field} ${String(recipient)} is not an allowed solver address`,
213
+ };
214
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The intent lifecycle's state, and the error value it carries.
3
+ */
4
+ import type { ChainId, IntentStatus, SelectableChainId, SettlementType } from "../shared/index.js";
5
+ import type { QuoteResponse } from "./wire.js";
6
+ import type { StoredDepositAttestation } from "./deposit.js";
7
+ /**
8
+ * A failure the UI can categorize by HTTP code and, on 429/503 backpressure,
9
+ * run a retry countdown for. Most failures are a plain string instead.
10
+ */
11
+ export interface StructuredError {
12
+ message: string;
13
+ code?: number;
14
+ retryAfter?: number;
15
+ reason?: string;
16
+ }
17
+ /**
18
+ * Why a sign ceremony stopped before the wallet was asked for the signature
19
+ * that moves funds, while the quote is still the one to show. Typed so the
20
+ * renderer words it and nothing parses a message. No funds move on any of
21
+ * these: the first three stop Confirm before any prompt; the pull kinds stop
22
+ * after signature 1, before signature 2 (the origin pull) is requested.
23
+ */
24
+ export type SignRefusal =
25
+ /** The wallet is not on the origin chain the signature binds. */
26
+ {
27
+ kind: "wrong_chain";
28
+ chainId: SelectableChainId;
29
+ }
30
+ /** The account or network changed since this quote was made. */
31
+ | {
32
+ kind: "wallet_changed";
33
+ }
34
+ /** The payload to sign does not pay what the quote shows, to whom it shows. */
35
+ | {
36
+ kind: "payout_mismatch";
37
+ }
38
+ /** The served origin-pull authorization names an unpinned spender/recipient or amount. */
39
+ | {
40
+ kind: "pull_unpinned";
41
+ }
42
+ /** The token approval the origin pull needs was not granted. */
43
+ | {
44
+ kind: "approval_required";
45
+ };
46
+ export interface IntentState {
47
+ status: IntentStatus;
48
+ intentId: string | null;
49
+ quoteId: string | null;
50
+ quote: QuoteResponse | null;
51
+ error: string | StructuredError | null;
52
+ txHash: string | null;
53
+ receivingTxHash: string | null;
54
+ /** Public fill receipt (Riddler #632) + its chain; drives the receipt CTA and the settled state. */
55
+ settlementTxHash: string | null;
56
+ settlementChainId: ChainId | null;
57
+ /**
58
+ * Riddler's granular fill substatus, verbatim (`filling` | `filled` | ...).
59
+ * Read only to tell a delivered destination leg apart from one still in
60
+ * flight: Riddler can hold an intent at top-level `settling` with
61
+ * `substatus: filled` indefinitely (xochi#376). Never advances the lifecycle.
62
+ * Also `filled` when a status poll reads `settlementConfirmed` (a private
63
+ * intent's fill confirmed, its receipt withheld from an anonymous read).
64
+ */
65
+ substatus: string | null;
66
+ /**
67
+ * True while the latest status poll was the server's anonymous view of a
68
+ * stealth or shielded intent (xochi#563): its on-chain legs were withheld, so
69
+ * null hashes mean "not shown", not "not there". The host offers wallet SIWE
70
+ * recovery on it; a later owner read (no marker) clears it. Never a token.
71
+ */
72
+ legsWithheld: boolean;
73
+ reconciling: boolean;
74
+ /** Plain-language refund explanation; set only on a `refunded` status. */
75
+ refundReason: string | null;
76
+ /**
77
+ * Wallet account connected when this quote was produced. The quote's EIP-712
78
+ * digest is bound to it, so `sign()` refuses when the live wallet has since
79
+ * switched accounts (wrong-signer), and a mid-flow change resets the intent.
80
+ * Null until a quote lands.
81
+ */
82
+ quotedAddress: string | null;
83
+ /** Wallet chain id at quote time; guards against a wrong-chain signature. */
84
+ quotedChainId: number | null;
85
+ /**
86
+ * Pay amount (base units) this quote was requested at. The receipt renders
87
+ * the HELD quote against THIS figure, never the live input field: during the
88
+ * debounce-plus-fetch window after an edit, the two describe different
89
+ * trades, and mixing them shows a garbage implied rate and a false
90
+ * below-market flag. Null until a quote lands.
91
+ */
92
+ quotedFromAmount: string | null;
93
+ /**
94
+ * Settlement preference this quote was requested under. The receipt states
95
+ * the HELD quote's settlement, never the live form's: the two come apart when
96
+ * the user flips settlement while a quote is on screen, and the wallet signs
97
+ * the held one. Null until a quote lands.
98
+ */
99
+ quotedSettlement: SettlementType | null;
100
+ /**
101
+ * Deposit-acceptance deadline (unix seconds) for a deposit route, or null.
102
+ *
103
+ * Held on the INTENT, not read off `state.quote`, because it is a fact about
104
+ * the solver's acceptance window rather than about a price. The two come apart
105
+ * on a resumed swap: no quote is ever synthesized (a stale price shown as
106
+ * current would be a lie), so a quote-sourced deadline was undefined there and
107
+ * the status poll fell back to its flat 10-minute ceiling while the restored
108
+ * screen counted down the real 30-minute window. The poll then stopped and
109
+ * said so, next to a countdown saying otherwise.
110
+ *
111
+ * `pollCeilingAttempts` reads this, so it must be set anywhere an intent can
112
+ * enter `awaiting_deposit`: quote ingest and resume both.
113
+ */
114
+ depositDeadline: number | null;
115
+ /**
116
+ * The VERIFIED deposit binding: the exact fields the solver signed plus the
117
+ * signature over them, as proven by `verifyDepositAttestation` at ingest.
118
+ *
119
+ * Held so it can be persisted with the swap record and re-proven on resume. A
120
+ * resumed deposit address is read back out of localStorage, which is not a
121
+ * trust boundary, so it has to be checked by the same code that checks a live
122
+ * one rather than trusted because it was ours once. Null on any route without
123
+ * an attested deposit.
124
+ */
125
+ attestedDeposit: StoredDepositAttestation | null;
126
+ /**
127
+ * True once the status poll has hit its ceiling and stopped for this intent.
128
+ *
129
+ * Surfaced because a stopped poll and a poll that has found nothing yet are
130
+ * otherwise indistinguishable on screen: both render a spinner. A view that
131
+ * claims to be checking must be able to tell whether it still is.
132
+ */
133
+ pollExhausted: boolean;
134
+ /**
135
+ * Set on a `failed` read after an execute (or a revealed deposit address)
136
+ * that does not carry the Worker's nothing-moved marker
137
+ * (`failureMayHaveMoved`): the order did not complete and the funds may have
138
+ * moved. Terminal and never offered Retry: the phase is `may-have-moved`, the
139
+ * poll is stopped, and no quote starts until Reset.
140
+ */
141
+ mayHaveMoved: boolean;
142
+ /**
143
+ * Set when this client stops resending an execute the Worker answered
144
+ * not-accepted (`decideExecuteResend` gives up), when the execute's own
145
+ * answer says the intent reads `quoted` again (the Worker ended its claim
146
+ * without finishing the intent), or when a resumed `executing` intent (no
147
+ * quote and no sign flow in this tab) reads `quoted`: the order was not
148
+ * sent, but the signed request stays executable until its deadline, so
149
+ * there is no Retry yet. The status stays `executing`, the phase is
150
+ * `not-sent`, and the poll runs on until the Worker ends the intent:
151
+ * `failed` with `NOT_EXECUTED_SUBSTATUS` is Retry, and so is a 404 read past
152
+ * the Worker's finish (`unexecutedFinishAt`: only the quote sweep deletes a
153
+ * row, only one never claimed, and past the finish nothing can claim it). A
154
+ * 404 before the finish, or with no quote to compute it (a resumed intent),
155
+ * holds: it may come from a router, a proxy or a rollback while the signed
156
+ * request is still claimable. Cleared by any other read than `quoted`
157
+ * (another execute claimed it: shown in doubt) and by leaving `executing`.
158
+ */
159
+ notSent: boolean;
160
+ /**
161
+ * Set when a sign ceremony was refused before the funds-moving signature
162
+ * (see SignRefusal). The intent is `quoted` so the quote stays on screen; a
163
+ * new quote, a successful Confirm or a reset clears it.
164
+ */
165
+ refusal: SignRefusal | null;
166
+ }
167
+ export declare const INITIAL_INTENT_STATE: IntentState;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * The intent lifecycle's state, and the error value it carries.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.INITIAL_INTENT_STATE = void 0;
7
+ exports.INITIAL_INTENT_STATE = {
8
+ status: "idle",
9
+ intentId: null,
10
+ quoteId: null,
11
+ quote: null,
12
+ error: null,
13
+ txHash: null,
14
+ receivingTxHash: null,
15
+ settlementTxHash: null,
16
+ settlementChainId: null,
17
+ substatus: null,
18
+ legsWithheld: false,
19
+ reconciling: false,
20
+ refundReason: null,
21
+ quotedAddress: null,
22
+ quotedChainId: null,
23
+ quotedFromAmount: null,
24
+ quotedSettlement: null,
25
+ depositDeadline: null,
26
+ attestedDeposit: null,
27
+ pollExhausted: false,
28
+ mayHaveMoved: false,
29
+ notSent: false,
30
+ refusal: null,
31
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Status-transition reducer for status-driven state.
3
+ *
4
+ * Validates each move against a predicate, no-ops on an invalid one, and
5
+ * otherwise merges { status, ...updates }. Pure: the machine reports the
6
+ * rejected move itself (an `Emit` log effect), so this never touches a console.
7
+ */
8
+ /**
9
+ * Apply a validated status transition to a state object. Returns the previous
10
+ * state unchanged (same reference) when the move is invalid, otherwise the
11
+ * merged next state. Never mutates `prev`.
12
+ */
13
+ export declare function applyStatusTransition<TStatus extends string, TState extends {
14
+ status: TStatus;
15
+ }>(prev: TState, to: TStatus, canTransition: (from: TStatus, to: TStatus) => boolean, updates?: Partial<TState>): TState;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * Status-transition reducer for status-driven state.
4
+ *
5
+ * Validates each move against a predicate, no-ops on an invalid one, and
6
+ * otherwise merges { status, ...updates }. Pure: the machine reports the
7
+ * rejected move itself (an `Emit` log effect), so this never touches a console.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.applyStatusTransition = applyStatusTransition;
11
+ /**
12
+ * Apply a validated status transition to a state object. Returns the previous
13
+ * state unchanged (same reference) when the move is invalid, otherwise the
14
+ * merged next state. Never mutates `prev`.
15
+ */
16
+ function applyStatusTransition(prev, to, canTransition, updates) {
17
+ if (!canTransition(prev.status, to))
18
+ return prev;
19
+ return { ...prev, status: to, ...updates };
20
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Quote request validation.
3
+ *
4
+ * Composable validation functions using functional patterns. Each validator
5
+ * returns a Result type that can be combined. Pure: the deadline check takes
6
+ * the clock as an argument, so the machine validates against the same `now`
7
+ * that stamped the request.
8
+ */
9
+ import type { QuoteRequest } from "./wire.js";
10
+ export interface SwapValidationResult {
11
+ isValid: boolean;
12
+ errors: string[];
13
+ }
14
+ /** Every reason `params` is not a sendable quote request, judged at `nowMs`. */
15
+ export declare const validateSwapParams: (params: Partial<QuoteRequest>, nowMs: number) => SwapValidationResult;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Quote request validation.
4
+ *
5
+ * Composable validation functions using functional patterns. Each validator
6
+ * returns a Result type that can be combined. Pure: the deadline check takes
7
+ * the clock as an argument, so the machine validates against the same `now`
8
+ * that stamped the request.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.validateSwapParams = void 0;
12
+ const index_js_1 = require("../shared/index.js");
13
+ const err = (error) => ({ ok: false, error });
14
+ const okVoid = { ok: true };
15
+ /** Chain validators: short-circuit on first error */
16
+ const chain = (...validators) => (value) => {
17
+ for (const v of validators) {
18
+ const result = v(value);
19
+ if (!result.ok)
20
+ return result;
21
+ }
22
+ return okVoid;
23
+ };
24
+ const notEmpty = (s) => (s && s.trim() !== "" ? okVoid : err("Required"));
25
+ const matchesRegex = (regex, message) => (s) => regex.test(s) ? okVoid : err(message);
26
+ const notEqual = (forbidden, message) => (v) => v !== forbidden ? okVoid : err(message);
27
+ // ============================================================
28
+ // Address Validators
29
+ // ============================================================
30
+ const startsWithHex = (s) => s.startsWith("0x") ? okVoid : err("Must start with 0x");
31
+ const hasCorrectLength = (s) => s.length === index_js_1.ETH_ADDRESS_LENGTH ? okVoid : err(`Must be ${index_js_1.ETH_ADDRESS_LENGTH} characters`);
32
+ const hasValidHexChars = matchesRegex(index_js_1.ETH_ADDRESS_REGEX, "Contains invalid characters");
33
+ const notZeroAddress = notEqual(index_js_1.ZERO_ADDRESS, "Zero address is not allowed");
34
+ const validateAddress = chain(notEmpty, startsWithHex, hasCorrectLength, hasValidHexChars, notZeroAddress);
35
+ const validateTokenAddress = chain(validateAddress, notEqual(index_js_1.ZERO_ADDRESS, "Use wrapped token address (WETH)"));
36
+ const errorOf = (result) => (result.ok ? "" : result.error);
37
+ // Individual field validators as simple functions
38
+ const validateWallet = (wallet) => !wallet
39
+ ? ["Wallet is required"]
40
+ : !validateAddress(wallet).ok
41
+ ? [`Wallet: ${errorOf(validateAddress(wallet))}`]
42
+ : [];
43
+ // Origin may be a non-EVM chain (Tron deposit route) as well as EVM.
44
+ const validateFromChain = (chainId) => chainId === undefined
45
+ ? ["Source chain is required"]
46
+ : !(0, index_js_1.isChainId)(chainId) && !(0, index_js_1.isTronChainId)(chainId)
47
+ ? ["Invalid source chain"]
48
+ : [];
49
+ // Destination may be a non-EVM chain (Tron); the origin stays EVM.
50
+ const validateToChain = (chainId) => chainId === undefined
51
+ ? ["Destination chain is required"]
52
+ : !(0, index_js_1.isChainId)(chainId) && !(0, index_js_1.isTronChainId)(chainId)
53
+ ? ["Invalid destination chain"]
54
+ : [];
55
+ // The source token can be a Tron TRC-20 (base58) on a Tron-origin deposit route.
56
+ const validateFromToken = (token) => {
57
+ if (!token)
58
+ return ["Source token is required"];
59
+ if (validateTokenAddress(token).ok || (0, index_js_1.isValidTronAddress)(token))
60
+ return [];
61
+ return [`Source token: ${errorOf(validateTokenAddress(token))}`];
62
+ };
63
+ // The destination token can be a Tron TRC-20 (base58) on a Tron destination, so
64
+ // accept either an EVM token address or a valid Tron address.
65
+ const validateToToken = (token) => {
66
+ if (!token)
67
+ return ["Destination token is required"];
68
+ if (validateTokenAddress(token).ok || (0, index_js_1.isValidTronAddress)(token))
69
+ return [];
70
+ return [`Destination token: ${errorOf(validateTokenAddress(token))}`];
71
+ };
72
+ const validateFromAmount = (amount) => {
73
+ if (!amount)
74
+ return ["Amount is required"];
75
+ try {
76
+ const bigAmount = BigInt(amount);
77
+ return bigAmount > 0n ? [] : ["Amount must be greater than 0"];
78
+ }
79
+ catch {
80
+ return ["Invalid amount format"];
81
+ }
82
+ };
83
+ const validateDeadline = (deadline, nowMs) => {
84
+ if (deadline === undefined)
85
+ return ["Deadline is required"];
86
+ const now = Math.floor(nowMs / 1000);
87
+ if (deadline <= now)
88
+ return ["Deadline must be in the future"];
89
+ if (deadline > now + index_js_1.MAX_DEADLINE_OFFSET_SEC)
90
+ return ["Deadline cannot exceed 1 hour"];
91
+ return [];
92
+ };
93
+ const validateSlippageBps = (bps) => bps === undefined
94
+ ? ["Slippage tolerance is required"]
95
+ : bps < 1
96
+ ? ["Slippage must be at least 0.01%"]
97
+ : bps > index_js_1.MAX_SLIPPAGE_BPS
98
+ ? [`Slippage cannot exceed ${index_js_1.MAX_SLIPPAGE_PERCENT}%`]
99
+ : [];
100
+ const validateSettlement = (pref) => !pref
101
+ ? ["Settlement preference is required"]
102
+ : !(0, index_js_1.isSettlementType)(pref)
103
+ ? ["Invalid settlement preference"]
104
+ : [];
105
+ const validateSameToken = (p) => p.fromToken &&
106
+ p.toToken &&
107
+ p.fromChainId === p.toChainId &&
108
+ p.fromToken.toLowerCase() === p.toToken.toLowerCase()
109
+ ? ["Cannot swap token to itself"]
110
+ : [];
111
+ /** Every reason `params` is not a sendable quote request, judged at `nowMs`. */
112
+ const validateSwapParams = (params, nowMs) => {
113
+ const errors = [
114
+ ...(params.preview ? [] : validateWallet(params.wallet)),
115
+ ...validateFromChain(params.fromChainId),
116
+ ...validateToChain(params.toChainId),
117
+ ...validateFromToken(params.fromToken),
118
+ ...validateToToken(params.toToken),
119
+ ...validateFromAmount(params.fromAmount),
120
+ ...validateDeadline(params.deadline, nowMs),
121
+ ...validateSlippageBps(params.slippageBps),
122
+ ...validateSettlement(params.settlementPreference),
123
+ ...validateSameToken(params),
124
+ ];
125
+ return { isValid: errors.length === 0, errors };
126
+ };
127
+ exports.validateSwapParams = validateSwapParams;
@@ -0,0 +1,3 @@
1
+ import { type Model } from "./machine.js";
2
+ import { type Receipt } from "./receipt.js";
3
+ export declare function view(model: Model): Receipt;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.view = view;
4
+ /**
5
+ * view(model) -> Receipt. Pure; the renderer draws this and nothing it derives
6
+ * on its own from the raw quote.
7
+ */
8
+ const index_js_1 = require("../shared/index.js");
9
+ const decisions_js_1 = require("./decisions.js");
10
+ const machine_js_1 = require("./machine.js");
11
+ const phase_js_1 = require("./phase.js");
12
+ const receipt_js_1 = require("./receipt.js");
13
+ function quoteSlot(model) {
14
+ const { status, quote } = model.intent;
15
+ // The receipt STAYS MOUNTED while a replacement is fetched, but only over a
16
+ // solvable quote: after a decline the held quote's figures are empty.
17
+ if (quote && (status === "quoted" || (status === "quoting" && quote.canSolve))) {
18
+ return {
19
+ kind: "ready",
20
+ executable: !quote.preview,
21
+ refreshing: status === "quoting",
22
+ expiresInSec: model.quoteExpiresIn,
23
+ };
24
+ }
25
+ if (status === "quoting")
26
+ return { kind: "loading" };
27
+ if (quote && !quote.canSolve)
28
+ return { kind: "declined", reason: quote.error ?? null };
29
+ return { kind: "none" };
30
+ }
31
+ function signingSlot(model) {
32
+ const quote = model.intent.quote;
33
+ const signatures = quote?.pullAuthorization ? 2 : 1;
34
+ const flow = model.sign;
35
+ if (flow) {
36
+ if (flow.stage === "execute") {
37
+ const attempt = flow.execute;
38
+ return typeof attempt?.resendAt === "number"
39
+ ? {
40
+ kind: "busy",
41
+ retryAt: Math.ceil(attempt.resendAt / 1000),
42
+ resend: attempt.resends + 1,
43
+ of: index_js_1.MAX_EXECUTE_RESENDS,
44
+ }
45
+ : { kind: "submitted" };
46
+ }
47
+ return flow.stage === "intent"
48
+ ? { kind: "in_wallet", step: 1, of: signatures }
49
+ : { kind: "in_wallet", step: 2, of: 2 };
50
+ }
51
+ if (model.awaitingPull)
52
+ return { kind: "awaiting_second" };
53
+ // A quote for a trade the form no longer describes is shown, never offered.
54
+ if (!(0, decisions_js_1.canSignQuote)(model.intent.status, quote) || !(0, machine_js_1.holdsCurrentQuote)(model)) {
55
+ return { kind: "unavailable" };
56
+ }
57
+ if (model.intent.refusal)
58
+ return { kind: "refused", refusal: model.intent.refusal };
59
+ return quote?.depositAddress ? { kind: "deposit" } : { kind: "ready", signatures };
60
+ }
61
+ function view(model) {
62
+ const { status, reconciling, mayHaveMoved, notSent, quote, quotedSettlement } = model.intent;
63
+ // A held quote is disclosed under the settlement it was requested (and is
64
+ // signed) under, not whatever the form says now.
65
+ const settlement = (quote && quotedSettlement) || model.ctx.settlementPreference;
66
+ return {
67
+ phase: (0, phase_js_1.phaseFromStatus)(status, { reconciling, mayHaveMoved, notSent }),
68
+ quote: quoteSlot(model),
69
+ payout: (0, receipt_js_1.payoutOf)(quote),
70
+ cost: (0, receipt_js_1.costOf)(quote),
71
+ settlement: (0, receipt_js_1.settlementDisclosure)(settlement),
72
+ signing: signingSlot(model),
73
+ };
74
+ }