update web ui

This commit is contained in:
2025-11-08 00:09:28 +07:00
parent e0795677e4
commit 42fb17d5e4
18 changed files with 2375 additions and 30 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 = () => {