@aegis-engine/core 0.4.0 → 0.5.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.
@@ -2,6 +2,7 @@ import type { Address, Hex, PublicClient, TransactionReceipt } from 'viem';
2
2
  import type { AegisSwapChainConfig, AegisSwapToken } from '../config/index.js';
3
3
  import type { SignedPermitBatch } from '../encoding.js';
4
4
  import type { WalletSession } from '../swap/types.js';
5
+ import type { AegisEngineTapInfeasibleReason, AegisEngineTapRootPolicy } from './trade/sizing.js';
5
6
  export type AegisEngineMode = 'lend' | 'borrow' | 'positions';
6
7
  export type AegisEngineMarketSource = 'rest' | 'static';
7
8
  export type AegisEngineMarketStatus = 'enabled' | 'disabled' | 'hidden' | 'malformed';
@@ -254,12 +255,21 @@ export type AegisEngineVaultLedgerAction = (AegisEngineActivityActionBase & {
254
255
  initialLtvPips: number | null;
255
256
  });
256
257
  export type AegisEngineLendLedgerAction = (AegisEngineActivityActionBase & {
257
- kind: 'deposit' | 'withdraw';
258
+ kind: 'deposit';
258
259
  deltaSharesRaw: string;
259
260
  deltaAmount0Raw: string;
260
261
  deltaAmount1Raw: string;
261
262
  }) | (AegisEngineActivityActionBase & {
262
- kind: 'transfer_in' | 'transfer_out';
263
+ kind: 'withdraw';
264
+ deltaSharesRaw: string;
265
+ deltaAmount0Raw: string;
266
+ deltaAmount1Raw: string;
267
+ }) | (AegisEngineActivityActionBase & {
268
+ kind: 'transfer_in';
269
+ sharesRaw: string;
270
+ counterparty: Address;
271
+ }) | (AegisEngineActivityActionBase & {
272
+ kind: 'transfer_out';
263
273
  sharesRaw: string;
264
274
  counterparty: Address;
265
275
  });
@@ -275,6 +285,20 @@ export type AegisEngineActivityMarketState = {
275
285
  shareTotalSupply: string | null;
276
286
  warnings: string[];
277
287
  };
