@xochi/client 0.1.1 → 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 CHANGED
@@ -4,6 +4,16 @@ All notable changes to `@xochi/client` are documented here. Format follows [Keep
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.2] - 2026-10-27
8
+
9
+ ### Added
10
+
11
+ - Typed wire shapes and a zod decoder for Riddler's shielded terms preview (`POST /xochi/shielded/preview`, Riddler#1296): `ShieldedPreviewResponseSchema`, `ShieldedPreviewSchema`, `ShieldedFeeBreakdownSchema`, `ShieldedNetworkIdentitySchema`, `ShieldedCorridorSchema`, the `ShieldedPreviewRequestWire` / `ShieldedPreviewResponseWire` types and `decodeShieldedPreview(body)`, which also refuses a fee breakdown whose totals do not add up. Shapes only, with no transport method: the preview needs a `quote`-scoped Riddler credential, and the Xochi Worker proxy route that would expose it is a follow-up owned by xochi#635. A test holds the schemas' members to the vendored `wire/openapi.json`.
12
+
13
+ ### Fixed
14
+
15
+ - A wallet signature prompt can be cancelled immediately, and every prompt now times out after 90 seconds. A dismissed WalletConnect request previously left consumers in `signing` forever with no retry action; `CancelSignRequested` returns the unchanged quote to its confirm surface, timeout returns to retry or quote refresh as its expiry allows, and any late signature is ignored.
16
+
7
17
  ## [0.1.1] - 2026-10-26
8
18
 
9
19
  ### Added
