From ffe1bed05194c2da6aada2482831dab9969d9050 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 27 Oct 2025 19:23:12 +0700 Subject: [PATCH] Prepare production deploy --- scripts/safe-migrate.sh | 64 +- src/Managing.Api/Program.cs | 2 - src/Managing.Api/appsettings.Production.json | 22 +- .../appsettings.ProductionLocal.json | 13 + src/Managing.Api/appsettings.Sandbox.json | 4 - .../appsettings.SandboxLocal.json | 20 +- src/Managing.Bootstrap/ApiBootstrap.cs | 6 - .../PrivyServiceTests.cs | 504 ---------------- .../Abstractions/IPrivyService.cs | 47 -- .../Abstractions/IPrivySettings.cs | 8 - .../Models/Privy/PrivySettings.cs | 10 - .../Services/PrivyService.cs | 549 ------------------ 12 files changed, 71 insertions(+), 1178 deletions(-) create mode 100644 src/Managing.Api/appsettings.ProductionLocal.json delete mode 100644 src/Managing.Infrastructure.Tests/PrivyServiceTests.cs delete mode 100644 src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs delete mode 100644 src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs delete mode 100644 src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs delete mode 100644 src/Managing.Infrastructure.Web3/Services/PrivyService.cs diff --git a/scripts/safe-migrate.sh b/scripts/safe-migrate.sh index fb25b23c..4b6380af 100755 --- a/scripts/safe-migrate.sh +++ b/scripts/safe-migrate.sh @@ -82,11 +82,11 @@ log "🚀 Starting safe migration for environment: $ENVIRONMENT" # Validate environment case $ENVIRONMENT in - "Development"|"SandboxLocal"|"Production"|"Oda") + "Development"|"SandboxLocal"|"ProductionLocal"|"Oda") log "✅ Environment '$ENVIRONMENT' is valid" ;; *) - error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, SandboxLocal, Production, or Oda" + error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, SandboxLocal, ProductionLocal, or Oda" ;; esac @@ -423,12 +423,40 @@ log "đŸ“Ļ Step 2: Checking if database backup is needed..." DB_EXISTS=false if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then DB_EXISTS=true - log "✅ Target database '$DB_NAME' exists - proceeding with backup" + log "✅ Target database '$DB_NAME' exists" else log "â„šī¸ Target database '$DB_NAME' does not exist - skipping backup" fi +# Ask user if they want to create a backup +CREATE_BACKUP=false if [ "$DB_EXISTS" = "true" ]; then + echo "" + echo "==========================================" + echo "đŸ“Ļ DATABASE BACKUP" + echo "==========================================" + echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" + echo "Environment: $ENVIRONMENT" + echo "" + echo "Would you like to create a backup before proceeding?" + echo "âš ī¸ It is highly recommended to create a backup for safety." + echo "==========================================" + echo "" + + read -p "🔧 Create database backup? (y/n, default: y): " create_backup + create_backup=${create_backup:-y} # Default to 'y' if user just presses Enter + + if [[ "$create_backup" =~ ^[Yy]$ ]]; then + log "✅ User chose to create backup - proceeding with backup" + CREATE_BACKUP=true + else + warn "âš ī¸ User chose to skip backup - proceeding without backup" + warn " This is not recommended. Proceed at your own risk!" + CREATE_BACKUP=false + fi +fi + +if [ "$DB_EXISTS" = "true" ] && [ "$CREATE_BACKUP" = "true" ]; then # Define the actual backup file path (absolute) BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" # Backup file display path (relative to script execution) @@ -660,7 +688,9 @@ if [ "$DB_HAS_TABLES" = "true" ]; then error "❌ Failed to generate complete migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." - error " Backup script available at: $BACKUP_FILE_DISPLAY" + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi fi else # Fallback: generate script without specifying from migration @@ -672,7 +702,9 @@ if [ "$DB_HAS_TABLES" = "true" ]; then error "❌ Failed to generate idempotent migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." - error " Backup script available at: $BACKUP_FILE_DISPLAY" + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi fi fi else @@ -691,7 +723,9 @@ else error "❌ Failed to generate complete migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." - error " Backup script available at: $BACKUP_FILE_DISPLAY" + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi fi else # Fallback: generate script without specifying from migration @@ -703,7 +737,9 @@ else error "❌ Failed to generate fallback migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." - error " Backup script available at: $BACKUP_FILE_DISPLAY" + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi fi fi fi @@ -776,7 +812,9 @@ fi error "❌ Database migration failed during final update." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." - error " Backup script available at: $BACKUP_FILE_DISPLAY" + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi fi fi @@ -817,7 +855,9 @@ log "✅ Kept last 5 backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$E # Success Summary log "🎉 Migration completed successfully for environment: $ENVIRONMENT!" -log "📁 EF Core Migration SQL Script: $BACKUP_FILE_DISPLAY" +if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + log "📁 EF Core Migration SQL Script: $BACKUP_FILE_DISPLAY" +fi log "📝 Full Log file: $LOG_FILE" echo "" @@ -827,6 +867,10 @@ echo "==========================================" echo "Environment: $ENVIRONMENT" echo "Timestamp: $TIMESTAMP" echo "Status: ✅ SUCCESS" -echo "EF Core SQL Backup: $BACKUP_FILE_DISPLAY" +if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + echo "EF Core SQL Backup: $BACKUP_FILE_DISPLAY" +else + echo "Database Backup: Skipped by user" +fi echo "Log: $LOG_FILE" echo "==========================================" \ No newline at end of file diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index 344d3694..8a5dc5af 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -13,7 +13,6 @@ using Managing.Core.Middleawares; using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.PostgreSql; using Managing.Infrastructure.Databases.PostgreSql.Configurations; -using Managing.Infrastructure.Evm.Models.Privy; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.EntityFrameworkCore; @@ -175,7 +174,6 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) => builder.Services.AddOptions(); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.PostgreSql)); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.InfluxDb)); -builder.Services.Configure(builder.Configuration.GetSection(Constants.ThirdParty.Privy)); builder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); diff --git a/src/Managing.Api/appsettings.Production.json b/src/Managing.Api/appsettings.Production.json index 990d2acb..d8c948a2 100644 --- a/src/Managing.Api/appsettings.Production.json +++ b/src/Managing.Api/appsettings.Production.json @@ -1,17 +1,13 @@ { "PostgreSql": { - "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37", - "Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37" + "ConnectionString": "Host=srv-captain--kaigen-db;Port=5432;Database=managing;Username=postgres;Password=2ab5423dcca4aa2d", + "Orleans": "Host=srv-captain--kaigen-db;Port=5432;Database=orleans;Username=postgres;Password=2ab5423dcca4aa2d" }, "InfluxDb": { "Url": "https://influx-db.apps.managing.live", "Organization": "managing-org", "Token": "_BtklT_aQ7GRqWG-HGILYEd8MJzxdbxxckPadzUsRofnwJBKQuXYLbCrVcLD7TrD4BlXgGAsyuqQItsOtanfBw==" }, - "Privy": { - "AppId": "cm6kkz5ke00n5ffmpwdbr05mp", - "AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a" - }, "Web3Proxy": { "BaseUrl": "http://srv-captain--web3-proxy:4111", "MaxRetryAttempts": 2, @@ -27,19 +23,7 @@ } } }, - "ElasticConfiguration": { - "Uri": "http://elasticsearch:9200" - }, - "RunOrleansGrains": true, - "DeploymentMode": false, - "Orleans": { - "EnableClustering": true, - "ConnectionTimeout": 60, - "MaxJoinAttempts": 3 - }, "AllowedHosts": "*", - "WorkerBotManager": true, - "WorkerBalancesTracking": true, "Kaigen": { "BaseUrl": "https://api.kaigen.managing.live", "DebitEndpoint": "/api/credits/debit", @@ -48,7 +32,7 @@ "SqlMonitoring": { "Enabled": true, "LoggingEnabled": false, - "SentryEnabled": true, + "SentryEnabled": false, "LoopDetectionEnabled": true, "LogErrorsOnly": true } diff --git a/src/Managing.Api/appsettings.ProductionLocal.json b/src/Managing.Api/appsettings.ProductionLocal.json new file mode 100644 index 00000000..c5e6cac4 --- /dev/null +++ b/src/Managing.Api/appsettings.ProductionLocal.json @@ -0,0 +1,13 @@ +īģŋ{ + "PostgreSql": { + "ConnectionString": "Host=kaigen-db.kaigen.managing.live;Port=5433;Database=managing;Username=postgres;Password=2ab5423dcca4aa2d", + "Orleans": "Host=kaigen-db.kaigen.managing.live;Port=5433;Database=orleans;Username=postgres;Password=2ab5423dcca4aa2d" + }, + "InfluxDb": { + "Url": "https://influx-db.apps.managing.live", + "Organization": "managing-org", + "Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ==" + }, + "RunOrleansGrains": true, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/src/Managing.Api/appsettings.Sandbox.json b/src/Managing.Api/appsettings.Sandbox.json index eb5d9f34..bb39301a 100644 --- a/src/Managing.Api/appsettings.Sandbox.json +++ b/src/Managing.Api/appsettings.Sandbox.json @@ -8,10 +8,6 @@ "Organization": "managing-org", "Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ==" }, - "Privy": { - "AppId": "cm6f47n1l003jx7mjwaembhup", - "AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF" - }, "Kaigen": { "BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app", "DebitEndpoint": "/api/credits/debit", diff --git a/src/Managing.Api/appsettings.SandboxLocal.json b/src/Managing.Api/appsettings.SandboxLocal.json index 8815ab6e..b22bb201 100644 --- a/src/Managing.Api/appsettings.SandboxLocal.json +++ b/src/Managing.Api/appsettings.SandboxLocal.json @@ -8,24 +8,6 @@ "Organization": "managing-org", "Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ==" }, - "Privy": { - "AppId": "cm6f47n1l003jx7mjwaembhup", - "AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF" - }, - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Information", - "System": "Warning" - } - } - }, - "ElasticConfiguration": { - "Uri": "http://elasticsearch:9200" - }, "RunOrleansGrains": true, - "AllowedHosts": "*", - "WorkerBotManager": false, - "WorkerBalancesTracking": false + "AllowedHosts": "*" } \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 449ad980..d370f2ec 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -32,8 +32,6 @@ using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.PostgreSql; using Managing.Infrastructure.Databases.PostgreSql.Configurations; using Managing.Infrastructure.Evm; -using Managing.Infrastructure.Evm.Abstractions; -using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Exchanges; @@ -395,7 +393,6 @@ public static class ApiBootstrap services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -418,9 +415,6 @@ public static class ApiBootstrap services.AddSingleton(sp => sp.GetRequiredService>().Value); - services.AddSingleton(sp => - sp.GetRequiredService>().Value); - services.Configure(configuration.GetSection("Kaigen")); // Evm diff --git a/src/Managing.Infrastructure.Tests/PrivyServiceTests.cs b/src/Managing.Infrastructure.Tests/PrivyServiceTests.cs deleted file mode 100644 index 77fab676..00000000 --- a/src/Managing.Infrastructure.Tests/PrivyServiceTests.cs +++ /dev/null @@ -1,504 +0,0 @@ -using System.Net; -using System.Reflection; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Managing.Infrastructure.Evm; -using Managing.Infrastructure.Evm.Abstractions; -using Managing.Infrastructure.Evm.Models.Privy; -using Moq; -using Moq.Protected; -using Org.Webpki.JsonCanonicalizer; -using Xunit; - -namespace Managing.Infrastructure.Tests; - -public class PrivyServiceTests -{ - private readonly PrivyService _privyService; - - public PrivyServiceTests() - { - _privyService = new PrivyService(new PrivySettings() - { - AppId = "cm7u09v0u002zrkuf2yjjr58p", - AppSecret = "25wwYu5AgxArU7djgvQEuioc9YSdGY3WN3r1dmXftPfH33KfGVfzopW3vqoPFjy1b8wS2gkDDZ9iQ8yxSo9Vi4iN", - AuthorizationKey = - "wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggpJ65PCo4E6NYpY867AyE6p1KxOrs8LJqHZw+t+076yhRANCAAS2EM23CtIfQRmHWTxcqb1j5yfrVePjZyBOZZ2RoPZHb9bDGLos206fTuVA3zgLVomlOoHTeYifkBASCn9Mfg3b" - }); - } - - [Fact] - public async Task Should_sign_message() - { - // Arrange - var walletId = "cm7vxs99f0007blcl8cmzv74t"; - var message = "Hello, Ethereum"; - var address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78"; - - // Act - var signature = await _privyService.SignMessageAsync(address, message); - - // Assert - Assert.NotNull(signature); - } - // GsyIYCt202K339eTNmB8ZG/bziRiGUcwFoXwfq85Wf+kpZgaSvJSJ/zO6TSEbDdqrb6JEWVyv4zaBV6j28w2SQ== - // MEUCIQCxx3BVsVu+GyFI/vIYm2x4hloHojKhpFrnj4KjfypgkgIgFnrlQ8CmJ479qgXpY+wdt1D5ki5+SFzXXBzMd+ckIAM= - - - [Fact] - public async Task Should_Get_User_Wallets() - { - var did = "did:privy:cm7vxs99f0007blcl8cmzv74t"; - var result = await _privyService.GetUserWalletsAsync(did); - - Assert.NotNull(result); - } - - - [Fact] - public async Task Should_Get_User_Wallets_With_Delegation_Status() - { - // Arrange - var mockHttpMessageHandler = new Mock(); - var testDid = "did:privy:test123456789"; - - // Sample JSON response matching the actual API response structure - var mockResponse = new - { - did = testDid, - linked_accounts = new object[] - { - new - { - type = "wallet", - address = "0x123abc456def789ghi", - chain_type = "ethereum", - chain_id = "eip155:1", - delegated = true, - verified = true, - wallet_index = 0, - wallet_client = "privy", - wallet_client_type = "privy", - connector_type = "embedded", - imported = false, - recovery_method = "privy", - verified_at = 1741180715, - first_verified_at = 1741180715, - latest_verified_at = 1741180715, - id = "abc123" - }, - new - { - type = "wallet", - address = "0x987zyx654wvu321tsr", - chain_type = "ethereum", - chain_id = "eip155:1", - delegated = false, - verified = true, - wallet_index = 1, - wallet_client = "privy", - wallet_client_type = "privy", - connector_type = "embedded", - imported = false, - recovery_method = "privy", - verified_at = 1741180715, - first_verified_at = 1741180715, - latest_verified_at = 1741180715, - id = "def456" - }, - new - { - type = "email", - address = "test@example.com", - verified = true - } - } - }; - - var mockResponseJson = JsonSerializer.Serialize(mockResponse); - - // Setup mock HttpClient to return our mock response - mockHttpMessageHandler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .ReturnsAsync(new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(mockResponseJson, Encoding.UTF8, "application/json") - }); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) - { - BaseAddress = new Uri("https://auth.privy.io/") - }; - - // Create mock settings - var mockSettings = new Mock(); - mockSettings.Setup(s => s.AppId).Returns("test-app-id"); - mockSettings.Setup(s => s.AppSecret).Returns("test-app-secret"); - mockSettings.Setup(s => s.AuthorizationKey).Returns("wallet-auth:test-auth-key"); - - // Create PrivyService with mocked dependencies - var privyService = new PrivyService(mockSettings.Object); - - // Use reflection to set the private _privyClient field - var privyClientField = typeof(PrivyService).GetField("_privyClient", - BindingFlags.Instance | BindingFlags.NonPublic); - privyClientField.SetValue(privyService, httpClient); - - // Act - var result = await privyService.GetUserWalletsAsync(testDid); - - // Assert - Assert.NotNull(result); - Assert.Equal(testDid, result.Did); - Assert.Equal(3, result.LinkedAccounts.Count); - - // Verify wallet accounts correctly processed - var delegatedWallets = result.LinkedAccounts.Where(a => a.Type == "wallet" && a.Delegated).ToList(); - Assert.Single(delegatedWallets); - Assert.Equal("0x123abc456def789ghi", delegatedWallets[0].Address); - Assert.Equal("privy", delegatedWallets[0].WalletClient); - Assert.Equal(0, delegatedWallets[0].WalletIndex); - Assert.Equal(1741180715, delegatedWallets[0].VerifiedAtTimestamp); - Assert.NotNull(delegatedWallets[0].VerifiedAt); // Verify timestamp conversion works - - // Verify non-delegated wallet - var nonDelegatedWallets = result.LinkedAccounts.Where(a => a.Type == "wallet" && !a.Delegated).ToList(); - Assert.Single(nonDelegatedWallets); - Assert.Equal("0x987zyx654wvu321tsr", nonDelegatedWallets[0].Address); - - // Verify non-wallet account - var nonWalletAccounts = result.LinkedAccounts.Where(a => a.Type != "wallet").ToList(); - Assert.Single(nonWalletAccounts); - Assert.Equal("email", nonWalletAccounts[0].Type); - - // Verify HTTP request made correctly - mockHttpMessageHandler.Protected().Verify( - "SendAsync", - Times.Once(), - ItExpr.Is(req => - req.Method == HttpMethod.Get && - req.RequestUri.ToString().Contains($"/api/v1/users/{testDid}")), - ItExpr.IsAny() - ); - } - - [Fact] - public void Should_Generate_Correct_Authorization_Signature() - { - // Arrange - Use the exact values from Privy documentation - var url = "https://api.privy.io/v1/wallets"; - var body = new { chain_type = "ethereum" }; - var httpMethod = "POST"; - - // Expected signature from Privy documentation - var expectedSignature = - "MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck="; - - // Create a PrivyService instance with the sample auth key from the docs - var settings = new PrivySettings - { - AppId = "cm4db8x9t000ccn87pctvcg9j", // Sample app ID from Privy docs - AppSecret = "test-app-secret", // Not used for signature generation - AuthorizationKey = - "wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqOBE+hZld+PCaj051uOl0XpEwe3tKBC5tsYsKdnPymGhRANCAAQ2HyYUbLRcfj9obpViwjYU/S7FdNUehkcfjYdd+R2gH/1q0ZJx7mOF1zpiEbbBNRLuXzP0NPN6nonkI8umzLXZ" - }; - - var privyService = new PrivyService(settings); - - // Act - var actualSignature = privyService.GenerateAuthorizationSignature(url, body, httpMethod); - - // Assert - Assert.Equal(expectedSignature, actualSignature); - Console.WriteLine($"Signature verification passed! Generated: {actualSignature}"); - } - - [Fact] - public void Should_Generate_Same_Signature_For_Identical_Requests() - { - // Arrange - var url = "https://auth.privy.io/v1/wallets/rpc"; - var body = new - { - address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78", - chain_type = "ethereum", - method = "personal_sign", - @params = new - { - message = "Hello, Ethereum", - encoding = "utf-8" - } - }; - - // Act - var signature1 = _privyService.GenerateAuthorizationSignature(url, body); - var signature2 = _privyService.GenerateAuthorizationSignature(url, body); - - // Assert - Assert.NotNull(signature1); - Assert.NotNull(signature2); - Assert.Equal(signature1, signature2); // Signatures should be deterministic for the same input - } - - [Fact] - public void Should_Generate_Different_Signatures_For_Different_Inputs() - { - // Arrange - var url = "https://auth.privy.io/v1/wallets/rpc"; - var body1 = new - { - address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78", - chain_type = "ethereum", - method = "personal_sign", - @params = new - { - message = "Hello, Ethereum", - encoding = "utf-8" - } - }; - - var body2 = new - { - address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78", - chain_type = "ethereum", - method = "personal_sign", - @params = new - { - message = "Different message", // Only the message is different - encoding = "utf-8" - } - }; - - var differentUrl = "https://auth.privy.io/v1/wallets/different"; - - // Act - var signature1 = _privyService.GenerateAuthorizationSignature(url, body1); - var signature2 = _privyService.GenerateAuthorizationSignature(url, body2); - var signature3 = _privyService.GenerateAuthorizationSignature(differentUrl, body1); - - // Assert - Assert.NotEqual(signature1, signature2); // Different message should produce different signature - Assert.NotEqual(signature1, signature3); // Different URL should produce different signature - Assert.NotEqual(signature2, signature3); // Different URL and message should produce different signature - } - - [Fact] - public void Should_Generate_Correct_Signature_Payload_Structure() - { - // This test validates that we're constructing the payload structure correctly - - // Arrange - Use the same sample data as the signature test - var url = "https://api.privy.io/v1/wallets"; - var body = new { chain_type = "ethereum" }; - var appId = "cm4db8x9t000ccn87pctvcg9j"; - var httpMethod = "POST"; - - // Expected payload structure from the Privy documentation - var expectedPayloadStructure = new - { - version = 1, - method = "POST", - url = "https://api.privy.io/v1/wallets", - body = new { chain_type = "ethereum" }, - headers = new Dictionary { { "privy-app-id", appId } } - }; - - // Capture the actual payload by overriding the signature generation - string capturedPayload = null; - - // We need to create a testable service that lets us inspect the payload - // Here's a simple mock that captures the JSON payload by inheriting from PrivyService - var testService = new TestPrivyService(new PrivySettings - { - AppId = appId, - AppSecret = "test-secret", - AuthorizationKey = "wallet-auth:test-key" - }); - - // Act - Generate the signature (this will capture the payload internally) - var payload = testService.GetSignaturePayload(url, body, httpMethod); - - // Assert - verify payload matches expected structure - - // Convert both to dictionaries for easier comparison - var expectedDict = ConvertAnonymousObjectToDictionary(expectedPayloadStructure); - var actualDict = ConvertAnonymousObjectToDictionary(payload); - - // Check top-level properties - Assert.Equal(expectedDict["version"], actualDict["version"]); - Assert.Equal(expectedDict["method"], actualDict["method"]); - Assert.Equal(expectedDict["url"], actualDict["url"]); - - // Compare body properties - var expectedBody = ConvertAnonymousObjectToDictionary(expectedDict["body"]); - var actualBody = ConvertAnonymousObjectToDictionary(actualDict["body"]); - Assert.Equal(expectedBody["chain_type"], actualBody["chain_type"]); - - // Compare headers - var expectedHeaders = (Dictionary)expectedDict["headers"]; - var actualHeaders = (Dictionary)actualDict["headers"]; - Assert.Equal(expectedHeaders["privy-app-id"], actualHeaders["privy-app-id"]); - - Console.WriteLine("Payload structure verification passed!"); - } - - [Fact] - public async Task Should_Use_n8n_To_sign() - { - var httpClient = new HttpClient(); - var url = "https://n8n.kaigen.managing.live/webhook-test/3d931e75-40c8-403d-a26e-6200b328ca85"; - httpClient.BaseAddress = new Uri(url); - - var privyUrl = "https://api.privy.io/v1/wallets"; - var body = new { chain_type = "ethereum" }; - var httpMethod = "POST"; - - // Expected signature from Privy documentation - var expectedSignature = - "MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck="; - - // Use Privy docs data for the request body - var signaturePayload = new Dictionary - { - ["method"] = "POST", - ["url"] = privyUrl, // Use the FULL URL for signature calculation as per Privy docs - ["body"] = body, - }; - - // Serialize the payload to JSON - var jsonPayload = JsonSerializer.Serialize(signaturePayload); - - // Create the request - var request = new HttpRequestMessage(HttpMethod.Post, url) - { - Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") - }; - - // Act - var response = await httpClient.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.True(response.IsSuccessStatusCode); - - var responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedSignature, responseContent); - } - - [Fact] - public void Should_Sign_Correctly() - { - // Arrange - var url = "https://api.privy.io/v1/wallets"; - var body = new { chain_type = "ethereum" }; - var httpMethod = "POST"; - - var expectedSignature = - "MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck="; - - var settings = new PrivySettings - { - AppId = "cm4db8x9t000ccn87pctvcg9j", - AuthorizationKey = - "wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqOBE+hZld+PCaj051uOl0XpEwe3tKBC5tsYsKdnPymGhRANCAAQ2HyYUbLRcfj9obpViwjYU/S7FdNUehkcfjYdd+R2gH/1q0ZJx7mOF1zpiEbbBNRLuXzP0NPN6nonkI8umzLXZ" - }; - - var payload = new - { - version = 1, - method = httpMethod, - url = url.TrimEnd('/'), - body = body, - headers = new - { - privy_app_id = settings.AppId - } - }; - - string serializedPayload = JsonSerializer.Serialize(payload); - JsonCanonicalizer jsonCanonicalizer = new JsonCanonicalizer(serializedPayload); - byte[] canonicalizedBytes = jsonCanonicalizer.GetEncodedUTF8(); - - Console.WriteLine($"Canonicalized JSON: {Encoding.UTF8.GetString(canonicalizedBytes)}"); - - byte[] privateKeyBytes = Convert.FromBase64String(settings.AuthorizationKey.Replace("wallet-auth:", "")); - using var privateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); - privateKey.ImportPkcs8PrivateKey(privateKeyBytes, out _); - - byte[] signatureBuffer = privateKey.SignData(canonicalizedBytes, HashAlgorithmName.SHA256); - string signature = Convert.ToBase64String(signatureBuffer); - - Console.WriteLine($"Generated Signature: {signature}"); - - Assert.Equal(expectedSignature, signature); - } - - // Helper class for testing the payload structure - private class TestPrivyService : PrivyService - { - public TestPrivyService(IPrivySettings settings) : base(settings) - { - } - - public object GetSignaturePayload(string url, object body, string httpMethod) - { - // 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(new Uri("https://auth.privy.io/"), relativePath).ToString(); - } - - // Create the signature payload structure exactly as in the GenerateAuthorizationSignature method - var headers = new Dictionary { { "privy-app-id", GetPrivyAppId() } }; - - return new - { - version = 1, - method = httpMethod, - url = fullUrl, - body = body, - headers = headers - }; - } - - // Helper to expose the private _appId - private string GetPrivyAppId() - { - // Use reflection to get the private field - var field = typeof(PrivyService).GetField("_appId", - BindingFlags.Instance | BindingFlags.NonPublic); - return (string)field.GetValue(this); - } - } - - // Helper for converting anonymous objects to dictionaries for easier comparison - private Dictionary ConvertAnonymousObjectToDictionary(object obj) - { - if (obj is Dictionary dict) - return dict; - - var result = new Dictionary(); - foreach (var prop in obj.GetType().GetProperties()) - { - result[prop.Name] = prop.GetValue(obj); - } - - return result; - } -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs b/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs deleted file mode 100644 index 6d2c27f0..00000000 --- a/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Managing.Infrastructure.Evm.Models; -using Managing.Infrastructure.Evm.Models.Privy; - -namespace Managing.Infrastructure.Evm.Abstractions; - -public interface IPrivyService -{ - Task CreateWalletAsync(string chainType = "ethereum"); - - Task SendTransactionAsync(string walletId, string recipientAddress, long value, - string caip2 = "eip155:84532"); - - /// - /// Signs a message using the embedded wallet - /// - /// The ID of the wallet to use for signing - /// The message to sign - /// The signing method to use (e.g., "personal_sign", "eth_sign") - /// The signature response - Task SignMessageAsync(string embeddedWallet, string message, - string method = "personal_sign"); - - /// - /// Signs typed data (EIP-712) using the embedded wallet - /// - /// The ID of the wallet to use for signing - /// The typed data to sign (must be a valid JSON string conforming to EIP-712) - /// The CAIP-2 chain identifier - /// The signature - Task SignTypedDataAsync(string walletId, string typedData, string caip2 = "eip155:84532"); - - /// - /// Gets information about a user, including their linked wallet accounts and delegation status - /// - /// The Privy DID of the user (format: did:privy:XXXXX) - /// User information including wallets and delegation status - Task GetUserWalletsAsync(string userDid); - - /// - /// Generates an authorization signature for a request to the Privy API - /// - /// The full URL for the request - /// The request body - /// The HTTP method to use for the request (defaults to POST) - /// The generated signature - string GenerateAuthorizationSignature(string url, object body, string httpMethod = "POST"); -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs b/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs deleted file mode 100644 index d30f629c..00000000 --- a/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Managing.Infrastructure.Evm.Abstractions; - -public interface IPrivySettings -{ - string AppId { get; set; } - string AppSecret { get; set; } - string AuthorizationKey { get; set; } -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs b/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs deleted file mode 100644 index 40ce0b0b..00000000 --- a/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Managing.Infrastructure.Evm.Abstractions; - -namespace Managing.Infrastructure.Evm.Models.Privy; - -public class PrivySettings : IPrivySettings -{ - public string AppId { get; set; } - public string AppSecret { get; set; } - public string AuthorizationKey { get; set; } -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/PrivyService.cs b/src/Managing.Infrastructure.Web3/Services/PrivyService.cs deleted file mode 100644 index 6482d36d..00000000 --- a/src/Managing.Infrastructure.Web3/Services/PrivyService.cs +++ /dev/null @@ -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")); - } - - /// - /// Generates an authorization signature for a request to the Privy API - /// - /// The full URL for the request - /// The request body - /// The HTTP method to use for the request (defaults to POST) - /// The generated signature - 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 - { - { "privy-app-id", _appId } - }; - - // Create the properly structured payload object according to Privy's specification - var signaturePayload = new Dictionary - { - ["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); - } - } - - /// - /// Generates an authorization signature for delegated actions and sends the HTTP request with the same payload - /// - /// The full URL for the request - /// The request body - /// The HTTP method to use for the request (defaults to POST) - /// The HTTP response from the request - private async Task 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); - } - } - - /// - /// Adds the authorization signature header to the request - /// - /// - /// This method is kept for backward compatibility. - /// Prefer using GenerateAuthorizationSignatureAndSendRequestAsync which both generates - /// the signature and sends the request with the same payload. - /// - /// The HTTP request message - /// The full URL for the request - /// The request body - 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 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(); - } - else - { - throw new Exception(await response.Content.ReadAsStringAsync()); - } - - return result; - } - catch (Exception ex) - { - throw new Exception(ex.Message); - } - } - - public async Task 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; } - } - - /// - /// Signs a message using the embedded wallet - /// - /// The address of the embedded wallet - /// The message to sign - /// The signing method to use (e.g., "personal_sign", "eth_sign") - /// The signature response - public async Task 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(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); - } - } - - /// - /// Signs typed data (EIP-712) using the embedded wallet - /// - /// The ID of the wallet to use for signing - /// The typed data to sign (must be a valid JSON string conforming to EIP-712) - /// The CAIP-2 chain identifier - /// The signature - public async Task SignTypedDataAsync(string walletId, string typedData, string caip2 = "eip155:84532") - { - try - { - // Parse the typed data to ensure it's valid JSON - var typedDataJson = JsonSerializer.Deserialize(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(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); - } - } - - /// - /// Gets information about a user, including their linked wallet accounts and delegation status - /// - /// The Privy DID of the user (format: did:privy:XXXXX) - /// User information including wallets and delegation status - public async Task 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(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); - } - } -} \ No newline at end of file