288
+ /** Indexed lot facts derived from the activity ledger + market borrow index.
289
+ * Raw rL / wad strings only — this plane does not synthesize USD. */
290
+ export type AegisEngineActivityLotDisplay = {
291
+ deposited: {
292
+ raw: string;
293
+ side: 0 | 1;
294
+ } | null;
295
+ principalRaw: string | null;
296
+ openBorrowIndexWad: string | null;
297
+ indexedBorrowIndexWad: string | null;
298
+ debtAtOpenRl: string | null;
299
+ debtIndexedRl: string | null;
300
+ interestAccruedRl: string | null;
301
+ };
278
302
  export type AegisEnginePositionVaultActivity = {
279
303
  vaultId: string;
280
304
  poolId: Hex;
@@ -284,6 +308,8 @@ export type AegisEnginePositionVaultActivity = {
284
308
  burnedAt: string | null;
285
309
  /** Oldest-first on-chain execution order. */
286
310
  actions: AegisEngineVaultLedgerAction[];
311
+ /** Present on REST-parsed snapshots. Hand-built fixtures may omit it. */
312
+ display?: AegisEngineActivityLotDisplay;
287
313
  };
288
314
  export type AegisEnginePositionLendActivity = {
289
315
  poolId: Hex;
@@ -294,7 +320,8 @@ export type AegisEnginePositionLendActivity = {
294
320
  };
295
321
  /** Wallet-scoped action ledger returned by GET /v1/engine/positions/activity.
296
322
  * Raw amount strings preserve the backend's exact signed bigint wire values;
297
- * this endpoint does not synthesize USD values. */
323
+ * this endpoint does not synthesize USD values. Core attaches indexed lot
324
+ * `display` fields after parse from the ledger already in the payload. */
298
325
  export type AegisEnginePositionActivitySnapshot = {
299
326
  schemaVersion: '1';
300
327
  chainId: number;
@@ -311,11 +338,51 @@ export type AegisEnginePositionActivitySnapshot = {
311
338
  lendActionCount: number;
312
339
  };
313
340
  freshness: {
314
- source: 'subgraph';
341
+ source: 'subgraph' | 'static';
315
342
  /** Oldest indexed block across all pages; null when any page omitted it. */
316
343
  blockNumber: number | null;
317
344
  };
318
345
  };
346
+ /** Off-chain opening intent for one confirmed Long/Short transaction. The
347
+ * Engine transaction does not encode its target scenario or average leverage,
348
+ * so consumers may persist this small record and join it back to the indexed
349
+ * opening transaction. Indexed actions remain authoritative for direction and
350
+ * entry price. */
351
+ export type AegisEngineTradePositionIntent = {
352
+ schemaVersion: '1';
353
+ chainId: number;
354
+ account: Address;
355
+ marketId: string;
356
+ poolId: Hex;
357
+ txHash: Hex;
358
+ direction: AegisEngineTradeDirection;
359
+ /** Average leverage over the reviewed move to `targetPrice`. */
360
+ averageLeverage: number;
361
+ /** Reviewed target scenario in token1 per token0. Not an on-chain order. */
362
+ targetPrice: number;
363
+ horizonSeconds: number;
364
+ recordedAt: string;
365
+ };
366
+ /** Durable Long/Short identity reconstructed from one vault's indexed opening
367
+ * transaction. Optional scenario fields are present only when a matching
368
+ * off-chain intent record survived; they never override indexed direction or
369
+ * entry price. */
370
+ export type AegisEngineTradePositionStrategy = {
371
+ kind: 'trade';
372
+ vaultId: string;
373
+ poolId: Hex;
374
+ direction: AegisEngineTradeDirection;
375
+ openTxHash: Hex;
376
+ openedAt: number;
377
+ /** Executed opening spot, token1 per token0, derived from DebtModified. */
378
+ entryPrice: number | null;
379
+ /** Reviewed average leverage to the target scenario, not current leverage. */
380
+ averageLeverage: number | null;
381
+ /** Reviewed scenario only. Null when no matching intent record is available. */
382
+ targetPrice: number | null;
383
+ horizonSeconds: number | null;
384
+ source: 'indexed-actions' | 'indexed-actions+stored-intent';
385
+ };
319
386
  export type AegisEngineBorrowPositionHydration = {
320
387
  positionId: string;
321
388
  vaultId: bigint;
@@ -391,6 +458,9 @@ export type AegisEngineStaticSource = {
391
458
  markets: AegisEngineMarket[];
392
459
  positions?: AegisEnginePosition[];
393
460
  lendPositions?: AegisEngineLendSharesPosition[];
461
+ /** Vault action ledgers for the static positions. Lets offline demos and
462
+ * tests reconstruct Long/Short identity exactly like the REST ledger does. */
463
+ vaultActivity?: AegisEnginePositionVaultActivity[];
394
464
  };
395
465
  export type AegisEngineDataSource = AegisEngineRestSource | AegisEngineStaticSource;
396
466
  export type AegisEngineMarketReadInput = {
@@ -424,8 +494,28 @@ export type AegisEngineMarketSnapshot = {
424
494
  hardLtvPips: number;
425
495
  };
426
496
  };
497
+ /**
498
+ * Live pool mid for one market in token1-per-token0 display units — the same
499
+ * price plane trade quotes size against (`Ppool` from `sqrtPriceX96`), read
500
+ * on-chain. Indexed token `priceUsd` metadata is a different, slower plane;
501
+ * never mix the two in one comparison.
502
+ */
503
+ export type AegisEngineMarketMark = {
504
+ poolId: Hex;
505
+ chainId: number;
506
+ /** token1 per token0, display units. */
507
+ price: number;
508
+ sqrtPriceX96: bigint;
509
+ /** Local wall-clock read time, in Unix milliseconds. */
510
+ observedAt: number;
511
+ };
427
512
  export type AegisEngineReadFrame = {
428
513
  blockNumber: bigint;
514
+ /**
515
+ * Canonical identity of `blockNumber`. SDK-backed reads always provide it;
516
+ * optional only while legacy hosts migrate hand-built quote contexts.
517
+ */
518
+ blockHash?: Hex;
429
519
  blockTimestamp: bigint;
430
520
  /** Local wall-clock time when the frame was assembled, in Unix milliseconds. */
431
521
  observedAt: number;
@@ -681,6 +771,143 @@ export type AegisEngineBorrowQuote = {
681
771
  costProjection: AegisEngineBorrowCostProjection;
682
772
  tx: AegisEnginePreparedTransaction | null;
683
773
  };
774
+ export type AegisEngineTradeDirection = 'long' | 'short';
775
+ export type AegisEngineTradeOpenQuoteInput = {
776
+ publicClient: PublicClient;
777
+ market: AegisEngineMarket;
778
+ owner: Address;
779
+ direction: AegisEngineTradeDirection;
780
+ /** Collateral in base units of the token named by `inputIsToken0`. */
781
+ collateralIn: bigint;
782
+ inputIsToken0: boolean;
783
+ /** Horizon the opening LTV is derived from, in seconds. */
784
+ horizonSeconds: number;
785
+ /**
786
+ * Magnitude of the move to the target price, relative to the pool price.
787
+ * `direction` sets the sign; a zero move has no target and is rejected.
788
+ */
789
+ priceMove: number;
790
+ /**
791
+ * Absolute LTV ceiling, independent of the horizon. Defaults to the live
792
+ * market open cap, where only the horizon and the solver's own margin bind.
793
+ */
794
+ tauPips?: number;
795
+ /** Average leverage to open at. Omitted opens the band's maximum. */
796
+ leverage?: number;
797
+ /** @deprecated Ignored. Spec-0168 always selects the lower-debt frontier. */
798
+ rootPolicy?: AegisEngineTapRootPolicy;
799
+ slippageBps: number;
800
+ deadline?: bigint;
801
+ /** Abort a superseded indicative quote between its block-pinned phases. */
802
+ signal?: AbortSignal;
803
+ /** Submit paths set this after changing router authorization. */
804
+ forceExecutionEvidenceRefresh?: boolean;
805
+ };
806
+ export type AegisEngineTradeOpenInfeasibleReason = AegisEngineTapInfeasibleReason['kind']
807
+ /** The chosen root's swap trades against the requested direction at entry. */
808
+ | 'swap-direction-mismatch'
809
+ /** Post-borrow idle cannot fund the swap the chosen root needs. */
810
+ | 'insufficient-idle'
811
+ /** Principal below the engine's `lockVault` minimum; it would revert. */
812
+ | 'debt-below-minimum' | 'delta-above-cap'
813
+ /** LTV recomputed from the encoded amounts is at or above the open cap. */
814
+ | 'ltv-above-max'
815
+ /** The swap was quoted at a different block than the sizing state. */
816
+ | 'stale-frame';
817
+ /** Actionable bounds for a rejected quote. Each field is null when the
818
+ * rejection did not produce one. */
819
+ export type AegisEngineTradeOpenLimits = {
820
+ /** Highest average leverage on the openable debt-minimizing frontier. */
821
+ maxLeverage: number | null;
822
+ /** Lowest average leverage on that frontier. */
823
+ minLeverage: number | null;
824
+ /** Largest collateral the utilization cap still finances, in base units. */
825
+ maxCollateralIn: bigint | null;
826
+ };
827
+ export type AegisEngineTradeOpenPosition = {
828
+ /** Exposure ratio the α band settled on. */
829
+ alpha: number;
830
+ /** Average leverage over the move to the target price, evaluated from the
831
+ * exact balances the router locks. Always positive. */
832
+ leverage: number;
833
+ /** Instantaneous leverage at entry from the exact locked balances. */
834
+ localLeverage: number;
835
+ /** Leverage as price runs away from entry, from the exact locked balances. */
836
+ asymptoticLeverage: number;
837
+ /** Opening LTV from the sized band. */
838
+ ltvPips: number;
839
+ /** LTV recomputed from the exact encoded amounts — what `lockVault` checks. */
840
+ executionLtvPips: number;
841
+ /**
842
+ * Days until interest accrual drifts LTV into the liquidation threshold.
843
+ * Realized at the position's LTV, never the caller's `horizonSeconds`: the
844
+ * solver holds a margin below the cap that stretches short horizons.
845
+ */
846
+ daysToLiquidation: AegisEngineDaysToLiquidation;
847
+ /** Simple interest on the exact execution debt over the horizon at the rate
848
+ * used to size the quote, in token1 base units. */
849
+ interestCostToken1: bigint;
850
+ /**
851
+ * `min E(P) / E_0` over ALL prices from the exact locked balances. Equity is
852
+ * convex in `√P` with a strictly negative discriminant, so this floor is never
853
+ * zero and there is no liquidation price — only this ratio and
854
+ * {@link daysToLiquidation}.
855
+ */
856
+ worstEquityRatio: number;
857
+ entryPrice: number;
858
+ targetPrice: number;
859
+ /** Gross return from exact entry equity to target equity, before horizon
860
+ * interest. The encoded swap output floor is already reflected in both. */
861
+ grossReturnAtTargetPct: number;
862
+ /** Borrow principal in rL units. */
863
+ principalDeltaRl: bigint;
864
+ /** Vault idle at lock, in base units — the collateral `lockVault` values. */
865
+ collateral0: bigint;
866
+ collateral1: bigint;
867
+ };
868
+ export type AegisEngineTradeOpenSwap = {
869
+ inputIsToken0: boolean;
870
+ amountIn: bigint;
871
+ amountOutQuoted: bigint;
872
+ /** Encoded on-chain floor; also the exact amount redeposited to the vault. */
873
+ amountOutMinimum: bigint;
874
+ /** Execution price against the pool mid, in basis points. Positive when the
875
+ * pool fills worse than mid. */
876
+ priceImpactBps: number;
877
+ /** True when vault idle, not the solver, set `amountIn`. */
878
+ clampedByIdle: boolean;
879
+ };
880
+ /**
881
+ * One block-pinned tap open: create a vault, deposit, borrow, swap the borrow
882
+ * into the target exposure, and lock — as a single router call.
883
+ */
884
+ export type AegisEngineTradeOpenQuote = {
885
+ kind: 'trade-open';
886
+ market: AegisEngineMarket;
887
+ direction: AegisEngineTradeDirection;
888
+ collateralToken: AegisEngineToken;
889
+ /** Thresholds read in the same live frame used to size the position. */
890
+ thresholds: {
891
+ maxLtvPips: number;
892
+ hardLtvPips: number;
893
+ };
894
+ feasible: boolean;
895
+ executable: boolean;
896
+ infeasibleReason: AegisEngineTradeOpenInfeasibleReason | null;
897
+ limits: AegisEngineTradeOpenLimits;
898
+ gateFailures: readonly AegisEngineLifecycleGateFailure[];
899
+ approvals: readonly AegisEngineLifecycleApprovalPlan[];
900
+ position: AegisEngineTradeOpenPosition | null;
901
+ /** Null when the borrow already delivers the target mix. */
902
+ swap: AegisEngineTradeOpenSwap | null;
903
+ deposit: {
904
+ amount0: bigint;
905
+ amount1: bigint;
906
+ };
907
+ frame: AegisEngineReadFrame;
908
+ expiresAt: bigint;
909
+ tx: AegisEnginePreparedTransaction | null;
910
+ };
684
911
  export type AegisEnginePositionActionKind = 'add-collateral' | 'repay' | 'close' | 'withdraw' | 'redeem';
685
912
  export type AegisEngineBorrowPositionTarget = {
686
913
  kind: 'borrow_vault';
@@ -965,6 +1192,70 @@ export type AegisEngineTransactionResult = {
965
1192
  feeWei: bigint;
966
1193
  };
967
1194
  };
1195
+ /** Wallet actions that can precede a fresh Engine Long/Short position. */
1196
+ export type AegisEngineAuthorizationStepKind = 'vault-operator' | 'erc20-reset' | 'erc20-permit2' | 'permit2-router' | 'open-position';
1197
+ export type AegisEngineAuthorizationStepStatus = 'satisfied' | 'required';
1198
+ export type AegisEngineAuthorizationStep = {
1199
+ /** Stable semantic id. A plan contains at most one step of each kind. */
1200
+ id: AegisEngineAuthorizationStepKind;
1201
+ kind: AegisEngineAuthorizationStepKind;
1202
+ status: AegisEngineAuthorizationStepStatus;
1203
+ /** Every current Long/Short authorization is an on-chain wallet transaction. */
1204
+ walletAction: 'transaction';
1205
+ /** Contract the wallet action targets. */
1206
+ target: Address;
1207
+ /** Contract receiving authority, when distinct from the target. */
1208
+ spender: Address | null;
1209
+ /** Token governed by this step. Null for vault and final-position actions. */
1210
+ token: AegisEngineToken | null;
1211
+ /** Current on-chain allowance, when the step governs a token amount. */
1212
+ currentAmount: bigint | null;
1213
+ /** Amount a required wallet action will set. Null for satisfied or non-token actions. */
1214
+ requestedAmount: bigint | null;
1215
+ /** Permit2 expiry in Unix seconds. Null when the authority has no expiry field. */
1216
+ expiration: number | null;
1217
+ /** Whether authority remains after the requested position is opened. */
1218
+ persistent: boolean;
1219
+ scope: 'all-vaults' | 'token-allowance-reset' | 'exact-token-amount' | 'existing-token-amount' | 'maximum-token-amount' | 'position-open';
1220
+ };
1221
+ export type AegisEngineAuthorizationPlan = {
1222
+ kind: 'engine-authorization-plan';
1223
+ /** Fresh Long/Short only. Do not widen in place for borrow or lend. */
1224
+ operation: 'trade-open';
1225
+ account: Address;
1226
+ chainId: number;
1227
+ marketId: string;
1228
+ token: AegisEngineToken;
1229
+ amount: bigint;
1230
+ approvalMode: 'exact' | 'max';
1231
+ router: Address;
1232
+ permit2: Address;
1233
+ vaultRegistry: Address;
1234
+ steps: readonly AegisEngineAuthorizationStep[];
1235
+ /** Missing permission grants, excluding a zero-first reset and the final open. */
1236
+ missingPermissionCount: number;
1237
+ /** True when the known wallet chain adds a network-switch request; null when unknown. */
1238
+ walletChainSwitchRequired: boolean | null;
1239
+ /** Sequential transaction request count. A network switch is reported separately. */
1240
+ walletPromptCount: number;
1241
+ /** True when only the final position transaction remains. */
1242
+ readyForOpen: boolean;
1243
+ };
1244
+ /** Fresh Long/Short only. No `vaultId`: this plan reads blanket operator approval. */
1245
+ export type AegisEngineAuthorizationPlanInput = {
1246
+ publicClient: PublicClient;
1247
+ market: AegisEngineMarket;
1248
+ owner: Address;
1249
+ /** Current wallet chain. Omit or pass null when it has not resolved yet. */
1250
+ walletChainId?: number | null;
1251
+ token: AegisEngineToken;
1252
+ amount: bigint;
1253
+ approvalMode?: 'exact' | 'max';
1254
+ };
1255
+ export type AegisEngineAuthorizationProgress = {
1256
+ kind: AegisEngineAuthorizationStepKind;
1257
+ status: 'requesting' | 'confirmed';
1258
+ };
968
1259
  export type AegisEngineApprovalInput = {
969
1260
  wallet: AegisEngineWalletSession;
970
1261
  publicClient: PublicClient;
@@ -975,13 +1266,24 @@ export type AegisEngineApprovalInput = {
975
1266
  amount: bigint;
976
1267
  approvalMode?: 'exact' | 'max';
977
1268
  permit2AuthorizationMode?: 'signature' | 'spending-cap';
1269
+ /** Stops the sequence between wallet requests when its reviewed context changes. */
1270
+ signal?: AbortSignal;
978
1271
  onStatus?: AegisEngineActionCallbacks['onStatus'];
1272
+ /** Fine-grained prerequisite progress for truthful wallet onboarding. */
1273
+ onAuthorizationProgress?: (progress: AegisEngineAuthorizationProgress) => void;
1274
+ /** Awaited after each prerequisite receipt and before another wallet request. */
1275
+ onAuthorizationCheckpoint?: () => Promise<void>;
979
1276
  };
980
1277
  export type AegisEngineSendInput = {
981
1278
  wallet: AegisEngineWalletSession;
982
1279
  publicClient: PublicClient;
983
1280
  activeChain: AegisSwapChainConfig;
984
1281
  tx: AegisEnginePreparedTransaction;
1282
+ /**
1283
+ * Cancels preparation before broadcast when the reviewed UI context changes.
1284
+ * Once a wallet returns a transaction hash, receipt tracking always finishes.
1285
+ */
1286
+ signal?: AbortSignal;
985
1287
  /**
986
1288
  * Absolute Unix timestamp encoded into the prepared Engine transaction.
987
1289
  * When present, the send boundary refuses to open the wallet if too little
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,19 @@
1
1
  import type { Hex } from 'viem';
2
+ /** Failure families the widget surfaces reason about (retry affordances,
3
+ toast dwell, explorer-link degradation) without string comparison. */
4
+ export type UserFacingErrorKind = 'rpc' | 'quote' | 'backend' | 'wallet-rejected' | 'wrong-chain' | 'tx-reverted' | 'tx-timeout' | 'market-not-ready' | 'validation' | 'unknown';
5
+ export type ClassifiedError = {
6
+ message: string;
7
+ kind: UserFacingErrorKind;
8
+ retryable: boolean;
9
+ };
2
10
  export declare class UserFacingError extends Error {
3
- constructor(message: string);
11
+ readonly kind: UserFacingErrorKind;
12
+ readonly retryable: boolean;
13
+ constructor(message: string, options?: {
14
+ kind?: UserFacingErrorKind;
15
+ retryable?: boolean;
16
+ });
4
17
  }
5
18
  export declare class TransactionReceiptTimeoutError extends UserFacingError {
6
19
  readonly hash: Hex;
@@ -10,4 +23,14 @@ export declare class TransactionReceiptTimeoutError extends UserFacingError {
10
23
  }
11
24
  export declare function isUserRejectedError(error: unknown): boolean;
12
25
  export declare const USER_REJECTED_MESSAGE = "Request rejected.";
26
+ /**
27
+ * Map raw wallet/RPC errors to short, user-facing messages plus their failure
28
+ * family and retryability. UserFacingError subclasses win first (their copy
29
+ * and kind are authored at the throw site); everything else must earn its way
30
+ * through the legacy substring whitelist. Anything unrecognized (including
31
+ * quote aborts) collapses to the caller-supplied fallback as retryable
32
+ * 'unknown'.
33
+ */
34
+ export declare function classifyError(error: unknown, fallbackMessage: string): ClassifiedError;
35
+ /** Message-only compat wrapper over {@link classifyError}. */
13
36
  export declare function errorMessage(error: unknown, fallbackMessage: string): string;