package/README.md CHANGED
@@ -83,6 +83,7 @@ Lifecycle rules the machine enforces, beyond the signer checks under [Fund safet
83
83
  - Confirm is gated on `canSignQuote` and `holdsCurrentQuote`: a held quote is signable (and a deposit address revealable) only while its quoted amount and trade match the form. The trade key is built from the same normalised fields as the quote request (`normalizeTradeFields`), so an edit that leaves the request unchanged keeps the quote.
84
84
  - A new amount, a trade/wallet context change, Reset, a resume, or entering `signing` / `awaiting_deposit` / `executing` drops in-flight quote flights. Refresh is ignored past `quoting`, Allocate outside `ALLOCATABLE_STATES` (idle / pending / quoting / quoted), and a quote reply that finds the trade past `quoting` or ended is dropped; a failed allocation never reopens a trade as `quoted`.
85
85
  - The sign ceremony survives `signing -> executing` only for the machine's own execute step (`sign.stage === "execute"`); a status-driven move abandons it, and a signature reply outside `signing` is ignored.
86
+ - Every wallet prompt arms the one-shot `signPrompt` timer for `SIGN_PROMPT_TIMEOUT_MS` (90 s). Schedule it like the other timers. A host should expose `CancelSignRequested` while `view(model).phase === "awaiting-wallet"`; cancellation returns to the unchanged quote, clears the live ceremony and ignores a late signature. A timeout does the same, then normal quote expiry may move the receipt to its refresh surface.
86
87
  - `StreamStatus` frames for another intent are ignored. Read stream frames with `mapStatusFrame`: it passes in-flight frames only, so every terminal arrives by the poll with its receipt and reason. A `failed` frame a host dispatches itself follows the poll's rule ([Fund safety](#fund-safety)).
87
88
  - An execute reported `notAccepted` (below) waits with no status read and resends from the `execute` timer under a new flow id; a status move off `executing`, Reset, or any end of the ceremony drops the pending resend (`clear:execute`). **Hosts must schedule the `execute` timer** like the others and render the `busy` signing slot. A host that drops the timer is slowed, not stranded: the poll timer stays armed through the wait, and the first poll tick (every `STATUS_POLL_MS`, 4 s) or returning tab (`VisibilityChanged`) at or after the resend time does the timer's work, so the resend goes up to 4 s late. The deadline check still decides: a resend that late can fall inside `EXECUTE_RESEND_MARGIN_SECS`, and then the machine stops resending ("not sent yet", [Fund safety](#fund-safety)) instead.
88
89
 
@@ -219,6 +219,8 @@ export type MsgBody = {
219
219
  type: "SignRequested";
220
220
  } | {
221
221
  type: "PullSignRequested";
222
+ } | {
223
+ type: "CancelSignRequested";
222
224
  } | {
223
225
  type: "ResetRequested";
224
226
  } | {
@@ -162,6 +162,10 @@ function step(m, msg, fx) {
162
162
  return m.amount ? startQuote(m, fx, msg.now, quoteOpts(m.amount)) : m;
163
163
  case "SignRequested":
164
164
  return beginSign(m, fx, msg.now);
165
+ case "CancelSignRequested":
166
+ if (m.intent.status !== "signing" || !m.sign)
167
+ return m;
168
+ return transition(abandonSign(m, fx), fx, "quoted", { error: "Signature cancelled" });
165
169
  case "PullSignRequested":
166
170
  return beginPullSign(m, fx, msg.now);
167
171
  case "ResetRequested":
@@ -723,8 +727,9 @@ function onTimer(m, fx, timer, now) {
723
727
  return resendBackstop(m, fx, now);
724
728
  return m.poll && !m.poll.stopped ? pollTick(m, fx, true) : m;
725
729
  case "signPrompt":
726
- // A deep-link prompt that never re-opened the wallet.
727
- return m.sign?.stage === "pull"
730
+ // A wallet prompt that never resolved. The flow still owns the prompt;
731
+ // `signFlowFailed` makes any reply arriving after this timer inert.
732
+ return m.sign
728
733
  ? signFlowFailed(m, fx, {
729
734
  rejected: false,
730
735
  value: "wallet_prompt_timeout",
@@ -832,6 +837,10 @@ function beginSign(m, fx, now) {
832
837
  chainId: next.ctx.wallet.chainId,
833
838
  },
834
839
  });
840
+ // A wallet request can be dismissed without rejecting (mobile deep links in
841
+ // particular). Bound every prompt, including signature 1, so the ceremony
842
+ // always returns to an actionable surface.
843
+ setTimer(fx, "signPrompt", exports.SIGN_PROMPT_TIMEOUT_MS, false);
835
844
  return { ...next, sign: { id, origin: "sign", stage: "intent", intentSignature: null } };
836
845
  }
837
846
  function beginPullSign(m, fx, now) {
@@ -932,8 +941,7 @@ function onSigned(m, fx, flowId, purpose, result, now) {
932
941
  // open; its signature then belongs to no ceremony.
933
942
  if (m.intent.status !== "signing")
934
943
  return abandonSign(m, fx);
935
- if (purpose === "pull")
936
- clearTimer(fx, "signPrompt");
944
+ clearTimer(fx, "signPrompt");
937
945
  if (!result.ok)
938
946
  return signFlowFailed(m, fx, result.error);
939
947
  if (purpose === "pull") {
@@ -1216,7 +1224,7 @@ function endSignFlow(m, flow) {
1216
1224
  * affordance, no prompt timer. Nothing signed so far can execute after this.
1217
1225
  */
1218
1226
  function abandonSign(m, fx) {
1219
- if (m.sign?.stage === "pull")
1227
+ if (m.sign)
1220
1228
  clearTimer(fx, "signPrompt");
1221
1229
  if (awaitingResend(m))
1222
1230
  clearTimer(fx, "execute");
@@ -1232,12 +1240,16 @@ function signFlowFailed(m, fx, report) {
1232
1240
  const flow = m.sign;
1233
1241
  if (!flow)
1234
1242
  return m;
1235
- if (flow.stage === "pull")
1236
- clearTimer(fx, "signPrompt");
1243
+ clearTimer(fx, "signPrompt");
1237
1244
  const next = endSignFlow(m, flow);
1238
1245
  if (flow.origin === "sign") {
1239
1246
  if (report.rejected)
1240
1247
  return transition(next, fx, "quoted", { error: "Signature rejected" });
1248
+ if (report.value === "wallet_prompt_timeout") {
1249
+ return transition(next, fx, "quoted", {
1250
+ error: "Couldn't reach the wallet. Press Confirm to try again.",
1251
+ });
1252
+ }
1241
1253
  log(fx, "error", `${LOG} Sign error:`, report.detail);
1242
1254
  return transition(next, fx, "failed", { error: report.value ?? "Signing failed" });
1243
1255
  }
@@ -13,3 +13,4 @@ export * from "./validators.js";
13
13
  export * from "./schemas.js";
14
14
  export * from "./api.js";
15
15
  export * from "./intentStream.js";
16
+ export * from "./shieldedPreview.js";
@@ -29,3 +29,4 @@ __exportStar(require("./validators.js"), exports);
29
29
  __exportStar(require("./schemas.js"), exports);
30
30
  __exportStar(require("./api.js"), exports);
31
31
  __exportStar(require("./intentStream.js"), exports);
32
+ __exportStar(require("./shieldedPreview.js"), exports);