@metamask-previews/perps-controller 17.3.0-preview-d3f9964be → 17.4.0-preview-aecbce4ff
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 +72 -1
- package/dist/PerpsController-method-action-types.d.ts +10 -3
- package/dist/PerpsController-method-action-types.d.ts.map +1 -1
- package/dist/PerpsController-method-action-types.js.map +1 -1
- package/dist/PerpsController.d.ts +10 -3
- package/dist/PerpsController.d.ts.map +1 -1
- package/dist/PerpsController.js +62 -8
- package/dist/PerpsController.js.map +1 -1
- package/dist/constants/perpsConfig.d.ts +55 -0
- package/dist/constants/perpsConfig.d.ts.map +1 -1
- package/dist/constants/perpsConfig.js +55 -0
- package/dist/constants/perpsConfig.js.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.d.ts +19 -0
- package/dist/providers/AggregatedPerpsProvider.d.ts.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.js +21 -0
- package/dist/providers/AggregatedPerpsProvider.js.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.ts +7 -0
- package/dist/providers/HyperLiquidProvider.d.ts.map +1 -1
- package/dist/providers/HyperLiquidProvider.js +133 -34
- package/dist/providers/HyperLiquidProvider.js.map +1 -1
- package/dist/providers/LighterProvider.d.ts.map +1 -1
- package/dist/providers/LighterProvider.js +2 -0
- package/dist/providers/LighterProvider.js.map +1 -1
- package/dist/services/MarketDataService.d.ts.map +1 -1
- package/dist/services/MarketDataService.js +16 -4
- package/dist/services/MarketDataService.js.map +1 -1
- package/dist/services/RewardsIntegrationService.d.ts +47 -2
- package/dist/services/RewardsIntegrationService.d.ts.map +1 -1
- package/dist/services/RewardsIntegrationService.js +400 -19
- package/dist/services/RewardsIntegrationService.js.map +1 -1
- package/dist/services/ServiceContext.d.ts +9 -1
- package/dist/services/ServiceContext.d.ts.map +1 -1
- package/dist/services/ServiceContext.js.map +1 -1
- package/dist/services/TerminalMarketService.d.ts +1 -1
- package/dist/services/TerminalMarketService.d.ts.map +1 -1
- package/dist/services/TerminalMarketService.js +11 -2
- package/dist/services/TerminalMarketService.js.map +1 -1
- package/dist/services/TradingService.d.ts.map +1 -1
- package/dist/services/TradingService.js +291 -18
- package/dist/services/TradingService.js.map +1 -1
- package/dist/types/index.d.ts +65 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/messenger.d.ts +17 -1
- package/dist/types/messenger.d.ts.map +1 -1
- package/dist/types/messenger.js.map +1 -1
- package/dist/utils/hyperLiquidAdapter.d.ts +7 -0
- package/dist/utils/hyperLiquidAdapter.d.ts.map +1 -1
- package/dist/utils/hyperLiquidAdapter.js +1 -1
- package/dist/utils/hyperLiquidAdapter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/subscriptionFeeWaiver.d.ts +201 -0
- package/dist/utils/subscriptionFeeWaiver.d.ts.map +1 -0
- package/dist/utils/subscriptionFeeWaiver.js +334 -0
- package/dist/utils/subscriptionFeeWaiver.js.map +1 -0
- package/package.json +11 -6
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { isHexString } from '@metamask/utils';
|
|
2
|
+
import { BASIS_POINTS_DIVISOR, BUILDER_FEE_CONFIG, } from '../constants/hyperLiquidConfig.js';
|
|
3
|
+
import { SUBSCRIPTION_CLOID_CONFIG, SUBSCRIPTION_CLOID_FLAGS, } from '../constants/perpsConfig.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the subscription source's effective fee rate for one order.
|
|
6
|
+
*
|
|
7
|
+
* ADR 0064 replaces the binary 0-bips waiver with a blended rate:
|
|
8
|
+
*
|
|
9
|
+
* - `remaining >= orderNotional` → `0` bips; the allowance covers the order.
|
|
10
|
+
* - `0 < remaining < orderNotional` → `maxFeeBips * (1 - remaining/orderNotional)`;
|
|
11
|
+
* the fee is charged only on the share the allowance did not cover.
|
|
12
|
+
* - `remaining <= 0` → the allowance is spent, so the source does not apply.
|
|
13
|
+
*
|
|
14
|
+
* An absent `remainingNotionalUsd` means the backend did not bound the
|
|
15
|
+
* allowance, which stays a full waiver — the pre-existing behavior for an
|
|
16
|
+
* eligible gate that reports no cap.
|
|
17
|
+
*
|
|
18
|
+
* A *bounded* allowance with an absent or non-positive `orderNotionalUsd` does
|
|
19
|
+
* not apply at all. Quoting the full waiver there would charge nothing on an
|
|
20
|
+
* order whose size is unknown and silently over-consume the cap; withholding is
|
|
21
|
+
* the fail-closed direction, and the same one an exhausted or stale gate takes.
|
|
22
|
+
* A rate-only preview of a bounded allowance therefore quotes the next-lowest
|
|
23
|
+
* source rather than a waiver the order may not receive.
|
|
24
|
+
*
|
|
25
|
+
* Pure, so preview and submit consume exactly the same arithmetic and their
|
|
26
|
+
* quoted and charged fees cannot drift.
|
|
27
|
+
*
|
|
28
|
+
* @param params - The inputs to the blended-rate formula.
|
|
29
|
+
* @param params.status - The subscription eligibility gate outcome.
|
|
30
|
+
* @param params.maxFeeBips - The default MetaMask builder fee, in basis points.
|
|
31
|
+
* @param params.orderNotionalUsd - Order notional (USD), when the caller knows it.
|
|
32
|
+
* @returns The subscription source's effective rate and how much it covered.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSubscriptionWaiverRate(params) {
|
|
35
|
+
const { status, maxFeeBips, orderNotionalUsd } = params;
|
|
36
|
+
if (!status.eligible) {
|
|
37
|
+
return { applies: false, feeBips: maxFeeBips, kind: 'none' };
|
|
38
|
+
}
|
|
39
|
+
const remaining = status.remainingNotionalUsd;
|
|
40
|
+
// The backend reported no bound on the allowance, so nothing limits it.
|
|
41
|
+
if (remaining === undefined) {
|
|
42
|
+
return { applies: true, feeBips: 0, kind: 'full' };
|
|
43
|
+
}
|
|
44
|
+
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
45
|
+
// A reported allowance of zero is spent, whatever the gate said.
|
|
46
|
+
return { applies: false, feeBips: maxFeeBips, kind: 'none' };
|
|
47
|
+
}
|
|
48
|
+
// A bounded allowance with no notional to measure it against cannot be
|
|
49
|
+
// honoured: granting the full waiver would charge nothing on an order of
|
|
50
|
+
// unknown size and over-consume the cap. Withholding is the fail-closed
|
|
51
|
+
// direction, and matches how an exhausted or stale gate already behaves.
|
|
52
|
+
if (orderNotionalUsd === undefined ||
|
|
53
|
+
!Number.isFinite(orderNotionalUsd) ||
|
|
54
|
+
orderNotionalUsd <= 0) {
|
|
55
|
+
return { applies: false, feeBips: maxFeeBips, kind: 'none' };
|
|
56
|
+
}
|
|
57
|
+
if (remaining >= orderNotionalUsd) {
|
|
58
|
+
return {
|
|
59
|
+
applies: true,
|
|
60
|
+
feeBips: 0,
|
|
61
|
+
kind: 'full',
|
|
62
|
+
coveredNotionalUsd: orderNotionalUsd,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
applies: true,
|
|
67
|
+
feeBips: maxFeeBips * (1 - remaining / orderNotionalUsd),
|
|
68
|
+
kind: 'partial',
|
|
69
|
+
coveredNotionalUsd: remaining,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Tenths of a basis point in one unit rate.
|
|
74
|
+
*
|
|
75
|
+
* `MaxFeeTenthsBps` is `MaxFeeDecimal * 100000`, so dividing a tenths-of-a-bip
|
|
76
|
+
* figure by this returns the same decimal rate the fee quote reports.
|
|
77
|
+
*/
|
|
78
|
+
const BUILDER_FEE_TENTHS_BPS_PER_UNIT = 100_000;
|
|
79
|
+
/** Hex index of the flag byte inside a cloid string (after `0x` + 4 bytes). */
|
|
80
|
+
const FLAG_BYTE_START = 2 + SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength;
|
|
81
|
+
/** Full length of a venue cloid string: `0x` plus 16 bytes of hex. */
|
|
82
|
+
const CLOID_HEX_LENGTH = 34;
|
|
83
|
+
/** A well-formed venue cloid: `0x` followed by exactly 32 hex characters. */
|
|
84
|
+
const CLOID_PATTERN = /^0x[0-9a-f]{32}$/u;
|
|
85
|
+
/**
|
|
86
|
+
* Read the flag byte out of a cloid.
|
|
87
|
+
*
|
|
88
|
+
* @param clientOrderId - A venue client order ID, or nothing.
|
|
89
|
+
* @returns The flag byte, or undefined when the id is not a well-formed cloid.
|
|
90
|
+
*/
|
|
91
|
+
export function readSubscriptionCloidFlags(clientOrderId) {
|
|
92
|
+
const normalized = clientOrderId?.toLowerCase();
|
|
93
|
+
// The whole id must be well-formed hex, not merely the right length: a byte
|
|
94
|
+
// like `1z` parses as 1 under `parseInt`, so a malformed id would otherwise
|
|
95
|
+
// report whichever flags its leading digit happens to encode.
|
|
96
|
+
if (normalized === undefined || !CLOID_PATTERN.test(normalized)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return Number.parseInt(normalized.slice(FLAG_BYTE_START, FLAG_BYTE_START + 2), 16);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a cloid declares that a subscription fee reduction was applied.
|
|
103
|
+
*
|
|
104
|
+
* This is what the fill fan-out decodes downstream, so it is the honest
|
|
105
|
+
* definition of "marked": the program marker alone does not mean a reduction
|
|
106
|
+
* was charged.
|
|
107
|
+
*
|
|
108
|
+
* **The flag byte is only trusted behind the subscription program marker.** An
|
|
109
|
+
* arbitrary cloid has no reserved flag byte, so reading one out of it is
|
|
110
|
+
* meaningless — and a Scale ladder placed before this release is worse than
|
|
111
|
+
* meaningless: `createScaleOrderIdentity` only began zeroing that byte in this
|
|
112
|
+
* change, so historical rungs carry random entropy there and roughly half of
|
|
113
|
+
* them decode as waived if the byte is read on the Scale marker alone. The
|
|
114
|
+
* program marker is the one prefix no previously released client ever emitted,
|
|
115
|
+
* which is what makes this safe to point at historical fills.
|
|
116
|
+
*
|
|
117
|
+
* The consequence is that a **marked Scale rung reads as unwaived here**: it
|
|
118
|
+
* keeps its Scale marker so group recovery and cancel-by-cloid keep working, and
|
|
119
|
+
* therefore cannot be told apart from a legacy rung by its bytes alone. A
|
|
120
|
+
* decoder that needs Scale attribution has to correlate on something other than
|
|
121
|
+
* the cloid, or wait for a ladder layout that carries a version.
|
|
122
|
+
*
|
|
123
|
+
* The marker it checks is {@link SUBSCRIPTION_CLOID_CONFIG.ProgramId}, the
|
|
124
|
+
* registered perps-subscription program id.
|
|
125
|
+
*
|
|
126
|
+
* @param clientOrderId - A venue client order ID, or nothing.
|
|
127
|
+
* @returns True when the `fee_reduction_applied` flag is set behind the
|
|
128
|
+
* subscription program marker.
|
|
129
|
+
*/
|
|
130
|
+
export function hasFeeReductionAppliedFlag(clientOrderId) {
|
|
131
|
+
if (!isSubscriptionProgramCloid(clientOrderId)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const flags = readSubscriptionCloidFlags(clientOrderId);
|
|
135
|
+
return (flags !== undefined &&
|
|
136
|
+
isFlagSet(flags, SUBSCRIPTION_CLOID_FLAGS.FeeReductionApplied));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether one bit of a flag byte is set.
|
|
140
|
+
*
|
|
141
|
+
* Written arithmetically rather than with a bitwise `&`: the flag byte is a
|
|
142
|
+
* small unsigned integer, so shifting the bit into place and reading its parity
|
|
143
|
+
* is exact, and it keeps this file free of bitwise operators the repo's lint
|
|
144
|
+
* rules disallow.
|
|
145
|
+
*
|
|
146
|
+
* @param flags - The flag byte read out of a cloid.
|
|
147
|
+
* @param bit - The single-bit flag value to test, e.g. `0x01`.
|
|
148
|
+
* @returns True when that bit is set.
|
|
149
|
+
*/
|
|
150
|
+
function isFlagSet(flags, bit) {
|
|
151
|
+
return Math.floor(flags / bit) % 2 === 1;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether a cloid carries the subscription program marker in its leading bytes.
|
|
155
|
+
*
|
|
156
|
+
* Only orders that had no cloid of their own get the program marker; an order
|
|
157
|
+
* that already carried one (a Scale rung) keeps its own leading marker and
|
|
158
|
+
* carries the subscription attribution in the flag byte instead. Decoders
|
|
159
|
+
* should therefore key on {@link hasFeeReductionAppliedFlag}, and use this only
|
|
160
|
+
* to tell the two layouts apart.
|
|
161
|
+
*
|
|
162
|
+
* @param clientOrderId - A venue client order ID, or nothing.
|
|
163
|
+
* @returns True when the cloid starts with the subscription program id.
|
|
164
|
+
*/
|
|
165
|
+
export function isSubscriptionProgramCloid(clientOrderId) {
|
|
166
|
+
const normalized = clientOrderId?.toLowerCase();
|
|
167
|
+
// Hex-validated for the same reason the flag reader is: a length-and-prefix
|
|
168
|
+
// check would accept an id whose remaining bytes are not hex at all, and this
|
|
169
|
+
// predicate is what gates the decoder.
|
|
170
|
+
return Boolean(normalized &&
|
|
171
|
+
CLOID_PATTERN.test(normalized) &&
|
|
172
|
+
normalized.startsWith(`0x${SUBSCRIPTION_CLOID_CONFIG.ProgramId}`));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Stamp the subscription marking onto a venue client order ID.
|
|
176
|
+
*
|
|
177
|
+
* A cloid is 16 bytes, laid out as:
|
|
178
|
+
*
|
|
179
|
+
* ```
|
|
180
|
+
* 0x <program marker: 4 bytes> <flags: 1 byte> <entropy: 11 bytes>
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* The flag byte sits *after* the leading marker rather than replacing it, which
|
|
184
|
+
* is what lets the marking compose with the cloid the Scale ladder already
|
|
185
|
+
* builds. Three cases:
|
|
186
|
+
*
|
|
187
|
+
* - **No existing cloid** — the leading bytes become
|
|
188
|
+
* {@link SUBSCRIPTION_CLOID_CONFIG.ProgramId} and the rest is fresh entropy.
|
|
189
|
+
* - **An id this package generated**, declared by the caller through
|
|
190
|
+
* `isGenerated` (a Scale rung) — its own leading marker and its trailing bytes
|
|
191
|
+
* are preserved, and only the flag byte is set. The Scale group marker still
|
|
192
|
+
* prefixes the id, so group recovery from open orders and cancel-by-cloid keep
|
|
193
|
+
* working, and the rung index in the last byte still keeps every rung unique.
|
|
194
|
+
* - **A caller-supplied `OrderParams.clientOrderId`** — returned untouched, and
|
|
195
|
+
* therefore unattributed. That id is the caller's own reconciliation,
|
|
196
|
+
* idempotency, and telemetry key: submitting a byte-altered version would hand
|
|
197
|
+
* the venue an id the caller never chose and cannot match a fill against.
|
|
198
|
+
* Losing attribution on those orders is the strictly better trade, since
|
|
199
|
+
* attribution is observability while the id is a correctness contract.
|
|
200
|
+
*
|
|
201
|
+
* Provenance is declared, never inferred. A leading marker cannot prove who
|
|
202
|
+
* generated an id: a caller is free to supply a 16-byte cloid that happens to
|
|
203
|
+
* begin with the Scale or subscription marker, and guessing from the prefix
|
|
204
|
+
* would rewrite exactly the id the contract promises to preserve.
|
|
205
|
+
*
|
|
206
|
+
* A cloid is only ever marked when the subscription source actually won, so any
|
|
207
|
+
* other fee source leaves the id exactly as the caller built it.
|
|
208
|
+
*
|
|
209
|
+
* @param params - The marking inputs.
|
|
210
|
+
* @param params.clientOrderId - The cloid already chosen for this order, if any.
|
|
211
|
+
* @param params.isGenerated - True only when `clientOrderId` was generated by
|
|
212
|
+
* this package and may therefore be re-stamped. Defaults to false, so an id of
|
|
213
|
+
* unknown provenance is preserved rather than rewritten.
|
|
214
|
+
* @param params.entropy - Hex entropy used when there is no existing cloid.
|
|
215
|
+
* @returns The marked cloid, or the caller's own id unchanged.
|
|
216
|
+
*/
|
|
217
|
+
export function markSubscriptionCloid(params) {
|
|
218
|
+
const { clientOrderId, isGenerated = false, entropy } = params;
|
|
219
|
+
const flags = SUBSCRIPTION_CLOID_FLAGS.FeeReductionApplied.toString(16).padStart(2, '0');
|
|
220
|
+
let body;
|
|
221
|
+
if (clientOrderId !== undefined && !isGenerated) {
|
|
222
|
+
// Not ours to rewrite: hand it back exactly as supplied and forgo
|
|
223
|
+
// attribution.
|
|
224
|
+
if (!isHexString(clientOrderId)) {
|
|
225
|
+
throw new Error('Client order ID is not a hex string');
|
|
226
|
+
}
|
|
227
|
+
return clientOrderId;
|
|
228
|
+
}
|
|
229
|
+
else if (clientOrderId?.length === CLOID_HEX_LENGTH) {
|
|
230
|
+
const existing = clientOrderId.slice(2).toLowerCase();
|
|
231
|
+
// Keep the generated marker and trailing bytes; claim only the flag byte.
|
|
232
|
+
body = `${existing.slice(0, SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength)}${flags}${existing.slice(SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength + 2)}`;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const suffix = entropy
|
|
236
|
+
.toLowerCase()
|
|
237
|
+
.replace(/[^0-9a-f]/gu, '')
|
|
238
|
+
.slice(0, SUBSCRIPTION_CLOID_CONFIG.EntropyHexLength)
|
|
239
|
+
.padEnd(SUBSCRIPTION_CLOID_CONFIG.EntropyHexLength, '0');
|
|
240
|
+
body = `${SUBSCRIPTION_CLOID_CONFIG.ProgramId}${flags}${suffix}`;
|
|
241
|
+
}
|
|
242
|
+
const marked = `0x${body}`;
|
|
243
|
+
if (!isHexString(marked) || marked.length !== CLOID_HEX_LENGTH) {
|
|
244
|
+
throw new Error('Failed to mark subscription client order ID');
|
|
245
|
+
}
|
|
246
|
+
return marked;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The MetaMask builder fee a discount actually buys, in tenths of a basis point.
|
|
250
|
+
*
|
|
251
|
+
* HyperLiquid's builder fee is an integer number of tenths of a basis point, so
|
|
252
|
+
* the venue floors whatever fraction a discount implies. Both preview and submit
|
|
253
|
+
* resolve the charged fee through here: quoting the unfloored fraction would
|
|
254
|
+
* promise a rate the venue cannot charge — a 6.667-bip blend is submitted as
|
|
255
|
+
* 6.6 — and the difference, though always in the user's favour, is a
|
|
256
|
+
* quote-versus-charge mismatch of exactly the kind this change set out to close.
|
|
257
|
+
*
|
|
258
|
+
* @param discountBips - Discount off the default builder fee, in basis points.
|
|
259
|
+
* @returns The charged builder fee in tenths of a basis point.
|
|
260
|
+
*/
|
|
261
|
+
export function quantizeBuilderFeeTenthsBps(discountBips) {
|
|
262
|
+
return Math.floor(BUILDER_FEE_CONFIG.MaxFeeTenthsBps *
|
|
263
|
+
(1 - discountBips / BASIS_POINTS_DIVISOR));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Re-price a fee quote from the unified fee resolution.
|
|
267
|
+
*
|
|
268
|
+
* The provider quotes the MetaMask component from whatever discount the last
|
|
269
|
+
* submit pushed into it, which knows nothing about the notional being quoted.
|
|
270
|
+
* The resolver does, so the preview replaces the MetaMask component with the
|
|
271
|
+
* resolved rate and rebuilds the total from it. That is what makes a quoted
|
|
272
|
+
* blended fee equal the fee the order will actually be charged.
|
|
273
|
+
*
|
|
274
|
+
* A quote whose placement carries no builder fee at all (`metamaskFeeRate` of
|
|
275
|
+
* `0`, e.g. TWAP) is left alone: there is no MetaMask fee to discount.
|
|
276
|
+
*
|
|
277
|
+
* @param params - The re-pricing inputs.
|
|
278
|
+
* @param params.fees - The provider's fee quote.
|
|
279
|
+
* @param params.resolution - The unified fee resolution, when one was computed.
|
|
280
|
+
* @param params.amount - Order notional (USD) as a string, when provided.
|
|
281
|
+
* @param params.chargesBuilderFee - The provider's policy on whether this
|
|
282
|
+
* placement carries a MetaMask builder fee at all, or undefined when it does not
|
|
283
|
+
* report one. Distinguishes a genuine zero (a TWAP, for instance) from the zero
|
|
284
|
+
* a concurrent fully-waived submit leaves in provider state.
|
|
285
|
+
* @returns The quote with its MetaMask component and totals re-priced.
|
|
286
|
+
*/
|
|
287
|
+
export function applyFeeResolution(params) {
|
|
288
|
+
const { fees, resolution, amount, chargesBuilderFee } = params;
|
|
289
|
+
if (resolution === undefined || fees.metamaskFeeRate === undefined) {
|
|
290
|
+
return fees;
|
|
291
|
+
}
|
|
292
|
+
// A provider rate of zero is not proof that this placement carries no builder
|
|
293
|
+
// fee: it is also what a concurrent fully-waived submit leaves behind in
|
|
294
|
+
// provider state. Distinguish the two by asking the policy, not the leftover
|
|
295
|
+
// number — otherwise an ordinary preview inherits someone else's waiver.
|
|
296
|
+
//
|
|
297
|
+
// Repricing a zero is opt-in: only a provider that explicitly reports it does
|
|
298
|
+
// charge a builder fee gets its zero overwritten. A provider that reports no
|
|
299
|
+
// policy at all keeps its zero, because a structural zero is the older and far
|
|
300
|
+
// likelier meaning, and quoting a fee the order will not pay is the worse of
|
|
301
|
+
// the two failures.
|
|
302
|
+
if (fees.metamaskFeeRate === 0 && chargesBuilderFee !== true) {
|
|
303
|
+
return fees;
|
|
304
|
+
}
|
|
305
|
+
// An unresolved discount means the `default` source won, which is a real
|
|
306
|
+
// answer of "no reduction" — not "leave the provider's number alone". The
|
|
307
|
+
// provider's rate reflects whatever discount the last submit pushed into it,
|
|
308
|
+
// so a concurrent order could otherwise leak its discount into this quote.
|
|
309
|
+
const discountBips = resolution.discountBips ?? 0;
|
|
310
|
+
// Quantized exactly as the venue will charge it, so the quote matches the
|
|
311
|
+
// fill rather than the unfloored fraction the discount implies.
|
|
312
|
+
const metamaskFeeRate = quantizeBuilderFeeTenthsBps(discountBips) / BUILDER_FEE_TENTHS_BPS_PER_UNIT;
|
|
313
|
+
const parsedAmount = amount === undefined ? undefined : Number.parseFloat(amount);
|
|
314
|
+
// A non-positive notional is not an order size, and recomputing from it would
|
|
315
|
+
// quote a negative fee. The rates are still re-priced; only the amounts are
|
|
316
|
+
// left as the provider reported them.
|
|
317
|
+
const notional = parsedAmount !== undefined &&
|
|
318
|
+
Number.isFinite(parsedAmount) &&
|
|
319
|
+
parsedAmount > 0
|
|
320
|
+
? parsedAmount
|
|
321
|
+
: undefined;
|
|
322
|
+
const protocolFeeRate = fees.protocolFeeRate ?? 0;
|
|
323
|
+
const feeRate = protocolFeeRate + metamaskFeeRate;
|
|
324
|
+
return {
|
|
325
|
+
...fees,
|
|
326
|
+
metamaskFeeRate,
|
|
327
|
+
feeRate,
|
|
328
|
+
...(notional !== undefined && {
|
|
329
|
+
metamaskFeeAmount: notional * metamaskFeeRate,
|
|
330
|
+
feeAmount: notional * feeRate,
|
|
331
|
+
}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
//# sourceMappingURL=subscriptionFeeWaiver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptionFeeWaiver.js","sourceRoot":"","sources":["../../src/utils/subscriptionFeeWaiver.ts"],"names":[],"mappings":"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","sourcesContent":["import { isHexString } from '@metamask/utils';\nimport type { Hex } from '@metamask/utils';\n\nimport {\n BASIS_POINTS_DIVISOR,\n BUILDER_FEE_CONFIG,\n} from '../constants/hyperLiquidConfig.js';\nimport {\n SUBSCRIPTION_CLOID_CONFIG,\n SUBSCRIPTION_CLOID_FLAGS,\n} from '../constants/perpsConfig.js';\nimport type {\n FeeCalculationResult,\n PerpsFeeResolution,\n PerpsSubscriptionFeeWaiverStatus,\n} from '../types/index.js';\n\n/**\n * How much of an order the subscription allowance covered.\n *\n * - `full` — the remaining allowance covered the whole order notional, so the\n * MetaMask builder fee is waived outright.\n * - `partial` — the allowance covered part of the order, so the fee is charged\n * on the uncovered share only.\n * - `none` — the gate did not pass, so subscription contributes no rate.\n */\nexport type PerpsSubscriptionWaiverKind = 'full' | 'partial' | 'none';\n\n/**\n * The subscription source's contribution to the unified fee comparison.\n */\nexport type PerpsSubscriptionWaiverRate = {\n /** Whether the subscription source produced a usable rate at all. */\n applies: boolean;\n\n /** Effective MetaMask builder fee in basis points under the waiver. */\n feeBips: number;\n\n /** How much of the order the allowance covered. */\n kind: PerpsSubscriptionWaiverKind;\n\n /** Order notional (USD) the allowance actually covered, when bounded. */\n coveredNotionalUsd?: number;\n};\n\n/**\n * Resolve the subscription source's effective fee rate for one order.\n *\n * ADR 0064 replaces the binary 0-bips waiver with a blended rate:\n *\n * - `remaining >= orderNotional` → `0` bips; the allowance covers the order.\n * - `0 < remaining < orderNotional` → `maxFeeBips * (1 - remaining/orderNotional)`;\n * the fee is charged only on the share the allowance did not cover.\n * - `remaining <= 0` → the allowance is spent, so the source does not apply.\n *\n * An absent `remainingNotionalUsd` means the backend did not bound the\n * allowance, which stays a full waiver — the pre-existing behavior for an\n * eligible gate that reports no cap.\n *\n * A *bounded* allowance with an absent or non-positive `orderNotionalUsd` does\n * not apply at all. Quoting the full waiver there would charge nothing on an\n * order whose size is unknown and silently over-consume the cap; withholding is\n * the fail-closed direction, and the same one an exhausted or stale gate takes.\n * A rate-only preview of a bounded allowance therefore quotes the next-lowest\n * source rather than a waiver the order may not receive.\n *\n * Pure, so preview and submit consume exactly the same arithmetic and their\n * quoted and charged fees cannot drift.\n *\n * @param params - The inputs to the blended-rate formula.\n * @param params.status - The subscription eligibility gate outcome.\n * @param params.maxFeeBips - The default MetaMask builder fee, in basis points.\n * @param params.orderNotionalUsd - Order notional (USD), when the caller knows it.\n * @returns The subscription source's effective rate and how much it covered.\n */\nexport function resolveSubscriptionWaiverRate(params: {\n status: PerpsSubscriptionFeeWaiverStatus;\n maxFeeBips: number;\n orderNotionalUsd?: number;\n}): PerpsSubscriptionWaiverRate {\n const { status, maxFeeBips, orderNotionalUsd } = params;\n\n if (!status.eligible) {\n return { applies: false, feeBips: maxFeeBips, kind: 'none' };\n }\n\n const remaining = status.remainingNotionalUsd;\n\n // The backend reported no bound on the allowance, so nothing limits it.\n if (remaining === undefined) {\n return { applies: true, feeBips: 0, kind: 'full' };\n }\n\n if (!Number.isFinite(remaining) || remaining <= 0) {\n // A reported allowance of zero is spent, whatever the gate said.\n return { applies: false, feeBips: maxFeeBips, kind: 'none' };\n }\n\n // A bounded allowance with no notional to measure it against cannot be\n // honoured: granting the full waiver would charge nothing on an order of\n // unknown size and over-consume the cap. Withholding is the fail-closed\n // direction, and matches how an exhausted or stale gate already behaves.\n if (\n orderNotionalUsd === undefined ||\n !Number.isFinite(orderNotionalUsd) ||\n orderNotionalUsd <= 0\n ) {\n return { applies: false, feeBips: maxFeeBips, kind: 'none' };\n }\n\n if (remaining >= orderNotionalUsd) {\n return {\n applies: true,\n feeBips: 0,\n kind: 'full',\n coveredNotionalUsd: orderNotionalUsd,\n };\n }\n\n return {\n applies: true,\n feeBips: maxFeeBips * (1 - remaining / orderNotionalUsd),\n kind: 'partial',\n coveredNotionalUsd: remaining,\n };\n}\n\n/**\n * Tenths of a basis point in one unit rate.\n *\n * `MaxFeeTenthsBps` is `MaxFeeDecimal * 100000`, so dividing a tenths-of-a-bip\n * figure by this returns the same decimal rate the fee quote reports.\n */\nconst BUILDER_FEE_TENTHS_BPS_PER_UNIT = 100_000;\n\n/** Hex index of the flag byte inside a cloid string (after `0x` + 4 bytes). */\nconst FLAG_BYTE_START = 2 + SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength;\n\n/** Full length of a venue cloid string: `0x` plus 16 bytes of hex. */\nconst CLOID_HEX_LENGTH = 34;\n\n/** A well-formed venue cloid: `0x` followed by exactly 32 hex characters. */\nconst CLOID_PATTERN = /^0x[0-9a-f]{32}$/u;\n\n/**\n * Read the flag byte out of a cloid.\n *\n * @param clientOrderId - A venue client order ID, or nothing.\n * @returns The flag byte, or undefined when the id is not a well-formed cloid.\n */\nexport function readSubscriptionCloidFlags(\n clientOrderId: string | null | undefined,\n): number | undefined {\n const normalized = clientOrderId?.toLowerCase();\n // The whole id must be well-formed hex, not merely the right length: a byte\n // like `1z` parses as 1 under `parseInt`, so a malformed id would otherwise\n // report whichever flags its leading digit happens to encode.\n if (normalized === undefined || !CLOID_PATTERN.test(normalized)) {\n return undefined;\n }\n return Number.parseInt(\n normalized.slice(FLAG_BYTE_START, FLAG_BYTE_START + 2),\n 16,\n );\n}\n\n/**\n * Whether a cloid declares that a subscription fee reduction was applied.\n *\n * This is what the fill fan-out decodes downstream, so it is the honest\n * definition of \"marked\": the program marker alone does not mean a reduction\n * was charged.\n *\n * **The flag byte is only trusted behind the subscription program marker.** An\n * arbitrary cloid has no reserved flag byte, so reading one out of it is\n * meaningless — and a Scale ladder placed before this release is worse than\n * meaningless: `createScaleOrderIdentity` only began zeroing that byte in this\n * change, so historical rungs carry random entropy there and roughly half of\n * them decode as waived if the byte is read on the Scale marker alone. The\n * program marker is the one prefix no previously released client ever emitted,\n * which is what makes this safe to point at historical fills.\n *\n * The consequence is that a **marked Scale rung reads as unwaived here**: it\n * keeps its Scale marker so group recovery and cancel-by-cloid keep working, and\n * therefore cannot be told apart from a legacy rung by its bytes alone. A\n * decoder that needs Scale attribution has to correlate on something other than\n * the cloid, or wait for a ladder layout that carries a version.\n *\n * The marker it checks is {@link SUBSCRIPTION_CLOID_CONFIG.ProgramId}, the\n * registered perps-subscription program id.\n *\n * @param clientOrderId - A venue client order ID, or nothing.\n * @returns True when the `fee_reduction_applied` flag is set behind the\n * subscription program marker.\n */\nexport function hasFeeReductionAppliedFlag(\n clientOrderId: string | null | undefined,\n): boolean {\n if (!isSubscriptionProgramCloid(clientOrderId)) {\n return false;\n }\n const flags = readSubscriptionCloidFlags(clientOrderId);\n return (\n flags !== undefined &&\n isFlagSet(flags, SUBSCRIPTION_CLOID_FLAGS.FeeReductionApplied)\n );\n}\n\n/**\n * Whether one bit of a flag byte is set.\n *\n * Written arithmetically rather than with a bitwise `&`: the flag byte is a\n * small unsigned integer, so shifting the bit into place and reading its parity\n * is exact, and it keeps this file free of bitwise operators the repo's lint\n * rules disallow.\n *\n * @param flags - The flag byte read out of a cloid.\n * @param bit - The single-bit flag value to test, e.g. `0x01`.\n * @returns True when that bit is set.\n */\nfunction isFlagSet(flags: number, bit: number): boolean {\n return Math.floor(flags / bit) % 2 === 1;\n}\n\n/**\n * Whether a cloid carries the subscription program marker in its leading bytes.\n *\n * Only orders that had no cloid of their own get the program marker; an order\n * that already carried one (a Scale rung) keeps its own leading marker and\n * carries the subscription attribution in the flag byte instead. Decoders\n * should therefore key on {@link hasFeeReductionAppliedFlag}, and use this only\n * to tell the two layouts apart.\n *\n * @param clientOrderId - A venue client order ID, or nothing.\n * @returns True when the cloid starts with the subscription program id.\n */\nexport function isSubscriptionProgramCloid(\n clientOrderId: string | null | undefined,\n): boolean {\n const normalized = clientOrderId?.toLowerCase();\n // Hex-validated for the same reason the flag reader is: a length-and-prefix\n // check would accept an id whose remaining bytes are not hex at all, and this\n // predicate is what gates the decoder.\n return Boolean(\n normalized &&\n CLOID_PATTERN.test(normalized) &&\n normalized.startsWith(`0x${SUBSCRIPTION_CLOID_CONFIG.ProgramId}`),\n );\n}\n\n/**\n * Stamp the subscription marking onto a venue client order ID.\n *\n * A cloid is 16 bytes, laid out as:\n *\n * ```\n * 0x <program marker: 4 bytes> <flags: 1 byte> <entropy: 11 bytes>\n * ```\n *\n * The flag byte sits *after* the leading marker rather than replacing it, which\n * is what lets the marking compose with the cloid the Scale ladder already\n * builds. Three cases:\n *\n * - **No existing cloid** — the leading bytes become\n * {@link SUBSCRIPTION_CLOID_CONFIG.ProgramId} and the rest is fresh entropy.\n * - **An id this package generated**, declared by the caller through\n * `isGenerated` (a Scale rung) — its own leading marker and its trailing bytes\n * are preserved, and only the flag byte is set. The Scale group marker still\n * prefixes the id, so group recovery from open orders and cancel-by-cloid keep\n * working, and the rung index in the last byte still keeps every rung unique.\n * - **A caller-supplied `OrderParams.clientOrderId`** — returned untouched, and\n * therefore unattributed. That id is the caller's own reconciliation,\n * idempotency, and telemetry key: submitting a byte-altered version would hand\n * the venue an id the caller never chose and cannot match a fill against.\n * Losing attribution on those orders is the strictly better trade, since\n * attribution is observability while the id is a correctness contract.\n *\n * Provenance is declared, never inferred. A leading marker cannot prove who\n * generated an id: a caller is free to supply a 16-byte cloid that happens to\n * begin with the Scale or subscription marker, and guessing from the prefix\n * would rewrite exactly the id the contract promises to preserve.\n *\n * A cloid is only ever marked when the subscription source actually won, so any\n * other fee source leaves the id exactly as the caller built it.\n *\n * @param params - The marking inputs.\n * @param params.clientOrderId - The cloid already chosen for this order, if any.\n * @param params.isGenerated - True only when `clientOrderId` was generated by\n * this package and may therefore be re-stamped. Defaults to false, so an id of\n * unknown provenance is preserved rather than rewritten.\n * @param params.entropy - Hex entropy used when there is no existing cloid.\n * @returns The marked cloid, or the caller's own id unchanged.\n */\nexport function markSubscriptionCloid(params: {\n clientOrderId?: string;\n isGenerated?: boolean;\n entropy: string;\n}): Hex {\n const { clientOrderId, isGenerated = false, entropy } = params;\n const flags = SUBSCRIPTION_CLOID_FLAGS.FeeReductionApplied.toString(\n 16,\n ).padStart(2, '0');\n\n let body: string;\n if (clientOrderId !== undefined && !isGenerated) {\n // Not ours to rewrite: hand it back exactly as supplied and forgo\n // attribution.\n if (!isHexString(clientOrderId)) {\n throw new Error('Client order ID is not a hex string');\n }\n return clientOrderId as Hex;\n } else if (clientOrderId?.length === CLOID_HEX_LENGTH) {\n const existing = clientOrderId.slice(2).toLowerCase();\n // Keep the generated marker and trailing bytes; claim only the flag byte.\n body = `${existing.slice(0, SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength)}${flags}${existing.slice(\n SUBSCRIPTION_CLOID_CONFIG.ProgramIdHexLength + 2,\n )}`;\n } else {\n const suffix = entropy\n .toLowerCase()\n .replace(/[^0-9a-f]/gu, '')\n .slice(0, SUBSCRIPTION_CLOID_CONFIG.EntropyHexLength)\n .padEnd(SUBSCRIPTION_CLOID_CONFIG.EntropyHexLength, '0');\n body = `${SUBSCRIPTION_CLOID_CONFIG.ProgramId}${flags}${suffix}`;\n }\n\n const marked: Hex = `0x${body}`;\n\n if (!isHexString(marked) || marked.length !== CLOID_HEX_LENGTH) {\n throw new Error('Failed to mark subscription client order ID');\n }\n\n return marked;\n}\n\n/**\n * The MetaMask builder fee a discount actually buys, in tenths of a basis point.\n *\n * HyperLiquid's builder fee is an integer number of tenths of a basis point, so\n * the venue floors whatever fraction a discount implies. Both preview and submit\n * resolve the charged fee through here: quoting the unfloored fraction would\n * promise a rate the venue cannot charge — a 6.667-bip blend is submitted as\n * 6.6 — and the difference, though always in the user's favour, is a\n * quote-versus-charge mismatch of exactly the kind this change set out to close.\n *\n * @param discountBips - Discount off the default builder fee, in basis points.\n * @returns The charged builder fee in tenths of a basis point.\n */\nexport function quantizeBuilderFeeTenthsBps(discountBips: number): number {\n return Math.floor(\n BUILDER_FEE_CONFIG.MaxFeeTenthsBps *\n (1 - discountBips / BASIS_POINTS_DIVISOR),\n );\n}\n\n/**\n * Re-price a fee quote from the unified fee resolution.\n *\n * The provider quotes the MetaMask component from whatever discount the last\n * submit pushed into it, which knows nothing about the notional being quoted.\n * The resolver does, so the preview replaces the MetaMask component with the\n * resolved rate and rebuilds the total from it. That is what makes a quoted\n * blended fee equal the fee the order will actually be charged.\n *\n * A quote whose placement carries no builder fee at all (`metamaskFeeRate` of\n * `0`, e.g. TWAP) is left alone: there is no MetaMask fee to discount.\n *\n * @param params - The re-pricing inputs.\n * @param params.fees - The provider's fee quote.\n * @param params.resolution - The unified fee resolution, when one was computed.\n * @param params.amount - Order notional (USD) as a string, when provided.\n * @param params.chargesBuilderFee - The provider's policy on whether this\n * placement carries a MetaMask builder fee at all, or undefined when it does not\n * report one. Distinguishes a genuine zero (a TWAP, for instance) from the zero\n * a concurrent fully-waived submit leaves in provider state.\n * @returns The quote with its MetaMask component and totals re-priced.\n */\nexport function applyFeeResolution(params: {\n fees: FeeCalculationResult;\n resolution: PerpsFeeResolution | undefined;\n amount?: string;\n chargesBuilderFee?: boolean;\n}): FeeCalculationResult {\n const { fees, resolution, amount, chargesBuilderFee } = params;\n\n if (resolution === undefined || fees.metamaskFeeRate === undefined) {\n return fees;\n }\n\n // A provider rate of zero is not proof that this placement carries no builder\n // fee: it is also what a concurrent fully-waived submit leaves behind in\n // provider state. Distinguish the two by asking the policy, not the leftover\n // number — otherwise an ordinary preview inherits someone else's waiver.\n //\n // Repricing a zero is opt-in: only a provider that explicitly reports it does\n // charge a builder fee gets its zero overwritten. A provider that reports no\n // policy at all keeps its zero, because a structural zero is the older and far\n // likelier meaning, and quoting a fee the order will not pay is the worse of\n // the two failures.\n if (fees.metamaskFeeRate === 0 && chargesBuilderFee !== true) {\n return fees;\n }\n\n // An unresolved discount means the `default` source won, which is a real\n // answer of \"no reduction\" — not \"leave the provider's number alone\". The\n // provider's rate reflects whatever discount the last submit pushed into it,\n // so a concurrent order could otherwise leak its discount into this quote.\n const discountBips = resolution.discountBips ?? 0;\n\n // Quantized exactly as the venue will charge it, so the quote matches the\n // fill rather than the unfloored fraction the discount implies.\n const metamaskFeeRate =\n quantizeBuilderFeeTenthsBps(discountBips) / BUILDER_FEE_TENTHS_BPS_PER_UNIT;\n const parsedAmount =\n amount === undefined ? undefined : Number.parseFloat(amount);\n // A non-positive notional is not an order size, and recomputing from it would\n // quote a negative fee. The rates are still re-priced; only the amounts are\n // left as the provider reported them.\n const notional =\n parsedAmount !== undefined &&\n Number.isFinite(parsedAmount) &&\n parsedAmount > 0\n ? parsedAmount\n : undefined;\n\n const protocolFeeRate = fees.protocolFeeRate ?? 0;\n const feeRate = protocolFeeRate + metamaskFeeRate;\n\n return {\n ...fees,\n metamaskFeeRate,\n feeRate,\n ...(notional !== undefined && {\n metamaskFeeAmount: notional * metamaskFeeRate,\n feeAmount: notional * feeRate,\n }),\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask-previews/perps-controller",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.4.0-preview-aecbce4ff",
|
|
4
4
|
"description": "Controller for perpetual trading functionality in MetaMask",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ethereum",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/types/index.d.ts",
|
|
38
38
|
"default": "./dist/types/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./utils": {
|
|
41
|
+
"types": "./dist/utils/index.d.ts",
|
|
42
|
+
"default": "./dist/utils/index.js"
|
|
43
|
+
},
|
|
40
44
|
"./utils/*": {
|
|
41
45
|
"types": "./dist/utils/*.d.ts",
|
|
42
46
|
"default": "./dist/utils/*.js"
|
|
@@ -80,21 +84,22 @@
|
|
|
80
84
|
"devDependencies": {
|
|
81
85
|
"@metamask/account-tree-controller": "^10.0.1",
|
|
82
86
|
"@metamask/authenticated-user-storage": "^4.0.0",
|
|
83
|
-
"@metamask/auto-changelog": "^6.1
|
|
87
|
+
"@metamask/auto-changelog": "^6.2.1",
|
|
84
88
|
"@metamask/geolocation-controller": "^2.0.0",
|
|
85
89
|
"@metamask/keyring-controller": "^28.0.0",
|
|
86
90
|
"@metamask/keyring-internal-api": "^12.0.0",
|
|
87
91
|
"@metamask/network-controller": "^37.0.0",
|
|
88
|
-
"@metamask/profile-sync-controller": "^32.
|
|
92
|
+
"@metamask/profile-sync-controller": "^32.2.0",
|
|
89
93
|
"@metamask/remote-feature-flag-controller": "^7.0.0",
|
|
94
|
+
"@metamask/subscription-controller": "^9.1.0",
|
|
90
95
|
"@metamask/transaction-controller": "^70.1.0",
|
|
91
96
|
"@types/jest": "^30.0.0",
|
|
92
97
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
93
98
|
"deepmerge": "^4.2.2",
|
|
94
99
|
"jest": "^30.4.2",
|
|
95
|
-
"rimraf": "^
|
|
96
|
-
"ts-jest": "^29.4.
|
|
97
|
-
"tsx": "^4.
|
|
100
|
+
"rimraf": "^6.1.3",
|
|
101
|
+
"ts-jest": "^29.4.12",
|
|
102
|
+
"tsx": "^4.23.13",
|
|
98
103
|
"typedoc": "^0.25.13",
|
|
99
104
|
"typedoc-plugin-missing-exports": "^2.0.0",
|
|
100
105
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|