Swap tokens
This commit is contained in:
@@ -89,7 +89,6 @@ Key Principles
|
|||||||
- Place static content and interfaces at file end.
|
- Place static content and interfaces at file end.
|
||||||
- Use content variables for static content outside render functions.
|
- Use content variables for static content outside render functions.
|
||||||
- Minimize 'use client', 'useEffect', and 'setState'. Favor RSC.
|
- Minimize 'use client', 'useEffect', and 'setState'. Favor RSC.
|
||||||
- Use Zod for form validation.
|
|
||||||
- Wrap client components in Suspense with fallback.
|
- Wrap client components in Suspense with fallback.
|
||||||
- Use dynamic loading for non-critical components.
|
- Use dynamic loading for non-critical components.
|
||||||
- Optimize images: WebP format, size data, lazy loading.
|
- Optimize images: WebP format, size data, lazy loading.
|
||||||
@@ -105,5 +104,6 @@ Key Principles
|
|||||||
- When you think its necessary update all the code from the database to the front end
|
- When you think its necessary update all the code from the database to the front end
|
||||||
- Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build
|
- Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build
|
||||||
- Do not reference new react library if a component already exist in mollecules or atoms
|
- Do not reference new react library if a component already exist in mollecules or atoms
|
||||||
|
- After finishing the editing, build the project
|
||||||
|
|
||||||
Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components.
|
Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Api.Models.Requests;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -89,6 +90,30 @@ namespace Managing.Api.Controllers
|
|||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swaps tokens on GMX for a specific account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the account to perform the swap for.</param>
|
||||||
|
/// <param name="request">The swap request containing ticker symbols, amount, and order parameters.</param>
|
||||||
|
/// <returns>The swap response with transaction details.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{name}/gmx-swap")]
|
||||||
|
public async Task<ActionResult<SwapInfos>> SwapGmxTokens(string name, [FromBody] SwapTokensRequest request)
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
var result = await _AccountService.SwapGmxTokensAsync(
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
request.FromTicker,
|
||||||
|
request.ToTicker,
|
||||||
|
request.Amount,
|
||||||
|
request.OrderType,
|
||||||
|
request.TriggerRatio,
|
||||||
|
request.AllowedSlippage
|
||||||
|
);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a specific account by name for the authenticated user.
|
/// Deletes a specific account by name for the authenticated user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
45
src/Managing.Api/Models/Requests/SwapTokensRequest.cs
Normal file
45
src/Managing.Api/Models/Requests/SwapTokensRequest.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for GMX token swap operations
|
||||||
|
/// </summary>
|
||||||
|
public class SwapTokensRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ticker symbol of the token to swap from
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public Ticker FromTicker { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ticker symbol of the token to swap to
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public Ticker ToTicker { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount to swap
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Range(0.000001, double.MaxValue, ErrorMessage = "Amount must be greater than 0")]
|
||||||
|
public double Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The order type (market or limit)
|
||||||
|
/// </summary>
|
||||||
|
public string OrderType { get; set; } = "market";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The trigger ratio for limit orders (optional)
|
||||||
|
/// </summary>
|
||||||
|
public double? TriggerRatio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The allowed slippage percentage (default 0.5%)
|
||||||
|
/// </summary>
|
||||||
|
[Range(0, 100, ErrorMessage = "Allowed slippage must be between 0 and 100")]
|
||||||
|
public double AllowedSlippage { get; set; } = 0.5;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.Abstractions.Services;
|
namespace Managing.Application.Abstractions.Services;
|
||||||
|
|
||||||
@@ -14,4 +15,7 @@ public interface IAccountService
|
|||||||
Task<Account> GetAccountByKey(string key, bool hideSecrets, bool getBalance);
|
Task<Account> GetAccountByKey(string key, bool hideSecrets, bool getBalance);
|
||||||
IEnumerable<Account> GetAccountsBalancesByUser(User user, bool hideSecrets = true);
|
IEnumerable<Account> GetAccountsBalancesByUser(User user, bool hideSecrets = true);
|
||||||
Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(User user, string accountName);
|
Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(User user, string accountName);
|
||||||
|
|
||||||
|
Task<SwapInfos> SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker,
|
||||||
|
double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.Abstractions.Services
|
namespace Managing.Application.Abstractions.Services
|
||||||
{
|
{
|
||||||
@@ -9,5 +10,8 @@ namespace Managing.Application.Abstractions.Services
|
|||||||
Task<T> CallGmxServiceAsync<T>(string endpoint, object payload);
|
Task<T> CallGmxServiceAsync<T>(string endpoint, object payload);
|
||||||
Task<T> GetGmxServiceAsync<T>(string endpoint, object payload = null);
|
Task<T> GetGmxServiceAsync<T>(string endpoint, object payload = null);
|
||||||
Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account);
|
Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account);
|
||||||
|
|
||||||
|
Task<SwapInfos> SwapGmxTokensAsync(string account, Ticker fromTicker, Ticker toTicker, double amount,
|
||||||
|
string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Managing.Application.Abstractions.Repositories;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Common;
|
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.Accounts;
|
namespace Managing.Application.Accounts;
|
||||||
|
|
||||||
@@ -47,15 +47,15 @@ public class AccountService : IAccountService
|
|||||||
{
|
{
|
||||||
request.User = user;
|
request.User = user;
|
||||||
|
|
||||||
if (request.Exchange == Enums.TradingExchanges.Evm
|
if (request.Exchange == TradingExchanges.Evm
|
||||||
&& request.Type == Enums.AccountType.Trader)
|
&& request.Type == AccountType.Trader)
|
||||||
{
|
{
|
||||||
var keys = _evmManager.GenerateAddress();
|
var keys = _evmManager.GenerateAddress();
|
||||||
request.Key = keys.Key;
|
request.Key = keys.Key;
|
||||||
request.Secret = keys.Secret;
|
request.Secret = keys.Secret;
|
||||||
}
|
}
|
||||||
else if (request.Exchange == Enums.TradingExchanges.Evm
|
else if (request.Exchange == TradingExchanges.Evm
|
||||||
&& request.Type == Enums.AccountType.Privy)
|
&& request.Type == AccountType.Privy)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(request.Key))
|
if (string.IsNullOrEmpty(request.Key))
|
||||||
{
|
{
|
||||||
@@ -200,6 +200,45 @@ public class AccountService : IAccountService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<SwapInfos> SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker, double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5)
|
||||||
|
{
|
||||||
|
// Get the account for the user
|
||||||
|
var account = await GetAccountByUser(user, accountName, true, false);
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Account '{accountName}' not found for user '{user.Name}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the account has a valid address/key
|
||||||
|
if (string.IsNullOrEmpty(account.Key))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Account '{accountName}' does not have a valid address");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Call the Web3ProxyService to swap GMX tokens
|
||||||
|
var swapInfos = await _web3ProxyService.SwapGmxTokensAsync(
|
||||||
|
account.Key,
|
||||||
|
fromTicker,
|
||||||
|
toTicker,
|
||||||
|
amount,
|
||||||
|
orderType,
|
||||||
|
triggerRatio,
|
||||||
|
allowedSlippage
|
||||||
|
);
|
||||||
|
|
||||||
|
return swapInfos;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!(ex is ArgumentException || ex is InvalidOperationException))
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error swapping GMX tokens for account {AccountName} and user {UserName}",
|
||||||
|
accountName, user.Name);
|
||||||
|
throw new InvalidOperationException($"Failed to swap GMX tokens: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ManageProperties(bool hideSecrets, bool getBalance, Account account)
|
private void ManageProperties(bool hideSecrets, bool getBalance, Account account)
|
||||||
{
|
{
|
||||||
if (account != null)
|
if (account != null)
|
||||||
|
|||||||
37
src/Managing.Domain/Accounts/SwapInfos.cs
Normal file
37
src/Managing.Domain/Accounts/SwapInfos.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace Managing.Domain.Accounts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Domain model for swap operation information
|
||||||
|
/// </summary>
|
||||||
|
public class SwapInfos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the swap operation was successful
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transaction hash if successful
|
||||||
|
/// </summary>
|
||||||
|
public string Hash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success message
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message if failed
|
||||||
|
/// </summary>
|
||||||
|
public string Error { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error type if failed
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suggestion for error resolution
|
||||||
|
/// </summary>
|
||||||
|
public string Suggestion { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Evm.Models.Proxy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for GMX swap operations
|
||||||
|
/// </summary>
|
||||||
|
public class GmxSwapResponse : Web3ProxyBaseResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transaction hash if successful
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("hash")]
|
||||||
|
[JsonPropertyName("hash")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success message
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("message")]
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error type if failed
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("errorType")]
|
||||||
|
[JsonPropertyName("errorType")]
|
||||||
|
public string ErrorType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suggestion for error resolution
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("suggestion")]
|
||||||
|
[JsonPropertyName("suggestion")]
|
||||||
|
public string Suggestion { get; set; }
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using Managing.Application.Abstractions.Services;
|
|||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Infrastructure.Evm.Models.Proxy;
|
using Managing.Infrastructure.Evm.Models.Proxy;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Infrastructure.Evm.Services
|
namespace Managing.Infrastructure.Evm.Services
|
||||||
{
|
{
|
||||||
@@ -180,6 +181,38 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<SwapInfos> SwapGmxTokensAsync(string account, Ticker fromTicker, Ticker toTicker, double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
account,
|
||||||
|
fromTicker = fromTicker.ToString(),
|
||||||
|
toTicker = toTicker.ToString(),
|
||||||
|
amount,
|
||||||
|
orderType,
|
||||||
|
triggerRatio,
|
||||||
|
allowedSlippage
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await CallGmxServiceAsync<GmxSwapResponse>("/swap-tokens", payload);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException("GMX swap response is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map from infrastructure model to domain model
|
||||||
|
return new SwapInfos
|
||||||
|
{
|
||||||
|
Success = response.Success,
|
||||||
|
Hash = response.Hash,
|
||||||
|
Message = response.Message,
|
||||||
|
Error = null, // GmxSwapResponse doesn't have Error property
|
||||||
|
ErrorType = response.ErrorType,
|
||||||
|
Suggestion = response.Suggestion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleErrorResponse(HttpResponseMessage response)
|
private async Task HandleErrorResponse(HttpResponseMessage response)
|
||||||
{
|
{
|
||||||
var statusCode = (int)response.StatusCode;
|
var statusCode = (int)response.StatusCode;
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ declare module 'fastify' {
|
|||||||
closeGmxPosition: typeof closeGmxPosition;
|
closeGmxPosition: typeof closeGmxPosition;
|
||||||
getGmxTrade: typeof getGmxTrade;
|
getGmxTrade: typeof getGmxTrade;
|
||||||
getGmxPositions: typeof getGmxPositions;
|
getGmxPositions: typeof getGmxPositions;
|
||||||
|
swapGmxTokens: typeof swapGmxTokens;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,45 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Define route to swap tokens
|
||||||
|
fastify.post('/swap-tokens', {
|
||||||
|
schema: {
|
||||||
|
body: Type.Object({
|
||||||
|
account: Type.String(),
|
||||||
|
fromTicker: Type.String(),
|
||||||
|
toTicker: Type.String(),
|
||||||
|
amount: Type.Number(),
|
||||||
|
orderType: Type.Optional(Type.Union([Type.Literal('market'), Type.Literal('limit')])),
|
||||||
|
triggerRatio: Type.Optional(Type.Number()),
|
||||||
|
allowedSlippage: Type.Optional(Type.Number())
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Type.Object({
|
||||||
|
success: Type.Boolean(),
|
||||||
|
hash: Type.Optional(Type.String()),
|
||||||
|
message: Type.Optional(Type.String()),
|
||||||
|
error: Type.Optional(Type.String()),
|
||||||
|
errorType: Type.Optional(Type.String()),
|
||||||
|
suggestion: Type.Optional(Type.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const { account, fromTicker, toTicker, amount, orderType, triggerRatio, allowedSlippage } = request.body
|
||||||
|
|
||||||
|
// Call the swapGmxTokens method from the GMX plugin
|
||||||
|
return request.swapGmxTokens(
|
||||||
|
reply,
|
||||||
|
account,
|
||||||
|
fromTicker,
|
||||||
|
toTicker,
|
||||||
|
amount,
|
||||||
|
orderType || 'market',
|
||||||
|
triggerRatio,
|
||||||
|
allowedSlippage || 0.5
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// Define route to get a trade
|
// Define route to get a trade
|
||||||
fastify.get('/trades', {
|
fastify.get('/trades', {
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
@@ -14,7 +14,21 @@ const Modal: React.FC<IModalProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
{showModal ? (
|
{showModal ? (
|
||||||
<form onSubmit={onSubmit}>
|
onSubmit ? (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="modal modal-bottom sm:modal-middle modal-open">
|
||||||
|
<div className="modal-box !max-w-4xl !w-11/12">
|
||||||
|
<ModalHeader
|
||||||
|
titleHeader={titleHeader}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
showModal={showModal}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
<div className="modal modal-bottom sm:modal-middle modal-open">
|
<div className="modal modal-bottom sm:modal-middle modal-open">
|
||||||
<div className="modal-box !max-w-4xl !w-11/12">
|
<div className="modal-box !max-w-4xl !w-11/12">
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
@@ -26,7 +40,7 @@ const Modal: React.FC<IModalProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -247,6 +247,49 @@ export class AccountClient extends AuthorizedApiBase {
|
|||||||
}
|
}
|
||||||
return Promise.resolve<GmxClaimableSummary>(null as any);
|
return Promise.resolve<GmxClaimableSummary>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account_SwapGmxTokens(name: string, request: SwapTokensRequest): Promise<SwapInfos> {
|
||||||
|
let url_ = this.baseUrl + "/Account/{name}/gmx-swap";
|
||||||
|
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_SwapGmxTokens(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processAccount_SwapGmxTokens(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) => {
|
||||||
|
console.log(_responseText)
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<SwapInfos>(null as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BacktestClient extends AuthorizedApiBase {
|
export class BacktestClient extends AuthorizedApiBase {
|
||||||
@@ -2871,68 +2914,22 @@ export interface RebateStatsData {
|
|||||||
discountFactor?: number;
|
discountFactor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Backtest {
|
export interface SwapInfos {
|
||||||
id: string;
|
success?: boolean;
|
||||||
finalPnl: number;
|
hash?: string | null;
|
||||||
winRate: number;
|
message?: string | null;
|
||||||
growthPercentage: number;
|
error?: string | null;
|
||||||
hodlPercentage: number;
|
errorType?: string | null;
|
||||||
config: TradingBotConfig;
|
suggestion?: string | null;
|
||||||
positions: Position[];
|
|
||||||
signals: Signal[];
|
|
||||||
candles: Candle[];
|
|
||||||
startDate: Date;
|
|
||||||
endDate: Date;
|
|
||||||
statistics: PerformanceMetrics;
|
|
||||||
fees: number;
|
|
||||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
|
||||||
optimizedMoneyManagement: MoneyManagement;
|
|
||||||
user: User;
|
|
||||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
|
||||||
score: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingBotConfig {
|
export interface SwapTokensRequest {
|
||||||
accountName: string;
|
fromTicker: Ticker;
|
||||||
moneyManagement: MoneyManagement;
|
toTicker: Ticker;
|
||||||
ticker: Ticker;
|
amount: number;
|
||||||
timeframe: Timeframe;
|
orderType?: string | null;
|
||||||
isForWatchingOnly: boolean;
|
triggerRatio?: number | null;
|
||||||
botTradingBalance: number;
|
allowedSlippage?: number;
|
||||||
isForBacktest: boolean;
|
|
||||||
cooldownPeriod: number;
|
|
||||||
maxLossStreak: number;
|
|
||||||
flipPosition: boolean;
|
|
||||||
name: string;
|
|
||||||
riskManagement?: RiskManagement | null;
|
|
||||||
scenario?: Scenario | null;
|
|
||||||
scenarioName?: string | null;
|
|
||||||
maxPositionTimeHours?: number | null;
|
|
||||||
closeEarlyWhenProfitable?: boolean;
|
|
||||||
flipOnlyWhenInProfit: boolean;
|
|
||||||
useSynthApi?: boolean;
|
|
||||||
useForPositionSizing?: boolean;
|
|
||||||
useForSignalFiltering?: boolean;
|
|
||||||
useForDynamicStopLoss?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoneyManagement {
|
|
||||||
name: string;
|
|
||||||
timeframe: Timeframe;
|
|
||||||
stopLoss: number;
|
|
||||||
takeProfit: number;
|
|
||||||
leverage: number;
|
|
||||||
user?: User | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Timeframe {
|
|
||||||
FiveMinutes = "FiveMinutes",
|
|
||||||
FifteenMinutes = "FifteenMinutes",
|
|
||||||
ThirtyMinutes = "ThirtyMinutes",
|
|
||||||
OneHour = "OneHour",
|
|
||||||
FourHour = "FourHour",
|
|
||||||
OneDay = "OneDay",
|
|
||||||
OneMinute = "OneMinute",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Ticker {
|
export enum Ticker {
|
||||||
@@ -3044,6 +3041,70 @@ export enum Ticker {
|
|||||||
Unknown = "Unknown",
|
Unknown = "Unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Backtest {
|
||||||
|
id: string;
|
||||||
|
finalPnl: number;
|
||||||
|
winRate: number;
|
||||||
|
growthPercentage: number;
|
||||||
|
hodlPercentage: number;
|
||||||
|
config: TradingBotConfig;
|
||||||
|
positions: Position[];
|
||||||
|
signals: Signal[];
|
||||||
|
candles: Candle[];
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
statistics: PerformanceMetrics;
|
||||||
|
fees: number;
|
||||||
|
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||||
|
optimizedMoneyManagement: MoneyManagement;
|
||||||
|
user: User;
|
||||||
|
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TradingBotConfig {
|
||||||
|
accountName: string;
|
||||||
|
moneyManagement: MoneyManagement;
|
||||||
|
ticker: Ticker;
|
||||||
|
timeframe: Timeframe;
|
||||||
|
isForWatchingOnly: boolean;
|
||||||
|
botTradingBalance: number;
|
||||||
|
isForBacktest: boolean;
|
||||||
|
cooldownPeriod: number;
|
||||||
|
maxLossStreak: number;
|
||||||
|
flipPosition: boolean;
|
||||||
|
name: string;
|
||||||
|
riskManagement?: RiskManagement | null;
|
||||||
|
scenario?: Scenario | null;
|
||||||
|
scenarioName?: string | null;
|
||||||
|
maxPositionTimeHours?: number | null;
|
||||||
|
closeEarlyWhenProfitable?: boolean;
|
||||||
|
flipOnlyWhenInProfit: boolean;
|
||||||
|
useSynthApi?: boolean;
|
||||||
|
useForPositionSizing?: boolean;
|
||||||
|
useForSignalFiltering?: boolean;
|
||||||
|
useForDynamicStopLoss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement {
|
||||||
|
name: string;
|
||||||
|
timeframe: Timeframe;
|
||||||
|
stopLoss: number;
|
||||||
|
takeProfit: number;
|
||||||
|
leverage: number;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Timeframe {
|
||||||
|
FiveMinutes = "FiveMinutes",
|
||||||
|
FifteenMinutes = "FifteenMinutes",
|
||||||
|
ThirtyMinutes = "ThirtyMinutes",
|
||||||
|
OneHour = "OneHour",
|
||||||
|
FourHour = "FourHour",
|
||||||
|
OneDay = "OneDay",
|
||||||
|
OneMinute = "OneMinute",
|
||||||
|
}
|
||||||
|
|
||||||
export interface RiskManagement {
|
export interface RiskManagement {
|
||||||
adverseProbabilityThreshold: number;
|
adverseProbabilityThreshold: number;
|
||||||
favorableProbabilityThreshold: number;
|
favorableProbabilityThreshold: number;
|
||||||
|
|||||||
@@ -83,68 +83,22 @@ export interface RebateStatsData {
|
|||||||
discountFactor?: number;
|
discountFactor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Backtest {
|
export interface SwapInfos {
|
||||||
id: string;
|
success?: boolean;
|
||||||
finalPnl: number;
|
hash?: string | null;
|
||||||
winRate: number;
|
message?: string | null;
|
||||||
growthPercentage: number;
|
error?: string | null;
|
||||||
hodlPercentage: number;
|
errorType?: string | null;
|
||||||
config: TradingBotConfig;
|
suggestion?: string | null;
|
||||||
positions: Position[];
|
|
||||||
signals: Signal[];
|
|
||||||
candles: Candle[];
|
|
||||||
startDate: Date;
|
|
||||||
endDate: Date;
|
|
||||||
statistics: PerformanceMetrics;
|
|
||||||
fees: number;
|
|
||||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
|
||||||
optimizedMoneyManagement: MoneyManagement;
|
|
||||||
user: User;
|
|
||||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
|
||||||
score: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingBotConfig {
|
export interface SwapTokensRequest {
|
||||||
accountName: string;
|
fromTicker: Ticker;
|
||||||
moneyManagement: MoneyManagement;
|
toTicker: Ticker;
|
||||||
ticker: Ticker;
|
amount: number;
|
||||||
timeframe: Timeframe;
|
orderType?: string | null;
|
||||||
isForWatchingOnly: boolean;
|
triggerRatio?: number | null;
|
||||||
botTradingBalance: number;
|
allowedSlippage?: number;
|
||||||
isForBacktest: boolean;
|
|
||||||
cooldownPeriod: number;
|
|
||||||
maxLossStreak: number;
|
|
||||||
flipPosition: boolean;
|
|
||||||
name: string;
|
|
||||||
riskManagement?: RiskManagement | null;
|
|
||||||
scenario?: Scenario | null;
|
|
||||||
scenarioName?: string | null;
|
|
||||||
maxPositionTimeHours?: number | null;
|
|
||||||
closeEarlyWhenProfitable?: boolean;
|
|
||||||
flipOnlyWhenInProfit: boolean;
|
|
||||||
useSynthApi?: boolean;
|
|
||||||
useForPositionSizing?: boolean;
|
|
||||||
useForSignalFiltering?: boolean;
|
|
||||||
useForDynamicStopLoss?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoneyManagement {
|
|
||||||
name: string;
|
|
||||||
timeframe: Timeframe;
|
|
||||||
stopLoss: number;
|
|
||||||
takeProfit: number;
|
|
||||||
leverage: number;
|
|
||||||
user?: User | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Timeframe {
|
|
||||||
FiveMinutes = "FiveMinutes",
|
|
||||||
FifteenMinutes = "FifteenMinutes",
|
|
||||||
ThirtyMinutes = "ThirtyMinutes",
|
|
||||||
OneHour = "OneHour",
|
|
||||||
FourHour = "FourHour",
|
|
||||||
OneDay = "OneDay",
|
|
||||||
OneMinute = "OneMinute",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Ticker {
|
export enum Ticker {
|
||||||
@@ -256,6 +210,70 @@ export enum Ticker {
|
|||||||
Unknown = "Unknown",
|
Unknown = "Unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Backtest {
|
||||||
|
id: string;
|
||||||
|
finalPnl: number;
|
||||||
|
winRate: number;
|
||||||
|
growthPercentage: number;
|
||||||
|
hodlPercentage: number;
|
||||||
|
config: TradingBotConfig;
|
||||||
|
positions: Position[];
|
||||||
|
signals: Signal[];
|
||||||
|
candles: Candle[];
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
statistics: PerformanceMetrics;
|
||||||
|
fees: number;
|
||||||
|
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||||
|
optimizedMoneyManagement: MoneyManagement;
|
||||||
|
user: User;
|
||||||
|
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TradingBotConfig {
|
||||||
|
accountName: string;
|
||||||
|
moneyManagement: MoneyManagement;
|
||||||
|
ticker: Ticker;
|
||||||
|
timeframe: Timeframe;
|
||||||
|
isForWatchingOnly: boolean;
|
||||||
|
botTradingBalance: number;
|
||||||
|
isForBacktest: boolean;
|
||||||
|
cooldownPeriod: number;
|
||||||
|
maxLossStreak: number;
|
||||||
|
flipPosition: boolean;
|
||||||
|
name: string;
|
||||||
|
riskManagement?: RiskManagement | null;
|
||||||
|
scenario?: Scenario | null;
|
||||||
|
scenarioName?: string | null;
|
||||||
|
maxPositionTimeHours?: number | null;
|
||||||
|
closeEarlyWhenProfitable?: boolean;
|
||||||
|
flipOnlyWhenInProfit: boolean;
|
||||||
|
useSynthApi?: boolean;
|
||||||
|
useForPositionSizing?: boolean;
|
||||||
|
useForSignalFiltering?: boolean;
|
||||||
|
useForDynamicStopLoss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement {
|
||||||
|
name: string;
|
||||||
|
timeframe: Timeframe;
|
||||||
|
stopLoss: number;
|
||||||
|
takeProfit: number;
|
||||||
|
leverage: number;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Timeframe {
|
||||||
|
FiveMinutes = "FiveMinutes",
|
||||||
|
FifteenMinutes = "FifteenMinutes",
|
||||||
|
ThirtyMinutes = "ThirtyMinutes",
|
||||||
|
OneHour = "OneHour",
|
||||||
|
FourHour = "FourHour",
|
||||||
|
OneDay = "OneDay",
|
||||||
|
OneMinute = "OneMinute",
|
||||||
|
}
|
||||||
|
|
||||||
export interface RiskManagement {
|
export interface RiskManagement {
|
||||||
adverseProbabilityThreshold: number;
|
adverseProbabilityThreshold: number;
|
||||||
favorableProbabilityThreshold: number;
|
favorableProbabilityThreshold: number;
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export type ICardPositionFlipped = {
|
|||||||
export type IAccountRowDetail = {
|
export type IAccountRowDetail = {
|
||||||
balances: Balance[]
|
balances: Balance[]
|
||||||
showTotal?: boolean
|
showTotal?: boolean
|
||||||
|
account?: Account
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IGridTile = {
|
export type IGridTile = {
|
||||||
|
|||||||
42
src/Managing.WebApp/src/hooks/useApiError.ts
Normal file
42
src/Managing.WebApp/src/hooks/useApiError.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {useCallback, useState} from 'react'
|
||||||
|
import {extractErrorMessage} from '../utils/apiErrorHandler'
|
||||||
|
|
||||||
|
interface UseApiErrorReturn {
|
||||||
|
error: string | null
|
||||||
|
setError: (error: string | null) => void
|
||||||
|
clearError: () => void
|
||||||
|
handleError: (err: unknown) => string
|
||||||
|
handleApiErrorWithToast: (err: unknown, toast: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for handling API errors consistently across components
|
||||||
|
* @returns Error handling utilities
|
||||||
|
*/
|
||||||
|
export function useApiError(): UseApiErrorReturn {
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setError(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleError = useCallback((err: unknown): string => {
|
||||||
|
const errorMessage = extractErrorMessage(err)
|
||||||
|
setError(errorMessage)
|
||||||
|
return errorMessage
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleApiErrorWithToast = useCallback((err: unknown, toast: any) => {
|
||||||
|
const errorMessage = extractErrorMessage(err)
|
||||||
|
setError(errorMessage)
|
||||||
|
toast.update('error', `Error: ${errorMessage}`)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
clearError,
|
||||||
|
handleError,
|
||||||
|
handleApiErrorWithToast
|
||||||
|
}
|
||||||
|
}
|
||||||
263
src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx
Normal file
263
src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const SwapModal: React.FC<SwapModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
account,
|
||||||
|
fromTicker,
|
||||||
|
availableAmount,
|
||||||
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 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}>
|
||||||
|
<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"
|
||||||
|
{...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="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"
|
||||||
|
{...register('allowedSlippage', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
min: 0.1,
|
||||||
|
max: 10,
|
||||||
|
value: 0.5
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
{selectedOrderType === 'limit' && (
|
||||||
|
<FormInput label="Trigger Ratio" htmlFor="triggerRatio">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
placeholder="Enter trigger ratio"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register('triggerRatio', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
@@ -1,53 +1,118 @@
|
|||||||
import React from 'react'
|
import React, {useState} from 'react'
|
||||||
|
import {FiRefreshCw} from 'react-icons/fi'
|
||||||
|
|
||||||
import { SelectColumnFilter, Table } from '../../../components/mollecules'
|
import {SelectColumnFilter, Table} from '../../../components/mollecules'
|
||||||
import { IAccountRowDetail } from '../../../global/type'
|
import type {IAccountRowDetail} from '../../../global/type.tsx'
|
||||||
|
import type {Account, Balance} from '../../../generated/ManagingApi'
|
||||||
|
import {Ticker} from '../../../generated/ManagingApi'
|
||||||
|
import SwapModal from './SwapModal'
|
||||||
|
|
||||||
const columns = [
|
interface IAccountRowDetailProps extends IAccountRowDetail {
|
||||||
{
|
account: Account
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const AccountRowDetails: React.FC<IAccountRowDetail> = ({
|
const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||||
balances,
|
balances,
|
||||||
showTotal,
|
showTotal,
|
||||||
|
account,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [swapModalState, setSwapModalState] = 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 closeSwapModal = () => {
|
||||||
|
setSwapModalState({
|
||||||
|
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">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Header: 'Actions',
|
||||||
|
accessor: 'actions',
|
||||||
|
disableFilters: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table
|
<Table
|
||||||
@@ -56,6 +121,16 @@ const AccountRowDetails: React.FC<IAccountRowDetail> = ({
|
|||||||
showTotal={showTotal}
|
showTotal={showTotal}
|
||||||
showPagination={false}
|
showPagination={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{swapModalState.isOpen && swapModalState.fromTicker && (
|
||||||
|
<SwapModal
|
||||||
|
isOpen={swapModalState.isOpen}
|
||||||
|
onClose={closeSwapModal}
|
||||||
|
account={account}
|
||||||
|
fromTicker={swapModalState.fromTicker}
|
||||||
|
availableAmount={swapModalState.availableAmount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
|
|||||||
<AccountRowDetails
|
<AccountRowDetails
|
||||||
balances={balances}
|
balances={balances}
|
||||||
showTotal={true}
|
showTotal={true}
|
||||||
|
account={row.original}
|
||||||
></AccountRowDetails>
|
></AccountRowDetails>
|
||||||
) : (
|
) : (
|
||||||
<div>No balances</div>
|
<div>No balances</div>
|
||||||
|
|||||||
161
src/Managing.WebApp/src/utils/apiErrorHandler.md
Normal file
161
src/Managing.WebApp/src/utils/apiErrorHandler.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# API Error Handling Utilities
|
||||||
|
|
||||||
|
This module provides utilities for handling API errors consistently across the application.
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
### `extractErrorMessage(err: unknown): string`
|
||||||
|
|
||||||
|
Extracts meaningful error messages from API exceptions and other errors.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `err` - The error object caught from API calls
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- A user-friendly error message string
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { extractErrorMessage } from '../utils/apiErrorHandler'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiCall()
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = extractErrorMessage(err)
|
||||||
|
console.log(errorMessage) // "Insufficient balance for swap"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `handleApiError(err: unknown)`
|
||||||
|
|
||||||
|
Handles API errors and returns a standardized error object with additional context.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `err` - The error object caught from API calls
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
message: string
|
||||||
|
isApiException: boolean
|
||||||
|
status?: number
|
||||||
|
response?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { handleApiError } from '../utils/apiErrorHandler'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiCall()
|
||||||
|
} catch (err) {
|
||||||
|
const errorInfo = handleApiError(err)
|
||||||
|
console.log(errorInfo.message) // "Insufficient balance for swap"
|
||||||
|
console.log(errorInfo.status) // 400
|
||||||
|
console.log(errorInfo.isApiException) // true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Hook
|
||||||
|
|
||||||
|
### `useApiError()`
|
||||||
|
|
||||||
|
A React hook that provides error handling utilities for API calls.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
error: string | null
|
||||||
|
setError: (error: string | null) => void
|
||||||
|
clearError: () => void
|
||||||
|
handleError: (err: unknown) => string
|
||||||
|
handleApiErrorWithToast: (err: unknown, toast: any) => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { useApiError } from '../hooks/useApiError'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { error, setError, clearError, handleError, handleApiErrorWithToast } = useApiError()
|
||||||
|
|
||||||
|
const handleApiCall = async () => {
|
||||||
|
try {
|
||||||
|
const result = await apiCall()
|
||||||
|
clearError() // Clear any previous errors
|
||||||
|
} catch (err) {
|
||||||
|
// Option 1: Simple error handling
|
||||||
|
const errorMessage = handleError(err)
|
||||||
|
|
||||||
|
// Option 2: Error handling with toast
|
||||||
|
handleApiErrorWithToast(err, toast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error && <div className="alert alert-error">{error}</div>}
|
||||||
|
<button onClick={handleApiCall}>Make API Call</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Types Handled
|
||||||
|
|
||||||
|
1. **ApiException** - Generated client exceptions with server response details
|
||||||
|
2. **Error** - Standard JavaScript Error objects
|
||||||
|
3. **Unknown** - Any other error types
|
||||||
|
|
||||||
|
## Response Parsing
|
||||||
|
|
||||||
|
The utilities attempt to parse JSON responses in this order:
|
||||||
|
1. `message` - Standard error message field
|
||||||
|
2. `error` - Alternative error field
|
||||||
|
3. `detail` - ASP.NET Core validation error field
|
||||||
|
4. Raw response text if JSON parsing fails
|
||||||
|
5. Exception message as final fallback
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const result = await client.someApiCall()
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = extractErrorMessage(err)
|
||||||
|
setError(errorMessage)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Toast Notifications
|
||||||
|
```typescript
|
||||||
|
const { handleApiErrorWithToast } = useApiError()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.someApiCall()
|
||||||
|
toast.update('success', 'Operation successful!')
|
||||||
|
} catch (err) {
|
||||||
|
handleApiErrorWithToast(err, toast)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Custom Error Handling
|
||||||
|
```typescript
|
||||||
|
const { handleError } = useApiError()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.someApiCall()
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = handleError(err)
|
||||||
|
|
||||||
|
// Custom logic based on error type
|
||||||
|
if (errorMessage.includes('insufficient')) {
|
||||||
|
showBalanceWarning()
|
||||||
|
} else if (errorMessage.includes('network')) {
|
||||||
|
showRetryButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
50
src/Managing.WebApp/src/utils/apiErrorHandler.ts
Normal file
50
src/Managing.WebApp/src/utils/apiErrorHandler.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Extracts meaningful error messages from API exceptions and other errors
|
||||||
|
* @param err - The error object caught from API calls
|
||||||
|
* @returns A user-friendly error message
|
||||||
|
*/
|
||||||
|
export function extractErrorMessage(err: unknown): string {
|
||||||
|
// Handle ApiException specifically to extract the actual error message
|
||||||
|
if (err && typeof err === 'object' && 'isApiException' in err) {
|
||||||
|
const apiException = err as any
|
||||||
|
try {
|
||||||
|
// Try to parse the response as JSON to get the actual error message
|
||||||
|
const responseData = JSON.parse(apiException.response)
|
||||||
|
return responseData.message || responseData.error || responseData.detail || apiException.message
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, use the response text directly
|
||||||
|
return apiException.response || apiException.message
|
||||||
|
}
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
return err.message
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'An unknown error occurred'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles API errors and returns a standardized error object
|
||||||
|
* @param err - The error object caught from API calls
|
||||||
|
* @returns An object containing the error message and additional context
|
||||||
|
*/
|
||||||
|
export function handleApiError(err: unknown): {
|
||||||
|
message: string
|
||||||
|
isApiException: boolean
|
||||||
|
status?: number
|
||||||
|
response?: string
|
||||||
|
} {
|
||||||
|
if (err && typeof err === 'object' && 'isApiException' in err) {
|
||||||
|
const apiException = err as any
|
||||||
|
return {
|
||||||
|
message: extractErrorMessage(err),
|
||||||
|
isApiException: true,
|
||||||
|
status: apiException.status,
|
||||||
|
response: apiException.response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: extractErrorMessage(err),
|
||||||
|
isApiException: false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user