@metamask-previews/perps-controller 18.0.1-preview-c92646a67 → 19.0.0-preview-cdde826

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 (100) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/README.md +69 -0
  3. package/dist/PerpsController-method-action-types.d.ts +65 -1
  4. package/dist/PerpsController-method-action-types.d.ts.map +1 -1
  5. package/dist/PerpsController-method-action-types.js.map +1 -1
  6. package/dist/PerpsController.d.ts +56 -1
  7. package/dist/PerpsController.d.ts.map +1 -1
  8. package/dist/PerpsController.js +181 -2
  9. package/dist/PerpsController.js.map +1 -1
  10. package/dist/constants/hyperLiquidConfig.d.ts +10 -0
  11. package/dist/constants/hyperLiquidConfig.d.ts.map +1 -1
  12. package/dist/constants/hyperLiquidConfig.js +10 -0
  13. package/dist/constants/hyperLiquidConfig.js.map +1 -1
  14. package/dist/constants/orderTypes.d.ts +3 -0
  15. package/dist/constants/orderTypes.d.ts.map +1 -1
  16. package/dist/constants/orderTypes.js +3 -0
  17. package/dist/constants/orderTypes.js.map +1 -1
  18. package/dist/index.d.ts +4 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/perpsErrorCodes.d.ts.map +1 -1
  23. package/dist/perpsErrorCodes.js +3 -0
  24. package/dist/perpsErrorCodes.js.map +1 -1
  25. package/dist/providers/AggregatedPerpsProvider.d.ts +9 -0
  26. package/dist/providers/AggregatedPerpsProvider.d.ts.map +1 -1
  27. package/dist/providers/AggregatedPerpsProvider.js +46 -0
  28. package/dist/providers/AggregatedPerpsProvider.js.map +1 -1
  29. package/dist/providers/HyperLiquidProvider.d.ts +18 -1
  30. package/dist/providers/HyperLiquidProvider.d.ts.map +1 -1
  31. package/dist/providers/HyperLiquidProvider.js +747 -186
  32. package/dist/providers/HyperLiquidProvider.js.map +1 -1
  33. package/dist/providers/LighterProvider.d.ts +10 -0
  34. package/dist/providers/LighterProvider.d.ts.map +1 -1
  35. package/dist/providers/LighterProvider.js +93 -10
  36. package/dist/providers/LighterProvider.js.map +1 -1
  37. package/dist/services/HyperLiquidClientService.d.ts +4 -17
  38. package/dist/services/HyperLiquidClientService.d.ts.map +1 -1
  39. package/dist/services/HyperLiquidClientService.js.map +1 -1
  40. package/dist/services/HyperLiquidWalletService.d.ts +27 -29
  41. package/dist/services/HyperLiquidWalletService.d.ts.map +1 -1
  42. package/dist/services/HyperLiquidWalletService.js +101 -40
  43. package/dist/services/HyperLiquidWalletService.js.map +1 -1
  44. package/dist/services/LighterClientService.js +2 -2
  45. package/dist/services/LighterClientService.js.map +1 -1
  46. package/dist/services/LighterWalletService.d.ts +13 -8
  47. package/dist/services/LighterWalletService.d.ts.map +1 -1
  48. package/dist/services/LighterWalletService.js +41 -22
  49. package/dist/services/LighterWalletService.js.map +1 -1
  50. package/dist/services/MarketDataService.d.ts.map +1 -1
  51. package/dist/services/MarketDataService.js +14 -3
  52. package/dist/services/MarketDataService.js.map +1 -1
  53. package/dist/services/RewardsIntegrationService.d.ts +5 -4
  54. package/dist/services/RewardsIntegrationService.d.ts.map +1 -1
  55. package/dist/services/RewardsIntegrationService.js +35 -20
  56. package/dist/services/RewardsIntegrationService.js.map +1 -1
  57. package/dist/services/TradingReadinessCache.js.map +1 -1
  58. package/dist/services/TradingService.d.ts.map +1 -1
  59. package/dist/services/TradingService.js +37 -21
  60. package/dist/services/TradingService.js.map +1 -1
  61. package/dist/services/accountSigner.d.ts +18 -0
  62. package/dist/services/accountSigner.d.ts.map +1 -0
  63. package/dist/services/accountSigner.js +23 -0
  64. package/dist/services/accountSigner.js.map +1 -0
  65. package/dist/services/agentSigner.d.ts +61 -0
  66. package/dist/services/agentSigner.d.ts.map +1 -0
  67. package/dist/services/agentSigner.js +87 -0
  68. package/dist/services/agentSigner.js.map +1 -0
  69. package/dist/services/causeChain.d.ts +10 -0
  70. package/dist/services/causeChain.d.ts.map +1 -0
  71. package/dist/services/causeChain.js +21 -0
  72. package/dist/services/causeChain.js.map +1 -0
  73. package/dist/services/providerNetwork.d.ts +13 -0
  74. package/dist/services/providerNetwork.d.ts.map +1 -0
  75. package/dist/services/providerNetwork.js +17 -0
  76. package/dist/services/providerNetwork.js.map +1 -0
  77. package/dist/types/hyperliquid-types.d.ts +7 -7
  78. package/dist/types/hyperliquid-types.d.ts.map +1 -1
  79. package/dist/types/hyperliquid-types.js +4 -5
  80. package/dist/types/hyperliquid-types.js.map +1 -1
  81. package/dist/types/index.d.ts +190 -10
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/dist/types/index.js.map +1 -1
  84. package/dist/types/lighter-types.d.ts +7 -17
  85. package/dist/types/lighter-types.d.ts.map +1 -1
  86. package/dist/types/lighter-types.js.map +1 -1
  87. package/dist/utils/accountUtils.js +1 -1
  88. package/dist/utils/accountUtils.js.map +1 -1
  89. package/dist/utils/errorUtils.d.ts +1 -1
  90. package/dist/utils/errorUtils.d.ts.map +1 -1
  91. package/dist/utils/errorUtils.js +3 -11
  92. package/dist/utils/errorUtils.js.map +1 -1
  93. package/dist/utils/hyperLiquidAbstraction.d.ts.map +1 -1
  94. package/dist/utils/hyperLiquidAbstraction.js +0 -1
  95. package/dist/utils/hyperLiquidAbstraction.js.map +1 -1
  96. package/dist/utils/lighterAdapter.d.ts +2 -2
  97. package/dist/utils/lighterAdapter.d.ts.map +1 -1
  98. package/dist/utils/lighterAdapter.js +23 -17
  99. package/dist/utils/lighterAdapter.js.map +1 -1
  100. package/package.json +6 -7
