@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
package/dist/cjs/fee.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Layered fee model -- the network-free reference for the Xochi `fee_breakdown`
|
|
3
|
+
// (SPEC.md Section 4). Byte/amount-parity with the server's authoritative
|
|
4
|
+
// `Riddler.Integrations.Xochi.FeePolicy.calculate_layered_fee/1`; the fee
|
|
5
|
+
// vectors in `@xochi/spec/fee/vectors.json` are generated from this module
|
|
6
|
+
// (`packages/spec/fee/generate.ts`) and re-checked by
|
|
7
|
+
// `packages/reference/test/fee.test.ts`, making Section 4 conformance-tested
|
|
8
|
+
// the same way the signing layer is.
|
|
9
|
+
//
|
|
10
|
+
// Two roles:
|
|
11
|
+
// - `computeFeeBreakdown(params)` -- predict the deterministic core of a
|
|
12
|
+
// breakdown (bps + token-unit amounts) from the published schedule, so a
|
|
13
|
+
// taker can cross-check a server quote instead of trusting it blindly.
|
|
14
|
+
// - `assertFeeBreakdownInvariants(fb)` -- structurally verify a server-returned
|
|
15
|
+
// breakdown reconciles (layers sum to the total, bps sum to `total_bps`, no
|
|
16
|
+
// discount crosses the solver floor). This moves real funds; a misreported
|
|
17
|
+
// split MUST fail loud, not settle.
|
|
18
|
+
//
|
|
19
|
+
// The `gas_floor` / `*_usd` fields are price-oracle-derived and added by the
|
|
20
|
+
// server's quote adapter -- they are outside this deterministic core and are
|
|
21
|
+
// validated only for shape (see `LayeredFeeBreakdownSchema`), never recomputed
|
|
22
|
+
// here.
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.LayeredFeeBreakdownSchema = exports.FEE_SCHEDULE = void 0;
|
|
25
|
+
exports.scoreToTier = scoreToTier;
|
|
26
|
+
exports.gateTier = gateTier;
|
|
27
|
+
exports.priceImpactBps = priceImpactBps;
|
|
28
|
+
exports.computeFeeBreakdown = computeFeeBreakdown;
|
|
29
|
+
exports.parseFeeBreakdown = parseFeeBreakdown;
|
|
30
|
+
exports.feeBreakdownReconcilable = feeBreakdownReconcilable;
|
|
31
|
+
exports.assertFeeBreakdownInvariants = assertFeeBreakdownInvariants;
|
|
32
|
+
const zod_1 = require("zod");
|
|
33
|
+
// Canonical fee schedule. Single source of truth: mirrors the module attributes
|
|
34
|
+
// of `Riddler.Integrations.Xochi.FeePolicy`. `@xochi/spec/fee/schedule.json`
|
|
35
|
+
// is a generated projection of this object (see `packages/spec/fee/generate.ts`).
|
|
36
|
+
// Headline total per tier = solver + venue + routing (no impact, non-ACP):
|
|
37
|
+
// stable 22/19/15/12/10, volatile 40/35/29/25/22.
|
|
38
|
+
exports.FEE_SCHEDULE = {
|
|
39
|
+
// Percentage of positive fill slippage (surplus) captured; the rest returns
|
|
40
|
+
// to the taker. Reported, not part of the fee-bps sum.
|
|
41
|
+
surplus_share_pct: 15,
|
|
42
|
+
// Trust score -> tier, highest boundary first (a score matches the first
|
|
43
|
+
// boundary it meets or exceeds).
|
|
44
|
+
tier_boundaries: [
|
|
45
|
+
{ min_trust_score: 100, tier: "institutional" },
|
|
46
|
+
{ min_trust_score: 75, tier: "premium" },
|
|
47
|
+
{ min_trust_score: 50, tier: "verified" },
|
|
48
|
+
{ min_trust_score: 25, tier: "trusted" },
|
|
49
|
+
{ min_trust_score: 0, tier: "standard" },
|
|
50
|
+
],
|
|
51
|
+
// Solver spread -- the never-discounted cost floor, identical at every tier.
|
|
52
|
+
solver_base_bps: { stable: 8, volatile: 18 },
|
|
53
|
+
// Venue (Xochi) cut -- tier-discounted.
|
|
54
|
+
venue_bps: {
|
|
55
|
+
standard: { stable: 6, volatile: 10 },
|
|
56
|
+
trusted: { stable: 5, volatile: 8 },
|
|
57
|
+
verified: { stable: 3, volatile: 5 },
|
|
58
|
+
premium: { stable: 2, volatile: 3 },
|
|
59
|
+
institutional: { stable: 1, volatile: 2 },
|
|
60
|
+
},
|
|
61
|
+
// Routing (Raxol) cut -- tier-discounted; waived entirely when the intent
|
|
62
|
+
// originates from ACP (the routing cut is already paid via the ACP job budget).
|
|
63
|
+
routing_bps: {
|
|
64
|
+
standard: { stable: 8, volatile: 12 },
|
|
65
|
+
trusted: { stable: 6, volatile: 9 },
|
|
66
|
+
verified: { stable: 4, volatile: 6 },
|
|
67
|
+
premium: { stable: 2, volatile: 4 },
|
|
68
|
+
institutional: { stable: 1, volatile: 2 },
|
|
69
|
+
},
|
|
70
|
+
// Price impact is a solver cost, not a fee: impact_bps = clamp(round(k *
|
|
71
|
+
// fill/liq), 0, cap). Zero on unknown/zero depth.
|
|
72
|
+
price_impact: { k: 50, cap_bps: 150, max_fill_fraction: 0.25 },
|
|
73
|
+
// Attestation proofs a tier requires; without them the tier is gated down to
|
|
74
|
+
// the highest qualifying one.
|
|
75
|
+
tier_attestation_requirements: {
|
|
76
|
+
standard: [],
|
|
77
|
+
trusted: [],
|
|
78
|
+
verified: ["compliance"],
|
|
79
|
+
premium: ["compliance", "non_membership"],
|
|
80
|
+
institutional: ["compliance", "non_membership"],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const BPS_DENOMINATOR = 10000n;
|
|
84
|
+
// Highest-privilege-first, for gating a tier down to the best one whose proof
|
|
85
|
+
// requirements are satisfied. Mirrors FeePolicy's @tier_gate_order.
|
|
86
|
+
const TIER_GATE_ORDER = ["institutional", "premium", "verified", "trusted", "standard"];
|
|
87
|
+
// Maps a trust score to its fee tier. Non-integer or negative scores fall back
|
|
88
|
+
// to `standard`, matching the server.
|
|
89
|
+
function scoreToTier(score) {
|
|
90
|
+
if (!Number.isInteger(score) || score < 0)
|
|
91
|
+
return "standard";
|
|
92
|
+
for (const { min_trust_score, tier } of exports.FEE_SCHEDULE.tier_boundaries) {
|
|
93
|
+
if (score >= min_trust_score)
|
|
94
|
+
return tier;
|
|
95
|
+
}
|
|
96
|
+
return "standard";
|
|
97
|
+
}
|
|
98
|
+
function isSubset(required, provided) {
|
|
99
|
+
return required.every((r) => provided.has(r));
|
|
100
|
+
}
|
|
101
|
+
// Gates a raw tier down to the highest tier whose attestation requirements are
|
|
102
|
+
// met by `provided`. A tier with no requirements is returned unchanged.
|
|
103
|
+
function gateTier(rawTier, provided) {
|
|
104
|
+
const providedSet = new Set(provided);
|
|
105
|
+
const required = exports.FEE_SCHEDULE.tier_attestation_requirements[rawTier];
|
|
106
|
+
if (isSubset(required, providedSet))
|
|
107
|
+
return rawTier;
|
|
108
|
+
for (const tier of TIER_GATE_ORDER) {
|
|
109
|
+
if (isSubset(exports.FEE_SCHEDULE.tier_attestation_requirements[tier], providedSet))
|
|
110
|
+
return tier;
|
|
111
|
+
}
|
|
112
|
+
return "standard";
|
|
113
|
+
}
|
|
114
|
+
// Price-impact in bps for `fillUsd` filled against `liquidityUsd` of solver
|
|
115
|
+
// inventory. Returns 0 when depth is unknown or non-positive (depth-blind => no
|
|
116
|
+
// impact term). impact_bps = clamp(round(k * fill/liq), 0, cap).
|
|
117
|
+
function priceImpactBps(fillUsd, liquidityUsd) {
|
|
118
|
+
if (typeof fillUsd !== "number" ||
|
|
119
|
+
typeof liquidityUsd !== "number" ||
|
|
120
|
+
!Number.isFinite(fillUsd) ||
|
|
121
|
+
!Number.isFinite(liquidityUsd) ||
|
|
122
|
+
liquidityUsd <= 0) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const { k, cap_bps } = exports.FEE_SCHEDULE.price_impact;
|
|
126
|
+
return Math.round(Math.min(cap_bps, Math.max(0, (k * fillUsd) / liquidityUsd)));
|
|
127
|
+
}
|
|
128
|
+
// Per-layer fee amount in token base units: floor(base * bps / 10000). Each
|
|
129
|
+
// layer floors independently, so the sum of layers can be a hair below
|
|
130
|
+
// floor(base * total_bps / 10000) -- that per-layer floor is the normative rule.
|
|
131
|
+
function layerAmount(base, bps) {
|
|
132
|
+
if (base <= 0n || bps <= 0)
|
|
133
|
+
return 0n;
|
|
134
|
+
return (base * BigInt(bps)) / BPS_DENOMINATOR;
|
|
135
|
+
}
|
|
136
|
+
function parseBaseAmount(input) {
|
|
137
|
+
if (typeof input === "bigint") {
|
|
138
|
+
if (input < 0n)
|
|
139
|
+
throw new Error(`fee: input_amount must be non-negative, got ${input}`);
|
|
140
|
+
return input;
|
|
141
|
+
}
|
|
142
|
+
if (!/^\d+$/.test(input)) {
|
|
143
|
+
throw new Error(`fee: input_amount must be a non-negative integer string, got ${JSON.stringify(input)}`);
|
|
144
|
+
}
|
|
145
|
+
return BigInt(input);
|
|
146
|
+
}
|
|
147
|
+
// Reference layered-fee computation, byte-parity with the server. Produces the
|
|
148
|
+
// deterministic core a taker can compare against a returned quote.
|
|
149
|
+
function computeFeeBreakdown(params) {
|
|
150
|
+
const assetClass = params.asset_class ?? "stable";
|
|
151
|
+
const tier = params.tier ?? gateTier(scoreToTier(params.trust_score ?? 0), params.attestations ?? []);
|
|
152
|
+
const solverBps = exports.FEE_SCHEDULE.solver_base_bps[assetClass];
|
|
153
|
+
const venueBps = exports.FEE_SCHEDULE.venue_bps[tier][assetClass];
|
|
154
|
+
const routingBps = params.acp_origin ? 0 : exports.FEE_SCHEDULE.routing_bps[tier][assetClass];
|
|
155
|
+
const impactBps = priceImpactBps(params.fill_usd ?? 0, params.available_liquidity_usd ?? null);
|
|
156
|
+
// Non-negative layers => the sum already dominates the solver floor; the clamp
|
|
157
|
+
// is a defensive invariant, not arithmetic that fires in practice.
|
|
158
|
+
const totalBps = Math.max(solverBps + venueBps + routingBps + impactBps, solverBps);
|
|
159
|
+
const base = parseBaseAmount(params.input_amount);
|
|
160
|
+
const solverFee = layerAmount(base, solverBps);
|
|
161
|
+
const impactFee = layerAmount(base, impactBps);
|
|
162
|
+
const venueFee = layerAmount(base, venueBps);
|
|
163
|
+
const routingFee = layerAmount(base, routingBps);
|
|
164
|
+
const totalFee = solverFee + impactFee + venueFee + routingFee;
|
|
165
|
+
return {
|
|
166
|
+
tier,
|
|
167
|
+
asset_class: assetClass,
|
|
168
|
+
solver_fee_bps: solverBps,
|
|
169
|
+
solver_fee: solverFee.toString(),
|
|
170
|
+
price_impact_bps: impactBps,
|
|
171
|
+
price_impact: impactFee.toString(),
|
|
172
|
+
venue_fee_bps: venueBps,
|
|
173
|
+
venue_fee: venueFee.toString(),
|
|
174
|
+
routing_fee_bps: routingBps,
|
|
175
|
+
routing_fee: routingFee.toString(),
|
|
176
|
+
total_bps: totalBps,
|
|
177
|
+
total_fee: totalFee.toString(),
|
|
178
|
+
surplus_share_pct: exports.FEE_SCHEDULE.surplus_share_pct,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// -- Wire validation (HTTP boundary) --
|
|
182
|
+
const bpsField = zod_1.z.number().int().nonnegative();
|
|
183
|
+
const amountField = zod_1.z
|
|
184
|
+
.string()
|
|
185
|
+
.regex(/^\d+$/, "must be a non-negative integer string (token base units)");
|
|
186
|
+
// The solver's `fee_breakdown` object as `/xochi/quote` serves it (SPEC.md
|
|
187
|
+
// Section 4; `LayeredFeeBreakdown` in `@xochi/spec/wire/openapi.json`).
|
|
188
|
+
// `gas_floor`/`total_usd` are price-oracle-derived and optional here so a
|
|
189
|
+
// pre-adapter core still parses; `.passthrough()` tolerates additive fields a
|
|
190
|
+
// newer server may send.
|
|
191
|
+
//
|
|
192
|
+
// Deliberately NOT the Worker's `FeeBreakdownSchema` (`shared/schemas.ts`),
|
|
193
|
+
// though both carry the same fifteen layer fields and the same optional
|
|
194
|
+
// `discount`. They are two contracts on two surfaces and neither accepts a
|
|
195
|
+
// superset of the other: the Worker's `/api/*` re-emission REQUIRES
|
|
196
|
+
// `gas_floor`/`total_usd`, closes `tier` to the five-tier enum and strips
|
|
197
|
+
// unknown keys, while this one validates the solver's own object (and the
|
|
198
|
+
// network-free core, which has no oracle envelope), pins every bps to a
|
|
199
|
+
// non-negative integer and `gas_floor.amount` to base units, and passes
|
|
200
|
+
// unknown keys through. Merging them would either make every pre-adapter core
|
|
201
|
+
// unparseable or relax the Worker contract the frontend renders against.
|
|
202
|
+
exports.LayeredFeeBreakdownSchema = zod_1.z
|
|
203
|
+
.object({
|
|
204
|
+
tier: zod_1.z.string(),
|
|
205
|
+
asset_class: zod_1.z.enum(["stable", "volatile"]),
|
|
206
|
+
gas_floor: zod_1.z.object({ amount: amountField, usd: zod_1.z.string() }).optional(),
|
|
207
|
+
solver_fee_bps: bpsField,
|
|
208
|
+
solver_fee: amountField,
|
|
209
|
+
price_impact_bps: bpsField,
|
|
210
|
+
price_impact: amountField,
|
|
211
|
+
venue_fee_bps: bpsField,
|
|
212
|
+
venue_fee: amountField,
|
|
213
|
+
routing_fee_bps: bpsField,
|
|
214
|
+
routing_fee: amountField,
|
|
215
|
+
total_bps: bpsField,
|
|
216
|
+
total_fee: amountField,
|
|
217
|
+
total_usd: zod_1.z.string().optional(),
|
|
218
|
+
surplus_share_pct: bpsField,
|
|
219
|
+
// Served only when the request carried `promo_discount_bps > 0` (xochi#584):
|
|
220
|
+
// which discount priced the quote and how much it took off the Standard
|
|
221
|
+
// venue + routing layers. Informational -- `venue_fee`/`routing_fee` are
|
|
222
|
+
// already net of it, so the reconciliation in `assertFeeBreakdownInvariants`
|
|
223
|
+
// neither reads nor subtracts it. Shape-only, like `gas_floor`: the promo
|
|
224
|
+
// input is worker-side and no fee vector carries it.
|
|
225
|
+
discount: zod_1.z
|
|
226
|
+
.object({ source: zod_1.z.enum(["tier", "promo"]), bps: bpsField, amount: amountField })
|
|
227
|
+
.optional(),
|
|
228
|
+
})
|
|
229
|
+
.passthrough();
|
|
230
|
+
// Parses + shape-validates a solver `fee_breakdown`. Throws (zod) on a
|
|
231
|
+
// malformed object -- callers at the HTTP boundary should not swallow this.
|
|
232
|
+
function parseFeeBreakdown(raw) {
|
|
233
|
+
return exports.LayeredFeeBreakdownSchema.parse(raw);
|
|
234
|
+
}
|
|
235
|
+
// The reconciliation fields of a decoded quote's `feeBreakdown` (the camelCase
|
|
236
|
+
// `FeeBreakdown` that `getQuote` and the machine hand back), so the client's own
|
|
237
|
+
// quote can go through `assertFeeBreakdownInvariants` without the wire object.
|
|
238
|
+
function feeBreakdownReconcilable(fb) {
|
|
239
|
+
return {
|
|
240
|
+
solver_fee_bps: fb.solverFeeBps,
|
|
241
|
+
price_impact_bps: fb.priceImpactBps,
|
|
242
|
+
venue_fee_bps: fb.venueFeeBps,
|
|
243
|
+
routing_fee_bps: fb.routingFeeBps,
|
|
244
|
+
total_bps: fb.totalBps,
|
|
245
|
+
solver_fee: fb.solverFee,
|
|
246
|
+
price_impact: fb.priceImpact,
|
|
247
|
+
venue_fee: fb.venueFee,
|
|
248
|
+
routing_fee: fb.routingFee,
|
|
249
|
+
total_fee: fb.totalFee,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Structurally verifies a wire `fee_breakdown` reconciles. Throws with context
|
|
253
|
+
// on any violation; a server that misreports the split MUST fail loud here
|
|
254
|
+
// before funds move. Checks, in order:
|
|
255
|
+
// 1. bps layers sum to total_bps (with the solver-floor clamp),
|
|
256
|
+
// 2. total_bps >= solver_fee_bps (no discount crosses the floor),
|
|
257
|
+
// 3. amount layers sum to total_fee,
|
|
258
|
+
// 4. (optional) each layer amount == floor(base * bps / 10000).
|
|
259
|
+
function assertFeeBreakdownInvariants(fb, opts = {}) {
|
|
260
|
+
const bpsSum = fb.solver_fee_bps + fb.price_impact_bps + fb.venue_fee_bps + fb.routing_fee_bps;
|
|
261
|
+
const expectedTotalBps = Math.max(bpsSum, fb.solver_fee_bps);
|
|
262
|
+
if (fb.total_bps !== expectedTotalBps) {
|
|
263
|
+
throw new Error(`fee_breakdown: total_bps ${fb.total_bps} != clamped layer sum ${expectedTotalBps} ` +
|
|
264
|
+
`(solver ${fb.solver_fee_bps} + impact ${fb.price_impact_bps} + venue ${fb.venue_fee_bps} + routing ${fb.routing_fee_bps})`);
|
|
265
|
+
}
|
|
266
|
+
if (fb.total_bps < fb.solver_fee_bps) {
|
|
267
|
+
throw new Error(`fee_breakdown: total_bps ${fb.total_bps} below solver floor ${fb.solver_fee_bps}`);
|
|
268
|
+
}
|
|
269
|
+
const layers = {
|
|
270
|
+
solver: BigInt(fb.solver_fee),
|
|
271
|
+
price_impact: BigInt(fb.price_impact),
|
|
272
|
+
venue: BigInt(fb.venue_fee),
|
|
273
|
+
routing: BigInt(fb.routing_fee),
|
|
274
|
+
};
|
|
275
|
+
const feeSum = layers.solver + layers.price_impact + layers.venue + layers.routing;
|
|
276
|
+
const totalFee = BigInt(fb.total_fee);
|
|
277
|
+
if (feeSum !== totalFee) {
|
|
278
|
+
throw new Error(`fee_breakdown: layer amounts sum to ${feeSum} but total_fee is ${totalFee}`);
|
|
279
|
+
}
|
|
280
|
+
if (opts.input_amount !== undefined) {
|
|
281
|
+
const base = parseBaseAmount(opts.input_amount);
|
|
282
|
+
const checks = [
|
|
283
|
+
["solver_fee", layers.solver, fb.solver_fee_bps],
|
|
284
|
+
["price_impact", layers.price_impact, fb.price_impact_bps],
|
|
285
|
+
["venue_fee", layers.venue, fb.venue_fee_bps],
|
|
286
|
+
["routing_fee", layers.routing, fb.routing_fee_bps],
|
|
287
|
+
];
|
|
288
|
+
for (const [name, amount, bps] of checks) {
|
|
289
|
+
const expected = layerAmount(base, bps);
|
|
290
|
+
if (amount !== expected) {
|
|
291
|
+
throw new Error(`fee_breakdown: ${name} ${amount} != floor(${base} * ${bps} / 10000) = ${expected}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -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,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @xochi/client: a client for the Xochi trade API (`https://api.xochi.fi/api/*`).
|
|
4
|
+
*
|
|
5
|
+
* - The wire contract and shared vocabulary (chains, statuses, schemas).
|
|
6
|
+
* - The trade lifecycle as a pure state machine: `update(model, msg)`.
|
|
7
|
+
* - A transport over the Worker API: `createXochiClient`.
|
|
8
|
+
* - Pre-sign fund-safety checks, from `@xochi/signer`, run by the machine at
|
|
9
|
+
* quote ingest and exported for hosts that drive their own flow.
|
|
10
|
+
* - The layered fee reference (SPEC.md Section 4): the published schedule, the
|
|
11
|
+
* deterministic breakdown it implies, and the reconciliation check a served
|
|
12
|
+
* `fee_breakdown` must pass.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
__exportStar(require("./shared/index.js"), exports);
|
|
30
|
+
__exportStar(require("./machine/index.js"), exports);
|
|
31
|
+
__exportStar(require("./transport/client.js"), exports);
|
|
32
|
+
__exportStar(require("./claim.js"), exports);
|
|
33
|
+
__exportStar(require("./fee.js"), exports);
|
|
@@ -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;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The stealth claim-info gate, applied at quote ingest.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.claimInfoError = claimInfoError;
|
|
7
|
+
const index_js_1 = require("../shared/index.js");
|
|
8
|
+
function claimInfoError(claimInfo, chainId) {
|
|
9
|
+
if (!claimInfo)
|
|
10
|
+
return null; // Not a stealth quote; nothing to check.
|
|
11
|
+
if (!(0, index_js_1.isTrustedStealthFactory)(claimInfo.factoryAddress, chainId)) {
|
|
12
|
+
return {
|
|
13
|
+
code: "untrusted_factory",
|
|
14
|
+
message: "This quote settles through an account factory this app does not recognize",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Salt is the other half of the CREATE2 input: moving it relocates the
|
|
18
|
+
// account exactly as changing the factory does.
|
|
19
|
+
if (claimInfo.salt !== undefined && claimInfo.salt !== index_js_1.STEALTH_ACCOUNT_SALT) {
|
|
20
|
+
return {
|
|
21
|
+
code: "unexpected_salt",
|
|
22
|
+
message: "This quote settles to an unexpected account slot",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|