@strobelabs/perpcity-sdk 0.1.7 → 0.2.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.
package/dist/index.mjs CHANGED
@@ -1,49 +1,7 @@
1
1
  // src/context.ts
2
2
  import { GraphQLClient } from "graphql-request";
3
-
4
- // src/deployments.ts
5
- var DEPLOYMENTS = {
6
- // Base Sepolia
7
- [84532]: {
8
- perpManager: "0x59F1766b77fd67af6c80217C2025A0D536998000",
9
- usdc: "0xC1a5D4E99BB224713dd179eA9CA2Fa6600706210",
10
- goldskyPublic: "https://api.goldsky.com/api/public/project_cmbawn40q70fj01ws4jmsfj7f/subgraphs/perp-city/36ac28e6-20250925_150813/gn",
11
- goldskyPrivate: "https://api.goldsky.com/api/private/project_cmbawn40q70fj01ws4jmsfj7f/subgraphs/perp-city/36ac28e6-20250925_150813/gn"
12
- }
13
- };
14
-
15
- // src/context.ts
16
- import { publicActions } from "viem";
17
- var PerpCityContext = class {
18
- constructor(config) {
19
- this.walletClient = config.walletClient.extend(publicActions);
20
- const chainId = this.validateChainId();
21
- const deployments = DEPLOYMENTS[chainId];
22
- const headers = {};
23
- let goldskyEndpoint;
24
- if (config.goldskyBearerToken) {
25
- headers.authorization = `Bearer ${config.goldskyBearerToken}`;
26
- goldskyEndpoint = deployments.goldskyPrivate;
27
- } else {
28
- goldskyEndpoint = deployments.goldskyPublic;
29
- }
30
- this.goldskyClient = new GraphQLClient(goldskyEndpoint, {
31
- headers
32
- });
33
- }
34
- validateChainId() {
35
- const chainId = this.walletClient.chain?.id;
36
- if (!chainId) throw new Error(`Chain ID is not set.`);
37
- if (!DEPLOYMENTS[chainId]) throw new Error(`Unsupported chainId: ${chainId}.`);
38
- return chainId;
39
- }
40
- deployments() {
41
- return DEPLOYMENTS[this.validateChainId()];
42
- }
43
- };
44
-
45
- // src/entities/openPosition.ts
46
- import { publicActions as publicActions2 } from "viem";
3
+ import { publicActions, formatUnits } from "viem";
4
+ import { parse } from "graphql";
47
5
 
48
6
  // src/utils/constants.ts
49
7
  var NUMBER_1E6 = 1e6;
@@ -1436,451 +1394,524 @@ async function estimateLiquidity(context, tickLower, tickUpper, usdScaled) {
1436
1394
  });
1437
1395
  }
1438
1396
 
