@megatao/sdk 1.1.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/.env.example +37 -0
- package/CHANGELOG.md +19 -0
- package/README.md +199 -0
- package/bin/alf +4 -0
- package/cli/README.md +198 -0
- package/cli/TEST_MANUAL.md +577 -0
- package/cli/commands/account.ts +545 -0
- package/cli/commands/funding.ts +481 -0
- package/cli/commands/liquidation.ts +523 -0
- package/cli/commands/market.ts +590 -0
- package/cli/commands/orders.ts +395 -0
- package/cli/commands/position.ts +1085 -0
- package/cli/commands/shared/positionUtils.ts +239 -0
- package/cli/commands/trading.ts +483 -0
- package/cli/commands/utils.ts +281 -0
- package/cli/commands/vault.ts +522 -0
- package/cli/index.ts +169 -0
- package/cli/interactive.ts +530 -0
- package/cli/utils/client.ts +457 -0
- package/cli/utils/config.ts +226 -0
- package/cli/utils/display.ts +258 -0
- package/cli/utils/index.ts +10 -0
- package/cli/utils/prompts.ts +364 -0
- package/config.example.json +23 -0
- package/dist/AlphaFuturesClient.d.ts +36 -0
- package/dist/AlphaFuturesClient.d.ts.map +1 -0
- package/dist/AlphaFuturesClient.js +116 -0
- package/dist/AlphaFuturesClient.js.map +1 -0
- package/dist/abi/Alpha.json +5987 -0
- package/dist/abi/abis.d.ts +319 -0
- package/dist/abi/abis.d.ts.map +1 -0
- package/dist/abi/abis.js +128 -0
- package/dist/abi/abis.js.map +1 -0
- package/dist/abi/index.d.ts +11 -0
- package/dist/abi/index.d.ts.map +1 -0
- package/dist/abi/index.js +15 -0
- package/dist/abi/index.js.map +1 -0
- package/dist/config/contracts.config.d.ts +70 -0
- package/dist/config/contracts.config.d.ts.map +1 -0
- package/dist/config/contracts.config.js +137 -0
- package/dist/config/contracts.config.js.map +1 -0
- package/dist/config/environments/alpha.config.d.ts +17 -0
- package/dist/config/environments/alpha.config.d.ts.map +1 -0
- package/dist/config/environments/alpha.config.js +140 -0
- package/dist/config/environments/alpha.config.js.map +1 -0
- package/dist/config/environments/beta.config.d.ts +16 -0
- package/dist/config/environments/beta.config.d.ts.map +1 -0
- package/dist/config/environments/beta.config.js +131 -0
- package/dist/config/environments/beta.config.js.map +1 -0
- package/dist/config/environments/dev.config.d.ts +13 -0
- package/dist/config/environments/dev.config.d.ts.map +1 -0
- package/dist/config/environments/dev.config.js +123 -0
- package/dist/config/environments/dev.config.js.map +1 -0
- package/dist/config/environments/index.d.ts +48 -0
- package/dist/config/environments/index.d.ts.map +1 -0
- package/dist/config/environments/index.js +81 -0
- package/dist/config/environments/index.js.map +1 -0
- package/dist/config/environments/localhost.config.d.ts +16 -0
- package/dist/config/environments/localhost.config.d.ts.map +1 -0
- package/dist/config/environments/localhost.config.js +152 -0
- package/dist/config/environments/localhost.config.js.map +1 -0
- package/dist/config/environments/prod.config.d.ts +20 -0
- package/dist/config/environments/prod.config.d.ts.map +1 -0
- package/dist/config/environments/prod.config.js +143 -0
- package/dist/config/environments/prod.config.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/constants/assets.d.ts +76 -0
- package/dist/constants/assets.d.ts.map +1 -0
- package/dist/constants/assets.js +277 -0
- package/dist/constants/assets.js.map +1 -0
- package/dist/constants/contracts.d.ts +41 -0
- package/dist/constants/contracts.d.ts.map +1 -0
- package/dist/constants/contracts.js +57 -0
- package/dist/constants/contracts.js.map +1 -0
- package/dist/constants/index.d.ts +36 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +75 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/networks.d.ts +32 -0
- package/dist/constants/networks.d.ts.map +1 -0
- package/dist/constants/networks.js +174 -0
- package/dist/constants/networks.js.map +1 -0
- package/dist/contracts/index.d.ts +5 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +21 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/viem/AlphaViem.d.ts +518 -0
- package/dist/contracts/viem/AlphaViem.d.ts.map +1 -0
- package/dist/contracts/viem/AlphaViem.js +1287 -0
- package/dist/contracts/viem/AlphaViem.js.map +1 -0
- package/dist/contracts/viem/PriceOracleViem.d.ts +71 -0
- package/dist/contracts/viem/PriceOracleViem.d.ts.map +1 -0
- package/dist/contracts/viem/PriceOracleViem.js +212 -0
- package/dist/contracts/viem/PriceOracleViem.js.map +1 -0
- package/dist/contracts/viem/index.d.ts +9 -0
- package/dist/contracts/viem/index.d.ts.map +1 -0
- package/dist/contracts/viem/index.js +17 -0
- package/dist/contracts/viem/index.js.map +1 -0
- package/dist/errors/index.d.ts +44 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +83 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/types/alpha.d.ts +299 -0
- package/dist/types/alpha.d.ts.map +1 -0
- package/dist/types/alpha.js +6 -0
- package/dist/types/alpha.js.map +1 -0
- package/dist/types/client.d.ts +24 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/client.js +13 -0
- package/dist/types/client.js.map +1 -0
- package/dist/types/contracts.d.ts +48 -0
- package/dist/types/contracts.d.ts.map +1 -0
- package/dist/types/contracts.js +6 -0
- package/dist/types/contracts.js.map +1 -0
- package/dist/types/funding.d.ts +27 -0
- package/dist/types/funding.d.ts.map +1 -0
- package/dist/types/funding.js +6 -0
- package/dist/types/funding.js.map +1 -0
- package/dist/types/index.d.ts +92 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +47 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/liquidation.d.ts +20 -0
- package/dist/types/liquidation.d.ts.map +1 -0
- package/dist/types/liquidation.js +6 -0
- package/dist/types/liquidation.js.map +1 -0
- package/dist/types/margin.d.ts +29 -0
- package/dist/types/margin.d.ts.map +1 -0
- package/dist/types/margin.js +6 -0
- package/dist/types/margin.js.map +1 -0
- package/dist/types/oracle.d.ts +21 -0
- package/dist/types/oracle.d.ts.map +1 -0
- package/dist/types/oracle.js +6 -0
- package/dist/types/oracle.js.map +1 -0
- package/dist/types/positions.d.ts +43 -0
- package/dist/types/positions.d.ts.map +1 -0
- package/dist/types/positions.js +13 -0
- package/dist/types/positions.js.map +1 -0
- package/dist/utils/calculations.d.ts +84 -0
- package/dist/utils/calculations.d.ts.map +1 -0
- package/dist/utils/calculations.js +155 -0
- package/dist/utils/calculations.js.map +1 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +129 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/events.d.ts +40 -0
- package/dist/utils/events.d.ts.map +1 -0
- package/dist/utils/events.js +73 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/format.d.ts +40 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +86 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +26 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/network.d.ts +52 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +192 -0
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/positionCalculations.d.ts +145 -0
- package/dist/utils/positionCalculations.d.ts.map +1 -0
- package/dist/utils/positionCalculations.js +278 -0
- package/dist/utils/positionCalculations.js.map +1 -0
- package/dist/utils/validation.d.ts +28 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +68 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs/README.md +40 -0
- package/docs/api/API.md +831 -0
- package/docs/guides/GETTING_STARTED.md +316 -0
- package/docs/guides/TRADING_GUIDE.md +677 -0
- package/docs/integration/INTEGRATION_GUIDE.md +1679 -0
- package/docs/integration/VIEM_INTEGRATION.md +294 -0
- package/docs/reference/CLI_QUICK_REFERENCE.md +197 -0
- package/docs/reference/TROUBLESHOOTING.md +922 -0
- package/package.json +113 -0
- package/src/AlphaFuturesClient.ts +158 -0
- package/src/abi/.gitkeep +1 -0
- package/src/abi/Alpha.json +5987 -0
- package/src/abi/README.md +99 -0
- package/src/abi/abis.ts +131 -0
- package/src/abi/index.ts +13 -0
- package/src/config/contracts.config.ts +186 -0
- package/src/config/environments/alpha.config.ts +139 -0
- package/src/config/environments/beta.config.ts +130 -0
- package/src/config/environments/dev.config.ts +122 -0
- package/src/config/environments/index.ts +87 -0
- package/src/config/environments/localhost.config.ts +153 -0
- package/src/config/environments/prod.config.ts +142 -0
- package/src/config/index.ts +29 -0
- package/src/constants/assets.ts +299 -0
- package/src/constants/contracts.ts +64 -0
- package/src/constants/index.ts +69 -0
- package/src/constants/networks.ts +182 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/viem/AlphaViem.ts +1615 -0
- package/src/contracts/viem/PriceOracleViem.ts +272 -0
- package/src/contracts/viem/index.ts +11 -0
- package/src/errors/index.ts +87 -0
- package/src/index.ts +59 -0
- package/src/types/VIEM_TYPES_README.md +70 -0
- package/src/types/alpha.ts +358 -0
- package/src/types/client.ts +27 -0
- package/src/types/contracts.ts +74 -0
- package/src/types/funding.ts +31 -0
- package/src/types/index.ts +108 -0
- package/src/types/liquidation.ts +23 -0
- package/src/types/margin.ts +34 -0
- package/src/types/oracle.ts +24 -0
- package/src/types/positions.ts +48 -0
- package/src/utils/calculations.ts +175 -0
- package/src/utils/errors.ts +147 -0
- package/src/utils/events.ts +98 -0
- package/src/utils/format.ts +84 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/network.ts +212 -0
- package/src/utils/positionCalculations.ts +317 -0
- package/src/utils/validation.ts +76 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Commands - Manage trading positions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { parseEther } from 'viem';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
import { AlphaFuturesClient } from '../../src';
|
|
11
|
+
import { formatUSD, formatTAO, formatPercentage } from '../../src/utils';
|
|
12
|
+
import { calculateMaintenanceMargin } from '../../src/utils/calculations';
|
|
13
|
+
import { getClient, handleError, validateMarketAddresses, getMarketAddress } from '../utils/client';
|
|
14
|
+
import type { Address } from 'viem';
|
|
15
|
+
import { confirmAction, selectFromList, validateDirection } from '../utils/prompts';
|
|
16
|
+
import { displayPositionDetails, calculatePnL } from '../utils/display';
|
|
17
|
+
import { detectParadigm, openPositionFromCollateral } from './shared/positionUtils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates position ID format and provides helpful error message
|
|
21
|
+
* @param positionId Position ID to validate
|
|
22
|
+
* @throws Error with helpful message if format is incorrect
|
|
23
|
+
*/
|
|
24
|
+
function validatePositionIdFormat(positionId: string): void {
|
|
25
|
+
if (!positionId) {
|
|
26
|
+
throw new Error(`Position ID cannot be empty.
|
|
27
|
+
|
|
28
|
+
Expected format: 32 hex characters after 0x (bytes16)
|
|
29
|
+
Example: 0x00000000000000000000000000000001`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hex = positionId.startsWith('0x') ? positionId.slice(2) : positionId;
|
|
33
|
+
|
|
34
|
+
// Check if contains only hex characters
|
|
35
|
+
if (!/^[0-9a-fA-F]*$/.test(hex)) {
|
|
36
|
+
throw new Error(`Invalid position ID format: "${positionId}"
|
|
37
|
+
|
|
38
|
+
Position ID must contain only hexadecimal characters (0-9, a-f, A-F).
|
|
39
|
+
Expected format: 32 hex characters after 0x (bytes16)
|
|
40
|
+
Example: 0x00000000000000000000000000000001`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Warn if too long (would be truncated)
|
|
44
|
+
if (hex.length > 32) {
|
|
45
|
+
console.warn(
|
|
46
|
+
chalk.yellow(`ā ļø Position ID is longer than 32 hex characters and will be truncated.
|
|
47
|
+
Given: ${positionId}
|
|
48
|
+
Will use: 0x${hex.slice(0, 32)}`),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert position ID to proper bytes16 format (32 hex characters)
|
|
55
|
+
* @param positionId Raw position ID (could be short like 0x123 or full bytes16)
|
|
56
|
+
* @returns Properly formatted bytes16 position ID
|
|
57
|
+
*/
|
|
58
|
+
function normalizePositionId(positionId: string): `0x${string}` {
|
|
59
|
+
// Validate format first
|
|
60
|
+
validatePositionIdFormat(positionId);
|
|
61
|
+
|
|
62
|
+
// Remove 0x prefix if present
|
|
63
|
+
const hex = positionId.startsWith('0x') ? positionId.slice(2) : positionId;
|
|
64
|
+
|
|
65
|
+
// Pad to 32 characters (16 bytes) or truncate if too long
|
|
66
|
+
const normalizedHex = hex.length > 32 ? hex.slice(0, 32) : hex.padEnd(32, '0');
|
|
67
|
+
|
|
68
|
+
return `0x${normalizedHex}` as `0x${string}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function positionCommands(program: Command) {
|
|
72
|
+
const position = program.command('position').alias('pos').description('Manage trading positions');
|
|
73
|
+
|
|
74
|
+
// Open position command
|
|
75
|
+
position
|
|
76
|
+
.command('open')
|
|
77
|
+
.description(
|
|
78
|
+
`Open a new position
|
|
79
|
+
|
|
80
|
+
Two modes available:
|
|
81
|
+
|
|
82
|
+
1. Collateral-First (NEW - Recommended):
|
|
83
|
+
$ alpha-futures position open --asset BITMIND --direction long --collateral 100 --leverage 3.5
|
|
84
|
+
|
|
85
|
+
2. Size-First (Legacy - Backward Compatible):
|
|
86
|
+
$ alpha-futures position open --asset BITMIND --direction long --size 30 --margin 10
|
|
87
|
+
`,
|
|
88
|
+
)
|
|
89
|
+
.requiredOption('-a, --asset <asset>', 'Asset to trade (e.g., BITMIND)')
|
|
90
|
+
.requiredOption('-d, --direction <direction>', 'Direction: long or short')
|
|
91
|
+
.option('-c, --collateral <amount>', 'Collateral amount in USD (e.g., 100 for $100) [NEW]')
|
|
92
|
+
.option('-s, --size <size>', 'Position size in USD [LEGACY]')
|
|
93
|
+
.option('-m, --margin <margin>', 'Margin amount in TAO [LEGACY]')
|
|
94
|
+
.option(
|
|
95
|
+
'-l, --leverage <leverage>',
|
|
96
|
+
'Leverage multiplier (supports decimals, e.g., 3.5)',
|
|
97
|
+
parseFloat,
|
|
98
|
+
)
|
|
99
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const spinner = ora('Preparing position...').start();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const client = await getClient(program.opts());
|
|
105
|
+
const userAddress = await client.getSignerAddress();
|
|
106
|
+
const alpha = client.getAlpha();
|
|
107
|
+
|
|
108
|
+
// Detect paradigm
|
|
109
|
+
const paradigm = detectParadigm({
|
|
110
|
+
size: options.size,
|
|
111
|
+
margin: options.margin,
|
|
112
|
+
collateral: options.collateral,
|
|
113
|
+
leverage: options.leverage,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (paradigm === 'invalid') {
|
|
117
|
+
spinner.stop();
|
|
118
|
+
if (options.collateral && (options.size || options.margin)) {
|
|
119
|
+
console.error(chalk.red('ā Cannot mix paradigms! Use either:'));
|
|
120
|
+
console.error(chalk.cyan(' ⢠Collateral-First: --collateral and --leverage'));
|
|
121
|
+
console.error(chalk.cyan(' ⢠Size-First: --size and --margin'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
} else {
|
|
124
|
+
console.error(chalk.red('ā Missing required options! Use either:'));
|
|
125
|
+
console.error(chalk.cyan(' ⢠Collateral-First: --collateral and --leverage'));
|
|
126
|
+
console.error(chalk.cyan(' ⢠Size-First: --size and --margin (or --leverage)'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse common inputs
|
|
132
|
+
const asset = options.asset.toUpperCase();
|
|
133
|
+
|
|
134
|
+
// Validate direction with strict checking
|
|
135
|
+
let direction: 'long' | 'short';
|
|
136
|
+
try {
|
|
137
|
+
direction = validateDirection(options.direction);
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
spinner.fail(error.message);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isLong = direction === 'long';
|
|
144
|
+
|
|
145
|
+
// Get market data
|
|
146
|
+
const marketAddress = getMarketAddress(asset);
|
|
147
|
+
const currentPrice = await client.oracle.getPrice(marketAddress);
|
|
148
|
+
|
|
149
|
+
spinner.stop();
|
|
150
|
+
|
|
151
|
+
// Execute based on paradigm
|
|
152
|
+
if (paradigm === 'collateral-first') {
|
|
153
|
+
// NEW PARADIGM
|
|
154
|
+
console.log(chalk.bold('š Using Collateral-First mode'));
|
|
155
|
+
|
|
156
|
+
const collateral = parseFloat(options.collateral);
|
|
157
|
+
const leverage = options.leverage || 3; // Default 3x
|
|
158
|
+
|
|
159
|
+
// Validate leverage precision (0.1x increments)
|
|
160
|
+
const leverageDecimal = leverage % 0.1;
|
|
161
|
+
if (leverageDecimal > 0.001 && leverageDecimal < 0.099) {
|
|
162
|
+
// Allow some floating point error
|
|
163
|
+
console.error(
|
|
164
|
+
chalk.red('ā Leverage must be in 0.1x increments (e.g., 1.5, 2.3, 10.0)'),
|
|
165
|
+
);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate leverage range
|
|
170
|
+
if (leverage < 1 || leverage > 30) {
|
|
171
|
+
console.error(chalk.red('ā Leverage must be between 1x and 30x'));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check available balance
|
|
176
|
+
const collateralWei = parseEther(collateral.toString());
|
|
177
|
+
const available = await alpha.getAvailableMargin(userAddress);
|
|
178
|
+
if (collateralWei > available) {
|
|
179
|
+
console.error(chalk.red('ā Insufficient available balance'));
|
|
180
|
+
console.log(` Required: ${formatTAO(collateralWei)} TAO`);
|
|
181
|
+
console.log(` Available: ${formatTAO(available)} TAO`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Display position preview
|
|
186
|
+
console.log(chalk.bold('\nš Position Preview:'));
|
|
187
|
+
console.log(` Asset: ${chalk.cyan(asset)}`);
|
|
188
|
+
console.log(` Direction: ${isLong ? chalk.green('LONG ā') : chalk.red('SHORT ā')}`);
|
|
189
|
+
console.log(` Current Price: ${formatUSD(currentPrice)}`);
|
|
190
|
+
console.log(` Collateral: $${collateral}`);
|
|
191
|
+
console.log(` Leverage: ${chalk.yellow(leverage.toFixed(1) + 'x')}`);
|
|
192
|
+
console.log(` Position Value: $${(collateral * leverage).toFixed(2)}`);
|
|
193
|
+
|
|
194
|
+
// Calculate and display fees
|
|
195
|
+
const positionValueUSD = parseEther((collateral * leverage).toString());
|
|
196
|
+
const tradingFee = (positionValueUSD * 10n) / 10000n; // 0.1%
|
|
197
|
+
console.log(` Trading Fee: ${formatUSD(tradingFee)} (0.1%)`);
|
|
198
|
+
|
|
199
|
+
// Show liquidation price estimate
|
|
200
|
+
const liquidationPrice = isLong
|
|
201
|
+
? (currentPrice * (100n - 2000n)) / 100n // 20% below for longs
|
|
202
|
+
: (currentPrice * (100n + 2000n)) / 100n; // 20% above for shorts
|
|
203
|
+
console.log(` Est. Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
|
|
204
|
+
|
|
205
|
+
// Confirm action
|
|
206
|
+
if (!options.yes) {
|
|
207
|
+
const confirmed = await confirmAction('Open this position?');
|
|
208
|
+
if (!confirmed) {
|
|
209
|
+
console.log(chalk.yellow('Position opening cancelled'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
spinner.start('Opening position...');
|
|
215
|
+
|
|
216
|
+
const hash = await openPositionFromCollateral(
|
|
217
|
+
alpha,
|
|
218
|
+
marketAddress,
|
|
219
|
+
isLong,
|
|
220
|
+
collateral,
|
|
221
|
+
leverage,
|
|
222
|
+
currentPrice,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
spinner.text = 'Waiting for confirmation...';
|
|
226
|
+
const publicClient = client.getPublicClient();
|
|
227
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
228
|
+
|
|
229
|
+
spinner.succeed('Position opened successfully!');
|
|
230
|
+
console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
|
|
231
|
+
console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
|
|
232
|
+
|
|
233
|
+
// Parse position opened event
|
|
234
|
+
const positionEvent = alpha.parsePositionOpenedEvent(receipt);
|
|
235
|
+
if (positionEvent) {
|
|
236
|
+
console.log(chalk.green(`\nā Position ID: ${positionEvent.positionId}`));
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// LEGACY PARADIGM
|
|
240
|
+
console.log(chalk.bold('š Using Size-First mode (legacy)'));
|
|
241
|
+
|
|
242
|
+
const positionSizeUSD = parseEther(options.size); // USD amount
|
|
243
|
+
const marginAmount = parseEther(options.margin);
|
|
244
|
+
|
|
245
|
+
// Calculate leverage
|
|
246
|
+
const leverage = options.leverage
|
|
247
|
+
? parseFloat(options.leverage)
|
|
248
|
+
: Number(positionSizeUSD) / Number(marginAmount);
|
|
249
|
+
|
|
250
|
+
// Validate leverage
|
|
251
|
+
if (leverage > 30 || leverage < 1) {
|
|
252
|
+
console.error(chalk.red('ā Invalid leverage: must be between 1x and 30x'));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check available balance
|
|
257
|
+
const available = await alpha.getAvailableMargin(userAddress);
|
|
258
|
+
if (marginAmount > available) {
|
|
259
|
+
console.error(chalk.red('ā Insufficient available balance'));
|
|
260
|
+
console.log(` Required: ${formatTAO(marginAmount)} TAO`);
|
|
261
|
+
console.log(` Available: ${formatTAO(available)} TAO`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Convert USD position size to token amount
|
|
266
|
+
const positionSize = (positionSizeUSD * parseEther('1')) / currentPrice;
|
|
267
|
+
|
|
268
|
+
// Display position preview
|
|
269
|
+
console.log(chalk.bold('\nš Position Preview:'));
|
|
270
|
+
console.log(` Asset: ${chalk.cyan(asset)}`);
|
|
271
|
+
console.log(` Direction: ${isLong ? chalk.green('LONG ā') : chalk.red('SHORT ā')}`);
|
|
272
|
+
console.log(` Current Price: ${formatUSD(currentPrice)}`);
|
|
273
|
+
console.log(
|
|
274
|
+
` Position Size: ${formatUSD(positionSizeUSD)} (${formatUSD(positionSize)} tokens)`,
|
|
275
|
+
);
|
|
276
|
+
console.log(` Margin: ${formatTAO(marginAmount)} TAO`);
|
|
277
|
+
console.log(` Leverage: ${chalk.yellow(leverage.toFixed(2) + 'x')}`);
|
|
278
|
+
|
|
279
|
+
// Calculate and display fees
|
|
280
|
+
const tradingFee = (positionSizeUSD * 10n) / 10000n; // 0.1% of USD notional
|
|
281
|
+
console.log(` Trading Fee: ${formatUSD(tradingFee)} (0.1%)`);
|
|
282
|
+
|
|
283
|
+
// Show liquidation price
|
|
284
|
+
const liquidationPrice = isLong
|
|
285
|
+
? (currentPrice * (100n - 2000n)) / 100n // 20% below for longs
|
|
286
|
+
: (currentPrice * (100n + 2000n)) / 100n; // 20% above for shorts
|
|
287
|
+
console.log(` Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
|
|
288
|
+
|
|
289
|
+
// Confirm action
|
|
290
|
+
if (!options.yes) {
|
|
291
|
+
const confirmed = await confirmAction('Open this position?');
|
|
292
|
+
if (!confirmed) {
|
|
293
|
+
console.log(chalk.yellow('Position opening cancelled'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
spinner.start('Opening position...');
|
|
299
|
+
const leverageBigInt = parseEther(leverage.toString()); // Convert to PRECISION (1e18)
|
|
300
|
+
const maxSlippage = 100n; // 1% max slippage
|
|
301
|
+
|
|
302
|
+
const hash = await alpha.openMarketPosition(
|
|
303
|
+
marketAddress,
|
|
304
|
+
isLong,
|
|
305
|
+
marginAmount,
|
|
306
|
+
leverageBigInt,
|
|
307
|
+
maxSlippage,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
spinner.text = 'Waiting for confirmation...';
|
|
311
|
+
const publicClient = client.getPublicClient();
|
|
312
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
313
|
+
|
|
314
|
+
spinner.succeed('Position opened successfully!');
|
|
315
|
+
console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
|
|
316
|
+
console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
|
|
317
|
+
|
|
318
|
+
// Parse position opened event
|
|
319
|
+
const positionEvent = alpha.parsePositionOpenedEvent(receipt);
|
|
320
|
+
if (positionEvent) {
|
|
321
|
+
console.log(chalk.green(`\nā Position ID: ${positionEvent.positionId}`));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
spinner.fail('Failed to open position');
|
|
326
|
+
handleError(error, program.opts());
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Close position command
|
|
331
|
+
position
|
|
332
|
+
.command('close <positionId>')
|
|
333
|
+
.description('Close an existing position')
|
|
334
|
+
.option('-s, --size <size>', 'Size to close (partial close)')
|
|
335
|
+
.option('-p, --percent <percent>', 'Percentage to close (e.g., 50 for 50%)')
|
|
336
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
337
|
+
.addHelpText(
|
|
338
|
+
'after',
|
|
339
|
+
`
|
|
340
|
+
Examples:
|
|
341
|
+
$ alpha-futures position close 0x00000000000000000000000000000001
|
|
342
|
+
$ alpha-futures position close 0x12345000000000000000000000000000 --percent 50
|
|
343
|
+
$ alpha-futures position close 0xabcd1234000000000000000000000000 --size 500 --yes
|
|
344
|
+
|
|
345
|
+
Note: Position ID must be in bytes16 format (32 hex characters after 0x).
|
|
346
|
+
Use "alpha-futures position find" to get your position IDs.`,
|
|
347
|
+
)
|
|
348
|
+
.action(async (positionId, options) => {
|
|
349
|
+
const spinner = ora('Loading position...').start();
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const client = await getClient(program.opts());
|
|
353
|
+
const alpha = client.getAlpha();
|
|
354
|
+
|
|
355
|
+
// Get position details - position ID needs to be converted to bytes16 format
|
|
356
|
+
const normalizedPositionId = normalizePositionId(positionId);
|
|
357
|
+
const position = await alpha.getPosition(normalizedPositionId);
|
|
358
|
+
|
|
359
|
+
// Note: Monolith positions don't have isActive field, we check size > 0
|
|
360
|
+
if (position.notionalValue === 0n) {
|
|
361
|
+
spinner.fail('Position is not active');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Calculate close size
|
|
366
|
+
let closeSize: bigint;
|
|
367
|
+
if (options.percent) {
|
|
368
|
+
const percent = BigInt(Math.floor(parseFloat(options.percent) * 100));
|
|
369
|
+
closeSize = (position.notionalValue * percent) / 10000n;
|
|
370
|
+
} else if (options.size) {
|
|
371
|
+
closeSize = parseEther(options.size);
|
|
372
|
+
} else {
|
|
373
|
+
// Close entire position - pass 0 to smart contract for full close
|
|
374
|
+
closeSize = 0n;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Validate close size (only if not closing full position)
|
|
378
|
+
if (closeSize > 0n && closeSize > position.notionalValue) {
|
|
379
|
+
spinner.fail('Close size exceeds position size');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
spinner.stop();
|
|
384
|
+
|
|
385
|
+
// Get current price - derive market address from position or use environment lookup
|
|
386
|
+
// For now, using BITMIND as default since most tests use BITMIND
|
|
387
|
+
const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
|
|
388
|
+
const currentPrice = await client.oracle.getPrice(marketAddress);
|
|
389
|
+
// Use position.notionalValue for P&L calculation when closing full position
|
|
390
|
+
const sizeForPnl = closeSize === 0n ? position.notionalValue : closeSize;
|
|
391
|
+
const pnl = calculatePnL(position, currentPrice, sizeForPnl);
|
|
392
|
+
|
|
393
|
+
// Display close preview
|
|
394
|
+
console.log(chalk.bold('\nš Position Close Preview:'));
|
|
395
|
+
displayPositionDetails(position, currentPrice);
|
|
396
|
+
|
|
397
|
+
console.log(chalk.bold('\nClose Details:'));
|
|
398
|
+
const displaySize = closeSize === 0n ? position.notionalValue : closeSize;
|
|
399
|
+
const closePercent =
|
|
400
|
+
position.notionalValue > 0n
|
|
401
|
+
? ((Number(displaySize) * 100) / Number(position.notionalValue)).toFixed(1)
|
|
402
|
+
: '100.0';
|
|
403
|
+
console.log(` Size to Close: ${formatUSD(displaySize)} (${closePercent}%)`);
|
|
404
|
+
console.log(
|
|
405
|
+
` P&L: ${pnl >= 0 ? chalk.green('+' + formatUSD(pnl)) : chalk.red(formatUSD(pnl))}`,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Confirm action
|
|
409
|
+
if (!options.yes) {
|
|
410
|
+
const confirmed = await confirmAction('Close this position?');
|
|
411
|
+
if (!confirmed) {
|
|
412
|
+
console.log(chalk.yellow('Position close cancelled'));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
spinner.start('Closing position...');
|
|
418
|
+
const hash = await alpha.closePosition(normalizedPositionId, closeSize);
|
|
419
|
+
|
|
420
|
+
spinner.text = 'Waiting for confirmation...';
|
|
421
|
+
const publicClient = client.getPublicClient();
|
|
422
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
423
|
+
|
|
424
|
+
spinner.succeed('Position closed successfully!');
|
|
425
|
+
console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
|
|
426
|
+
console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
|
|
427
|
+
|
|
428
|
+
// Parse position closed event
|
|
429
|
+
const positionEvent = alpha.parsePositionClosedEvent(receipt);
|
|
430
|
+
if (positionEvent) {
|
|
431
|
+
const realizedPnL = positionEvent.pnl;
|
|
432
|
+
console.log(
|
|
433
|
+
chalk.bold(
|
|
434
|
+
`\nš° Realized P&L: ${realizedPnL >= 0 ? chalk.green('+' + formatTAO(realizedPnL) + ' TAO') : chalk.red(formatTAO(realizedPnL) + ' TAO')}`,
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
console.log(
|
|
438
|
+
chalk.cyan(`š³ P&L ${realizedPnL >= 0 ? 'credited to' : 'debited from'} TAO wallet`),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Show updated TAO wallet balance
|
|
442
|
+
try {
|
|
443
|
+
const userAddress = await client.getSignerAddress();
|
|
444
|
+
const publicClient = client.getPublicClient();
|
|
445
|
+
const taoTokenAddress = client.getCollateralTokenAddress();
|
|
446
|
+
if (taoTokenAddress) {
|
|
447
|
+
const updatedTaoBalance = (await publicClient.readContract({
|
|
448
|
+
address: taoTokenAddress,
|
|
449
|
+
abi: [
|
|
450
|
+
{
|
|
451
|
+
name: 'balanceOf',
|
|
452
|
+
type: 'function',
|
|
453
|
+
stateMutability: 'view',
|
|
454
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
455
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
functionName: 'balanceOf',
|
|
459
|
+
args: [userAddress],
|
|
460
|
+
})) as bigint;
|
|
461
|
+
console.log(
|
|
462
|
+
chalk.gray(`š³ Updated TAO wallet balance: ${formatTAO(updatedTaoBalance)} TAO`),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
// Ignore error fetching updated balance
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
spinner.fail('Failed to close position');
|
|
471
|
+
handleError(error, program.opts());
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// List positions command
|
|
476
|
+
position
|
|
477
|
+
.command('list [address]')
|
|
478
|
+
.alias('ls')
|
|
479
|
+
.description('List all positions')
|
|
480
|
+
.option('-a, --active', 'Show only active positions')
|
|
481
|
+
.option('-c, --closed', 'Show only closed positions')
|
|
482
|
+
.option('-s, --sort <field>', 'Sort by: size, pnl, margin, age', 'size')
|
|
483
|
+
.action(async (address, options) => {
|
|
484
|
+
const spinner = ora('Loading positions...').start();
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const client = await getClient(program.opts());
|
|
488
|
+
const alpha = client.getAlpha();
|
|
489
|
+
const userAddress = address || (await client.getSignerAddress());
|
|
490
|
+
|
|
491
|
+
if (!userAddress) {
|
|
492
|
+
spinner.fail('No address provided');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Get all position IDs
|
|
497
|
+
const positionIds = await alpha.getUserPositions(userAddress);
|
|
498
|
+
|
|
499
|
+
if (positionIds.length === 0) {
|
|
500
|
+
spinner.stop();
|
|
501
|
+
console.log(chalk.yellow('\nNo positions found'));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Fetch position details
|
|
506
|
+
const positions = await Promise.all(positionIds.map((id) => alpha.getPosition(id)));
|
|
507
|
+
|
|
508
|
+
// Filter positions - alpha positions use notionalValue > 0 to indicate active
|
|
509
|
+
let filteredPositions = positions;
|
|
510
|
+
if (options.active) {
|
|
511
|
+
filteredPositions = positions.filter((p) => p.notionalValue > 0n);
|
|
512
|
+
} else if (options.closed) {
|
|
513
|
+
filteredPositions = positions.filter((p) => p.notionalValue === 0n);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
spinner.stop();
|
|
517
|
+
|
|
518
|
+
if (filteredPositions.length === 0) {
|
|
519
|
+
console.log(chalk.yellow('\nNo matching positions found'));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Get current prices for P&L calculation
|
|
524
|
+
// Use BITMIND as default market for testing
|
|
525
|
+
const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
|
|
526
|
+
const currentPrice = await client.oracle.getPrice(marketAddress);
|
|
527
|
+
const priceMap = new Map<string, bigint>();
|
|
528
|
+
priceMap.set('ALPHA', currentPrice);
|
|
529
|
+
|
|
530
|
+
// Create positions table
|
|
531
|
+
console.log(chalk.bold(`\nš Positions for ${chalk.cyan(userAddress)}:`));
|
|
532
|
+
|
|
533
|
+
const table = new Table({
|
|
534
|
+
head: ['ID', 'Asset', 'Direction', 'Size', 'Margin', 'Entry', 'Current', 'P&L', 'Status'],
|
|
535
|
+
colWidths: [6, 8, 10, 15, 15, 12, 12, 15, 8],
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Sort positions
|
|
539
|
+
const sortedPositions = [...filteredPositions].sort((a, b) => {
|
|
540
|
+
switch (options.sort) {
|
|
541
|
+
case 'size':
|
|
542
|
+
return Number(b.notionalValue - a.notionalValue);
|
|
543
|
+
case 'margin':
|
|
544
|
+
return Number(b.margin - a.margin);
|
|
545
|
+
case 'age':
|
|
546
|
+
return Number(b.lastUpdated - a.lastUpdated);
|
|
547
|
+
case 'pnl':
|
|
548
|
+
if (a.notionalValue === 0n || b.notionalValue === 0n) return 0;
|
|
549
|
+
const pnlA = calculatePnL(a, currentPrice, a.notionalValue);
|
|
550
|
+
const pnlB = calculatePnL(b, currentPrice, b.notionalValue);
|
|
551
|
+
return Number(pnlB - pnlA);
|
|
552
|
+
default:
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
let totalPnL = 0n;
|
|
558
|
+
let totalMargin = 0n;
|
|
559
|
+
|
|
560
|
+
for (const pos of sortedPositions) {
|
|
561
|
+
const isActive = pos.notionalValue > 0n;
|
|
562
|
+
const pnl = isActive ? calculatePnL(pos, currentPrice, pos.notionalValue) : 0n;
|
|
563
|
+
|
|
564
|
+
if (isActive) {
|
|
565
|
+
totalPnL += pnl;
|
|
566
|
+
totalMargin += pos.margin;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const directionIcon = pos.isLong ? chalk.green('LONG ā') : chalk.red('SHORT ā');
|
|
570
|
+
const pnlDisplay = !isActive
|
|
571
|
+
? '-'
|
|
572
|
+
: pnl >= 0
|
|
573
|
+
? chalk.green('+' + formatUSD(pnl))
|
|
574
|
+
: chalk.red(formatUSD(pnl));
|
|
575
|
+
const statusDisplay = isActive ? chalk.green('Active') : chalk.gray('Closed');
|
|
576
|
+
|
|
577
|
+
// Calculate USD value of position: notionalValue represents the position's notional value
|
|
578
|
+
const positionValueUSD = pos.notionalValue;
|
|
579
|
+
|
|
580
|
+
table.push([
|
|
581
|
+
positionIds.indexOf(positionIds.find((id) => id === positionIds[0]) || positionIds[0]) +
|
|
582
|
+
1, // Simple index for now
|
|
583
|
+
'ALPHA', // Placeholder asset
|
|
584
|
+
directionIcon,
|
|
585
|
+
formatUSD(positionValueUSD),
|
|
586
|
+
formatTAO(pos.margin),
|
|
587
|
+
formatUSD(pos.entryPrice),
|
|
588
|
+
isActive ? formatUSD(currentPrice) : '-',
|
|
589
|
+
pnlDisplay,
|
|
590
|
+
statusDisplay,
|
|
591
|
+
]);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log(table.toString());
|
|
595
|
+
|
|
596
|
+
// Show summary for active positions
|
|
597
|
+
const activePositions = filteredPositions.filter((p) => p.notionalValue > 0n);
|
|
598
|
+
if (activePositions.length > 0) {
|
|
599
|
+
console.log(chalk.bold('\nSummary:'));
|
|
600
|
+
console.log(` Active Positions: ${activePositions.length}`);
|
|
601
|
+
console.log(` Total Margin: ${formatTAO(totalMargin)} TAO`);
|
|
602
|
+
console.log(
|
|
603
|
+
` Total P&L: ${totalPnL >= 0 ? chalk.green('+' + formatUSD(totalPnL)) : chalk.red(formatUSD(totalPnL))}`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
spinner.fail('Failed to load positions');
|
|
608
|
+
handleError(error, program.opts());
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Position info command
|
|
613
|
+
position
|
|
614
|
+
.command('info <positionId>')
|
|
615
|
+
.description('Get detailed information about a position')
|
|
616
|
+
.option('-w, --watch', 'Watch position in real-time')
|
|
617
|
+
.addHelpText(
|
|
618
|
+
'after',
|
|
619
|
+
`
|
|
620
|
+
Examples:
|
|
621
|
+
$ alpha-futures position info 0x00000000000000000000000000000001
|
|
622
|
+
$ alpha-futures position info 0x12345000000000000000000000000000 --watch
|
|
623
|
+
|
|
624
|
+
Note: Position ID must be in bytes16 format (32 hex characters after 0x).
|
|
625
|
+
Use "alpha-futures position find" to get your position IDs.`,
|
|
626
|
+
)
|
|
627
|
+
.action(async (positionId, options) => {
|
|
628
|
+
const spinner = ora('Loading position...').start();
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const client = await getClient(program.opts());
|
|
632
|
+
const alpha = client.getAlpha();
|
|
633
|
+
|
|
634
|
+
const displayInfo = async () => {
|
|
635
|
+
const normalizedPositionId = normalizePositionId(positionId);
|
|
636
|
+
const position = await alpha.getPosition(normalizedPositionId);
|
|
637
|
+
// Use BITMIND as default market for testing
|
|
638
|
+
const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
|
|
639
|
+
const currentPrice = await client.oracle.getPrice(marketAddress);
|
|
640
|
+
|
|
641
|
+
if (!options.watch) spinner.stop();
|
|
642
|
+
|
|
643
|
+
console.clear();
|
|
644
|
+
console.log(chalk.bold(`\nš Position #${positionId} Details:`));
|
|
645
|
+
|
|
646
|
+
displayPositionDetails(position, currentPrice);
|
|
647
|
+
|
|
648
|
+
if (position.notionalValue > 0n) {
|
|
649
|
+
// Calculate additional metrics
|
|
650
|
+
const pnl = calculatePnL(position, currentPrice, position.notionalValue);
|
|
651
|
+
const pnlPercent = (Number(pnl) * 100) / Number(position.margin);
|
|
652
|
+
const marginRatio = await alpha.getMarginRatio(normalizedPositionId);
|
|
653
|
+
|
|
654
|
+
console.log(chalk.bold('\nš° Financial Metrics:'));
|
|
655
|
+
console.log(
|
|
656
|
+
` Unrealized P&L: ${pnl >= 0 ? chalk.green('+' + formatUSD(pnl)) : chalk.red(formatUSD(pnl))}`,
|
|
657
|
+
);
|
|
658
|
+
console.log(
|
|
659
|
+
` P&L %: ${pnlPercent >= 0 ? chalk.green('+' + pnlPercent.toFixed(2) + '%') : chalk.red(pnlPercent.toFixed(2) + '%')}`,
|
|
660
|
+
);
|
|
661
|
+
console.log(` Margin Ratio: ${formatPercentage(marginRatio)}`);
|
|
662
|
+
|
|
663
|
+
// Use liquidation price from position structure
|
|
664
|
+
const liquidationPrice = position.liquidationPrice;
|
|
665
|
+
|
|
666
|
+
const distanceToLiquidation = position.isLong
|
|
667
|
+
? ((Number(currentPrice) - Number(liquidationPrice)) / Number(currentPrice)) * 100
|
|
668
|
+
: ((Number(liquidationPrice) - Number(currentPrice)) / Number(currentPrice)) * 100;
|
|
669
|
+
|
|
670
|
+
console.log(` Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
|
|
671
|
+
console.log(` Distance to Liquidation: ${distanceToLiquidation.toFixed(2)}%`);
|
|
672
|
+
|
|
673
|
+
// Show funding rate impact - use BITMIND market address
|
|
674
|
+
const fundingRate = await alpha.getFundingRate(marketAddress);
|
|
675
|
+
const fundingDirection = position.isLong
|
|
676
|
+
? fundingRate > 0n
|
|
677
|
+
? 'Pay'
|
|
678
|
+
: 'Receive'
|
|
679
|
+
: fundingRate > 0n
|
|
680
|
+
? 'Receive'
|
|
681
|
+
: 'Pay';
|
|
682
|
+
|
|
683
|
+
console.log(chalk.bold('\nš Funding Rate:'));
|
|
684
|
+
console.log(` Current Rate: ${formatPercentage(fundingRate)} per 8h`);
|
|
685
|
+
console.log(` Direction: ${fundingDirection}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (program.opts().json) {
|
|
689
|
+
console.log(
|
|
690
|
+
'\n' +
|
|
691
|
+
JSON.stringify(
|
|
692
|
+
{
|
|
693
|
+
position: {
|
|
694
|
+
size: position.notionalValue.toString(),
|
|
695
|
+
margin: position.margin.toString(),
|
|
696
|
+
entryPrice: position.entryPrice.toString(),
|
|
697
|
+
isLong: position.isLong,
|
|
698
|
+
lastUpdated: position.lastUpdated.toString(),
|
|
699
|
+
fundingIndex: position.fundingIndex.toString(),
|
|
700
|
+
leverage: position.leverage.toString(),
|
|
701
|
+
liquidationPrice: position.liquidationPrice.toString(),
|
|
702
|
+
},
|
|
703
|
+
currentPrice: currentPrice.toString(),
|
|
704
|
+
},
|
|
705
|
+
null,
|
|
706
|
+
2,
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
await displayInfo();
|
|
713
|
+
|
|
714
|
+
if (options.watch) {
|
|
715
|
+
// Update every 5 seconds
|
|
716
|
+
setInterval(displayInfo, 5000);
|
|
717
|
+
|
|
718
|
+
// Handle exit
|
|
719
|
+
process.on('SIGINT', () => {
|
|
720
|
+
console.log(chalk.yellow('\n\nStopped watching'));
|
|
721
|
+
process.exit(0);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
spinner.fail('Failed to load position');
|
|
726
|
+
handleError(error, program.opts());
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Position ID lookup command
|
|
731
|
+
position
|
|
732
|
+
.command('id <trader> [asset]')
|
|
733
|
+
.description('Get position ID for a trader and asset')
|
|
734
|
+
.action(async (trader, asset) => {
|
|
735
|
+
const spinner = ora('Looking up position ID...').start();
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const client = await getClient(program.opts());
|
|
739
|
+
const alpha = client.getAlpha();
|
|
740
|
+
|
|
741
|
+
const targetAsset = asset || 'BITMIND';
|
|
742
|
+
const marketAddress = getMarketAddress(targetAsset);
|
|
743
|
+
|
|
744
|
+
// Check if position exists
|
|
745
|
+
const hasPosition = await alpha.hasPosition(trader as Address, marketAddress);
|
|
746
|
+
|
|
747
|
+
if (!hasPosition) {
|
|
748
|
+
spinner.stop();
|
|
749
|
+
console.log(chalk.yellow(`\nNo active position found`));
|
|
750
|
+
console.log(` Trader: ${chalk.cyan(trader)}`);
|
|
751
|
+
console.log(` Asset: ${chalk.cyan(targetAsset.toUpperCase())}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Get position ID
|
|
756
|
+
const positionId = await alpha.getPositionId(trader as Address, marketAddress);
|
|
757
|
+
|
|
758
|
+
spinner.stop();
|
|
759
|
+
|
|
760
|
+
console.log(chalk.bold('\nš Position ID Lookup:'));
|
|
761
|
+
console.log(` Trader: ${chalk.cyan(trader)}`);
|
|
762
|
+
console.log(` Asset: ${chalk.cyan(targetAsset.toUpperCase())}`);
|
|
763
|
+
console.log(` Market: ${chalk.gray(marketAddress)}`);
|
|
764
|
+
console.log(` Position ID: ${chalk.green(positionId)}`);
|
|
765
|
+
|
|
766
|
+
// Get basic position info
|
|
767
|
+
try {
|
|
768
|
+
const position = await alpha.getPosition(positionId);
|
|
769
|
+
console.log(chalk.bold('\nš Quick Info:'));
|
|
770
|
+
console.log(` Size: ${formatUSD(position.notionalValue)}`);
|
|
771
|
+
console.log(
|
|
772
|
+
` Direction: ${position.isLong ? chalk.green('LONG ā') : chalk.red('SHORT ā')}`,
|
|
773
|
+
);
|
|
774
|
+
console.log(` Margin: ${formatTAO(position.margin)} TAO`);
|
|
775
|
+
console.log(` Entry Price: ${formatUSD(position.entryPrice)}`);
|
|
776
|
+
|
|
777
|
+
console.log(
|
|
778
|
+
chalk.gray(
|
|
779
|
+
'\nš” Use "alpha-futures position info ' + positionId + '" for detailed information',
|
|
780
|
+
),
|
|
781
|
+
);
|
|
782
|
+
} catch (error) {
|
|
783
|
+
console.log(chalk.yellow('\nā ļø Could not fetch position details'));
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
spinner.fail('Failed to lookup position ID');
|
|
787
|
+
handleError(error, program.opts());
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Find positions command
|
|
792
|
+
position
|
|
793
|
+
.command('find [trader]')
|
|
794
|
+
.description('Find all position IDs for a trader')
|
|
795
|
+
.option('-a, --asset <asset>', 'Filter by specific asset')
|
|
796
|
+
.action(async (trader, options) => {
|
|
797
|
+
const spinner = ora('Finding positions...').start();
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const client = await getClient(program.opts());
|
|
801
|
+
const alpha = client.getAlpha();
|
|
802
|
+
const walletClient = client.getWalletClient();
|
|
803
|
+
|
|
804
|
+
// Use provided trader or wallet address
|
|
805
|
+
const userAddress = trader || walletClient?.account?.address;
|
|
806
|
+
|
|
807
|
+
if (!userAddress) {
|
|
808
|
+
spinner.fail('No trader address provided and no wallet client available');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (options.asset) {
|
|
813
|
+
// Find position for specific asset
|
|
814
|
+
const marketAddress = getMarketAddress(options.asset);
|
|
815
|
+
|
|
816
|
+
const hasPosition = await alpha.hasPosition(userAddress as Address, marketAddress);
|
|
817
|
+
|
|
818
|
+
if (hasPosition) {
|
|
819
|
+
const positionId = await alpha.getPositionId(userAddress as Address, marketAddress);
|
|
820
|
+
const position = await alpha.getPosition(positionId);
|
|
821
|
+
|
|
822
|
+
spinner.stop();
|
|
823
|
+
|
|
824
|
+
console.log(
|
|
825
|
+
chalk.bold(`\nšÆ Position Found: ${chalk.cyan(options.asset.toUpperCase())}`),
|
|
826
|
+
);
|
|
827
|
+
console.log(` Trader: ${chalk.cyan(userAddress)}`);
|
|
828
|
+
console.log(` Position ID: ${chalk.green(positionId)}`);
|
|
829
|
+
console.log(` Size: ${formatUSD(position.notionalValue)}`);
|
|
830
|
+
console.log(
|
|
831
|
+
` Direction: ${position.isLong ? chalk.green('LONG ā') : chalk.red('SHORT ā')}`,
|
|
832
|
+
);
|
|
833
|
+
console.log(
|
|
834
|
+
` Status: ${position.notionalValue > 0n ? chalk.green('Active') : chalk.gray('Closed')}`,
|
|
835
|
+
);
|
|
836
|
+
} else {
|
|
837
|
+
spinner.stop();
|
|
838
|
+
console.log(chalk.yellow(`\nNo position found for ${options.asset.toUpperCase()}`));
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
// Find all positions for trader
|
|
842
|
+
const positionIds = await alpha.getUserPositions(userAddress as Address);
|
|
843
|
+
|
|
844
|
+
if (positionIds.length === 0) {
|
|
845
|
+
spinner.stop();
|
|
846
|
+
console.log(chalk.yellow(`\nNo positions found for trader: ${userAddress}`));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
spinner.stop();
|
|
851
|
+
|
|
852
|
+
console.log(chalk.bold(`\nš All Positions: ${chalk.cyan(userAddress)}`));
|
|
853
|
+
console.log(` Total Positions: ${positionIds.length}`);
|
|
854
|
+
|
|
855
|
+
const table = new Table({
|
|
856
|
+
head: ['#', 'Position ID', 'Size', 'Direction', 'Status'],
|
|
857
|
+
colWidths: [5, 42, 15, 12, 10],
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
for (let i = 0; i < positionIds.length; i++) {
|
|
861
|
+
try {
|
|
862
|
+
const position = await alpha.getPosition(positionIds[i]);
|
|
863
|
+
const status =
|
|
864
|
+
position.notionalValue > 0n ? chalk.green('Active') : chalk.gray('Closed');
|
|
865
|
+
const direction = position.isLong ? chalk.green('LONG') : chalk.red('SHORT');
|
|
866
|
+
|
|
867
|
+
table.push([
|
|
868
|
+
i + 1,
|
|
869
|
+
positionIds[i],
|
|
870
|
+
formatUSD(position.notionalValue),
|
|
871
|
+
direction,
|
|
872
|
+
status,
|
|
873
|
+
]);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
table.push([
|
|
876
|
+
i + 1,
|
|
877
|
+
positionIds[i],
|
|
878
|
+
chalk.red('Error'),
|
|
879
|
+
chalk.red('Error'),
|
|
880
|
+
chalk.red('Error'),
|
|
881
|
+
]);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
console.log(table.toString());
|
|
886
|
+
console.log(
|
|
887
|
+
chalk.gray(
|
|
888
|
+
'\nš” Use "alpha-futures position info <positionId>" for detailed information',
|
|
889
|
+
),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
} catch (error) {
|
|
893
|
+
spinner.fail('Failed to find positions');
|
|
894
|
+
handleError(error, program.opts());
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Modify position command
|
|
899
|
+
position
|
|
900
|
+
.command('modify <positionId>')
|
|
901
|
+
.description('Modify an existing position (add/remove margin)')
|
|
902
|
+
.option('-a, --add <amount>', 'Add margin to position')
|
|
903
|
+
.option('-r, --remove <amount>', 'Remove margin from position')
|
|
904
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
905
|
+
.addHelpText(
|
|
906
|
+
'after',
|
|
907
|
+
`
|
|
908
|
+
Examples:
|
|
909
|
+
$ alpha-futures position modify 0x00000000000000000000000000000001 --add 100
|
|
910
|
+
$ alpha-futures position modify 0x12345000000000000000000000000000 --remove 50 --yes
|
|
911
|
+
|
|
912
|
+
Note: Position ID must be in bytes16 format (32 hex characters after 0x).
|
|
913
|
+
Use "alpha-futures position find" to get your position IDs.`,
|
|
914
|
+
)
|
|
915
|
+
.action(async (positionId, options) => {
|
|
916
|
+
if (!options.add && !options.remove) {
|
|
917
|
+
console.error(chalk.red('ā Must specify either --add or --remove'));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const spinner = ora('Loading position...').start();
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
const client = await getClient(program.opts());
|
|
925
|
+
const alpha = client.getAlpha();
|
|
926
|
+
const normalizedPositionId = normalizePositionId(positionId);
|
|
927
|
+
const position = await alpha.getPosition(normalizedPositionId);
|
|
928
|
+
|
|
929
|
+
if (position.notionalValue === 0n) {
|
|
930
|
+
spinner.fail('Position is not active');
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
spinner.stop();
|
|
935
|
+
|
|
936
|
+
// Use BITMIND as default market for testing
|
|
937
|
+
const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
|
|
938
|
+
const currentPrice = await client.oracle.getPrice(marketAddress);
|
|
939
|
+
|
|
940
|
+
console.log(chalk.bold('\nš Current Position:'));
|
|
941
|
+
displayPositionDetails(position, currentPrice);
|
|
942
|
+
|
|
943
|
+
if (options.add) {
|
|
944
|
+
const addAmount = parseEther(options.add);
|
|
945
|
+
|
|
946
|
+
// Check available balance first
|
|
947
|
+
const userAddress = await client.getSignerAddress();
|
|
948
|
+
const availableMargin = await alpha.getAvailableMargin(userAddress);
|
|
949
|
+
if (addAmount > availableMargin) {
|
|
950
|
+
console.error(chalk.red('ā Insufficient available margin'));
|
|
951
|
+
console.log(` Required: ${formatTAO(addAmount)} TAO`);
|
|
952
|
+
console.log(` Available: ${formatTAO(availableMargin)} TAO`);
|
|
953
|
+
console.log(
|
|
954
|
+
chalk.yellow('š” Use "alpha-futures account deposit <amount>" to deposit more TAO'),
|
|
955
|
+
);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const newMargin = position.margin + addAmount;
|
|
960
|
+
const newLeverage =
|
|
961
|
+
position.notionalValue > 0n ? Number(position.notionalValue) / Number(newMargin) : 0;
|
|
962
|
+
|
|
963
|
+
console.log(chalk.bold('\nā Add Margin:'));
|
|
964
|
+
console.log(` Amount to Add: ${formatTAO(addAmount)} TAO`);
|
|
965
|
+
console.log(` New Margin: ${formatTAO(newMargin)} TAO`);
|
|
966
|
+
console.log(` New Leverage: ${newLeverage.toFixed(2)}x`);
|
|
967
|
+
|
|
968
|
+
if (!options.yes && !(await confirmAction('Add margin to position?'))) {
|
|
969
|
+
console.log(chalk.yellow('Cancelled'));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
spinner.start('Adding margin...');
|
|
974
|
+
try {
|
|
975
|
+
const hash = await alpha.addMargin(normalizedPositionId, addAmount);
|
|
976
|
+
spinner.text = 'Waiting for confirmation...';
|
|
977
|
+
const publicClient = client.getPublicClient();
|
|
978
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
979
|
+
spinner.succeed('Margin added successfully!');
|
|
980
|
+
console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
|
|
981
|
+
console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
|
|
982
|
+
} catch (error: any) {
|
|
983
|
+
spinner.fail('Failed to add margin');
|
|
984
|
+
if (error.message?.includes('insufficient funds')) {
|
|
985
|
+
console.error(chalk.red('\nā Insufficient TAO balance'));
|
|
986
|
+
console.log(
|
|
987
|
+
chalk.yellow(
|
|
988
|
+
'š” Make sure you have deposited enough TAO using "alpha-futures account deposit <amount>"',
|
|
989
|
+
),
|
|
990
|
+
);
|
|
991
|
+
} else if (error.message?.includes('revert')) {
|
|
992
|
+
console.error(chalk.red('\nā Contract reverted the transaction'));
|
|
993
|
+
console.log(chalk.gray('Reason: ' + (error.reason || error.message)));
|
|
994
|
+
} else {
|
|
995
|
+
console.error(chalk.red('\nā Error: ' + error.message));
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
} else if (options.remove) {
|
|
1000
|
+
const removeAmount = parseEther(options.remove);
|
|
1001
|
+
|
|
1002
|
+
// Validate remove amount
|
|
1003
|
+
if (removeAmount > position.margin) {
|
|
1004
|
+
console.error(chalk.red('ā Cannot remove more than current margin'));
|
|
1005
|
+
console.log(` Current margin: ${formatTAO(position.margin)} TAO`);
|
|
1006
|
+
console.log(` Requested removal: ${formatTAO(removeAmount)} TAO`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const newMargin = position.margin - removeAmount;
|
|
1011
|
+
|
|
1012
|
+
// Simple maintenance margin check: ensure position doesn't exceed max leverage (30x)
|
|
1013
|
+
// Minimum margin = position size / max leverage
|
|
1014
|
+
const maxLeverage = 30n;
|
|
1015
|
+
const minMarginForMaxLeverage = position.notionalValue / maxLeverage;
|
|
1016
|
+
|
|
1017
|
+
// Also ensure we don't go below 20% margin ratio (maintenance margin)
|
|
1018
|
+
// For a position, minimum margin ā position value * 0.2 / current_price
|
|
1019
|
+
// Simplified: position.notionalValue already includes leverage, so min = position.notionalValue / 5 (20%)
|
|
1020
|
+
const maintenanceMarginSimple = position.notionalValue / 5n; // 20% maintenance
|
|
1021
|
+
const minMargin =
|
|
1022
|
+
minMarginForMaxLeverage > maintenanceMarginSimple
|
|
1023
|
+
? minMarginForMaxLeverage
|
|
1024
|
+
: maintenanceMarginSimple;
|
|
1025
|
+
|
|
1026
|
+
if (newMargin < minMargin) {
|
|
1027
|
+
console.error(chalk.red('ā Cannot remove that much margin'));
|
|
1028
|
+
console.log(` Current margin: ${formatTAO(position.margin)} TAO`);
|
|
1029
|
+
console.log(` Requested removal: ${formatTAO(removeAmount)} TAO`);
|
|
1030
|
+
console.log(` New margin would be: ${formatTAO(newMargin)} TAO`);
|
|
1031
|
+
console.log(` Minimum required margin: ${formatTAO(minMargin)} TAO`);
|
|
1032
|
+
console.log(` Maximum removable: ${formatTAO(position.margin - minMargin)} TAO`);
|
|
1033
|
+
console.log(chalk.gray(' (Based on 30x max leverage and 20% maintenance margin)'));
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const newLeverage =
|
|
1038
|
+
position.notionalValue > 0n ? Number(position.notionalValue) / Number(newMargin) : 0;
|
|
1039
|
+
|
|
1040
|
+
// The margin validation above already handles max leverage, so this is just for display
|
|
1041
|
+
|
|
1042
|
+
console.log(chalk.bold('\nā Remove Margin:'));
|
|
1043
|
+
console.log(` Amount to Remove: ${formatTAO(removeAmount)} TAO`);
|
|
1044
|
+
console.log(` New Margin: ${formatTAO(newMargin)} TAO`);
|
|
1045
|
+
console.log(` New Leverage: ${newLeverage.toFixed(2)}x`);
|
|
1046
|
+
console.log(` Required Margin: ${formatTAO(minMargin)} TAO`);
|
|
1047
|
+
|
|
1048
|
+
if (!options.yes && !(await confirmAction('Remove margin from position?'))) {
|
|
1049
|
+
console.log(chalk.yellow('Cancelled'));
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
spinner.start('Removing margin...');
|
|
1054
|
+
try {
|
|
1055
|
+
const hash = await alpha.removeMargin(normalizedPositionId, removeAmount);
|
|
1056
|
+
spinner.text = 'Waiting for confirmation...';
|
|
1057
|
+
const publicClient = client.getPublicClient();
|
|
1058
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
1059
|
+
spinner.succeed('Margin removed successfully!');
|
|
1060
|
+
console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
|
|
1061
|
+
console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
|
|
1062
|
+
} catch (error: any) {
|
|
1063
|
+
spinner.fail('Failed to remove margin');
|
|
1064
|
+
if (error.message?.includes('insufficient margin')) {
|
|
1065
|
+
console.error(chalk.red('\nā Insufficient margin for removal'));
|
|
1066
|
+
console.log(
|
|
1067
|
+
chalk.yellow(
|
|
1068
|
+
'š” The contract rejected the removal. Position may be too close to liquidation.',
|
|
1069
|
+
),
|
|
1070
|
+
);
|
|
1071
|
+
} else if (error.message?.includes('revert')) {
|
|
1072
|
+
console.error(chalk.red('\nā Contract reverted the transaction'));
|
|
1073
|
+
console.log(chalk.gray('Reason: ' + (error.reason || error.message)));
|
|
1074
|
+
} else {
|
|
1075
|
+
console.error(chalk.red('\nā Error: ' + error.message));
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
spinner.fail('Failed to modify position');
|
|
1082
|
+
handleError(error, program.opts());
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|