@velocity-exchange/sdk 0.8.0 → 0.9.0

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 (55) hide show
  1. package/lib/browser/adminClient.d.ts +11 -8
  2. package/lib/browser/adminClient.js +13 -10
  3. package/lib/browser/decode/user.js +3 -1
  4. package/lib/browser/equityFloorManager.d.ts +150 -0
  5. package/lib/browser/equityFloorManager.js +279 -0
  6. package/lib/browser/idl/velocity.d.ts +84 -10
  7. package/lib/browser/idl/velocity.json +84 -10
  8. package/lib/browser/index.d.ts +2 -0
  9. package/lib/browser/index.js +2 -0
  10. package/lib/browser/jupiter/jupiterClient.js +24 -3
  11. package/lib/browser/math/margin.d.ts +26 -0
  12. package/lib/browser/math/margin.js +41 -1
  13. package/lib/browser/swap/UnifiedSwapClient.js +17 -3
  14. package/lib/browser/titan/titanClient.d.ts +6 -0
  15. package/lib/browser/titan/titanClient.js +44 -13
  16. package/lib/browser/types.d.ts +6 -2
  17. package/lib/browser/types.js +2 -0
  18. package/lib/browser/user.d.ts +38 -11
  19. package/lib/browser/user.js +84 -33
  20. package/lib/browser/velocityClient.d.ts +8 -7
  21. package/lib/browser/velocityClient.js +11 -15
  22. package/lib/node/adminClient.d.ts +11 -8
  23. package/lib/node/adminClient.d.ts.map +1 -1
  24. package/lib/node/adminClient.js +13 -10
  25. package/lib/node/decode/user.d.ts.map +1 -1
  26. package/lib/node/decode/user.js +3 -1
  27. package/lib/node/equityFloorManager.d.ts +151 -0
  28. package/lib/node/equityFloorManager.d.ts.map +1 -0
  29. package/lib/node/equityFloorManager.js +279 -0
  30. package/lib/node/idl/velocity.d.ts +84 -10
  31. package/lib/node/idl/velocity.d.ts.map +1 -1
  32. package/lib/node/idl/velocity.json +84 -10
  33. package/lib/node/index.d.ts +2 -0
  34. package/lib/node/index.d.ts.map +1 -1
  35. package/lib/node/index.js +2 -0
  36. package/lib/node/jupiter/jupiterClient.d.ts.map +1 -1
  37. package/lib/node/jupiter/jupiterClient.js +24 -3
  38. package/lib/node/math/margin.d.ts +26 -0
  39. package/lib/node/math/margin.d.ts.map +1 -1
  40. package/lib/node/math/margin.js +41 -1
  41. package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
  42. package/lib/node/swap/UnifiedSwapClient.js +17 -3
  43. package/lib/node/titan/titanClient.d.ts +6 -0
  44. package/lib/node/titan/titanClient.d.ts.map +1 -1
  45. package/lib/node/titan/titanClient.js +44 -13
  46. package/lib/node/types.d.ts +6 -2
  47. package/lib/node/types.d.ts.map +1 -1
  48. package/lib/node/types.js +2 -0
  49. package/lib/node/user.d.ts +38 -11
  50. package/lib/node/user.d.ts.map +1 -1
  51. package/lib/node/user.js +84 -33
  52. package/lib/node/velocityClient.d.ts +8 -7
  53. package/lib/node/velocityClient.d.ts.map +1 -1
  54. package/lib/node/velocityClient.js +11 -15
  55. package/package.json +1 -1
