@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.
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Riddler's shielded terms preview wire (`POST /xochi/shielded/preview`,
4
+ * Riddler#1296): the typed request and response shapes and a decoder.
5
+ *
6
+ * There is NO transport method: the preview needs a `quote`-scoped Riddler
7
+ * credential, so only the Xochi Worker calls it, and the Worker proxy route is
8
+ * a follow-up owned by xochi#635. These shapes exist so that proxy and its
9
+ * consumers decode the solver's answer with the same schema the solver
10
+ * publishes (`packages/spec/wire/openapi.json`, `ShieldedPreviewResponse`).
11
+ *
12
+ * The preview is NOT a signing payload: `preview_digest` is the EIP-712 digest
13
+ * of `ShieldedPreviewV1` under the `Xochi Shielded Preview` domain, which no
14
+ * `ShieldedTermsV1` signature verifies against (`@xochi/spec`
15
+ * `conformance/shielded-terms.json`, PROPOSED). `executable` is always false.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ShieldedPreviewResponseSchema = exports.ShieldedPreviewSchema = exports.ShieldedFeeBreakdownSchema = exports.ShieldedCorridorSchema = exports.ShieldedNetworkIdentitySchema = exports.aztecValue = void 0;
19
+ exports.decodeShieldedPreview = decodeShieldedPreview;
20
+ const zod_1 = require("zod");
21
+ const schemas_js_1 = require("./schemas.js");
22
+ /** 32-byte hex (`0x` + 64). */
23
+ const bytes32 = zod_1.z.string().regex(/^0x[0-9a-fA-F]{64}$/);
24
+ /**
25
+ * A 32-byte Aztec address or field element. The decoder checks the shape only;
26
+ * the solver has already refused zero and values at or above the BN254 modulus.
27
+ * A 20-byte EVM address never fits.
28
+ */
29
+ exports.aztecValue = bytes32;
30
+ /** Canonical unsigned decimal string (uint256 members). */
31
+ const uintString = zod_1.z.string().regex(/^(0|[1-9][0-9]*)$/);
32
+ /** Canonical positive base-unit amount. */
33
+ const baseUnits = zod_1.z.string().regex(/^[1-9][0-9]*$/);
34
+ exports.ShieldedNetworkIdentitySchema = zod_1.z.object({
35
+ aztec_chain_id: zod_1.z.string().regex(/^[1-9][0-9]*$/),
36
+ protocol_version: zod_1.z.string().regex(/^[1-9][0-9]*$/),
37
+ rollup_address: schemas_js_1.ethAddress,
38
+ l1_chain_id: zod_1.z.string().regex(/^[1-9][0-9]*$/),
39
+ genesis_or_network_digest: bytes32,
40
+ });
41
+ exports.ShieldedCorridorSchema = zod_1.z.object({
42
+ corridor_id: bytes32,
43
+ revision: zod_1.z.number().int().min(1).max(4294967295),
44
+ manifest_digest: bytes32,
45
+ });
46
+ exports.ShieldedFeeBreakdownSchema = zod_1.z.object({
47
+ gas_floor: uintString,
48
+ solver_fee_bps: zod_1.z.number().int().min(0).max(10000),
49
+ solver_fee: uintString,
50
+ price_impact_bps: zod_1.z.number().int().min(0).max(10000),
51
+ price_impact: uintString,
52
+ venue_fee_bps: zod_1.z.number().int().min(0).max(10000),
53
+ venue_fee: uintString,
54
+ routing_fee_bps: zod_1.z.number().int().min(0).max(10000),
55
+ routing_fee: uintString,
56
+ total_bps: zod_1.z.number().int().min(0).max(40000),
57
+ total_fee: uintString,
58
+ });
59
+ /** `ShieldedPreviewV1` in snake_case: the terms minus the per-execution members. */
60
+ exports.ShieldedPreviewSchema = zod_1.z.object({
61
+ protocol_version: zod_1.z.literal("xochi.shielded.preview.v1"),
62
+ environment: zod_1.z.string().min(1),
63
+ source: zod_1.z.object({
64
+ chain_id: zod_1.z.number().int().positive(),
65
+ token_address: schemas_js_1.ethAddress,
66
+ wallet: schemas_js_1.ethAddress,
67
+ collector: schemas_js_1.ethAddress,
68
+ amount: baseUnits,
69
+ }),
70
+ destination: zod_1.z.object({
71
+ settlement_type: zod_1.z.literal("shielded"),
72
+ privacy_level: zod_1.z.literal("private"),
73
+ disclosure_profile: zod_1.z.literal("recipient-private-public-amount-v1"),
74
+ rail: zod_1.z.literal("a_tracer"),
75
+ network: exports.ShieldedNetworkIdentitySchema,
76
+ corridor: exports.ShieldedCorridorSchema,
77
+ asset_id: zod_1.z.string().min(1),
78
+ token_address: exports.aztecValue,
79
+ recipient_binding_id: zod_1.z.string().min(1),
80
+ recipient_descriptor_digest: bytes32,
81
+ recipient_address: exports.aztecValue,
82
+ amount: baseUnits,
83
+ fee_payer: exports.aztecValue,
84
+ }),
85
+ fee_breakdown: exports.ShieldedFeeBreakdownSchema,
86
+ refund_address: schemas_js_1.ethAddress,
87
+ refund_amount: baseUnits,
88
+ collect_before: zod_1.z.number().int().nonnegative(),
89
+ settlement_service_deadline: zod_1.z.number().int().nonnegative(),
90
+ recipient_binding_digest: bytes32,
91
+ policy_revision: zod_1.z.number().int().nonnegative(),
92
+ });
93
+ exports.ShieldedPreviewResponseSchema = zod_1.z.object({
94
+ executable: zod_1.z.literal(false),
95
+ preview_version: zod_1.z.literal("xochi.shielded.preview.v1"),
96
+ preview_digest: bytes32,
97
+ recipient_binding_digest: bytes32,
98
+ domain: zod_1.z.object({
99
+ name: zod_1.z.literal("Xochi Shielded Preview"),
100
+ version: zod_1.z.literal("1"),
101
+ chain_id: zod_1.z.number().int().positive(),
102
+ salt: bytes32,
103
+ }),
104
+ preview: exports.ShieldedPreviewSchema,
105
+ manifest: zod_1.z.object({
106
+ manifest_digest: bytes32,
107
+ aztec_sdk_version: zod_1.z.string().min(1),
108
+ recipient_account_class_id: exports.aztecValue,
109
+ }),
110
+ });
111
+ /**
112
+ * Decode a preview response. Throws (zod) on any shape the solver does not
113
+ * publish, including an executable or terms-shaped answer, and on a fee
114
+ * breakdown whose totals disagree: `total_fee` must be `max(gas_floor, layers)` (the gas
115
+ * floor is a minimum fee, as in FeePolicy) and `total_bps` the four rates.
116
+ */
117
+ function decodeShieldedPreview(body) {
118
+ const decoded = exports.ShieldedPreviewResponseSchema.parse(body);
119
+ const f = decoded.preview.fee_breakdown;
120
+ const layers = BigInt(f.solver_fee) + BigInt(f.price_impact) + BigInt(f.venue_fee) + BigInt(f.routing_fee);
121
+ const gasFloor = BigInt(f.gas_floor);
122
+ const totalFee = layers > gasFloor ? layers : gasFloor;
123
+ const totalBps = f.solver_fee_bps + f.price_impact_bps + f.venue_fee_bps + f.routing_fee_bps;
124
+ if (BigInt(f.total_fee) !== totalFee || f.total_bps !== totalBps) {
125
+ throw new Error("shielded preview fee_breakdown totals disagree with max(gas_floor, layers)");
126
+ }
127
+ return decoded;
128
+ }
@@ -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
  } | {
@@ -156,6 +156,10 @@ function step(m, msg, fx) {
156
156
  return m.amount ? startQuote(m, fx, msg.now, quoteOpts(m.amount)) : m;
157
157
  case "SignRequested":
158
158
  return beginSign(m, fx, msg.now);
159
+ case "CancelSignRequested":
160
+ if (m.intent.status !== "signing" || !m.sign)
161
+ return m;
162
+ return transition(abandonSign(m, fx), fx, "quoted", { error: "Signature cancelled" });
159
163
  case "PullSignRequested":
160
164
  return beginPullSign(m, fx, msg.now);
161
165
  case "ResetRequested":
@@ -717,8 +721,9 @@ function onTimer(m, fx, timer, now) {
717
721
  return resendBackstop(m, fx, now);
718
722
  return m.poll && !m.poll.stopped ? pollTick(m, fx, true) : m;
719
723
  case "signPrompt":
720
- // A deep-link prompt that never re-opened the wallet.
721
- return m.sign?.stage === "pull"
724
+ // A wallet prompt that never resolved. The flow still owns the prompt;
725
+ // `signFlowFailed` makes any reply arriving after this timer inert.
726
+ return m.sign
722
727
  ? signFlowFailed(m, fx, {
723
728
  rejected: false,
724
729
  value: "wallet_prompt_timeout",
@@ -826,6 +831,10 @@ function beginSign(m, fx, now) {
826
831
  chainId: next.ctx.wallet.chainId,
827
832
  },
828
833
  });
834
+ // A wallet request can be dismissed without rejecting (mobile deep links in
835
+ // particular). Bound every prompt, including signature 1, so the ceremony
836
+ // always returns to an actionable surface.
837
+ setTimer(fx, "signPrompt", SIGN_PROMPT_TIMEOUT_MS, false);
829
838
  return { ...next, sign: { id, origin: "sign", stage: "intent", intentSignature: null } };
830
839
  }
831
840
  function beginPullSign(m, fx, now) {
@@ -926,8 +935,7 @@ function onSigned(m, fx, flowId, purpose, result, now) {
926
935
  // open; its signature then belongs to no ceremony.
927
936
  if (m.intent.status !== "signing")
928
937
  return abandonSign(m, fx);
929
- if (purpose === "pull")
930
- clearTimer(fx, "signPrompt");
938
+ clearTimer(fx, "signPrompt");
931
939
  if (!result.ok)
932
940
  return signFlowFailed(m, fx, result.error);
933
941
  if (purpose === "pull") {
@@ -1210,7 +1218,7 @@ function endSignFlow(m, flow) {
1210
1218
  * affordance, no prompt timer. Nothing signed so far can execute after this.
1211
1219
  */
1212
1220
  function abandonSign(m, fx) {
1213
- if (m.sign?.stage === "pull")
1221
+ if (m.sign)
1214
1222
  clearTimer(fx, "signPrompt");
1215
1223
  if (awaitingResend(m))
1216
1224
  clearTimer(fx, "execute");
@@ -1226,12 +1234,16 @@ function signFlowFailed(m, fx, report) {
1226
1234
  const flow = m.sign;
1227
1235
  if (!flow)
1228
1236
  return m;
1229
- if (flow.stage === "pull")
1230
- clearTimer(fx, "signPrompt");
1237
+ clearTimer(fx, "signPrompt");
1231
1238
  const next = endSignFlow(m, flow);
1232
1239
  if (flow.origin === "sign") {
1233
1240
  if (report.rejected)
1234
1241
  return transition(next, fx, "quoted", { error: "Signature rejected" });
1242
+ if (report.value === "wallet_prompt_timeout") {
1243
+ return transition(next, fx, "quoted", {
1244
+ error: "Couldn't reach the wallet. Press Confirm to try again.",
1245
+ });
1246
+ }
1235
1247
  log(fx, "error", `${LOG} Sign error:`, report.detail);
1236
1248
  return transition(next, fx, "failed", { error: report.value ?? "Signing failed" });
1237
1249
  }
@@ -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";
@@ -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";