1439
- // src/entities/openPosition.ts
1440
- var OpenPosition = class _OpenPosition {
1441
- constructor(context, perpId, positionId) {
1442
- this.context = context;
1443
- this.perpId = perpId;
1444
- this.positionId = positionId;
1397
+ // src/utils/errors.ts
1398
+ import { BaseError, ContractFunctionRevertedError } from "viem";
1399
+ var PerpCityError = class extends Error {
1400
+ constructor(message, cause) {
1401
+ super(message);
1402
+ this.cause = cause;
1403
+ this.name = "PerpCityError";
1445
1404
  }
1446
- async closePosition(params) {
1447
- const contractParams = {
1448
- posId: this.positionId,
1449
- minAmt0Out: scale6Decimals(params.minAmt0Out),
1450
- minAmt1Out: scale6Decimals(params.minAmt1Out),
1451
- maxAmt1In: scale6Decimals(params.maxAmt1In)
1452
- };
1453
- const { result, request } = await this.context.walletClient.extend(publicActions2).simulateContract({
1454
- address: this.context.deployments().perpManager,
1455
- abi: PERP_MANAGER_ABI,
1456
- functionName: "closePosition",
1457
- args: [this.perpId, contractParams],
1458
- account: this.context.walletClient.account
1459
- });
1460
- await this.context.walletClient.writeContract(request);
1461
- return result === null ? null : new _OpenPosition(this.context, this.perpId, result);
1405
+ };
1406
+ var ContractError = class extends PerpCityError {
1407
+ constructor(message, errorName, args, cause) {
1408
+ super(message, cause);
1409
+ this.errorName = errorName;
1410
+ this.args = args;
1411
+ this.name = "ContractError";
1462
1412
  }
1463
- async liveDetails() {
1464
- const { result, request } = await this.context.walletClient.simulateContract({
1465
- address: this.context.deployments().perpManager,
1466
- abi: PERP_MANAGER_ABI,
1467
- functionName: "livePositionDetails",
1468
- args: [this.perpId, this.positionId],
1469
- account: this.context.walletClient.account
1470
- });
1471
- return {
1472
- pnl: scaleFrom6Decimals(Number(result[0])),
1473
- fundingPayment: scaleFrom6Decimals(Number(result[1])),
1474
- effectiveMargin: scaleFrom6Decimals(Number(result[2])),
1475
- isLiquidatable: result[3]
1476
- };
1413
+ };
1414
+ var TransactionRejectedError = class extends PerpCityError {
1415
+ constructor(message = "Transaction rejected by user", cause) {
1416
+ super(message, cause);
1417
+ this.name = "TransactionRejectedError";
1477
1418
  }
1478
1419
  };
1479
-
1480
- // src/entities/perp.ts
1481
- import { nearestUsableTick } from "@uniswap/v3-sdk";
1482
- import { gql } from "graphql-request";
1483
- import { parse } from "graphql";
1484
- var Perp = class {
1485
- constructor(context, id) {
1486
- this.context = context;
1487
- this.id = id;
1420
+ var InsufficientFundsError = class extends PerpCityError {
1421
+ constructor(message = "Insufficient funds for transaction", cause) {
1422
+ super(message, cause);
1423
+ this.name = "InsufficientFundsError";
1488
1424
  }
1489
- // READS
1490
- async tickSpacing() {
1491
- return await this.context.walletClient.readContract({
1492
- address: this.context.deployments().perpManager,
1493
- abi: PERP_MANAGER_ABI,
1494
- functionName: "tickSpacing",
1495
- args: [this.id]
1496
- });
1425
+ };
1426
+ var GraphQLError = class extends PerpCityError {
1427
+ constructor(message, cause) {
1428
+ super(message, cause);
1429
+ this.name = "GraphQLError";
1497
1430
  }
1498
- async mark() {
1499
- const sqrtPriceX96 = await this.context.walletClient.readContract({
1500
- address: this.context.deployments().perpManager,
1501
- abi: PERP_MANAGER_ABI,
1502
- functionName: "sqrtPriceX96",
1503
- args: [this.id]
1504
- });
1505
- return sqrtPriceX96ToPrice(sqrtPriceX96);
1431
+ };
1432
+ var RPCError = class extends PerpCityError {
1433
+ constructor(message, cause) {
1434
+ super(message, cause);
1435
+ this.name = "RPCError";
1506
1436
  }
1507
- async index() {
1508
- const beacon = await this.beacon();
1509
- const query = parse(gql`
1510
- query ($beaconAddr: Bytes!) {
1511
- beaconSnapshots(
1512
- first: 1
1513
- orderBy: timestamp
1514
- orderDirection: desc
1515
- where: { beacon: $beaconAddr }
1516
- ) { indexPrice }
1517
- }
1518
- `);
1519
- const response = await this.context.goldskyClient.request(query, { beaconAddr: beacon });
1520
- return Number(response.beaconSnapshots[0].indexPrice);
1437
+ };
1438
+ var ValidationError = class extends PerpCityError {
1439
+ constructor(message, cause) {
1440
+ super(message, cause);
1441
+ this.name = "ValidationError";
1521
1442
  }
1522
- async beacon() {
1523
- const query = parse(gql`
1524
- query ($perpId: Bytes!) {
1525
- perp(id: $perpId) {
1526
- beacon { id }
1527
- }
1528
- }
1529
- `);
1530
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1531
- return response.perp.beacon.id;
1443
+ };
1444
+ function parseContractError(error) {
1445
+ if (error instanceof PerpCityError) {
1446
+ return error;
1532
1447
  }
1533
- async lastIndexUpdate() {
1534
- const beacon = await this.beacon();
1535
- const query = parse(gql`
1536
- query ($beaconAddr: Bytes!) {
1537
- beaconSnapshots(
1538
- first: 1
1539
- orderBy: timestamp
1540
- orderDirection: desc
1541
- where: { beacon: $beaconAddr }
1542
- ) { timestamp }
1543
- }
1544
- `);
1545
- const response = await this.context.goldskyClient.request(query, { beaconAddr: beacon });
1546
- return Number(response.beaconSnapshots[0].timestamp);
1448
+ if (error instanceof BaseError) {
1449
+ const revertError = error.walk((err) => err instanceof ContractFunctionRevertedError);
1450
+ if (revertError instanceof ContractFunctionRevertedError) {
1451
+ const errorName = revertError.data?.errorName ?? "Unknown";
1452
+ const args = revertError.data?.args ?? [];
1453
+ const message = formatContractError(errorName, args);
1454
+ return new ContractError(message, errorName, args, error);
1455
+ }
1456
+ if (error.message?.includes("User rejected") || error.code === 4001) {
1457
+ return new TransactionRejectedError(error.message, error);
1458
+ }
1459
+ if (error.message?.includes("insufficient funds")) {
1460
+ return new InsufficientFundsError(error.message, error);
1461
+ }
1462
+ return new PerpCityError(error.shortMessage || error.message, error);
1547
1463
  }
1548
- async openInterest() {
1549
- const query = parse(gql`
1550
- query ($perpId: Bytes!) {
1551
- perpSnapshots(
1552
- first: 1
1553
- orderBy: timestamp
1554
- orderDirection: desc
1555
- where: { perp: $perpId }
1556
- ) {
1557
- takerLongNotional
1558
- takerShortNotional
1559
- }
1560
- }
1561
- `);
1562
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1563
- return {
1564
- takerLongNotional: Number(response.perpSnapshots[0].takerLongNotional),
1565
- takerShortNotional: Number(response.perpSnapshots[0].takerShortNotional)
1566
- };
1464
+ if (error instanceof Error) {
1465
+ return new PerpCityError(error.message, error);
1567
1466
  }
1568
- async markTimeSeries() {
1569
- const query = parse(gql`
1570
- query ($perpId: Bytes!) {
1571
- perpSnapshots(
1572
- orderBy: timestamp
1573
- orderDirection: asc
1574
- where: { perp: $perpId }
1575
- ) {
1576
- timestamp
1577
- markPrice
1578
- }
1579
- }
1580
- `);
1581
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1582
- return response.perpSnapshots.map((snapshot) => ({
1583
- timestamp: Number(snapshot.timestamp),
1584
- value: Number(snapshot.markPrice)
1585
- }));
1586
- }
1587
- async indexTimeSeries() {
1588
- const beacon = await this.beacon();
1589
- const query = parse(gql`
1590
- query ($beaconAddr: Bytes!) {
1591
- beaconSnapshots(
1592
- orderBy: timestamp
1593
- orderDirection: asc
1594
- where: { beacon: $beaconAddr }
1595
- ) {
1596
- timestamp
1597
- indexPrice
1598
- }
1599
- }
1600
- `);
1601
- const response = await this.context.goldskyClient.request(query, { beaconAddr: beacon });
1602
- return response.beaconSnapshots.map((snapshot) => ({
1603
- timestamp: Number(snapshot.timestamp),
1604
- value: Number(snapshot.indexPrice)
1605
- }));
1467
+ return new PerpCityError(String(error));
1468
+ }
1469
+ function formatContractError(errorName, args) {
1470
+ switch (errorName) {
1471
+ case "InvalidBeaconAddress":
1472
+ return `Invalid beacon address: ${args[0]}`;
1473
+ case "InvalidTradingFeeSplits":
1474
+ return `Invalid trading fee splits. Insurance split: ${args[0]}, Creator split: ${args[1]}`;
1475
+ case "InvalidMaxOpeningLev":
1476
+ return `Invalid maximum opening leverage: ${args[0]}`;
1477
+ case "InvalidLiquidationLev":
1478
+ return `Invalid liquidation leverage: ${args[0]}. Must be less than max opening leverage: ${args[1]}`;
1479
+ case "InvalidLiquidationFee":
1480
+ return `Invalid liquidation fee: ${args[0]}`;
1481
+ case "InvalidLiquidatorFeeSplit":
1482
+ return `Invalid liquidator fee split: ${args[0]}`;
1483
+ case "InvalidClose":
1484
+ return `Cannot close position. Caller: ${args[0]}, Holder: ${args[1]}, Is Liquidated: ${args[2]}`;
1485
+ case "InvalidCaller":
1486
+ return `Invalid caller. Expected: ${args[1]}, Got: ${args[0]}`;
1487
+ case "InvalidLiquidity":
1488
+ return `Invalid liquidity amount: ${args[0]}`;
1489
+ case "InvalidMargin":
1490
+ return `Invalid margin amount: ${args[0]}`;
1491
+ case "InvalidLevX96":
1492
+ return `Invalid leverage: ${args[0]}. Maximum allowed: ${args[1]}`;
1493
+ case "MakerPositionLocked":
1494
+ return `Maker position is locked until ${new Date(Number(args[1]) * 1e3).toISOString()}. Current time: ${new Date(Number(args[0]) * 1e3).toISOString()}`;
1495
+ case "MaximumAmountExceeded":
1496
+ return `Maximum amount exceeded. Maximum: ${args[0]}, Requested: ${args[1]}`;
1497
+ case "MinimumAmountInsufficient":
1498
+ return `Minimum amount not met. Required: ${args[0]}, Received: ${args[1]}`;
1499
+ case "PriceImpactTooHigh":
1500
+ return `Price impact too high. Current price: ${args[0]}, Min acceptable: ${args[1]}, Max acceptable: ${args[2]}`;
1501
+ case "SwapReverted":
1502
+ return "Swap failed. This may be due to insufficient liquidity or slippage tolerance.";
1503
+ case "ZeroSizePosition":
1504
+ return `Cannot create zero-size position. Perp delta: ${args[0]}, USD delta: ${args[1]}`;
1505
+ case "InvalidFundingInterval":
1506
+ return `Invalid funding interval: ${args[0]}`;
1507
+ case "InvalidPriceImpactBand":
1508
+ return `Invalid price impact band: ${args[0]}`;
1509
+ case "InvalidMarketDeathThreshold":
1510
+ return `Invalid market death threshold: ${args[0]}`;
1511
+ case "InvalidTickRange":
1512
+ return `Invalid tick range. Lower: ${args[0]}, Upper: ${args[1]}`;
1513
+ case "MarketNotKillable":
1514
+ return `Market health (${args[0]}) is above death threshold (${args[1]}). Market cannot be killed yet.`;
1515
+ case "InvalidStartingSqrtPriceX96":
1516
+ return `Invalid starting sqrt price: ${args[0]}`;
1517
+ default:
1518
+ return `Contract error: ${errorName}${args.length > 0 ? ` (${args.join(", ")})` : ""}`;
1606
1519
  }
1607
- async fundingRate() {
1608
- const query = parse(gql`
1609
- query ($perpId: Bytes!) {
1610
- perpSnapshots(
1611
- first: 1
1612
- orderBy: timestamp
1613
- orderDirection: desc
1614
- where: { perp: $perpId }
1615
- ) {
1616
- fundingRate
1617
- }
1618
- }
1619
- `);
1620
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1621
- return Number(response.perpSnapshots[0].fundingRate);
1520
+ }
1521
+ async function withErrorHandling(fn, context) {
1522
+ try {
1523
+ return await fn();
1524
+ } catch (error) {
1525
+ const parsedError = parseContractError(error);
1526
+ parsedError.message = `${context}: ${parsedError.message}`;
1527
+ throw parsedError;
1622
1528
  }
1623
- async bounds() {
1624
- const result = await this.context.walletClient.readContract({
1625
- address: this.context.deployments().perpManager,
1626
- abi: PERP_MANAGER_ABI,
1627
- functionName: "tradingBounds",
1628
- args: [this.id]
1529
+ }
1530
+
1531
+ // src/context.ts
1532
+ import { erc20Abi as erc20Abi2 } from "viem";
1533
+ var PerpCityContext = class {
1534
+ constructor(config) {
1535
+ this.walletClient = config.walletClient.extend(publicActions);
1536
+ this._deployments = config.deployments;
1537
+ const headers = {};
1538
+ if (config.goldskyBearerToken) {
1539
+ headers.authorization = `Bearer ${config.goldskyBearerToken}`;
1540
+ }
1541
+ this.goldskyClient = new GraphQLClient(config.goldskyEndpoint, {
1542
+ headers
1629
1543
  });
1630
- return {
1631
- minMargin: Number(result[0]),
1632
- minTakerLeverage: marginRatioToLeverage(result[5]),
1633
- maxTakerLeverage: marginRatioToLeverage(result[4])
1634
- };
1635
- }
1636
- // TODO
1637
- async maxTakerNotional(isLong) {
1638
- return 0;
1639
1544
  }
1640
- async simulateTaker(params) {
1641
- const contractParams = {
1642
- isLong: params.isLong,
1643
- margin: scale6Decimals(params.margin),
1644
- levX96: scaleToX96(params.leverage),
1645
- unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
1646
- };
1647
- const { result, request } = await this.context.walletClient.simulateContract({
1648
- address: this.context.deployments().perpManager,
1649
- abi: PERP_MANAGER_ABI,
1650
- functionName: "quoteTakerPosition",
1651
- args: [this.id, contractParams],
1652
- account: this.context.walletClient.account
1653
- });
1654
- return {
1655
- success: result[0],
1656
- size: scaleFrom6Decimals(Math.abs(Number(result[1]))),
1657
- notional: scaleFrom6Decimals(Math.abs(Number(result[2]))),
1658
- creatorFeeAmt: scaleFrom6Decimals(Number(result[3])),
1659
- insuranceFeeAmt: scaleFrom6Decimals(Number(result[4])),
1660
- lpFeeAmt: scaleFrom6Decimals(Number(result[5]))
1661
- };
1545
+ deployments() {
1546
+ return this._deployments;
1662
1547
  }
1663
- async allMakerPositions() {
1664
- const query = parse(gql`
1665
- query ($perpId: Bytes!) {
1666
- openPositions(
1667
- where: { perp: $perpId, isMaker: true }
1668
- ) {
1669
- perp { id }
1670
- inContractPosId
1548
+ // Optimized batch data fetching methods
1549
+ async fetchPerpData(perpId) {
1550
+ return withErrorHandling(async () => {
1551
+ const perpQuery = parse(`
1552
+ query ($perpId: Bytes!) {
1553
+ perp(id: $perpId) {
1554
+ beacon { id }
1555
+ }
1556
+ perpSnapshots(
1557
+ orderBy: timestamp
1558
+ orderDirection: asc
1559
+ where: { perp: $perpId }
1560
+ ) {
1561
+ timestamp
1562
+ markPrice
1563
+ takerLongNotional
1564
+ takerShortNotional
1565
+ fundingRate
1566
+ }
1671
1567
  }
1672
- }
1673
- `);
1674
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1675
- return response.openPositions.map((position) => new OpenPosition(this.context, position.perp.id, position.inContractPosId));
1676
- }
1677
- async allTakerPositions() {
1678
- const query = parse(gql`
1679
- query ($perpId: Bytes!) {
1680
- openPositions(
1681
- where: { perp: $perpId, isMaker: false }
1682
- ) {
1683
- perp { id }
1684
- inContractPosId
1568
+ `);
1569
+ const beaconQuery = parse(`
1570
+ query ($beaconAddr: Bytes!) {
1571
+ beaconSnapshots(
1572
+ orderBy: timestamp
1573
+ orderDirection: asc
1574
+ where: { beacon: $beaconAddr }
1575
+ ) {
1576
+ timestamp
1577
+ indexPrice
1578
+ }
1685
1579
  }
1686
- }
1687
- `);
1688
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1689
- return response.openPositions.map((position) => new OpenPosition(this.context, position.perp.id, position.inContractPosId));
1580
+ `);
1581
+ let perpResponse;
1582
+ try {
1583
+ perpResponse = await this.goldskyClient.request(perpQuery, { perpId });
1584
+ } catch (error) {
1585
+ throw new GraphQLError(`Failed to fetch perp data for ${perpId}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
1586
+ }
1587
+ if (!perpResponse.perp || !perpResponse.perp.beacon) {
1588
+ throw new GraphQLError(`Perp ${perpId} not found or has no beacon`);
1589
+ }
1590
+ let beaconResponse;
1591
+ let contractData;
1592
+ try {
1593
+ [beaconResponse, contractData] = await Promise.all([
1594
+ this.goldskyClient.request(beaconQuery, { beaconAddr: perpResponse.perp.beacon.id }),
1595
+ this.fetchPerpContractData(perpId)
1596
+ ]);
1597
+ } catch (error) {
1598
+ if (error instanceof GraphQLError || error instanceof RPCError) {
1599
+ throw error;
1600
+ }
1601
+ throw new GraphQLError(`Failed to fetch beacon or contract data: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
1602
+ }
1603
+ if (perpResponse.perpSnapshots.length === 0) {
1604
+ throw new GraphQLError(`No perpSnapshots found for perp ${perpId}. This perp may be newly created with no trading activity yet.`);
1605
+ }
1606
+ if (beaconResponse.beaconSnapshots.length === 0) {
1607
+ throw new GraphQLError(`No beaconSnapshots found for perp ${perpId} beacon ${perpResponse.perp.beacon.id}. The beacon may not have any price updates yet.`);
1608
+ }
1609
+ const markTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
1610
+ timestamp: Number(snapshot.timestamp),
1611
+ value: Number(snapshot.markPrice)
1612
+ }));
1613
+ const indexTimeSeries = beaconResponse.beaconSnapshots.map((snapshot) => ({
1614
+ timestamp: Number(snapshot.timestamp),
1615
+ value: Number(snapshot.indexPrice)
1616
+ }));
1617
+ const openInterestTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
1618
+ timestamp: Number(snapshot.timestamp),
1619
+ value: {
1620
+ takerLongNotional: Number(snapshot.takerLongNotional),
1621
+ takerShortNotional: Number(snapshot.takerShortNotional)
1622
+ }
1623
+ }));
1624
+ const fundingRateTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
1625
+ timestamp: Number(snapshot.timestamp),
1626
+ value: Number(snapshot.fundingRate)
1627
+ }));
1628
+ const latestSnapshot = perpResponse.perpSnapshots[perpResponse.perpSnapshots.length - 1];
1629
+ const latestBeaconSnapshot = beaconResponse.beaconSnapshots[beaconResponse.beaconSnapshots.length - 1];
1630
+ const perpData = {
1631
+ id: perpId,
1632
+ tickSpacing: contractData.tickSpacing,
1633
+ mark: sqrtPriceX96ToPrice(contractData.sqrtPriceX96),
1634
+ index: Number(latestBeaconSnapshot.indexPrice),
1635
+ beacon: perpResponse.perp.beacon.id,
1636
+ lastIndexUpdate: Number(latestBeaconSnapshot.timestamp),
1637
+ openInterest: {
1638
+ takerLongNotional: Number(latestSnapshot.takerLongNotional),
1639
+ takerShortNotional: Number(latestSnapshot.takerShortNotional)
1640
+ },
1641
+ markTimeSeries,
1642
+ indexTimeSeries,
1643
+ fundingRate: Number(latestSnapshot.fundingRate),
1644
+ bounds: contractData.bounds,
1645
+ fees: contractData.fees,
1646
+ openInterestTimeSeries,
1647
+ fundingRateTimeSeries,
1648
+ totalOpenMakerPnl: 0,
1649
+ // These will be calculated by functions
1650
+ totalOpenTakerPnl: 0
1651
+ // These will be calculated by functions
1652
+ };
1653
+ return perpData;
1654
+ }, `fetchPerpData for perp ${perpId}`);
1690
1655
  }
1691
- async totalOpenMakerPnl() {
1692
- const positions = await this.allMakerPositions();
1693
- const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
1694
- return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
1656
+ async fetchPerpContractData(perpId) {
1657
+ return withErrorHandling(async () => {
1658
+ const [tickSpacing, sqrtPriceX96, boundsRaw, feesRaw] = await Promise.all([
1659
+ this.walletClient.readContract({
1660
+ address: this.deployments().perpManager,
1661
+ abi: PERP_MANAGER_ABI,
1662
+ functionName: "tickSpacing",
1663
+ args: [perpId]
1664
+ }),
1665
+ this.walletClient.readContract({
1666
+ address: this.deployments().perpManager,
1667
+ abi: PERP_MANAGER_ABI,
1668
+ functionName: "sqrtPriceX96",
1669
+ args: [perpId]
1670
+ }),
1671
+ this.walletClient.readContract({
1672
+ address: this.deployments().perpManager,
1673
+ abi: PERP_MANAGER_ABI,
1674
+ functionName: "tradingBounds",
1675
+ args: [perpId]
1676
+ }),
1677
+ this.walletClient.readContract({
1678
+ address: this.deployments().perpManager,
1679
+ abi: PERP_MANAGER_ABI,
1680
+ functionName: "fees",
1681
+ args: [perpId]
1682
+ })
1683
+ ]);
1684
+ const bounds = boundsRaw;
1685
+ const fees = feesRaw;
1686
+ return {
1687
+ tickSpacing: Number(tickSpacing),
1688
+ sqrtPriceX96,
1689
+ bounds: {
1690
+ minMargin: Number(formatUnits(bounds[0], 6)),
1691
+ minTakerLeverage: marginRatioToLeverage(Number(formatUnits(bounds[4], 6))),
1692
+ maxTakerLeverage: marginRatioToLeverage(Number(formatUnits(bounds[5], 6)))
1693
+ },
1694
+ fees: {
1695
+ creatorFee: Number(formatUnits(fees[0], 6)),
1696
+ insuranceFee: Number(formatUnits(fees[1], 6)),
1697
+ lpFee: Number(formatUnits(fees[2], 6)),
1698
+ liquidationFee: Number(formatUnits(fees[3], 6))
1699
+ }
1700
+ };
1701
+ }, `fetchPerpContractData for perp ${perpId}`);
1695
1702
  }
1696
- async totalOpenTakerPnl() {
1697
- const positions = await this.allTakerPositions();
1698
- const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
1699
- return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
1703
+ /**
1704
+ * Fetch comprehensive perp data with all related information in a single batched request
1705
+ */
1706
+ async getPerpData(perpId) {
1707
+ return this.fetchPerpData(perpId);
1700
1708
  }
1701
- async fees() {
1702
- const result = await this.context.walletClient.readContract({
1703
- address: this.context.deployments().perpManager,
1704
- abi: PERP_MANAGER_ABI,
1705
- functionName: "fees",
1706
- args: [this.id]
1707
- });
1708
- return {
1709
- creatorFee: scaleFrom6Decimals(result[0]),
1710
- insuranceFee: scaleFrom6Decimals(result[1]),
1711
- lpFee: scaleFrom6Decimals(result[2]),
1712
- liquidationFee: scaleFrom6Decimals(result[3])
1713
- };
1714
- }
1715
- async openInterestTimeSeries() {
1716
- const query = parse(gql`
1717
- query ($perpId: Bytes!) {
1709
+ /**
1710
+ * Fetch data for multiple perps efficiently with true batching
1711
+ * This fetches all perps in just 2 Goldsky requests total (not 2N!)
1712
+ */
1713
+ async getMultiplePerpData(perpIds) {
1714
+ if (perpIds.length === 0) {
1715
+ return /* @__PURE__ */ new Map();
1716
+ }
1717
+ if (perpIds.length === 1) {
1718
+ const data = await this.fetchPerpData(perpIds[0]);
1719
+ return /* @__PURE__ */ new Map([[perpIds[0], data]]);
1720
+ }
1721
+ const batchPerpQuery = parse(`
1722
+ query ($perpIds: [Bytes!]!) {
1723
+ perps(where: { id_in: $perpIds }) {
1724
+ id
1725
+ beacon { id }
1726
+ }
1718
1727
  perpSnapshots(
1719
1728
  orderBy: timestamp
1720
1729
  orderDirection: asc
1721
- where: { perp: $perpId }
1730
+ where: { perp_in: $perpIds }
1722
1731
  ) {
1732
+ perp { id }
1723
1733
  timestamp
1734
+ markPrice
1724
1735
  takerLongNotional
1725
1736
  takerShortNotional
1737
+ fundingRate
1726
1738
  }
1727
1739
  }
1728
1740
  `);
1729
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1730
- return response.perpSnapshots.map((snapshot) => ({
1731
- timestamp: Number(snapshot.timestamp),
1732
- value: {
1733
- takerLongNotional: Number(snapshot.takerLongNotional),
1734
- takerShortNotional: Number(snapshot.takerShortNotional)
1735
- }
1736
- }));
1737
- }
1738
- async fundingRateTimeSeries() {
1739
- const query = parse(gql`
1740
- query ($perpId: Bytes!) {
1741
- perpSnapshots(
1741
+ const perpResponse = await this.goldskyClient.request(batchPerpQuery, { perpIds });
1742
+ const beaconIds = [...new Set(perpResponse.perps.map((p) => p.beacon.id))];
1743
+ const batchBeaconQuery = parse(`
1744
+ query ($beaconIds: [Bytes!]!) {
1745
+ beaconSnapshots(
1742
1746
  orderBy: timestamp
1743
1747
  orderDirection: asc
1744
- where: { perp: $perpId }
1748
+ where: { beacon_in: $beaconIds }
1745
1749
  ) {
1750
+ beacon { id }
1746
1751
  timestamp
1747
- fundingRate
1752
+ indexPrice
1748
1753
  }
1749
1754
  }
1750
1755
  `);
1751
- const response = await this.context.goldskyClient.request(query, { perpId: this.id });
1752
- return response.perpSnapshots.map((snapshot) => ({
1753
- timestamp: Number(snapshot.timestamp),
1754
- value: Number(snapshot.fundingRate)
1755
- }));
1756
- }
1757
- // WRITES
1758
- async approveAndOpenMakerPosition(params) {
1759
- await approveUsdc(this.context, scale6Decimals(params.margin));
1760
- return await this.openMakerPosition(params);
1761
- }
1762
- async approveAndOpenTakerPosition(params) {
1763
- await approveUsdc(this.context, scale6Decimals(params.margin));
1764
- return await this.openTakerPosition(params);
1765
- }
1766
- async openMakerPosition(params) {
1767
- const deployments = this.context.deployments();
1768
- const tickSpacing = await this.tickSpacing();
1769
- const scaledUsd = scale6Decimals(params.margin);
1770
- const tickLower = nearestUsableTick(priceToTick(params.priceLower, true), tickSpacing);
1771
- const tickUpper = nearestUsableTick(priceToTick(params.priceUpper, false), tickSpacing);
1772
- const contractParams = {
1773
- margin: scaledUsd,
1774
- liquidity: await estimateLiquidity(this.context, tickLower, tickUpper, scaledUsd),
1775
- tickLower,
1776
- tickUpper,
1777
- maxAmt0In: scale6Decimals(params.maxAmt0In),
1778
- maxAmt1In: scale6Decimals(params.maxAmt1In)
1779
- };
1780
- const { result, request } = await this.context.walletClient.simulateContract({
1781
- address: deployments.perpManager,
1782
- abi: PERP_MANAGER_ABI,
1783
- functionName: "openMakerPosition",
1784
- args: [this.id, contractParams],
1785
- account: this.context.walletClient.account
1756
+ const [beaconResponse, contractDataMap] = await Promise.all([
1757
+ this.goldskyClient.request(batchBeaconQuery, { beaconIds }),
1758
+ this.fetchMultiplePerpContractData(perpIds)
1759
+ ]);
1760
+ const snapshotsByPerp = /* @__PURE__ */ new Map();
1761
+ perpResponse.perpSnapshots.forEach((snapshot) => {
1762
+ const perpId = snapshot.perp.id;
1763
+ if (!snapshotsByPerp.has(perpId)) {
1764
+ snapshotsByPerp.set(perpId, []);
1765
+ }
1766
+ snapshotsByPerp.get(perpId).push(snapshot);
1786
1767
  });
1787
- await this.context.walletClient.writeContract(request);
1788
- return new OpenPosition(this.context, this.id, result);
1789
- }
1790
- async openTakerPosition(params) {
1791
- const contractParams = {
1792
- isLong: params.isLong,
1793
- margin: scale6Decimals(params.margin),
1794
- levX96: scaleToX96(params.leverage),
1795
- unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
1796
- };
1797
- const { result, request } = await this.context.walletClient.simulateContract({
1798
- address: this.context.deployments().perpManager,
1799
- abi: PERP_MANAGER_ABI,
1800
- functionName: "openTakerPosition",
1801
- args: [this.id, contractParams],
1802
- account: this.context.walletClient.account
1768
+ const snapshotsByBeacon = /* @__PURE__ */ new Map();
1769
+ beaconResponse.beaconSnapshots.forEach((snapshot) => {
1770
+ const beaconId = snapshot.beacon.id;
1771
+ if (!snapshotsByBeacon.has(beaconId)) {
1772
+ snapshotsByBeacon.set(beaconId, []);
1773
+ }
1774
+ snapshotsByBeacon.get(beaconId).push(snapshot);
1803
1775
  });
1804
- await this.context.walletClient.writeContract(request);
1805
- return new OpenPosition(this.context, this.id, result);
1806
- }
1807
- };
1808
-
1809
- // src/entities/perp-manager.ts
1810
- import { gql as gql2 } from "graphql-request";
1811
- import { parse as parse2 } from "graphql";
1812
- var PerpManager = class {
1813
- constructor(context) {
1814
- this.context = context;
1815
- }
1816
- // READS
1817
- async getPerps() {
1818
- const query = parse2(gql2`
1819
- {
1820
- perps {
1821
- id
1776
+ const perpLookup = new Map(
1777
+ perpResponse.perps.map((p) => [
1778
+ p.id,
1779
+ p
1780
+ ])
1781
+ );
1782
+ const resultMap = /* @__PURE__ */ new Map();
1783
+ for (const perpId of perpIds) {
1784
+ const perp = perpLookup.get(perpId);
1785
+ if (!perp) {
1786
+ throw new Error(`Perp ${perpId} not found`);
1787
+ }
1788
+ const beaconId = perp.beacon.id;
1789
+ const snapshots = snapshotsByPerp.get(perpId) || [];
1790
+ const beaconSnapshots = snapshotsByBeacon.get(beaconId) || [];
1791
+ const contractData = contractDataMap.get(perpId);
1792
+ if (!contractData) {
1793
+ throw new Error(`Contract data for perp ${perpId} not found`);
1794
+ }
1795
+ if (snapshots.length === 0) {
1796
+ throw new Error(`No snapshots found for perp ${perpId}`);
1797
+ }
1798
+ if (beaconSnapshots.length === 0) {
1799
+ throw new Error(`No beacon snapshots found for perp ${perpId}`);
1800
+ }
1801
+ const markTimeSeries = snapshots.map((snapshot) => ({
1802
+ timestamp: Number(snapshot.timestamp),
1803
+ value: Number(snapshot.markPrice)
1804
+ }));
1805
+ const indexTimeSeries = beaconSnapshots.map((snapshot) => ({
1806
+ timestamp: Number(snapshot.timestamp),
1807
+ value: Number(snapshot.indexPrice)
1808
+ }));
1809
+ const openInterestTimeSeries = snapshots.map((snapshot) => ({
1810
+ timestamp: Number(snapshot.timestamp),
1811
+ value: {
1812
+ takerLongNotional: Number(snapshot.takerLongNotional),
1813
+ takerShortNotional: Number(snapshot.takerShortNotional)
1822
1814
  }
1823
- }
1824
- `);
1825
- const response = await this.context.goldskyClient.request(query);
1826
- return response.perps.map(
1827
- (perpData) => new Perp(this.context, perpData.id)
1815
+ }));
1816
+ const fundingRateTimeSeries = snapshots.map((snapshot) => ({
1817
+ timestamp: Number(snapshot.timestamp),
1818
+ value: Number(snapshot.fundingRate)
1819
+ }));
1820
+ const latestSnapshot = snapshots[snapshots.length - 1];
1821
+ const latestBeaconSnapshot = beaconSnapshots[beaconSnapshots.length - 1];
1822
+ const perpData = {
1823
+ id: perpId,
1824
+ tickSpacing: contractData.tickSpacing,
1825
+ mark: sqrtPriceX96ToPrice(contractData.sqrtPriceX96),
1826
+ index: Number(latestBeaconSnapshot.indexPrice),
1827
+ beacon: beaconId,
1828
+ lastIndexUpdate: Number(latestBeaconSnapshot.timestamp),
1829
+ openInterest: {
1830
+ takerLongNotional: Number(latestSnapshot.takerLongNotional),
1831
+ takerShortNotional: Number(latestSnapshot.takerShortNotional)
1832
+ },
1833
+ markTimeSeries,
1834
+ indexTimeSeries,
1835
+ fundingRate: Number(latestSnapshot.fundingRate),
1836
+ bounds: contractData.bounds,
1837
+ fees: contractData.fees,
1838
+ openInterestTimeSeries,
1839
+ fundingRateTimeSeries,
1840
+ totalOpenMakerPnl: 0,
1841
+ totalOpenTakerPnl: 0
1842
+ };
1843
+ resultMap.set(perpId, perpData);
1844
+ }
1845
+ return resultMap;
1846
+ }
1847
+ async fetchMultiplePerpContractData(perpIds) {
1848
+ const results = await Promise.all(
1849
+ perpIds.map(async (perpId) => ({
1850
+ perpId,
1851
+ data: await this.fetchPerpContractData(perpId)
1852
+ }))
1828
1853
  );
1854
+ return new Map(results.map(({ perpId, data }) => [perpId, data]));
1829
1855
  }
1830
- // WRITES
1831
- async createPerp(params) {
1832
- const sqrtPriceX96 = priceToSqrtPriceX96(params.startingPrice);
1833
- const contractParams = {
1834
- startingSqrtPriceX96: sqrtPriceX96,
1835
- beacon: params.beacon
1856
+ async fetchUserData(userAddress) {
1857
+ const [usdcBalance, openPositionsData, closedPositionsData] = await Promise.all([
1858
+ this.walletClient.readContract({
1859
+ address: this.deployments().usdc,
1860
+ abi: erc20Abi2,
1861
+ functionName: "balanceOf",
1862
+ args: [userAddress]
1863
+ }),
1864
+ this.fetchUserOpenPositions(userAddress),
1865
+ this.fetchUserClosedPositions(userAddress)
1866
+ ]);
1867
+ const realizedPnl = closedPositionsData.reduce((acc, position) => acc + position.pnlAtClose, 0);
1868
+ const unrealizedPnl = openPositionsData.reduce(
1869
+ (acc, position) => acc + position.liveDetails.pnl - position.liveDetails.fundingPayment,
1870
+ 0
1871
+ );
1872
+ return {
1873
+ walletAddress: userAddress,
1874
+ usdcBalance: Number(formatUnits(usdcBalance, 6)),
1875
+ openPositions: openPositionsData,
1876
+ closedPositions: closedPositionsData,
1877
+ realizedPnl,
1878
+ unrealizedPnl
1836
1879
  };
1837
- const { result, request } = await this.context.walletClient.simulateContract({
1838
- address: this.context.deployments().perpManager,
1839
- abi: PERP_MANAGER_ABI,
1840
- functionName: "createPerp",
1841
- args: [contractParams],
1842
- account: this.context.walletClient.account
1843
- });
1844
- await this.context.walletClient.writeContract(request);
1845
- return new Perp(this.context, result);
1846
1880
  }
1847
- };
1848
-
1849
- // src/entities/user.ts
1850
- import { erc20Abi as erc20Abi2 } from "viem";
1851
- import { parse as parse3 } from "graphql";
1852
- import { gql as gql3 } from "graphql-request";
1853
- var User = class {
1854
- constructor(context) {
1855
- this.context = context;
1856
- if (!context.walletClient.account) throw new Error("Wallet client account not found");
1857
- this.walletAddress = context.walletClient.account.address;
1858
- }
1859
- async usdcBalance() {
1860
- const result = await this.context.walletClient.readContract({
1861
- address: this.context.deployments().usdc,
1862
- abi: erc20Abi2,
1863
- functionName: "balanceOf",
1864
- args: [this.walletAddress]
1865
- });
1866
- return scaleFrom6Decimals(Number(result));
1867
- }
1868
- async openPositions() {
1869
- const query = parse3(gql3`
1881
+ async fetchUserOpenPositions(userAddress) {
1882
+ const query = parse(`
1870
1883
  query ($holder: Bytes!) {
1871
1884
  openPositions(
1872
1885
  where: { holder: $holder }
1873
1886
  ) {
1874
1887
  perp { id }
1875
1888
  inContractPosId
1889
+ isLong
1890
+ isMaker
1876
1891
  }
1877
1892
  }
1878
1893
  `);
1879
- const response = await this.context.goldskyClient.request(query, { holder: this.walletAddress });
1880
- return response.openPositions.map((position) => new OpenPosition(this.context, position.perp.id, position.inContractPosId));
1894
+ const response = await this.goldskyClient.request(query, { holder: userAddress });
1895
+ const positionsWithDetails = await Promise.all(
1896
+ response.openPositions.map(async (position) => {
1897
+ const positionId = typeof position.inContractPosId === "bigint" ? position.inContractPosId : BigInt(position.inContractPosId);
1898
+ const liveDetails = await this.fetchPositionLiveDetailsFromContract(
1899
+ position.perp.id,
1900
+ positionId
1901
+ );
1902
+ return {
1903
+ perpId: position.perp.id,
1904
+ positionId,
1905
+ isLong: position.isLong,
1906
+ isMaker: position.isMaker,
1907
+ liveDetails
1908
+ };
1909
+ })
1910
+ );
1911
+ return positionsWithDetails;
1881
1912
  }
1882
- async closedPositions() {
1883
- const query = parse3(gql3`
1913
+ async fetchUserClosedPositions(userAddress) {
1914
+ const query = parse(`
1884
1915
  query ($holder: Bytes!) {
1885
1916
  closedPositions(
1886
1917
  where: { holder: $holder }
@@ -1892,7 +1923,7 @@ var User = class {
1892
1923
  }
1893
1924
  }
1894
1925
  `);
1895
- const response = await this.context.goldskyClient.request(query, { holder: this.walletAddress });
1926
+ const response = await this.goldskyClient.request(query, { holder: userAddress });
1896
1927
  return response.closedPositions.map((position) => ({
1897
1928
  perpId: position.perp.id,
1898
1929
  wasMaker: position.wasMaker,
@@ -1900,13 +1931,60 @@ var User = class {
1900
1931
  pnlAtClose: Number(position.pnlAtClose)
1901
1932
  }));
1902
1933
  }
1903
- async realizedPnl() {
1904
- return (await this.closedPositions()).reduce((acc, position) => acc + position.pnlAtClose, 0);
1934
+ async fetchPositionLiveDetailsFromContract(perpId, positionId) {
1935
+ return withErrorHandling(async () => {
1936
+ const result = await this.walletClient.readContract({
1937
+ address: this.deployments().perpManager,
1938
+ abi: PERP_MANAGER_ABI,
1939
+ functionName: "livePositionDetails",
1940
+ args: [perpId, positionId]
1941
+ });
1942
+ return {
1943
+ pnl: Number(formatUnits(result[0], 6)),
1944
+ fundingPayment: Number(formatUnits(result[1], 6)),
1945
+ effectiveMargin: Number(formatUnits(result[2], 6)),
1946
+ isLiquidatable: result[3]
1947
+ };
1948
+ }, `fetchPositionLiveDetailsFromContract for position ${positionId}`);
1905
1949
  }
1906
- async unrealizedPnl() {
1907
- const openPositions = await this.openPositions();
1908
- const liveDetails = await Promise.all(openPositions.map((position) => position.liveDetails()));
1909
- return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
1950
+ /**
1951
+ * Fetch comprehensive user data with all positions in a single batched request
1952
+ */
1953
+ async getUserData(userAddress) {
1954
+ return this.fetchUserData(userAddress);
1955
+ }
1956
+ /**
1957
+ * Fetch open position data with live details
1958
+ */
1959
+ async getOpenPositionData(perpId, positionId) {
1960
+ const query = parse(`
1961
+ query ($perpId: Bytes!, $posId: BigInt!) {
1962
+ openPositions(
1963
+ where: { perp: $perpId, inContractPosId: $posId }
1964
+ first: 1
1965
+ ) {
1966
+ isLong
1967
+ isMaker
1968
+ }
1969
+ }
1970
+ `);
1971
+ const [positionResponse, liveDetails] = await Promise.all([
1972
+ this.goldskyClient.request(query, { perpId, posId: positionId.toString() }),
1973
+ this.fetchPositionLiveDetailsFromContract(perpId, positionId)
1974
+ ]);
1975
+ const position = positionResponse.openPositions[0];
1976
+ if (!position) {
1977
+ throw new Error(
1978
+ `Position not found in GraphQL: perpId=${perpId}, positionId=${positionId}. The position may not exist or may have been closed.`
1979
+ );
1980
+ }
1981
+ return {
1982
+ perpId,
1983
+ positionId,
1984
+ isLong: position.isLong,
1985
+ isMaker: position.isMaker,
1986
+ liveDetails
1987
+ };
1910
1988
  }
1911
1989
  };
1912
1990
 
@@ -2165,27 +2243,511 @@ var BEACON_ABI = [
2165
2243
  "type": "function"
2166
2244
  }
2167
2245
  ];
2246
+
2247
+ // src/functions/open-position.ts
2248
+ import { publicActions as publicActions2, formatUnits as formatUnits2, decodeEventLog } from "viem";
2249
+ var OpenPosition = class _OpenPosition {
2250
+ constructor(context, perpId, positionId, isLong, isMaker) {
2251
+ this.context = context;
2252
+ this.perpId = perpId;
2253
+ this.positionId = positionId;
2254
+ this.isLong = isLong;
2255
+ this.isMaker = isMaker;
2256
+ }
2257
+ async closePosition(params) {
2258
+ return withErrorHandling(async () => {
2259
+ const contractParams = {
2260
+ posId: this.positionId,
2261
+ minAmt0Out: scale6Decimals(params.minAmt0Out),
2262
+ minAmt1Out: scale6Decimals(params.minAmt1Out),
2263
+ maxAmt1In: scale6Decimals(params.maxAmt1In)
2264
+ };
2265
+ const { result, request } = await this.context.walletClient.extend(publicActions2).simulateContract({
2266
+ address: this.context.deployments().perpManager,
2267
+ abi: PERP_MANAGER_ABI,
2268
+ functionName: "closePosition",
2269
+ args: [this.perpId, contractParams],
2270
+ account: this.context.walletClient.account
2271
+ });
2272
+ const txHash = await this.context.walletClient.writeContract(request);
2273
+ const publicClient = this.context.walletClient.extend(publicActions2);
2274
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
2275
+ if (receipt.status === "reverted") {
2276
+ throw new Error(`Transaction reverted. Hash: ${txHash}`);
2277
+ }
2278
+ let newPositionId = result && result !== 0n ? result : null;
2279
+ for (const log of receipt.logs) {
2280
+ try {
2281
+ const closedDecoded = decodeEventLog({
2282
+ abi: PERP_MANAGER_ABI,
2283
+ data: log.data,
2284
+ topics: log.topics,
2285
+ eventName: "PositionClosed"
2286
+ });
2287
+ if (closedDecoded.args.perpId === this.perpId && closedDecoded.args.posId === this.positionId) {
2288
+ newPositionId = null;
2289
+ break;
2290
+ }
2291
+ } catch (e) {
2292
+ continue;
2293
+ }
2294
+ }
2295
+ if (!newPositionId) {
2296
+ return null;
2297
+ }
2298
+ return new _OpenPosition(this.context, this.perpId, newPositionId, this.isLong, this.isMaker);
2299
+ }, `closePosition for ${this.isMaker ? "maker" : "taker"} position ${this.positionId}`);
2300
+ }
2301
+ async liveDetails() {
2302
+ return withErrorHandling(async () => {
2303
+ const result = await this.context.walletClient.readContract({
2304
+ address: this.context.deployments().perpManager,
2305
+ abi: PERP_MANAGER_ABI,
2306
+ functionName: "livePositionDetails",
2307
+ args: [this.perpId, this.positionId]
2308
+ });
2309
+ return {
2310
+ pnl: Number(formatUnits2(result[0], 6)),
2311
+ fundingPayment: Number(formatUnits2(result[1], 6)),
2312
+ effectiveMargin: Number(formatUnits2(result[2], 6)),
2313
+ isLiquidatable: result[3]
2314
+ };
2315
+ }, `liveDetails for position ${this.positionId}`);
2316
+ }
2317
+ };
2318
+
2319
+ // src/functions/perp.ts
2320
+ function getPerpMark(perpData) {
2321
+ return perpData.mark;
2322
+ }
2323
+ function getPerpIndex(perpData) {
2324
+ return perpData.index;
2325
+ }
2326
+ function getPerpBeacon(perpData) {
2327
+ return perpData.beacon;
2328
+ }
2329
+ function getPerpLastIndexUpdate(perpData) {
2330
+ return perpData.lastIndexUpdate;
2331
+ }
2332
+ function getPerpOpenInterest(perpData) {
2333
+ return perpData.openInterest;
2334
+ }
2335
+ function getPerpMarkTimeSeries(perpData) {
2336
+ return perpData.markTimeSeries;
2337
+ }
2338
+ function getPerpIndexTimeSeries(perpData) {
2339
+ return perpData.indexTimeSeries;
2340
+ }
2341
+ function getPerpFundingRate(perpData) {
2342
+ return perpData.fundingRate;
2343
+ }
2344
+ function getPerpBounds(perpData) {
2345
+ return perpData.bounds;
2346
+ }
2347
+ function getPerpFees(perpData) {
2348
+ return perpData.fees;
2349
+ }
2350
+ function getPerpOpenInterestTimeSeries(perpData) {
2351
+ return perpData.openInterestTimeSeries;
2352
+ }
2353
+ function getPerpFundingRateTimeSeries(perpData) {
2354
+ return perpData.fundingRateTimeSeries;
2355
+ }
2356
+ function getPerpTickSpacing(perpData) {
2357
+ return perpData.tickSpacing;
2358
+ }
2359
+ async function getAllMakerPositions(context, perpId) {
2360
+ const query = `
2361
+ query ($perpId: Bytes!) {
2362
+ openPositions(
2363
+ where: { perp: $perpId, isMaker: true }
2364
+ ) {
2365
+ perp { id }
2366
+ inContractPosId
2367
+ isLong
2368
+ isMaker
2369
+ }
2370
+ }
2371
+ `;
2372
+ const response = await context.goldskyClient.request(query, { perpId });
2373
+ return response.openPositions.map(
2374
+ (position) => new OpenPosition(
2375
+ context,
2376
+ position.perp.id,
2377
+ BigInt(position.inContractPosId),
2378
+ position.isLong,
2379
+ position.isMaker
2380
+ )
2381
+ );
2382
+ }
2383
+ async function getAllTakerPositions(context, perpId) {
2384
+ const query = `
2385
+ query ($perpId: Bytes!) {
2386
+ openPositions(
2387
+ where: { perp: $perpId, isMaker: false }
2388
+ ) {
2389
+ perp { id }
2390
+ inContractPosId
2391
+ isLong
2392
+ isMaker
2393
+ }
2394
+ }
2395
+ `;
2396
+ const response = await context.goldskyClient.request(query, { perpId });
2397
+ return response.openPositions.map(
2398
+ (position) => new OpenPosition(
2399
+ context,
2400
+ position.perp.id,
2401
+ BigInt(position.inContractPosId),
2402
+ position.isLong,
2403
+ position.isMaker
2404
+ )
2405
+ );
2406
+ }
2407
+ async function getTotalOpenMakerPnl(context, perpId) {
2408
+ const positions = await getAllMakerPositions(context, perpId);
2409
+ const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
2410
+ return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
2411
+ }
2412
+ async function getTotalOpenTakerPnl(context, perpId) {
2413
+ const positions = await getAllTakerPositions(context, perpId);
2414
+ const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
2415
+ return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
2416
+ }
2417
+
2418
+ // src/functions/perp-manager.ts
2419
+ import { publicActions as publicActions3, decodeEventLog as decodeEventLog2 } from "viem";
2420
+ import { gql } from "graphql-request";
2421
+ import { parse as parse2 } from "graphql";
2422
+ async function getPerps(context) {
2423
+ return withErrorHandling(async () => {
2424
+ const query = parse2(gql`
2425
+ {
2426
+ perps {
2427
+ id
2428
+ }
2429
+ }
2430
+ `);
2431
+ const response = await context.goldskyClient.request(query);
2432
+ return response.perps.map((perpData) => perpData.id);
2433
+ }, "getPerps");
2434
+ }
2435
+ async function createPerp(context, params) {
2436
+ return withErrorHandling(async () => {
2437
+ const sqrtPriceX96 = priceToSqrtPriceX96(params.startingPrice);
2438
+ const contractParams = {
2439
+ startingSqrtPriceX96: sqrtPriceX96,
2440
+ beacon: params.beacon
2441
+ };
2442
+ const { request } = await context.walletClient.simulateContract({
2443
+ address: context.deployments().perpManager,
2444
+ abi: PERP_MANAGER_ABI,
2445
+ functionName: "createPerp",
2446
+ args: [contractParams],
2447
+ account: context.walletClient.account
2448
+ });
2449
+ const txHash = await context.walletClient.writeContract(request);
2450
+ const publicClient = context.walletClient.extend(publicActions3);
2451
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
2452
+ if (receipt.status === "reverted") {
2453
+ throw new Error(`Transaction reverted. Hash: ${txHash}`);
2454
+ }
2455
+ for (const log of receipt.logs) {
2456
+ try {
2457
+ const decoded = decodeEventLog2({
2458
+ abi: PERP_MANAGER_ABI,
2459
+ data: log.data,
2460
+ topics: log.topics,
2461
+ eventName: "PerpCreated"
2462
+ });
2463
+ return decoded.args.perpId;
2464
+ } catch (e) {
2465
+ continue;
2466
+ }
2467
+ }
2468
+ throw new Error("PerpCreated event not found in transaction receipt");
2469
+ }, "createPerp");
2470
+ }
2471
+ async function openTakerPosition(context, perpId, params) {
2472
+ return withErrorHandling(async () => {
2473
+ const marginScaled = scale6Decimals(params.margin);
2474
+ await approveUsdc(context, marginScaled);
2475
+ const levX96 = scaleToX96(params.leverage);
2476
+ const contractParams = {
2477
+ isLong: params.isLong,
2478
+ margin: marginScaled,
2479
+ levX96,
2480
+ unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
2481
+ };
2482
+ const { request } = await context.walletClient.simulateContract({
2483
+ address: context.deployments().perpManager,
2484
+ abi: PERP_MANAGER_ABI,
2485
+ functionName: "openTakerPosition",
2486
+ args: [perpId, contractParams],
2487
+ account: context.walletClient.account
2488
+ });
2489
+ const txHash = await context.walletClient.writeContract(request);
2490
+ const publicClient = context.walletClient.extend(publicActions3);
2491
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
2492
+ if (receipt.status === "reverted") {
2493
+ throw new Error(`Transaction reverted. Hash: ${txHash}`);
2494
+ }
2495
+ let takerPosId = null;
2496
+ for (const log of receipt.logs) {
2497
+ try {
2498
+ const decoded = decodeEventLog2({
2499
+ abi: PERP_MANAGER_ABI,
2500
+ data: log.data,
2501
+ topics: log.topics,
2502
+ eventName: "PositionOpened"
2503
+ });
2504
+ if (decoded.args.perpId === perpId && !decoded.args.isMaker) {
2505
+ takerPosId = decoded.args.posId;
2506
+ break;
2507
+ }
2508
+ } catch (e) {
2509
+ continue;
2510
+ }
2511
+ }
2512
+ if (!takerPosId) {
2513
+ throw new Error(`PositionOpened event not found in transaction receipt. Hash: ${txHash}`);
2514
+ }
2515
+ return new OpenPosition(context, perpId, takerPosId, params.isLong, false);
2516
+ }, "openTakerPosition");
2517
+ }
2518
+ async function openMakerPosition(context, perpId, params) {
2519
+ return withErrorHandling(async () => {
2520
+ const marginScaled = scale6Decimals(params.margin);
2521
+ await approveUsdc(context, marginScaled);
2522
+ const perpData = await context.getPerpData(perpId);
2523
+ const tickLower = priceToTick(params.priceLower, true);
2524
+ const tickUpper = priceToTick(params.priceUpper, false);
2525
+ const tickSpacing = perpData.tickSpacing;
2526
+ const alignedTickLower = Math.floor(tickLower / tickSpacing) * tickSpacing;
2527
+ const alignedTickUpper = Math.ceil(tickUpper / tickSpacing) * tickSpacing;
2528
+ const contractParams = {
2529
+ margin: marginScaled,
2530
+ liquidity: params.liquidity,
2531
+ tickLower: alignedTickLower,
2532
+ tickUpper: alignedTickUpper,
2533
+ maxAmt0In: scale6Decimals(params.maxAmt0In),
2534
+ maxAmt1In: scale6Decimals(params.maxAmt1In)
2535
+ };
2536
+ const { request } = await context.walletClient.simulateContract({
2537
+ address: context.deployments().perpManager,
2538
+ abi: PERP_MANAGER_ABI,
2539
+ functionName: "openMakerPosition",
2540
+ args: [perpId, contractParams],
2541
+ account: context.walletClient.account
2542
+ });
2543
+ const txHash = await context.walletClient.writeContract(request);
2544
+ const publicClient = context.walletClient.extend(publicActions3);
2545
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
2546
+ if (receipt.status === "reverted") {
2547
+ throw new Error(`Transaction reverted. Hash: ${txHash}`);
2548
+ }
2549
+ let makerPosId = null;
2550
+ for (const log of receipt.logs) {
2551
+ try {
2552
+ const decoded = decodeEventLog2({
2553
+ abi: PERP_MANAGER_ABI,
2554
+ data: log.data,
2555
+ topics: log.topics,
2556
+ eventName: "PositionOpened"
2557
+ });
2558
+ if (decoded.args.perpId === perpId && decoded.args.isMaker) {
2559
+ makerPosId = decoded.args.posId;
2560
+ break;
2561
+ }
2562
+ } catch (e) {
2563
+ continue;
2564
+ }
2565
+ }
2566
+ if (!makerPosId) {
2567
+ throw new Error(`PositionOpened event not found in transaction receipt. Hash: ${txHash}`);
2568
+ }
2569
+ return new OpenPosition(context, perpId, makerPosId, void 0, true);
2570
+ }, "openMakerPosition");
2571
+ }
2572
+
2573
+ // src/functions/user.ts
2574
+ function getUserUsdcBalance(userData) {
2575
+ return userData.usdcBalance;
2576
+ }
2577
+ function getUserOpenPositions(userData) {
2578
+ return userData.openPositions;
2579
+ }
2580
+ function getUserClosedPositions(userData) {
2581
+ return userData.closedPositions;
2582
+ }
2583
+ function getUserRealizedPnl(userData) {
2584
+ return userData.realizedPnl;
2585
+ }
2586
+ function getUserUnrealizedPnl(userData) {
2587
+ return userData.unrealizedPnl;
2588
+ }
2589
+ function getUserWalletAddress(userData) {
2590
+ return userData.walletAddress;
2591
+ }
2592
+
2593
+ // src/functions/position.ts
2594
+ import { formatUnits as formatUnits3, decodeEventLog as decodeEventLog3 } from "viem";
2595
+ import { publicActions as publicActions4 } from "viem";
2596
+ function getPositionPerpId(positionData) {
2597
+ return positionData.perpId;
2598
+ }
2599
+ function getPositionId(positionData) {
2600
+ return positionData.positionId;
2601
+ }
2602
+ function getPositionIsLong(positionData) {
2603
+ return positionData.isLong;
2604
+ }
2605
+ function getPositionIsMaker(positionData) {
2606
+ return positionData.isMaker;
2607
+ }
2608
+ function getPositionLiveDetails(positionData) {
2609
+ return positionData.liveDetails;
2610
+ }
2611
+ function getPositionPnl(positionData) {
2612
+ return positionData.liveDetails.pnl;
2613
+ }
2614
+ function getPositionFundingPayment(positionData) {
2615
+ return positionData.liveDetails.fundingPayment;
2616
+ }
2617
+ function getPositionEffectiveMargin(positionData) {
2618
+ return positionData.liveDetails.effectiveMargin;
2619
+ }
2620
+ function getPositionIsLiquidatable(positionData) {
2621
+ return positionData.liveDetails.isLiquidatable;
2622
+ }
2623
+ async function closePosition(context, perpId, positionId, params) {
2624
+ return withErrorHandling(async () => {
2625
+ const contractParams = {
2626
+ posId: positionId,
2627
+ minAmt0Out: scale6Decimals(params.minAmt0Out),
2628
+ minAmt1Out: scale6Decimals(params.minAmt1Out),
2629
+ maxAmt1In: scale6Decimals(params.maxAmt1In)
2630
+ };
2631
+ const { result, request } = await context.walletClient.extend(publicActions4).simulateContract({
2632
+ address: context.deployments().perpManager,
2633
+ abi: PERP_MANAGER_ABI,
2634
+ functionName: "closePosition",
2635
+ args: [perpId, contractParams],
2636
+ account: context.walletClient.account
2637
+ });
2638
+ const txHash = await context.walletClient.writeContract(request);
2639
+ const publicClient = context.walletClient.extend(publicActions4);
2640
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
2641
+ if (receipt.status === "reverted") {
2642
+ throw new Error(`Transaction reverted. Hash: ${txHash}`);
2643
+ }
2644
+ let newPositionId = null;
2645
+ for (const log of receipt.logs) {
2646
+ try {
2647
+ const decoded = decodeEventLog3({
2648
+ abi: PERP_MANAGER_ABI,
2649
+ data: log.data,
2650
+ topics: log.topics,
2651
+ eventName: "PositionOpened"
2652
+ });
2653
+ if (decoded.args.perpId === perpId) {
2654
+ newPositionId = decoded.args.posId;
2655
+ break;
2656
+ }
2657
+ } catch (e) {
2658
+ continue;
2659
+ }
2660
+ }
2661
+ if (!newPositionId) {
2662
+ return null;
2663
+ }
2664
+ return {
2665
+ perpId,
2666
+ positionId: newPositionId,
2667
+ liveDetails: await getPositionLiveDetailsFromContract(context, perpId, newPositionId)
2668
+ };
2669
+ }, `closePosition for position ${positionId}`);
2670
+ }
2671
+ async function getPositionLiveDetailsFromContract(context, perpId, positionId) {
2672
+ return withErrorHandling(async () => {
2673
+ const result = await context.walletClient.readContract({
2674
+ address: context.deployments().perpManager,
2675
+ abi: PERP_MANAGER_ABI,
2676
+ functionName: "livePositionDetails",
2677
+ args: [perpId, positionId]
2678
+ });
2679
+ return {
2680
+ pnl: Number(formatUnits3(result[0], 6)),
2681
+ fundingPayment: Number(formatUnits3(result[1], 6)),
2682
+ effectiveMargin: Number(formatUnits3(result[2], 6)),
2683
+ isLiquidatable: result[3]
2684
+ };
2685
+ }, `getPositionLiveDetailsFromContract for position ${positionId}`);
2686
+ }
2168
2687
  export {
2169
2688
  BEACON_ABI,
2170
2689
  BIGINT_1E6,
2171
- DEPLOYMENTS,
2690
+ ContractError,
2691
+ GraphQLError,
2692
+ InsufficientFundsError,
2172
2693
  NUMBER_1E6,
2173
2694
  OpenPosition,
2174
2695
  PERP_MANAGER_ABI,
2175
- Perp,
2176
2696
  PerpCityContext,
2177
- PerpManager,
2697
+ PerpCityError,
2178
2698
  Q96,
2179
- User,
2699
+ RPCError,
2700
+ TransactionRejectedError,
2701
+ ValidationError,
2180
2702
  approveUsdc,
2703
+ closePosition,
2704
+ createPerp,
2181
2705
  estimateLiquidity,
2706
+ getAllMakerPositions,
2707
+ getAllTakerPositions,
2708
+ getPerpBeacon,
2709
+ getPerpBounds,
2710
+ getPerpFees,
2711
+ getPerpFundingRate,
2712
+ getPerpFundingRateTimeSeries,
2713
+ getPerpIndex,
2714
+ getPerpIndexTimeSeries,
2715
+ getPerpLastIndexUpdate,
2716
+ getPerpMark,
2717
+ getPerpMarkTimeSeries,
2718
+ getPerpOpenInterest,
2719
+ getPerpOpenInterestTimeSeries,
2720
+ getPerpTickSpacing,
2721
+ getPerps,
2722
+ getPositionEffectiveMargin,
2723
+ getPositionFundingPayment,
2724
+ getPositionId,
2725
+ getPositionIsLiquidatable,
2726
+ getPositionIsLong,
2727
+ getPositionIsMaker,
2728
+ getPositionLiveDetails,
2729
+ getPositionLiveDetailsFromContract,
2730
+ getPositionPerpId,
2731
+ getPositionPnl,
2732
+ getTotalOpenMakerPnl,
2733
+ getTotalOpenTakerPnl,
2734
+ getUserClosedPositions,
2735
+ getUserOpenPositions,
2736
+ getUserRealizedPnl,
2737
+ getUserUnrealizedPnl,
2738
+ getUserUsdcBalance,
2739
+ getUserWalletAddress,
2182
2740
  marginRatioToLeverage,
2741
+ openMakerPosition,
2742
+ openTakerPosition,
2743
+ parseContractError,
2183
2744
  priceToSqrtPriceX96,
2184
2745
  priceToTick,
2185
2746
  scale6Decimals,
2186
2747
  scaleFrom6Decimals,
2187
2748
  scaleFromX96,
2188
2749
  scaleToX96,
2189
- sqrtPriceX96ToPrice
2750
+ sqrtPriceX96ToPrice,
2751
+ withErrorHandling
2190
2752
  };
2191
2753
  //# sourceMappingURL=index.mjs.map