@@ -2500,24 +2500,27 @@ export declare class AdminClient extends VelocityClient {
2500
2500
  */
2501
2501
  getUpdateSpecialUserStatusIx(userAccountPublicKey: PublicKey, status: number): Promise<TransactionInstruction>;
2502
2502
  /**
2503
- * Sets a `User` account's `equityFloor`: the minimum cross-margin total
2504
- * collateral (QUOTE_PRECISION) the account must keep. Below the floor the
2505
- * program rejects risk-increasing order placement and fills, withdrawals,
2506
- * and deposit/position transfers out of the account; reduce-only activity
2507
- * stays allowed. Requires warm admin (`check_warm`); the account's
2508
- * authority and delegate cannot change it. Pass `0` to disable.
2503
+ * Sets a `User` account's `equityFloor` and `equityFloorBuffer`. The floor
2504
+ * is the minimum cross-margin total collateral (QUOTE_PRECISION) below
2505
+ * which the permissionless breaker can trip; risk-increasing order
2506
+ * placement and fills, withdrawals, and deposit/position transfers out of
2507
+ * the account must clear `floor + buffer` (`EquityBelowFloor` otherwise);
2508
+ * reduce-only activity stays allowed. Requires warm admin (`check_warm`);
2509
+ * the account's authority and delegate cannot change either value. Pass
2510
+ * `0` as the floor to disable both checks.
2509
2511
  * @param userAccountPublicKey - `User` PDA to update.
2510
2512
  * @param equityFloor - New floor, QUOTE_PRECISION; `0` disables.
2513
+ * @param equityFloorBuffer - New buffer, QUOTE_PRECISION; no effect while the floor is 0.
2511
2514
  * @param txParams - Optional transaction-building overrides.
2512
2515
  * @returns Transaction signature.
2513
2516
  */
2514
- updateUserEquityFloor(userAccountPublicKey: PublicKey, equityFloor: BN, txParams?: TxParams): Promise<TransactionSignature>;
2517
+ updateUserEquityFloor(userAccountPublicKey: PublicKey, equityFloor: BN, equityFloorBuffer: BN, txParams?: TxParams): Promise<TransactionSignature>;
2515
2518
  /**
2516
2519
  * Builds the `updateUserEquityFloor` instruction without sending it. See
2517
2520
  * `updateUserEquityFloor`.
2518
2521
  * @returns The unsigned `updateUserEquityFloor` instruction.
2519
2522
  */
2520
- getUpdateUserEquityFloorIx(userAccountPublicKey: PublicKey, equityFloor: BN): Promise<TransactionInstruction>;
2523
+ getUpdateUserEquityFloorIx(userAccountPublicKey: PublicKey, equityFloor: BN, equityFloorBuffer: BN): Promise<TransactionInstruction>;
2521
2524
  /**
2522
2525
  * Clears the authority-wide equity floor breaker set by the permissionless
2523
2526
  * `tripEquityFloorBreaker` keeper instruction, unfreezing all of the
@@ -4936,19 +4936,22 @@ class AdminClient extends velocityClient_1.VelocityClient {
4936
4936
  });
4937
4937
  }
4938
4938
  /**
4939
- * Sets a `User` account's `equityFloor`: the minimum cross-margin total
4940
- * collateral (QUOTE_PRECISION) the account must keep. Below the floor the
4941
- * program rejects risk-increasing order placement and fills, withdrawals,
4942
- * and deposit/position transfers out of the account; reduce-only activity
4943
- * stays allowed. Requires warm admin (`check_warm`); the account's
4944
- * authority and delegate cannot change it. Pass `0` to disable.
4939
+ * Sets a `User` account's `equityFloor` and `equityFloorBuffer`. The floor
4940
+ * is the minimum cross-margin total collateral (QUOTE_PRECISION) below
4941
+ * which the permissionless breaker can trip; risk-increasing order
4942
+ * placement and fills, withdrawals, and deposit/position transfers out of
4943
+ * the account must clear `floor + buffer` (`EquityBelowFloor` otherwise);
4944
+ * reduce-only activity stays allowed. Requires warm admin (`check_warm`);
4945
+ * the account's authority and delegate cannot change either value. Pass
4946
+ * `0` as the floor to disable both checks.
4945
4947
  * @param userAccountPublicKey - `User` PDA to update.
4946
4948
  * @param equityFloor - New floor, QUOTE_PRECISION; `0` disables.
4949
+ * @param equityFloorBuffer - New buffer, QUOTE_PRECISION; no effect while the floor is 0.
4947
4950
  * @param txParams - Optional transaction-building overrides.
4948
4951
  * @returns Transaction signature.
4949
4952
  */
4950
- async updateUserEquityFloor(userAccountPublicKey, equityFloor, txParams) {
4951
- const ix = await this.getUpdateUserEquityFloorIx(userAccountPublicKey, equityFloor);
4953
+ async updateUserEquityFloor(userAccountPublicKey, equityFloor, equityFloorBuffer, txParams) {
4954
+ const ix = await this.getUpdateUserEquityFloorIx(userAccountPublicKey, equityFloor, equityFloorBuffer);
4952
4955
  const tx = await this.buildTransaction(ix, txParams);
4953
4956
  const { txSig } = await this.sendTransaction(tx, [], this.opts);
4954
4957
  return txSig;
@@ -4958,8 +4961,8 @@ class AdminClient extends velocityClient_1.VelocityClient {
4958
4961
  * `updateUserEquityFloor`.
4959
4962
  * @returns The unsigned `updateUserEquityFloor` instruction.
4960
4963
  */
4961
- async getUpdateUserEquityFloorIx(userAccountPublicKey, equityFloor) {
4962
- return this.program.instruction.updateUserEquityFloor(equityFloor, {
4964
+ async getUpdateUserEquityFloorIx(userAccountPublicKey, equityFloor, equityFloorBuffer) {
4965
+ return this.program.instruction.updateUserEquityFloor(equityFloor, equityFloorBuffer, {
4963
4966
  accounts: {
4964
4967
  admin: this.useHotWalletAdmin
4965
4968
  ? this.wallet.publicKey
@@ -336,7 +336,8 @@ function decodeUser(buffer) {
336
336
  offset += 3; // padding
337
337
  const equityFloor = readUnsignedBigInt64LE(buffer, offset);
338
338
  offset += 8;
339
- offset += 8; // padding2
339
+ const equityFloorBuffer = readUnsignedBigInt64LE(buffer, offset);
340
+ offset += 8;
340
341
  return {
341
342
  authority,
342
343
  delegate,
@@ -366,6 +367,7 @@ function decodeUser(buffer) {
366
367
  poolId,
367
368
  specialUserStatus,
368
369
  equityFloor,
370
+ equityFloorBuffer,
369
371
  };
370
372
  }
371
373
  exports.decodeUser = decodeUser;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Maker-facing abstraction over the per-subaccount equity floor. The on-chain
3
+ * checks are deliberately per-subaccount (each check reads only the one User
4
+ * account already loaded in the hot path), which leaves the burden of placing
5
+ * floor where the equity is on the delegate. This module removes that burden:
6
+ * it treats an authority's subaccounts as one pool, plans quote transfers with
7
+ * the exact floor delta they must carry, computes how much can leave a
8
+ * subaccount, and rebalances the floor split to match where the equity
9
+ * actually sits — so a delegate never has to reason about floor placement to
10
+ * stay clear of the breaker.
11
+ */
12
+ import { PublicKey } from '@solana/web3.js';
13
+ import { TransactionSignature } from '@solana/web3.js';
14
+ import { BN } from './isomorphic/anchor';
15
+ import { EquityFloorLevel } from './math/margin';
16
+ import { VelocityClient } from './velocityClient';
17
+ import { TxParams } from './types';
18
+ /** One subaccount's standing relative to its floor. All BN values QUOTE_PRECISION. */
19
+ export type SubaccountFloorStatus = {
20
+ subAccountId: number;
21
+ /** Cross-margin total collateral, strict (TWAP-bounded) pricing — what the on-chain checks see. */
22
+ equity: BN;
23
+ equityFloor: BN;
24
+ equityFloorBuffer: BN;
25
+ /** `equityFloor + equityFloorBuffer`: what risk-increasing actions must clear. */
26
+ bufferedFloor: BN;
27
+ /** `equity - equityFloor`; negative means the breaker can trip on this subaccount. */
28
+ headroom: BN;
29
+ /** `equity - bufferedFloor`; negative means risk-increasing actions are rejecting. */
30
+ bufferedHeadroom: BN;
31
+ level: EquityFloorLevel;
32
+ };
33
+ /** Authority-wide standing: aggregates plus the per-subaccount breakdown. */
34
+ export type EquityFloorStatus = {
35
+ authority: PublicKey;
36
+ breakerTripped: boolean;
37
+ totalEquity: BN;
38
+ totalFloor: BN;
39
+ totalBuffer: BN;
40
+ /** `totalEquity - (totalFloor + totalBuffer)`: the slack the whole pool has to allocate. */
41
+ totalBufferedHeadroom: BN;
42
+ /** Worst level across subaccounts with a floor set. */
43
+ level: EquityFloorLevel;
44
+ subaccounts: SubaccountFloorStatus[];
45
+ };
46
+ /** A floor-only rebalance step: zero-amount `transferDepositByDelegate` carrying `equityFloorDelta`. */
47
+ export type FloorMove = {
48
+ fromSubAccountId: number;
49
+ toSubAccountId: number;
50
+ equityFloorDelta: BN;
51
+ };
52
+ /** A fully resolved quote transfer ready to submit. */
53
+ export type QuoteTransferPlan = {
54
+ amount: BN;
55
+ marketIndex: number;
56
+ fromSubAccountId: number;
57
+ toSubAccountId: number;
58
+ equityFloorDelta: BN;
59
+ };
60
+ /**
61
+ * Splits `totalFloor` across subaccounts proportionally to their equity,
62
+ * clamped so every allocation is backed (`floor_i <= max(0, equity_i -
63
+ * buffer_i)`), with the clamped remainder water-filled into subaccounts that
64
+ * still have capacity. Entries in `pinned` keep their current floor and
65
+ * receive none of the remainder (used for breached subaccounts, which cannot
66
+ * shed floor on-chain). Returns `null` when no backed allocation exists, i.e.
67
+ * the pool's equity cannot cover `totalFloor` plus buffers.
68
+ */
69
+ export declare function allocateEquityFloors(totalFloor: BN, subaccounts: {
70
+ equity: BN;
71
+ buffer: BN;
72
+ currentFloor: BN;
73
+ }[], pinned?: boolean[]): BN[] | null;
74
+ /**
75
+ * Turns a current → target floor split into concrete moves, greedily matching
76
+ * surpluses against deficits. The moves conserve the floor sum by
77
+ * construction and each one only ever sheds floor from a subaccount whose
78
+ * floor is above its target.
79
+ */
80
+ export declare function planFloorMoves(subAccountIds: number[], current: BN[], target: BN[]): FloorMove[];
81
+ export type EquityFloorManagerConfig = {
82
+ /**
83
+ * Subaccount ids to manage. Defaults to every subaccount of the client's
84
+ * authority currently subscribed on the `VelocityClient`.
85
+ */
86
+ subAccountIds?: number[];
87
+ /**
88
+ * Client-side equity haircut (QUOTE_PRECISION) applied when sizing floor
89
+ * deltas, absorbing the dust by which strict on-chain pricing can differ
90
+ * from the client's. Defaults to 1 quote unit ($1).
91
+ */
92
+ collateralHaircut?: BN;
93
+ };
94
+ /**
95
+ * See the module doc. All reads use the subaccounts already subscribed on the
96
+ * wrapped `VelocityClient` (the delegate's client, whose `authority` is the
97
+ * subaccounts' owner); all writes go through `transferDepositByDelegate`.
98
+ */
99
+ export declare class EquityFloorManager {
100
+ private velocityClient;
101
+ private collateralHaircut;
102
+ private subAccountIds?;
103
+ constructor(velocityClient: VelocityClient, config?: EquityFloorManagerConfig);
104
+ private getManagedUsers;
105
+ private getSubaccountStatus;
106
+ /** Full authority-wide standing: aggregates, worst level, per-subaccount detail. */
107
+ getStatus(): EquityFloorStatus;
108
+ /**
109
+ * The most quote that can leave `subAccountId` to the outside (a
110
+ * withdrawal, which cannot move floor): equity above the buffered floor,
111
+ * less the haircut. Floor constraint only — the withdrawal itself is still
112
+ * subject to margin and borrow limits.
113
+ */
114
+ getMaxWithdrawable(subAccountId: number): BN;
115
+ /**
116
+ * The most quote that can move from one subaccount to another when the
117
+ * transfer carries floor with it. Because floor travels with the funds
118
+ * (capped at the floor the debited side holds, after which its check
119
+ * disables entirely), this is normally the debited side's whole equity —
120
+ * bounded by what the credited side's equity can back. Floor constraint
121
+ * only; margin and borrow limits still apply on top.
122
+ */
123
+ getMaxQuoteTransferable(fromSubAccountId: number, toSubAccountId: number): BN;
124
+ /**
125
+ * Resolves a quote transfer into the exact instruction parameters,
126
+ * padding the auto floor delta by the haircut so on-chain strict pricing
127
+ * dust cannot fail it. The padded delta never exceeds the amount or the
128
+ * debited side's floor, so the credited side stays backed whenever it was
129
+ * before.
130
+ */
131
+ planQuoteTransfer(amount: BN, fromSubAccountId: number, toSubAccountId: number): QuoteTransferPlan;
132
+ /** Plans and submits a quote transfer between two subaccounts in one call. */
133
+ transferQuote(amount: BN, fromSubAccountId: number, toSubAccountId: number, txParams?: TxParams): Promise<TransactionSignature>;
134
+ /**
135
+ * Plans the floor-only moves (zero-amount transfers) that re-split the
136
+ * total floor proportionally to where the equity currently sits, so every
137
+ * subaccount ends with the same relative headroom. Breached subaccounts
138
+ * (below their raw floor) cannot shed floor on-chain, so their floor is
139
+ * pinned in place and the rest is allocated around them. Throws when the
140
+ * pool's equity cannot back the total floor plus buffers — at that point
141
+ * no split works and equity must be deposited (or the admin must lower
142
+ * the floor).
143
+ */
144
+ planFloorRebalance(): FloorMove[];
145
+ /**
146
+ * Executes `planFloorRebalance` serially. Safe to run at any time; a
147
+ * no-op when the split already matches the equity distribution.
148
+ */
149
+ rebalanceFloors(txParams?: TxParams): Promise<TransactionSignature[]>;
150
+ }
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EquityFloorManager = exports.planFloorMoves = exports.allocateEquityFloors = void 0;
4
+ const anchor_1 = require("./isomorphic/anchor");
5
+ const numericConstants_1 = require("./constants/numericConstants");
6
+ const numericConstants_2 = require("./constants/numericConstants");
7
+ const margin_1 = require("./math/margin");
8
+ /**
9
+ * Splits `totalFloor` across subaccounts proportionally to their equity,
10
+ * clamped so every allocation is backed (`floor_i <= max(0, equity_i -
11
+ * buffer_i)`), with the clamped remainder water-filled into subaccounts that
12
+ * still have capacity. Entries in `pinned` keep their current floor and
13
+ * receive none of the remainder (used for breached subaccounts, which cannot
14
+ * shed floor on-chain). Returns `null` when no backed allocation exists, i.e.
15
+ * the pool's equity cannot cover `totalFloor` plus buffers.
16
+ */
17
+ function allocateEquityFloors(totalFloor, subaccounts, pinned = []) {
18
+ const n = subaccounts.length;
19
+ const targets = new Array(n).fill(numericConstants_1.ZERO);
20
+ let remaining = totalFloor;
21
+ for (let i = 0; i < n; i++) {
22
+ if (pinned[i]) {
23
+ targets[i] = subaccounts[i].currentFloor;
24
+ remaining = remaining.sub(subaccounts[i].currentFloor);
25
+ }
26
+ }
27
+ if (remaining.isNeg()) {
28
+ // pinned floors alone exceed the total: nothing to allocate elsewhere
29
+ return null;
30
+ }
31
+ const free = [...Array(n).keys()].filter((i) => !pinned[i]);
32
+ const caps = subaccounts.map((s, i) => pinned[i] ? numericConstants_1.ZERO : anchor_1.BN.max(s.equity.sub(s.buffer), numericConstants_1.ZERO));
33
+ const totalCap = free.reduce((sum, i) => sum.add(caps[i]), numericConstants_1.ZERO);
34
+ if (totalCap.lt(remaining)) {
35
+ return null;
36
+ }
37
+ const totalFreeEquity = free.reduce((sum, i) => sum.add(anchor_1.BN.max(subaccounts[i].equity, numericConstants_1.ZERO)), numericConstants_1.ZERO);
38
+ // proportional-to-equity first pass (floor division), clamped to capacity
39
+ let assigned = numericConstants_1.ZERO;
40
+ for (const i of free) {
41
+ const share = totalFreeEquity.gt(numericConstants_1.ZERO)
42
+ ? remaining.mul(anchor_1.BN.max(subaccounts[i].equity, numericConstants_1.ZERO)).div(totalFreeEquity)
43
+ : numericConstants_1.ZERO;
44
+ targets[i] = anchor_1.BN.min(share, caps[i]);
45
+ assigned = assigned.add(targets[i]);
46
+ }
47
+ // water-fill the rounding/clamping remainder into leftover capacity
48
+ let leftover = remaining.sub(assigned);
49
+ for (const i of free) {
50
+ if (leftover.lte(numericConstants_1.ZERO)) {
51
+ break;
52
+ }
53
+ const slack = caps[i].sub(targets[i]);
54
+ const add = anchor_1.BN.min(slack, leftover);
55
+ targets[i] = targets[i].add(add);
56
+ leftover = leftover.sub(add);
57
+ }
58
+ if (leftover.gt(numericConstants_1.ZERO)) {
59
+ return null;
60
+ }
61
+ return targets;
62
+ }
63
+ exports.allocateEquityFloors = allocateEquityFloors;
64
+ /**
65
+ * Turns a current → target floor split into concrete moves, greedily matching
66
+ * surpluses against deficits. The moves conserve the floor sum by
67
+ * construction and each one only ever sheds floor from a subaccount whose
68
+ * floor is above its target.
69
+ */
70
+ function planFloorMoves(subAccountIds, current, target) {
71
+ const surpluses = [];
72
+ const deficits = [];
73
+ for (let i = 0; i < current.length; i++) {
74
+ const diff = current[i].sub(target[i]);
75
+ if (diff.gt(numericConstants_1.ZERO)) {
76
+ surpluses.push({ index: i, amount: diff });
77
+ }
78
+ else if (diff.lt(numericConstants_1.ZERO)) {
79
+ deficits.push({ index: i, amount: diff.neg() });
80
+ }
81
+ }
82
+ const moves = [];
83
+ let s = 0;
84
+ let d = 0;
85
+ while (s < surpluses.length && d < deficits.length) {
86
+ const delta = anchor_1.BN.min(surpluses[s].amount, deficits[d].amount);
87
+ moves.push({
88
+ fromSubAccountId: subAccountIds[surpluses[s].index],
89
+ toSubAccountId: subAccountIds[deficits[d].index],
90
+ equityFloorDelta: delta,
91
+ });
92
+ surpluses[s].amount = surpluses[s].amount.sub(delta);
93
+ deficits[d].amount = deficits[d].amount.sub(delta);
94
+ if (surpluses[s].amount.isZero()) {
95
+ s++;
96
+ }
97
+ if (deficits[d].amount.isZero()) {
98
+ d++;
99
+ }
100
+ }
101
+ return moves;
102
+ }
103
+ exports.planFloorMoves = planFloorMoves;
104
+ const LEVEL_SEVERITY = {
105
+ breached: 4,
106
+ critical: 3,
107
+ warning: 2,
108
+ healthy: 1,
109
+ disabled: 0,
110
+ };
111
+ /**
112
+ * See the module doc. All reads use the subaccounts already subscribed on the
113
+ * wrapped `VelocityClient` (the delegate's client, whose `authority` is the
114
+ * subaccounts' owner); all writes go through `transferDepositByDelegate`.
115
+ */
116
+ class EquityFloorManager {
117
+ constructor(velocityClient, config = {}) {
118
+ var _a;
119
+ this.velocityClient = velocityClient;
120
+ this.subAccountIds = config.subAccountIds;
121
+ this.collateralHaircut = (_a = config.collateralHaircut) !== null && _a !== void 0 ? _a : numericConstants_1.QUOTE_PRECISION;
122
+ }
123
+ getManagedUsers() {
124
+ const authority = this.velocityClient.authority;
125
+ let users = this.velocityClient
126
+ .getUsers()
127
+ .filter((user) => user.getUserAccountOrThrow().authority.equals(authority));
128
+ if (this.subAccountIds !== undefined) {
129
+ const wanted = new Set(this.subAccountIds);
130
+ users = users.filter((user) => wanted.has(user.getUserAccountOrThrow().subAccountId));
131
+ }
132
+ return users.sort((a, b) => a.getUserAccountOrThrow().subAccountId -
133
+ b.getUserAccountOrThrow().subAccountId);
134
+ }
135
+ getSubaccountStatus(user) {
136
+ const userAccount = user.getUserAccountOrThrow();
137
+ const equity = user.getTotalCollateral('Initial', true);
138
+ const bufferedFloor = userAccount.equityFloor.add(userAccount.equityFloorBuffer);
139
+ return {
140
+ subAccountId: userAccount.subAccountId,
141
+ equity,
142
+ equityFloor: userAccount.equityFloor,
143
+ equityFloorBuffer: userAccount.equityFloorBuffer,
144
+ bufferedFloor,
145
+ headroom: equity.sub(userAccount.equityFloor),
146
+ bufferedHeadroom: equity.sub(bufferedFloor),
147
+ level: (0, margin_1.getEquityFloorLevel)(equity, userAccount.equityFloor, userAccount.equityFloorBuffer),
148
+ };
149
+ }
150
+ /** Full authority-wide standing: aggregates, worst level, per-subaccount detail. */
151
+ getStatus() {
152
+ var _a, _b, _c;
153
+ const subaccounts = this.getManagedUsers().map((user) => this.getSubaccountStatus(user));
154
+ const totalEquity = subaccounts.reduce((s, u) => s.add(u.equity), numericConstants_1.ZERO);
155
+ const totalFloor = subaccounts.reduce((s, u) => s.add(u.equityFloor), numericConstants_1.ZERO);
156
+ const totalBuffer = subaccounts.reduce((s, u) => s.add(u.equityFloorBuffer), numericConstants_1.ZERO);
157
+ const level = subaccounts.reduce((worst, u) => LEVEL_SEVERITY[u.level] > LEVEL_SEVERITY[worst] ? u.level : worst, 'disabled');
158
+ return {
159
+ authority: this.velocityClient.authority,
160
+ breakerTripped: ((_c = (_b = (_a = this.velocityClient.getUserStats()) === null || _a === void 0 ? void 0 : _a.getAccount()) === null || _b === void 0 ? void 0 : _b.equityBreakerTripped) !== null && _c !== void 0 ? _c : 0) !== 0,
161
+ totalEquity,
162
+ totalFloor,
163
+ totalBuffer,
164
+ totalBufferedHeadroom: totalEquity.sub(totalFloor).sub(totalBuffer),
165
+ level,
166
+ subaccounts,
167
+ };
168
+ }
169
+ /**
170
+ * The most quote that can leave `subAccountId` to the outside (a
171
+ * withdrawal, which cannot move floor): equity above the buffered floor,
172
+ * less the haircut. Floor constraint only — the withdrawal itself is still
173
+ * subject to margin and borrow limits.
174
+ */
175
+ getMaxWithdrawable(subAccountId) {
176
+ const status = this.getSubaccountStatus(this.velocityClient.getUser(subAccountId, this.velocityClient.authority));
177
+ if (status.equityFloor.lte(numericConstants_1.ZERO)) {
178
+ return status.equity;
179
+ }
180
+ return anchor_1.BN.max(status.bufferedHeadroom.sub(this.collateralHaircut), numericConstants_1.ZERO);
181
+ }
182
+ /**
183
+ * The most quote that can move from one subaccount to another when the
184
+ * transfer carries floor with it. Because floor travels with the funds
185
+ * (capped at the floor the debited side holds, after which its check
186
+ * disables entirely), this is normally the debited side's whole equity —
187
+ * bounded by what the credited side's equity can back. Floor constraint
188
+ * only; margin and borrow limits still apply on top.
189
+ */
190
+ getMaxQuoteTransferable(fromSubAccountId, toSubAccountId) {
191
+ const from = this.getSubaccountStatus(this.velocityClient.getUser(fromSubAccountId, this.velocityClient.authority));
192
+ const to = this.getSubaccountStatus(this.velocityClient.getUser(toSubAccountId, this.velocityClient.authority));
193
+ const haircut = this.collateralHaircut;
194
+ // how much floor the credited side can absorb beyond what the incoming
195
+ // funds themselves back: its own buffered headroom (delta <= amount
196
+ // keeps it backed; beyond that it eats into existing headroom)
197
+ const toSlack = anchor_1.BN.max(to.bufferedHeadroom.sub(haircut), numericConstants_1.ZERO);
198
+ if (from.equityFloor.lte(numericConstants_1.ZERO)) {
199
+ return anchor_1.BN.max(from.equity.sub(haircut), numericConstants_1.ZERO);
200
+ }
201
+ // shedding the entire floor disables the debited side's check; possible
202
+ // only if the credited side can absorb floor faster than the funds back
203
+ // it, i.e. it has slack of its own
204
+ const fullShedViable = from.equity
205
+ .sub(from.bufferedFloor)
206
+ .add(from.equityFloor)
207
+ .add(toSlack);
208
+ // without full shed: amount <= excess + floor (auto delta caps at floor)
209
+ const partialShedMax = anchor_1.BN.max(from.bufferedHeadroom, numericConstants_1.ZERO).add(from.equityFloor);
210
+ const floorwiseMax = anchor_1.BN.min(anchor_1.BN.max(fullShedViable, partialShedMax), from.equity);
211
+ return anchor_1.BN.max(floorwiseMax.sub(haircut), numericConstants_1.ZERO);
212
+ }
213
+ /**
214
+ * Resolves a quote transfer into the exact instruction parameters,
215
+ * padding the auto floor delta by the haircut so on-chain strict pricing
216
+ * dust cannot fail it. The padded delta never exceeds the amount or the
217
+ * debited side's floor, so the credited side stays backed whenever it was
218
+ * before.
219
+ */
220
+ planQuoteTransfer(amount, fromSubAccountId, toSubAccountId) {
221
+ const fromUser = this.velocityClient.getUser(fromSubAccountId, this.velocityClient.authority);
222
+ const fromAccount = fromUser.getUserAccountOrThrow();
223
+ const equityFloorDelta = (0, margin_1.calculateEquityFloorAutoDelta)(amount, fromUser.getTotalCollateral('Initial', true).sub(this.collateralHaircut), fromAccount.equityFloor, fromAccount.equityFloorBuffer);
224
+ return {
225
+ amount,
226
+ marketIndex: numericConstants_2.QUOTE_SPOT_MARKET_INDEX,
227
+ fromSubAccountId,
228
+ toSubAccountId,
229
+ equityFloorDelta: anchor_1.BN.min(equityFloorDelta, amount),
230
+ };
231
+ }
232
+ /** Plans and submits a quote transfer between two subaccounts in one call. */
233
+ async transferQuote(amount, fromSubAccountId, toSubAccountId, txParams) {
234
+ const plan = this.planQuoteTransfer(amount, fromSubAccountId, toSubAccountId);
235
+ return this.velocityClient.transferDepositByDelegate(plan.amount, plan.marketIndex, plan.fromSubAccountId, plan.toSubAccountId, plan.equityFloorDelta, txParams);
236
+ }
237
+ /**
238
+ * Plans the floor-only moves (zero-amount transfers) that re-split the
239
+ * total floor proportionally to where the equity currently sits, so every
240
+ * subaccount ends with the same relative headroom. Breached subaccounts
241
+ * (below their raw floor) cannot shed floor on-chain, so their floor is
242
+ * pinned in place and the rest is allocated around them. Throws when the
243
+ * pool's equity cannot back the total floor plus buffers — at that point
244
+ * no split works and equity must be deposited (or the admin must lower
245
+ * the floor).
246
+ */
247
+ planFloorRebalance() {
248
+ const subaccounts = this.getManagedUsers().map((user) => this.getSubaccountStatus(user));
249
+ if (subaccounts.length < 2) {
250
+ return [];
251
+ }
252
+ const totalFloor = subaccounts.reduce((sum, u) => sum.add(u.equityFloor), numericConstants_1.ZERO);
253
+ if (totalFloor.lte(numericConstants_1.ZERO)) {
254
+ return [];
255
+ }
256
+ const pinned = subaccounts.map((u) => u.level === 'breached');
257
+ const targets = allocateEquityFloors(totalFloor, subaccounts.map((u) => ({
258
+ equity: anchor_1.BN.max(u.equity.sub(this.collateralHaircut), numericConstants_1.ZERO),
259
+ buffer: u.equityFloorBuffer,
260
+ currentFloor: u.equityFloor,
261
+ })), pinned);
262
+ if (targets === null) {
263
+ throw new Error('no backed floor split exists: total equity cannot cover the total floor plus buffers; deposit equity or have the admin lower the floor');
264
+ }
265
+ return planFloorMoves(subaccounts.map((u) => u.subAccountId), subaccounts.map((u) => u.equityFloor), targets);
266
+ }
267
+ /**
268
+ * Executes `planFloorRebalance` serially. Safe to run at any time; a
269
+ * no-op when the split already matches the equity distribution.
270
+ */
271
+ async rebalanceFloors(txParams) {
272
+ const sigs = [];
273
+ for (const move of this.planFloorRebalance()) {
274
+ sigs.push(await this.velocityClient.transferDepositByDelegate(numericConstants_1.ZERO, numericConstants_2.QUOTE_SPOT_MARKET_INDEX, move.fromSubAccountId, move.toSubAccountId, move.equityFloorDelta, txParams));
275
+ }
276
+ return sigs;
277
+ }
278
+ }
279
+ exports.EquityFloorManager = EquityFloorManager;
@@ -12466,6 +12466,10 @@ export type Velocity = {
12466
12466
  {
12467
12467
  "name": "equityFloor";
12468
12468
  "type": "u64";
12469
+ },
12470
+ {
12471
+ "name": "equityFloorBuffer";
12472
+ "type": "u64";
12469
12473
  }
12470
12474
  ];
12471
12475
  },
@@ -12887,6 +12891,63 @@ export type Velocity = {
12887
12891
  ];
12888
12892
  "args": [];
12889
12893
  },
12894
+ {
12895
+ "name": "updateUserVaultOwned";
12896
+ "docs": [
12897
+ "Mark a User as vault-owned (its authority is a vault PDA and its equity",
12898
+ "prices vault depositor shares). Set-only and authority-gated: only the",
12899
+ "User's authority may call it, and it is CPI'd by the vaults program at",
12900
+ "vault init. A vault-owned User is skipped by the revenue-share sweep so a",
12901
+ "builder/referral reward can never enter vault NAV (OtterSec #91/#92/#93)."
12902
+ ];
12903
+ "discriminator": [
12904
+ 50,
12905
+ 156,
12906
+ 218,
12907
+ 143,
12908
+ 216,
12909
+ 94,
12910
+ 68,
12911
+ 93
12912
+ ];
12913
+ "accounts": [
12914
+ {
12915
+ "name": "user";
12916
+ "writable": true;
12917
+ "pda": {
12918
+ "seeds": [
12919
+ {
12920
+ "kind": "const";
12921
+ "value": [
12922
+ 117,
12923
+ 115,
12924
+ 101,
12925
+ 114
12926
+ ];
12927
+ },
12928
+ {
12929
+ "kind": "account";
12930
+ "path": "authority";
12931
+ },
12932
+ {
12933
+ "kind": "arg";
12934
+ "path": "subAccountId";
12935
+ }
12936
+ ];
12937
+ };
12938
+ },
12939
+ {
12940
+ "name": "authority";
12941
+ "signer": true;
12942
+ }
12943
+ ];
12944
+ "args": [
12945
+ {
12946
+ "name": "subAccountId";
12947
+ "type": "u16";
12948
+ }
12949
+ ];
12950
+ },
12890
12951
  {
12891
12952
  "name": "updateWarmAdmin";
12892
12953
  "discriminator": [
@@ -16299,6 +16360,16 @@ export type Velocity = {
16299
16360
  "code": 6364;
16300
16361
  "name": "dailyDepositLimit";
16301
16362
  "msg": "Spot market daily deposit limit hit";
16363
+ },
16364
+ {
16365
+ "code": 6365;
16366
+ "name": "reservedSpotMarketName";
16367
+ "msg": "The name 'USDT' is reserved for the quote spot market (index 0)";
16368
+ },
16369
+ {
16370
+ "code": 6366;
16371
+ "name": "cannotModifyBuilderOrder";
16372
+ "msg": "Cannot modify a builder-coded order; cancel and re-place instead";
16302
16373
  }
16303
16374
  ];
16304
16375
  "types": [
@@ -24065,21 +24136,24 @@ export type Velocity = {
24065
24136
  {
24066
24137
  "name": "equityFloor";
24067
24138
  "docs": [
24068
- "Minimum account equity (cross-margin total collateral) required for",
24069
- "risk-increasing orders, fills, withdrawals and deposit transfers.",
24070
- "Settable only by the warm/cold admin; 0 disables the check.",
24139
+ "Minimum account equity (cross-margin total collateral). Below this the",
24140
+ "permissionless breaker can trip. Risk-increasing orders, fills,",
24141
+ "withdrawals and deposit transfers must clear `equity_floor +",
24142
+ "equity_floor_buffer`. Settable only by the warm/cold admin; 0 disables",
24143
+ "both checks.",
24071
24144
  "precision: QUOTE_PRECISION"
24072
24145
  ];
24073
24146
  "type": "u64";
24074
24147
  },
24075
24148
  {
24076
- "name": "padding2";
24077
- "type": {
24078
- "array": [
24079
- "u8",
24080
- 8
24081
- ];
24082
- };
24149
+ "name": "equityFloorBuffer";
24150
+ "docs": [
24151
+ "Extra headroom above `equity_floor` required by risk-increasing",
24152
+ "actions, so an account cannot legally end an action at the trip",
24153
+ "threshold. No effect while `equity_floor` is 0.",
24154
+ "precision: QUOTE_PRECISION"
24155
+ ];
24156
+ "type": "u64";
24083
24157
  }
24084
24158
  ];
24085
24159
  };