@pear-protocol/exchanges-sdk 0.6.0 → 0.6.1

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.
@@ -25,6 +25,9 @@ async function fetchClearinghouseState(user, dex, demo = false) {
25
25
  async function fetchUserAbstraction(user, demo = false) {
26
26
  return fetchInfo({ type: "userAbstraction", user }, demo);
27
27
  }
28
+ async function fetchUserRole(user, demo = false) {
29
+ return fetchInfo({ type: "userRole", user }, demo);
30
+ }
28
31
  async function fetchSpotClearinghouseState(user, demo = false) {
29
32
  return fetchInfo({ type: "spotClearinghouseState", user }, demo);
30
33
  }
@@ -32,4 +35,4 @@ async function fetchActiveAssetData(user, coin, demo = false) {
32
35
  return fetchInfo({ type: "activeAssetData", user, coin }, demo);
33
36
  }
34
37
 
35
- export { fetchActiveAssetData, fetchClearinghouseState, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction };
38
+ export { fetchActiveAssetData, fetchClearinghouseState, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole };
@@ -1,7 +1,17 @@
1
- import { fetchInfo } from '../../client/hyperliquid/rest';
1
+ import { fetchInfo, fetchUserRole } from '../../client/hyperliquid/rest';
2
2
 
3
3
  function createHyperliquidHelper(demo = false) {
4
4
  return {
5
+ /**
6
+ * Checks that the address holds a Hyperliquid account, which the exchange only creates on the
7
+ * first deposit. This is account existence alone — builder-fee approval, Pear trade-account
8
+ * setup, and agent approval stay separate gates. A failed request throws rather than reporting
9
+ * an inactive account, so callers never mistake an outage for a user who has to deposit.
10
+ */
11
+ async checkAccountActivation(userAddress) {
12
+ const role = await fetchUserRole(userAddress, demo);
13
+ return role.role !== "missing";
14
+ },
5
15
  /** Checks that the requested API wallet exists and has not expired. */
6
16
  async checkApiWallet(params) {
7
17
  const agents = await fetchInfo(
@@ -1,14 +1,41 @@
1
1
  import { vi, describe, beforeEach, it, expect } from 'vitest';
2
2
  import { createHyperliquidHelper } from './helper';
3
3
 
4
- const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
5
- vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
4
+ const { fetchInfoMock, fetchUserRoleMock } = vi.hoisted(() => ({
5
+ fetchInfoMock: vi.fn(),
6
+ fetchUserRoleMock: vi.fn()
7
+ }));
8
+ vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock, fetchUserRole: fetchUserRoleMock }));
6
9
  const userAddress = "0x1111111111111111111111111111111111111111";
7
10
  const agentWalletAddress = "0x2222222222222222222222222222222222222222";
8
11
  const builderAddress = "0x3333333333333333333333333333333333333333";
12
+ const masterAddress = "0x4444444444444444444444444444444444444444";
9
13
  describe("createHyperliquidHelper", () => {
10
14
  beforeEach(() => {
11
15
  fetchInfoMock.mockReset();
16
+ fetchUserRoleMock.mockReset();
17
+ });
18
+ it("reports an address Hyperliquid does not know as not activated", async () => {
19
+ fetchUserRoleMock.mockResolvedValue({ role: "missing" });
20
+ const activated = await createHyperliquidHelper(true).checkAccountActivation(userAddress);
21
+ expect(activated).toBe(false);
22
+ expect(fetchUserRoleMock).toHaveBeenCalledWith(userAddress, true);
23
+ });
24
+ it.each([
25
+ ["user", { role: "user" }],
26
+ ["agent", { role: "agent", data: { user: masterAddress } }],
27
+ ["vault", { role: "vault" }],
28
+ ["subAccount", { role: "subAccount", data: { master: masterAddress } }]
29
+ ])("reports the %s role as activated", async (_role, response) => {
30
+ fetchUserRoleMock.mockResolvedValue(response);
31
+ await expect(createHyperliquidHelper().checkAccountActivation(userAddress)).resolves.toBe(true);
32
+ expect(fetchUserRoleMock).toHaveBeenCalledWith(userAddress, false);
33
+ });
34
+ it("surfaces a failed role request as an error instead of an inactive account", async () => {
35
+ fetchUserRoleMock.mockRejectedValue(new Error("Hyperliquid info request failed: 429"));
36
+ await expect(createHyperliquidHelper().checkAccountActivation(userAddress)).rejects.toThrow(
37
+ "Hyperliquid info request failed: 429"
38
+ );
12
39
  });
13
40
  it("accepts unexpired and non-expiring API wallets on the selected network", async () => {
14
41
  fetchInfoMock.mockResolvedValue([
package/dist/index.d.ts CHANGED
@@ -211,6 +211,7 @@ type CreateExchangeWsConfig = {
211
211
  declare function createExchangeWs<E extends Connector>(exchange: E, config: CreateExchangeWsConfig): Promise<ExchangeWsByConnector[E]>;
212
212
 
213
213
  interface HyperliquidHelper {
214
+ checkAccountActivation(userAddress: `0x${string}`): Promise<boolean>;
214
215
  checkApiWallet(params: CheckApiWalletParams): Promise<boolean>;
215
216
  checkBuilderFeeApproval(userAddress: `0x${string}`, builderAddress: `0x${string}`, requiredFeeTenthsBps?: number): Promise<boolean>;
216
217
  }
@@ -49,6 +49,7 @@ class BaseAccountSyncer {
49
49
  // Phase B: Snapshot the exchange — fetch positions and fills since the watermark.
50
50
  async snapshotExchange(state) {
51
51
  const { openPositions, provisionalFills, instruments, watermark } = state;
52
+ const { firstSyncStartedAt } = watermark;
52
53
  const externalFills = await this.listExternalFills({
53
54
  since: new Date(watermark.sinceMs),
54
55
  instruments,
@@ -56,12 +57,16 @@ class BaseAccountSyncer {
56
57
  provisionalFills
57
58
  });
58
59
  const exchangePositions = await this.listExternalPositions({ instruments });
59
- return { exchangePositions, externalFills };
60
+ return {
61
+ exchangePositions,
62
+ externalFills,
63
+ firstSyncStartedAt
64
+ };
60
65
  }
61
66
  // Phase C: Compute the sync payload and post it across the three sync endpoints.
62
67
  async computeAndPostSync(state, snapshot) {
63
68
  const { openPositions, entryBySymbol, provisionalFills, watermark } = state;
64
- const { exchangePositions, externalFills } = snapshot;
69
+ const { exchangePositions, externalFills, firstSyncStartedAt } = snapshot;
65
70
  const payload = await computeSyncPayload({
66
71
  tradeAccountId: this.account.id,
67
72
  connector: this.connector,
@@ -83,7 +88,10 @@ class BaseAccountSyncer {
83
88
  await this.sdk.core.sync.fills.confirm(payload.confirmables);
84
89
  }
85
90
  const { firstSyncTrim } = payload;
86
- let meta = payload.meta;
91
+ let meta = {
92
+ ...payload.meta,
93
+ ...firstSyncStartedAt === void 0 ? {} : { firstSyncStartedAt }
94
+ };
87
95
  if (firstSyncTrim) {
88
96
  console.log(
89
97
  `${this.connector}: first-sync trim \u2014 dropped ${firstSyncTrim.trimmedCount} pre-cutoff fills (cutoffTs=${firstSyncTrim.cutoffTs})`
@@ -39,7 +39,12 @@ class FakeSyncer extends BaseAccountSyncer {
39
39
  }
40
40
  function makeSyncer(args) {
41
41
  const fills = {
42
- since: vi.fn().mockResolvedValue({ sinceMs: 0, neverSynced: args.neverSynced, confirmedExchangeFillIds: [] }),
42
+ since: vi.fn().mockResolvedValue({
43
+ sinceMs: 0,
44
+ neverSynced: args.neverSynced,
45
+ firstSyncStartedAt: args.neverSynced ? FIRST_SYNC_STARTED_AT : void 0,
46
+ confirmedExchangeFillIds: []
47
+ }),
43
48
  reconcile: vi.fn().mockResolvedValue({}),
44
49
  confirm: vi.fn().mockResolvedValue({}),
45
50
  create: vi.fn().mockResolvedValue({}),
@@ -64,6 +69,7 @@ function makeSyncer(args) {
64
69
  return { syncer: new FakeSyncer(config), sync };
65
70
  }
66
71
  const NEWEST_TS = 105e4;
72
+ const FIRST_SYNC_STARTED_AT = 1e6;
67
73
  function trimmableFills(btcEraTs) {
68
74
  return [
69
75
  extFill({ exchangeFillId: "a", symbol: "BTC", side: "BUY", quantity: "1", timestamp: btcEraTs.open }),
@@ -90,6 +96,17 @@ describe("BaseAccountSyncer \u2014 first-sync plumbing", () => {
90
96
  expect(sync.fills.create).toHaveBeenCalledTimes(1);
91
97
  expect(sync.fills.create.mock.calls[0]?.[0]?.exchangePositions).toBe(SOL_SNAPSHOT);
92
98
  });
99
+ it("captures an empty first-sync checkpoint before fetching fills", async () => {
100
+ const { syncer, sync } = makeSyncer({ neverSynced: true });
101
+ syncer.positionsFn.mockResolvedValue([]);
102
+ syncer.fillsFn.mockResolvedValue([]);
103
+ await syncer.synchronizeFills();
104
+ expect(sync.fills.since.mock.invocationCallOrder[0]).toBeLessThan(syncer.fillsFn.mock.invocationCallOrder[0] ?? 0);
105
+ expect(sync.fills.create.mock.calls[0]?.[0]?.meta).toEqual({
106
+ externalMaxTs: null,
107
+ firstSyncStartedAt: FIRST_SYNC_STARTED_AT
108
+ });
109
+ });
93
110
  it("bumps the posted watermark past the overlap window when trimmed fills fall inside it", async () => {
94
111
  const { syncer, sync } = makeSyncer({ neverSynced: true });
95
112
  syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
@@ -125,7 +142,7 @@ describe("BaseAccountSyncer \u2014 first-sync plumbing", () => {
125
142
  ]);
126
143
  await syncer.synchronizeFills();
127
144
  expect(syncer.positionsFn).toHaveBeenCalledTimes(1);
128
- expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS);
145
+ expect(sync.fills.create.mock.calls[0]?.[0]?.meta).toEqual({ externalMaxTs: NEWEST_TS });
129
146
  expect(logSpy).not.toHaveBeenCalled();
130
147
  });
131
148
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/exchanges-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Pear Protocol Exchanges SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@pear-protocol/core-sdk": "^1.12.0",
31
- "@pear-protocol/types": "^1.15.0",
32
- "@pear-protocol/utils": "^0.4.0",
31
+ "@pear-protocol/types": "^1.15.1",
32
+ "@pear-protocol/utils": "^0.4.1",
33
33
  "bignumber.js": "^11.1.1",
34
34
  "partysocket": "^1.0.3",
35
35
  "viem": "^2.48.8"