@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,33 @@
1
+ /**
2
+ * Pre-sign check for a stealth claim's UserOperation.
3
+ *
4
+ * `POST /api/stealth/claim/prepare` returns a sponsored ERC-4337 operation and
5
+ * the `userOpHash` the stealth key is asked to sign. The hash alone is valid over
6
+ * any preimage that hashes to it, so signing it unverified authorises whatever
7
+ * the server chose to build -- including a sweep to somebody else's address.
8
+ *
9
+ * `@xochi/signer`'s `assertPreparedClaimMatchesRequest` recomputes the hash
10
+ * from the served op under the pinned v0.8 EntryPoint, then checks `sender`,
11
+ * the settlement chain, `callData` (`execute(token, 0, transfer(recipient,
12
+ * amount))`), a zero native value and a paymaster. It runs here with
13
+ * `requireVerified` forced on: an absent op, EntryPoint or settlement chain is a
14
+ * refusal, never a warning, because the signature is the entire authorisation
15
+ * and nothing after it can catch a wrong one.
16
+ */
17
+ import { type ClaimExpectation, type PreparedClaim } from "@xochi/signer";
18
+ /** What the client independently knows the claim should do. */
19
+ export type PreparedClaimExpectation = Omit<ClaimExpectation, "requireVerified">;
20
+ /**
21
+ * `claim_op_unverified` is the category the widget already reports for a
22
+ * refused claim op, so a host branching on it keeps working.
23
+ */
24
+ export type PreparedClaimCheck = {
25
+ ok: true;
26
+ token: string;
27
+ amount: string;
28
+ } | {
29
+ ok: false;
30
+ errorCategory: "claim_op_unverified";
31
+ reason: string;
32
+ };
33
+ export declare function checkPreparedClaim(prepared: PreparedClaim, expected: PreparedClaimExpectation): PreparedClaimCheck;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pre-sign check for a stealth claim's UserOperation.
3
+ *
4
+ * `POST /api/stealth/claim/prepare` returns a sponsored ERC-4337 operation and
5
+ * the `userOpHash` the stealth key is asked to sign. The hash alone is valid over
6
+ * any preimage that hashes to it, so signing it unverified authorises whatever
7
+ * the server chose to build -- including a sweep to somebody else's address.
8
+ *
9
+ * `@xochi/signer`'s `assertPreparedClaimMatchesRequest` recomputes the hash
10
+ * from the served op under the pinned v0.8 EntryPoint, then checks `sender`,
11
+ * the settlement chain, `callData` (`execute(token, 0, transfer(recipient,
12
+ * amount))`), a zero native value and a paymaster. It runs here with
13
+ * `requireVerified` forced on: an absent op, EntryPoint or settlement chain is a
14
+ * refusal, never a warning, because the signature is the entire authorisation
15
+ * and nothing after it can catch a wrong one.
16
+ */
17
+ import { XochiClaimMismatchError, assertPreparedClaimMatchesRequest, } from "@xochi/signer";
18
+ export function checkPreparedClaim(prepared, expected) {
19
+ try {
20
+ const verification = assertPreparedClaimMatchesRequest(prepared, {
21
+ ...expected,
22
+ requireVerified: true,
23
+ });
24
+ // With requireVerified every gap throws, so an unverified result here is a
25
+ // contract break in the signer: refuse it rather than sign on it.
26
+ if (!verification.verified || !verification.token || !verification.amount) {
27
+ return {
28
+ ok: false,
29
+ errorCategory: "claim_op_unverified",
30
+ reason: verification.reason ?? "the claim operation could not be verified",
31
+ };
32
+ }
33
+ return { ok: true, token: verification.token, amount: verification.amount };
34
+ }
35
+ catch (err) {
36
+ const reason = err instanceof XochiClaimMismatchError
37
+ ? err.message
38
+ : `claim verification failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`;
39
+ return { ok: false, errorCategory: "claim_op_unverified", reason };
40
+ }
41
+ }
@@ -0,0 +1,247 @@
1
+ import { z } from "zod";
2
+ import type { FeeBreakdown } from "./shared/types.js";
3
+ export type FeeTier = "standard" | "trusted" | "verified" | "premium" | "institutional";
4
+ export type AssetClass = "stable" | "volatile";
5
+ export type AttestationType = "compliance" | "non_membership";
6
+ export declare const FEE_SCHEDULE: {
7
+ readonly surplus_share_pct: 15;
8
+ readonly tier_boundaries: readonly [{
9
+ readonly min_trust_score: 100;
10
+ readonly tier: "institutional";
11
+ }, {
12
+ readonly min_trust_score: 75;
13
+ readonly tier: "premium";
14
+ }, {
15
+ readonly min_trust_score: 50;
16
+ readonly tier: "verified";
17
+ }, {
18
+ readonly min_trust_score: 25;
19
+ readonly tier: "trusted";
20
+ }, {
21
+ readonly min_trust_score: 0;
22
+ readonly tier: "standard";
23
+ }];
24
+ readonly solver_base_bps: {
25
+ readonly stable: 8;
26
+ readonly volatile: 18;
27
+ };
28
+ readonly venue_bps: {
29
+ readonly standard: {
30
+ readonly stable: 6;
31
+ readonly volatile: 10;
32
+ };
33
+ readonly trusted: {
34
+ readonly stable: 5;
35
+ readonly volatile: 8;
36
+ };
37
+ readonly verified: {
38
+ readonly stable: 3;
39
+ readonly volatile: 5;
40
+ };
41
+ readonly premium: {
42
+ readonly stable: 2;
43
+ readonly volatile: 3;
44
+ };
45
+ readonly institutional: {
46
+ readonly stable: 1;
47
+ readonly volatile: 2;
48
+ };
49
+ };
50
+ readonly routing_bps: {
51
+ readonly standard: {
52
+ readonly stable: 8;
53
+ readonly volatile: 12;
54
+ };
55
+ readonly trusted: {
56
+ readonly stable: 6;
57
+ readonly volatile: 9;
58
+ };
59
+ readonly verified: {
60
+ readonly stable: 4;
61
+ readonly volatile: 6;
62
+ };
63
+ readonly premium: {
64
+ readonly stable: 2;
65
+ readonly volatile: 4;
66
+ };
67
+ readonly institutional: {
68
+ readonly stable: 1;
69
+ readonly volatile: 2;
70
+ };
71
+ };
72
+ readonly price_impact: {
73
+ readonly k: 50;
74
+ readonly cap_bps: 150;
75
+ readonly max_fill_fraction: 0.25;
76
+ };
77
+ readonly tier_attestation_requirements: {
78
+ readonly standard: AttestationType[];
79
+ readonly trusted: AttestationType[];
80
+ readonly verified: AttestationType[];
81
+ readonly premium: AttestationType[];
82
+ readonly institutional: AttestationType[];
83
+ };
84
+ };
85
+ export declare function scoreToTier(score: number): FeeTier;
86
+ export declare function gateTier(rawTier: FeeTier, provided: AttestationType[]): FeeTier;
87
+ export declare function priceImpactBps(fillUsd: number, liquidityUsd: number | null | undefined): number;
88
+ export interface FeeComputeParams {
89
+ input_amount: string | bigint;
90
+ asset_class?: AssetClass;
91
+ trust_score?: number;
92
+ tier?: FeeTier;
93
+ attestations?: AttestationType[];
94
+ acp_origin?: boolean;
95
+ fill_usd?: number;
96
+ available_liquidity_usd?: number | null;
97
+ }
98
+ export interface FeeBreakdownCore {
99
+ tier: FeeTier;
100
+ asset_class: AssetClass;
101
+ solver_fee_bps: number;
102
+ solver_fee: string;
103
+ price_impact_bps: number;
104
+ price_impact: string;
105
+ venue_fee_bps: number;
106
+ venue_fee: string;
107
+ routing_fee_bps: number;
108
+ routing_fee: string;
109
+ total_bps: number;
110
+ total_fee: string;
111
+ surplus_share_pct: number;
112
+ }
113
+ export declare function computeFeeBreakdown(params: FeeComputeParams): FeeBreakdownCore;
114
+ export declare const LayeredFeeBreakdownSchema: z.ZodObject<{
115
+ tier: z.ZodString;
116
+ asset_class: z.ZodEnum<["stable", "volatile"]>;
117
+ gas_floor: z.ZodOptional<z.ZodObject<{
118
+ amount: z.ZodString;
119
+ usd: z.ZodString;
120
+ }, "strip", z.ZodTypeAny, {
121
+ amount: string;
122
+ usd: string;
123
+ }, {
124
+ amount: string;
125
+ usd: string;
126
+ }>>;
127
+ solver_fee_bps: z.ZodNumber;
128
+ solver_fee: z.ZodString;
129
+ price_impact_bps: z.ZodNumber;
130
+ price_impact: z.ZodString;
131
+ venue_fee_bps: z.ZodNumber;
132
+ venue_fee: z.ZodString;
133
+ routing_fee_bps: z.ZodNumber;
134
+ routing_fee: z.ZodString;
135
+ total_bps: z.ZodNumber;
136
+ total_fee: z.ZodString;
137
+ total_usd: z.ZodOptional<z.ZodString>;
138
+ surplus_share_pct: z.ZodNumber;
139
+ discount: z.ZodOptional<z.ZodObject<{
140
+ source: z.ZodEnum<["tier", "promo"]>;
141
+ bps: z.ZodNumber;
142
+ amount: z.ZodString;
143
+ }, "strip", z.ZodTypeAny, {
144
+ amount: string;
145
+ source: "tier" | "promo";
146
+ bps: number;
147
+ }, {
148
+ amount: string;
149
+ source: "tier" | "promo";
150
+ bps: number;
151
+ }>>;
152
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
153
+ tier: z.ZodString;
154
+ asset_class: z.ZodEnum<["stable", "volatile"]>;
155
+ gas_floor: z.ZodOptional<z.ZodObject<{
156
+ amount: z.ZodString;
157
+ usd: z.ZodString;
158
+ }, "strip", z.ZodTypeAny, {
159
+ amount: string;
160
+ usd: string;
161
+ }, {
162
+ amount: string;
163
+ usd: string;
164
+ }>>;
165
+ solver_fee_bps: z.ZodNumber;
166
+ solver_fee: z.ZodString;
167
+ price_impact_bps: z.ZodNumber;
168
+ price_impact: z.ZodString;
169
+ venue_fee_bps: z.ZodNumber;
170
+ venue_fee: z.ZodString;
171
+ routing_fee_bps: z.ZodNumber;
172
+ routing_fee: z.ZodString;
173
+ total_bps: z.ZodNumber;
174
+ total_fee: z.ZodString;
175
+ total_usd: z.ZodOptional<z.ZodString>;
176
+ surplus_share_pct: z.ZodNumber;
177
+ discount: z.ZodOptional<z.ZodObject<{
178
+ source: z.ZodEnum<["tier", "promo"]>;
179
+ bps: z.ZodNumber;
180
+ amount: z.ZodString;
181
+ }, "strip", z.ZodTypeAny, {
182
+ amount: string;
183
+ source: "tier" | "promo";
184
+ bps: number;
185
+ }, {
186
+ amount: string;
187
+ source: "tier" | "promo";
188
+ bps: number;
189
+ }>>;
190
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
191
+ tier: z.ZodString;
192
+ asset_class: z.ZodEnum<["stable", "volatile"]>;
193
+ gas_floor: z.ZodOptional<z.ZodObject<{
194
+ amount: z.ZodString;
195
+ usd: z.ZodString;
196
+ }, "strip", z.ZodTypeAny, {
197
+ amount: string;
198
+ usd: string;
199
+ }, {
200
+ amount: string;
201
+ usd: string;
202
+ }>>;
203
+ solver_fee_bps: z.ZodNumber;
204
+ solver_fee: z.ZodString;
205
+ price_impact_bps: z.ZodNumber;
206
+ price_impact: z.ZodString;
207
+ venue_fee_bps: z.ZodNumber;
208
+ venue_fee: z.ZodString;
209
+ routing_fee_bps: z.ZodNumber;
210
+ routing_fee: z.ZodString;
211
+ total_bps: z.ZodNumber;
212
+ total_fee: z.ZodString;
213
+ total_usd: z.ZodOptional<z.ZodString>;
214
+ surplus_share_pct: z.ZodNumber;
215
+ discount: z.ZodOptional<z.ZodObject<{
216
+ source: z.ZodEnum<["tier", "promo"]>;
217
+ bps: z.ZodNumber;
218
+ amount: z.ZodString;
219
+ }, "strip", z.ZodTypeAny, {
220
+ amount: string;
221
+ source: "tier" | "promo";
222
+ bps: number;
223
+ }, {
224
+ amount: string;
225
+ source: "tier" | "promo";
226
+ bps: number;
227
+ }>>;
228
+ }, z.ZodTypeAny, "passthrough">>;
229
+ export type LayeredFeeBreakdownWire = z.infer<typeof LayeredFeeBreakdownSchema>;
230
+ export declare function parseFeeBreakdown(raw: unknown): LayeredFeeBreakdownWire;
231
+ export interface FeeInvariantOptions {
232
+ input_amount?: string | bigint;
233
+ }
234
+ export interface FeeReconcilable {
235
+ solver_fee_bps: number;
236
+ price_impact_bps: number;
237
+ venue_fee_bps: number;
238
+ routing_fee_bps: number;
239
+ total_bps: number;
240
+ solver_fee: string;
241
+ price_impact: string;
242
+ venue_fee: string;
243
+ routing_fee: string;
244
+ total_fee: string;
245
+ }
246
+ export declare function feeBreakdownReconcilable(fb: FeeBreakdown): FeeReconcilable;
247
+ export declare function assertFeeBreakdownInvariants(fb: FeeReconcilable, opts?: FeeInvariantOptions): void;
@@ -0,0 +1,285 @@
1
+ // Layered fee model -- the network-free reference for the Xochi `fee_breakdown`
2
+ // (SPEC.md Section 4). Byte/amount-parity with the server's authoritative
3
+ // `Riddler.Integrations.Xochi.FeePolicy.calculate_layered_fee/1`; the fee
4
+ // vectors in `@xochi/spec/fee/vectors.json` are generated from this module
5
+ // (`packages/spec/fee/generate.ts`) and re-checked by
6
+ // `packages/reference/test/fee.test.ts`, making Section 4 conformance-tested
7
+ // the same way the signing layer is.
8
+ //
9
+ // Two roles:
10
+ // - `computeFeeBreakdown(params)` -- predict the deterministic core of a
11
+ // breakdown (bps + token-unit amounts) from the published schedule, so a
12
+ // taker can cross-check a server quote instead of trusting it blindly.
13
+ // - `assertFeeBreakdownInvariants(fb)` -- structurally verify a server-returned
14
+ // breakdown reconciles (layers sum to the total, bps sum to `total_bps`, no
15
+ // discount crosses the solver floor). This moves real funds; a misreported
16
+ // split MUST fail loud, not settle.
17
+ //
18
+ // The `gas_floor` / `*_usd` fields are price-oracle-derived and added by the
19
+ // server's quote adapter -- they are outside this deterministic core and are
20
+ // validated only for shape (see `LayeredFeeBreakdownSchema`), never recomputed
21
+ // here.
22
+ import { z } from "zod";
23
+ // Canonical fee schedule. Single source of truth: mirrors the module attributes
24
+ // of `Riddler.Integrations.Xochi.FeePolicy`. `@xochi/spec/fee/schedule.json`
25
+ // is a generated projection of this object (see `packages/spec/fee/generate.ts`).
26
+ // Headline total per tier = solver + venue + routing (no impact, non-ACP):
27
+ // stable 22/19/15/12/10, volatile 40/35/29/25/22.
28
+ export const FEE_SCHEDULE = {
29
+ // Percentage of positive fill slippage (surplus) captured; the rest returns
30
+ // to the taker. Reported, not part of the fee-bps sum.
31
+ surplus_share_pct: 15,
32
+ // Trust score -> tier, highest boundary first (a score matches the first
33
+ // boundary it meets or exceeds).
34
+ tier_boundaries: [
35
+ { min_trust_score: 100, tier: "institutional" },
36
+ { min_trust_score: 75, tier: "premium" },
37
+ { min_trust_score: 50, tier: "verified" },
38
+ { min_trust_score: 25, tier: "trusted" },
39
+ { min_trust_score: 0, tier: "standard" },
40
+ ],
41
+ // Solver spread -- the never-discounted cost floor, identical at every tier.
42
+ solver_base_bps: { stable: 8, volatile: 18 },
43
+ // Venue (Xochi) cut -- tier-discounted.
44
+ venue_bps: {
45
+ standard: { stable: 6, volatile: 10 },
46
+ trusted: { stable: 5, volatile: 8 },
47
+ verified: { stable: 3, volatile: 5 },
48
+ premium: { stable: 2, volatile: 3 },
49
+ institutional: { stable: 1, volatile: 2 },
50
+ },
51
+ // Routing (Raxol) cut -- tier-discounted; waived entirely when the intent
52
+ // originates from ACP (the routing cut is already paid via the ACP job budget).
53
+ routing_bps: {
54
+ standard: { stable: 8, volatile: 12 },
55
+ trusted: { stable: 6, volatile: 9 },
56
+ verified: { stable: 4, volatile: 6 },
57
+ premium: { stable: 2, volatile: 4 },
58
+ institutional: { stable: 1, volatile: 2 },
59
+ },
60
+ // Price impact is a solver cost, not a fee: impact_bps = clamp(round(k *
61
+ // fill/liq), 0, cap). Zero on unknown/zero depth.
62
+ price_impact: { k: 50, cap_bps: 150, max_fill_fraction: 0.25 },
63
+ // Attestation proofs a tier requires; without them the tier is gated down to
64
+ // the highest qualifying one.
65
+ tier_attestation_requirements: {
66
+ standard: [],
67
+ trusted: [],
68
+ verified: ["compliance"],
69
+ premium: ["compliance", "non_membership"],
70
+ institutional: ["compliance", "non_membership"],
71
+ },
72
+ };
73
+ const BPS_DENOMINATOR = 10000n;
74
+ // Highest-privilege-first, for gating a tier down to the best one whose proof
75
+ // requirements are satisfied. Mirrors FeePolicy's @tier_gate_order.
76
+ const TIER_GATE_ORDER = ["institutional", "premium", "verified", "trusted", "standard"];
77
+ // Maps a trust score to its fee tier. Non-integer or negative scores fall back
78
+ // to `standard`, matching the server.
79
+ export function scoreToTier(score) {
80
+ if (!Number.isInteger(score) || score < 0)
81
+ return "standard";
82
+ for (const { min_trust_score, tier } of FEE_SCHEDULE.tier_boundaries) {
83
+ if (score >= min_trust_score)
84
+ return tier;
85
+ }
86
+ return "standard";
87
+ }
88
+ function isSubset(required, provided) {
89
+ return required.every((r) => provided.has(r));
90
+ }
91
+ // Gates a raw tier down to the highest tier whose attestation requirements are
92
+ // met by `provided`. A tier with no requirements is returned unchanged.
93
+ export function gateTier(rawTier, provided) {
94
+ const providedSet = new Set(provided);
95
+ const required = FEE_SCHEDULE.tier_attestation_requirements[rawTier];
96
+ if (isSubset(required, providedSet))
97
+ return rawTier;
98
+ for (const tier of TIER_GATE_ORDER) {
99
+ if (isSubset(FEE_SCHEDULE.tier_attestation_requirements[tier], providedSet))
100
+ return tier;
101
+ }
102
+ return "standard";
103
+ }
104
+ // Price-impact in bps for `fillUsd` filled against `liquidityUsd` of solver
105
+ // inventory. Returns 0 when depth is unknown or non-positive (depth-blind => no
106
+ // impact term). impact_bps = clamp(round(k * fill/liq), 0, cap).
107
+ export function priceImpactBps(fillUsd, liquidityUsd) {
108
+ if (typeof fillUsd !== "number" ||
109
+ typeof liquidityUsd !== "number" ||
110
+ !Number.isFinite(fillUsd) ||
111
+ !Number.isFinite(liquidityUsd) ||
112
+ liquidityUsd <= 0) {
113
+ return 0;
114
+ }
115
+ const { k, cap_bps } = FEE_SCHEDULE.price_impact;
116
+ return Math.round(Math.min(cap_bps, Math.max(0, (k * fillUsd) / liquidityUsd)));
117
+ }
118
+ // Per-layer fee amount in token base units: floor(base * bps / 10000). Each
119
+ // layer floors independently, so the sum of layers can be a hair below
120
+ // floor(base * total_bps / 10000) -- that per-layer floor is the normative rule.
121
+ function layerAmount(base, bps) {
122
+ if (base <= 0n || bps <= 0)
123
+ return 0n;
124
+ return (base * BigInt(bps)) / BPS_DENOMINATOR;
125
+ }
126
+ function parseBaseAmount(input) {
127
+ if (typeof input === "bigint") {
128
+ if (input < 0n)
129
+ throw new Error(`fee: input_amount must be non-negative, got ${input}`);
130
+ return input;
131
+ }
132
+ if (!/^\d+$/.test(input)) {
133
+ throw new Error(`fee: input_amount must be a non-negative integer string, got ${JSON.stringify(input)}`);
134
+ }
135
+ return BigInt(input);
136
+ }
137
+ // Reference layered-fee computation, byte-parity with the server. Produces the
138
+ // deterministic core a taker can compare against a returned quote.
139
+ export function computeFeeBreakdown(params) {
140
+ const assetClass = params.asset_class ?? "stable";
141
+ const tier = params.tier ?? gateTier(scoreToTier(params.trust_score ?? 0), params.attestations ?? []);
142
+ const solverBps = FEE_SCHEDULE.solver_base_bps[assetClass];
143
+ const venueBps = FEE_SCHEDULE.venue_bps[tier][assetClass];
144
+ const routingBps = params.acp_origin ? 0 : FEE_SCHEDULE.routing_bps[tier][assetClass];
145
+ const impactBps = priceImpactBps(params.fill_usd ?? 0, params.available_liquidity_usd ?? null);
146
+ // Non-negative layers => the sum already dominates the solver floor; the clamp
147
+ // is a defensive invariant, not arithmetic that fires in practice.
148
+ const totalBps = Math.max(solverBps + venueBps + routingBps + impactBps, solverBps);
149
+ const base = parseBaseAmount(params.input_amount);
150
+ const solverFee = layerAmount(base, solverBps);
151
+ const impactFee = layerAmount(base, impactBps);
152
+ const venueFee = layerAmount(base, venueBps);
153
+ const routingFee = layerAmount(base, routingBps);
154
+ const totalFee = solverFee + impactFee + venueFee + routingFee;
155
+ return {
156
+ tier,
157
+ asset_class: assetClass,
158
+ solver_fee_bps: solverBps,
159
+ solver_fee: solverFee.toString(),
160
+ price_impact_bps: impactBps,
161
+ price_impact: impactFee.toString(),
162
+ venue_fee_bps: venueBps,
163
+ venue_fee: venueFee.toString(),
164
+ routing_fee_bps: routingBps,
165
+ routing_fee: routingFee.toString(),
166
+ total_bps: totalBps,
167
+ total_fee: totalFee.toString(),
168
+ surplus_share_pct: FEE_SCHEDULE.surplus_share_pct,
169
+ };
170
+ }
171
+ // -- Wire validation (HTTP boundary) --
172
+ const bpsField = z.number().int().nonnegative();
173
+ const amountField = z
174
+ .string()
175
+ .regex(/^\d+$/, "must be a non-negative integer string (token base units)");
176
+ // The solver's `fee_breakdown` object as `/xochi/quote` serves it (SPEC.md
177
+ // Section 4; `LayeredFeeBreakdown` in `@xochi/spec/wire/openapi.json`).
178
+ // `gas_floor`/`total_usd` are price-oracle-derived and optional here so a
179
+ // pre-adapter core still parses; `.passthrough()` tolerates additive fields a
180
+ // newer server may send.
181
+ //
182
+ // Deliberately NOT the Worker's `FeeBreakdownSchema` (`shared/schemas.ts`),
183
+ // though both carry the same fifteen layer fields and the same optional
184
+ // `discount`. They are two contracts on two surfaces and neither accepts a
185
+ // superset of the other: the Worker's `/api/*` re-emission REQUIRES
186
+ // `gas_floor`/`total_usd`, closes `tier` to the five-tier enum and strips
187
+ // unknown keys, while this one validates the solver's own object (and the
188
+ // network-free core, which has no oracle envelope), pins every bps to a
189
+ // non-negative integer and `gas_floor.amount` to base units, and passes
190
+ // unknown keys through. Merging them would either make every pre-adapter core
191
+ // unparseable or relax the Worker contract the frontend renders against.
192
+ export const LayeredFeeBreakdownSchema = z
193
+ .object({
194
+ tier: z.string(),
195
+ asset_class: z.enum(["stable", "volatile"]),
196
+ gas_floor: z.object({ amount: amountField, usd: z.string() }).optional(),
197
+ solver_fee_bps: bpsField,
198
+ solver_fee: amountField,
199
+ price_impact_bps: bpsField,
200
+ price_impact: amountField,
201
+ venue_fee_bps: bpsField,
202
+ venue_fee: amountField,
203
+ routing_fee_bps: bpsField,
204
+ routing_fee: amountField,
205
+ total_bps: bpsField,
206
+ total_fee: amountField,
207
+ total_usd: z.string().optional(),
208
+ surplus_share_pct: bpsField,
209
+ // Served only when the request carried `promo_discount_bps > 0` (xochi#584):
210
+ // which discount priced the quote and how much it took off the Standard
211
+ // venue + routing layers. Informational -- `venue_fee`/`routing_fee` are
212
+ // already net of it, so the reconciliation in `assertFeeBreakdownInvariants`
213
+ // neither reads nor subtracts it. Shape-only, like `gas_floor`: the promo
214
+ // input is worker-side and no fee vector carries it.
215
+ discount: z
216
+ .object({ source: z.enum(["tier", "promo"]), bps: bpsField, amount: amountField })
217
+ .optional(),
218
+ })
219
+ .passthrough();
220
+ // Parses + shape-validates a solver `fee_breakdown`. Throws (zod) on a
221
+ // malformed object -- callers at the HTTP boundary should not swallow this.
222
+ export function parseFeeBreakdown(raw) {
223
+ return LayeredFeeBreakdownSchema.parse(raw);
224
+ }
225
+ // The reconciliation fields of a decoded quote's `feeBreakdown` (the camelCase
226
+ // `FeeBreakdown` that `getQuote` and the machine hand back), so the client's own
227
+ // quote can go through `assertFeeBreakdownInvariants` without the wire object.
228
+ export function feeBreakdownReconcilable(fb) {
229
+ return {
230
+ solver_fee_bps: fb.solverFeeBps,
231
+ price_impact_bps: fb.priceImpactBps,
232
+ venue_fee_bps: fb.venueFeeBps,
233
+ routing_fee_bps: fb.routingFeeBps,
234
+ total_bps: fb.totalBps,
235
+ solver_fee: fb.solverFee,
236
+ price_impact: fb.priceImpact,
237
+ venue_fee: fb.venueFee,
238
+ routing_fee: fb.routingFee,
239
+ total_fee: fb.totalFee,
240
+ };
241
+ }
242
+ // Structurally verifies a wire `fee_breakdown` reconciles. Throws with context
243
+ // on any violation; a server that misreports the split MUST fail loud here
244
+ // before funds move. Checks, in order:
245
+ // 1. bps layers sum to total_bps (with the solver-floor clamp),
246
+ // 2. total_bps >= solver_fee_bps (no discount crosses the floor),
247
+ // 3. amount layers sum to total_fee,
248
+ // 4. (optional) each layer amount == floor(base * bps / 10000).
249
+ export function assertFeeBreakdownInvariants(fb, opts = {}) {
250
+ const bpsSum = fb.solver_fee_bps + fb.price_impact_bps + fb.venue_fee_bps + fb.routing_fee_bps;
251
+ const expectedTotalBps = Math.max(bpsSum, fb.solver_fee_bps);
252
+ if (fb.total_bps !== expectedTotalBps) {
253
+ throw new Error(`fee_breakdown: total_bps ${fb.total_bps} != clamped layer sum ${expectedTotalBps} ` +
254
+ `(solver ${fb.solver_fee_bps} + impact ${fb.price_impact_bps} + venue ${fb.venue_fee_bps} + routing ${fb.routing_fee_bps})`);
255
+ }
256
+ if (fb.total_bps < fb.solver_fee_bps) {
257
+ throw new Error(`fee_breakdown: total_bps ${fb.total_bps} below solver floor ${fb.solver_fee_bps}`);
258
+ }
259
+ const layers = {
260
+ solver: BigInt(fb.solver_fee),
261
+ price_impact: BigInt(fb.price_impact),
262
+ venue: BigInt(fb.venue_fee),
263
+ routing: BigInt(fb.routing_fee),
264
+ };
265
+ const feeSum = layers.solver + layers.price_impact + layers.venue + layers.routing;
266
+ const totalFee = BigInt(fb.total_fee);
267
+ if (feeSum !== totalFee) {
268
+ throw new Error(`fee_breakdown: layer amounts sum to ${feeSum} but total_fee is ${totalFee}`);
269
+ }
270
+ if (opts.input_amount !== undefined) {
271
+ const base = parseBaseAmount(opts.input_amount);
272
+ const checks = [
273
+ ["solver_fee", layers.solver, fb.solver_fee_bps],
274
+ ["price_impact", layers.price_impact, fb.price_impact_bps],
275
+ ["venue_fee", layers.venue, fb.venue_fee_bps],
276
+ ["routing_fee", layers.routing, fb.routing_fee_bps],
277
+ ];
278
+ for (const [name, amount, bps] of checks) {
279
+ const expected = layerAmount(base, bps);
280
+ if (amount !== expected) {
281
+ throw new Error(`fee_breakdown: ${name} ${amount} != floor(${base} * ${bps} / 10000) = ${expected}`);
282
+ }
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @xochi/client: a client for the Xochi trade API (`https://api.xochi.fi/api/*`).
3
+ *
4
+ * - The wire contract and shared vocabulary (chains, statuses, schemas).
5
+ * - The trade lifecycle as a pure state machine: `update(model, msg)`.
6
+ * - A transport over the Worker API: `createXochiClient`.
7
+ * - Pre-sign fund-safety checks, from `@xochi/signer`, run by the machine at
8
+ * quote ingest and exported for hosts that drive their own flow.
9
+ * - The layered fee reference (SPEC.md Section 4): the published schedule, the
10
+ * deterministic breakdown it implies, and the reconciliation check a served
11
+ * `fee_breakdown` must pass.
12
+ */
13
+ export * from "./shared/index.js";
14
+ export * from "./machine/index.js";
15
+ export * from "./transport/client.js";
16
+ export * from "./claim.js";
17
+ export * from "./fee.js";
18
+ export type { MethodPin, PinnedPullRecipients, PreparedClaim } from "@xochi/signer";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @xochi/client: a client for the Xochi trade API (`https://api.xochi.fi/api/*`).
3
+ *
4
+ * - The wire contract and shared vocabulary (chains, statuses, schemas).
5
+ * - The trade lifecycle as a pure state machine: `update(model, msg)`.
6
+ * - A transport over the Worker API: `createXochiClient`.
7
+ * - Pre-sign fund-safety checks, from `@xochi/signer`, run by the machine at
8
+ * quote ingest and exported for hosts that drive their own flow.
9
+ * - The layered fee reference (SPEC.md Section 4): the published schedule, the
10
+ * deterministic breakdown it implies, and the reconciliation check a served
11
+ * `fee_breakdown` must pass.
12
+ */
13
+ export * from "./shared/index.js";
14
+ export * from "./machine/index.js";
15
+ export * from "./transport/client.js";
16
+ export * from "./claim.js";
17
+ export * from "./fee.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The stealth claim-info gate, applied at quote ingest.
3
+ */
4
+ /**
5
+ * Why a served `claim_info` cannot be acted on, or null when it is fine.
6
+ *
7
+ * A stealth payout lands in a counterfactual SimpleAccount, and the FACTORY in
8
+ * `claim_info` is what decides who owns that account. The client follows the
9
+ * advertised factory on purpose (#302, so it and the solver derive the same
10
+ * address), which means an attacker who can influence the served value picks
11
+ * where the money goes and both sides agree on it.
12
+ *
13
+ * This is the last check before that value is trusted: the factory must be one
14
+ * this build ships, and the salt must be the documented 0. Note the client
15
+ * cannot verify the stealth ADDRESS itself at quote time (the solver picks the
16
+ * ephemeral key, so the address is only derivable once announced), which is
17
+ * exactly why the factory has to carry the weight.
18
+ */
19
+ export type ClaimInfoRejection = {
20
+ code: "untrusted_factory";
21
+ message: string;
22
+ } | {
23
+ code: "unexpected_salt";
24
+ message: string;
25
+ };
26
+ export declare function claimInfoError(claimInfo: {
27
+ factoryAddress?: string;
28
+ salt?: number;
29
+ } | undefined | null, chainId: number | undefined | null): ClaimInfoRejection | null;