@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/LICENSE.md +21 -0
- package/README.md +339 -5
- package/dist/index.d.mts +295 -193
- package/dist/index.d.ts +295 -193
- package/dist/index.js +1057 -452
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1008 -446
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -4
package/dist/index.js
CHANGED
|
@@ -22,74 +22,75 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BEACON_ABI: () => BEACON_ABI,
|
|
24
24
|
BIGINT_1E6: () => BIGINT_1E6,
|
|
25
|
-
|
|
25
|
+
ContractError: () => ContractError,
|
|
26
|
+
GraphQLError: () => GraphQLError,
|
|
27
|
+
InsufficientFundsError: () => InsufficientFundsError,
|
|
26
28
|
NUMBER_1E6: () => NUMBER_1E6,
|
|
27
29
|
OpenPosition: () => OpenPosition,
|
|
28
30
|
PERP_MANAGER_ABI: () => PERP_MANAGER_ABI,
|
|
29
|
-
Perp: () => Perp,
|
|
30
31
|
PerpCityContext: () => PerpCityContext,
|
|
31
|
-
|
|
32
|
+
PerpCityError: () => PerpCityError,
|
|
32
33
|
Q96: () => Q96,
|
|
33
|
-
|
|
34
|
+
RPCError: () => RPCError,
|
|
35
|
+
TransactionRejectedError: () => TransactionRejectedError,
|
|
36
|
+
ValidationError: () => ValidationError,
|
|
34
37
|
approveUsdc: () => approveUsdc,
|
|
38
|
+
closePosition: () => closePosition,
|
|
39
|
+
createPerp: () => createPerp,
|
|
35
40
|
estimateLiquidity: () => estimateLiquidity,
|
|
41
|
+
getAllMakerPositions: () => getAllMakerPositions,
|
|
42
|
+
getAllTakerPositions: () => getAllTakerPositions,
|
|
43
|
+
getPerpBeacon: () => getPerpBeacon,
|
|
44
|
+
getPerpBounds: () => getPerpBounds,
|
|
45
|
+
getPerpFees: () => getPerpFees,
|
|
46
|
+
getPerpFundingRate: () => getPerpFundingRate,
|
|
47
|
+
getPerpFundingRateTimeSeries: () => getPerpFundingRateTimeSeries,
|
|
48
|
+
getPerpIndex: () => getPerpIndex,
|
|
49
|
+
getPerpIndexTimeSeries: () => getPerpIndexTimeSeries,
|
|
50
|
+
getPerpLastIndexUpdate: () => getPerpLastIndexUpdate,
|
|
51
|
+
getPerpMark: () => getPerpMark,
|
|
52
|
+
getPerpMarkTimeSeries: () => getPerpMarkTimeSeries,
|
|
53
|
+
getPerpOpenInterest: () => getPerpOpenInterest,
|
|
54
|
+
getPerpOpenInterestTimeSeries: () => getPerpOpenInterestTimeSeries,
|
|
55
|
+
getPerpTickSpacing: () => getPerpTickSpacing,
|
|
56
|
+
getPerps: () => getPerps,
|
|
57
|
+
getPositionEffectiveMargin: () => getPositionEffectiveMargin,
|
|
58
|
+
getPositionFundingPayment: () => getPositionFundingPayment,
|
|
59
|
+
getPositionId: () => getPositionId,
|
|
60
|
+
getPositionIsLiquidatable: () => getPositionIsLiquidatable,
|
|
61
|
+
getPositionIsLong: () => getPositionIsLong,
|
|
62
|
+
getPositionIsMaker: () => getPositionIsMaker,
|
|
63
|
+
getPositionLiveDetails: () => getPositionLiveDetails,
|
|
64
|
+
getPositionLiveDetailsFromContract: () => getPositionLiveDetailsFromContract,
|
|
65
|
+
getPositionPerpId: () => getPositionPerpId,
|
|
66
|
+
getPositionPnl: () => getPositionPnl,
|
|
67
|
+
getTotalOpenMakerPnl: () => getTotalOpenMakerPnl,
|
|
68
|
+
getTotalOpenTakerPnl: () => getTotalOpenTakerPnl,
|
|
69
|
+
getUserClosedPositions: () => getUserClosedPositions,
|
|
70
|
+
getUserOpenPositions: () => getUserOpenPositions,
|
|
71
|
+
getUserRealizedPnl: () => getUserRealizedPnl,
|
|
72
|
+
getUserUnrealizedPnl: () => getUserUnrealizedPnl,
|
|
73
|
+
getUserUsdcBalance: () => getUserUsdcBalance,
|
|
74
|
+
getUserWalletAddress: () => getUserWalletAddress,
|
|
36
75
|
marginRatioToLeverage: () => marginRatioToLeverage,
|
|
76
|
+
openMakerPosition: () => openMakerPosition,
|
|
77
|
+
openTakerPosition: () => openTakerPosition,
|
|
78
|
+
parseContractError: () => parseContractError,
|
|
37
79
|
priceToSqrtPriceX96: () => priceToSqrtPriceX96,
|
|
38
80
|
priceToTick: () => priceToTick,
|
|
39
81
|
scale6Decimals: () => scale6Decimals,
|
|
40
82
|
scaleFrom6Decimals: () => scaleFrom6Decimals,
|
|
41
83
|
scaleFromX96: () => scaleFromX96,
|
|
42
84
|
scaleToX96: () => scaleToX96,
|
|
43
|
-
sqrtPriceX96ToPrice: () => sqrtPriceX96ToPrice
|
|
85
|
+
sqrtPriceX96ToPrice: () => sqrtPriceX96ToPrice,
|
|
86
|
+
withErrorHandling: () => withErrorHandling
|
|
44
87
|
});
|
|
45
88
|
module.exports = __toCommonJS(index_exports);
|
|
46
89
|
|
|
47
90
|
// src/context.ts
|
|
48
91
|
var import_graphql_request = require("graphql-request");
|
|
49
|
-
|
|
50
|
-
// src/deployments.ts
|
|
51
|
-
var DEPLOYMENTS = {
|
|
52
|
-
// Base Sepolia
|
|
53
|
-
[84532]: {
|
|
54
|
-
perpManager: "0x59F1766b77fd67af6c80217C2025A0D536998000",
|
|
55
|
-
usdc: "0xC1a5D4E99BB224713dd179eA9CA2Fa6600706210",
|
|
56
|
-
goldskyPublic: "https://api.goldsky.com/api/public/project_cmbawn40q70fj01ws4jmsfj7f/subgraphs/perp-city/36ac28e6-20250925_150813/gn",
|
|
57
|
-
goldskyPrivate: "https://api.goldsky.com/api/private/project_cmbawn40q70fj01ws4jmsfj7f/subgraphs/perp-city/36ac28e6-20250925_150813/gn"
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// src/context.ts
|
|
62
|
-
var import_viem = require("viem");
|
|
63
|
-
var PerpCityContext = class {
|
|
64
|
-
constructor(config) {
|
|
65
|
-
this.walletClient = config.walletClient.extend(import_viem.publicActions);
|
|
66
|
-
const chainId = this.validateChainId();
|
|
67
|
-
const deployments = DEPLOYMENTS[chainId];
|
|
68
|
-
const headers = {};
|
|
69
|
-
let goldskyEndpoint;
|
|
70
|
-
if (config.goldskyBearerToken) {
|
|
71
|
-
headers.authorization = `Bearer ${config.goldskyBearerToken}`;
|
|
72
|
-
goldskyEndpoint = deployments.goldskyPrivate;
|
|
73
|
-
} else {
|
|
74
|
-
goldskyEndpoint = deployments.goldskyPublic;
|
|
75
|
-
}
|
|
76
|
-
this.goldskyClient = new import_graphql_request.GraphQLClient(goldskyEndpoint, {
|
|
77
|
-
headers
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
validateChainId() {
|
|
81
|
-
const chainId = this.walletClient.chain?.id;
|
|
82
|
-
if (!chainId) throw new Error(`Chain ID is not set.`);
|
|
83
|
-
if (!DEPLOYMENTS[chainId]) throw new Error(`Unsupported chainId: ${chainId}.`);
|
|
84
|
-
return chainId;
|
|
85
|
-
}
|
|
86
|
-
deployments() {
|
|
87
|
-
return DEPLOYMENTS[this.validateChainId()];
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// src/entities/openPosition.ts
|
|
92
92
|
var import_viem3 = require("viem");
|
|
93
|
+
var import_graphql = require("graphql");
|
|
93
94
|
|
|
94
95
|
// src/utils/constants.ts
|
|
95
96
|
var NUMBER_1E6 = 1e6;
|
|
@@ -136,13 +137,13 @@ function scaleFrom6Decimals(value) {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// src/utils/approve.ts
|
|
139
|
-
var
|
|
140
|
+
var import_viem = require("viem");
|
|
140
141
|
var DEFAULT_CONFIRMATIONS = 2;
|
|
141
142
|
async function approveUsdc(context, amount, confirmations = DEFAULT_CONFIRMATIONS) {
|
|
142
143
|
const deployments = context.deployments();
|
|
143
144
|
const { request } = await context.walletClient.simulateContract({
|
|
144
145
|
address: deployments.usdc,
|
|
145
|
-
abi:
|
|
146
|
+
abi: import_viem.erc20Abi,
|
|
146
147
|
functionName: "approve",
|
|
147
148
|
args: [deployments.perpManager, amount],
|
|
148
149
|
account: context.walletClient.account
|
|
@@ -1482,451 +1483,524 @@ async function estimateLiquidity(context, tickLower, tickUpper, usdScaled) {
|
|
|
1482
1483
|
});
|
|
1483
1484
|
}
|
|
1484
1485
|
|
|
1485
|
-
// src/
|
|
1486
|
-
var
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
this.
|
|
1491
|
-
|
|
1492
|
-
async closePosition(params) {
|
|
1493
|
-
const contractParams = {
|
|
1494
|
-
posId: this.positionId,
|
|
1495
|
-
minAmt0Out: scale6Decimals(params.minAmt0Out),
|
|
1496
|
-
minAmt1Out: scale6Decimals(params.minAmt1Out),
|
|
1497
|
-
maxAmt1In: scale6Decimals(params.maxAmt1In)
|
|
1498
|
-
};
|
|
1499
|
-
const { result, request } = await this.context.walletClient.extend(import_viem3.publicActions).simulateContract({
|
|
1500
|
-
address: this.context.deployments().perpManager,
|
|
1501
|
-
abi: PERP_MANAGER_ABI,
|
|
1502
|
-
functionName: "closePosition",
|
|
1503
|
-
args: [this.perpId, contractParams],
|
|
1504
|
-
account: this.context.walletClient.account
|
|
1505
|
-
});
|
|
1506
|
-
await this.context.walletClient.writeContract(request);
|
|
1507
|
-
return result === null ? null : new _OpenPosition(this.context, this.perpId, result);
|
|
1486
|
+
// src/utils/errors.ts
|
|
1487
|
+
var import_viem2 = require("viem");
|
|
1488
|
+
var PerpCityError = class extends Error {
|
|
1489
|
+
constructor(message, cause) {
|
|
1490
|
+
super(message);
|
|
1491
|
+
this.cause = cause;
|
|
1492
|
+
this.name = "PerpCityError";
|
|
1508
1493
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
});
|
|
1517
|
-
return {
|
|
1518
|
-
pnl: scaleFrom6Decimals(Number(result[0])),
|
|
1519
|
-
fundingPayment: scaleFrom6Decimals(Number(result[1])),
|
|
1520
|
-
effectiveMargin: scaleFrom6Decimals(Number(result[2])),
|
|
1521
|
-
isLiquidatable: result[3]
|
|
1522
|
-
};
|
|
1494
|
+
};
|
|
1495
|
+
var ContractError = class extends PerpCityError {
|
|
1496
|
+
constructor(message, errorName, args, cause) {
|
|
1497
|
+
super(message, cause);
|
|
1498
|
+
this.errorName = errorName;
|
|
1499
|
+
this.args = args;
|
|
1500
|
+
this.name = "ContractError";
|
|
1523
1501
|
}
|
|
1524
1502
|
};
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
var import_graphql = require("graphql");
|
|
1530
|
-
var Perp = class {
|
|
1531
|
-
constructor(context, id) {
|
|
1532
|
-
this.context = context;
|
|
1533
|
-
this.id = id;
|
|
1503
|
+
var TransactionRejectedError = class extends PerpCityError {
|
|
1504
|
+
constructor(message = "Transaction rejected by user", cause) {
|
|
1505
|
+
super(message, cause);
|
|
1506
|
+
this.name = "TransactionRejectedError";
|
|
1534
1507
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
functionName: "tickSpacing",
|
|
1541
|
-
args: [this.id]
|
|
1542
|
-
});
|
|
1508
|
+
};
|
|
1509
|
+
var InsufficientFundsError = class extends PerpCityError {
|
|
1510
|
+
constructor(message = "Insufficient funds for transaction", cause) {
|
|
1511
|
+
super(message, cause);
|
|
1512
|
+
this.name = "InsufficientFundsError";
|
|
1543
1513
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
args: [this.id]
|
|
1550
|
-
});
|
|
1551
|
-
return sqrtPriceX96ToPrice(sqrtPriceX96);
|
|
1514
|
+
};
|
|
1515
|
+
var GraphQLError = class extends PerpCityError {
|
|
1516
|
+
constructor(message, cause) {
|
|
1517
|
+
super(message, cause);
|
|
1518
|
+
this.name = "GraphQLError";
|
|
1552
1519
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
first: 1
|
|
1559
|
-
orderBy: timestamp
|
|
1560
|
-
orderDirection: desc
|
|
1561
|
-
where: { beacon: $beaconAddr }
|
|
1562
|
-
) { indexPrice }
|
|
1563
|
-
}
|
|
1564
|
-
`);
|
|
1565
|
-
const response = await this.context.goldskyClient.request(query, { beaconAddr: beacon });
|
|
1566
|
-
return Number(response.beaconSnapshots[0].indexPrice);
|
|
1520
|
+
};
|
|
1521
|
+
var RPCError = class extends PerpCityError {
|
|
1522
|
+
constructor(message, cause) {
|
|
1523
|
+
super(message, cause);
|
|
1524
|
+
this.name = "RPCError";
|
|
1567
1525
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
`);
|
|
1576
|
-
const response = await this.context.goldskyClient.request(query, { perpId: this.id });
|
|
1577
|
-
return response.perp.beacon.id;
|
|
1526
|
+
};
|
|
1527
|
+
var ValidationError = class extends PerpCityError {
|
|
1528
|
+
constructor(message, cause) {
|
|
1529
|
+
super(message, cause);
|
|
1530
|
+
this.name = "ValidationError";
|
|
1578
1531
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
beaconSnapshots(
|
|
1584
|
-
first: 1
|
|
1585
|
-
orderBy: timestamp
|
|
1586
|
-
orderDirection: desc
|
|
1587
|
-
where: { beacon: $beaconAddr }
|
|
1588
|
-
) { timestamp }
|
|
1589
|
-
}
|
|
1590
|
-
`);
|
|
1591
|
-
const response = await this.context.goldskyClient.request(query, { beaconAddr: beacon });
|
|
1592
|
-
return Number(response.beaconSnapshots[0].timestamp);
|
|
1532
|
+
};
|
|
1533
|
+
function parseContractError(error) {
|
|
1534
|
+
if (error instanceof PerpCityError) {
|
|
1535
|
+
return error;
|
|
1593
1536
|
}
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
return {
|
|
1610
|
-
takerLongNotional: Number(response.perpSnapshots[0].takerLongNotional),
|
|
1611
|
-
takerShortNotional: Number(response.perpSnapshots[0].takerShortNotional)
|
|
1612
|
-
};
|
|
1537
|
+
if (error instanceof import_viem2.BaseError) {
|
|
1538
|
+
const revertError = error.walk((err) => err instanceof import_viem2.ContractFunctionRevertedError);
|
|
1539
|
+
if (revertError instanceof import_viem2.ContractFunctionRevertedError) {
|
|
1540
|
+
const errorName = revertError.data?.errorName ?? "Unknown";
|
|
1541
|
+
const args = revertError.data?.args ?? [];
|
|
1542
|
+
const message = formatContractError(errorName, args);
|
|
1543
|
+
return new ContractError(message, errorName, args, error);
|
|
1544
|
+
}
|
|
1545
|
+
if (error.message?.includes("User rejected") || error.code === 4001) {
|
|
1546
|
+
return new TransactionRejectedError(error.message, error);
|
|
1547
|
+
}
|
|
1548
|
+
if (error.message?.includes("insufficient funds")) {
|
|
1549
|
+
return new InsufficientFundsError(error.message, error);
|
|
1550
|
+
}
|
|
1551
|
+
return new PerpCityError(error.shortMessage || error.message, error);
|
|
1613
1552
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
query ($perpId: Bytes!) {
|
|
1617
|
-
perpSnapshots(
|
|
1618
|
-
orderBy: timestamp
|
|
1619
|
-
orderDirection: asc
|
|
1620
|
-
where: { perp: $perpId }
|
|
1621
|
-
) {
|
|
1622
|
-
timestamp
|
|
1623
|
-
markPrice
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
`);
|
|
1627
|
-
const response = await this.context.goldskyClient.request(query, { perpId: this.id });
|
|
1628
|
-
return response.perpSnapshots.map((snapshot) => ({
|
|
1629
|
-
timestamp: Number(snapshot.timestamp),
|
|
1630
|
-
value: Number(snapshot.markPrice)
|
|
1631
|
-
}));
|
|
1553
|
+
if (error instanceof Error) {
|
|
1554
|
+
return new PerpCityError(error.message, error);
|
|
1632
1555
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1556
|
+
return new PerpCityError(String(error));
|
|
1557
|
+
}
|
|
1558
|
+
function formatContractError(errorName, args) {
|
|
1559
|
+
switch (errorName) {
|
|
1560
|
+
case "InvalidBeaconAddress":
|
|
1561
|
+
return `Invalid beacon address: ${args[0]}`;
|
|
1562
|
+
case "InvalidTradingFeeSplits":
|
|
1563
|
+
return `Invalid trading fee splits. Insurance split: ${args[0]}, Creator split: ${args[1]}`;
|
|
1564
|
+
case "InvalidMaxOpeningLev":
|
|
1565
|
+
return `Invalid maximum opening leverage: ${args[0]}`;
|
|
1566
|
+
case "InvalidLiquidationLev":
|
|
1567
|
+
return `Invalid liquidation leverage: ${args[0]}. Must be less than max opening leverage: ${args[1]}`;
|
|
1568
|
+
case "InvalidLiquidationFee":
|
|
1569
|
+
return `Invalid liquidation fee: ${args[0]}`;
|
|
1570
|
+
case "InvalidLiquidatorFeeSplit":
|
|
1571
|
+
return `Invalid liquidator fee split: ${args[0]}`;
|
|
1572
|
+
case "InvalidClose":
|
|
1573
|
+
return `Cannot close position. Caller: ${args[0]}, Holder: ${args[1]}, Is Liquidated: ${args[2]}`;
|
|
1574
|
+
case "InvalidCaller":
|
|
1575
|
+
return `Invalid caller. Expected: ${args[1]}, Got: ${args[0]}`;
|
|
1576
|
+
case "InvalidLiquidity":
|
|
1577
|
+
return `Invalid liquidity amount: ${args[0]}`;
|
|
1578
|
+
case "InvalidMargin":
|
|
1579
|
+
return `Invalid margin amount: ${args[0]}`;
|
|
1580
|
+
case "InvalidLevX96":
|
|
1581
|
+
return `Invalid leverage: ${args[0]}. Maximum allowed: ${args[1]}`;
|
|
1582
|
+
case "MakerPositionLocked":
|
|
1583
|
+
return `Maker position is locked until ${new Date(Number(args[1]) * 1e3).toISOString()}. Current time: ${new Date(Number(args[0]) * 1e3).toISOString()}`;
|
|
1584
|
+
case "MaximumAmountExceeded":
|
|
1585
|
+
return `Maximum amount exceeded. Maximum: ${args[0]}, Requested: ${args[1]}`;
|
|
1586
|
+
case "MinimumAmountInsufficient":
|
|
1587
|
+
return `Minimum amount not met. Required: ${args[0]}, Received: ${args[1]}`;
|
|
1588
|
+
case "PriceImpactTooHigh":
|
|
1589
|
+
return `Price impact too high. Current price: ${args[0]}, Min acceptable: ${args[1]}, Max acceptable: ${args[2]}`;
|
|
1590
|
+
case "SwapReverted":
|
|
1591
|
+
return "Swap failed. This may be due to insufficient liquidity or slippage tolerance.";
|
|
1592
|
+
case "ZeroSizePosition":
|
|
1593
|
+
return `Cannot create zero-size position. Perp delta: ${args[0]}, USD delta: ${args[1]}`;
|
|
1594
|
+
case "InvalidFundingInterval":
|
|
1595
|
+
return `Invalid funding interval: ${args[0]}`;
|
|
1596
|
+
case "InvalidPriceImpactBand":
|
|
1597
|
+
return `Invalid price impact band: ${args[0]}`;
|
|
1598
|
+
case "InvalidMarketDeathThreshold":
|
|
1599
|
+
return `Invalid market death threshold: ${args[0]}`;
|
|
1600
|
+
case "InvalidTickRange":
|
|
1601
|
+
return `Invalid tick range. Lower: ${args[0]}, Upper: ${args[1]}`;
|
|
1602
|
+
case "MarketNotKillable":
|
|
1603
|
+
return `Market health (${args[0]}) is above death threshold (${args[1]}). Market cannot be killed yet.`;
|
|
1604
|
+
case "InvalidStartingSqrtPriceX96":
|
|
1605
|
+
return `Invalid starting sqrt price: ${args[0]}`;
|
|
1606
|
+
default:
|
|
1607
|
+
return `Contract error: ${errorName}${args.length > 0 ? ` (${args.join(", ")})` : ""}`;
|
|
1652
1608
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
) {
|
|
1662
|
-
fundingRate
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
`);
|
|
1666
|
-
const response = await this.context.goldskyClient.request(query, { perpId: this.id });
|
|
1667
|
-
return Number(response.perpSnapshots[0].fundingRate);
|
|
1609
|
+
}
|
|
1610
|
+
async function withErrorHandling(fn, context) {
|
|
1611
|
+
try {
|
|
1612
|
+
return await fn();
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
const parsedError = parseContractError(error);
|
|
1615
|
+
parsedError.message = `${context}: ${parsedError.message}`;
|
|
1616
|
+
throw parsedError;
|
|
1668
1617
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/context.ts
|
|
1621
|
+
var import_viem4 = require("viem");
|
|
1622
|
+
var PerpCityContext = class {
|
|
1623
|
+
constructor(config) {
|
|
1624
|
+
this.walletClient = config.walletClient.extend(import_viem3.publicActions);
|
|
1625
|
+
this._deployments = config.deployments;
|
|
1626
|
+
const headers = {};
|
|
1627
|
+
if (config.goldskyBearerToken) {
|
|
1628
|
+
headers.authorization = `Bearer ${config.goldskyBearerToken}`;
|
|
1629
|
+
}
|
|
1630
|
+
this.goldskyClient = new import_graphql_request.GraphQLClient(config.goldskyEndpoint, {
|
|
1631
|
+
headers
|
|
1675
1632
|
});
|
|
1676
|
-
return {
|
|
1677
|
-
minMargin: Number(result[0]),
|
|
1678
|
-
minTakerLeverage: marginRatioToLeverage(result[5]),
|
|
1679
|
-
maxTakerLeverage: marginRatioToLeverage(result[4])
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
// TODO
|
|
1683
|
-
async maxTakerNotional(isLong) {
|
|
1684
|
-
return 0;
|
|
1685
1633
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
isLong: params.isLong,
|
|
1689
|
-
margin: scale6Decimals(params.margin),
|
|
1690
|
-
levX96: scaleToX96(params.leverage),
|
|
1691
|
-
unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
|
|
1692
|
-
};
|
|
1693
|
-
const { result, request } = await this.context.walletClient.simulateContract({
|
|
1694
|
-
address: this.context.deployments().perpManager,
|
|
1695
|
-
abi: PERP_MANAGER_ABI,
|
|
1696
|
-
functionName: "quoteTakerPosition",
|
|
1697
|
-
args: [this.id, contractParams],
|
|
1698
|
-
account: this.context.walletClient.account
|
|
1699
|
-
});
|
|
1700
|
-
return {
|
|
1701
|
-
success: result[0],
|
|
1702
|
-
size: scaleFrom6Decimals(Math.abs(Number(result[1]))),
|
|
1703
|
-
notional: scaleFrom6Decimals(Math.abs(Number(result[2]))),
|
|
1704
|
-
creatorFeeAmt: scaleFrom6Decimals(Number(result[3])),
|
|
1705
|
-
insuranceFeeAmt: scaleFrom6Decimals(Number(result[4])),
|
|
1706
|
-
lpFeeAmt: scaleFrom6Decimals(Number(result[5]))
|
|
1707
|
-
};
|
|
1634
|
+
deployments() {
|
|
1635
|
+
return this._deployments;
|
|
1708
1636
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1637
|
+
// Optimized batch data fetching methods
|
|
1638
|
+
async fetchPerpData(perpId) {
|
|
1639
|
+
return withErrorHandling(async () => {
|
|
1640
|
+
const perpQuery = (0, import_graphql.parse)(`
|
|
1641
|
+
query ($perpId: Bytes!) {
|
|
1642
|
+
perp(id: $perpId) {
|
|
1643
|
+
beacon { id }
|
|
1644
|
+
}
|
|
1645
|
+
perpSnapshots(
|
|
1646
|
+
orderBy: timestamp
|
|
1647
|
+
orderDirection: asc
|
|
1648
|
+
where: { perp: $perpId }
|
|
1649
|
+
) {
|
|
1650
|
+
timestamp
|
|
1651
|
+
markPrice
|
|
1652
|
+
takerLongNotional
|
|
1653
|
+
takerShortNotional
|
|
1654
|
+
fundingRate
|
|
1655
|
+
}
|
|
1717
1656
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
perp { id }
|
|
1730
|
-
inContractPosId
|
|
1657
|
+
`);
|
|
1658
|
+
const beaconQuery = (0, import_graphql.parse)(`
|
|
1659
|
+
query ($beaconAddr: Bytes!) {
|
|
1660
|
+
beaconSnapshots(
|
|
1661
|
+
orderBy: timestamp
|
|
1662
|
+
orderDirection: asc
|
|
1663
|
+
where: { beacon: $beaconAddr }
|
|
1664
|
+
) {
|
|
1665
|
+
timestamp
|
|
1666
|
+
indexPrice
|
|
1667
|
+
}
|
|
1731
1668
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1669
|
+
`);
|
|
1670
|
+
let perpResponse;
|
|
1671
|
+
try {
|
|
1672
|
+
perpResponse = await this.goldskyClient.request(perpQuery, { perpId });
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
throw new GraphQLError(`Failed to fetch perp data for ${perpId}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
|
|
1675
|
+
}
|
|
1676
|
+
if (!perpResponse.perp || !perpResponse.perp.beacon) {
|
|
1677
|
+
throw new GraphQLError(`Perp ${perpId} not found or has no beacon`);
|
|
1678
|
+
}
|
|
1679
|
+
let beaconResponse;
|
|
1680
|
+
let contractData;
|
|
1681
|
+
try {
|
|
1682
|
+
[beaconResponse, contractData] = await Promise.all([
|
|
1683
|
+
this.goldskyClient.request(beaconQuery, { beaconAddr: perpResponse.perp.beacon.id }),
|
|
1684
|
+
this.fetchPerpContractData(perpId)
|
|
1685
|
+
]);
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
if (error instanceof GraphQLError || error instanceof RPCError) {
|
|
1688
|
+
throw error;
|
|
1689
|
+
}
|
|
1690
|
+
throw new GraphQLError(`Failed to fetch beacon or contract data: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
|
|
1691
|
+
}
|
|
1692
|
+
if (perpResponse.perpSnapshots.length === 0) {
|
|
1693
|
+
throw new GraphQLError(`No perpSnapshots found for perp ${perpId}. This perp may be newly created with no trading activity yet.`);
|
|
1694
|
+
}
|
|
1695
|
+
if (beaconResponse.beaconSnapshots.length === 0) {
|
|
1696
|
+
throw new GraphQLError(`No beaconSnapshots found for perp ${perpId} beacon ${perpResponse.perp.beacon.id}. The beacon may not have any price updates yet.`);
|
|
1697
|
+
}
|
|
1698
|
+
const markTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
|
|
1699
|
+
timestamp: Number(snapshot.timestamp),
|
|
1700
|
+
value: Number(snapshot.markPrice)
|
|
1701
|
+
}));
|
|
1702
|
+
const indexTimeSeries = beaconResponse.beaconSnapshots.map((snapshot) => ({
|
|
1703
|
+
timestamp: Number(snapshot.timestamp),
|
|
1704
|
+
value: Number(snapshot.indexPrice)
|
|
1705
|
+
}));
|
|
1706
|
+
const openInterestTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
|
|
1707
|
+
timestamp: Number(snapshot.timestamp),
|
|
1708
|
+
value: {
|
|
1709
|
+
takerLongNotional: Number(snapshot.takerLongNotional),
|
|
1710
|
+
takerShortNotional: Number(snapshot.takerShortNotional)
|
|
1711
|
+
}
|
|
1712
|
+
}));
|
|
1713
|
+
const fundingRateTimeSeries = perpResponse.perpSnapshots.map((snapshot) => ({
|
|
1714
|
+
timestamp: Number(snapshot.timestamp),
|
|
1715
|
+
value: Number(snapshot.fundingRate)
|
|
1716
|
+
}));
|
|
1717
|
+
const latestSnapshot = perpResponse.perpSnapshots[perpResponse.perpSnapshots.length - 1];
|
|
1718
|
+
const latestBeaconSnapshot = beaconResponse.beaconSnapshots[beaconResponse.beaconSnapshots.length - 1];
|
|
1719
|
+
const perpData = {
|
|
1720
|
+
id: perpId,
|
|
1721
|
+
tickSpacing: contractData.tickSpacing,
|
|
1722
|
+
mark: sqrtPriceX96ToPrice(contractData.sqrtPriceX96),
|
|
1723
|
+
index: Number(latestBeaconSnapshot.indexPrice),
|
|
1724
|
+
beacon: perpResponse.perp.beacon.id,
|
|
1725
|
+
lastIndexUpdate: Number(latestBeaconSnapshot.timestamp),
|
|
1726
|
+
openInterest: {
|
|
1727
|
+
takerLongNotional: Number(latestSnapshot.takerLongNotional),
|
|
1728
|
+
takerShortNotional: Number(latestSnapshot.takerShortNotional)
|
|
1729
|
+
},
|
|
1730
|
+
markTimeSeries,
|
|
1731
|
+
indexTimeSeries,
|
|
1732
|
+
fundingRate: Number(latestSnapshot.fundingRate),
|
|
1733
|
+
bounds: contractData.bounds,
|
|
1734
|
+
fees: contractData.fees,
|
|
1735
|
+
openInterestTimeSeries,
|
|
1736
|
+
fundingRateTimeSeries,
|
|
1737
|
+
totalOpenMakerPnl: 0,
|
|
1738
|
+
// These will be calculated by functions
|
|
1739
|
+
totalOpenTakerPnl: 0
|
|
1740
|
+
// These will be calculated by functions
|
|
1741
|
+
};
|
|
1742
|
+
return perpData;
|
|
1743
|
+
}, `fetchPerpData for perp ${perpId}`);
|
|
1741
1744
|
}
|
|
1742
|
-
async
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1745
|
+
async fetchPerpContractData(perpId) {
|
|
1746
|
+
return withErrorHandling(async () => {
|
|
1747
|
+
const [tickSpacing, sqrtPriceX96, boundsRaw, feesRaw] = await Promise.all([
|
|
1748
|
+
this.walletClient.readContract({
|
|
1749
|
+
address: this.deployments().perpManager,
|
|
1750
|
+
abi: PERP_MANAGER_ABI,
|
|
1751
|
+
functionName: "tickSpacing",
|
|
1752
|
+
args: [perpId]
|
|
1753
|
+
}),
|
|
1754
|
+
this.walletClient.readContract({
|
|
1755
|
+
address: this.deployments().perpManager,
|
|
1756
|
+
abi: PERP_MANAGER_ABI,
|
|
1757
|
+
functionName: "sqrtPriceX96",
|
|
1758
|
+
args: [perpId]
|
|
1759
|
+
}),
|
|
1760
|
+
this.walletClient.readContract({
|
|
1761
|
+
address: this.deployments().perpManager,
|
|
1762
|
+
abi: PERP_MANAGER_ABI,
|
|
1763
|
+
functionName: "tradingBounds",
|
|
1764
|
+
args: [perpId]
|
|
1765
|
+
}),
|
|
1766
|
+
this.walletClient.readContract({
|
|
1767
|
+
address: this.deployments().perpManager,
|
|
1768
|
+
abi: PERP_MANAGER_ABI,
|
|
1769
|
+
functionName: "fees",
|
|
1770
|
+
args: [perpId]
|
|
1771
|
+
})
|
|
1772
|
+
]);
|
|
1773
|
+
const bounds = boundsRaw;
|
|
1774
|
+
const fees = feesRaw;
|
|
1775
|
+
return {
|
|
1776
|
+
tickSpacing: Number(tickSpacing),
|
|
1777
|
+
sqrtPriceX96,
|
|
1778
|
+
bounds: {
|
|
1779
|
+
minMargin: Number((0, import_viem3.formatUnits)(bounds[0], 6)),
|
|
1780
|
+
minTakerLeverage: marginRatioToLeverage(Number((0, import_viem3.formatUnits)(bounds[4], 6))),
|
|
1781
|
+
maxTakerLeverage: marginRatioToLeverage(Number((0, import_viem3.formatUnits)(bounds[5], 6)))
|
|
1782
|
+
},
|
|
1783
|
+
fees: {
|
|
1784
|
+
creatorFee: Number((0, import_viem3.formatUnits)(fees[0], 6)),
|
|
1785
|
+
insuranceFee: Number((0, import_viem3.formatUnits)(fees[1], 6)),
|
|
1786
|
+
lpFee: Number((0, import_viem3.formatUnits)(fees[2], 6)),
|
|
1787
|
+
liquidationFee: Number((0, import_viem3.formatUnits)(fees[3], 6))
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
}, `fetchPerpContractData for perp ${perpId}`);
|
|
1746
1791
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
args: [this.id]
|
|
1753
|
-
});
|
|
1754
|
-
return {
|
|
1755
|
-
creatorFee: scaleFrom6Decimals(result[0]),
|
|
1756
|
-
insuranceFee: scaleFrom6Decimals(result[1]),
|
|
1757
|
-
lpFee: scaleFrom6Decimals(result[2]),
|
|
1758
|
-
liquidationFee: scaleFrom6Decimals(result[3])
|
|
1759
|
-
};
|
|
1792
|
+
/**
|
|
1793
|
+
* Fetch comprehensive perp data with all related information in a single batched request
|
|
1794
|
+
*/
|
|
1795
|
+
async getPerpData(perpId) {
|
|
1796
|
+
return this.fetchPerpData(perpId);
|
|
1760
1797
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1798
|
+
/**
|
|
1799
|
+
* Fetch data for multiple perps efficiently with true batching
|
|
1800
|
+
* This fetches all perps in just 2 Goldsky requests total (not 2N!)
|
|
1801
|
+
*/
|
|
1802
|
+
async getMultiplePerpData(perpIds) {
|
|
1803
|
+
if (perpIds.length === 0) {
|
|
1804
|
+
return /* @__PURE__ */ new Map();
|
|
1805
|
+
}
|
|
1806
|
+
if (perpIds.length === 1) {
|
|
1807
|
+
const data = await this.fetchPerpData(perpIds[0]);
|
|
1808
|
+
return /* @__PURE__ */ new Map([[perpIds[0], data]]);
|
|
1809
|
+
}
|
|
1810
|
+
const batchPerpQuery = (0, import_graphql.parse)(`
|
|
1811
|
+
query ($perpIds: [Bytes!]!) {
|
|
1812
|
+
perps(where: { id_in: $perpIds }) {
|
|
1813
|
+
id
|
|
1814
|
+
beacon { id }
|
|
1815
|
+
}
|
|
1764
1816
|
perpSnapshots(
|
|
1765
1817
|
orderBy: timestamp
|
|
1766
1818
|
orderDirection: asc
|
|
1767
|
-
where: {
|
|
1819
|
+
where: { perp_in: $perpIds }
|
|
1768
1820
|
) {
|
|
1821
|
+
perp { id }
|
|
1769
1822
|
timestamp
|
|
1823
|
+
markPrice
|
|
1770
1824
|
takerLongNotional
|
|
1771
1825
|
takerShortNotional
|
|
1826
|
+
fundingRate
|
|
1772
1827
|
}
|
|
1773
1828
|
}
|
|
1774
1829
|
`);
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
takerShortNotional: Number(snapshot.takerShortNotional)
|
|
1781
|
-
}
|
|
1782
|
-
}));
|
|
1783
|
-
}
|
|
1784
|
-
async fundingRateTimeSeries() {
|
|
1785
|
-
const query = (0, import_graphql.parse)(import_graphql_request2.gql`
|
|
1786
|
-
query ($perpId: Bytes!) {
|
|
1787
|
-
perpSnapshots(
|
|
1830
|
+
const perpResponse = await this.goldskyClient.request(batchPerpQuery, { perpIds });
|
|
1831
|
+
const beaconIds = [...new Set(perpResponse.perps.map((p) => p.beacon.id))];
|
|
1832
|
+
const batchBeaconQuery = (0, import_graphql.parse)(`
|
|
1833
|
+
query ($beaconIds: [Bytes!]!) {
|
|
1834
|
+
beaconSnapshots(
|
|
1788
1835
|
orderBy: timestamp
|
|
1789
1836
|
orderDirection: asc
|
|
1790
|
-
where: {
|
|
1837
|
+
where: { beacon_in: $beaconIds }
|
|
1791
1838
|
) {
|
|
1839
|
+
beacon { id }
|
|
1792
1840
|
timestamp
|
|
1793
|
-
|
|
1841
|
+
indexPrice
|
|
1794
1842
|
}
|
|
1795
1843
|
}
|
|
1796
1844
|
`);
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
async approveAndOpenTakerPosition(params) {
|
|
1809
|
-
await approveUsdc(this.context, scale6Decimals(params.margin));
|
|
1810
|
-
return await this.openTakerPosition(params);
|
|
1811
|
-
}
|
|
1812
|
-
async openMakerPosition(params) {
|
|
1813
|
-
const deployments = this.context.deployments();
|
|
1814
|
-
const tickSpacing = await this.tickSpacing();
|
|
1815
|
-
const scaledUsd = scale6Decimals(params.margin);
|
|
1816
|
-
const tickLower = (0, import_v3_sdk.nearestUsableTick)(priceToTick(params.priceLower, true), tickSpacing);
|
|
1817
|
-
const tickUpper = (0, import_v3_sdk.nearestUsableTick)(priceToTick(params.priceUpper, false), tickSpacing);
|
|
1818
|
-
const contractParams = {
|
|
1819
|
-
margin: scaledUsd,
|
|
1820
|
-
liquidity: await estimateLiquidity(this.context, tickLower, tickUpper, scaledUsd),
|
|
1821
|
-
tickLower,
|
|
1822
|
-
tickUpper,
|
|
1823
|
-
maxAmt0In: scale6Decimals(params.maxAmt0In),
|
|
1824
|
-
maxAmt1In: scale6Decimals(params.maxAmt1In)
|
|
1825
|
-
};
|
|
1826
|
-
const { result, request } = await this.context.walletClient.simulateContract({
|
|
1827
|
-
address: deployments.perpManager,
|
|
1828
|
-
abi: PERP_MANAGER_ABI,
|
|
1829
|
-
functionName: "openMakerPosition",
|
|
1830
|
-
args: [this.id, contractParams],
|
|
1831
|
-
account: this.context.walletClient.account
|
|
1845
|
+
const [beaconResponse, contractDataMap] = await Promise.all([
|
|
1846
|
+
this.goldskyClient.request(batchBeaconQuery, { beaconIds }),
|
|
1847
|
+
this.fetchMultiplePerpContractData(perpIds)
|
|
1848
|
+
]);
|
|
1849
|
+
const snapshotsByPerp = /* @__PURE__ */ new Map();
|
|
1850
|
+
perpResponse.perpSnapshots.forEach((snapshot) => {
|
|
1851
|
+
const perpId = snapshot.perp.id;
|
|
1852
|
+
if (!snapshotsByPerp.has(perpId)) {
|
|
1853
|
+
snapshotsByPerp.set(perpId, []);
|
|
1854
|
+
}
|
|
1855
|
+
snapshotsByPerp.get(perpId).push(snapshot);
|
|
1832
1856
|
});
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
levX96: scaleToX96(params.leverage),
|
|
1841
|
-
unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
|
|
1842
|
-
};
|
|
1843
|
-
const { result, request } = await this.context.walletClient.simulateContract({
|
|
1844
|
-
address: this.context.deployments().perpManager,
|
|
1845
|
-
abi: PERP_MANAGER_ABI,
|
|
1846
|
-
functionName: "openTakerPosition",
|
|
1847
|
-
args: [this.id, contractParams],
|
|
1848
|
-
account: this.context.walletClient.account
|
|
1857
|
+
const snapshotsByBeacon = /* @__PURE__ */ new Map();
|
|
1858
|
+
beaconResponse.beaconSnapshots.forEach((snapshot) => {
|
|
1859
|
+
const beaconId = snapshot.beacon.id;
|
|
1860
|
+
if (!snapshotsByBeacon.has(beaconId)) {
|
|
1861
|
+
snapshotsByBeacon.set(beaconId, []);
|
|
1862
|
+
}
|
|
1863
|
+
snapshotsByBeacon.get(beaconId).push(snapshot);
|
|
1849
1864
|
});
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1865
|
+
const perpLookup = new Map(
|
|
1866
|
+
perpResponse.perps.map((p) => [
|
|
1867
|
+
p.id,
|
|
1868
|
+
p
|
|
1869
|
+
])
|
|
1870
|
+
);
|
|
1871
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1872
|
+
for (const perpId of perpIds) {
|
|
1873
|
+
const perp = perpLookup.get(perpId);
|
|
1874
|
+
if (!perp) {
|
|
1875
|
+
throw new Error(`Perp ${perpId} not found`);
|
|
1876
|
+
}
|
|
1877
|
+
const beaconId = perp.beacon.id;
|
|
1878
|
+
const snapshots = snapshotsByPerp.get(perpId) || [];
|
|
1879
|
+
const beaconSnapshots = snapshotsByBeacon.get(beaconId) || [];
|
|
1880
|
+
const contractData = contractDataMap.get(perpId);
|
|
1881
|
+
if (!contractData) {
|
|
1882
|
+
throw new Error(`Contract data for perp ${perpId} not found`);
|
|
1883
|
+
}
|
|
1884
|
+
if (snapshots.length === 0) {
|
|
1885
|
+
throw new Error(`No snapshots found for perp ${perpId}`);
|
|
1886
|
+
}
|
|
1887
|
+
if (beaconSnapshots.length === 0) {
|
|
1888
|
+
throw new Error(`No beacon snapshots found for perp ${perpId}`);
|
|
1889
|
+
}
|
|
1890
|
+
const markTimeSeries = snapshots.map((snapshot) => ({
|
|
1891
|
+
timestamp: Number(snapshot.timestamp),
|
|
1892
|
+
value: Number(snapshot.markPrice)
|
|
1893
|
+
}));
|
|
1894
|
+
const indexTimeSeries = beaconSnapshots.map((snapshot) => ({
|
|
1895
|
+
timestamp: Number(snapshot.timestamp),
|
|
1896
|
+
value: Number(snapshot.indexPrice)
|
|
1897
|
+
}));
|
|
1898
|
+
const openInterestTimeSeries = snapshots.map((snapshot) => ({
|
|
1899
|
+
timestamp: Number(snapshot.timestamp),
|
|
1900
|
+
value: {
|
|
1901
|
+
takerLongNotional: Number(snapshot.takerLongNotional),
|
|
1902
|
+
takerShortNotional: Number(snapshot.takerShortNotional)
|
|
1868
1903
|
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1904
|
+
}));
|
|
1905
|
+
const fundingRateTimeSeries = snapshots.map((snapshot) => ({
|
|
1906
|
+
timestamp: Number(snapshot.timestamp),
|
|
1907
|
+
value: Number(snapshot.fundingRate)
|
|
1908
|
+
}));
|
|
1909
|
+
const latestSnapshot = snapshots[snapshots.length - 1];
|
|
1910
|
+
const latestBeaconSnapshot = beaconSnapshots[beaconSnapshots.length - 1];
|
|
1911
|
+
const perpData = {
|
|
1912
|
+
id: perpId,
|
|
1913
|
+
tickSpacing: contractData.tickSpacing,
|
|
1914
|
+
mark: sqrtPriceX96ToPrice(contractData.sqrtPriceX96),
|
|
1915
|
+
index: Number(latestBeaconSnapshot.indexPrice),
|
|
1916
|
+
beacon: beaconId,
|
|
1917
|
+
lastIndexUpdate: Number(latestBeaconSnapshot.timestamp),
|
|
1918
|
+
openInterest: {
|
|
1919
|
+
takerLongNotional: Number(latestSnapshot.takerLongNotional),
|
|
1920
|
+
takerShortNotional: Number(latestSnapshot.takerShortNotional)
|
|
1921
|
+
},
|
|
1922
|
+
markTimeSeries,
|
|
1923
|
+
indexTimeSeries,
|
|
1924
|
+
fundingRate: Number(latestSnapshot.fundingRate),
|
|
1925
|
+
bounds: contractData.bounds,
|
|
1926
|
+
fees: contractData.fees,
|
|
1927
|
+
openInterestTimeSeries,
|
|
1928
|
+
fundingRateTimeSeries,
|
|
1929
|
+
totalOpenMakerPnl: 0,
|
|
1930
|
+
totalOpenTakerPnl: 0
|
|
1931
|
+
};
|
|
1932
|
+
resultMap.set(perpId, perpData);
|
|
1933
|
+
}
|
|
1934
|
+
return resultMap;
|
|
1935
|
+
}
|
|
1936
|
+
async fetchMultiplePerpContractData(perpIds) {
|
|
1937
|
+
const results = await Promise.all(
|
|
1938
|
+
perpIds.map(async (perpId) => ({
|
|
1939
|
+
perpId,
|
|
1940
|
+
data: await this.fetchPerpContractData(perpId)
|
|
1941
|
+
}))
|
|
1874
1942
|
);
|
|
1943
|
+
return new Map(results.map(({ perpId, data }) => [perpId, data]));
|
|
1875
1944
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1945
|
+
async fetchUserData(userAddress) {
|
|
1946
|
+
const [usdcBalance, openPositionsData, closedPositionsData] = await Promise.all([
|
|
1947
|
+
this.walletClient.readContract({
|
|
1948
|
+
address: this.deployments().usdc,
|
|
1949
|
+
abi: import_viem4.erc20Abi,
|
|
1950
|
+
functionName: "balanceOf",
|
|
1951
|
+
args: [userAddress]
|
|
1952
|
+
}),
|
|
1953
|
+
this.fetchUserOpenPositions(userAddress),
|
|
1954
|
+
this.fetchUserClosedPositions(userAddress)
|
|
1955
|
+
]);
|
|
1956
|
+
const realizedPnl = closedPositionsData.reduce((acc, position) => acc + position.pnlAtClose, 0);
|
|
1957
|
+
const unrealizedPnl = openPositionsData.reduce(
|
|
1958
|
+
(acc, position) => acc + position.liveDetails.pnl - position.liveDetails.fundingPayment,
|
|
1959
|
+
0
|
|
1960
|
+
);
|
|
1961
|
+
return {
|
|
1962
|
+
walletAddress: userAddress,
|
|
1963
|
+
usdcBalance: Number((0, import_viem3.formatUnits)(usdcBalance, 6)),
|
|
1964
|
+
openPositions: openPositionsData,
|
|
1965
|
+
closedPositions: closedPositionsData,
|
|
1966
|
+
realizedPnl,
|
|
1967
|
+
unrealizedPnl
|
|
1882
1968
|
};
|
|
1883
|
-
const { result, request } = await this.context.walletClient.simulateContract({
|
|
1884
|
-
address: this.context.deployments().perpManager,
|
|
1885
|
-
abi: PERP_MANAGER_ABI,
|
|
1886
|
-
functionName: "createPerp",
|
|
1887
|
-
args: [contractParams],
|
|
1888
|
-
account: this.context.walletClient.account
|
|
1889
|
-
});
|
|
1890
|
-
await this.context.walletClient.writeContract(request);
|
|
1891
|
-
return new Perp(this.context, result);
|
|
1892
|
-
}
|
|
1893
|
-
};
|
|
1894
|
-
|
|
1895
|
-
// src/entities/user.ts
|
|
1896
|
-
var import_viem4 = require("viem");
|
|
1897
|
-
var import_graphql3 = require("graphql");
|
|
1898
|
-
var import_graphql_request4 = require("graphql-request");
|
|
1899
|
-
var User = class {
|
|
1900
|
-
constructor(context) {
|
|
1901
|
-
this.context = context;
|
|
1902
|
-
if (!context.walletClient.account) throw new Error("Wallet client account not found");
|
|
1903
|
-
this.walletAddress = context.walletClient.account.address;
|
|
1904
|
-
}
|
|
1905
|
-
async usdcBalance() {
|
|
1906
|
-
const result = await this.context.walletClient.readContract({
|
|
1907
|
-
address: this.context.deployments().usdc,
|
|
1908
|
-
abi: import_viem4.erc20Abi,
|
|
1909
|
-
functionName: "balanceOf",
|
|
1910
|
-
args: [this.walletAddress]
|
|
1911
|
-
});
|
|
1912
|
-
return scaleFrom6Decimals(Number(result));
|
|
1913
1969
|
}
|
|
1914
|
-
async
|
|
1915
|
-
const query = (0,
|
|
1970
|
+
async fetchUserOpenPositions(userAddress) {
|
|
1971
|
+
const query = (0, import_graphql.parse)(`
|
|
1916
1972
|
query ($holder: Bytes!) {
|
|
1917
1973
|
openPositions(
|
|
1918
1974
|
where: { holder: $holder }
|
|
1919
1975
|
) {
|
|
1920
1976
|
perp { id }
|
|
1921
1977
|
inContractPosId
|
|
1978
|
+
isLong
|
|
1979
|
+
isMaker
|
|
1922
1980
|
}
|
|
1923
1981
|
}
|
|
1924
1982
|
`);
|
|
1925
|
-
const response = await this.
|
|
1926
|
-
|
|
1983
|
+
const response = await this.goldskyClient.request(query, { holder: userAddress });
|
|
1984
|
+
const positionsWithDetails = await Promise.all(
|
|
1985
|
+
response.openPositions.map(async (position) => {
|
|
1986
|
+
const positionId = typeof position.inContractPosId === "bigint" ? position.inContractPosId : BigInt(position.inContractPosId);
|
|
1987
|
+
const liveDetails = await this.fetchPositionLiveDetailsFromContract(
|
|
1988
|
+
position.perp.id,
|
|
1989
|
+
positionId
|
|
1990
|
+
);
|
|
1991
|
+
return {
|
|
1992
|
+
perpId: position.perp.id,
|
|
1993
|
+
positionId,
|
|
1994
|
+
isLong: position.isLong,
|
|
1995
|
+
isMaker: position.isMaker,
|
|
1996
|
+
liveDetails
|
|
1997
|
+
};
|
|
1998
|
+
})
|
|
1999
|
+
);
|
|
2000
|
+
return positionsWithDetails;
|
|
1927
2001
|
}
|
|
1928
|
-
async
|
|
1929
|
-
const query = (0,
|
|
2002
|
+
async fetchUserClosedPositions(userAddress) {
|
|
2003
|
+
const query = (0, import_graphql.parse)(`
|
|
1930
2004
|
query ($holder: Bytes!) {
|
|
1931
2005
|
closedPositions(
|
|
1932
2006
|
where: { holder: $holder }
|
|
@@ -1938,7 +2012,7 @@ var User = class {
|
|
|
1938
2012
|
}
|
|
1939
2013
|
}
|
|
1940
2014
|
`);
|
|
1941
|
-
const response = await this.
|
|
2015
|
+
const response = await this.goldskyClient.request(query, { holder: userAddress });
|
|
1942
2016
|
return response.closedPositions.map((position) => ({
|
|
1943
2017
|
perpId: position.perp.id,
|
|
1944
2018
|
wasMaker: position.wasMaker,
|
|
@@ -1946,13 +2020,60 @@ var User = class {
|
|
|
1946
2020
|
pnlAtClose: Number(position.pnlAtClose)
|
|
1947
2021
|
}));
|
|
1948
2022
|
}
|
|
1949
|
-
async
|
|
1950
|
-
return (
|
|
2023
|
+
async fetchPositionLiveDetailsFromContract(perpId, positionId) {
|
|
2024
|
+
return withErrorHandling(async () => {
|
|
2025
|
+
const result = await this.walletClient.readContract({
|
|
2026
|
+
address: this.deployments().perpManager,
|
|
2027
|
+
abi: PERP_MANAGER_ABI,
|
|
2028
|
+
functionName: "livePositionDetails",
|
|
2029
|
+
args: [perpId, positionId]
|
|
2030
|
+
});
|
|
2031
|
+
return {
|
|
2032
|
+
pnl: Number((0, import_viem3.formatUnits)(result[0], 6)),
|
|
2033
|
+
fundingPayment: Number((0, import_viem3.formatUnits)(result[1], 6)),
|
|
2034
|
+
effectiveMargin: Number((0, import_viem3.formatUnits)(result[2], 6)),
|
|
2035
|
+
isLiquidatable: result[3]
|
|
2036
|
+
};
|
|
2037
|
+
}, `fetchPositionLiveDetailsFromContract for position ${positionId}`);
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Fetch comprehensive user data with all positions in a single batched request
|
|
2041
|
+
*/
|
|
2042
|
+
async getUserData(userAddress) {
|
|
2043
|
+
return this.fetchUserData(userAddress);
|
|
1951
2044
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2045
|
+
/**
|
|
2046
|
+
* Fetch open position data with live details
|
|
2047
|
+
*/
|
|
2048
|
+
async getOpenPositionData(perpId, positionId) {
|
|
2049
|
+
const query = (0, import_graphql.parse)(`
|
|
2050
|
+
query ($perpId: Bytes!, $posId: BigInt!) {
|
|
2051
|
+
openPositions(
|
|
2052
|
+
where: { perp: $perpId, inContractPosId: $posId }
|
|
2053
|
+
first: 1
|
|
2054
|
+
) {
|
|
2055
|
+
isLong
|
|
2056
|
+
isMaker
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
`);
|
|
2060
|
+
const [positionResponse, liveDetails] = await Promise.all([
|
|
2061
|
+
this.goldskyClient.request(query, { perpId, posId: positionId.toString() }),
|
|
2062
|
+
this.fetchPositionLiveDetailsFromContract(perpId, positionId)
|
|
2063
|
+
]);
|
|
2064
|
+
const position = positionResponse.openPositions[0];
|
|
2065
|
+
if (!position) {
|
|
2066
|
+
throw new Error(
|
|
2067
|
+
`Position not found in GraphQL: perpId=${perpId}, positionId=${positionId}. The position may not exist or may have been closed.`
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
perpId,
|
|
2072
|
+
positionId,
|
|
2073
|
+
isLong: position.isLong,
|
|
2074
|
+
isMaker: position.isMaker,
|
|
2075
|
+
liveDetails
|
|
2076
|
+
};
|
|
1956
2077
|
}
|
|
1957
2078
|
};
|
|
1958
2079
|
|
|
@@ -2211,28 +2332,512 @@ var BEACON_ABI = [
|
|
|
2211
2332
|
"type": "function"
|
|
2212
2333
|
}
|
|
2213
2334
|
];
|
|
2335
|
+
|
|
2336
|
+
// src/functions/open-position.ts
|
|
2337
|
+
var import_viem5 = require("viem");
|
|
2338
|
+
var OpenPosition = class _OpenPosition {
|
|
2339
|
+
constructor(context, perpId, positionId, isLong, isMaker) {
|
|
2340
|
+
this.context = context;
|
|
2341
|
+
this.perpId = perpId;
|
|
2342
|
+
this.positionId = positionId;
|
|
2343
|
+
this.isLong = isLong;
|
|
2344
|
+
this.isMaker = isMaker;
|
|
2345
|
+
}
|
|
2346
|
+
async closePosition(params) {
|
|
2347
|
+
return withErrorHandling(async () => {
|
|
2348
|
+
const contractParams = {
|
|
2349
|
+
posId: this.positionId,
|
|
2350
|
+
minAmt0Out: scale6Decimals(params.minAmt0Out),
|
|
2351
|
+
minAmt1Out: scale6Decimals(params.minAmt1Out),
|
|
2352
|
+
maxAmt1In: scale6Decimals(params.maxAmt1In)
|
|
2353
|
+
};
|
|
2354
|
+
const { result, request } = await this.context.walletClient.extend(import_viem5.publicActions).simulateContract({
|
|
2355
|
+
address: this.context.deployments().perpManager,
|
|
2356
|
+
abi: PERP_MANAGER_ABI,
|
|
2357
|
+
functionName: "closePosition",
|
|
2358
|
+
args: [this.perpId, contractParams],
|
|
2359
|
+
account: this.context.walletClient.account
|
|
2360
|
+
});
|
|
2361
|
+
const txHash = await this.context.walletClient.writeContract(request);
|
|
2362
|
+
const publicClient = this.context.walletClient.extend(import_viem5.publicActions);
|
|
2363
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2364
|
+
if (receipt.status === "reverted") {
|
|
2365
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
2366
|
+
}
|
|
2367
|
+
let newPositionId = result && result !== 0n ? result : null;
|
|
2368
|
+
for (const log of receipt.logs) {
|
|
2369
|
+
try {
|
|
2370
|
+
const closedDecoded = (0, import_viem5.decodeEventLog)({
|
|
2371
|
+
abi: PERP_MANAGER_ABI,
|
|
2372
|
+
data: log.data,
|
|
2373
|
+
topics: log.topics,
|
|
2374
|
+
eventName: "PositionClosed"
|
|
2375
|
+
});
|
|
2376
|
+
if (closedDecoded.args.perpId === this.perpId && closedDecoded.args.posId === this.positionId) {
|
|
2377
|
+
newPositionId = null;
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
} catch (e) {
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (!newPositionId) {
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
return new _OpenPosition(this.context, this.perpId, newPositionId, this.isLong, this.isMaker);
|
|
2388
|
+
}, `closePosition for ${this.isMaker ? "maker" : "taker"} position ${this.positionId}`);
|
|
2389
|
+
}
|
|
2390
|
+
async liveDetails() {
|
|
2391
|
+
return withErrorHandling(async () => {
|
|
2392
|
+
const result = await this.context.walletClient.readContract({
|
|
2393
|
+
address: this.context.deployments().perpManager,
|
|
2394
|
+
abi: PERP_MANAGER_ABI,
|
|
2395
|
+
functionName: "livePositionDetails",
|
|
2396
|
+
args: [this.perpId, this.positionId]
|
|
2397
|
+
});
|
|
2398
|
+
return {
|
|
2399
|
+
pnl: Number((0, import_viem5.formatUnits)(result[0], 6)),
|
|
2400
|
+
fundingPayment: Number((0, import_viem5.formatUnits)(result[1], 6)),
|
|
2401
|
+
effectiveMargin: Number((0, import_viem5.formatUnits)(result[2], 6)),
|
|
2402
|
+
isLiquidatable: result[3]
|
|
2403
|
+
};
|
|
2404
|
+
}, `liveDetails for position ${this.positionId}`);
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
// src/functions/perp.ts
|
|
2409
|
+
function getPerpMark(perpData) {
|
|
2410
|
+
return perpData.mark;
|
|
2411
|
+
}
|
|
2412
|
+
function getPerpIndex(perpData) {
|
|
2413
|
+
return perpData.index;
|
|
2414
|
+
}
|
|
2415
|
+
function getPerpBeacon(perpData) {
|
|
2416
|
+
return perpData.beacon;
|
|
2417
|
+
}
|
|
2418
|
+
function getPerpLastIndexUpdate(perpData) {
|
|
2419
|
+
return perpData.lastIndexUpdate;
|
|
2420
|
+
}
|
|
2421
|
+
function getPerpOpenInterest(perpData) {
|
|
2422
|
+
return perpData.openInterest;
|
|
2423
|
+
}
|
|
2424
|
+
function getPerpMarkTimeSeries(perpData) {
|
|
2425
|
+
return perpData.markTimeSeries;
|
|
2426
|
+
}
|
|
2427
|
+
function getPerpIndexTimeSeries(perpData) {
|
|
2428
|
+
return perpData.indexTimeSeries;
|
|
2429
|
+
}
|
|
2430
|
+
function getPerpFundingRate(perpData) {
|
|
2431
|
+
return perpData.fundingRate;
|
|
2432
|
+
}
|
|
2433
|
+
function getPerpBounds(perpData) {
|
|
2434
|
+
return perpData.bounds;
|
|
2435
|
+
}
|
|
2436
|
+
function getPerpFees(perpData) {
|
|
2437
|
+
return perpData.fees;
|
|
2438
|
+
}
|
|
2439
|
+
function getPerpOpenInterestTimeSeries(perpData) {
|
|
2440
|
+
return perpData.openInterestTimeSeries;
|
|
2441
|
+
}
|
|
2442
|
+
function getPerpFundingRateTimeSeries(perpData) {
|
|
2443
|
+
return perpData.fundingRateTimeSeries;
|
|
2444
|
+
}
|
|
2445
|
+
function getPerpTickSpacing(perpData) {
|
|
2446
|
+
return perpData.tickSpacing;
|
|
2447
|
+
}
|
|
2448
|
+
async function getAllMakerPositions(context, perpId) {
|
|
2449
|
+
const query = `
|
|
2450
|
+
query ($perpId: Bytes!) {
|
|
2451
|
+
openPositions(
|
|
2452
|
+
where: { perp: $perpId, isMaker: true }
|
|
2453
|
+
) {
|
|
2454
|
+
perp { id }
|
|
2455
|
+
inContractPosId
|
|
2456
|
+
isLong
|
|
2457
|
+
isMaker
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
`;
|
|
2461
|
+
const response = await context.goldskyClient.request(query, { perpId });
|
|
2462
|
+
return response.openPositions.map(
|
|
2463
|
+
(position) => new OpenPosition(
|
|
2464
|
+
context,
|
|
2465
|
+
position.perp.id,
|
|
2466
|
+
BigInt(position.inContractPosId),
|
|
2467
|
+
position.isLong,
|
|
2468
|
+
position.isMaker
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
async function getAllTakerPositions(context, perpId) {
|
|
2473
|
+
const query = `
|
|
2474
|
+
query ($perpId: Bytes!) {
|
|
2475
|
+
openPositions(
|
|
2476
|
+
where: { perp: $perpId, isMaker: false }
|
|
2477
|
+
) {
|
|
2478
|
+
perp { id }
|
|
2479
|
+
inContractPosId
|
|
2480
|
+
isLong
|
|
2481
|
+
isMaker
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
`;
|
|
2485
|
+
const response = await context.goldskyClient.request(query, { perpId });
|
|
2486
|
+
return response.openPositions.map(
|
|
2487
|
+
(position) => new OpenPosition(
|
|
2488
|
+
context,
|
|
2489
|
+
position.perp.id,
|
|
2490
|
+
BigInt(position.inContractPosId),
|
|
2491
|
+
position.isLong,
|
|
2492
|
+
position.isMaker
|
|
2493
|
+
)
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
async function getTotalOpenMakerPnl(context, perpId) {
|
|
2497
|
+
const positions = await getAllMakerPositions(context, perpId);
|
|
2498
|
+
const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
|
|
2499
|
+
return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
|
|
2500
|
+
}
|
|
2501
|
+
async function getTotalOpenTakerPnl(context, perpId) {
|
|
2502
|
+
const positions = await getAllTakerPositions(context, perpId);
|
|
2503
|
+
const liveDetails = await Promise.all(positions.map((position) => position.liveDetails()));
|
|
2504
|
+
return liveDetails.reduce((acc, detail) => acc + detail.pnl - detail.fundingPayment, 0);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/functions/perp-manager.ts
|
|
2508
|
+
var import_viem6 = require("viem");
|
|
2509
|
+
var import_graphql_request2 = require("graphql-request");
|
|
2510
|
+
var import_graphql2 = require("graphql");
|
|
2511
|
+
async function getPerps(context) {
|
|
2512
|
+
return withErrorHandling(async () => {
|
|
2513
|
+
const query = (0, import_graphql2.parse)(import_graphql_request2.gql`
|
|
2514
|
+
{
|
|
2515
|
+
perps {
|
|
2516
|
+
id
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
`);
|
|
2520
|
+
const response = await context.goldskyClient.request(query);
|
|
2521
|
+
return response.perps.map((perpData) => perpData.id);
|
|
2522
|
+
}, "getPerps");
|
|
2523
|
+
}
|
|
2524
|
+
async function createPerp(context, params) {
|
|
2525
|
+
return withErrorHandling(async () => {
|
|
2526
|
+
const sqrtPriceX96 = priceToSqrtPriceX96(params.startingPrice);
|
|
2527
|
+
const contractParams = {
|
|
2528
|
+
startingSqrtPriceX96: sqrtPriceX96,
|
|
2529
|
+
beacon: params.beacon
|
|
2530
|
+
};
|
|
2531
|
+
const { request } = await context.walletClient.simulateContract({
|
|
2532
|
+
address: context.deployments().perpManager,
|
|
2533
|
+
abi: PERP_MANAGER_ABI,
|
|
2534
|
+
functionName: "createPerp",
|
|
2535
|
+
args: [contractParams],
|
|
2536
|
+
account: context.walletClient.account
|
|
2537
|
+
});
|
|
2538
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
2539
|
+
const publicClient = context.walletClient.extend(import_viem6.publicActions);
|
|
2540
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2541
|
+
if (receipt.status === "reverted") {
|
|
2542
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
2543
|
+
}
|
|
2544
|
+
for (const log of receipt.logs) {
|
|
2545
|
+
try {
|
|
2546
|
+
const decoded = (0, import_viem6.decodeEventLog)({
|
|
2547
|
+
abi: PERP_MANAGER_ABI,
|
|
2548
|
+
data: log.data,
|
|
2549
|
+
topics: log.topics,
|
|
2550
|
+
eventName: "PerpCreated"
|
|
2551
|
+
});
|
|
2552
|
+
return decoded.args.perpId;
|
|
2553
|
+
} catch (e) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
throw new Error("PerpCreated event not found in transaction receipt");
|
|
2558
|
+
}, "createPerp");
|
|
2559
|
+
}
|
|
2560
|
+
async function openTakerPosition(context, perpId, params) {
|
|
2561
|
+
return withErrorHandling(async () => {
|
|
2562
|
+
const marginScaled = scale6Decimals(params.margin);
|
|
2563
|
+
await approveUsdc(context, marginScaled);
|
|
2564
|
+
const levX96 = scaleToX96(params.leverage);
|
|
2565
|
+
const contractParams = {
|
|
2566
|
+
isLong: params.isLong,
|
|
2567
|
+
margin: marginScaled,
|
|
2568
|
+
levX96,
|
|
2569
|
+
unspecifiedAmountLimit: scale6Decimals(params.unspecifiedAmountLimit)
|
|
2570
|
+
};
|
|
2571
|
+
const { request } = await context.walletClient.simulateContract({
|
|
2572
|
+
address: context.deployments().perpManager,
|
|
2573
|
+
abi: PERP_MANAGER_ABI,
|
|
2574
|
+
functionName: "openTakerPosition",
|
|
2575
|
+
args: [perpId, contractParams],
|
|
2576
|
+
account: context.walletClient.account
|
|
2577
|
+
});
|
|
2578
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
2579
|
+
const publicClient = context.walletClient.extend(import_viem6.publicActions);
|
|
2580
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2581
|
+
if (receipt.status === "reverted") {
|
|
2582
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
2583
|
+
}
|
|
2584
|
+
let takerPosId = null;
|
|
2585
|
+
for (const log of receipt.logs) {
|
|
2586
|
+
try {
|
|
2587
|
+
const decoded = (0, import_viem6.decodeEventLog)({
|
|
2588
|
+
abi: PERP_MANAGER_ABI,
|
|
2589
|
+
data: log.data,
|
|
2590
|
+
topics: log.topics,
|
|
2591
|
+
eventName: "PositionOpened"
|
|
2592
|
+
});
|
|
2593
|
+
if (decoded.args.perpId === perpId && !decoded.args.isMaker) {
|
|
2594
|
+
takerPosId = decoded.args.posId;
|
|
2595
|
+
break;
|
|
2596
|
+
}
|
|
2597
|
+
} catch (e) {
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
if (!takerPosId) {
|
|
2602
|
+
throw new Error(`PositionOpened event not found in transaction receipt. Hash: ${txHash}`);
|
|
2603
|
+
}
|
|
2604
|
+
return new OpenPosition(context, perpId, takerPosId, params.isLong, false);
|
|
2605
|
+
}, "openTakerPosition");
|
|
2606
|
+
}
|
|
2607
|
+
async function openMakerPosition(context, perpId, params) {
|
|
2608
|
+
return withErrorHandling(async () => {
|
|
2609
|
+
const marginScaled = scale6Decimals(params.margin);
|
|
2610
|
+
await approveUsdc(context, marginScaled);
|
|
2611
|
+
const perpData = await context.getPerpData(perpId);
|
|
2612
|
+
const tickLower = priceToTick(params.priceLower, true);
|
|
2613
|
+
const tickUpper = priceToTick(params.priceUpper, false);
|
|
2614
|
+
const tickSpacing = perpData.tickSpacing;
|
|
2615
|
+
const alignedTickLower = Math.floor(tickLower / tickSpacing) * tickSpacing;
|
|
2616
|
+
const alignedTickUpper = Math.ceil(tickUpper / tickSpacing) * tickSpacing;
|
|
2617
|
+
const contractParams = {
|
|
2618
|
+
margin: marginScaled,
|
|
2619
|
+
liquidity: params.liquidity,
|
|
2620
|
+
tickLower: alignedTickLower,
|
|
2621
|
+
tickUpper: alignedTickUpper,
|
|
2622
|
+
maxAmt0In: scale6Decimals(params.maxAmt0In),
|
|
2623
|
+
maxAmt1In: scale6Decimals(params.maxAmt1In)
|
|
2624
|
+
};
|
|
2625
|
+
const { request } = await context.walletClient.simulateContract({
|
|
2626
|
+
address: context.deployments().perpManager,
|
|
2627
|
+
abi: PERP_MANAGER_ABI,
|
|
2628
|
+
functionName: "openMakerPosition",
|
|
2629
|
+
args: [perpId, contractParams],
|
|
2630
|
+
account: context.walletClient.account
|
|
2631
|
+
});
|
|
2632
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
2633
|
+
const publicClient = context.walletClient.extend(import_viem6.publicActions);
|
|
2634
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2635
|
+
if (receipt.status === "reverted") {
|
|
2636
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
2637
|
+
}
|
|
2638
|
+
let makerPosId = null;
|
|
2639
|
+
for (const log of receipt.logs) {
|
|
2640
|
+
try {
|
|
2641
|
+
const decoded = (0, import_viem6.decodeEventLog)({
|
|
2642
|
+
abi: PERP_MANAGER_ABI,
|
|
2643
|
+
data: log.data,
|
|
2644
|
+
topics: log.topics,
|
|
2645
|
+
eventName: "PositionOpened"
|
|
2646
|
+
});
|
|
2647
|
+
if (decoded.args.perpId === perpId && decoded.args.isMaker) {
|
|
2648
|
+
makerPosId = decoded.args.posId;
|
|
2649
|
+
break;
|
|
2650
|
+
}
|
|
2651
|
+
} catch (e) {
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
if (!makerPosId) {
|
|
2656
|
+
throw new Error(`PositionOpened event not found in transaction receipt. Hash: ${txHash}`);
|
|
2657
|
+
}
|
|
2658
|
+
return new OpenPosition(context, perpId, makerPosId, void 0, true);
|
|
2659
|
+
}, "openMakerPosition");
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// src/functions/user.ts
|
|
2663
|
+
function getUserUsdcBalance(userData) {
|
|
2664
|
+
return userData.usdcBalance;
|
|
2665
|
+
}
|
|
2666
|
+
function getUserOpenPositions(userData) {
|
|
2667
|
+
return userData.openPositions;
|
|
2668
|
+
}
|
|
2669
|
+
function getUserClosedPositions(userData) {
|
|
2670
|
+
return userData.closedPositions;
|
|
2671
|
+
}
|
|
2672
|
+
function getUserRealizedPnl(userData) {
|
|
2673
|
+
return userData.realizedPnl;
|
|
2674
|
+
}
|
|
2675
|
+
function getUserUnrealizedPnl(userData) {
|
|
2676
|
+
return userData.unrealizedPnl;
|
|
2677
|
+
}
|
|
2678
|
+
function getUserWalletAddress(userData) {
|
|
2679
|
+
return userData.walletAddress;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/functions/position.ts
|
|
2683
|
+
var import_viem7 = require("viem");
|
|
2684
|
+
var import_viem8 = require("viem");
|
|
2685
|
+
function getPositionPerpId(positionData) {
|
|
2686
|
+
return positionData.perpId;
|
|
2687
|
+
}
|
|
2688
|
+
function getPositionId(positionData) {
|
|
2689
|
+
return positionData.positionId;
|
|
2690
|
+
}
|
|
2691
|
+
function getPositionIsLong(positionData) {
|
|
2692
|
+
return positionData.isLong;
|
|
2693
|
+
}
|
|
2694
|
+
function getPositionIsMaker(positionData) {
|
|
2695
|
+
return positionData.isMaker;
|
|
2696
|
+
}
|
|
2697
|
+
function getPositionLiveDetails(positionData) {
|
|
2698
|
+
return positionData.liveDetails;
|
|
2699
|
+
}
|
|
2700
|
+
function getPositionPnl(positionData) {
|
|
2701
|
+
return positionData.liveDetails.pnl;
|
|
2702
|
+
}
|
|
2703
|
+
function getPositionFundingPayment(positionData) {
|
|
2704
|
+
return positionData.liveDetails.fundingPayment;
|
|
2705
|
+
}
|
|
2706
|
+
function getPositionEffectiveMargin(positionData) {
|
|
2707
|
+
return positionData.liveDetails.effectiveMargin;
|
|
2708
|
+
}
|
|
2709
|
+
function getPositionIsLiquidatable(positionData) {
|
|
2710
|
+
return positionData.liveDetails.isLiquidatable;
|
|
2711
|
+
}
|
|
2712
|
+
async function closePosition(context, perpId, positionId, params) {
|
|
2713
|
+
return withErrorHandling(async () => {
|
|
2714
|
+
const contractParams = {
|
|
2715
|
+
posId: positionId,
|
|
2716
|
+
minAmt0Out: scale6Decimals(params.minAmt0Out),
|
|
2717
|
+
minAmt1Out: scale6Decimals(params.minAmt1Out),
|
|
2718
|
+
maxAmt1In: scale6Decimals(params.maxAmt1In)
|
|
2719
|
+
};
|
|
2720
|
+
const { result, request } = await context.walletClient.extend(import_viem8.publicActions).simulateContract({
|
|
2721
|
+
address: context.deployments().perpManager,
|
|
2722
|
+
abi: PERP_MANAGER_ABI,
|
|
2723
|
+
functionName: "closePosition",
|
|
2724
|
+
args: [perpId, contractParams],
|
|
2725
|
+
account: context.walletClient.account
|
|
2726
|
+
});
|
|
2727
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
2728
|
+
const publicClient = context.walletClient.extend(import_viem8.publicActions);
|
|
2729
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2730
|
+
if (receipt.status === "reverted") {
|
|
2731
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
2732
|
+
}
|
|
2733
|
+
let newPositionId = null;
|
|
2734
|
+
for (const log of receipt.logs) {
|
|
2735
|
+
try {
|
|
2736
|
+
const decoded = (0, import_viem7.decodeEventLog)({
|
|
2737
|
+
abi: PERP_MANAGER_ABI,
|
|
2738
|
+
data: log.data,
|
|
2739
|
+
topics: log.topics,
|
|
2740
|
+
eventName: "PositionOpened"
|
|
2741
|
+
});
|
|
2742
|
+
if (decoded.args.perpId === perpId) {
|
|
2743
|
+
newPositionId = decoded.args.posId;
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
} catch (e) {
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (!newPositionId) {
|
|
2751
|
+
return null;
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
perpId,
|
|
2755
|
+
positionId: newPositionId,
|
|
2756
|
+
liveDetails: await getPositionLiveDetailsFromContract(context, perpId, newPositionId)
|
|
2757
|
+
};
|
|
2758
|
+
}, `closePosition for position ${positionId}`);
|
|
2759
|
+
}
|
|
2760
|
+
async function getPositionLiveDetailsFromContract(context, perpId, positionId) {
|
|
2761
|
+
return withErrorHandling(async () => {
|
|
2762
|
+
const result = await context.walletClient.readContract({
|
|
2763
|
+
address: context.deployments().perpManager,
|
|
2764
|
+
abi: PERP_MANAGER_ABI,
|
|
2765
|
+
functionName: "livePositionDetails",
|
|
2766
|
+
args: [perpId, positionId]
|
|
2767
|
+
});
|
|
2768
|
+
return {
|
|
2769
|
+
pnl: Number((0, import_viem7.formatUnits)(result[0], 6)),
|
|
2770
|
+
fundingPayment: Number((0, import_viem7.formatUnits)(result[1], 6)),
|
|
2771
|
+
effectiveMargin: Number((0, import_viem7.formatUnits)(result[2], 6)),
|
|
2772
|
+
isLiquidatable: result[3]
|
|
2773
|
+
};
|
|
2774
|
+
}, `getPositionLiveDetailsFromContract for position ${positionId}`);
|
|
2775
|
+
}
|
|
2214
2776
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2215
2777
|
0 && (module.exports = {
|
|
2216
2778
|
BEACON_ABI,
|
|
2217
2779
|
BIGINT_1E6,
|
|
2218
|
-
|
|
2780
|
+
ContractError,
|
|
2781
|
+
GraphQLError,
|
|
2782
|
+
InsufficientFundsError,
|
|
2219
2783
|
NUMBER_1E6,
|
|
2220
2784
|
OpenPosition,
|
|
2221
2785
|
PERP_MANAGER_ABI,
|
|
2222
|
-
Perp,
|
|
2223
2786
|
PerpCityContext,
|
|
2224
|
-
|
|
2787
|
+
PerpCityError,
|
|
2225
2788
|
Q96,
|
|
2226
|
-
|
|
2789
|
+
RPCError,
|
|
2790
|
+
TransactionRejectedError,
|
|
2791
|
+
ValidationError,
|
|
2227
2792
|
approveUsdc,
|
|
2793
|
+
closePosition,
|
|
2794
|
+
createPerp,
|
|
2228
2795
|
estimateLiquidity,
|
|
2796
|
+
getAllMakerPositions,
|
|
2797
|
+
getAllTakerPositions,
|
|
2798
|
+
getPerpBeacon,
|
|
2799
|
+
getPerpBounds,
|
|
2800
|
+
getPerpFees,
|
|
2801
|
+
getPerpFundingRate,
|
|
2802
|
+
getPerpFundingRateTimeSeries,
|
|
2803
|
+
getPerpIndex,
|
|
2804
|
+
getPerpIndexTimeSeries,
|
|
2805
|
+
getPerpLastIndexUpdate,
|
|
2806
|
+
getPerpMark,
|
|
2807
|
+
getPerpMarkTimeSeries,
|
|
2808
|
+
getPerpOpenInterest,
|
|
2809
|
+
getPerpOpenInterestTimeSeries,
|
|
2810
|
+
getPerpTickSpacing,
|
|
2811
|
+
getPerps,
|
|
2812
|
+
getPositionEffectiveMargin,
|
|
2813
|
+
getPositionFundingPayment,
|
|
2814
|
+
getPositionId,
|
|
2815
|
+
getPositionIsLiquidatable,
|
|
2816
|
+
getPositionIsLong,
|
|
2817
|
+
getPositionIsMaker,
|
|
2818
|
+
getPositionLiveDetails,
|
|
2819
|
+
getPositionLiveDetailsFromContract,
|
|
2820
|
+
getPositionPerpId,
|
|
2821
|
+
getPositionPnl,
|
|
2822
|
+
getTotalOpenMakerPnl,
|
|
2823
|
+
getTotalOpenTakerPnl,
|
|
2824
|
+
getUserClosedPositions,
|
|
2825
|
+
getUserOpenPositions,
|
|
2826
|
+
getUserRealizedPnl,
|
|
2827
|
+
getUserUnrealizedPnl,
|
|
2828
|
+
getUserUsdcBalance,
|
|
2829
|
+
getUserWalletAddress,
|
|
2229
2830
|
marginRatioToLeverage,
|
|
2831
|
+
openMakerPosition,
|
|
2832
|
+
openTakerPosition,
|
|
2833
|
+
parseContractError,
|
|
2230
2834
|
priceToSqrtPriceX96,
|
|
2231
2835
|
priceToTick,
|
|
2232
2836
|
scale6Decimals,
|
|
2233
2837
|
scaleFrom6Decimals,
|
|
2234
2838
|
scaleFromX96,
|
|
2235
2839
|
scaleToX96,
|
|
2236
|
-
sqrtPriceX96ToPrice
|
|
2840
|
+
sqrtPriceX96ToPrice,
|
|
2841
|
+
withErrorHandling
|
|
2237
2842
|
});
|
|
2238
2843
|
//# sourceMappingURL=index.js.map
|