@pear-protocol/exchanges-sdk 0.3.0 → 0.4.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/README.md +78 -12
  2. package/dist/client/hyperliquid/client.test.js +41 -0
  3. package/dist/client/hyperliquid/exchange-ws.js +3 -2
  4. package/dist/client/hyperliquid/rest.js +26 -5
  5. package/dist/client/hyperliquid/ws.js +16 -4
  6. package/dist/client/index.js +2 -2
  7. package/dist/client/lighter/constants.js +9 -1
  8. package/dist/client/lighter/constants.test.js +22 -0
  9. package/dist/client/lighter/exchange-ws.js +2 -2
  10. package/dist/client/lighter/rest.js +101 -32
  11. package/dist/client/lighter/rest.test.js +152 -0
  12. package/dist/client/lighter/ws.js +7 -2
  13. package/dist/client/okx/rest.js +18 -9
  14. package/dist/helper/factory.js +2 -2
  15. package/dist/helper/hyperliquid/helper.js +28 -13
  16. package/dist/helper/hyperliquid/helper.test.js +45 -0
  17. package/dist/helper/lighter/helper.js +77 -16
  18. package/dist/helper/lighter/helper.test.js +289 -0
  19. package/dist/helper/lighter/types.js +9 -8
  20. package/dist/index.d.ts +182 -38
  21. package/dist/sdk.js +20 -16
  22. package/dist/sdk.test.js +101 -0
  23. package/dist/status/hyperliquid/status.js +18 -5
  24. package/dist/status/hyperliquid/status.test.js +81 -0
  25. package/dist/status/lighter/status.js +18 -11
  26. package/dist/status/lighter/status.test.js +75 -0
  27. package/dist/syncer/base.js +0 -2
  28. package/dist/syncer/base.test.js +0 -1
  29. package/dist/syncer/hyperliquid/syncer.js +96 -43
  30. package/dist/syncer/hyperliquid/syncer.test.js +274 -0
  31. package/dist/syncer/lighter/funding.js +25 -0
  32. package/dist/syncer/lighter/funding.test.js +48 -0
  33. package/dist/syncer/lighter/syncer.js +30 -6
  34. package/dist/syncer/lighter/syncer.test.js +132 -6
  35. package/dist/tracker/base.js +23 -6
  36. package/dist/tracker/base.test.js +132 -0
  37. package/dist/tracker/binance/tracker.js +48 -18
  38. package/dist/tracker/binance/tracker.test.js +138 -0
  39. package/dist/tracker/bybit/tracker.js +78 -23
  40. package/dist/tracker/bybit/tracker.test.js +145 -0
  41. package/dist/tracker/hyperliquid/mapper.js +244 -44
  42. package/dist/tracker/hyperliquid/mapper.test.js +255 -0
  43. package/dist/tracker/hyperliquid/tracker.js +241 -74
  44. package/dist/tracker/hyperliquid/tracker.test.js +587 -0
  45. package/dist/tracker/index.js +15 -37
  46. package/dist/tracker/lighter/mapper.js +163 -33
  47. package/dist/tracker/lighter/mapper.test.js +199 -0
  48. package/dist/tracker/lighter/tracker.js +219 -45
  49. package/dist/tracker/lighter/tracker.test.js +515 -0
  50. package/dist/tracker/lighter/types.js +10 -2
  51. package/dist/tracker/okx/tracker.js +50 -16
  52. package/dist/tracker/okx/tracker.test.js +126 -0
  53. package/dist/tracker/types.js +2 -1
  54. package/dist/tracker/utils.test.js +8 -0
  55. package/package.json +3 -2
