Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/social-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional perp fields to the `Trade` type (and `TradeStruct`): `classification` (`'spot' | 'perp' | 'send' | 'receive' | null`), `perpPositionType` (`'long' | 'short' | null`), and `perpLeverage` (`number | null`) — exposing Hyperliquid/perp trade metadata to consumers ([#9094](https://github.com/MetaMask/core/pull/9094))
- Add optional perp fields to the `Position` type (and `PositionStruct`): `perpPositionType` (`'long' | 'short' | null`), `perpLeverage` (`number | null`), and `positionAmountWithLeverage` (`number | null`) — exposing Hyperliquid/perp position metadata to consumers ([#9094](https://github.com/MetaMask/core/pull/9094))

### Changed

- Bump `@metamask/controller-utils` from `^12.0.0` to `^12.2.0` ([#8774](https://github.com/MetaMask/core/pull/8774), [#9058](https://github.com/MetaMask/core/pull/9058), [#9083](https://github.com/MetaMask/core/pull/9083))
Expand Down
111 changes: 111 additions & 0 deletions packages/social-controllers/src/SocialService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,37 @@ const mockPosition = {
tokenImageUrl: 'https://assets.daylight.xyz/images/token-eth.png',
};

const mockPerpTrade = {
direction: 'buy',
intent: 'enter',
classification: 'perp',
perpPositionType: 'long',
perpLeverage: 10,
tokenAmount: 1.5,
usdCost: 3000,
timestamp: 1700000000,
transactionHash:
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
};

const mockPerpPosition = {
positionId: 'position-perp-1',
tokenSymbol: 'BTC',
tokenName: 'Bitcoin',
tokenAddress: 'BTC',
chain: 'hyperliquid',
positionAmount: 2.5,
boughtUsd: 112500,
soldUsd: 0,
realizedPnl: 0,
costBasis: 112500,
trades: [mockPerpTrade],
lastTradeAt: 1700000000,
perpPositionType: 'long',
perpLeverage: 10,
positionAmountWithLeverage: 25,
};

const MOCK_TOKEN = 'mock-bearer-token';

type RootMessenger = Messenger<
Expand Down Expand Up @@ -497,6 +528,86 @@ describe('SocialService', () => {
SocialServiceErrorMessage.FETCH_OPEN_POSITIONS_INVALID_RESPONSE,
);
});

it('passes through perp metadata on positions and trades', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () =>
Promise.resolve({
positions: [mockPerpPosition],
pagination: { hasMore: false },
}),
});

const service = createService();
const result = await service.fetchOpenPositions({
addressOrId: '0x1234',
});

expect(result.positions[0]).toStrictEqual(mockPerpPosition);
expect(result.positions[0].perpPositionType).toBe('long');
expect(result.positions[0].perpLeverage).toBe(10);
expect(result.positions[0].positionAmountWithLeverage).toBe(25);
expect(result.positions[0].trades[0].classification).toBe('perp');
expect(result.positions[0].trades[0].perpPositionType).toBe('long');
expect(result.positions[0].trades[0].perpLeverage).toBe(10);
});

it('accepts null perp fields for spot positions', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () =>
Promise.resolve({
positions: [
{
...mockPosition,
perpPositionType: null,
perpLeverage: null,
positionAmountWithLeverage: null,
trades: [
{
...mockTrade,
classification: null,
perpPositionType: null,
perpLeverage: null,
},
],
},
],
pagination: { hasMore: false },
}),
});

const service = createService();
const result = await service.fetchOpenPositions({
addressOrId: '0x1234',
});

expect(result.positions[0].perpPositionType).toBeNull();
expect(result.positions[0].trades[0].classification).toBeNull();
});

it('rejects an invalid perpPositionType', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () =>
Promise.resolve({
positions: [{ ...mockPerpPosition, perpPositionType: 'sideways' }],
pagination: { hasMore: false },
}),
});

const service = createService();

await expect(
service.fetchOpenPositions({ addressOrId: '0x1234' }),
).rejects.toThrow(
SocialServiceErrorMessage.FETCH_OPEN_POSITIONS_INVALID_RESPONSE,
);
});
});

describe('fetchClosedPositions', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/social-controllers/src/SocialService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AuthenticationController } from '@metamask/profile-sync-controller
import {
array,
boolean,
enums,
is,
nullable,
number,
Expand Down Expand Up @@ -76,6 +77,9 @@ const PositionStruct = structType({
currentValueUSD: optional(nullable(number())),
pnlValueUsd: optional(nullable(number())),
pnlPercent: optional(nullable(number())),
perpPositionType: optional(nullable(enums(['long', 'short']))),
perpLeverage: optional(nullable(number())),
positionAmountWithLeverage: optional(nullable(number())),
});

const PaginationStruct = structType({
Expand Down
18 changes: 18 additions & 0 deletions packages/social-controllers/src/social-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Infer } from '@metamask/superstruct';
import {
enums,
nullable,
number,
optional,
string,
Expand Down Expand Up @@ -39,6 +40,14 @@ export const TradeStruct = structType({
direction: enums(['buy', 'sell']),
intent: enums(['enter', 'exit']),
category: optional(string()),
/** High-level trade classification. `null` when Clicker does not classify. */
classification: optional(
nullable(enums(['spot', 'perp', 'send', 'receive'])),
),
/** Perp side for this fill. `null` for spot trades. */
perpPositionType: optional(nullable(enums(['long', 'short']))),
/** Leverage multiplier for perp trades (e.g. `5` for 5x). `null` for spot. */
perpLeverage: optional(nullable(number())),
tokenAmount: number(),
usdCost: number(),
timestamp: number(),
Expand Down Expand Up @@ -153,6 +162,15 @@ export type Position = {
pnlValueUsd?: number | null;
/** PnL as a percentage of cost basis. */
pnlPercent?: number | null;
/** Perp side of the position. `null`/absent for spot positions. */
perpPositionType?: 'long' | 'short' | null;
/** Leverage multiplier for perp positions. `null`/absent for spot. */
perpLeverage?: number | null;
/**
* Leveraged position size (un-leveraged `positionAmount` × leverage), i.e.
* the capital at risk. Hyperliquid/perp positions only; absent for spot.
*/
positionAmountWithLeverage?: number | null;
};

export type Pagination = {
Expand Down
Loading