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:
@@ -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");
|
||||
}
|
||||
@@ -4,4 +4,5 @@ public interface IPrivySettings
|
||||
{
|
||||
string AppId { get; set; }
|
||||
string AppSecret { get; set; }
|
||||
string AuthorizationKey { get; set; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Managing.Infrastructure.Evm.Models.Privy;
|
||||
|
||||
public class PrivyApproveTokenResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Hash { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -6,4 +6,5 @@ public class PrivySettings : IPrivySettings
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public string AppSecret { get; set; }
|
||||
public string AuthorizationKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Managing.Infrastructure.Evm.Models.Privy;
|
||||
|
||||
public class PrivySigningResponse
|
||||
{
|
||||
public string Signature { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
107
src/Managing.Infrastructure.Web3/Models/Privy/PrivyUserInfo.cs
Normal file
107
src/Managing.Infrastructure.Web3/Models/Privy/PrivyUserInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
using Managing.Infrastructure.Evm.Models.Proxy;
|
||||
using Newtonsoft.Json;
|
||||
public class ClosePositionResponse : Web3ProxyBaseResponse
|
||||
{
|
||||
[JsonProperty("hash")]
|
||||
public string Hash { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Managing.Infrastructure.Evm.Models.Proxy;
|
||||
|
||||
public abstract class Web3ProxyBaseResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
129
src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs
Normal file
129
src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs
Normal file
223
src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user