@@ -6,16 +6,19 @@ import { VISIBLE_CANDLE_COUNT_CONFIG, } from './constants/chartConfig.js';
6
6
  import { PERPS_EVENT_PROPERTY, PERPS_EVENT_VALUE, } from './constants/eventNames.js';
7
7
  import { canonicalizeHyperLiquidDexes, MAINNET_HIP3_CONFIG, TESTNET_HIP3_CONFIG, USDC_SYMBOL, } from './constants/hyperLiquidConfig.js';
8
8
  import { PerpsMeasurementName } from './constants/performanceMetrics.js';
9
- import { PERPS_CONSTANTS, MARKET_SORTING_CONFIG, PROVIDER_CONFIG, buildProviderCacheKey, MAX_SLIPPAGE_BOUNDS, DEFAULT_PERPS_MODE, DEFAULT_ORDER_BOOK_PREFERENCES, DEFAULT_PRO_LAYOUT_PREFERENCES, DEFAULT_SELECTED_ORDER_TYPE, } from './constants/perpsConfig.js';
9
+ import { PERPS_CONSTANTS, MARKET_SORTING_CONFIG, buildProviderCacheKey, MAX_SLIPPAGE_BOUNDS, DEFAULT_PERPS_MODE, DEFAULT_ORDER_BOOK_PREFERENCES, DEFAULT_PRO_LAYOUT_PREFERENCES, DEFAULT_SELECTED_ORDER_TYPE, } from './constants/perpsConfig.js';
10
10
  import { PERPS_ERROR_CODES } from './perpsErrorCodes.js';
