|
|
|
|
@@ -1,549 +0,0 @@
|
|
|
|
|
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://auth.privy.io/");
|
|
|
|
|
var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_appId}:{_appSecret}"));
|
|
|
|
|
_privyClient.DefaultRequestHeaders.Add("privy-app-id", _appId);
|
|
|
|
|
_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 requestBody = new { chain_type = chainType };
|
|
|
|
|
var url = "https://api.privy.io/v1/wallets";
|
|
|
|
|
|
|
|
|
|
// Use the new method that both generates the signature and sends the request
|
|
|
|
|
var response = await GenerateAuthorizationSignatureAndSendRequestAsync(url, requestBody);
|
|
|
|
|
|
|
|
|
|
var result = new PrivyWallet();
|
|
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
|
|
|
{
|
|
|
|
|
result = await response.Content.ReadFromJsonAsync<PrivyWallet>();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new Exception(await response.Content.ReadAsStringAsync());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<HttpResponseMessage> SendTransactionAsync(string walletId, string recipientAddress, long value,
|
|
|
|
|
string caip2 = "eip155:84532")
|
|
|
|
|
{
|
|
|
|
|
var requestBody = new
|
|
|
|
|
{
|
|
|
|
|
method = "eth_sendTransaction",
|
|
|
|
|
caip2,
|
|
|
|
|
@params = new
|
|
|
|
|
{
|
|
|
|
|
transaction = new
|
|
|
|
|
{
|
|
|
|
|
to = recipientAddress,
|
|
|
|
|
value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|