Send tokens
This commit is contained in:
@@ -284,7 +284,48 @@ export class AccountClient extends AuthorizedApiBase {
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
console.log(_responseText)
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<SwapInfos>(null as any);
|
||||
}
|
||||
|
||||
account_SendToken(name: string, request: SendTokenRequest): Promise<SwapInfos> {
|
||||
let url_ = this.baseUrl + "/Account/{name}/send-token";
|
||||
if (name === undefined || name === null)
|
||||
throw new Error("The parameter 'name' must be defined.");
|
||||
url_ = url_.replace("{name}", encodeURIComponent("" + name));
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processAccount_SendToken(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processAccount_SendToken(response: Response): Promise<SwapInfos> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as SwapInfos;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
@@ -3041,6 +3082,13 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface SendTokenRequest {
|
||||
recipientAddress: string;
|
||||
ticker: Ticker;
|
||||
amount: number;
|
||||
chainId?: number | null;
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
|
||||
@@ -210,6 +210,13 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface SendTokenRequest {
|
||||
recipientAddress: string;
|
||||
ticker: Ticker;
|
||||
amount: number;
|
||||
chainId?: number | null;
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
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
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, {useState} from 'react'
|
||||
import {FiRefreshCw} from 'react-icons/fi'
|
||||
import {FiRefreshCw, FiSend} from 'react-icons/fi'
|
||||
|
||||
import {SelectColumnFilter, Table} from '../../../components/mollecules'
|
||||
import type {IAccountRowDetail} from '../../../global/type.tsx'
|
||||
import type {Account, Balance} from '../../../generated/ManagingApi'
|
||||
import {Ticker} from '../../../generated/ManagingApi'
|
||||
import SwapModal from './SwapModal'
|
||||
import SendTokenModal from './SendTokenModal'
|
||||
|
||||
interface IAccountRowDetailProps extends IAccountRowDetail {
|
||||
account: Account
|
||||
@@ -26,6 +27,16 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
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
|
||||
@@ -40,6 +51,20 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -48,6 +73,14 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const closeSendTokenModal = () => {
|
||||
setSendTokenModalState({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Chain',
|
||||
@@ -92,16 +125,26 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
const balance = cell.row.original as Balance
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<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-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>
|
||||
)
|
||||
@@ -131,6 +174,16 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
availableAmount={swapModalState.availableAmount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sendTokenModalState.isOpen && sendTokenModalState.fromTicker && (
|
||||
<SendTokenModal
|
||||
isOpen={sendTokenModalState.isOpen}
|
||||
onClose={closeSendTokenModal}
|
||||
account={account}
|
||||
fromTicker={sendTokenModalState.fromTicker}
|
||||
availableAmount={sendTokenModalState.availableAmount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user