@@ -0,0 +1,152 @@
1
+ import { afterEach, vi, describe, it, expect } from 'vitest';
2
+ import { fetchAccount, fetchAssetDetails, fetchAccountsByL1Address, fetchAccountLimits, fetchPositionFundingSince, fetchTradesSince } from './rest';
3
+
4
+ afterEach(() => {
5
+ vi.unstubAllGlobals();
6
+ });
7
+ describe("Lighter REST client", () => {
8
+ it("routes demo account reads to testnet and rejects exchange-level errors", async () => {
9
+ const fetchMock = vi.fn().mockResolvedValue(
10
+ new Response(JSON.stringify({ code: 20001, message: "account not found" }), {
11
+ status: 200
12
+ })
13
+ );
14
+ vi.stubGlobal("fetch", fetchMock);
15
+ await expect(fetchAccount(42, true)).rejects.toThrow("Lighter /account returned 20001: account not found");
16
+ expect(fetchMock).toHaveBeenCalledWith("https://testnet.zklighter.elliot.ai/api/v1/account?by=index&value=42");
17
+ });
18
+ it("rejects HTTP failures before reading endpoint data", async () => {
19
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("unavailable", { status: 503 })));
20
+ await expect(fetchAssetDetails(false)).rejects.toThrow("Lighter /assetDetails returned 503");
21
+ });
22
+ it("preserves the account-not-registered lookup result for the helper", async () => {
23
+ vi.stubGlobal(
24
+ "fetch",
25
+ vi.fn().mockResolvedValue(
26
+ new Response(JSON.stringify({ code: 21100, message: "account not found" }), {
27
+ status: 200
28
+ })
29
+ )
30
+ );
31
+ await expect(fetchAccountsByL1Address("0xabc", true)).resolves.toEqual({
32
+ code: 21100,
33
+ message: "account not found"
34
+ });
35
+ });
36
+ it("paginates and de-duplicates subaccounts while stopping a repeated cursor", async () => {
37
+ const fetchMock = vi.fn().mockResolvedValueOnce(
38
+ new Response(
39
+ JSON.stringify({
40
+ code: 200,
41
+ sub_accounts: [
42
+ { account_type: 0, index: 1 },
43
+ { account_type: 1, index: 2 }
44
+ ],
45
+ next_cursor: "page-2"
46
+ }),
47
+ { status: 200 }
48
+ )
49
+ ).mockResolvedValueOnce(
50
+ new Response(
51
+ JSON.stringify({
52
+ code: 200,
53
+ sub_accounts: [
54
+ { account_type: 1, index: 2 },
55
+ { account_type: 1, index: 3 }
56
+ ],
57
+ next_cursor: "page-2"
58
+ }),
59
+ { status: 200 }
60
+ )
61
+ );
62
+ vi.stubGlobal("fetch", fetchMock);
63
+ await expect(fetchAccountsByL1Address("0xabc", true)).resolves.toMatchObject({
64
+ code: 200,
65
+ sub_accounts: [
66
+ { account_type: 0, index: 1 },
67
+ { account_type: 1, index: 2 },
68
+ { account_type: 1, index: 3 }
69
+ ]
70
+ });
71
+ expect(fetchMock).toHaveBeenCalledTimes(2);
72
+ expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
73
+ });
74
+ it("reads authenticated account limits from the selected environment", async () => {
75
+ const fetchMock = vi.fn().mockResolvedValue(
76
+ new Response(JSON.stringify({ code: 200, user_tier: "premium" }), {
77
+ status: 200
78
+ })
79
+ );
80
+ vi.stubGlobal("fetch", fetchMock);
81
+ await expect(fetchAccountLimits(42, "auth-token", true)).resolves.toMatchObject({ user_tier: "premium" });
82
+ expect(fetchMock).toHaveBeenCalledWith(
83
+ "https://testnet.zklighter.elliot.ai/api/v1/accountLimits?account_index=42",
84
+ { headers: { authorization: "auth-token" } }
85
+ );
86
+ });
87
+ it("returns typed L1 and L2 decimals from assetDetails", async () => {
88
+ const fetchMock = vi.fn().mockResolvedValue(
89
+ new Response(
90
+ JSON.stringify({
91
+ code: 200,
92
+ asset_details: [{ asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }]
93
+ }),
94
+ { status: 200 }
95
+ )
96
+ );
97
+ vi.stubGlobal("fetch", fetchMock);
98
+ await expect(fetchAssetDetails(false)).resolves.toEqual([
99
+ { asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }
100
+ ]);
101
+ expect(fetchMock).toHaveBeenCalledWith("https://mainnet.zklighter.elliot.ai/api/v1/assetDetails");
102
+ });
103
+ it("paginates and de-duplicates funding rows by market and funding id", async () => {
104
+ const duplicate = fundingRow({ funding_id: 2, timestamp: 2e3 });
105
+ const fetchMock = vi.fn().mockResolvedValueOnce(
106
+ new Response(
107
+ JSON.stringify({
108
+ code: 200,
109
+ position_fundings: [fundingRow({ funding_id: 1, timestamp: 1e3 }), duplicate],
110
+ next_cursor: "page-2"
111
+ }),
112
+ { status: 200 }
113
+ )
114
+ ).mockResolvedValueOnce(
115
+ new Response(
116
+ JSON.stringify({
117
+ code: 200,
118
+ position_fundings: [duplicate, fundingRow({ funding_id: 3, timestamp: 3e3 })]
119
+ }),
120
+ { status: 200 }
121
+ )
122
+ );
123
+ vi.stubGlobal("fetch", fetchMock);
124
+ const rows = await fetchPositionFundingSince(42, "auth", /* @__PURE__ */ new Date(1e3), true);
125
+ expect(rows.map((row) => row.funding_id)).toEqual([1, 2, 3]);
126
+ expect(fetchMock).toHaveBeenCalledTimes(2);
127
+ expect(String(fetchMock.mock.calls[0]?.[0])).toContain("start_timestamp=1000");
128
+ expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
129
+ });
130
+ it("stops trades pagination when the API repeats a cursor", async () => {
131
+ const page = new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 });
132
+ const fetchMock = vi.fn().mockResolvedValueOnce(page).mockResolvedValueOnce(
133
+ new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 })
134
+ );
135
+ vi.stubGlobal("fetch", fetchMock);
136
+ await expect(fetchTradesSince(42, "auth", /* @__PURE__ */ new Date(1e3), true)).resolves.toEqual([]);
137
+ expect(fetchMock).toHaveBeenCalledTimes(2);
138
+ expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=same-page");
139
+ });
140
+ });
141
+ function fundingRow(overrides) {
142
+ return {
143
+ timestamp: overrides.timestamp,
144
+ market_id: 1,
145
+ funding_id: overrides.funding_id,
146
+ change: "-2",
147
+ discount: "0",
148
+ rate: "0.0001",
149
+ position_size: "4",
150
+ position_side: "long"
151
+ };
152
+ }
@@ -1,10 +1,15 @@
1
1
  import { BaseWs } from '../base-ws';
