Filter everything with users (#16)

* Filter everything with users

* Fix backtests and user management

* Add cursor rules

* Fix backtest and bots

* Update configs names

* Sign until unauth

* Setup delegate

* Setup delegate and sign

* refact

* Enhance Privy signature generation with improved cryptographic methods

* Add Fastify backend

* Add Fastify backend routes for privy

* fix privy signing

* fix privy client

* Fix tests

* add gmx core

* fix merging sdk

* Fix tests

* add gmx core

* add gmx core

* add privy to boilerplate

* clean

* fix

* add fastify

* Remove Managing.Fastify submodule

* Add Managing.Fastify as regular directory instead of submodule

* Update .gitignore to exclude Managing.Fastify dist and node_modules directories

* Add token approval functionality to Privy plugin

- Introduced a new endpoint `/approve-token` for approving ERC20 tokens.
- Added `approveToken` method to the Privy plugin for handling token approvals.
- Updated `signPrivyMessage` to differentiate between message signing and token approval requests.
- Enhanced the plugin with additional schemas for input validation.
- Included new utility functions for token data retrieval and message construction.
- Updated tests to verify the new functionality and ensure proper request decoration.

* Add PrivyApproveTokenResponse model for token approval response

- Created a new class `PrivyApproveTokenResponse` to encapsulate the response structure for token approval requests.
- The class includes properties for `Success` status and a transaction `Hash`.

* Refactor trading commands and enhance API routes

- Updated `OpenPositionCommandHandler` to use asynchronous methods for opening trades and canceling orders.
- Introduced new Fastify routes for opening positions and canceling orders with appropriate request validation.
- Modified `EvmManager` to handle both Privy and non-Privy wallet operations, utilizing the Fastify API for Privy wallets.
- Adjusted test configurations to reflect changes in account types and added helper methods for testing Web3 proxy services.

* Enhance GMX trading functionality and update dependencies

- Updated `dev:start` script in `package.json` to include the `-d` flag for Fastify.
- Upgraded `fastify-cli` dependency to version 7.3.0.
- Added `sourceMap` option to `tsconfig.json`.
- Refactored GMX plugin to improve position opening logic, including enhanced error handling and validation.
- Introduced a new method `getMarketInfoFromTicker` for better market data retrieval.
- Updated account type in `PrivateKeys.cs` to use `Privy`.
- Adjusted `EvmManager` to utilize the `direction` enum directly for trade direction handling.

* Refactor GMX plugin for improved trading logic and market data retrieval

- Enhanced the `openGmxPositionImpl` function to utilize the `TradeDirection` enum for trade direction handling.
- Introduced `getTokenDataFromTicker` and `getMarketByIndexToken` functions for better market and token data retrieval.
- Updated collateral calculation and logging for clarity.
- Adjusted `EvmManager` to ensure proper handling of price values in trade requests.

* Refactor GMX plugin and enhance testing for position opening

- Updated `test:single` script in `package.json` to include TypeScript compilation before running tests.
- Removed `this` context from `getClientForAddress` function and replaced logging with `console.error`.
- Improved collateral calculation in `openGmxPositionImpl` for better precision.
- Adjusted type casting for `direction` in the API route to utilize `TradeDirection` enum.
- Added a new test for opening a long position in GMX, ensuring functionality and correctness.

* Update sdk

* Update

* update fastify

* Refactor start script in package.json to simplify command execution

- Removed the build step from the start script, allowing for a more direct launch of the Fastify server.

* Update package.json for Web3Proxy

- Changed the name from "Web3Proxy" to "web3-proxy".
- Updated version from "0.0.0" to "1.0.0".
- Modified the description to "The official Managing Web3 Proxy".

* Update Dockerfile for Web3Proxy

- Upgraded Node.js base image from 18-alpine to 22.14.0-alpine.
- Added NODE_ENV environment variable set to production.

* Refactor Dockerfile and package.json for Web3Proxy

- Removed the build step from the Dockerfile to streamline the image creation process.
- Updated the start script in package.json to include the build step, ensuring the application is built before starting the server.

* Add fastify-tsconfig as a development dependency in Dockerfile-web3proxy

* Remove fastify-tsconfig extension from tsconfig.json for Web3Proxy

* Add PrivyInitAddressResponse model for handling initialization responses

- Introduced a new class `PrivyInitAddressResponse` to encapsulate the response structure for Privy initialization, including properties for success status, USDC hash, order vault hash, and error message.

* Update

* Update

* Remove fastify-tsconfig installation from Dockerfile-web3proxy

* Add build step to Dockerfile-web3proxy

- Included `npm run build` in the Dockerfile to ensure the application is built during the image creation process.

* Update

* approvals

* Open position from front embedded wallet

* Open position from front embedded wallet

* Open position from front embedded wallet

* Fix call contracts

* Fix limit price

* Close position

* Fix close position

* Fix close position

* add pinky

* Refactor position handling logic

* Update Dockerfile-pinky to copy package.json and source code from the correct directory

* Implement password protection modal and enhance UI with new styles; remove unused audio elements and update package dependencies.

* add cancel orders

* Update callContract function to explicitly cast account address as Address type

* Update callContract function to cast transaction parameters as any type for compatibility

* Cast transaction parameters as any type in approveTokenImpl for compatibility

* Cast wallet address and transaction parameters as Address type in approveTokenImpl for type safety

* Add .env configuration file for production setup including database and server settings

* Refactor home route to update welcome message and remove unused SDK configuration code

* add referral code

* fix referral

* Add sltp

* Fix typo

* Fix typo

* setup sltp on backtend

* get orders

* get positions with slp

* fixes

* fixes close position

* fixes

* Remove MongoDB project references from Dockerfiles for managing and worker APIs

* Comment out BotManagerWorker service registration and remove MongoDB project reference from Dockerfile

* fixes
This commit is contained in:
Oda
2025-04-20 22:18:27 +07:00
committed by GitHub
parent 0ae96a3278
commit 528c62a0a1
400 changed files with 94446 additions and 1635 deletions

View File

@@ -1,45 +1,276 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models;
using Managing.Infrastructure.Evm.Models.Privy;
using Org.Webpki.JsonCanonicalizer;
public class PrivyService : IPrivyService
{
private readonly HttpClient _privyClient;
private readonly string _appId;
private readonly string _appSecret;
private readonly string _authorizationKey;
public PrivyService(IPrivySettings settings)
{
_privyClient = new HttpClient();
_appId = settings.AppId;
_appSecret = settings.AppSecret;
_authorizationKey = settings.AuthorizationKey;
ConfigureHttpClient();
}
private void ConfigureHttpClient()
{
_privyClient.BaseAddress = new Uri("https://api.privy.io/");
_privyClient.BaseAddress = new Uri("https://auth.privy.io/");
var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_appId}:{_appSecret}"));
// _privyClient.DefaultRequestHeaders.Authorization =
// new AuthenticationHeaderValue("Basic", $"{_appId}:{_appSecret}");
_privyClient.DefaultRequestHeaders.Add("privy-app-id", _appId);
// add custom header
_privyClient.DefaultRequestHeaders.Add("Authorization", authToken);
_privyClient.DefaultRequestHeaders.Add("Authorization", $"Basic {authToken}");
_privyClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
/// <summary>
/// Generates an authorization signature for a request to the Privy API
/// </summary>
/// <param name="url">The full URL for the request</param>
/// <param name="body">The request body</param>
/// <param name="httpMethod">The HTTP method to use for the request (defaults to POST)</param>
/// <returns>The generated signature</returns>
public string GenerateAuthorizationSignature(string url, object body, string httpMethod = "POST")
{
try
{
// Ensure we have a full, absolute URL for signature calculation
string fullUrl;
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
// Already a full URL
fullUrl = url;
}
else
{
// It's a relative path, so construct the full URL using the base address
string relativePath = url.StartsWith("/") ? url.Substring(1) : url;
fullUrl = new Uri(_privyClient.BaseAddress, relativePath).ToString();
}
Console.WriteLine($"Full URL for signature: {fullUrl}");
// Create a new dictionary for headers to ensure consistent ordering
var headers = new Dictionary<string, string>
{
{ "privy-app-id", _appId }
};
// Create the properly structured payload object according to Privy's specification
var signaturePayload = new Dictionary<string, object>
{
["version"] = 1,
["method"] = httpMethod,
["url"] = fullUrl, // Use the FULL URL for signature calculation as per Privy docs
["body"] = body,
["headers"] = headers
};
// Serialize to JSON with consistent settings
// Note: We're not forcing camelCase conversion, preserving original property casing
var options = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = null // Preserve original property casing
};
string serializedPayload = JsonSerializer.Serialize(signaturePayload, options);
Console.WriteLine($"Request payload for signature: {serializedPayload}");
// Use the JSON Canonicalizer to ensure consistent JSON formatting
JsonCanonicalizer jsonCanonicalizer = new JsonCanonicalizer(serializedPayload);
byte[] canonicalizedBytes = jsonCanonicalizer.GetEncodedUTF8();
string canonicalizedString = jsonCanonicalizer.GetEncodedString();
Console.WriteLine($"Request jsonCanonicalizer payload for signature: {canonicalizedString}");
// Remove the 'wallet-auth:' prefix from the authorization key
string privateKeyAsString = _authorizationKey.Replace("wallet-auth:", "");
// Convert the private key to PEM format
string privateKeyAsPem = $"-----BEGIN PRIVATE KEY-----\n{privateKeyAsString}\n-----END PRIVATE KEY-----";
// Create a private key object explicitly using ECDSA P-256 curve
using var privateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
privateKey.ImportFromPem(privateKeyAsPem);
// Sign the canonicalized payload buffer with the private key using SHA-256
// CngAlgorithm.ECDsaP256 is implicitly used through the curve specification above
byte[] signatureBuffer = privateKey.SignData(canonicalizedBytes, HashAlgorithmName.SHA256);
// Convert the signature to a base64 string
string signature = Convert.ToBase64String(signatureBuffer);
Console.WriteLine($"Generated signature: {signature}");
return signature;
}
catch (Exception ex)
{
Console.WriteLine($"Error generating signature: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
throw new Exception($"Failed to generate authorization signature: {ex.Message}", ex);
}
}
/// <summary>
/// Generates an authorization signature for delegated actions and sends the HTTP request with the same payload
/// </summary>
/// <param name="url">The full URL for the request</param>
/// <param name="body">The request body</param>
/// <param name="httpMethod">The HTTP method to use for the request (defaults to POST)</param>
/// <returns>The HTTP response from the request</returns>
private async Task<HttpResponseMessage> GenerateAuthorizationSignatureAndSendRequestAsync(string url, object body, HttpMethod httpMethod = null)
{
httpMethod ??= HttpMethod.Post;
try
{
// Ensure we have a full, absolute URL for the request
string fullUrl;
string requestPath;
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
// Already a full URL
fullUrl = url;
// For the HTTP request, we need just the path if it matches our base address
if (uri.Host == new Uri(_privyClient.BaseAddress.ToString()).Host)
{
requestPath = uri.PathAndQuery;
}
else
{
// Using a different host than the base address
throw new InvalidOperationException($"URL host {uri.Host} doesn't match base address host {_privyClient.BaseAddress.Host}");
}
}
else
{
// It's a relative path, so construct the full URL using the base address
string relativePath = url.StartsWith("/") ? url.Substring(1) : url;
fullUrl = new Uri(_privyClient.BaseAddress, relativePath).ToString();
requestPath = url.StartsWith("/") ? url : $"/{url}";
}
Console.WriteLine($"Full URL for signature: {fullUrl}");
Console.WriteLine($"Request path for HTTP request: {requestPath}");
// Generate the authorization signature
string signature = GenerateAuthorizationSignature(fullUrl, body, httpMethod.Method);
// Prepare the JSON serialization options
var options = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = null // Preserve original property casing
};
// Create the HTTP request
var request = new HttpRequestMessage(httpMethod, requestPath);
// Use the same serialization options to ensure the request body is identical to what we signed
var json = JsonSerializer.Serialize(body, options);
// Create StringContent with explicit Content-Type header
var content = new StringContent(json, Encoding.UTF8);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
request.Content = content;
// Add the headers in the same order we used for signing
request.Headers.Add("privy-app-id", _appId);
request.Headers.Add("privy-authorization-signature", signature);
// Log all request headers and content for debugging
Console.WriteLine($"Sending request to {fullUrl}");
Console.WriteLine($"With signature: {signature}");
Console.WriteLine($"Request content: {json}");
Console.WriteLine("Request headers:");
foreach (var header in request.Headers)
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
}
if (request.Content != null && request.Content.Headers != null)
{
Console.WriteLine("Content headers:");
foreach (var header in request.Content.Headers)
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
}
}
// Send the request and return the response
var response = await _privyClient.SendAsync(request);
// Log response information
Console.WriteLine($"Response status: {response.StatusCode}");
string responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response content: {responseContent}");
return response;
}
catch (Exception ex)
{
Console.WriteLine($"Error sending request: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
throw new Exception($"Failed to send request: {ex.Message}", ex);
}
}
/// <summary>
/// Adds the authorization signature header to the request
/// </summary>
/// <remarks>
/// This method is kept for backward compatibility.
/// Prefer using GenerateAuthorizationSignatureAndSendRequestAsync which both generates
/// the signature and sends the request with the same payload.
/// </remarks>
/// <param name="request">The HTTP request message</param>
/// <param name="url">The full URL for the request</param>
/// <param name="body">The request body</param>
private void AddAuthorizationSignatureHeader(HttpRequestMessage request, string url, object body)
{
if (!string.IsNullOrEmpty(_authorizationKey))
{
string signature = GenerateAuthorizationSignature(url, body);
request.Headers.Add("privy-authorization-signature", signature);
}
}
public async Task<PrivyWallet> CreateWalletAsync(string chainType = "ethereum")
{
try
{
var json = JsonSerializer.Serialize(new { chain_type = chainType });
var content = new StringContent(json, Encoding.UTF8, "application/json");
var requestBody = new { chain_type = chainType };
var url = "https://api.privy.io/v1/wallets";
var response = await _privyClient.PostAsJsonAsync("/v1/wallets", content);
// Use the new method that both generates the signature and sends the request
var response = await GenerateAuthorizationSignatureAndSendRequestAsync(url, requestBody);
var result = new PrivyWallet();
@@ -77,9 +308,242 @@ public class PrivyService : IPrivyService
}
};
return await _privyClient.PostAsJsonAsync(
$"/v1/wallets/{walletId}/rpc",
requestBody
);
var url = $"https://api.privy.io/v1/wallets/{walletId}/rpc";
// Use the new method that both generates the signature and sends the request
return await GenerateAuthorizationSignatureAndSendRequestAsync(url, requestBody);
}
public class PrivyRequest
{
[JsonPropertyName("method")] public string Method { get; set; }
[JsonPropertyName("chain_type")] public string ChainType { get; set; }
[JsonPropertyName("address")] public string Address { get; set; }
[JsonPropertyName("params")] public PrivyParamsRequest Params { get; set; }
}
public class PrivyParamsRequest
{
[JsonPropertyName("message")] public string Message { get; set; }
[JsonPropertyName("encoding")] public string Encoding { get; set; }
}
/// <summary>
/// Signs a message using the embedded wallet
/// </summary>
/// <param name="embeddedWallet">The address of the embedded wallet</param>
/// <param name="message">The message to sign</param>
/// <param name="method">The signing method to use (e.g., "personal_sign", "eth_sign")</param>
/// <returns>The signature response</returns>
public async Task<string> SignMessageAsync(string embeddedWallet, string message,
string method = "personal_sign")
{
try
{
// Construct the request body using the exact format from Privy documentation
var requestBody = new
{
address = embeddedWallet,
chain_type = "ethereum",
method = method,
@params = new
{
message = message,
encoding = "utf-8"
}
};
// The full URL for the Privy RPC endpoint exactly as specified in docs
var url = "https://auth.privy.io/v1/wallets/rpc";
// Use the new method that both generates the signature and sends the request
var response = await GenerateAuthorizationSignatureAndSendRequestAsync(url, requestBody);
// Check for successful response
string responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Failed to sign message: {response.StatusCode} - {responseContent}");
}
// Parse the response to get the signature
var responseObject = JsonSerializer.Deserialize<JsonElement>(responseContent);
// Extract the signature from the response
if (responseObject.TryGetProperty("data", out var dataElement))
{
string signatureResult = dataElement.GetString() ?? string.Empty;
Console.WriteLine($"Extracted signature: {signatureResult}");
return signatureResult;
}
throw new Exception($"Invalid signature response format: {responseContent}");
}
catch (Exception ex)
{
Console.WriteLine($"SignMessageAsync error: {ex}");
throw new Exception($"Error signing message: {ex.Message}", ex);
}
}
/// <summary>
/// Signs typed data (EIP-712) using the embedded wallet
/// </summary>
/// <param name="walletId">The ID of the wallet to use for signing</param>
/// <param name="typedData">The typed data to sign (must be a valid JSON string conforming to EIP-712)</param>
/// <param name="caip2">The CAIP-2 chain identifier</param>
/// <returns>The signature</returns>
public async Task<string> SignTypedDataAsync(string walletId, string typedData, string caip2 = "eip155:84532")
{
try
{
// Parse the typed data to ensure it's valid JSON
var typedDataJson = JsonSerializer.Deserialize<JsonElement>(typedData);
// Construct the request body according to the Privy documentation
var requestBody = new
{
method = "eth_signTypedData_v4",
caip2,
@params = new[] { walletId, typedData }
};
// Construct the full URL for the request
var url = $"https://api.privy.io/v1/wallets/{walletId}/rpc";
// Use the new method that both generates the signature and sends the request
var response = await GenerateAuthorizationSignatureAndSendRequestAsync(url, requestBody);
// Handle the response
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed to sign typed data: {errorContent}");
}
// Parse the response to get the signature
var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonSerializer.Deserialize<JsonElement>(responseContent);
// Extract the signature from the response
if (responseObject.TryGetProperty("data", out var dataElement))
{
return dataElement.GetString() ?? string.Empty;
}
throw new Exception($"Invalid signature response format: {responseContent}");
}
catch (JsonException ex)
{
throw new Exception($"Invalid typed data JSON format: {ex.Message}", ex);
}
catch (Exception ex)
{
throw new Exception($"Error signing typed data: {ex.Message}", ex);
}
}
/// <summary>
/// Gets information about a user, including their linked wallet accounts and delegation status
/// </summary>
/// <param name="userDid">The Privy DID of the user (format: did:privy:XXXXX)</param>
/// <returns>User information including wallets and delegation status</returns>
public async Task<PrivyUserInfo> GetUserWalletsAsync(string userDid)
{
if (string.IsNullOrEmpty(userDid))
{
throw new ArgumentException("User DID cannot be null or empty", nameof(userDid));
}
if (!userDid.StartsWith("did:privy:"))
{
throw new ArgumentException("User DID must start with 'did:privy:'", nameof(userDid));
}
try
{
// Construct the URL for getting user information
var url = $"/api/v1/users/{userDid}";
// Create the HTTP request
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Send the request
var response = await _privyClient.SendAsync(request);
// Check for success
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed to get user wallets: {response.StatusCode} - {errorContent}");
}
// Parse the response
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"User API Response: {responseContent}");
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
// Parse the response manually to handle potentially unexpected formats
using var document = JsonDocument.Parse(responseContent);
var root = document.RootElement;
// Create the user info object
var userInfo = new PrivyUserInfo();
// Extract the DID
if (root.TryGetProperty("did", out var didElement))
{
userInfo.Did = didElement.GetString();
}
// Extract timestamps if they exist
if (root.TryGetProperty("created_at", out var createdElement) &&
createdElement.TryGetInt64(out var createdTimestamp))
{
userInfo.CreatedAtTimestamp = createdTimestamp;
}
if (root.TryGetProperty("updated_at", out var updatedElement) &&
updatedElement.TryGetInt64(out var updatedTimestamp))
{
userInfo.UpdatedAtTimestamp = updatedTimestamp;
}
// Extract linked accounts
if (root.TryGetProperty("linked_accounts", out var accountsArray) &&
accountsArray.ValueKind == JsonValueKind.Array)
{
foreach (var accountElement in accountsArray.EnumerateArray())
{
try
{
var account =
JsonSerializer.Deserialize<PrivyLinkedAccount>(accountElement.GetRawText(), options);
if (account != null)
{
userInfo.LinkedAccounts.Add(account);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error deserializing account: {ex.Message}");
// Continue with the next account if one fails
}
}
}
return userInfo;
}
catch (Exception ex)
{
throw new Exception($"Error retrieving user wallets: {ex.Message}", ex);
}
}
}