11
11
  import { AggregatedPerpsProvider } from './providers/AggregatedPerpsProvider.js';
12
12
  import { HyperLiquidProvider } from './providers/HyperLiquidProvider.js';
13
13
  import { AccountService } from './services/AccountService.js';
14
+ import { isMainAccountSignerReady } from './services/accountSigner.js';
15
+ import { AgentBindings } from './services/agentSigner.js';
14
16
  import { DataLakeService } from './services/DataLakeService.js';
15
17
  import { DepositService } from './services/DepositService.js';
16
18
  import { EligibilityService } from './services/EligibilityService.js';
17
19
  import { FeatureFlagConfigurationService } from './services/FeatureFlagConfigurationService.js';
18
20
  import { MarketDataService } from './services/MarketDataService.js';
21
+ import { isProviderOnTestnet } from './services/providerNetwork.js';
19
22
  import { RewardsIntegrationService } from './services/RewardsIntegrationService.js';
20
23
  import { TerminalMarketService } from './services/TerminalMarketService.js';
21
24
  import { TradingService } from './services/TradingService.js';
@@ -389,6 +392,7 @@ const MESSENGER_EXPOSED_METHODS = [
389
392
  'calculateMaintenanceMargin',
390
393
  'cancelOrder',
391
394
  'cancelOrders',
395
+ 'clearAgentSigners',
392
396
  'clearAttributionContext',
393
397
  'clearDepositResult',
394
398
  'clearPendingTradeConfiguration',
@@ -452,6 +456,7 @@ const MESSENGER_EXPOSED_METHODS = [
452
456
  'markFirstOrderCompleted',
453
457
  'markTutorialCompleted',
454
458
  'placeOrder',
459
+ 'prepareTradingWallet',
455
460
  'previewPositionModify',
456
461
  'reconnect',
457
462
  'recordMarketViewed',
@@ -470,6 +475,7 @@ const MESSENGER_EXPOSED_METHODS = [
470
475
  'saveOrderBookGrouping',
471
476
  'savePendingTradeConfiguration',
472
477
  'saveTradeConfiguration',
478
+ 'setAgentSigner',
473
479
  'setAttributionContext',
474
480
  'setLiveDataConfig',
475
481
  'setSelectedPaymentToken',
@@ -513,6 +519,8 @@ export class PerpsController extends BaseController {
513
519
  providers;
514
520
  isInitialized = false;
515
521
  #initializationPromise = null;
522
+ // Actions that saw a disconnect wait here for the client's follow-up init().
523
+ #initializationStartWaiters = new Set();
516
524
  #isReinitializing = false;
517
525
  #reinitializationOperationPromise = null;
518
526
  #disconnectOperationPromise = null;
@@ -588,6 +596,9 @@ export class PerpsController extends BaseController {
588
596
  */
589
597
  #standaloneProvider = null;
590
598
  #handlersRegistered = false;
599
+ // HyperLiquid agent bindings made through setAgentSigner, kept across
600
+ // provider instances; they answer before the host's getAgentSigner.
601
+ #agentBindings;
591
602
  #standaloneProviderIsTestnet = null;
592
603
  #standaloneProviderHip3Version = null;
593
604
  #standaloneProviderOperations = new Map();
@@ -636,6 +647,7 @@ export class PerpsController extends BaseController {
636
647
  clientConfig,
637
648
  infrastructure,
638
649
  };
650
+ this.#agentBindings = new AgentBindings(clientConfig?.providerCredentials?.hyperliquid?.getAgentSigner);
639
651
  // Instantiate services with platform dependencies
640
652
  // Services that need cross-controller access receive the messenger
641
653
  this.#tradingService = new TradingService(infrastructure);
@@ -1392,8 +1404,30 @@ export class PerpsController extends BaseController {
1392
1404
  return this.#initializationPromise;
1393
1405
  }
1394
1406
  this.#initializationPromise = this.#performInitialization();
1407
+ this.#initializationStartWaiters.forEach((notifyStarted) => notifyStarted());
1395
1408
  return this.#initializationPromise;
1396
1409
  }
1410
+ /**
1411
+ * Resolve once a new initialization starts, or after the timeout.
1412
+ *
1413
+ * @param timeoutMs - Longest time to wait for init() to be called.
1414
+ * @returns A promise that resolves when init starts or the timeout elapses.
1415
+ */
1416
+ async #waitForInitializationStart(timeoutMs) {
1417
+ let notifyStarted = () => undefined;
1418
+ const started = new Promise((resolve) => {
1419
+ notifyStarted = resolve;
1420
+ });
1421
+ this.#initializationStartWaiters.add(notifyStarted);
1422
+ const timeout = setTimeout(notifyStarted, timeoutMs);
1423
+ try {
1424
+ await started;
1425
+ }
1426
+ finally {
1427
+ clearTimeout(timeout);
1428
+ this.#initializationStartWaiters.delete(notifyStarted);
1429
+ }
1430
+ }
1397
1431
  /**
1398
1432
  * Track a network or provider reinitialization so disconnect can serialize
1399
1433
  * behind the whole operation, including work before and after init().
@@ -1544,6 +1578,11 @@ export class PerpsController extends BaseController {
1544
1578
  subscriptionBuilderAddressMainnet: this.#options.clientConfig?.providerCredentials?.hyperliquid
1545
1579
  ?.subscriptionBuilderAddressMainnet,
1546
1580
  onChaseOrderMaxDistanceReached: this.#publishChaseOrderMaxDistanceReached,
1581
+ getAgentSigner: this.#agentBindings.resolve,
1582
+ onAgentRejected: (account, agentAddress) => {
1583
+ this.#agentBindings.release(account, agentAddress);
1584
+ this.#options.clientConfig?.providerCredentials?.hyperliquid?.onAgentRejected?.(account, agentAddress);
1585
+ },
1547
1586
  });
1548
1587
  this.providers.set('hyperliquid', hyperLiquidProvider);
1549
1588
  // Register Lighter provider if enabled (POC). Dynamic import so clients
@@ -1576,7 +1615,7 @@ export class PerpsController extends BaseController {
1576
1615
  * @param LighterProviderClass - Constructor class for the Lighter provider.
1577
1616
  */
1578
1617
  registerLighterProvider(LighterProviderClass) {
1579
- const lighterIsTestnet = PROVIDER_CONFIG.LIGHTER_TESTNET_ONLY || this.state.isTestnet;
1618
+ const lighterIsTestnet = isProviderOnTestnet('lighter', this.state.isTestnet);
1580
1619
  const lighter = this.#options.clientConfig?.providerCredentials?.lighter ?? {};
1581
1620
  const lighterProvider = new LighterProviderClass({
1582
1621
  isTestnet: lighterIsTestnet,
@@ -1628,6 +1667,7 @@ export class PerpsController extends BaseController {
1628
1667
  providers: this.providers,
1629
1668
  defaultProvider: 'hyperliquid',
1630
1669
  infrastructure: this.#options.infrastructure,
1670
+ isTestnet: this.state.isTestnet,
1631
1671
  });
1632
1672
  this.#debugLog('PerpsController: Using aggregated provider (multi-provider)', { registeredProviders: Array.from(this.providers.keys()) });
1633
1673
  }
@@ -1812,9 +1852,16 @@ export class PerpsController extends BaseController {
1812
1852
  * @returns The active provider once initialization completes.
1813
1853
  */
1814
1854
  async #getActiveProviderWhenReady() {
1855
+ // The context the action was issued under. A client reconnect
1856
+ // (disconnect then init) that switches account, network or provider must
1857
+ // not carry the action into the new context.
1858
+ const issuedContext = this.#getActionContext();
1859
+ let awaitedDisconnect = false;
1860
+ let awaitedInitializationStart = false;
1815
1861
  while (true) {
1816
1862
  const pendingDisconnect = this.#disconnectOperationPromise;
1817
1863
  if (pendingDisconnect) {
1864
+ awaitedDisconnect = true;
1818
1865
  await pendingDisconnect;
1819
1866
  continue;
1820
1867
  }
@@ -1829,9 +1876,37 @@ export class PerpsController extends BaseController {
1829
1876
  await pendingInitialization;
1830
1877
  continue;
1831
1878
  }
1879
+ // Clients reconnect with disconnect() followed by init(), and the
1880
+ // disconnect settles before init() is called. Give that init a bounded
1881
+ // window to start rather than failing an action the reconnect will
1882
+ // serve. Nothing here starts a connection the client did not ask for.
1883
+ if (awaitedDisconnect &&
1884
+ !awaitedInitializationStart &&
1885
+ !this.isInitialized &&
1886
+ !pendingInitialization) {
1887
+ awaitedInitializationStart = true;
1888
+ await this.#waitForInitializationStart(PERPS_CONSTANTS.ConnectionTimeoutMs);
1889
+ continue;
1890
+ }
1891
+ if (awaitedDisconnect && this.#getActionContext() !== issuedContext) {
1892
+ throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
1893
+ }
1832
1894
  return this.getActiveProvider();
1833
1895
  }
1834
1896
  }
1897
+ /**
1898
+ * Identify the account, network and provider an action runs under.
1899
+ *
1900
+ * @returns A key that changes when any of them changes.
1901
+ */
1902
+ #getActionContext() {
1903
+ const address = getSelectedEvmAccountFromMessenger(this.messenger)?.address;
1904
+ return [
1905
+ address?.toLowerCase() ?? '',
1906
+ this.state.isTestnet ? 'testnet' : 'mainnet',
1907
+ this.state.activeProvider,
1908
+ ].join('|');
1909
+ }
1835
1910
  /**
1836
1911
  * Get the currently active provider, returning null if not available
1837
1912
  * Use this method when the caller can gracefully handle a missing provider
@@ -4428,6 +4503,110 @@ export class PerpsController extends BaseController {
4428
4503
  });
4429
4504
  return this.#marketDataService.calculateFees({ provider, params, context });
4430
4505
  }
4506
+ /**
4507
+ * Sign HyperLiquid L1 actions (orders, cancels, leverage, ...) for a main
4508
+ * account on a network with an approved agent, or pin them to the main
4509
+ * account with null (`getAgentSigner` is then not asked for that account and
4510
+ * network until `clearAgentSigners`). User-signed actions stay on the main
4511
+ * account, and the agent is never used for another account or network. The
4512
+ * controller keeps the binding across provider re-creation (a provider or
4513
+ * network switch, or re-initialization), so it can also be set before
4514
+ * `init`. Like every controller action, it is available through the
4515
+ * messenger once `init` has run.
4516
+ *
4517
+ * @param account - The main account and network the agent is approved for.
4518
+ * @param agentSigner - The host-owned agent signer, or null to pin the main
4519
+ * account.
4520
+ */
4521
+ setAgentSigner(account, agentSigner) {
4522
+ this.#agentBindings.set(account, agentSigner);
4523
+ // Drop agents the providers already resolved so the binding applies to
4524
+ // the next L1 action.
4525
+ this.#clearProviderAgentSigners();
4526
+ }
4527
+ /**
4528
+ * Forget every HyperLiquid agent, set or resolved, so the next L1 action
4529
+ * asks `providerCredentials.hyperliquid.getAgentSigner` again; an answer
4530
+ * still pending is discarded too. Call it when the wallet locks (with
4531
+ * `getAgentSigner` returning null while locked) and nothing signs with an
4532
+ * agent until it returns one again. Like every controller action, it is
4533
+ * available through the messenger once `init` has run.
4534
+ */
4535
+ clearAgentSigners() {
4536
+ this.#agentBindings.clear();
4537
+ this.#clearProviderAgentSigners();
4538
+ }
4539
+ /**
4540
+ * Drop the agents every provider resolved.
4541
+ */
4542
+ #clearProviderAgentSigners() {
4543
+ for (const provider of this.providers.values()) {
4544
+ provider.clearAgentSigners?.();
4545
+ }
4546
+ }
4547
+ /**
4548
+ * Run the active provider's deferred trading setup ahead of the first order
4549
+ * (HyperLiquid account migration, builder fee and referral; Lighter
4550
+ * venue-key registration), so its signatures happen in one guided session,
4551
+ * such as agent setup, instead of at order time. The builder fee and
4552
+ * Lighter's registration are signed by the main account; with an agent,
4553
+ * the referral and the account migration are L1 actions the agent signs.
4554
+ *
4555
+ * @returns `ready: true` when none of these steps will need a signature
4556
+ * again before the first order, and only while an account is selected and
4557
+ * the main account can sign, whichever provider answered (including
4558
+ * providers without deferred setup, for example in aggregated mode). A
4559
+ * HyperLiquid referral whose MetaMask referral code is not ready yet is
4560
+ * checked again at the next call, not before orders, so it does not hold
4561
+ * it back. Otherwise `ready: false`, without an error while a step will
4562
+ * be asked again (a declined HyperLiquid migration, builder fee or Lighter
4563
+ * registration, or a step the agent could not sign), or with:
4564
+ * - `KEYRING_LOCKED` when the main account cannot sign, before or during
4565
+ * setup;
4566
+ * - `EXCHANGE_ACCOUNT_NOT_FOUND` for a wallet with no account on the venue
4567
+ * yet;
4568
+ * - `NO_ACCOUNT_SELECTED` when no account is selected;
4569
+ * - `PROVIDER_LIFECYCLE_STALE` when the provider disconnected or the account
4570
+ * changed during setup;
4571
+ * - otherwise the message of the error that stopped setup, which is logged.
4572
+ * @throws Like the other provider-backed actions, `CLIENT_NOT_INITIALIZED`
4573
+ * before `init`, and `CLIENT_REINITIALIZING` or `PROVIDER_NOT_AVAILABLE`
4574
+ * when no active provider is available.
4575
+ */
4576
+ async prepareTradingWallet() {
4577
+ const provider = await this.#getActiveProviderWhenReady();
4578
+ // With nothing selected, the AccountsController answers an empty account.
4579
+ const readSelectedAddress = () => {
4580
+ const address = getSelectedEvmAccountFromMessenger(this.messenger)?.address;
4581
+ return address ? address.toLowerCase() : undefined;
4582
+ };
4583
+ const addressAtStart = readSelectedAddress();
4584
+ const result = (await provider.prepareTradingWallet?.()) ?? {
4585
+ ready: true,
4586
+ };
4587
+ const address = readSelectedAddress();
4588
+ // The steps ran for the account selected when they started (in aggregated
4589
+ // mode, one provider after another), so their result is not the current
4590
+ // account's.
4591
+ if (address !== addressAtStart) {
4592
+ return {
4593
+ ready: false,
4594
+ error: PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE,
4595
+ };
4596
+ }
4597
+ if (!result.ready) {
4598
+ return result;
4599
+ }
4600
+ // A provider with nothing to prepare, alone or aggregated, checks neither
4601
+ // the signer nor the selected account.
4602
+ if (!isMainAccountSignerReady(this.#options.infrastructure.accountSigner, () => this.messenger.call('KeyringController:getState').isUnlocked)) {
4603
+ return { ready: false, error: PERPS_ERROR_CODES.KEYRING_LOCKED };
4604
+ }
4605
+ if (!address) {
4606
+ return { ready: false, error: PERPS_ERROR_CODES.NO_ACCOUNT_SELECTED };
4607
+ }
4608
+ return result;
4609
+ }
4431
4610
  /**
4432
4611
  * Approve the dedicated subscription builder outside order submission.
4433
4612
  *