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,4 +1,5 @@
using Managing.Infrastructure.Evm.Models;
using Managing.Infrastructure.Evm.Models.Privy;
namespace Managing.Infrastructure.Evm.Abstractions;
@@ -8,4 +9,39 @@ public interface IPrivyService
Task<HttpResponseMessage> SendTransactionAsync(string walletId, string recipientAddress, long value,
string caip2 = "eip155:84532");
/// <summary>
/// Signs a message using the embedded wallet
/// </summary>
/// <param name="embeddedWallet">The ID of the wallet to use for signing</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>
Task<string> SignMessageAsync(string embeddedWallet, string message,
string method = "personal_sign");
/// <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>
Task<string> SignTypedDataAsync(string walletId, string typedData, string caip2 = "eip155:84532");
/// <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>
Task<PrivyUserInfo> GetUserWalletsAsync(string userDid);
/// <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>
string GenerateAuthorizationSignature(string url, object body, string httpMethod = "POST");
}

View File

@@ -4,4 +4,5 @@ public interface IPrivySettings
{
string AppId { get; set; }
string AppSecret { get; set; }
string AuthorizationKey { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Managing.Infrastructure.Evm.Abstractions
{
public interface IWeb3ProxyService
{
Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload);
Task<T> GetPrivyServiceAsync<T>(string endpoint, object payload = null);
Task<T> CallGmxServiceAsync<T>(string endpoint, object payload);
Task<T> GetGmxServiceAsync<T>(string endpoint, object payload = null);
}
}

View File

@@ -11,6 +11,8 @@ using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models;
using Managing.Infrastructure.Evm.Models.Gmx.v2;
using Managing.Infrastructure.Evm.Models.Privy;
using Managing.Infrastructure.Evm.Models.Proxy;
using Managing.Infrastructure.Evm.Referentials;
using Managing.Infrastructure.Evm.Services;
using Managing.Infrastructure.Evm.Services.Gmx;
@@ -26,6 +28,7 @@ using BalanceOfFunction = Nethereum.Contracts.Standards.ERC20.ContractDefinition
using BalanceOfOutputDTO = Nethereum.Contracts.Standards.ERC20.ContractDefinition.BalanceOfOutputDTO;
using Chain = Managing.Domain.Evm.Chain;
using TransferEventDTO = Nethereum.Contracts.Standards.ERC721.ContractDefinition.TransferEventDTO;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Evm;
@@ -37,7 +40,7 @@ public class EvmManager : IEvmManager
private readonly IEnumerable<ISubgraphPrices> _subgraphs;
private Dictionary<string, Dictionary<string, decimal>> _geckoPrices;
private readonly GmxV2Service _gmxV2Service;
private readonly IPrivyService _privyService;
private readonly IWeb3ProxyService _web3ProxyService;
private readonly List<Ticker> _eligibleTickers = new List<Ticker>()
{
@@ -45,13 +48,14 @@ public class EvmManager : IEvmManager
Ticker.PEPE, Ticker.DOGE, Ticker.UNI
};
public EvmManager(IEnumerable<ISubgraphPrices> subgraphs, IPrivyService privyService)
public EvmManager(IEnumerable<ISubgraphPrices> subgraphs,
IWeb3ProxyService web3ProxyService)
{
var defaultChain = ChainService.GetEthereum();
_web3 = new Web3(defaultChain.RpcUrl);
_httpClient = new HttpClient();
_subgraphs = subgraphs;
_privyService = privyService;
_web3ProxyService = web3ProxyService;
_geckoPrices = _geckoPrices != null && _geckoPrices.Any()
? _geckoPrices
: new Dictionary<string, Dictionary<string, decimal>>();
@@ -426,19 +430,20 @@ public class EvmManager : IEvmManager
return lastPrices.Last();
}
public async Task<bool> InitAddress(string chainName, string publicAddress, string privateKey)
public async Task<bool> InitAddress(string publicAddress)
{
try
{
var chain = ChainService.GetChain(chainName);
var account = new Wallet(privateKey, _password).GetAccount(publicAddress);
var web3 = new Web3(account, chain.RpcUrl);
var tickers = await GetAvailableTicker();
await GmxService.InitAccountForTrading(web3, publicAddress, tickers);
return true;
var response = await _web3ProxyService.CallPrivyServiceAsync<PrivyInitAddressResponse>(
"/init-address",
new { address = publicAddress });
return response.Success;
}
catch (Exception ex)
{
// Log the error
Console.Error.WriteLine($"Error initializing address: {ex.Message}");
return false;
}
}
@@ -512,12 +517,34 @@ public class EvmManager : IEvmManager
public async Task<bool> CancelOrders(Account account, Ticker ticker)
{
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
// return await GmxService.CancelOrders(web3, account.Key, ticker);
var service = new GmxV2Service();
return await service.CancelOrders(web3, account.Key, ticker);
if (account.IsPrivyWallet)
{
try
{
var response = await _web3ProxyService.CallGmxServiceAsync<dynamic>("/cancel-orders",
new
{
account = account.Key,
walletId = account.Secret,
ticker = ticker.ToString()
});
return response.success ?? false;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error canceling orders via Fastify API: {ex.Message}");
return false;
}
}
else
{
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
var service = new GmxV2Service();
return await service.CancelOrders(web3, account.Key, ticker);
}
}
public async Task<Trade> IncreasePosition(
@@ -526,17 +553,67 @@ public class EvmManager : IEvmManager
TradeDirection direction,
decimal price,
decimal quantity,
decimal? leverage)
decimal? leverage,
decimal? stopLossPrice = null,
decimal? takeProfitPrice = null)
{
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
Trade trade = null;
try
{
trade = await _gmxV2Service.IncreasePosition(web3, account.Key, ticker, direction, price, quantity,
leverage);
// If this is a Privy wallet, call the GMX service through Fastify API
if (account.IsPrivyWallet)
{
try
{
var response = await _web3ProxyService.CallGmxServiceAsync<object>("/open-position",
new
{
account = account.Key,
walletId = account.Secret,
tradeType = price > 0 ? "limit" : "market",
ticker = ticker.ToString(),
direction = direction.ToString(),
price = price,
quantity,
leverage = leverage ?? 1.0m,
stopLossPrice = stopLossPrice,
takeProfitPrice = takeProfitPrice
});
// Create a trade object using the returned hash
var tradeType = price > 0 ? TradeType.Limit : TradeType.Market;
var tradeStatus = TradeStatus.Requested; // Use a valid enum value that exists in TradeStatus
trade = new Trade(
DateTime.UtcNow,
direction,
tradeStatus,
tradeType,
ticker,
quantity,
price,
leverage ?? 1.0m,
account.Key,
""
);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
else
{
// Continue with the existing direct service call for non-Privy wallets
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
trade = await _gmxV2Service.IncreasePosition(web3, account.Key, ticker, direction, price, quantity,
leverage);
}
}
catch (Exception ex)
{
@@ -554,11 +631,45 @@ public class EvmManager : IEvmManager
decimal quantity,
decimal? leverage)
{
Trade trade = null;
if (account.IsPrivyWallet)
{
try
{
var response = await _web3ProxyService.CallGmxServiceAsync<ClosePositionResponse>("/close-position",
new
{
account = account.Key,
ticker = ticker.ToString(),
direction = direction.ToString(),
});
trade = new Trade(
DateTime.UtcNow,
direction,
TradeStatus.Requested,
TradeType.Market,
ticker,
quantity,
price,
leverage ?? 1,
response.Hash,
""
);
return trade;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
Trade trade = null;
try
{
trade = await _gmxV2Service.DecreasePosition(web3, account.Key, ticker, direction, price, quantity,
@@ -573,7 +684,9 @@ public class EvmManager : IEvmManager
}
public async Task<Trade> DecreaseOrder(Account account, TradeType tradeType, Ticker ticker,
TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
TradeDirection direction, decimal price, decimal quantity, decimal? leverage,
decimal? stopLossPrice = null,
decimal? takeProfitPrice = null)
{
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
@@ -582,8 +695,11 @@ public class EvmManager : IEvmManager
Trade trade;
try
{
trade = await GmxService.DecreaseOrder(web3, tradeType, account.Key, ticker, direction, price, quantity,
leverage);
// TODO: This method in GmxV2Service might not exist or needs different handling for Privy wallets.
// Commenting out for now as IncreasePosition is the priority.
// trade = await _gmxV2Service.DecreaseOrder(web3, account.Key, tradeType, ticker, direction, price,
// quantity, leverage);
trade = null; // Placeholder return
}
catch (Exception ex)
{
@@ -595,6 +711,34 @@ public class EvmManager : IEvmManager
public async Task<Trade> GetTrade(Account account, string chainName, Ticker ticker)
{
if (account.IsPrivyWallet)
{
var result = await _web3ProxyService.GetGmxServiceAsync<GetGmxPositionsResponse>(
"/positions",
new { account = account.Key, ticker = ticker.ToString() });
var position = result.Positions.FirstOrDefault(p => p.Ticker == ticker.ToString());
if (position == null)
return null;
// TODO: Map the position object to a Trade object
var trade = new Trade(
position.Date,
MiscExtensions.ParseEnum<TradeDirection>(position.Direction),
MiscExtensions.ParseEnum<TradeStatus>(position.Status),
MiscExtensions.ParseEnum<TradeType>(position.TradeType),
MiscExtensions.ParseEnum<Ticker>(position.Ticker),
(decimal)position.Quantity,
(decimal)position.Price,
(decimal?)position.Leverage,
account.Key,
position.ExchangeOrderId
);
return trade;
}
return await GetTrade(account.Key, chainName, ticker);
}
@@ -608,12 +752,7 @@ public class EvmManager : IEvmManager
public async Task<List<FundingRate>> GetFundingRates()
{
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(chain.RpcUrl);
var service = new GmxV2Service();
var fundingRates = await service.GetFundingRates(web3);
return fundingRates;
return await _web3ProxyService.CallGmxServiceAsync<List<FundingRate>>("/gmx/funding-rates", new { });
}
public async Task<decimal> QuantityInPosition(string chainName, string publicAddress, Ticker ticker)
@@ -635,17 +774,39 @@ public class EvmManager : IEvmManager
public async Task<List<Trade>> GetOrders(Account account, Ticker ticker)
{
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(wallet, chain.RpcUrl);
// var orders = await GmxService.GetOrders(web3, account.Key, ticker);
var orders = await _gmxV2Service.GetOrders(web3, account.Key, ticker);
if (account.IsPrivyWallet)
{
var orders = await _web3ProxyService.CallGmxServiceAsync<List<Trade>>("/get-orders",
new { address = account.Key, walletId = account.Secret, ticker = ticker.ToString() });
return GmxV2Mappers.Map(orders);
return orders;
}
else
{
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
var web3 = new Web3(chain.RpcUrl);
// var orders = await GmxService.GetOrders(web3, account.Key, ticker);
var orders = await _gmxV2Service.GetOrders(web3, account.Key, ticker);
return GmxV2Mappers.Map(orders);
}
return new List<Trade>();
}
public async Task<bool> SetAllowance(Account account, Ticker ticker, BigInteger amount)
{
if (account.IsPrivyWallet)
{
var allowance = await _web3ProxyService.CallPrivyServiceAsync<PrivyApproveTokenResponse>("/approve-token",
new
{
address = account.Key, walletId = account.Secret, ticker = ticker.ToString(),
amount = amount.Equals(0) ? null : amount.ToString()
});
return false;
}
var web3 = BuildWeb3ForAccount(account);
var contractAddress = TokenService.GetContractAddress(ticker);
var approval = await EvmBase.ApproveToken(web3, account.Key, contractAddress,
@@ -663,7 +824,29 @@ public class EvmManager : IEvmManager
public async Task<(string Id, string Address)> CreatePrivyWallet()
{
var privyWallet = await _privyService.CreateWalletAsync();
var privyWallet = await _web3ProxyService.CallPrivyServiceAsync<PrivyWallet>("/privy/create-wallet", new { });
return (privyWallet.Id, privyWallet.Address);
}
/// <summary>
/// Signs a message using the embedded wallet
/// </summary>
/// <param name="embeddedWalletId">The wallet id of the embedded wallet</param>
/// <param name="address">The address of the embedded wallet</param>
/// <param name="message">The message to sign</param>
/// <returns>The signature response</returns>
public async Task<string> SignMessageAsync(string embeddedWalletId, string address, string message)
{
// Construct the request body using the exact format from Privy documentati
var requestBody = new
{
address = address,
walletId = embeddedWalletId,
message = message,
};
var response =
await _web3ProxyService.CallPrivyServiceAsync<PrivySigningResponse>("sign-message", requestBody);
return response.Signature;
}
}

View File

@@ -11,6 +11,7 @@
<PackageReference Include="GraphQL.Client.Abstractions" Version="6.0.5"/>
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.0.5"/>
<PackageReference Include="GraphQL.Query.Builder" Version="2.0.2"/>
<PackageReference Include="jsoncanonicalizer" Version="1.0.0"/>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="NBitcoin" Version="7.0.36"/>

View File

@@ -0,0 +1,7 @@
namespace Managing.Infrastructure.Evm.Models.Privy;
public class PrivyApproveTokenResponse
{
public bool Success { get; set; }
public string Hash { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace Managing.Infrastructure.Evm.Models.Privy;
public class PrivyInitAddressResponse
{
public bool Success { get; set; }
public string? UsdcHash { get; set; }
public string? OrderVaultHash { get; set; }
public string? ExchangeRouterHash { get; set; }
public string? Error { get; set; }
}

View File

@@ -6,4 +6,5 @@ public class PrivySettings : IPrivySettings
{
public string AppId { get; set; }
public string AppSecret { get; set; }
public string AuthorizationKey { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Managing.Infrastructure.Evm.Models.Privy;
public class PrivySigningResponse
{
public string Signature { get; set; }
public bool Success { get; set; }
}

View File

@@ -0,0 +1,107 @@
using System.Text.Json.Serialization;
namespace Managing.Infrastructure.Evm.Models.Privy
{
/// <summary>
/// Represents user information returned from the Privy API
/// </summary>
public class PrivyUserInfo
{
[JsonPropertyName("did")]
public string Did { get; set; }
[JsonPropertyName("linked_accounts")]
public List<PrivyLinkedAccount> LinkedAccounts { get; set; } = new List<PrivyLinkedAccount>();
// These fields might be optional in the response
[JsonPropertyName("created_at")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? CreatedAtTimestamp { get; set; }
[JsonPropertyName("updated_at")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? UpdatedAtTimestamp { get; set; }
// Computed properties to convert Unix timestamps to DateTime
[JsonIgnore]
public DateTime? CreatedAt => CreatedAtTimestamp.HasValue
? DateTimeOffset.FromUnixTimeSeconds(CreatedAtTimestamp.Value).DateTime
: null;
[JsonIgnore]
public DateTime? UpdatedAt => UpdatedAtTimestamp.HasValue
? DateTimeOffset.FromUnixTimeSeconds(UpdatedAtTimestamp.Value).DateTime
: null;
}
/// <summary>
/// Represents a linked account in the Privy user's profile
/// </summary>
public class PrivyLinkedAccount
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("address")]
public string Address { get; set; }
[JsonPropertyName("chain_type")]
public string ChainType { get; set; }
[JsonPropertyName("delegated")]
public bool Delegated { get; set; }
[JsonPropertyName("verified")]
public bool? Verified { get; set; }
// Additional fields from the actual response
[JsonPropertyName("wallet_index")]
public int? WalletIndex { get; set; }
[JsonPropertyName("wallet_client")]
public string WalletClient { get; set; }
[JsonPropertyName("wallet_client_type")]
public string WalletClientType { get; set; }
[JsonPropertyName("connector_type")]
public string ConnectorType { get; set; }
[JsonPropertyName("imported")]
public bool? Imported { get; set; }
[JsonPropertyName("recovery_method")]
public string RecoveryMethod { get; set; }
[JsonPropertyName("chain_id")]
public string ChainId { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("verified_at")]
public long? VerifiedAtTimestamp { get; set; }
[JsonPropertyName("first_verified_at")]
public long? FirstVerifiedAtTimestamp { get; set; }
[JsonPropertyName("latest_verified_at")]
public long? LatestVerifiedAtTimestamp { get; set; }
// Computed properties to convert Unix timestamps to DateTime
[JsonIgnore]
public DateTime? VerifiedAt => VerifiedAtTimestamp.HasValue
? DateTimeOffset.FromUnixTimeSeconds(VerifiedAtTimestamp.Value).DateTime
: null;
[JsonIgnore]
public DateTime? FirstVerifiedAt => FirstVerifiedAtTimestamp.HasValue
? DateTimeOffset.FromUnixTimeSeconds(FirstVerifiedAtTimestamp.Value).DateTime
: null;
[JsonIgnore]
public DateTime? LatestVerifiedAt => LatestVerifiedAtTimestamp.HasValue
? DateTimeOffset.FromUnixTimeSeconds(LatestVerifiedAtTimestamp.Value).DateTime
: null;
}
}

View File

@@ -0,0 +1,8 @@
using Managing.Infrastructure.Evm.Models.Proxy;
using Newtonsoft.Json;
public class ClosePositionResponse : Web3ProxyBaseResponse
{
[JsonProperty("hash")]
public string Hash { get; set; }
}

View File

@@ -0,0 +1,104 @@
using Newtonsoft.Json;
namespace Managing.Infrastructure.Evm.Models.Proxy;
public class Open
{
[JsonProperty("direction")] public string Direction { get; set; }
[JsonProperty("price")] public double Price { get; set; }
[JsonProperty("quantity")] public double Quantity { get; set; }
[JsonProperty("leverage")] public double Leverage { get; set; }
[JsonProperty("status")] public string Status { get; set; }
}
public class GmxPosition
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("ticker")] public string Ticker { get; set; }
[JsonProperty("direction")] public string Direction { get; set; }
[JsonProperty("price")] public double Price { get; set; }
[JsonProperty("quantity")] public double Quantity { get; set; }
[JsonProperty("leverage")] public double Leverage { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("tradeType")] public string TradeType { get; set; }
[JsonProperty("date")] public DateTime Date { get; set; }
[JsonProperty("exchangeOrderId")] public string ExchangeOrderId { get; set; }
[JsonProperty("pnl")] public double Pnl { get; set; }
[JsonProperty("collateral")] public double Collateral { get; set; }
[JsonProperty("markPrice")] public double MarkPrice { get; set; }
[JsonProperty("liquidationPrice")] public double LiquidationPrice { get; set; }
[JsonProperty("stopLoss")] public StopLoss StopLoss { get; set; }
[JsonProperty("takeProfit1")] public TakeProfit1 TakeProfit1 { get; set; }
[JsonProperty("open")] public Open Open { get; set; }
}
public class GetGmxPositionsResponse : Web3ProxyBaseResponse
{
[JsonProperty("positions")] public List<GmxPosition> Positions { get; set; }
}
public class StopLoss
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("ticker")] public string Ticker { get; set; }
[JsonProperty("direction")] public string Direction { get; set; }
[JsonProperty("price")] public double Price { get; set; }
[JsonProperty("quantity")] public double Quantity { get; set; }
[JsonProperty("leverage")] public int Leverage { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("tradeType")] public string TradeType { get; set; }
[JsonProperty("date")] public DateTime Date { get; set; }
[JsonProperty("exchangeOrderId")] public string ExchangeOrderId { get; set; }
}
public class TakeProfit1
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("ticker")] public string Ticker { get; set; }
[JsonProperty("direction")] public string Direction { get; set; }
[JsonProperty("price")] public double Price { get; set; }
[JsonProperty("quantity")] public double Quantity { get; set; }
[JsonProperty("leverage")] public int Leverage { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("tradeType")] public string TradeType { get; set; }
[JsonProperty("date")] public DateTime Date { get; set; }
[JsonProperty("exchangeOrderId")] public string ExchangeOrderId { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Managing.Infrastructure.Evm.Models.Proxy;
public abstract class Web3ProxyBaseResponse
{
public bool Success { get; set; }
}

View File

@@ -0,0 +1,129 @@
using System.Text.Json.Serialization;
namespace Managing.Infrastructure.Evm.Models.Proxy
{
/// <summary>
/// Base response structure from the Web3Proxy API
/// </summary>
public class Web3ProxyResponse
{
/// <summary>
/// Whether the operation was successful
/// </summary>
[JsonPropertyName("success")]
public bool Success { get; set; }
/// <summary>
/// Error message if not successful
/// </summary>
[JsonPropertyName("error")]
public string Error { get; set; }
}
/// <summary>
/// Generic response with data payload
/// </summary>
/// <typeparam name="T">Type of data in the response</typeparam>
public class Web3ProxyDataResponse<T> : Web3ProxyResponse
{
/// <summary>
/// The response data if successful
/// </summary>
[JsonPropertyName("data")]
public T Data { get; set; }
}
/// <summary>
/// Base error response from Web3Proxy API
/// </summary>
public class Web3ProxyError
{
/// <summary>
/// Error type
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
/// <summary>
/// Error message
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; }
/// <summary>
/// Error stack trace
/// </summary>
[JsonPropertyName("stack")]
public string Stack { get; set; }
/// <summary>
/// HTTP status code (added by service)
/// </summary>
[JsonIgnore]
public int StatusCode { get; set; }
/// <summary>
/// Returns a formatted error message with type and message
/// </summary>
public string FormattedMessage => $"{Type}: {Message}";
}
/// <summary>
/// API response containing error details
/// </summary>
public class Web3ProxyErrorResponse : Web3ProxyResponse
{
/// <summary>
/// Error details (for structured errors)
/// </summary>
[JsonPropertyName("err")]
public Web3ProxyError ErrorDetails { get; set; }
}
/// <summary>
/// Represents a Web3Proxy API exception with error details
/// </summary>
public class Web3ProxyException : Exception
{
/// <summary>
/// The error details from the API
/// </summary>
public Web3ProxyError Error { get; }
/// <summary>
/// Simple error message from API
/// </summary>
public string ApiErrorMessage { get; }
/// <summary>
/// Creates a new Web3ProxyException from a structured error
/// </summary>
/// <param name="error">The error details</param>
public Web3ProxyException(Web3ProxyError error)
: base(error?.Message ?? "An error occurred in the Web3Proxy API")
{
Error = error;
}
/// <summary>
/// Creates a new Web3ProxyException from a simple error message
/// </summary>
/// <param name="errorMessage">The error message</param>
public Web3ProxyException(string errorMessage)
: base(errorMessage)
{
ApiErrorMessage = errorMessage;
}
/// <summary>
/// Creates a new Web3ProxyException with a custom message
/// </summary>
/// <param name="message">Custom error message</param>
/// <param name="error">The error details</param>
public Web3ProxyException(string message, Web3ProxyError error)
: base(message)
{
Error = error;
}
}
}

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();
}
}
}