2
- import { LIGHTER_WS_URL } from './constants';
2
+ import { getLighterWsUrl } from './constants';
3
3
 
4
4
  const PING_INTERVAL_MS = 9e4;
5
5
  class LighterWs extends BaseWs {
6
+ constructor(demo = false) {
7
+ super();
8
+ this.demo = demo;
9
+ }
10
+ demo;
6
11
  getWsUrl() {
7
- return LIGHTER_WS_URL;
12
+ return getLighterWsUrl(this.demo);
8
13
  }
9
14
  getPingIntervalMs() {
10
15
  return PING_INTERVAL_MS;
@@ -44,14 +44,23 @@ async function fetchBalance(creds, demo) {
44
44
  async function fetchPositions(creds, demo) {
45
45
  return signedGet(creds, "/api/v5/account/positions", { instType: "SWAP" }, demo);
46
46
  }
47
- async function fetchLeverageInfo(creds, instId, demo) {
48
- const result = await signedGet(
49
- creds,
50
- "/api/v5/account/leverage-info",
51
- { instId, mgnMode: "cross" },
52
- demo
53
- ).catch(() => []);
54
- return result[0];
47
+ const LEVERAGE_INFO_MAX_INST_IDS = 20;
48
+ async function fetchLeverageInfos(creds, instIds, demo) {
49
+ const chunks = [];
50
+ for (let i = 0; i < instIds.length; i += LEVERAGE_INFO_MAX_INST_IDS) {
51
+ chunks.push(instIds.slice(i, i + LEVERAGE_INFO_MAX_INST_IDS));
52
+ }
53
+ const results = await Promise.all(
54
+ chunks.map(
55
+ (chunk) => signedGet(
56
+ creds,
57
+ "/api/v5/account/leverage-info",
58
+ { instId: chunk.join(","), mgnMode: "cross" },
59
+ demo
60
+ )
61
+ )
62
+ );
63
+ return results.flat();
55
64
  }
56
65
  const FILLS_HISTORY_LIMIT = 100;
57
66
  async function fetchFillsHistorySince(creds, since, demo) {
@@ -81,4 +90,4 @@ async function fetchFillsHistorySince(creds, since, demo) {
81
90
  return Array.from(byId.values());
82
91
  }
83
92
 
84
- export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince, fetchLeverageInfo, fetchPositions };
93
+ export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince, fetchLeverageInfos, fetchPositions };
@@ -4,9 +4,9 @@ import { createLighterHelper } from './lighter';
4
4
  function createExchangeHelper(config) {
5
5
  switch (config.exchange) {
6
6
  case "hyperliquid":
7
- return createHyperliquidHelper();
7
+ return createHyperliquidHelper(config.demo ?? false);
8
8
  case "lighter":
9
- return createLighterHelper(config.sdk);
9
+ return createLighterHelper(config.sdk, config.demo ?? false);
10
10
  }
11
11
  }
12
12
 
@@ -1,21 +1,36 @@
1
1
  import { fetchInfo } from '../../client/hyperliquid/rest';
2
2
 
3
- function createHyperliquidHelper() {
3
+ function createHyperliquidHelper(demo = false) {
4
4
  return {
5
+ /** Checks that the requested API wallet exists and has not expired. */
5
6
  async checkApiWallet(params) {
6
- const agents = await fetchInfo({
7
- type: "extraAgents",
8
- user: params.userAddress
9
- });
10
- return agents.some((agent) => agent.address.toLowerCase() === params.agentWalletAddress.toLowerCase());
7
+ const agents = await fetchInfo(
8
+ {
9
+ type: "extraAgents",
10
+ user: params.userAddress
11
+ },
12
+ demo
13
+ );
14
+ const now = Date.now();
15
+ return agents.some(
16
+ (agent) => agent.address.toLowerCase() === params.agentWalletAddress.toLowerCase() && (agent.validUntil === null || agent.validUntil > now)
17
+ );
11
18
  },
12
- async checkBuilderFeeApproval(userAddress, builderAddress) {
13
- const maxBuilderFee = await fetchInfo({
14
- type: "maxBuilderFee",
15
- user: userAddress,
16
- builder: builderAddress
17
- });
18
- return Number(maxBuilderFee) > 0;
19
+ /** Checks that the approval covers the exact builder fee requested by orders. */
20
+ async checkBuilderFeeApproval(userAddress, builderAddress, requiredFeeTenthsBps) {
21
+ const requiredFee = requiredFeeTenthsBps ?? 1;
22
+ if (!Number.isInteger(requiredFee) || requiredFee <= 0) {
23
+ return false;
24
+ }
25
+ const maxBuilderFee = await fetchInfo(
26
+ {
27
+ type: "maxBuilderFee",
28
+ user: userAddress,
29
+ builder: builderAddress
30
+ },
31
+ demo
32
+ );
33
+ return Number(maxBuilderFee) >= requiredFee;
19
34
  }
20
35
  };
21
36
  }
@@ -0,0 +1,45 @@
1
+ import { vi, describe, beforeEach, it, expect } from 'vitest';
2
+ import { createHyperliquidHelper } from './helper';
3
+
4
+ const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
5
+ vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
6
+ const userAddress = "0x1111111111111111111111111111111111111111";
7
+ const agentWalletAddress = "0x2222222222222222222222222222222222222222";
8
+ const builderAddress = "0x3333333333333333333333333333333333333333";
9
+ describe("createHyperliquidHelper", () => {
10
+ beforeEach(() => {
11
+ fetchInfoMock.mockReset();
12
+ });
13
+ it("accepts unexpired and non-expiring API wallets on the selected network", async () => {
14
+ fetchInfoMock.mockResolvedValue([
15
+ { name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 },
16
+ { name: "active", address: agentWalletAddress.toUpperCase(), validUntil: null }
17
+ ]);
18
+ const valid = await createHyperliquidHelper(true).checkApiWallet({ userAddress, agentWalletAddress });
19
+ expect(valid).toBe(true);
20
+ expect(fetchInfoMock).toHaveBeenCalledWith({ type: "extraAgents", user: userAddress }, true);
21
+ });
22
+ it("rejects an expired API wallet", async () => {
23
+ fetchInfoMock.mockResolvedValue([{ name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 }]);
24
+ await expect(createHyperliquidHelper().checkApiWallet({ userAddress, agentWalletAddress })).resolves.toBe(false);
25
+ });
26
+ it("requires the approval to cover the requested fee rather than merely be positive", async () => {
27
+ fetchInfoMock.mockResolvedValue(6);
28
+ const helper = createHyperliquidHelper(true);
29
+ await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 10)).resolves.toBe(false);
30
+ await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 6)).resolves.toBe(true);
31
+ expect(fetchInfoMock).toHaveBeenCalledWith(
32
+ { type: "maxBuilderFee", user: userAddress, builder: builderAddress },
33
+ true
34
+ );
35
+ });
36
+ it("preserves the two-argument helper contract as an any-positive-approval check", async () => {
37
+ fetchInfoMock.mockResolvedValue(1);
38
+ const approved = await createHyperliquidHelper().checkBuilderFeeApproval(userAddress, builderAddress);
39
+ expect(approved).toBe(true);
40
+ expect(fetchInfoMock).toHaveBeenCalledWith(
41
+ { type: "maxBuilderFee", user: userAddress, builder: builderAddress },
42
+ false
43
+ );
44
+ });
45
+ });
@@ -1,12 +1,46 @@
1
1
  import { parseUnits, parseEther } from 'viem';
2
- import { fetchApiKeys, fetchAccount, fetchAccountsByL1Address } from '../../client/lighter/rest';
2
+ import { fetchApiKeys, fetchAccount, fetchAccountsByL1Address, fetchAssetDetails } from '../../client/lighter/rest';
3
3
  import { writeWithdraw, writeDeposit } from './ethereum';
4
- import { getAssetDecimals } from './types';
4
+ import { AssetId } from './types';
5
5
 
6
- function createLighterHelper(sdk) {
6
+ const MIN_USER_API_KEY_INDEX = 4;
7
+ const MAX_API_KEY_INDEX = 254;
8
+ function findAvailableApiKeyIndex(apiKeys) {
9
+ const occupied = new Set(apiKeys.map((key) => key.api_key_index));
10
+ for (let index = MIN_USER_API_KEY_INDEX; index <= MAX_API_KEY_INDEX; index += 1) {
11
+ if (!occupied.has(index)) return index;
12
+ }
13
+ throw new Error(
14
+ `No available Lighter API key slots: non-reserved indices ${MIN_USER_API_KEY_INDEX}-${MAX_API_KEY_INDEX} are occupied`
15
+ );
16
+ }
17
+ function validateDecimals(decimals, source) {
18
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 255) {
19
+ throw new Error(`Invalid ${source} decimals: ${decimals}`);
20
+ }
21
+ return decimals;
22
+ }
23
+ async function resolveAssetDecimals(assetId, field, suppliedDecimals, demo) {
24
+ if (suppliedDecimals !== void 0) return validateDecimals(suppliedDecimals, field);
25
+ const assets = await fetchAssetDetails(demo);
26
+ const asset = assets.find((candidate) => candidate.asset_id === assetId);
27
+ if (!asset) throw new Error(`Lighter asset ${assetId} was not returned by assetDetails`);
28
+ return validateDecimals(asset[field], `assetDetails.${field}`);
29
+ }
30
+ function validateApprovalRequirements(requirements) {
31
+ for (const [name, value] of [
32
+ ["minPerpsMakerFee", requirements.minPerpsMakerFee],
33
+ ["minPerpsTakerFee", requirements.minPerpsTakerFee]
34
+ ]) {
35
+ if (!Number.isSafeInteger(value) || value < 0) {
36
+ throw new Error(`Invalid Lighter integrator ${name}: ${value}`);
37
+ }
38
+ }
39
+ }
40
+ function createLighterHelper(sdk, demo = false) {
7
41
  return {
8
42
  async lookupAccount(l1Address) {
9
- const data = await fetchAccountsByL1Address(l1Address);
43
+ const data = await fetchAccountsByL1Address(l1Address, demo);
10
44
  if (data.code === 21100) {
11
45
  throw new Error("Account not registered on Lighter");
12
46
  }
@@ -20,16 +54,24 @@ function createLighterHelper(sdk) {
20
54
  subAccountIndexes: accounts.filter((s) => s.account_type !== 0).map((s) => s.index)
21
55
  };
22
56
  },
23
- async checkIntegratorApproval(accountIndex, integratorAccountIndex) {
24
- const data = await fetchAccount(accountIndex);
57
+ async checkIntegratorApproval(accountIndex, integratorAccountIndex, requirements) {
58
+ if (requirements) validateApprovalRequirements(requirements);
59
+ const data = await fetchAccount(accountIndex, demo);
25
60
  const account = data.accounts[0];
26
61
  if (!account) return false;
62
+ const minMakerFee = requirements?.minPerpsMakerFee ?? 0;
63
+ const minTakerFee = requirements?.minPerpsTakerFee ?? 0;
27
64
  const integrators = account.approved_integrators ?? [];
28
- return integrators.some((i) => i.account_index === integratorAccountIndex);
65
+ return integrators.some(
66
+ (integrator) => integrator.account_index === integratorAccountIndex && integrator.approval_expiry > Date.now() && integrator.max_perps_maker_fee >= minMakerFee && integrator.max_perps_taker_fee >= minTakerFee
67
+ );
29
68
  },
30
69
  async connectWallet(walletClient, params) {
31
- const apiKeys = await fetchApiKeys(params.accountIndex);
32
- const apiKeyIndex = Math.max(4, ...apiKeys.map((k) => k.api_key_index + 1));
70
+ if (demo) {
71
+ throw new Error("Lighter demo wallet connection is unavailable because backend signing is mainnet-only");
72
+ }
73
+ const apiKeys = await fetchApiKeys(params.accountIndex, demo);
74
+ const apiKeyIndex = findAvailableApiKeyIndex(apiKeys);
33
75
  const { privateKey, messageToSign, txInfo } = await sdk.core.misc.lighter.createApiKey({
34
76
  accountIndex: params.accountIndex,
35
77
  apiKeyIndex
@@ -47,6 +89,9 @@ function createLighterHelper(sdk) {
47
89
  return { txHash, apiKeyIndex, apiPrivateKey: privateKey };
48
90
  },
49
91
  async approveIntegrator(walletClient, params) {
92
+ if (demo) {
93
+ throw new Error("Lighter demo integrator approval is unavailable because backend signing is mainnet-only");
94
+ }
50
95
  const { messageToSign, txInfo } = await sdk.core.misc.lighter.prepareApproveIntegrator({
51
96
  accountIndex: params.accountIndex,
52
97
  apiKeyIndex: params.apiKeyIndex,
@@ -65,28 +110,44 @@ function createLighterHelper(sdk) {
65
110
  return { txHash };
66
111
  },
67
112
  async deposit(walletClient, params) {
113
+ if (demo) {
114
+ throw new Error("Lighter demo deposits are unavailable because the testnet gateway is not verified");
115
+ }
116
+ const l1Decimals = await resolveAssetDecimals(params.assetId, "l1_decimals", params.l1Decimals, demo);
117
+ const amount = parseUnits(params.amount, l1Decimals);
118
+ let value;
119
+ if (params.assetId === AssetId.ETH) {
120
+ value = parseEther(params.nativeValue ?? params.amount);
121
+ if (value !== amount) {
122
+ throw new Error("Lighter native ETH deposit amount and msg.value must match");
123
+ }
124
+ } else if (params.nativeValue !== void 0 && parseEther(params.nativeValue) !== 0n) {
125
+ throw new Error("Lighter nativeValue is only valid for ETH deposits");
126
+ }
68
127
  const txHash = await writeDeposit(walletClient, {
69
128
  to: params.to,
70
129
  assetIndex: params.assetId,
71
130
  routeType: params.routeType,
72
- // `amount` is a human string in the asset's own units; scale to base units.
73
- amount: parseUnits(params.amount, getAssetDecimals(params.assetId)),
74
- // Native-ETH deposits are payable; `nativeValue` is the ETH sent as
75
- // msg.value (18-decimal wei), separate from the accounting `amount`.
76
- value: params.nativeValue ? parseEther(params.nativeValue) : void 0
131
+ // Ethereum deposits use the L1 token precision, not Lighter's L2/base-amount precision.
132
+ amount,
133
+ value
77
134
  });
78
135
  return { txHash };
79
136
  },
80
137
  async withdraw(walletClient, params) {
138
+ if (demo) {
139
+ throw new Error("Lighter demo withdrawals are unavailable because the testnet gateway is not verified");
140
+ }
141
+ const l2Decimals = await resolveAssetDecimals(params.assetId, "decimals", params.l2Decimals, demo);
81
142
  const txHash = await writeWithdraw(walletClient, {
82
143
  accountIndex: params.accountIndex,
83
144
  assetIndex: params.assetId,
84
145
  routeType: params.routeType,
85
- baseAmount: parseUnits(params.amount, getAssetDecimals(params.assetId))
146
+ baseAmount: parseUnits(params.amount, l2Decimals)
86
147
  });
87
148
  return { txHash };
88
149
  }
89
150
  };
90
151
  }
91
152
 
92
- export { createLighterHelper };
153
+ export { MAX_API_KEY_INDEX, MIN_USER_API_KEY_INDEX, createLighterHelper, findAvailableApiKeyIndex };