update web ui
This commit is contained in:
@@ -6,9 +6,11 @@ import DeskWidget from '../../pages/desk/deskWidget'
|
||||
import Scenario from '../../pages/scenarioPage/scenario'
|
||||
import Tools from '../../pages/toolsPage/tools'
|
||||
|
||||
const Admin = lazy(() => import('../../pages/adminPage/admin'))
|
||||
const Backtest = lazy(() => import('../../pages/backtestPage/backtest'))
|
||||
const Bots = lazy(() => import('../../pages/botsPage/bots'))
|
||||
const Dashboard = lazy(() => import('../../pages/dashboardPage/dashboard'))
|
||||
const Monitoring = lazy(() => import('../../pages/monitoringPage/monitoring'))
|
||||
const Settings = lazy(() => import('../../pages/settingsPage/settings'))
|
||||
|
||||
const MyRoutes = () => {
|
||||
@@ -58,6 +60,28 @@ const MyRoutes = () => {
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/admin" element={<LayoutMain />}>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense fallback={null}>
|
||||
<Admin />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/monitoring" element={<LayoutMain />}>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense fallback={null}>
|
||||
<Monitoring />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/backtest" element={<LayoutMain />}>
|
||||
<Route
|
||||
index
|
||||
|
||||
@@ -12,11 +12,12 @@ import {Loader} from '../../atoms'
|
||||
import useCookie from '../../../hooks/useCookie'
|
||||
|
||||
const navigation = [
|
||||
{ href: '/bots', name: 'Bots' },
|
||||
{ href: '/backtest', name: 'Backtest' },
|
||||
{ href: '/scenarios', name: 'Scenarios' },
|
||||
{ href: '/bots', name: 'Strategies' },
|
||||
{ href: '/backtest', name: 'Backtests' },
|
||||
{ href: '/tools', name: 'Tools' },
|
||||
{ href: '/monitoring', name: 'Monitoring' },
|
||||
{ href: '/settings', name: 'Settings' },
|
||||
{ href: '/admin', name: 'Admin' },
|
||||
]
|
||||
|
||||
function navItems(isMobile = false) {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, {useState} from 'react'
|
||||
import type {SubmitHandler} from 'react-hook-form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {Account, RiskLevel, Ticker, TradeDirection, TradingClient,} from '../../../generated/ManagingApi'
|
||||
import Modal from '../../../components/mollecules/Modal/Modal'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {FormInput, Toast} from '../../../components/mollecules'
|
||||
import type {IOpenPositionFormInput} from '../../../global/type'
|
||||
|
||||
interface GmxPositionModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
account: Account
|
||||
}
|
||||
|
||||
const GmxPositionModal: React.FC<GmxPositionModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
}) => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new TradingClient({}, apiUrl)
|
||||
const [selectedTicker, setSelectedTicker] = useState<Ticker>(Ticker.BTC)
|
||||
const [selectedDirection, setSelectedDirection] = useState<TradeDirection>(
|
||||
TradeDirection.Long
|
||||
)
|
||||
const { register, handleSubmit } = useForm<IOpenPositionFormInput>()
|
||||
|
||||
function setSelectedDirectionEvent(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
setSelectedDirection(e.target.value as TradeDirection)
|
||||
}
|
||||
|
||||
function setSelectedTickerEvent(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
setSelectedTicker(e.target.value as Ticker)
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<IOpenPositionFormInput> = async (form) => {
|
||||
const t = new Toast(`Opening ${form.direction} ${form.ticker} on ${account.name}`)
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await client.trading_Trade(
|
||||
account.name,
|
||||
null, // moneyManagementName - assuming none selected
|
||||
form.direction,
|
||||
form.ticker,
|
||||
form.riskLevel,
|
||||
false, // isForPaperTrading - adjust if needed
|
||||
form.stopLoss, // Pass stopLoss price to openPrice parameter
|
||||
undefined // moneyManagement - not passing the full object
|
||||
)
|
||||
t.update('success', 'Position opened successfully')
|
||||
onClose() // Close modal on success
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'An unknown error occurred'
|
||||
setError(errorMessage)
|
||||
t.update('error', `Error: ${errorMessage}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const modalContent = (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Opening GMX position...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="mb-2">
|
||||
<strong>Account:</strong> {account.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-wrap gap-4 mb-4">
|
||||
<FormInput label="Direction" htmlFor="direction">
|
||||
<select
|
||||
className="select select-bordered w-full h-auto max-w-xs"
|
||||
{...register('direction', {
|
||||
onChange(e) {
|
||||
setSelectedDirectionEvent(e)
|
||||
},
|
||||
value: selectedDirection,
|
||||
})}
|
||||
>
|
||||
{Object.keys(TradeDirection).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
<FormInput label="Ticker" htmlFor="ticker">
|
||||
<select
|
||||
className="select select-bordered w-full h-auto max-w-xs"
|
||||
{...register('ticker', {
|
||||
onChange(e) {
|
||||
setSelectedTickerEvent(e)
|
||||
},
|
||||
value: selectedTicker,
|
||||
})}
|
||||
>
|
||||
{Object.keys(Ticker).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
<FormInput label="Risk" htmlFor="riskLevel">
|
||||
<select
|
||||
className="select select-bordered w-full h-auto max-w-xs"
|
||||
{...register('riskLevel')}
|
||||
>
|
||||
{Object.keys(RiskLevel).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
<div className="collapse bg-base-200 w-full">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title text-xs font-medium">
|
||||
Add TP/SL
|
||||
</div>
|
||||
<div className="collapse-content flex flex-col gap-2">
|
||||
<FormInput label="Stop Loss" htmlFor="stopLoss">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter SL price (used as openPrice)"
|
||||
className="input input-bordered w-full h-auto max-w-xs"
|
||||
{...register('stopLoss', { valueAsNumber: true })}
|
||||
/>
|
||||
</FormInput>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full mt-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner"></span>
|
||||
) : (
|
||||
`${selectedDirection} ${selectedTicker}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-4">
|
||||
{/* Optionally keep or adjust info message */}
|
||||
{/* <div className="alert alert-info"> */}
|
||||
{/* <p className="text-sm"> */}
|
||||
{/* <strong>Note:</strong> Ensure account has sufficient margin.*/}
|
||||
{/* </p>*/}
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Open GMX Position"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default GmxPositionModal
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {useDelegatedActions, usePrivy, useWallets, WalletWithMetadata} from '@privy-io/react-auth'
|
||||
import {Account} from '../../../generated/ManagingApi'
|
||||
import canonicalize from 'canonicalize' // Support JSON canonicalization
|
||||
import elliptic from 'elliptic' // Elliptic curve cryptography for browsers
|
||||
import * as base64 from 'base64-js' // Base64 encoding/decoding
|
||||
import Modal from '../../../components/mollecules/Modal/Modal'
|
||||
|
||||
// Replace with your app configuration - these should come from environment variables in a real app
|
||||
const PRIVY_APP_ID = 'insert-your-app-id'
|
||||
const PRIVY_APP_SECRET = 'insert-your-app-secret' // Don't hardcode in production!
|
||||
const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here' // Don't hardcode in production!
|
||||
|
||||
// Initialize elliptic curve - P-256 is the curve used by Privy (NIST P-256)
|
||||
const EC = new elliptic.ec('p256')
|
||||
|
||||
interface PrivyDelegationModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
account: Account
|
||||
}
|
||||
|
||||
// Use Web Crypto API to hash the message with SHA-256
|
||||
async function sha256Hash(message: Uint8Array): Promise<Uint8Array> {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', message)
|
||||
return new Uint8Array(hashBuffer)
|
||||
}
|
||||
|
||||
async function generateAuthorizationSignature({url, body}: {url: string; body: object}) {
|
||||
try {
|
||||
// First, canonicalize the request body - this is crucial for consistent signatures
|
||||
const canonicalizedBody = JSON.parse(canonicalize(body) as string);
|
||||
|
||||
// Create the payload object that follows Privy's specification
|
||||
const payload = {
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
url,
|
||||
body: canonicalizedBody, // Use the canonicalized body here
|
||||
headers: {
|
||||
'privy-app-id': PRIVY_APP_ID
|
||||
}
|
||||
};
|
||||
|
||||
// JSON-canonicalize the payload to ensure deterministic serialization
|
||||
const serializedPayload = canonicalize(payload) as string;
|
||||
|
||||
console.log('Canonicalized payload:', serializedPayload);
|
||||
|
||||
// Extract private key from the authorization key by removing the prefix
|
||||
const privateKeyBase64 = PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', '');
|
||||
|
||||
// Decode the base64 private key to get the raw bytes
|
||||
const privateKeyBytes = base64.toByteArray(privateKeyBase64);
|
||||
|
||||
// Import the private key into elliptic
|
||||
const privateKey = EC.keyFromPrivate(privateKeyBytes);
|
||||
|
||||
// Convert the serialized payload to UTF-8 bytes
|
||||
const messageBytes = new TextEncoder().encode(serializedPayload);
|
||||
|
||||
// Hash the message using SHA-256 with Web Crypto API
|
||||
const messageHash = await sha256Hash(messageBytes);
|
||||
|
||||
// Sign the hash with the private key - use Uint8Array instead of Buffer
|
||||
// Using the array directly instead of creating a Buffer
|
||||
const signature = privateKey.sign(Array.from(messageHash));
|
||||
|
||||
// Convert signature to the DER format and then to base64
|
||||
const signatureDer = signature.toDER();
|
||||
const signatureBase64 = base64.fromByteArray(new Uint8Array(signatureDer));
|
||||
|
||||
return {
|
||||
payload: serializedPayload,
|
||||
signature: signatureBase64,
|
||||
body: canonicalizedBody, // Return the canonicalized body
|
||||
canonicalizedBodyString: canonicalize(canonicalizedBody) as string // Return the string form too
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error generating signature:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send the actual request with the generated signature
|
||||
async function sendPrivyRequest(url: string, body: any, signature: string): Promise<any> {
|
||||
try {
|
||||
// Create basic auth token for Privy API
|
||||
const authToken = btoa(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`);
|
||||
|
||||
// Set up the request headers
|
||||
const headers = {
|
||||
'Authorization': `Basic ${authToken}`,
|
||||
'privy-app-id': PRIVY_APP_ID,
|
||||
'privy-authorization-signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
console.log('Sending request with headers:', headers);
|
||||
console.log('Body:', body);
|
||||
|
||||
// Make the API call
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: body // Use the exact same canonicalized body string
|
||||
});
|
||||
|
||||
// Get the response body
|
||||
const responseText = await response.text();
|
||||
|
||||
// Try to parse as JSON if possible
|
||||
try {
|
||||
const responseJson = JSON.parse(responseText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error (${response.status}): ${JSON.stringify(responseJson)}`);
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
} catch (parseError) {
|
||||
// If not valid JSON, return as text
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error (${response.status}): ${responseText}`);
|
||||
}
|
||||
|
||||
return { text: responseText };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending Privy request:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const PrivyDelegationModal: React.FC<PrivyDelegationModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
}) => {
|
||||
const [isDelegated, setIsDelegated] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [signatureResult, setSignatureResult] = useState<{
|
||||
payload: string,
|
||||
signature: string,
|
||||
body?: any,
|
||||
canonicalizedBodyString?: string
|
||||
} | null>(null)
|
||||
const [isSigningMessage, setIsSigningMessage] = useState<boolean>(false)
|
||||
const [isSendingRequest, setIsSendingRequest] = useState<boolean>(false)
|
||||
const [messageToSign, setMessageToSign] = useState<string>('Hello, Ethereum')
|
||||
const [apiResponse, setApiResponse] = useState<any>(null)
|
||||
|
||||
const { delegateWallet, revokeWallets } = useDelegatedActions()
|
||||
const { user } = usePrivy()
|
||||
const { ready, wallets } = useWallets()
|
||||
|
||||
// Find the embedded wallet to delegate from the array of the user's wallets
|
||||
const walletToDelegate = wallets.find(
|
||||
(wallet) => wallet.walletClientType === 'privy' && wallet.address === account.key
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (user && ready) {
|
||||
checkDelegationStatus()
|
||||
}
|
||||
}, [user, ready])
|
||||
|
||||
const checkDelegationStatus = () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Check if the wallet is already delegated by inspecting the user's linked accounts
|
||||
const delegatedWallet = user?.linkedAccounts.find(
|
||||
(linkedAccount): linkedAccount is WalletWithMetadata =>
|
||||
linkedAccount.type === 'wallet' &&
|
||||
linkedAccount.delegated &&
|
||||
linkedAccount.address.toLowerCase() === account.key?.toLowerCase()
|
||||
)
|
||||
|
||||
setIsDelegated(!!delegatedWallet)
|
||||
} catch (err: any) {
|
||||
console.error('Error checking delegation status:', err)
|
||||
setError(`Failed to check delegation status: ${err.message}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRequestDelegation = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (!walletToDelegate) {
|
||||
throw new Error('No embedded wallet found to delegate')
|
||||
}
|
||||
|
||||
// Fixes the linter error by using the correct parameter type
|
||||
await delegateWallet({
|
||||
address: walletToDelegate.address,
|
||||
chainType: 'ethereum'
|
||||
})
|
||||
|
||||
setIsDelegated(true)
|
||||
} catch (err: any) {
|
||||
console.error('Error requesting delegation:', err)
|
||||
setError(`Failed to request delegation: ${err.message}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRevokeDelegation = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (!walletToDelegate) {
|
||||
throw new Error('No embedded wallet found to revoke delegation')
|
||||
}
|
||||
|
||||
// Fix for linter error - revokeWallets doesn't take parameters
|
||||
await revokeWallets()
|
||||
|
||||
setIsDelegated(false)
|
||||
} catch (err: any) {
|
||||
console.error('Error revoking delegation:', err)
|
||||
setError(`Failed to revoke delegation: ${err.message}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignMessage = async () => {
|
||||
try {
|
||||
setIsSigningMessage(true)
|
||||
setSignatureResult(null)
|
||||
setApiResponse(null)
|
||||
setError(null)
|
||||
|
||||
if (!walletToDelegate) {
|
||||
throw new Error('No embedded wallet found to sign with')
|
||||
}
|
||||
|
||||
// Prepare the request body
|
||||
const body = {
|
||||
address: walletToDelegate.address,
|
||||
chain_type: 'ethereum',
|
||||
method: 'personal_sign',
|
||||
params: {
|
||||
message: messageToSign,
|
||||
encoding: 'utf-8'
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the signature using browser crypto
|
||||
const result = await generateAuthorizationSignature({
|
||||
url: 'https://auth.privy.io/api/v1/wallets/rpc',
|
||||
body
|
||||
})
|
||||
|
||||
setSignatureResult(result)
|
||||
|
||||
// For debugging in the console
|
||||
console.log("Generated Authorization Signature:", result.signature);
|
||||
console.log("Payload:", result.payload);
|
||||
console.log("Body:", result.body);
|
||||
console.log("Canonicalized Body String:", result.canonicalizedBodyString);
|
||||
|
||||
// Create a curl command that could be used to make the request
|
||||
// Note: We're using the canonicalized body from the result to ensure it matches what was signed
|
||||
const curlCommand = `curl --request POST https://auth.privy.io/api/v1/wallets/rpc \\
|
||||
-u "${PRIVY_APP_ID}:YOUR_APP_SECRET" \\
|
||||
-H "privy-app-id: ${PRIVY_APP_ID}" \\
|
||||
-H "privy-authorization-signature: ${result.signature}" \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '${result.canonicalizedBodyString}'`;
|
||||
|
||||
console.log("Curl command for testing:");
|
||||
console.log(curlCommand);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Error signing message:', err)
|
||||
setError(`Failed to sign message: ${err.message}`)
|
||||
} finally {
|
||||
setIsSigningMessage(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendRequest = async () => {
|
||||
if (!signatureResult?.canonicalizedBodyString || !signatureResult?.signature) {
|
||||
setError('No signature available. Please generate a signature first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSendingRequest(true);
|
||||
setApiResponse(null);
|
||||
setError(null);
|
||||
|
||||
// Send the actual request using the same canonicalized body that was used for signature
|
||||
const response = await sendPrivyRequest(
|
||||
'https://auth.privy.io/api/v1/wallets/rpc',
|
||||
signatureResult.canonicalizedBodyString,
|
||||
signatureResult.signature
|
||||
);
|
||||
|
||||
setApiResponse(response);
|
||||
console.log('API Response:', response);
|
||||
} catch (err: any) {
|
||||
console.error('Error sending request:', err);
|
||||
setError(`Failed to send request: ${err.message}`);
|
||||
} finally {
|
||||
setIsSendingRequest(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Modal content to be passed to the Modal component
|
||||
const modalContent = (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Checking delegation status...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="mb-2">
|
||||
<strong>Wallet Address:</strong> {account.key}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>Delegation Status:</strong>{' '}
|
||||
<span className={isDelegated ? 'text-success' : 'text-error'}>
|
||||
{isDelegated ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-4">
|
||||
{isDelegated ? (
|
||||
<button
|
||||
onClick={handleRevokeDelegation}
|
||||
className="btn btn-error"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Revoke Delegation
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRequestDelegation}
|
||||
className="btn btn-primary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Enable Delegation
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divider">Message Signing</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-control w-full">
|
||||
<div className="label">
|
||||
<span className="label-text">Message to sign</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={messageToSign}
|
||||
onChange={(e) => setMessageToSign(e.target.value)}
|
||||
className="input input-bordered w-full"
|
||||
disabled={isSigningMessage}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<button
|
||||
onClick={handleSignMessage}
|
||||
className="btn btn-secondary"
|
||||
disabled={isSigningMessage || !messageToSign}
|
||||
>
|
||||
{isSigningMessage ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
Generating Signature...
|
||||
</>
|
||||
) : (
|
||||
'Generate Signature'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{signatureResult && (
|
||||
<button
|
||||
onClick={handleSendRequest}
|
||||
className="btn btn-primary"
|
||||
disabled={isSendingRequest || !signatureResult}
|
||||
>
|
||||
{isSendingRequest ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
Sending Request...
|
||||
</>
|
||||
) : (
|
||||
'Send Request Using Signature'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{signatureResult && (
|
||||
<div className="mb-4">
|
||||
<div className="label">
|
||||
<span className="label-text font-bold">Generated Signature:</span>
|
||||
</div>
|
||||
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
|
||||
<pre className="text-xs break-all whitespace-pre-wrap">{signatureResult.signature}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{apiResponse && (
|
||||
<div className="mb-4">
|
||||
<div className="label">
|
||||
<span className="label-text font-bold">API Response:</span>
|
||||
</div>
|
||||
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
|
||||
<pre className="text-xs break-all whitespace-pre-wrap">
|
||||
{JSON.stringify(apiResponse, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="alert alert-info">
|
||||
<p className="text-sm">
|
||||
<strong>Note:</strong> This component ensures that the exact same canonicalized payload is used
|
||||
for both signature generation and request sending, maintaining signature integrity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Wallet Delegation"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivyDelegationModal
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import React, {useState} from 'react'
|
||||
import type {SubmitHandler} from 'react-hook-form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {Account, AccountClient, Ticker,} from '../../../generated/ManagingApi'
|
||||
import Modal from '../../../components/mollecules/Modal/Modal'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {FormInput, Toast} from '../../../components/mollecules'
|
||||
import {useApiError} from '../../../hooks/useApiError'
|
||||
|
||||
interface SendTokenModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
account: Account
|
||||
fromTicker: Ticker
|
||||
availableAmount: number
|
||||
}
|
||||
|
||||
interface SendTokenFormInput {
|
||||
recipientAddress: string
|
||||
ticker: Ticker
|
||||
amount: number
|
||||
chainId?: number
|
||||
}
|
||||
|
||||
const SendTokenModal: React.FC<SendTokenModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
fromTicker,
|
||||
availableAmount,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { error, setError, handleApiErrorWithToast } = useApiError()
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
|
||||
const { register, handleSubmit, watch, setValue } = useForm<SendTokenFormInput>({
|
||||
defaultValues: {
|
||||
recipientAddress: '',
|
||||
ticker: fromTicker,
|
||||
amount: availableAmount * 0.1, // Start with 10% of available amount
|
||||
chainId: 42161, // Default to ARBITRUM
|
||||
}
|
||||
})
|
||||
|
||||
const watchedAmount = watch('amount')
|
||||
const watchedRecipientAddress = watch('recipientAddress')
|
||||
|
||||
const onSubmit: SubmitHandler<SendTokenFormInput> = async (form) => {
|
||||
const t = new Toast(`Sending ${form.amount} ${form.ticker} to ${form.recipientAddress} from ${account.name}`)
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await client.account_SendToken(
|
||||
account.name,
|
||||
{
|
||||
recipientAddress: form.recipientAddress,
|
||||
ticker: form.ticker,
|
||||
amount: form.amount,
|
||||
chainId: form.chainId,
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
t.update('success', `Token sent successfully! Hash: ${result.hash}`)
|
||||
onClose()
|
||||
} else {
|
||||
console.log(result)
|
||||
const errorMessage = result.error || result.message || 'Send token failed'
|
||||
setError(errorMessage)
|
||||
t.update('error', `Send token failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (err) {
|
||||
handleApiErrorWithToast(err, t)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
handleSubmit(onSubmit)(e)
|
||||
}
|
||||
|
||||
const modalContent = (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Processing token transfer...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="mb-2">
|
||||
<strong>Account:</strong> {account.name}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>Token:</strong> {fromTicker}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>Available:</strong> {availableAmount.toFixed(6)} {fromTicker}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="space-y-4 mb-4">
|
||||
<FormInput label="Recipient Address" htmlFor="recipientAddress">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
className="input input-bordered w-full"
|
||||
{...register('recipientAddress', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^0x[a-fA-F0-9]{40}$/,
|
||||
message: 'Please enter a valid Ethereum address'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Token" htmlFor="ticker">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('ticker')}
|
||||
defaultValue={fromTicker}
|
||||
>
|
||||
{Object.values(Ticker).map((ticker) => (
|
||||
<option key={ticker} value={ticker}>
|
||||
{ticker}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Amount" htmlFor="amount">
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter amount to send"
|
||||
className="input input-bordered w-full mb-2"
|
||||
{...register('amount', {
|
||||
valueAsNumber: true,
|
||||
min: 0.0001,
|
||||
max: availableAmount,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={availableAmount}
|
||||
step={availableAmount / 100}
|
||||
className="range range-primary w-full"
|
||||
value={watchedAmount || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
setValue('amount', value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="text-center text-xs text-gray-500 mt-1">
|
||||
{watchedAmount && availableAmount > 0 ? (
|
||||
<span>{((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance</span>
|
||||
) : (
|
||||
<span>0% of available balance</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Chain ID (Optional)" htmlFor="chainId">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('chainId', { valueAsNumber: true })}
|
||||
defaultValue={42161}
|
||||
>
|
||||
<option value={1}>Ethereum Mainnet (1)</option>
|
||||
<option value={42161}>Arbitrum One (42161)</option>
|
||||
<option value={421613}>Arbitrum Goerli (421613)</option>
|
||||
<option value={8453}>Base (8453)</option>
|
||||
<option value={84531}>Base Goerli (84531)</option>
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full mt-2"
|
||||
disabled={isLoading || !watchedAmount || watchedAmount <= 0 || !watchedRecipientAddress}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner"></span>
|
||||
) : (
|
||||
`Send ${watchedAmount || 0} ${fromTicker} to ${watchedRecipientAddress ? watchedRecipientAddress.slice(0, 6) + '...' + watchedRecipientAddress.slice(-4) : 'recipient'}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="alert alert-warning">
|
||||
<p className="text-sm">
|
||||
<strong>Warning:</strong> This will send tokens directly to the specified address. Make sure the address is correct as this action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Send Tokens"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendTokenModal
|
||||
|
||||
322
src/Managing.WebApp/src/pages/adminPage/account/SwapModal.tsx
Normal file
322
src/Managing.WebApp/src/pages/adminPage/account/SwapModal.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React, {useState} from 'react'
|
||||
import type {SubmitHandler} from 'react-hook-form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {Account, AccountClient, Ticker,} from '../../../generated/ManagingApi'
|
||||
import Modal from '../../../components/mollecules/Modal/Modal'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {FormInput, Toast} from '../../../components/mollecules'
|
||||
import {useApiError} from '../../../hooks/useApiError'
|
||||
|
||||
interface SwapModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
account: Account
|
||||
fromTicker: Ticker
|
||||
availableAmount: number
|
||||
}
|
||||
|
||||
interface SwapFormInput {
|
||||
fromTicker: Ticker
|
||||
toTicker: Ticker
|
||||
amount: number
|
||||
orderType: string
|
||||
triggerRatio?: number
|
||||
allowedSlippage: number
|
||||
}
|
||||
|
||||
interface ValidationErrorResponse {
|
||||
type: string
|
||||
title: string
|
||||
status: number
|
||||
errors: Record<string, string[]>
|
||||
traceId: string
|
||||
}
|
||||
|
||||
const SwapModal: React.FC<SwapModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
fromTicker,
|
||||
availableAmount,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({})
|
||||
const {error, setError, handleApiErrorWithToast} = useApiError()
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
const [selectedToTicker, setSelectedToTicker] = useState<Ticker>(Ticker.USDC)
|
||||
const [selectedOrderType, setSelectedOrderType] = useState<string>('market')
|
||||
|
||||
const {register, handleSubmit, watch, setValue} = useForm<SwapFormInput>({
|
||||
defaultValues: {
|
||||
fromTicker: fromTicker,
|
||||
toTicker: Ticker.USDC,
|
||||
amount: availableAmount * 0.1, // Start with 10% of available amount
|
||||
orderType: 'market',
|
||||
allowedSlippage: 0.5,
|
||||
}
|
||||
})
|
||||
|
||||
const watchedAmount = watch('amount')
|
||||
|
||||
function setSelectedToTickerEvent(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedToTicker(e.target.value as Ticker)
|
||||
}
|
||||
|
||||
function setSelectedOrderTypeEvent(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedOrderType(e.target.value)
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<SwapFormInput> = async (form) => {
|
||||
const t = new Toast(`Swapping ${form.amount} ${form.fromTicker} to ${form.toTicker} on ${account.name}`)
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setValidationErrors({})
|
||||
|
||||
try {
|
||||
const result = await client.account_SwapGmxTokens(
|
||||
account.name,
|
||||
{
|
||||
fromTicker: form.fromTicker,
|
||||
toTicker: form.toTicker,
|
||||
amount: form.amount,
|
||||
orderType: form.orderType,
|
||||
triggerRatio: form.triggerRatio,
|
||||
allowedSlippage: form.allowedSlippage,
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
t.update('success', `Swap successful! Hash: ${result.hash}`)
|
||||
onClose()
|
||||
} else {
|
||||
console.log(result)
|
||||
const errorMessage = result.error || result.message || 'Swap failed'
|
||||
setError(errorMessage)
|
||||
t.update('error', `Swap failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Handle validation errors from API
|
||||
if (err.response?.data && typeof err.response.data === 'object') {
|
||||
const errorData = err.response.data as ValidationErrorResponse
|
||||
console.log(errorData)
|
||||
if (errorData.errors && typeof errorData.errors === 'object') {
|
||||
setValidationErrors(errorData.errors)
|
||||
const errorMessages = Object.values(errorData.errors).flat()
|
||||
const errorMessage = errorMessages.join(', ')
|
||||
setError(errorMessage)
|
||||
t.update('error', `Validation failed: ${errorMessage}`)
|
||||
} else {
|
||||
handleApiErrorWithToast(err, t)
|
||||
}
|
||||
} else {
|
||||
handleApiErrorWithToast(err, t)
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
await handleSubmit(onSubmit)(e)
|
||||
}
|
||||
|
||||
const modalContent = (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Processing swap...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="mb-2">
|
||||
<strong>Account:</strong> {account.name}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>From:</strong> {fromTicker}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<div>
|
||||
<h4 className="font-bold">Validation Errors:</h4>
|
||||
{Object.entries(validationErrors).map(([field, errors]) => (
|
||||
<div key={field} className="mt-1">
|
||||
<strong>{field}:</strong> {errors.join(', ')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4 mb-4">
|
||||
<FormInput label="To Ticker" htmlFor="toTicker">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('toTicker', {
|
||||
onChange(e) {
|
||||
setSelectedToTickerEvent(e)
|
||||
},
|
||||
value: selectedToTicker,
|
||||
})}
|
||||
>
|
||||
{Object.values(Ticker)
|
||||
.filter(ticker => ticker !== fromTicker) // Exclude the from ticker
|
||||
.map((ticker) => (
|
||||
<option key={ticker} value={ticker}>
|
||||
{ticker}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Amount" htmlFor="amount">
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter amount to swap"
|
||||
className={`input input-bordered w-full mb-2 ${validationErrors.Amount ? 'input-error' : ''}`}
|
||||
{...register('amount', {
|
||||
valueAsNumber: true,
|
||||
min: 0.00000000000001,
|
||||
max: availableAmount,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
{validationErrors.Amount && (
|
||||
<div className="text-error text-xs mt-1">
|
||||
{validationErrors.Amount.map((error, index) => (
|
||||
<div key={index}>{error}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={availableAmount}
|
||||
step={0.00000000000001}
|
||||
className="range range-primary w-full"
|
||||
value={watchedAmount || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
setValue('amount', value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="text-center text-xs text-gray-500 mt-1">
|
||||
{watchedAmount && availableAmount > 0 ? (
|
||||
<span>{((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance</span>
|
||||
) : (
|
||||
<span>0% of available balance</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Order Type" htmlFor="orderType">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('orderType', {
|
||||
onChange(e) {
|
||||
setSelectedOrderTypeEvent(e)
|
||||
},
|
||||
value: selectedOrderType,
|
||||
})}
|
||||
>
|
||||
<option value="market">Market</option>
|
||||
<option value="limit">Limit</option>
|
||||
<option value="stop">Stop</option>
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Allowed Slippage (%)" htmlFor="allowedSlippage">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.5"
|
||||
className={`input input-bordered w-full ${validationErrors.AllowedSlippage ? 'input-error' : ''}`}
|
||||
{...register('allowedSlippage', {
|
||||
valueAsNumber: true,
|
||||
min: 0.1,
|
||||
max: 10,
|
||||
value: 0.5
|
||||
})}
|
||||
/>
|
||||
{validationErrors.AllowedSlippage && (
|
||||
<div className="text-error text-xs mt-1">
|
||||
{validationErrors.AllowedSlippage.map((error, index) => (
|
||||
<div key={index}>{error}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</FormInput>
|
||||
|
||||
{selectedOrderType === 'limit' && (
|
||||
<FormInput label="Trigger Ratio" htmlFor="triggerRatio">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter trigger ratio"
|
||||
className={`input input-bordered w-full ${validationErrors.TriggerRatio ? 'input-error' : ''}`}
|
||||
{...register('triggerRatio', {valueAsNumber: true})}
|
||||
/>
|
||||
{validationErrors.TriggerRatio && (
|
||||
<div className="text-error text-xs mt-1">
|
||||
{validationErrors.TriggerRatio.map((error, index) => (
|
||||
<div key={index}>{error}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</FormInput>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full mt-2"
|
||||
disabled={isLoading || !watchedAmount || watchedAmount <= 0}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner"></span>
|
||||
) : (
|
||||
`Swap ${watchedAmount || 0} ${fromTicker} to ${selectedToTicker}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="alert alert-info">
|
||||
<p className="text-sm">
|
||||
<strong>Note:</strong> Ensure account has sufficient balance for the swap.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Swap Tokens on GMX"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwapModal
|
||||
|
||||
175
src/Managing.WebApp/src/pages/adminPage/account/accountModal.tsx
Normal file
175
src/Managing.WebApp/src/pages/adminPage/account/accountModal.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import {useState} from 'react'
|
||||
import type {SubmitHandler} from 'react-hook-form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {Modal, Toast} from '../../../components/mollecules'
|
||||
import type {Account} from '../../../generated/ManagingApi'
|
||||
import {AccountClient, AccountType, TradingExchanges,} from '../../../generated/ManagingApi'
|
||||
import type {IAccountFormInput, IModalProps} from '../../../global/type.tsx'
|
||||
|
||||
const AccountModal: React.FC<IModalProps> = ({ showModal, toggleModal }) => {
|
||||
const [selectedExchange, setSelectedExchange] = useState<TradingExchanges>()
|
||||
const [selectedType, setSelectedType] = useState<AccountType>()
|
||||
const { register, handleSubmit } = useForm<IAccountFormInput>()
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
|
||||
async function createMoneyManagement(form: IAccountFormInput) {
|
||||
const t = new Toast('Creating account')
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
const a: Account = {
|
||||
exchange: form.exchange,
|
||||
key: form.key,
|
||||
name: form.name,
|
||||
secret: form.secret,
|
||||
type: form.type,
|
||||
}
|
||||
await client
|
||||
.account_PostAccount(a)
|
||||
.then(() => {
|
||||
t.update('success', 'Account created')
|
||||
})
|
||||
.catch((err) => {
|
||||
t.update('error', 'Error :' + err)
|
||||
})
|
||||
}
|
||||
const onSubmit: SubmitHandler<IAccountFormInput> = async (form) => {
|
||||
// @ts-ignore
|
||||
toggleModal()
|
||||
await createMoneyManagement(form)
|
||||
}
|
||||
|
||||
function setSelectedExchangeEvent(e: any) {
|
||||
setSelectedExchange(e.target.value)
|
||||
}
|
||||
|
||||
function setSelectedTypeEvent(e: any) {
|
||||
setSelectedType(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
showModal={showModal}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onClose={toggleModal}
|
||||
titleHeader={'Create Account'}
|
||||
>
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="exchange" className="label mr-6">
|
||||
Exchange
|
||||
</label>
|
||||
<select
|
||||
className="select w-full max-w-xs"
|
||||
{...register('exchange', {
|
||||
onChange: (e) => {
|
||||
setSelectedExchangeEvent(e)
|
||||
},
|
||||
})}
|
||||
>
|
||||
{Object.keys(TradingExchanges).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="name" className="label mr-6">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
style={{ color: 'black' }}
|
||||
className="w-full max-w-xs"
|
||||
{...register('name')}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="type" className="label mr-6">
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
className="select w-full max-w-xs"
|
||||
{...register('type', {
|
||||
onChange: (e) => {
|
||||
setSelectedTypeEvent(e)
|
||||
},
|
||||
})}
|
||||
>
|
||||
{Object.keys(AccountType).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(selectedExchange != TradingExchanges.Evm && selectedType != AccountType.Trader ) ||
|
||||
(selectedExchange == TradingExchanges.Evm && selectedType == AccountType.Privy )? (
|
||||
<>
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="key" className="label mr-6">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
style={{ color: 'black' }}
|
||||
className="w-full max-w-xs"
|
||||
{...register('key')}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="secret" className="label mr-6">
|
||||
Secret
|
||||
</label>
|
||||
<input
|
||||
style={{ color: 'black' }}
|
||||
className="w-full max-w-xs"
|
||||
{...register('secret')}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{selectedExchange == TradingExchanges.Evm &&
|
||||
selectedType == AccountType.Watch ? (
|
||||
<>
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="key" className="label mr-6">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
style={{ color: 'black' }}
|
||||
className="w-full max-w-xs"
|
||||
{...register('key')}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="modal-action">
|
||||
<button type="submit" className="btn">
|
||||
Build
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountModal
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import React, {useState} from 'react'
|
||||
import {FiRefreshCw, FiSend} from 'react-icons/fi'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {SelectColumnFilter, Table} from '../../../components/mollecules'
|
||||
import type {IAccountRowDetail} from '../../../global/type.tsx'
|
||||
import type {Account, Balance} from '../../../generated/ManagingApi'
|
||||
import {AccountClient, Ticker} from '../../../generated/ManagingApi'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import SwapModal from './SwapModal'
|
||||
import SendTokenModal from './SendTokenModal'
|
||||
|
||||
interface IAccountRowDetailProps extends IAccountRowDetail {
|
||||
account: Account
|
||||
}
|
||||
|
||||
const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
balances,
|
||||
showTotal,
|
||||
account,
|
||||
}) => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const accountClient = new AccountClient({}, apiUrl)
|
||||
|
||||
// Fetch exchange approval status using TanStack Query
|
||||
const { data: exchangeApprovalStatus, isLoading: isLoadingApprovalStatus, error: approvalStatusError, refetch: refetchApprovalStatus } = useQuery({
|
||||
queryKey: ['exchangeApprovalStatus'],
|
||||
queryFn: async () => {
|
||||
return await accountClient.account_GetExchangeApprovalStatus()
|
||||
},
|
||||
staleTime: 60000, // Consider data fresh for 1 minute
|
||||
gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
|
||||
retry: 2, // Retry failed requests up to 2 times
|
||||
enabled: !!apiUrl, // Only run query when apiUrl is available
|
||||
})
|
||||
|
||||
const [swapModalState, setSwapModalState] = useState<{
|
||||
isOpen: boolean
|
||||
fromTicker: Ticker | null
|
||||
availableAmount: number
|
||||
}>({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
|
||||
const [sendTokenModalState, setSendTokenModalState] = useState<{
|
||||
isOpen: boolean
|
||||
fromTicker: Ticker | null
|
||||
availableAmount: number
|
||||
}>({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
|
||||
const handleSwapClick = (balance: Balance) => {
|
||||
if (balance.tokenName && balance.amount) {
|
||||
// Convert tokenName to Ticker enum
|
||||
const ticker = balance.tokenName.toUpperCase() as Ticker
|
||||
if (Object.values(Ticker).includes(ticker)) {
|
||||
setSwapModalState({
|
||||
isOpen: true,
|
||||
fromTicker: ticker,
|
||||
availableAmount: balance.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendClick = (balance: Balance) => {
|
||||
if (balance.tokenName && balance.amount) {
|
||||
// Convert tokenName to Ticker enum
|
||||
const ticker = balance.tokenName.toUpperCase() as Ticker
|
||||
if (Object.values(Ticker).includes(ticker)) {
|
||||
setSendTokenModalState({
|
||||
isOpen: true,
|
||||
fromTicker: ticker,
|
||||
availableAmount: balance.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeSwapModal = () => {
|
||||
setSwapModalState({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const closeSendTokenModal = () => {
|
||||
setSendTokenModalState({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Chain',
|
||||
accessor: 'chain.name',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Assets',
|
||||
accessor: 'tokenName',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => (
|
||||
<>
|
||||
<div className="tooltip" data-tip={cell.row.tokenName}>
|
||||
{cell.row.values.amount.toFixed(4)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
Header: 'Quantity',
|
||||
accessor: 'amount',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <>{cell.row.values.value.toFixed(2)} $</>,
|
||||
Header: 'USD',
|
||||
accessor: 'value',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <> {cell.row.values.price} $</>,
|
||||
Header: 'Price',
|
||||
accessor: 'price',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => {
|
||||
const balance = cell.row.original as Balance
|
||||
|
||||
return (
|
||||
<div className="flex justify-center gap-1">
|
||||
{balance.tokenName && balance.amount && balance.amount > 0 && Object.values(Ticker).includes(balance.tokenName.toUpperCase() as Ticker) && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-xs btn-outline btn-info"
|
||||
onClick={() => handleSwapClick(balance)}
|
||||
title={`Swap ${balance.tokenName}`}
|
||||
>
|
||||
<FiRefreshCw className="h-3 w-3" />
|
||||
<span className="ml-1">Swap</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-outline btn-success"
|
||||
onClick={() => handleSendClick(balance)}
|
||||
title={`Send ${balance.tokenName}`}
|
||||
>
|
||||
<FiSend className="h-3 w-3" />
|
||||
<span className="ml-1">Send</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Header: 'Actions',
|
||||
accessor: 'actions',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={balances}
|
||||
showTotal={showTotal}
|
||||
showPagination={false}
|
||||
/>
|
||||
|
||||
{/* Exchange Approval Status */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">Exchange Approval Status</h4>
|
||||
<button
|
||||
className={`btn btn-xs btn-ghost ${isLoadingApprovalStatus ? 'loading' : ''}`}
|
||||
onClick={() => refetchApprovalStatus()}
|
||||
disabled={isLoadingApprovalStatus}
|
||||
title="Refresh approval status"
|
||||
>
|
||||
{!isLoadingApprovalStatus && <FiRefreshCw className="h-3 w-3" />}
|
||||
{!isLoadingApprovalStatus && <span className="ml-1">Refresh</span>}
|
||||
</button>
|
||||
</div>
|
||||
{isLoadingApprovalStatus ? (
|
||||
<div className="text-sm text-gray-500">Loading approval status...</div>
|
||||
) : approvalStatusError ? (
|
||||
<div className="text-sm text-red-500">Error loading approval status</div>
|
||||
) : exchangeApprovalStatus && exchangeApprovalStatus.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exchangeApprovalStatus.map((status) => (
|
||||
<div
|
||||
key={status.exchange}
|
||||
className={`badge ${
|
||||
status.isInitialized
|
||||
? 'badge-success'
|
||||
: 'badge-outline'
|
||||
}`}
|
||||
>
|
||||
{status.exchange}: {status.isInitialized ? 'Initialized' : 'Not Initialized'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">No exchange data available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{swapModalState.isOpen && swapModalState.fromTicker && (
|
||||
<SwapModal
|
||||
isOpen={swapModalState.isOpen}
|
||||
onClose={closeSwapModal}
|
||||
account={account}
|
||||
fromTicker={swapModalState.fromTicker}
|
||||
availableAmount={swapModalState.availableAmount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sendTokenModalState.isOpen && sendTokenModalState.fromTicker && (
|
||||
<SendTokenModal
|
||||
isOpen={sendTokenModalState.isOpen}
|
||||
onClose={closeSendTokenModal}
|
||||
account={account}
|
||||
fromTicker={sendTokenModalState.fromTicker}
|
||||
availableAmount={sendTokenModalState.availableAmount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountRowDetails
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import type {Account} from '../../../generated/ManagingApi'
|
||||
import {AccountClient} from '../../../generated/ManagingApi'
|
||||
|
||||
import AccountModal from './accountModal'
|
||||
import AccountTable from './accountTable'
|
||||
|
||||
const AccountSettings: React.FC = () => {
|
||||
const [accounts, setAccounts] = useState<Account[]>([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
setIsFetching(true)
|
||||
client
|
||||
.account_GetAccountsBalances()
|
||||
.then((data) => {
|
||||
setAccounts(data)
|
||||
})
|
||||
.finally(() => setIsFetching(false))
|
||||
}, [])
|
||||
|
||||
function toggleModal() {
|
||||
setShowModal(!showModal)
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<button className="btn" onClick={openModal}>
|
||||
Create account
|
||||
</button>
|
||||
<AccountTable list={accounts} isFetching={isFetching} />
|
||||
<AccountModal showModal={showModal} toggleModal={toggleModal} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default AccountSettings
|
||||
|
||||
249
src/Managing.WebApp/src/pages/adminPage/account/accountTable.tsx
Normal file
249
src/Managing.WebApp/src/pages/adminPage/account/accountTable.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import {ChevronDownIcon, ChevronRightIcon,} from '@heroicons/react/solid'
|
||||
import React, {useEffect, useMemo, useState} from 'react'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import {FiCopy, FiKey, FiPlay, FiTrash2, FiTrendingUp} from 'react-icons/fi'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {SelectColumnFilter, Table, Toast,} from '../../../components/mollecules'
|
||||
import type {Account} from '../../../generated/ManagingApi'
|
||||
import {AccountClient, AccountType, TradingClient} from '../../../generated/ManagingApi'
|
||||
|
||||
import AccountRowDetails from './accountRowDetails'
|
||||
import PrivyDelegationModal from './PrivyDelegationModal'
|
||||
import GmxPositionModal from './GmxPositionModal'
|
||||
|
||||
interface IAccountList {
|
||||
list: Account[]
|
||||
isFetching: boolean
|
||||
}
|
||||
|
||||
const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
|
||||
const [rows, setRows] = useState<Account[]>([])
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const navigate = useNavigate()
|
||||
const [modalAccount, setModalAccount] = useState<Account | null>(null)
|
||||
const [isDelegationModalOpen, setIsDelegationModalOpen] = useState(false)
|
||||
const [isGmxPositionModalOpen, setIsGmxPositionModalOpen] = useState(false)
|
||||
|
||||
async function deleteAcount(accountName: string) {
|
||||
const t = new Toast('Deleting money management')
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
|
||||
await client
|
||||
.account_DeleteAccount(accountName)
|
||||
.then(() => {
|
||||
t.update('success', 'Account deleted')
|
||||
})
|
||||
.catch((err) => {
|
||||
t.update('error', 'Error :' + err)
|
||||
})
|
||||
}
|
||||
|
||||
async function initPrivyWallet(publicAddress: string) {
|
||||
const t = new Toast('Initializing Privy wallet')
|
||||
const client = new TradingClient({}, apiUrl)
|
||||
|
||||
try {
|
||||
const response = await client.trading_InitPrivyWallet(publicAddress)
|
||||
if (response.success) {
|
||||
t.update('success', 'Privy wallet initialized successfully')
|
||||
} else {
|
||||
t.update('error', `Initialization failed: ${response.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
t.update('error', 'Error: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
const t = new Toast('Copying to clipboard...')
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
t.update('success', 'Address copied to clipboard!')
|
||||
} catch (err) {
|
||||
t.update('error', 'Failed to copy to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Cell: ({ row }: any) => (
|
||||
<span {...row.getToggleRowExpandedProps()}>
|
||||
{row.isExpanded ? (
|
||||
<ChevronDownIcon></ChevronDownIcon>
|
||||
) : (
|
||||
<ChevronRightIcon></ChevronRightIcon>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
|
||||
// Make sure it has an ID
|
||||
Header: ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }: any) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
{isAllRowsExpanded ? 'v' : '>'}
|
||||
</span>
|
||||
),
|
||||
// Build our expander column
|
||||
id: 'expander',
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Exchange',
|
||||
accessor: 'exchange',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => (
|
||||
<>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="tooltip" data-tip={cell.row.values.key}>
|
||||
{cell.row.values.key.substring(0, 6)}...
|
||||
{cell.row.values.key.slice(-4)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-xs btn-ghost"
|
||||
onClick={() => copyToClipboard(cell.row.values.key)}
|
||||
title="Copy full address"
|
||||
>
|
||||
<FiCopy className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
Header: 'Key',
|
||||
accessor: 'key',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row }: any) => {
|
||||
const account = row.original
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className="btn btn-sm btn-error btn-outline"
|
||||
onClick={() => deleteAcount(account.name)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</button>
|
||||
|
||||
{account.type === AccountType.Privy && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
setModalAccount(account)
|
||||
setIsDelegationModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<FiKey />
|
||||
<span className="ml-1">Delegate</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-outline"
|
||||
onClick={() => initPrivyWallet(account.key)}
|
||||
>
|
||||
<FiPlay />
|
||||
<span className="ml-1">Init</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-sm btn-success btn-outline"
|
||||
onClick={() => {
|
||||
setModalAccount(account)
|
||||
setIsGmxPositionModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<FiTrendingUp />
|
||||
<span className="ml-1">GMX</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Header: 'Actions',
|
||||
accessor: 'id',
|
||||
disableFilters: true,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setRows(list)
|
||||
}, [list])
|
||||
|
||||
const renderRowSubComponent = React.useCallback(
|
||||
({ row }: { row: any }) => {
|
||||
const { balances } = row.original
|
||||
return (
|
||||
<>
|
||||
{balances && balances.length > 0 ? (
|
||||
<AccountRowDetails
|
||||
balances={balances}
|
||||
showTotal={true}
|
||||
account={row.original}
|
||||
></AccountRowDetails>
|
||||
) : (
|
||||
<div>No balances</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetching ? (
|
||||
<div>
|
||||
<progress className="progress progress-primary w-56"></progress>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={rows}
|
||||
renderRowSubCompontent={renderRowSubComponent}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalAccount && (
|
||||
<>
|
||||
<PrivyDelegationModal
|
||||
isOpen={isDelegationModalOpen}
|
||||
onClose={() => {
|
||||
setIsDelegationModalOpen(false)
|
||||
setModalAccount(null)
|
||||
}}
|
||||
account={modalAccount}
|
||||
/>
|
||||
<GmxPositionModal
|
||||
isOpen={isGmxPositionModalOpen}
|
||||
onClose={() => {
|
||||
setIsGmxPositionModalOpen(false)
|
||||
setModalAccount(null)
|
||||
}}
|
||||
account={modalAccount}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountTable
|
||||
|
||||
41
src/Managing.WebApp/src/pages/adminPage/admin.tsx
Normal file
41
src/Managing.WebApp/src/pages/adminPage/admin.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {useState} from 'react'
|
||||
|
||||
import {Tabs} from '../../components/mollecules'
|
||||
|
||||
import AccountSettings from './account/accountSettings'
|
||||
import WhitelistSettings from './whitelist/whitelistSettings'
|
||||
|
||||
type TabsType = {
|
||||
label: string
|
||||
index: number
|
||||
Component: React.FC<{}>
|
||||
}[]
|
||||
|
||||
// Tabs Array
|
||||
const tabs: TabsType = [
|
||||
{
|
||||
Component: WhitelistSettings,
|
||||
index: 1,
|
||||
label: 'Whitelist',
|
||||
},
|
||||
{
|
||||
Component: AccountSettings,
|
||||
index: 2,
|
||||
label: 'Account',
|
||||
},
|
||||
]
|
||||
|
||||
const Admin: React.FC = () => {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<Tabs selectedTab={selectedTab} onClick={setSelectedTab} tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Admin
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import {useState} from 'react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {WhitelistClient} from '../../../generated/ManagingApi'
|
||||
|
||||
import WhitelistTable from './whitelistTable'
|
||||
|
||||
const WhitelistSettings: React.FC = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [pageNumber, setPageNumber] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [searchExternalEthereumAccount, setSearchExternalEthereumAccount] = useState<string>('')
|
||||
const [searchTwitterAccount, setSearchTwitterAccount] = useState<string>('')
|
||||
|
||||
const whitelistClient = new WhitelistClient({}, apiUrl)
|
||||
|
||||
const {
|
||||
data: whitelistData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['whitelistAccounts', pageNumber, pageSize, searchExternalEthereumAccount, searchTwitterAccount],
|
||||
queryFn: async () => {
|
||||
return await whitelistClient.whitelist_GetWhitelistAccounts(
|
||||
pageNumber,
|
||||
pageSize,
|
||||
searchExternalEthereumAccount || null,
|
||||
searchTwitterAccount || null
|
||||
)
|
||||
},
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const accounts = whitelistData?.accounts || []
|
||||
const totalCount = whitelistData?.totalCount || 0
|
||||
const totalPages = whitelistData?.totalPages || 0
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPageNumber(newPage)
|
||||
}
|
||||
|
||||
const handleSearchExternalEthereum = (value: string) => {
|
||||
setSearchExternalEthereumAccount(value)
|
||||
setPageNumber(1) // Reset to first page when searching
|
||||
}
|
||||
|
||||
const handleSearchTwitter = (value: string) => {
|
||||
setSearchTwitterAccount(value)
|
||||
setPageNumber(1) // Reset to first page when searching
|
||||
}
|
||||
|
||||
const handleWhitelistSuccess = () => {
|
||||
refetch()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-bold mb-4">Whitelist Accounts</h2>
|
||||
|
||||
{/* Search Filters */}
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="form-control w-full max-w-xs">
|
||||
<label className="label">
|
||||
<span className="label-text">Search by Ethereum Account</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Ethereum address..."
|
||||
className="input input-bordered w-full"
|
||||
value={searchExternalEthereumAccount}
|
||||
onChange={(e) => handleSearchExternalEthereum(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full max-w-xs">
|
||||
<label className="label">
|
||||
<span className="label-text">Search by Twitter Account</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Twitter handle..."
|
||||
className="input input-bordered w-full"
|
||||
value={searchTwitterAccount}
|
||||
onChange={(e) => handleSearchTwitter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WhitelistTable
|
||||
list={accounts}
|
||||
isFetching={isLoading}
|
||||
totalCount={totalCount}
|
||||
currentPage={pageNumber}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
onWhitelistSuccess={handleWhitelistSuccess}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error mt-4">
|
||||
<span>Failed to load whitelist accounts. Please try again.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhitelistSettings
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import React, {useMemo} from 'react'
|
||||
import {type WhitelistAccount, WhitelistClient} from '../../../generated/ManagingApi'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {Table, Toast} from '../../../components/mollecules'
|
||||
|
||||
interface IWhitelistList {
|
||||
list: WhitelistAccount[]
|
||||
isFetching: boolean
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
onWhitelistSuccess: () => void
|
||||
}
|
||||
|
||||
const WhitelistTable: React.FC<IWhitelistList> = ({
|
||||
list,
|
||||
isFetching,
|
||||
totalCount,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onWhitelistSuccess
|
||||
}) => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const whitelistClient = new WhitelistClient({}, apiUrl)
|
||||
|
||||
async function handleSetWhitelisted(id: number) {
|
||||
const t = new Toast('Updating whitelist status...')
|
||||
|
||||
try {
|
||||
await whitelistClient.whitelist_SetWhitelisted(id)
|
||||
t.update('success', 'Account whitelisted successfully')
|
||||
onWhitelistSuccess()
|
||||
} catch (err: any) {
|
||||
const errorMessage = err?.response || err?.message || 'Unknown error'
|
||||
t.update('error', `Error: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
Header: 'Is Whitelisted',
|
||||
accessor: 'isWhitelisted',
|
||||
width: 120,
|
||||
Cell: ({ value }: any) => (
|
||||
<div className={`badge badge-lg ${value ? 'badge-success' : 'badge-warning'}`}>
|
||||
{value ? 'Yes' : 'No'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
accessor: 'actions',
|
||||
width: 150,
|
||||
Cell: ({ row }: any) => {
|
||||
const account = row.original as WhitelistAccount
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{!account.isWhitelisted && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => handleSetWhitelisted(account.id!)}
|
||||
disabled={isFetching}
|
||||
>
|
||||
Whitelist
|
||||
</button>
|
||||
)}
|
||||
{account.isWhitelisted && (
|
||||
<span className="badge badge-success badge-lg">Whitelisted</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: 'Privy ID',
|
||||
accessor: 'privyId',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
Header: 'Embedded Wallet',
|
||||
accessor: 'embeddedWallet',
|
||||
width: 200,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'External Ethereum Account',
|
||||
accessor: 'externalEthereumAccount',
|
||||
width: 200,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Twitter Account',
|
||||
accessor: 'twitterAccount',
|
||||
width: 150,
|
||||
Cell: ({ value }: any) => (
|
||||
<span>{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
], [isFetching])
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
return list.map((account) => ({
|
||||
id: account.id,
|
||||
privyId: account.privyId || '-',
|
||||
embeddedWallet: account.embeddedWallet || '-',
|
||||
externalEthereumAccount: account.externalEthereumAccount || '-',
|
||||
twitterAccount: account.twitterAccount || '-',
|
||||
isWhitelisted: account.isWhitelisted || false,
|
||||
createdAt: account.createdAt,
|
||||
actions: account,
|
||||
}))
|
||||
}, [list])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Total accounts: {totalCount} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isFetching && (
|
||||
<div className="flex justify-center my-4">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isFetching && list.length === 0 && (
|
||||
<div className="alert alert-info">
|
||||
<span>No whitelist accounts found.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isFetching && list.length > 0 && (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
showPagination={false}
|
||||
hiddenColumns={[]}
|
||||
/>
|
||||
|
||||
{/* Manual Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-4">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{'<<'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
|
||||
{/* Page numbers */}
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn btn-sm ${currentPage === pageNum ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={currentPage >= totalPages}
|
||||
>
|
||||
{'>>'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhitelistTable
|
||||
|
||||
41
src/Managing.WebApp/src/pages/monitoringPage/monitoring.tsx
Normal file
41
src/Managing.WebApp/src/pages/monitoringPage/monitoring.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {useState} from 'react'
|
||||
|
||||
import {Tabs} from '../../components/mollecules'
|
||||
|
||||
import HealthChecks from './healthchecks/healthChecks'
|
||||
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
||||
|
||||
type TabsType = {
|
||||
label: string
|
||||
index: number
|
||||
Component: React.FC<{}>
|
||||
}[]
|
||||
|
||||
// Tabs Array
|
||||
const tabs: TabsType = [
|
||||
{
|
||||
Component: HealthChecks,
|
||||
index: 1,
|
||||
label: 'Health Checks',
|
||||
},
|
||||
{
|
||||
Component: SqlMonitoring,
|
||||
index: 2,
|
||||
label: 'SQL Monitoring',
|
||||
},
|
||||
]
|
||||
|
||||
const Monitoring: React.FC = () => {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<Tabs selectedTab={selectedTab} onClick={setSelectedTab} tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Monitoring
|
||||
|
||||
@@ -2,15 +2,11 @@ import {useState} from 'react'
|
||||
|
||||
import {Tabs} from '../../components/mollecules'
|
||||
|
||||
import AccountSettings from './account/accountSettings'
|
||||
import HealthChecks from './healthchecks/healthChecks'
|
||||
import MoneyManagementSettings from './moneymanagement/moneyManagement'
|
||||
import Theme from './theme'
|
||||
import DefaultConfig from './defaultConfig/defaultConfig'
|
||||
import UserInfoSettings from './UserInfoSettings'
|
||||
import AccountFee from './accountFee/accountFee'
|
||||
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
||||
import WhitelistSettings from './whitelist/whitelistSettings'
|
||||
|
||||
type TabsType = {
|
||||
label: string
|
||||
@@ -30,41 +26,21 @@ const tabs: TabsType = [
|
||||
index: 2,
|
||||
label: 'Money Management',
|
||||
},
|
||||
{
|
||||
Component: AccountSettings,
|
||||
index: 3,
|
||||
label: 'Account Settings',
|
||||
},
|
||||
{
|
||||
Component: AccountFee,
|
||||
index: 4,
|
||||
index: 3,
|
||||
label: 'Account Fee',
|
||||
},
|
||||
{
|
||||
Component: Theme,
|
||||
index: 5,
|
||||
index: 4,
|
||||
label: 'Theme',
|
||||
},
|
||||
{
|
||||
Component: DefaultConfig,
|
||||
index: 6,
|
||||
index: 5,
|
||||
label: 'Quick Start Config',
|
||||
},
|
||||
{
|
||||
Component: HealthChecks,
|
||||
index: 7,
|
||||
label: 'Health Checks',
|
||||
},
|
||||
{
|
||||
Component: SqlMonitoring,
|
||||
index: 8,
|
||||
label: 'SQL Monitoring',
|
||||
},
|
||||
{
|
||||
Component: WhitelistSettings,
|
||||
index: 9,
|
||||
label: 'Whitelist',
|
||||
},
|
||||
]
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
|
||||
Reference in New Issue
Block a user