View File

@@ -0,0 +1,223 @@
using System.Net.Http.Json;
using Managing.Infrastructure.Evm.Abstractions;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Web;
using Managing.Infrastructure.Evm.Models.Proxy;
namespace Managing.Infrastructure.Evm.Services
{
public class Web3ProxySettings
{
public string BaseUrl { get; set; } = "http://localhost:3000";
}
public class Web3ProxyService : IWeb3ProxyService
{
private readonly HttpClient _httpClient;
private readonly Web3ProxySettings _settings;
private readonly JsonSerializerOptions _jsonOptions;
public Web3ProxyService(IOptions<Web3ProxySettings> options)
{
_httpClient = new HttpClient();
_settings = options.Value;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload)
{
if (!endpoint.StartsWith("/"))
{
endpoint = $"/{endpoint}";
}
var url = $"{_settings.BaseUrl}privy{endpoint}";
try
{
var response = await _httpClient.PostAsJsonAsync(url, payload, _jsonOptions);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponse(response);
}
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
throw new Web3ProxyException($"Failed to call Privy service at {endpoint}: {ex.Message}");
}
}
public async Task<T> GetPrivyServiceAsync<T>(string endpoint, object payload = null)
{
if (!endpoint.StartsWith("/"))
{
endpoint = $"/{endpoint}";
}
var url = $"{_settings.BaseUrl}privy{endpoint}";
if (payload != null)
{
url += BuildQueryString(payload);
}
try
{
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponse(response);
}
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
throw new Web3ProxyException($"Failed to get Privy service at {endpoint}: {ex.Message}");
}
}
public async Task<T> CallGmxServiceAsync<T>(string endpoint, object payload)
{
if (!endpoint.StartsWith("/"))
{
endpoint = $"/{endpoint}";
}
var url = $"{_settings.BaseUrl}gmx{endpoint}";
try
{
var response = await _httpClient.PostAsJsonAsync(url, payload, _jsonOptions);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponse(response);
}
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
throw new Web3ProxyException($"Failed to call GMX service at {endpoint}: {ex.Message}");
}
}
public async Task<T> GetGmxServiceAsync<T>(string endpoint, object payload = null)
{
if (!endpoint.StartsWith("/"))
{
endpoint = $"/{endpoint}";
}
var url = $"{_settings.BaseUrl}gmx{endpoint}";
if (payload != null)
{
url += BuildQueryString(payload);
}
try
{
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponse(response);
}
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
throw new Web3ProxyException($"Failed to get GMX service at {endpoint}: {ex.Message}");
}
}
private async Task HandleErrorResponse(HttpResponseMessage response)
{
var statusCode = (int)response.StatusCode;
try
{
// Try to parse as the Web3Proxy error format (success: false, error: string)
var content = await response.Content.ReadAsStringAsync();
var errorResponse = await response.Content.ReadFromJsonAsync<Web3ProxyResponse>(_jsonOptions);
if (errorResponse != null && !errorResponse.Success && !string.IsNullOrEmpty(errorResponse.Error))
{
// Handle the standard Web3Proxy error format
throw new Web3ProxyException(errorResponse.Error);
}
// Fallback for other error formats
try
{
// Try to parse as structured error if it doesn't match the simple format
var structuredErrorResponse = await response.Content.ReadFromJsonAsync<Web3ProxyErrorResponse>(_jsonOptions);
if (structuredErrorResponse?.ErrorDetails != null)
{
structuredErrorResponse.ErrorDetails.StatusCode = statusCode;
throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
}
}
catch
{
// If we couldn't parse as structured error, use the simple error or fallback
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
}
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
// If we couldn't parse the error as JSON or another issue occurred
var content = await response.Content.ReadAsStringAsync();
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
}
}
private string BuildQueryString(object payload)
{
var properties = payload.GetType().GetProperties();
if (properties.Length == 0)
{
return string.Empty;
}
var queryString = new System.Text.StringBuilder("?");
bool isFirst = true;
foreach (var prop in properties)
{
var value = prop.GetValue(payload);
if (value != null)
{
if (!isFirst)
{
queryString.Append("&");
}
var paramName = prop.Name;
// Apply camelCase to match JSON property naming
paramName = char.ToLowerInvariant(paramName[0]) + paramName.Substring(1);
queryString.Append(HttpUtility.UrlEncode(paramName));
queryString.Append("=");
queryString.Append(HttpUtility.UrlEncode(value.ToString()));
isFirst = false;
}
}
return queryString.ToString();
}
}
}