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 Scenario from '../../pages/scenarioPage/scenario'
|
||||||
import Tools from '../../pages/toolsPage/tools'
|
import Tools from '../../pages/toolsPage/tools'
|
||||||
|
|
||||||
|
const Admin = lazy(() => import('../../pages/adminPage/admin'))
|
||||||
const Backtest = lazy(() => import('../../pages/backtestPage/backtest'))
|
const Backtest = lazy(() => import('../../pages/backtestPage/backtest'))
|
||||||
const Bots = lazy(() => import('../../pages/botsPage/bots'))
|
const Bots = lazy(() => import('../../pages/botsPage/bots'))
|
||||||
const Dashboard = lazy(() => import('../../pages/dashboardPage/dashboard'))
|
const Dashboard = lazy(() => import('../../pages/dashboardPage/dashboard'))
|
||||||
|
const Monitoring = lazy(() => import('../../pages/monitoringPage/monitoring'))
|
||||||
const Settings = lazy(() => import('../../pages/settingsPage/settings'))
|
const Settings = lazy(() => import('../../pages/settingsPage/settings'))
|
||||||
|
|
||||||
const MyRoutes = () => {
|
const MyRoutes = () => {
|
||||||
@@ -58,6 +60,28 @@ const MyRoutes = () => {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</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 path="/backtest" element={<LayoutMain />}>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {Loader} from '../../atoms'
|
|||||||
import useCookie from '../../../hooks/useCookie'
|
import useCookie from '../../../hooks/useCookie'
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ href: '/bots', name: 'Bots' },
|
{ href: '/bots', name: 'Strategies' },
|
||||||
{ href: '/backtest', name: 'Backtest' },
|
{ href: '/backtest', name: 'Backtests' },
|
||||||
{ href: '/scenarios', name: 'Scenarios' },
|
|
||||||
{ href: '/tools', name: 'Tools' },
|
{ href: '/tools', name: 'Tools' },
|
||||||
|
{ href: '/monitoring', name: 'Monitoring' },
|
||||||
{ href: '/settings', name: 'Settings' },
|
{ href: '/settings', name: 'Settings' },
|
||||||
|
{ href: '/admin', name: 'Admin' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function navItems(isMobile = false) {
|
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 {Tabs} from '../../components/mollecules'
|
||||||
|
|
||||||
import AccountSettings from './account/accountSettings'
|
|
||||||
import HealthChecks from './healthchecks/healthChecks'
|
|
||||||
import MoneyManagementSettings from './moneymanagement/moneyManagement'
|
import MoneyManagementSettings from './moneymanagement/moneyManagement'
|
||||||
import Theme from './theme'
|
import Theme from './theme'
|
||||||
import DefaultConfig from './defaultConfig/defaultConfig'
|
import DefaultConfig from './defaultConfig/defaultConfig'
|
||||||
import UserInfoSettings from './UserInfoSettings'
|
import UserInfoSettings from './UserInfoSettings'
|
||||||
import AccountFee from './accountFee/accountFee'
|
import AccountFee from './accountFee/accountFee'
|
||||||
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
|
||||||
import WhitelistSettings from './whitelist/whitelistSettings'
|
|
||||||
|
|
||||||
type TabsType = {
|
type TabsType = {
|
||||||
label: string
|
label: string
|
||||||
@@ -30,41 +26,21 @@ const tabs: TabsType = [
|
|||||||
index: 2,
|
index: 2,
|
||||||
label: 'Money Management',
|
label: 'Money Management',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Component: AccountSettings,
|
|
||||||
index: 3,
|
|
||||||
label: 'Account Settings',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Component: AccountFee,
|
Component: AccountFee,
|
||||||
index: 4,
|
index: 3,
|
||||||
label: 'Account Fee',
|
label: 'Account Fee',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Component: Theme,
|
Component: Theme,
|
||||||
index: 5,
|
index: 4,
|
||||||
label: 'Theme',
|
label: 'Theme',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Component: DefaultConfig,
|
Component: DefaultConfig,
|
||||||
index: 6,
|
index: 5,
|
||||||
label: 'Quick Start Config',
|
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 = () => {
|
const Settings: React.FC = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user