mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat: add one-click close position for all exchanges
- Add handleClosePosition API endpoint in server.go - Add closePosition API function in frontend - Add close position button to positions table in App.tsx and TraderDashboard.tsx - Fix GetFullConfig to include passphrase field for OKX - Fix OKX CloseLong/CloseShort to use position quantity directly (already in contracts)
This commit is contained in:
117
api/server.go
117
api/server.go
@@ -135,6 +135,7 @@ func (s *Server) setupRoutes() {
|
||||
protected.POST("/traders/:id/stop", s.handleStopTrader)
|
||||
protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt)
|
||||
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
|
||||
protected.POST("/traders/:id/close-position", s.handleClosePosition)
|
||||
|
||||
// AI模型配置
|
||||
protected.GET("/models", s.handleGetModelConfigs)
|
||||
@@ -1098,6 +1099,122 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleClosePosition 一键平仓
|
||||
func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Symbol string `json:"symbol" binding:"required"`
|
||||
Side string `json:"side" binding:"required"` // "LONG" or "SHORT"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: symbol和side必填"})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("🔻 用户 %s 请求平仓: trader=%s, symbol=%s, side=%s", userID, traderID, req.Symbol, req.Side)
|
||||
|
||||
// 从数据库获取交易员配置(包含交易所信息)
|
||||
fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
traderConfig := fullConfig.Trader
|
||||
exchangeCfg := fullConfig.Exchange
|
||||
|
||||
if exchangeCfg == nil || !exchangeCfg.Enabled {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建临时 trader 执行平仓
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
switch traderConfig.ExchangeID {
|
||||
case "binance":
|
||||
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
|
||||
case "hyperliquid":
|
||||
tempTrader, createErr = trader.NewHyperliquidTrader(
|
||||
exchangeCfg.APIKey,
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = trader.NewAsterTrader(
|
||||
exchangeCfg.AsterUser,
|
||||
exchangeCfg.AsterSigner,
|
||||
exchangeCfg.AsterPrivateKey,
|
||||
)
|
||||
case "bybit":
|
||||
tempTrader = trader.NewBybitTrader(
|
||||
exchangeCfg.APIKey,
|
||||
exchangeCfg.SecretKey,
|
||||
)
|
||||
case "okx":
|
||||
tempTrader = trader.NewOKXTrader(
|
||||
exchangeCfg.APIKey,
|
||||
exchangeCfg.SecretKey,
|
||||
exchangeCfg.Passphrase,
|
||||
)
|
||||
case "lighter":
|
||||
if exchangeCfg.LighterAPIKeyPrivateKey != "" {
|
||||
tempTrader, createErr = trader.NewLighterTraderV2(
|
||||
exchangeCfg.LighterPrivateKey,
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
exchangeCfg.LighterAPIKeyPrivateKey,
|
||||
exchangeCfg.Testnet,
|
||||
)
|
||||
} else {
|
||||
tempTrader, createErr = trader.NewLighterTrader(
|
||||
exchangeCfg.LighterPrivateKey,
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
)
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"})
|
||||
return
|
||||
}
|
||||
|
||||
if createErr != nil {
|
||||
logger.Infof("⚠️ 创建临时 trader 失败: %v", createErr)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行平仓操作
|
||||
var result map[string]interface{}
|
||||
var closeErr error
|
||||
|
||||
if req.Side == "LONG" {
|
||||
result, closeErr = tempTrader.CloseLong(req.Symbol, 0) // 0 表示全部平仓
|
||||
} else if req.Side == "SHORT" {
|
||||
result, closeErr = tempTrader.CloseShort(req.Symbol, 0) // 0 表示全部平仓
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "side必须是LONG或SHORT"})
|
||||
return
|
||||
}
|
||||
|
||||
if closeErr != nil {
|
||||
logger.Infof("❌ 平仓失败: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("平仓失败: %v", closeErr)})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("✅ 平仓成功: symbol=%s, side=%s, result=%v", req.Symbol, req.Side, result)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "平仓成功",
|
||||
"symbol": req.Symbol,
|
||||
"side": req.Side,
|
||||
"result": result,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetModelConfigs 获取AI模型配置
|
||||
func (s *Server) handleGetModelConfigs(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
@@ -227,7 +227,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
t.created_at, t.updated_at,
|
||||
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key,
|
||||
COALESCE(a.custom_api_url, ''), COALESCE(a.custom_model_name, ''), a.created_at, a.updated_at,
|
||||
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
|
||||
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, COALESCE(e.passphrase, ''), e.testnet,
|
||||
COALESCE(e.hyperliquid_wallet_addr, ''), COALESCE(e.aster_user, ''), COALESCE(e.aster_signer, ''),
|
||||
COALESCE(e.aster_private_key, ''), COALESCE(e.lighter_wallet_addr, ''), COALESCE(e.lighter_private_key, ''),
|
||||
COALESCE(e.lighter_api_key_private_key, ''), e.created_at, e.updated_at
|
||||
@@ -244,7 +244,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
||||
&aiModel.CustomAPIURL, &aiModel.CustomModelName, &aiModelCreatedAt, &aiModelUpdatedAt,
|
||||
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
|
||||
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.HyperliquidWalletAddr,
|
||||
&exchange.APIKey, &exchange.SecretKey, &exchange.Passphrase, &exchange.Testnet, &exchange.HyperliquidWalletAddr,
|
||||
&exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
|
||||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey,
|
||||
&exchangeCreatedAt, &exchangeUpdatedAt,
|
||||
@@ -264,6 +264,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
aiModel.APIKey = s.decrypt(aiModel.APIKey)
|
||||
exchange.APIKey = s.decrypt(exchange.APIKey)
|
||||
exchange.SecretKey = s.decrypt(exchange.SecretKey)
|
||||
exchange.Passphrase = s.decrypt(exchange.Passphrase)
|
||||
exchange.AsterPrivateKey = s.decrypt(exchange.AsterPrivateKey)
|
||||
exchange.LighterPrivateKey = s.decrypt(exchange.LighterPrivateKey)
|
||||
exchange.LighterAPIKeyPrivateKey = s.decrypt(exchange.LighterAPIKeyPrivateKey)
|
||||
|
||||
@@ -195,7 +195,9 @@ func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, er
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
if okxResp.Code != "0" {
|
||||
// code=1 表示部分成功,需要检查 data 里的具体结果
|
||||
// code=2 表示全部失败
|
||||
if okxResp.Code != "0" && okxResp.Code != "1" {
|
||||
return nil, fmt.Errorf("OKX API错误: code=%s, msg=%s", okxResp.Code, okxResp.Msg)
|
||||
}
|
||||
|
||||
@@ -639,7 +641,7 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma
|
||||
func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
instId := t.convertSymbol(symbol)
|
||||
|
||||
// 如果数量为0,获取当前持仓
|
||||
// 如果数量为0,获取当前持仓(positionAmt 就是张数)
|
||||
if quantity == 0 {
|
||||
positions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
@@ -647,7 +649,7 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity = pos["positionAmt"].(float64) // 这已经是张数
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -656,19 +658,17 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
||||
}
|
||||
}
|
||||
|
||||
// 获取合约信息
|
||||
// 获取合约信息用于格式化张数
|
||||
inst, err := t.getInstrument(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取合约信息失败: %w", err)
|
||||
}
|
||||
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取市价失败: %w", err)
|
||||
}
|
||||
// quantity 已经是张数,直接格式化
|
||||
szStr := t.formatSize(quantity, inst)
|
||||
|
||||
sz := quantity * price / inst.CtVal
|
||||
szStr := t.formatSize(sz, inst)
|
||||
logger.Infof("🔻 OKX平多仓参数: symbol=%s, instId=%s, quantity(张数)=%f, szStr=%s",
|
||||
symbol, instId, quantity, szStr)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
@@ -720,7 +720,7 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
||||
func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
instId := t.convertSymbol(symbol)
|
||||
|
||||
// 如果数量为0,获取当前持仓
|
||||
// 如果数量为0,获取当前持仓(positionAmt 就是张数)
|
||||
if quantity == 0 {
|
||||
positions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
@@ -728,7 +728,7 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity = pos["positionAmt"].(float64) // 这已经是张数
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -737,19 +737,17 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
||||
}
|
||||
}
|
||||
|
||||
// 获取合约信息
|
||||
// 获取合约信息用于格式化张数
|
||||
inst, err := t.getInstrument(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取合约信息失败: %w", err)
|
||||
}
|
||||
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取市价失败: %w", err)
|
||||
}
|
||||
// quantity 已经是张数,直接格式化
|
||||
szStr := t.formatSize(quantity, inst)
|
||||
|
||||
sz := quantity * price / inst.CtVal
|
||||
szStr := t.formatSize(sz, inst)
|
||||
logger.Infof("🔻 OKX平空仓参数: symbol=%s, instId=%s, quantity(张数)=%f, szStr=%s",
|
||||
symbol, instId, quantity, szStr)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
@@ -762,6 +760,8 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
||||
"tag": okxTag,
|
||||
}
|
||||
|
||||
logger.Infof("🔻 OKX平空仓请求体: %+v", body)
|
||||
|
||||
data, err := t.doRequest("POST", okxOrderPath, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("平空仓失败: %w", err)
|
||||
@@ -780,12 +780,13 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
||||
if len(orders) == 0 || orders[0].SCode != "0" {
|
||||
msg := "未知错误"
|
||||
if len(orders) > 0 {
|
||||
msg = orders[0].SMsg
|
||||
msg = fmt.Sprintf("sCode=%s, sMsg=%s", orders[0].SCode, orders[0].SMsg)
|
||||
}
|
||||
logger.Infof("❌ OKX平空仓失败: %s, 响应: %s", msg, string(data))
|
||||
return nil, fmt.Errorf("平空仓失败: %s", msg)
|
||||
}
|
||||
|
||||
logger.Infof("✓ OKX平空仓成功: %s", symbol)
|
||||
logger.Infof("✓ OKX平空仓成功: %s, ordId=%s", symbol, orders[0].OrdId)
|
||||
|
||||
// 平仓后取消挂单
|
||||
t.CancelAllOrders(symbol)
|
||||
|
||||
@@ -18,6 +18,7 @@ import { t, type Language } from './i18n/translations'
|
||||
import { useSystemConfig } from './hooks/useSystemConfig'
|
||||
import { DecisionCard } from './components/DecisionCard'
|
||||
import { BacktestPage } from './components/BacktestPage'
|
||||
import { LogOut, Loader2 } from 'lucide-react'
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -524,6 +525,31 @@ function TraderDetailsPage({
|
||||
lastUpdate: string
|
||||
language: Language
|
||||
}) {
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null)
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg = language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
|
||||
if (!confirm(confirmMsg)) return
|
||||
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
alert(successMsg)
|
||||
window.location.reload()
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
|
||||
alert(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
}
|
||||
}
|
||||
// If API failed with error, show empty state (likely backend not running)
|
||||
if (tradersError) {
|
||||
return (
|
||||
@@ -836,6 +862,9 @@ function TraderDetailsPage({
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('entryPrice', language)}
|
||||
</th>
|
||||
@@ -889,6 +918,27 @@ function TraderDetailsPage({
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClosePosition(pos.symbol, pos.side.toUpperCase())}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
}}
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
|
||||
@@ -102,6 +102,15 @@ export const api = {
|
||||
if (!result.success) throw new Error('停止交易员失败')
|
||||
},
|
||||
|
||||
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
|
||||
const result = await httpClient.post<{ message: string }>(
|
||||
`${API_BASE}/traders/${traderId}/close-position`,
|
||||
{ symbol, side }
|
||||
)
|
||||
if (!result.success) throw new Error('平仓失败')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async updateTraderPrompt(
|
||||
traderId: string,
|
||||
customPrompt: string
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
Check,
|
||||
X,
|
||||
XCircle,
|
||||
LogOut,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { stripLeadingIcons } from '../lib/text'
|
||||
import type {
|
||||
@@ -63,6 +65,9 @@ export default function TraderDashboard() {
|
||||
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>()
|
||||
const [chartUpdateKey, setChartUpdateKey] = useState(0)
|
||||
|
||||
// 平仓操作状态
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null) // symbol being closed
|
||||
|
||||
// 点击持仓币种时调用
|
||||
const handlePositionSymbolClick = (symbol: string) => {
|
||||
setSelectedChartSymbol(symbol)
|
||||
@@ -75,6 +80,31 @@ export default function TraderDashboard() {
|
||||
localStorage.setItem('decisionLimit', newLimit.toString())
|
||||
}
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg = language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
|
||||
if (!confirm(confirmMsg)) return
|
||||
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
alert(successMsg)
|
||||
// 刷新持仓数据
|
||||
window.location.reload()
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
|
||||
alert(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取trader列表(仅在用户登录时)
|
||||
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
|
||||
user && token ? 'traders' : null,
|
||||
@@ -474,6 +504,9 @@ export default function TraderDashboard() {
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('entryPrice', language)}
|
||||
</th>
|
||||
@@ -544,6 +577,27 @@ export default function TraderDashboard() {
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClosePosition(pos.symbol, pos.side.toUpperCase())}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
}}
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
|
||||
Reference in New Issue
Block a user