@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.mjs
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
1
|
// src/context.ts
|
|
2
2
|
import { GraphQLClient } from "graphql-request";
|
|
3
|
-
|
|
4
|
-
|
|
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/
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
this.
|
|
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
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
|
|
1689
|
-
|
|
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
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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: {
|
|
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
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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: {
|
|
1748
|
+
where: { beacon_in: $beaconIds }
|
|
1745
1749
|
) {
|
|
1750
|
+
beacon { id }
|
|
1746
1751
|
timestamp
|
|
1747
|
-
|
|
1752
|
+
indexPrice
|
|
1748
1753
|
}
|
|
1749
1754
|
}
|
|
1750
1755
|
`);
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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.
|
|
1880
|
-
|
|
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
|
|
1883
|
-
const query =
|
|
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.
|
|
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
|
|
1904
|
-
return (
|
|
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
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
2690
|
+
ContractError,
|
|
2691
|
+
GraphQLError,
|
|
2692
|
+
InsufficientFundsError,
|
|
2172
2693
|
NUMBER_1E6,
|
|
2173
2694
|
OpenPosition,
|
|
2174
2695
|
PERP_MANAGER_ABI,
|
|
2175
|
-
Perp,
|
|
2176
2696
|
PerpCityContext,
|
|
2177
|
-
|
|
2697
|
+
PerpCityError,
|
|
2178
2698
|
Q96,
|
|
2179
|
-
|
|
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
|