@strobelabs/perpcity-sdk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index.mjs → index.cjs} +664 -75
- package/dist/index.cjs.map +1 -0
- package/dist/{index.d.mts → index.d.cts} +116 -9
- package/dist/index.d.ts +116 -9
- package/dist/index.js +549 -162
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/index.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,95 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
BEACON_ABI: () => BEACON_ABI,
|
|
34
|
-
BIGINT_1E6: () => BIGINT_1E6,
|
|
35
|
-
ContractError: () => ContractError,
|
|
36
|
-
ErrorCategory: () => ErrorCategory,
|
|
37
|
-
ErrorSource: () => ErrorSource,
|
|
38
|
-
InsufficientFundsError: () => InsufficientFundsError,
|
|
39
|
-
NUMBER_1E6: () => NUMBER_1E6,
|
|
40
|
-
OpenPosition: () => OpenPosition,
|
|
41
|
-
PERP_MANAGER_ABI: () => PERP_MANAGER_ABI,
|
|
42
|
-
PerpCityContext: () => PerpCityContext,
|
|
43
|
-
PerpCityError: () => PerpCityError,
|
|
44
|
-
Q96: () => Q96,
|
|
45
|
-
RPCError: () => RPCError,
|
|
46
|
-
TransactionRejectedError: () => TransactionRejectedError,
|
|
47
|
-
ValidationError: () => ValidationError,
|
|
48
|
-
approveUsdc: () => approveUsdc,
|
|
49
|
-
calculateEntryPrice: () => calculateEntryPrice,
|
|
50
|
-
calculateLeverage: () => calculateLeverage,
|
|
51
|
-
calculateLiquidationPrice: () => calculateLiquidationPrice,
|
|
52
|
-
calculateLiquidityForTargetRatio: () => calculateLiquidityForTargetRatio,
|
|
53
|
-
calculatePositionSize: () => calculatePositionSize,
|
|
54
|
-
calculatePositionValue: () => calculatePositionValue,
|
|
55
|
-
closePosition: () => closePosition,
|
|
56
|
-
createPerp: () => createPerp,
|
|
57
|
-
estimateLiquidity: () => estimateLiquidity,
|
|
58
|
-
getPerpBeacon: () => getPerpBeacon,
|
|
59
|
-
getPerpBounds: () => getPerpBounds,
|
|
60
|
-
getPerpFees: () => getPerpFees,
|
|
61
|
-
getPerpMark: () => getPerpMark,
|
|
62
|
-
getPerpTickSpacing: () => getPerpTickSpacing,
|
|
63
|
-
getPositionEffectiveMargin: () => getPositionEffectiveMargin,
|
|
64
|
-
getPositionFundingPayment: () => getPositionFundingPayment,
|
|
65
|
-
getPositionId: () => getPositionId,
|
|
66
|
-
getPositionIsLiquidatable: () => getPositionIsLiquidatable,
|
|
67
|
-
getPositionIsLong: () => getPositionIsLong,
|
|
68
|
-
getPositionIsMaker: () => getPositionIsMaker,
|
|
69
|
-
getPositionLiveDetails: () => getPositionLiveDetails,
|
|
70
|
-
getPositionLiveDetailsFromContract: () => getPositionLiveDetailsFromContract,
|
|
71
|
-
getPositionPerpId: () => getPositionPerpId,
|
|
72
|
-
getPositionPnl: () => getPositionPnl,
|
|
73
|
-
getRpcUrl: () => getRpcUrl,
|
|
74
|
-
getUserOpenPositions: () => getUserOpenPositions,
|
|
75
|
-
getUserUsdcBalance: () => getUserUsdcBalance,
|
|
76
|
-
getUserWalletAddress: () => getUserWalletAddress,
|
|
77
|
-
marginRatioToLeverage: () => marginRatioToLeverage,
|
|
78
|
-
openMakerPosition: () => openMakerPosition,
|
|
79
|
-
openTakerPosition: () => openTakerPosition,
|
|
80
|
-
parseContractError: () => parseContractError,
|
|
81
|
-
priceToSqrtPriceX96: () => priceToSqrtPriceX96,
|
|
82
|
-
priceToTick: () => priceToTick,
|
|
83
|
-
scale6Decimals: () => scale6Decimals,
|
|
84
|
-
scaleFrom6Decimals: () => scaleFrom6Decimals,
|
|
85
|
-
scaleFromX96: () => scaleFromX96,
|
|
86
|
-
scaleToX96: () => scaleToX96,
|
|
87
|
-
sqrtPriceX96ToPrice: () => sqrtPriceX96ToPrice,
|
|
88
|
-
tickToPrice: () => tickToPrice,
|
|
89
|
-
withErrorHandling: () => withErrorHandling
|
|
90
|
-
});
|
|
91
|
-
module.exports = __toCommonJS(index_exports);
|
|
92
|
-
|
|
93
1
|
// src/abis/beacon.ts
|
|
94
2
|
var BEACON_ABI = [
|
|
95
3
|
{
|
|
@@ -1869,19 +1777,14 @@ var PERP_MANAGER_ABI = [
|
|
|
1869
1777
|
type: "int256"
|
|
1870
1778
|
},
|
|
1871
1779
|
{
|
|
1872
|
-
internalType: "
|
|
1780
|
+
internalType: "uint256",
|
|
1873
1781
|
name: "netMargin",
|
|
1874
|
-
type: "
|
|
1782
|
+
type: "uint256"
|
|
1875
1783
|
},
|
|
1876
1784
|
{
|
|
1877
1785
|
internalType: "bool",
|
|
1878
1786
|
name: "wasLiquidated",
|
|
1879
1787
|
type: "bool"
|
|
1880
|
-
},
|
|
1881
|
-
{
|
|
1882
|
-
internalType: "uint256",
|
|
1883
|
-
name: "notional",
|
|
1884
|
-
type: "uint256"
|
|
1885
1788
|
}
|
|
1886
1789
|
],
|
|
1887
1790
|
stateMutability: "nonpayable",
|
|
@@ -2358,8 +2261,13 @@ var PERP_MANAGER_ABI = [
|
|
|
2358
2261
|
];
|
|
2359
2262
|
|
|
2360
2263
|
// src/context.ts
|
|
2361
|
-
|
|
2362
|
-
|
|
2264
|
+
import TTLCache from "@isaacs/ttlcache";
|
|
2265
|
+
import {
|
|
2266
|
+
createPublicClient,
|
|
2267
|
+
erc20Abi as erc20Abi2,
|
|
2268
|
+
formatUnits,
|
|
2269
|
+
http
|
|
2270
|
+
} from "viem";
|
|
2363
2271
|
|
|
2364
2272
|
// src/abis/fees.ts
|
|
2365
2273
|
var FEES_ABI = [
|
|
@@ -2440,13 +2348,22 @@ var MARGIN_RATIOS_ABI = [
|
|
|
2440
2348
|
];
|
|
2441
2349
|
|
|
2442
2350
|
// src/utils/approve.ts
|
|
2443
|
-
|
|
2351
|
+
import { erc20Abi } from "viem";
|
|
2444
2352
|
var DEFAULT_CONFIRMATIONS = 2;
|
|
2353
|
+
async function getUsdcAllowance(context, owner) {
|
|
2354
|
+
const deployments = context.deployments();
|
|
2355
|
+
return context.publicClient.readContract({
|
|
2356
|
+
address: deployments.usdc,
|
|
2357
|
+
abi: erc20Abi,
|
|
2358
|
+
functionName: "allowance",
|
|
2359
|
+
args: [owner, deployments.perpManager]
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2445
2362
|
async function approveUsdc(context, amount, confirmations = DEFAULT_CONFIRMATIONS) {
|
|
2446
2363
|
const deployments = context.deployments();
|
|
2447
2364
|
const { request } = await context.publicClient.simulateContract({
|
|
2448
2365
|
address: deployments.usdc,
|
|
2449
|
-
abi:
|
|
2366
|
+
abi: erc20Abi,
|
|
2450
2367
|
functionName: "approve",
|
|
2451
2368
|
args: [deployments.perpManager, amount],
|
|
2452
2369
|
account: context.walletClient.account
|
|
@@ -2462,6 +2379,12 @@ async function approveUsdc(context, amount, confirmations = DEFAULT_CONFIRMATION
|
|
|
2462
2379
|
var NUMBER_1E6 = 1e6;
|
|
2463
2380
|
var BIGINT_1E6 = 1000000n;
|
|
2464
2381
|
var Q96 = 79228162514264337593543950336n;
|
|
2382
|
+
var MIN_TICK = -887272;
|
|
2383
|
+
var MAX_TICK = 887272;
|
|
2384
|
+
var MIN_PRICE = 1e-6;
|
|
2385
|
+
var MAX_PRICE = 1e6;
|
|
2386
|
+
var UINT256_MAX = (1n << 256n) - 1n;
|
|
2387
|
+
var INT256_THRESHOLD = 1n << 255n;
|
|
2465
2388
|
|
|
2466
2389
|
// src/utils/conversions.ts
|
|
2467
2390
|
function priceToSqrtPriceX96(price) {
|
|
@@ -2494,12 +2417,23 @@ function priceToTick(price, roundDown) {
|
|
|
2494
2417
|
if (price <= 0) {
|
|
2495
2418
|
throw new Error("Price must be positive");
|
|
2496
2419
|
}
|
|
2420
|
+
if (price < MIN_PRICE || price > MAX_PRICE) {
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`Price ${price} is outside the representable range [${MIN_PRICE}, ${MAX_PRICE}]`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2497
2425
|
const logPrice = Math.log(price) / Math.log(1.0001);
|
|
2498
2426
|
return roundDown ? Math.floor(logPrice) : Math.ceil(logPrice);
|
|
2499
2427
|
}
|
|
2500
2428
|
function tickToPrice(tick) {
|
|
2501
2429
|
return 1.0001 ** tick;
|
|
2502
2430
|
}
|
|
2431
|
+
function uint256ToInt256(value) {
|
|
2432
|
+
if (value >= INT256_THRESHOLD) {
|
|
2433
|
+
return value - UINT256_MAX - 1n;
|
|
2434
|
+
}
|
|
2435
|
+
return value;
|
|
2436
|
+
}
|
|
2503
2437
|
function sqrtPriceX96ToPrice(sqrtPriceX96) {
|
|
2504
2438
|
const priceX96 = sqrtPriceX96 * sqrtPriceX96 / Q96;
|
|
2505
2439
|
return scaleFromX96(priceX96);
|
|
@@ -2515,7 +2449,7 @@ function scaleFrom6Decimals(value) {
|
|
|
2515
2449
|
}
|
|
2516
2450
|
|
|
2517
2451
|
// src/utils/errors.ts
|
|
2518
|
-
|
|
2452
|
+
import { BaseError, ContractFunctionRevertedError } from "viem";
|
|
2519
2453
|
var ErrorCategory = /* @__PURE__ */ ((ErrorCategory2) => {
|
|
2520
2454
|
ErrorCategory2["USER_ERROR"] = "USER_ERROR";
|
|
2521
2455
|
ErrorCategory2["STATE_ERROR"] = "STATE_ERROR";
|
|
@@ -2573,9 +2507,9 @@ function parseContractError(error) {
|
|
|
2573
2507
|
if (error instanceof PerpCityError) {
|
|
2574
2508
|
return error;
|
|
2575
2509
|
}
|
|
2576
|
-
if (error instanceof
|
|
2577
|
-
const revertError = error.walk((err) => err instanceof
|
|
2578
|
-
if (revertError instanceof
|
|
2510
|
+
if (error instanceof BaseError) {
|
|
2511
|
+
const revertError = error.walk((err) => err instanceof ContractFunctionRevertedError);
|
|
2512
|
+
if (revertError instanceof ContractFunctionRevertedError) {
|
|
2579
2513
|
const errorName = revertError.data?.errorName ?? "Unknown";
|
|
2580
2514
|
const args = revertError.data?.args ?? [];
|
|
2581
2515
|
const { message, debug } = formatContractError(errorName, args);
|
|
@@ -2740,7 +2674,7 @@ function formatContractError(errorName, args) {
|
|
|
2740
2674
|
};
|
|
2741
2675
|
case "MinimumAmountInsufficient":
|
|
2742
2676
|
return {
|
|
2743
|
-
message:
|
|
2677
|
+
message: "Slippage tolerance exceeded. The position's value moved unfavorably during execution. Try increasing your slippage tolerance.",
|
|
2744
2678
|
debug: { source, category: "USER_ERROR" /* USER_ERROR */ }
|
|
2745
2679
|
};
|
|
2746
2680
|
case "PriceImpactTooHigh":
|
|
@@ -3019,6 +2953,23 @@ async function withErrorHandling(fn, context) {
|
|
|
3019
2953
|
}
|
|
3020
2954
|
}
|
|
3021
2955
|
|
|
2956
|
+
// src/utils/funding.ts
|
|
2957
|
+
var PRECISION = 10n ** 18n;
|
|
2958
|
+
function convertFundingDiffX96ToPercentPerPeriod(fundingDiffX96, interval, periodSeconds) {
|
|
2959
|
+
const scaledRate = fundingDiffX96 * periodSeconds * PRECISION / (Q96 * interval);
|
|
2960
|
+
return Number(scaledRate) / Number(PRECISION);
|
|
2961
|
+
}
|
|
2962
|
+
function convertFundingPerSecondX96ToPercentPerMinute(fundingPerSecondX96) {
|
|
2963
|
+
const SECONDS_PER_MINUTE = 60n;
|
|
2964
|
+
const scaledRate = fundingPerSecondX96 * SECONDS_PER_MINUTE * PRECISION / Q96;
|
|
2965
|
+
return Number(scaledRate) / Number(PRECISION);
|
|
2966
|
+
}
|
|
2967
|
+
function convertFundingPerSecondX96ToPercentPerDay(fundingPerSecondX96) {
|
|
2968
|
+
const SECONDS_PER_DAY = 86400n;
|
|
2969
|
+
const scaledRate = fundingPerSecondX96 * SECONDS_PER_DAY * PRECISION / Q96;
|
|
2970
|
+
return Number(scaledRate) / Number(PRECISION);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
3022
2973
|
// src/utils/liquidity.ts
|
|
3023
2974
|
async function estimateLiquidity(_context, tickLower, tickUpper, usdScaled) {
|
|
3024
2975
|
if (tickLower >= tickUpper) {
|
|
@@ -3121,16 +3072,16 @@ function getRpcUrl(config = {}) {
|
|
|
3121
3072
|
// src/context.ts
|
|
3122
3073
|
var PerpCityContext = class {
|
|
3123
3074
|
constructor(config) {
|
|
3124
|
-
this.configCache = new
|
|
3075
|
+
this.configCache = new TTLCache({ ttl: 5 * 60 * 1e3 });
|
|
3125
3076
|
this.walletClient = config.walletClient;
|
|
3126
3077
|
if (!config.walletClient.chain?.id) {
|
|
3127
3078
|
throw new Error(
|
|
3128
3079
|
"PerpCityContext: walletClient.chain must be defined with a numeric id. Ensure your walletClient was created with a chain parameter."
|
|
3129
3080
|
);
|
|
3130
3081
|
}
|
|
3131
|
-
this.publicClient =
|
|
3082
|
+
this.publicClient = createPublicClient({
|
|
3132
3083
|
chain: config.walletClient.chain,
|
|
3133
|
-
transport:
|
|
3084
|
+
transport: http(config.rpcUrl, { batch: true })
|
|
3134
3085
|
});
|
|
3135
3086
|
this._deployments = config.deployments;
|
|
3136
3087
|
}
|
|
@@ -3212,8 +3163,7 @@ var PerpCityContext = class {
|
|
|
3212
3163
|
const tickSpacing = Number(cfg.key.tickSpacing);
|
|
3213
3164
|
let sqrtPriceX96;
|
|
3214
3165
|
if (markPrice) {
|
|
3215
|
-
|
|
3216
|
-
sqrtPriceX96 = BigInt(Math.floor(sqrtPrice * 2 ** 96));
|
|
3166
|
+
sqrtPriceX96 = priceToSqrtPriceX96(markPrice);
|
|
3217
3167
|
} else {
|
|
3218
3168
|
sqrtPriceX96 = await this.publicClient.readContract({
|
|
3219
3169
|
address: this.deployments().perpManager,
|
|
@@ -3298,7 +3248,7 @@ var PerpCityContext = class {
|
|
|
3298
3248
|
async fetchUserData(userAddress, positions) {
|
|
3299
3249
|
const usdcBalance = await this.publicClient.readContract({
|
|
3300
3250
|
address: this.deployments().usdc,
|
|
3301
|
-
abi:
|
|
3251
|
+
abi: erc20Abi2,
|
|
3302
3252
|
functionName: "balanceOf",
|
|
3303
3253
|
args: [userAddress]
|
|
3304
3254
|
});
|
|
@@ -3316,7 +3266,7 @@ var PerpCityContext = class {
|
|
|
3316
3266
|
);
|
|
3317
3267
|
return {
|
|
3318
3268
|
walletAddress: userAddress,
|
|
3319
|
-
usdcBalance: Number(
|
|
3269
|
+
usdcBalance: Number(formatUnits(usdcBalance, 6)),
|
|
3320
3270
|
openPositions: openPositionsData
|
|
3321
3271
|
};
|
|
3322
3272
|
}
|
|
@@ -3335,9 +3285,10 @@ var PerpCityContext = class {
|
|
|
3335
3285
|
);
|
|
3336
3286
|
}
|
|
3337
3287
|
return {
|
|
3338
|
-
pnl: Number(
|
|
3339
|
-
|
|
3340
|
-
|
|
3288
|
+
pnl: Number(formatUnits(pnl, 6)),
|
|
3289
|
+
// Negate so positive = user receives funding
|
|
3290
|
+
fundingPayment: -Number(formatUnits(funding, 6)),
|
|
3291
|
+
effectiveMargin: Number(formatUnits(uint256ToInt256(netMargin), 6)),
|
|
3341
3292
|
isLiquidatable: wasLiquidated
|
|
3342
3293
|
};
|
|
3343
3294
|
}, `fetchPositionLiveDetailsFromContract for position ${positionId}`);
|
|
@@ -3381,29 +3332,35 @@ var PerpCityContext = class {
|
|
|
3381
3332
|
args: [positionId]
|
|
3382
3333
|
});
|
|
3383
3334
|
const resultArray = result;
|
|
3384
|
-
const [perpId, margin, entryPerpDelta, entryUsdDelta, , , , marginRatios] = resultArray;
|
|
3335
|
+
const [perpId, margin, entryPerpDelta, entryUsdDelta, , , , marginRatios, makerDetailsRaw] = resultArray;
|
|
3385
3336
|
const zeroPerpId = `0x${"0".repeat(64)}`;
|
|
3386
3337
|
if (perpId === zeroPerpId) {
|
|
3387
3338
|
throw new Error(`Position ${positionId} does not exist`);
|
|
3388
3339
|
}
|
|
3340
|
+
const isMaker = makerDetailsRaw != null && (makerDetailsRaw.tickLower !== 0 || makerDetailsRaw.tickUpper !== 0);
|
|
3389
3341
|
return {
|
|
3390
3342
|
perpId,
|
|
3391
3343
|
positionId,
|
|
3392
|
-
margin: Number(
|
|
3344
|
+
margin: Number(formatUnits(margin, 6)),
|
|
3393
3345
|
entryPerpDelta,
|
|
3394
3346
|
entryUsdDelta,
|
|
3395
3347
|
marginRatios: {
|
|
3396
3348
|
min: Number(marginRatios.min),
|
|
3397
3349
|
max: Number(marginRatios.max),
|
|
3398
3350
|
liq: Number(marginRatios.liq)
|
|
3399
|
-
}
|
|
3351
|
+
},
|
|
3352
|
+
makerDetails: isMaker ? {
|
|
3353
|
+
unlockTimestamp: Number(makerDetailsRaw.unlockTimestamp),
|
|
3354
|
+
tickLower: makerDetailsRaw.tickLower,
|
|
3355
|
+
tickUpper: makerDetailsRaw.tickUpper
|
|
3356
|
+
} : null
|
|
3400
3357
|
};
|
|
3401
3358
|
}, `getPositionRawData for position ${positionId}`);
|
|
3402
3359
|
}
|
|
3403
3360
|
};
|
|
3404
3361
|
|
|
3405
3362
|
// src/functions/open-position.ts
|
|
3406
|
-
|
|
3363
|
+
import { decodeEventLog, formatUnits as formatUnits2 } from "viem";
|
|
3407
3364
|
var OpenPosition = class _OpenPosition {
|
|
3408
3365
|
constructor(context, perpId, positionId, isLong, isMaker, txHash) {
|
|
3409
3366
|
this.context = context;
|
|
@@ -3440,7 +3397,7 @@ var OpenPosition = class _OpenPosition {
|
|
|
3440
3397
|
let _wasFullyClosed = false;
|
|
3441
3398
|
for (const log of receipt.logs) {
|
|
3442
3399
|
try {
|
|
3443
|
-
const openedDecoded =
|
|
3400
|
+
const openedDecoded = decodeEventLog({
|
|
3444
3401
|
abi: PERP_MANAGER_ABI,
|
|
3445
3402
|
data: log.data,
|
|
3446
3403
|
topics: log.topics,
|
|
@@ -3454,7 +3411,7 @@ var OpenPosition = class _OpenPosition {
|
|
|
3454
3411
|
}
|
|
3455
3412
|
} catch (_e) {
|
|
3456
3413
|
try {
|
|
3457
|
-
const closedDecoded =
|
|
3414
|
+
const closedDecoded = decodeEventLog({
|
|
3458
3415
|
abi: PERP_MANAGER_ABI,
|
|
3459
3416
|
data: log.data,
|
|
3460
3417
|
topics: log.topics,
|
|
@@ -3502,9 +3459,9 @@ var OpenPosition = class _OpenPosition {
|
|
|
3502
3459
|
);
|
|
3503
3460
|
}
|
|
3504
3461
|
return {
|
|
3505
|
-
pnl: Number((
|
|
3506
|
-
fundingPayment: Number((
|
|
3507
|
-
effectiveMargin: Number((
|
|
3462
|
+
pnl: Number(formatUnits2(pnl, 6)),
|
|
3463
|
+
fundingPayment: Number(formatUnits2(funding, 6)),
|
|
3464
|
+
effectiveMargin: Number(formatUnits2(uint256ToInt256(netMargin), 6)),
|
|
3508
3465
|
isLiquidatable: wasLiquidated
|
|
3509
3466
|
};
|
|
3510
3467
|
}, `liveDetails for position ${this.positionId}`);
|
|
@@ -3512,6 +3469,8 @@ var OpenPosition = class _OpenPosition {
|
|
|
3512
3469
|
};
|
|
3513
3470
|
|
|
3514
3471
|
// src/functions/perp.ts
|
|
3472
|
+
var TWAVG_WINDOW = 3600;
|
|
3473
|
+
var INTERVAL = 86400n;
|
|
3515
3474
|
function getPerpMark(perpData) {
|
|
3516
3475
|
return perpData.mark;
|
|
3517
3476
|
}
|
|
@@ -3527,9 +3486,58 @@ function getPerpFees(perpData) {
|
|
|
3527
3486
|
function getPerpTickSpacing(perpData) {
|
|
3528
3487
|
return perpData.tickSpacing;
|
|
3529
3488
|
}
|
|
3489
|
+
async function getFundingRate(context, perpId) {
|
|
3490
|
+
return withErrorHandling(async () => {
|
|
3491
|
+
const perpManagerAddr = context.deployments().perpManager;
|
|
3492
|
+
const cfg = await context.getPerpConfig(perpId);
|
|
3493
|
+
const [twAvgSqrtMarkX96, twAvgIndexX96] = await Promise.all([
|
|
3494
|
+
context.publicClient.readContract({
|
|
3495
|
+
address: perpManagerAddr,
|
|
3496
|
+
abi: PERP_MANAGER_ABI,
|
|
3497
|
+
functionName: "timeWeightedAvgSqrtPriceX96",
|
|
3498
|
+
args: [perpId, TWAVG_WINDOW]
|
|
3499
|
+
}),
|
|
3500
|
+
context.publicClient.readContract({
|
|
3501
|
+
address: cfg.beacon,
|
|
3502
|
+
abi: BEACON_ABI,
|
|
3503
|
+
functionName: "twAvg",
|
|
3504
|
+
args: [TWAVG_WINDOW]
|
|
3505
|
+
})
|
|
3506
|
+
]);
|
|
3507
|
+
const twAvgMarkX96 = twAvgSqrtMarkX96 * twAvgSqrtMarkX96 / Q96;
|
|
3508
|
+
const fundingDiffX96 = twAvgMarkX96 - twAvgIndexX96;
|
|
3509
|
+
const fundingPerSecondX96 = fundingDiffX96 / INTERVAL;
|
|
3510
|
+
return {
|
|
3511
|
+
ratePerDay: convertFundingDiffX96ToPercentPerPeriod(fundingDiffX96, INTERVAL, 86400n),
|
|
3512
|
+
ratePerMinute: convertFundingDiffX96ToPercentPerPeriod(fundingDiffX96, INTERVAL, 60n),
|
|
3513
|
+
rawX96: fundingPerSecondX96
|
|
3514
|
+
};
|
|
3515
|
+
}, `getFundingRate for perp ${perpId}`);
|
|
3516
|
+
}
|
|
3517
|
+
async function getIndexValue(context, perpId) {
|
|
3518
|
+
return withErrorHandling(async () => {
|
|
3519
|
+
const perpData = await context.getPerpData(perpId);
|
|
3520
|
+
return await context.publicClient.readContract({
|
|
3521
|
+
address: perpData.beacon,
|
|
3522
|
+
abi: BEACON_ABI,
|
|
3523
|
+
functionName: "index"
|
|
3524
|
+
});
|
|
3525
|
+
}, `getIndexValue for perp ${perpId}`);
|
|
3526
|
+
}
|
|
3527
|
+
async function getIndexTWAP(context, perpId, secondsAgo) {
|
|
3528
|
+
return withErrorHandling(async () => {
|
|
3529
|
+
const perpData = await context.getPerpData(perpId);
|
|
3530
|
+
return await context.publicClient.readContract({
|
|
3531
|
+
address: perpData.beacon,
|
|
3532
|
+
abi: BEACON_ABI,
|
|
3533
|
+
functionName: "twAvg",
|
|
3534
|
+
args: [secondsAgo]
|
|
3535
|
+
});
|
|
3536
|
+
}, `getIndexTWAP for perp ${perpId}`);
|
|
3537
|
+
}
|
|
3530
3538
|
|
|
3531
3539
|
// src/functions/perp-manager.ts
|
|
3532
|
-
|
|
3540
|
+
import { decodeErrorResult, decodeEventLog as decodeEventLog2, erc20Abi as erc20Abi3 } from "viem";
|
|
3533
3541
|
async function createPerp(context, params) {
|
|
3534
3542
|
return withErrorHandling(async () => {
|
|
3535
3543
|
const deployments = context.deployments();
|
|
@@ -3555,13 +3563,15 @@ async function createPerp(context, params) {
|
|
|
3555
3563
|
account: context.walletClient.account
|
|
3556
3564
|
});
|
|
3557
3565
|
const txHash = await context.walletClient.writeContract(request);
|
|
3558
|
-
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3566
|
+
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3567
|
+
hash: txHash
|
|
3568
|
+
});
|
|
3559
3569
|
if (receipt.status === "reverted") {
|
|
3560
3570
|
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
3561
3571
|
}
|
|
3562
3572
|
for (const log of receipt.logs) {
|
|
3563
3573
|
try {
|
|
3564
|
-
const decoded = (
|
|
3574
|
+
const decoded = decodeEventLog2({
|
|
3565
3575
|
abi: PERP_MANAGER_ABI,
|
|
3566
3576
|
data: log.data,
|
|
3567
3577
|
topics: log.topics,
|
|
@@ -3598,7 +3608,7 @@ async function openTakerPosition(context, perpId, params) {
|
|
|
3598
3608
|
const requiredAmount = marginScaled + totalFees;
|
|
3599
3609
|
const currentAllowance = await context.publicClient.readContract({
|
|
3600
3610
|
address: context.deployments().usdc,
|
|
3601
|
-
abi:
|
|
3611
|
+
abi: erc20Abi3,
|
|
3602
3612
|
functionName: "allowance",
|
|
3603
3613
|
args: [context.walletClient.account.address, context.deployments().perpManager],
|
|
3604
3614
|
blockTag: "latest"
|
|
@@ -3622,14 +3632,16 @@ async function openTakerPosition(context, perpId, params) {
|
|
|
3622
3632
|
account: context.walletClient.account
|
|
3623
3633
|
});
|
|
3624
3634
|
const txHash = await context.walletClient.writeContract(request);
|
|
3625
|
-
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3635
|
+
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3636
|
+
hash: txHash
|
|
3637
|
+
});
|
|
3626
3638
|
if (receipt.status === "reverted") {
|
|
3627
3639
|
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
3628
3640
|
}
|
|
3629
3641
|
let takerPosId = null;
|
|
3630
3642
|
for (const log of receipt.logs) {
|
|
3631
3643
|
try {
|
|
3632
|
-
const decoded = (
|
|
3644
|
+
const decoded = decodeEventLog2({
|
|
3633
3645
|
abi: PERP_MANAGER_ABI,
|
|
3634
3646
|
data: log.data,
|
|
3635
3647
|
topics: log.topics
|
|
@@ -3647,6 +3659,88 @@ async function openTakerPosition(context, perpId, params) {
|
|
|
3647
3659
|
return new OpenPosition(context, perpId, takerPosId, params.isLong, false, txHash);
|
|
3648
3660
|
}, "openTakerPosition");
|
|
3649
3661
|
}
|
|
3662
|
+
function buildMakerContractParams(context, marginScaled, params, alignedTickLower, alignedTickUpper, maxAmt0In, maxAmt1In) {
|
|
3663
|
+
return {
|
|
3664
|
+
holder: context.walletClient.account.address,
|
|
3665
|
+
margin: marginScaled,
|
|
3666
|
+
liquidity: params.liquidity,
|
|
3667
|
+
tickLower: alignedTickLower,
|
|
3668
|
+
tickUpper: alignedTickUpper,
|
|
3669
|
+
maxAmt0In,
|
|
3670
|
+
maxAmt1In
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
function calculateAlignedTicks(priceLower, priceUpper, tickSpacing) {
|
|
3674
|
+
const tickLower = priceToTick(priceLower, true);
|
|
3675
|
+
const tickUpper = priceToTick(priceUpper, false);
|
|
3676
|
+
const alignedTickLower = Math.floor(tickLower / tickSpacing) * tickSpacing;
|
|
3677
|
+
const alignedTickUpper = Math.ceil(tickUpper / tickSpacing) * tickSpacing;
|
|
3678
|
+
if (alignedTickLower < MIN_TICK) {
|
|
3679
|
+
throw new Error(
|
|
3680
|
+
`Lower tick ${alignedTickLower} is below MIN_TICK (${MIN_TICK}). Increase priceLower.`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
if (alignedTickUpper > MAX_TICK) {
|
|
3684
|
+
throw new Error(
|
|
3685
|
+
`Upper tick ${alignedTickUpper} exceeds MAX_TICK (${MAX_TICK}). Decrease priceUpper.`
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
if (alignedTickLower === alignedTickUpper) {
|
|
3689
|
+
throw new Error(
|
|
3690
|
+
"Price range too narrow: lower and upper ticks are equal after alignment. Widen the range."
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
return { alignedTickLower, alignedTickUpper };
|
|
3694
|
+
}
|
|
3695
|
+
function alignMakerTicks(params, tickSpacing) {
|
|
3696
|
+
return calculateAlignedTicks(params.priceLower, params.priceUpper, tickSpacing);
|
|
3697
|
+
}
|
|
3698
|
+
var DEFAULT_MAKER_SLIPPAGE_TOLERANCE = 0.01;
|
|
3699
|
+
var MAX_UINT128 = 2n ** 128n - 1n;
|
|
3700
|
+
async function quoteOpenMakerPosition(context, perpId, params) {
|
|
3701
|
+
return withErrorHandling(async () => {
|
|
3702
|
+
if (params.margin <= 0) {
|
|
3703
|
+
throw new Error("Margin must be greater than 0");
|
|
3704
|
+
}
|
|
3705
|
+
if (params.priceLower >= params.priceUpper) {
|
|
3706
|
+
throw new Error("priceLower must be less than priceUpper");
|
|
3707
|
+
}
|
|
3708
|
+
const marginScaled = scale6Decimals(params.margin);
|
|
3709
|
+
const perpData = await context.getPerpData(perpId);
|
|
3710
|
+
const { alignedTickLower, alignedTickUpper } = alignMakerTicks(params, perpData.tickSpacing);
|
|
3711
|
+
const contractParams = buildMakerContractParams(
|
|
3712
|
+
context,
|
|
3713
|
+
marginScaled,
|
|
3714
|
+
params,
|
|
3715
|
+
alignedTickLower,
|
|
3716
|
+
alignedTickUpper,
|
|
3717
|
+
MAX_UINT128,
|
|
3718
|
+
MAX_UINT128
|
|
3719
|
+
);
|
|
3720
|
+
const [unexpectedReason, perpDelta, usdDelta] = await context.publicClient.readContract({
|
|
3721
|
+
address: context.deployments().perpManager,
|
|
3722
|
+
abi: PERP_MANAGER_ABI,
|
|
3723
|
+
functionName: "quoteOpenMakerPosition",
|
|
3724
|
+
args: [perpId, contractParams]
|
|
3725
|
+
});
|
|
3726
|
+
if (unexpectedReason !== "0x") {
|
|
3727
|
+
throw new Error(`Quote failed: ${unexpectedReason}`);
|
|
3728
|
+
}
|
|
3729
|
+
return { perpDelta, usdDelta };
|
|
3730
|
+
}, "quoteOpenMakerPosition");
|
|
3731
|
+
}
|
|
3732
|
+
function applySlippage(delta, slippageTolerance, fallbackRef) {
|
|
3733
|
+
const slippageBps = BigInt(Math.ceil(slippageTolerance * 1e4));
|
|
3734
|
+
if (delta < 0n) {
|
|
3735
|
+
const absDelta = -delta;
|
|
3736
|
+
return absDelta + absDelta * slippageBps / 10000n;
|
|
3737
|
+
}
|
|
3738
|
+
if (fallbackRef !== void 0 && fallbackRef !== 0n) {
|
|
3739
|
+
const absRef = fallbackRef < 0n ? -fallbackRef : fallbackRef;
|
|
3740
|
+
return absRef * slippageBps / 10000n;
|
|
3741
|
+
}
|
|
3742
|
+
return 0n;
|
|
3743
|
+
}
|
|
3650
3744
|
async function openMakerPosition(context, perpId, params) {
|
|
3651
3745
|
return withErrorHandling(async () => {
|
|
3652
3746
|
if (params.margin <= 0) {
|
|
@@ -3656,11 +3750,27 @@ async function openMakerPosition(context, perpId, params) {
|
|
|
3656
3750
|
throw new Error("priceLower must be less than priceUpper");
|
|
3657
3751
|
}
|
|
3658
3752
|
const marginScaled = scale6Decimals(params.margin);
|
|
3659
|
-
const
|
|
3660
|
-
const
|
|
3753
|
+
const perpData = await context.getPerpData(perpId);
|
|
3754
|
+
const { alignedTickLower, alignedTickUpper } = alignMakerTicks(params, perpData.tickSpacing);
|
|
3755
|
+
let maxAmt0In;
|
|
3756
|
+
let maxAmt1In;
|
|
3757
|
+
if (params.maxAmt0In === void 0 !== (params.maxAmt1In === void 0)) {
|
|
3758
|
+
throw new Error(
|
|
3759
|
+
"Both maxAmt0In and maxAmt1In must be provided together or neither. Omit both to use automatic quote-based slippage calculation."
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3762
|
+
if (params.maxAmt0In !== void 0 && params.maxAmt1In !== void 0) {
|
|
3763
|
+
maxAmt0In = typeof params.maxAmt0In === "bigint" ? params.maxAmt0In : scale6Decimals(params.maxAmt0In);
|
|
3764
|
+
maxAmt1In = typeof params.maxAmt1In === "bigint" ? params.maxAmt1In : scale6Decimals(params.maxAmt1In);
|
|
3765
|
+
} else {
|
|
3766
|
+
const quote = await quoteOpenMakerPosition(context, perpId, params);
|
|
3767
|
+
const slippage = params.slippageTolerance ?? DEFAULT_MAKER_SLIPPAGE_TOLERANCE;
|
|
3768
|
+
maxAmt0In = applySlippage(quote.perpDelta, slippage, quote.usdDelta);
|
|
3769
|
+
maxAmt1In = applySlippage(quote.usdDelta, slippage, quote.perpDelta);
|
|
3770
|
+
}
|
|
3661
3771
|
const currentAllowance = await context.publicClient.readContract({
|
|
3662
3772
|
address: context.deployments().usdc,
|
|
3663
|
-
abi:
|
|
3773
|
+
abi: erc20Abi3,
|
|
3664
3774
|
functionName: "allowance",
|
|
3665
3775
|
args: [context.walletClient.account.address, context.deployments().perpManager],
|
|
3666
3776
|
blockTag: "latest"
|
|
@@ -3668,21 +3778,15 @@ async function openMakerPosition(context, perpId, params) {
|
|
|
3668
3778
|
if (currentAllowance < marginScaled) {
|
|
3669
3779
|
await approveUsdc(context, marginScaled);
|
|
3670
3780
|
}
|
|
3671
|
-
const
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
liquidity: params.liquidity,
|
|
3681
|
-
tickLower: alignedTickLower,
|
|
3682
|
-
tickUpper: alignedTickUpper,
|
|
3683
|
-
maxAmt0In: maxAmt0InScaled,
|
|
3684
|
-
maxAmt1In: maxAmt1InScaled
|
|
3685
|
-
};
|
|
3781
|
+
const contractParams = buildMakerContractParams(
|
|
3782
|
+
context,
|
|
3783
|
+
marginScaled,
|
|
3784
|
+
params,
|
|
3785
|
+
alignedTickLower,
|
|
3786
|
+
alignedTickUpper,
|
|
3787
|
+
maxAmt0In,
|
|
3788
|
+
maxAmt1In
|
|
3789
|
+
);
|
|
3686
3790
|
const { request } = await context.publicClient.simulateContract({
|
|
3687
3791
|
address: context.deployments().perpManager,
|
|
3688
3792
|
abi: PERP_MANAGER_ABI,
|
|
@@ -3691,14 +3795,16 @@ async function openMakerPosition(context, perpId, params) {
|
|
|
3691
3795
|
account: context.walletClient.account
|
|
3692
3796
|
});
|
|
3693
3797
|
const txHash = await context.walletClient.writeContract(request);
|
|
3694
|
-
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3798
|
+
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
3799
|
+
hash: txHash
|
|
3800
|
+
});
|
|
3695
3801
|
if (receipt.status === "reverted") {
|
|
3696
3802
|
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
3697
3803
|
}
|
|
3698
3804
|
let makerPosId = null;
|
|
3699
3805
|
for (const log of receipt.logs) {
|
|
3700
3806
|
try {
|
|
3701
|
-
const decoded = (
|
|
3807
|
+
const decoded = decodeEventLog2({
|
|
3702
3808
|
abi: PERP_MANAGER_ABI,
|
|
3703
3809
|
data: log.data,
|
|
3704
3810
|
topics: log.topics
|
|
@@ -3716,9 +3822,109 @@ async function openMakerPosition(context, perpId, params) {
|
|
|
3716
3822
|
return new OpenPosition(context, perpId, makerPosId, void 0, true, txHash);
|
|
3717
3823
|
}, "openMakerPosition");
|
|
3718
3824
|
}
|
|
3825
|
+
async function quoteTakerPosition(context, perpId, params) {
|
|
3826
|
+
return withErrorHandling(async () => {
|
|
3827
|
+
if (params.margin <= 0) {
|
|
3828
|
+
throw new Error("Margin must be greater than 0");
|
|
3829
|
+
}
|
|
3830
|
+
if (params.leverage <= 0) {
|
|
3831
|
+
throw new Error("Leverage must be greater than 0");
|
|
3832
|
+
}
|
|
3833
|
+
const marginScaled = scale6Decimals(params.margin);
|
|
3834
|
+
const marginRatio = Math.floor(NUMBER_1E6 / params.leverage);
|
|
3835
|
+
const result = await context.publicClient.simulateContract({
|
|
3836
|
+
address: context.deployments().perpManager,
|
|
3837
|
+
abi: PERP_MANAGER_ABI,
|
|
3838
|
+
functionName: "quoteOpenTakerPosition",
|
|
3839
|
+
args: [
|
|
3840
|
+
perpId,
|
|
3841
|
+
{
|
|
3842
|
+
holder: params.holder,
|
|
3843
|
+
isLong: params.isLong,
|
|
3844
|
+
margin: marginScaled,
|
|
3845
|
+
marginRatio,
|
|
3846
|
+
unspecifiedAmountLimit: params.isLong ? 0n : (1n << 128n) - 1n
|
|
3847
|
+
}
|
|
3848
|
+
],
|
|
3849
|
+
account: params.holder
|
|
3850
|
+
});
|
|
3851
|
+
const [unexpectedReason, perpDelta, usdDelta] = result.result;
|
|
3852
|
+
if (unexpectedReason && unexpectedReason !== "0x") {
|
|
3853
|
+
let errorName = "Quote simulation failed";
|
|
3854
|
+
try {
|
|
3855
|
+
const decoded = decodeErrorResult({
|
|
3856
|
+
abi: PERP_MANAGER_ABI,
|
|
3857
|
+
data: unexpectedReason
|
|
3858
|
+
});
|
|
3859
|
+
errorName = decoded.errorName;
|
|
3860
|
+
} catch {
|
|
3861
|
+
}
|
|
3862
|
+
throw new Error(errorName);
|
|
3863
|
+
}
|
|
3864
|
+
const absPerpDelta = perpDelta < 0n ? -perpDelta : perpDelta;
|
|
3865
|
+
const absUsdDelta = usdDelta < 0n ? -usdDelta : usdDelta;
|
|
3866
|
+
if (absPerpDelta === 0n) {
|
|
3867
|
+
throw new Error("Zero position size");
|
|
3868
|
+
}
|
|
3869
|
+
const fillPrice = Number(absUsdDelta) / Number(absPerpDelta);
|
|
3870
|
+
return { perpDelta, usdDelta, fillPrice };
|
|
3871
|
+
}, "quoteTakerPosition");
|
|
3872
|
+
}
|
|
3873
|
+
async function adjustMargin(context, positionId, marginDelta) {
|
|
3874
|
+
return withErrorHandling(async () => {
|
|
3875
|
+
const deployments = context.deployments();
|
|
3876
|
+
if (marginDelta > 0n) {
|
|
3877
|
+
const currentAllowance = await context.publicClient.readContract({
|
|
3878
|
+
address: deployments.usdc,
|
|
3879
|
+
abi: erc20Abi3,
|
|
3880
|
+
functionName: "allowance",
|
|
3881
|
+
args: [context.walletClient.account.address, deployments.perpManager],
|
|
3882
|
+
blockTag: "latest"
|
|
3883
|
+
});
|
|
3884
|
+
if (currentAllowance < marginDelta) {
|
|
3885
|
+
await approveUsdc(context, marginDelta);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
const { request } = await context.publicClient.simulateContract({
|
|
3889
|
+
address: deployments.perpManager,
|
|
3890
|
+
abi: PERP_MANAGER_ABI,
|
|
3891
|
+
functionName: "adjustMargin",
|
|
3892
|
+
args: [{ posId: positionId, marginDelta }],
|
|
3893
|
+
account: context.walletClient.account
|
|
3894
|
+
});
|
|
3895
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
3896
|
+
return { txHash };
|
|
3897
|
+
}, `adjustMargin for position ${positionId}`);
|
|
3898
|
+
}
|
|
3899
|
+
async function adjustNotional(context, positionId, usdDelta, perpLimit) {
|
|
3900
|
+
return withErrorHandling(async () => {
|
|
3901
|
+
const deployments = context.deployments();
|
|
3902
|
+
if (usdDelta > 0n) {
|
|
3903
|
+
const currentAllowance = await context.publicClient.readContract({
|
|
3904
|
+
address: deployments.usdc,
|
|
3905
|
+
abi: erc20Abi3,
|
|
3906
|
+
functionName: "allowance",
|
|
3907
|
+
args: [context.walletClient.account.address, deployments.perpManager],
|
|
3908
|
+
blockTag: "latest"
|
|
3909
|
+
});
|
|
3910
|
+
if (currentAllowance < usdDelta) {
|
|
3911
|
+
await approveUsdc(context, usdDelta);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
const { request } = await context.publicClient.simulateContract({
|
|
3915
|
+
address: deployments.perpManager,
|
|
3916
|
+
abi: PERP_MANAGER_ABI,
|
|
3917
|
+
functionName: "adjustNotional",
|
|
3918
|
+
args: [{ posId: positionId, usdDelta, perpLimit }],
|
|
3919
|
+
account: context.walletClient.account
|
|
3920
|
+
});
|
|
3921
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
3922
|
+
return { txHash };
|
|
3923
|
+
}, `adjustNotional for position ${positionId}`);
|
|
3924
|
+
}
|
|
3719
3925
|
|
|
3720
3926
|
// src/functions/position.ts
|
|
3721
|
-
|
|
3927
|
+
import { decodeErrorResult as decodeErrorResult2, decodeEventLog as decodeEventLog3, formatUnits as formatUnits3 } from "viem";
|
|
3722
3928
|
function getPositionPerpId(positionData) {
|
|
3723
3929
|
return positionData.perpId;
|
|
3724
3930
|
}
|
|
@@ -3746,6 +3952,131 @@ function getPositionEffectiveMargin(positionData) {
|
|
|
3746
3952
|
function getPositionIsLiquidatable(positionData) {
|
|
3747
3953
|
return positionData.liveDetails.isLiquidatable;
|
|
3748
3954
|
}
|
|
3955
|
+
async function quoteClosePosition(context, positionId) {
|
|
3956
|
+
return withErrorHandling(async () => {
|
|
3957
|
+
const result = await context.publicClient.readContract({
|
|
3958
|
+
address: context.deployments().perpManager,
|
|
3959
|
+
abi: PERP_MANAGER_ABI,
|
|
3960
|
+
functionName: "quoteClosePosition",
|
|
3961
|
+
args: [positionId]
|
|
3962
|
+
});
|
|
3963
|
+
const [unexpectedReason, pnl, funding, netMargin, wasLiquidated] = result;
|
|
3964
|
+
if (unexpectedReason && unexpectedReason !== "0x") {
|
|
3965
|
+
let errorName = "Quote simulation failed";
|
|
3966
|
+
try {
|
|
3967
|
+
const decoded = decodeErrorResult2({
|
|
3968
|
+
abi: PERP_MANAGER_ABI,
|
|
3969
|
+
data: unexpectedReason
|
|
3970
|
+
});
|
|
3971
|
+
errorName = decoded.errorName;
|
|
3972
|
+
} catch {
|
|
3973
|
+
}
|
|
3974
|
+
throw new Error(errorName);
|
|
3975
|
+
}
|
|
3976
|
+
return {
|
|
3977
|
+
pnl: Number(formatUnits3(pnl, 6)),
|
|
3978
|
+
// Negate so positive = user receives funding, matching live details convention
|
|
3979
|
+
funding: -Number(formatUnits3(funding, 6)),
|
|
3980
|
+
netMargin: Number(formatUnits3(uint256ToInt256(netMargin), 6)),
|
|
3981
|
+
wasLiquidated
|
|
3982
|
+
};
|
|
3983
|
+
}, `quoteClosePosition for position ${positionId}`);
|
|
3984
|
+
}
|
|
3985
|
+
async function closePositionWithQuote(context, _perpId, positionId, slippageTolerance = 0.01) {
|
|
3986
|
+
return withErrorHandling(async () => {
|
|
3987
|
+
const rawData = await context.getPositionRawData(positionId);
|
|
3988
|
+
const isMaker = rawData.makerDetails !== null;
|
|
3989
|
+
const result = await context.publicClient.readContract({
|
|
3990
|
+
address: context.deployments().perpManager,
|
|
3991
|
+
abi: PERP_MANAGER_ABI,
|
|
3992
|
+
functionName: "quoteClosePosition",
|
|
3993
|
+
args: [positionId]
|
|
3994
|
+
});
|
|
3995
|
+
const [unexpectedReason, quotePnl] = result;
|
|
3996
|
+
if (unexpectedReason && unexpectedReason !== "0x") {
|
|
3997
|
+
throw new Error("Quote failed \u2014 position may be invalid or already closed");
|
|
3998
|
+
}
|
|
3999
|
+
let contractParams = {
|
|
4000
|
+
posId: positionId,
|
|
4001
|
+
minAmt0Out: 0n,
|
|
4002
|
+
minAmt1Out: 0n,
|
|
4003
|
+
maxAmt1In: 0n
|
|
4004
|
+
};
|
|
4005
|
+
const canonicalPerpId = rawData.perpId;
|
|
4006
|
+
if (!isMaker) {
|
|
4007
|
+
const isLong = rawData.entryPerpDelta > 0n;
|
|
4008
|
+
const slippageBps = BigInt(Math.ceil(slippageTolerance * 1e4));
|
|
4009
|
+
const absEntryUsd = rawData.entryUsdDelta < 0n ? -rawData.entryUsdDelta : rawData.entryUsdDelta;
|
|
4010
|
+
if (isLong) {
|
|
4011
|
+
const swapRevenue = quotePnl + absEntryUsd;
|
|
4012
|
+
const minOut = swapRevenue > 0n ? swapRevenue - swapRevenue * slippageBps / 10000n : 0n;
|
|
4013
|
+
contractParams = {
|
|
4014
|
+
posId: positionId,
|
|
4015
|
+
minAmt0Out: 0n,
|
|
4016
|
+
minAmt1Out: minOut,
|
|
4017
|
+
maxAmt1In: 0n
|
|
4018
|
+
};
|
|
4019
|
+
} else {
|
|
4020
|
+
const swapCost = absEntryUsd - quotePnl;
|
|
4021
|
+
const maxIn = swapCost > 0n ? swapCost + swapCost * slippageBps / 10000n : 0n;
|
|
4022
|
+
contractParams = {
|
|
4023
|
+
posId: positionId,
|
|
4024
|
+
minAmt0Out: 0n,
|
|
4025
|
+
minAmt1Out: 0n,
|
|
4026
|
+
maxAmt1In: maxIn
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
const { request } = await context.publicClient.simulateContract({
|
|
4031
|
+
address: context.deployments().perpManager,
|
|
4032
|
+
abi: PERP_MANAGER_ABI,
|
|
4033
|
+
functionName: "closePosition",
|
|
4034
|
+
args: [contractParams],
|
|
4035
|
+
account: context.walletClient.account,
|
|
4036
|
+
gas: 500000n
|
|
4037
|
+
});
|
|
4038
|
+
const txHash = await context.walletClient.writeContract(request);
|
|
4039
|
+
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
4040
|
+
hash: txHash
|
|
4041
|
+
});
|
|
4042
|
+
if (receipt.status === "reverted") {
|
|
4043
|
+
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
4044
|
+
}
|
|
4045
|
+
let newPositionId = null;
|
|
4046
|
+
for (const log of receipt.logs) {
|
|
4047
|
+
try {
|
|
4048
|
+
const decoded = decodeEventLog3({
|
|
4049
|
+
abi: PERP_MANAGER_ABI,
|
|
4050
|
+
data: log.data,
|
|
4051
|
+
topics: log.topics,
|
|
4052
|
+
eventName: "PositionOpened"
|
|
4053
|
+
});
|
|
4054
|
+
const eventPerpId = decoded.args.perpId.toLowerCase();
|
|
4055
|
+
const eventPosId = decoded.args.posId;
|
|
4056
|
+
if (eventPerpId === canonicalPerpId.toLowerCase() && eventPosId !== positionId) {
|
|
4057
|
+
newPositionId = eventPosId;
|
|
4058
|
+
break;
|
|
4059
|
+
}
|
|
4060
|
+
} catch (_e) {
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
if (!newPositionId) {
|
|
4064
|
+
return { position: null, txHash };
|
|
4065
|
+
}
|
|
4066
|
+
return {
|
|
4067
|
+
position: {
|
|
4068
|
+
perpId: canonicalPerpId,
|
|
4069
|
+
positionId: newPositionId,
|
|
4070
|
+
liveDetails: await getPositionLiveDetailsFromContract(
|
|
4071
|
+
context,
|
|
4072
|
+
canonicalPerpId,
|
|
4073
|
+
newPositionId
|
|
4074
|
+
)
|
|
4075
|
+
},
|
|
4076
|
+
txHash
|
|
4077
|
+
};
|
|
4078
|
+
}, `closePositionWithQuote for position ${positionId}`);
|
|
4079
|
+
}
|
|
3749
4080
|
async function closePosition(context, perpId, positionId, params) {
|
|
3750
4081
|
return withErrorHandling(async () => {
|
|
3751
4082
|
const contractParams = {
|
|
@@ -3764,14 +4095,16 @@ async function closePosition(context, perpId, positionId, params) {
|
|
|
3764
4095
|
// Provide explicit gas limit to avoid estimation issues
|
|
3765
4096
|
});
|
|
3766
4097
|
const txHash = await context.walletClient.writeContract(request);
|
|
3767
|
-
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
4098
|
+
const receipt = await context.publicClient.waitForTransactionReceipt({
|
|
4099
|
+
hash: txHash
|
|
4100
|
+
});
|
|
3768
4101
|
if (receipt.status === "reverted") {
|
|
3769
4102
|
throw new Error(`Transaction reverted. Hash: ${txHash}`);
|
|
3770
4103
|
}
|
|
3771
4104
|
let newPositionId = null;
|
|
3772
4105
|
for (const log of receipt.logs) {
|
|
3773
4106
|
try {
|
|
3774
|
-
const decoded = (
|
|
4107
|
+
const decoded = decodeEventLog3({
|
|
3775
4108
|
abi: PERP_MANAGER_ABI,
|
|
3776
4109
|
data: log.data,
|
|
3777
4110
|
topics: log.topics,
|
|
@@ -3814,9 +4147,10 @@ async function getPositionLiveDetailsFromContract(context, _perpId, positionId)
|
|
|
3814
4147
|
);
|
|
3815
4148
|
}
|
|
3816
4149
|
return {
|
|
3817
|
-
pnl: Number((
|
|
3818
|
-
|
|
3819
|
-
|
|
4150
|
+
pnl: Number(formatUnits3(pnl, 6)),
|
|
4151
|
+
// Negate so positive = user receives funding, matching quoteClosePosition convention
|
|
4152
|
+
fundingPayment: -Number(formatUnits3(funding, 6)),
|
|
4153
|
+
effectiveMargin: Number(formatUnits3(uint256ToInt256(netMargin), 6)),
|
|
3820
4154
|
isLiquidatable: wasLiquidated
|
|
3821
4155
|
};
|
|
3822
4156
|
}, `getPositionLiveDetailsFromContract for position ${positionId}`);
|
|
@@ -3860,6 +4194,34 @@ function calculateLiquidationPrice(rawData, isLong) {
|
|
|
3860
4194
|
return liqPrice;
|
|
3861
4195
|
}
|
|
3862
4196
|
}
|
|
4197
|
+
function calculatePnlPercentage(pnl, funding, margin) {
|
|
4198
|
+
const totalPnl = pnl + funding;
|
|
4199
|
+
const initialMargin = margin - totalPnl;
|
|
4200
|
+
if (initialMargin <= 0) return 0;
|
|
4201
|
+
return totalPnl / initialMargin * 100;
|
|
4202
|
+
}
|
|
4203
|
+
function calculateClosePositionParams(opts) {
|
|
4204
|
+
if (opts.isMaker) {
|
|
4205
|
+
return { minAmt0Out: 0, minAmt1Out: 0, maxAmt1In: 0 };
|
|
4206
|
+
}
|
|
4207
|
+
if (typeof opts.isLong !== "boolean") {
|
|
4208
|
+
throw new Error("isLong must be explicitly set for taker positions");
|
|
4209
|
+
}
|
|
4210
|
+
const absNotional = Math.abs(opts.notional);
|
|
4211
|
+
if (opts.isLong) {
|
|
4212
|
+
return {
|
|
4213
|
+
minAmt0Out: 0,
|
|
4214
|
+
minAmt1Out: Math.max(0, absNotional * (1 - opts.slippagePercent / 100)),
|
|
4215
|
+
maxAmt1In: 0
|
|
4216
|
+
};
|
|
4217
|
+
} else {
|
|
4218
|
+
return {
|
|
4219
|
+
minAmt0Out: 0,
|
|
4220
|
+
minAmt1Out: 0,
|
|
4221
|
+
maxAmt1In: absNotional * (1 + opts.slippagePercent / 100)
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
3863
4225
|
|
|
3864
4226
|
// src/functions/user.ts
|
|
3865
4227
|
function getUserUsdcBalance(userData) {
|
|
@@ -3871,14 +4233,20 @@ function getUserOpenPositions(userData) {
|
|
|
3871
4233
|
function getUserWalletAddress(userData) {
|
|
3872
4234
|
return userData.walletAddress;
|
|
3873
4235
|
}
|
|
3874
|
-
|
|
3875
|
-
0 && (module.exports = {
|
|
4236
|
+
export {
|
|
3876
4237
|
BEACON_ABI,
|
|
3877
4238
|
BIGINT_1E6,
|
|
3878
4239
|
ContractError,
|
|
4240
|
+
DEFAULT_MAKER_SLIPPAGE_TOLERANCE,
|
|
3879
4241
|
ErrorCategory,
|
|
3880
4242
|
ErrorSource,
|
|
4243
|
+
INT256_THRESHOLD,
|
|
3881
4244
|
InsufficientFundsError,
|
|
4245
|
+
MAX_PRICE,
|
|
4246
|
+
MAX_TICK,
|
|
4247
|
+
MAX_UINT128,
|
|
4248
|
+
MIN_PRICE,
|
|
4249
|
+
MIN_TICK,
|
|
3882
4250
|
NUMBER_1E6,
|
|
3883
4251
|
OpenPosition,
|
|
3884
4252
|
PERP_MANAGER_ABI,
|
|
@@ -3887,17 +4255,31 @@ function getUserWalletAddress(userData) {
|
|
|
3887
4255
|
Q96,
|
|
3888
4256
|
RPCError,
|
|
3889
4257
|
TransactionRejectedError,
|
|
4258
|
+
UINT256_MAX,
|
|
3890
4259
|
ValidationError,
|
|
4260
|
+
adjustMargin,
|
|
4261
|
+
adjustNotional,
|
|
4262
|
+
applySlippage,
|
|
3891
4263
|
approveUsdc,
|
|
4264
|
+
calculateAlignedTicks,
|
|
4265
|
+
calculateClosePositionParams,
|
|
3892
4266
|
calculateEntryPrice,
|
|
3893
4267
|
calculateLeverage,
|
|
3894
4268
|
calculateLiquidationPrice,
|
|
3895
4269
|
calculateLiquidityForTargetRatio,
|
|
4270
|
+
calculatePnlPercentage,
|
|
3896
4271
|
calculatePositionSize,
|
|
3897
4272
|
calculatePositionValue,
|
|
3898
4273
|
closePosition,
|
|
4274
|
+
closePositionWithQuote,
|
|
4275
|
+
convertFundingDiffX96ToPercentPerPeriod,
|
|
4276
|
+
convertFundingPerSecondX96ToPercentPerDay,
|
|
4277
|
+
convertFundingPerSecondX96ToPercentPerMinute,
|
|
3899
4278
|
createPerp,
|
|
3900
4279
|
estimateLiquidity,
|
|
4280
|
+
getFundingRate,
|
|
4281
|
+
getIndexTWAP,
|
|
4282
|
+
getIndexValue,
|
|
3901
4283
|
getPerpBeacon,
|
|
3902
4284
|
getPerpBounds,
|
|
3903
4285
|
getPerpFees,
|
|
@@ -3914,6 +4296,7 @@ function getUserWalletAddress(userData) {
|
|
|
3914
4296
|
getPositionPerpId,
|
|
3915
4297
|
getPositionPnl,
|
|
3916
4298
|
getRpcUrl,
|
|
4299
|
+
getUsdcAllowance,
|
|
3917
4300
|
getUserOpenPositions,
|
|
3918
4301
|
getUserUsdcBalance,
|
|
3919
4302
|
getUserWalletAddress,
|
|
@@ -3923,12 +4306,16 @@ function getUserWalletAddress(userData) {
|
|
|
3923
4306
|
parseContractError,
|
|
3924
4307
|
priceToSqrtPriceX96,
|
|
3925
4308
|
priceToTick,
|
|
4309
|
+
quoteClosePosition,
|
|
4310
|
+
quoteOpenMakerPosition,
|
|
4311
|
+
quoteTakerPosition,
|
|
3926
4312
|
scale6Decimals,
|
|
3927
4313
|
scaleFrom6Decimals,
|
|
3928
4314
|
scaleFromX96,
|
|
3929
4315
|
scaleToX96,
|
|
3930
4316
|
sqrtPriceX96ToPrice,
|
|
3931
4317
|
tickToPrice,
|
|
4318
|
+
uint256ToInt256,
|
|
3932
4319
|
withErrorHandling
|
|
3933
|
-
}
|
|
4320
|
+
};
|
|
3934
4321
|
//# sourceMappingURL=index.js.map
|