Prepare production deploy
This commit is contained in:
@@ -82,11 +82,11 @@ log "🚀 Starting safe migration for environment: $ENVIRONMENT"
|
|||||||
|
|
||||||
# Validate environment
|
# Validate environment
|
||||||
case $ENVIRONMENT in
|
case $ENVIRONMENT in
|
||||||
"Development"|"SandboxLocal"|"Production"|"Oda")
|
"Development"|"SandboxLocal"|"ProductionLocal"|"Oda")
|
||||||
log "✅ Environment '$ENVIRONMENT' is valid"
|
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
|
esac
|
||||||
|
|
||||||
@@ -423,12 +423,40 @@ log "📦 Step 2: Checking if database backup is needed..."
|
|||||||
DB_EXISTS=false
|
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
|
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
|
DB_EXISTS=true
|
||||||
log "✅ Target database '$DB_NAME' exists - proceeding with backup"
|
log "✅ Target database '$DB_NAME' exists"
|
||||||
else
|
else
|
||||||
log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup"
|
log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Ask user if they want to create a backup
|
||||||
|
CREATE_BACKUP=false
|
||||||
if [ "$DB_EXISTS" = "true" ]; then
|
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)
|
# Define the actual backup file path (absolute)
|
||||||
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||||
# Backup file display path (relative to script execution)
|
# Backup file display path (relative to script execution)
|
||||||
@@ -660,8 +688,10 @@ if [ "$DB_HAS_TABLES" = "true" ]; then
|
|||||||
error "❌ Failed to generate complete migration script."
|
error "❌ Failed to generate complete migration script."
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
error " Check the .NET project logs for detailed errors."
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Fallback: generate script without specifying from migration
|
# Fallback: generate script without specifying from migration
|
||||||
log "📝 Fallback: Generating migration script without specifying from migration..."
|
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||||
@@ -672,9 +702,11 @@ if [ "$DB_HAS_TABLES" = "true" ]; then
|
|||||||
error "❌ Failed to generate idempotent migration script."
|
error "❌ Failed to generate idempotent migration script."
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
error " Check the .NET project logs for detailed errors."
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Use full script generation for empty databases (generate script from the very beginning)
|
# Use full script generation for empty databases (generate script from the very beginning)
|
||||||
log "📝 Generating full migration script for empty database..."
|
log "📝 Generating full migration script for empty database..."
|
||||||
@@ -691,8 +723,10 @@ else
|
|||||||
error "❌ Failed to generate complete migration script."
|
error "❌ Failed to generate complete migration script."
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
error " Check the .NET project logs for detailed errors."
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Fallback: generate script without specifying from migration
|
# Fallback: generate script without specifying from migration
|
||||||
log "📝 Fallback: Generating migration script without specifying from migration..."
|
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||||
@@ -703,9 +737,11 @@ else
|
|||||||
error "❌ Failed to generate fallback migration script."
|
error "❌ Failed to generate fallback migration script."
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
error " Check the .NET project logs for detailed errors."
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show the migration script path to the user for review
|
# Show the migration script path to the user for review
|
||||||
@@ -776,9 +812,11 @@ fi
|
|||||||
error "❌ Database migration failed during final update."
|
error "❌ Database migration failed during final update."
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
error " Check the .NET project logs for detailed errors."
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Save a copy of the migration script for reference before cleaning up
|
# Save a copy of the migration script for reference before cleaning up
|
||||||
MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
|
MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
|
||||||
@@ -817,7 +855,9 @@ log "✅ Kept last 5 backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$E
|
|||||||
|
|
||||||
# Success Summary
|
# Success Summary
|
||||||
log "🎉 Migration completed successfully for environment: $ENVIRONMENT!"
|
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"
|
log "📝 Full Log file: $LOG_FILE"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -827,6 +867,10 @@ echo "=========================================="
|
|||||||
echo "Environment: $ENVIRONMENT"
|
echo "Environment: $ENVIRONMENT"
|
||||||
echo "Timestamp: $TIMESTAMP"
|
echo "Timestamp: $TIMESTAMP"
|
||||||
echo "Status: ✅ SUCCESS"
|
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 "Log: $LOG_FILE"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
@@ -13,7 +13,6 @@ using Managing.Core.Middleawares;
|
|||||||
using Managing.Infrastructure.Databases.InfluxDb.Models;
|
using Managing.Infrastructure.Databases.InfluxDb.Models;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql;
|
using Managing.Infrastructure.Databases.PostgreSql;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
||||||
using Managing.Infrastructure.Evm.Models.Privy;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -175,7 +174,6 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
|
|||||||
builder.Services.AddOptions();
|
builder.Services.AddOptions();
|
||||||
builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql));
|
builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql));
|
||||||
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
|
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
|
||||||
builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Constants.ThirdParty.Privy));
|
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
"PostgreSql": {
|
"PostgreSql": {
|
||||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
|
"ConnectionString": "Host=srv-captain--kaigen-db;Port=5432;Database=managing;Username=postgres;Password=2ab5423dcca4aa2d",
|
||||||
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
|
"Orleans": "Host=srv-captain--kaigen-db;Port=5432;Database=orleans;Username=postgres;Password=2ab5423dcca4aa2d"
|
||||||
},
|
},
|
||||||
"InfluxDb": {
|
"InfluxDb": {
|
||||||
"Url": "https://influx-db.apps.managing.live",
|
"Url": "https://influx-db.apps.managing.live",
|
||||||
"Organization": "managing-org",
|
"Organization": "managing-org",
|
||||||
"Token": "_BtklT_aQ7GRqWG-HGILYEd8MJzxdbxxckPadzUsRofnwJBKQuXYLbCrVcLD7TrD4BlXgGAsyuqQItsOtanfBw=="
|
"Token": "_BtklT_aQ7GRqWG-HGILYEd8MJzxdbxxckPadzUsRofnwJBKQuXYLbCrVcLD7TrD4BlXgGAsyuqQItsOtanfBw=="
|
||||||
},
|
},
|
||||||
"Privy": {
|
|
||||||
"AppId": "cm6kkz5ke00n5ffmpwdbr05mp",
|
|
||||||
"AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a"
|
|
||||||
},
|
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
||||||
"MaxRetryAttempts": 2,
|
"MaxRetryAttempts": 2,
|
||||||
@@ -27,19 +23,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ElasticConfiguration": {
|
|
||||||
"Uri": "http://elasticsearch:9200"
|
|
||||||
},
|
|
||||||
"RunOrleansGrains": true,
|
|
||||||
"DeploymentMode": false,
|
|
||||||
"Orleans": {
|
|
||||||
"EnableClustering": true,
|
|
||||||
"ConnectionTimeout": 60,
|
|
||||||
"MaxJoinAttempts": 3
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"WorkerBotManager": true,
|
|
||||||
"WorkerBalancesTracking": true,
|
|
||||||
"Kaigen": {
|
"Kaigen": {
|
||||||
"BaseUrl": "https://api.kaigen.managing.live",
|
"BaseUrl": "https://api.kaigen.managing.live",
|
||||||
"DebitEndpoint": "/api/credits/debit",
|
"DebitEndpoint": "/api/credits/debit",
|
||||||
@@ -48,7 +32,7 @@
|
|||||||
"SqlMonitoring": {
|
"SqlMonitoring": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"LoggingEnabled": false,
|
"LoggingEnabled": false,
|
||||||
"SentryEnabled": true,
|
"SentryEnabled": false,
|
||||||
"LoopDetectionEnabled": true,
|
"LoopDetectionEnabled": true,
|
||||||
"LogErrorsOnly": true
|
"LogErrorsOnly": true
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/Managing.Api/appsettings.ProductionLocal.json
Normal file
13
src/Managing.Api/appsettings.ProductionLocal.json
Normal file
@@ -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": "*"
|
||||||
|
}
|
||||||
@@ -8,10 +8,6 @@
|
|||||||
"Organization": "managing-org",
|
"Organization": "managing-org",
|
||||||
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
|
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
|
||||||
},
|
},
|
||||||
"Privy": {
|
|
||||||
"AppId": "cm6f47n1l003jx7mjwaembhup",
|
|
||||||
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
|
|
||||||
},
|
|
||||||
"Kaigen": {
|
"Kaigen": {
|
||||||
"BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app",
|
"BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app",
|
||||||
"DebitEndpoint": "/api/credits/debit",
|
"DebitEndpoint": "/api/credits/debit",
|
||||||
|
|||||||
@@ -8,24 +8,6 @@
|
|||||||
"Organization": "managing-org",
|
"Organization": "managing-org",
|
||||||
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
|
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
|
||||||
},
|
},
|
||||||
"Privy": {
|
|
||||||
"AppId": "cm6f47n1l003jx7mjwaembhup",
|
|
||||||
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
|
|
||||||
},
|
|
||||||
"Serilog": {
|
|
||||||
"MinimumLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Override": {
|
|
||||||
"Microsoft": "Information",
|
|
||||||
"System": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ElasticConfiguration": {
|
|
||||||
"Uri": "http://elasticsearch:9200"
|
|
||||||
},
|
|
||||||
"RunOrleansGrains": true,
|
"RunOrleansGrains": true,
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*"
|
||||||
"WorkerBotManager": false,
|
|
||||||
"WorkerBalancesTracking": false
|
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,6 @@ using Managing.Infrastructure.Databases.InfluxDb.Models;
|
|||||||
using Managing.Infrastructure.Databases.PostgreSql;
|
using Managing.Infrastructure.Databases.PostgreSql;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
||||||
using Managing.Infrastructure.Evm;
|
using Managing.Infrastructure.Evm;
|
||||||
using Managing.Infrastructure.Evm.Abstractions;
|
|
||||||
using Managing.Infrastructure.Evm.Models.Privy;
|
|
||||||
using Managing.Infrastructure.Evm.Services;
|
using Managing.Infrastructure.Evm.Services;
|
||||||
using Managing.Infrastructure.Evm.Subgraphs;
|
using Managing.Infrastructure.Evm.Subgraphs;
|
||||||
using Managing.Infrastructure.Exchanges;
|
using Managing.Infrastructure.Exchanges;
|
||||||
@@ -395,7 +393,6 @@ public static class ApiBootstrap
|
|||||||
services.AddTransient<IExchangeService, ExchangeService>();
|
services.AddTransient<IExchangeService, ExchangeService>();
|
||||||
|
|
||||||
|
|
||||||
services.AddTransient<IPrivyService, PrivyService>();
|
|
||||||
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
||||||
services.AddTransient<IWebhookService, WebhookService>();
|
services.AddTransient<IWebhookService, WebhookService>();
|
||||||
services.AddTransient<IKaigenService, KaigenService>();
|
services.AddTransient<IKaigenService, KaigenService>();
|
||||||
@@ -418,9 +415,6 @@ public static class ApiBootstrap
|
|||||||
services.AddSingleton<IInfluxDbSettings>(sp =>
|
services.AddSingleton<IInfluxDbSettings>(sp =>
|
||||||
sp.GetRequiredService<IOptions<InfluxDbSettings>>().Value);
|
sp.GetRequiredService<IOptions<InfluxDbSettings>>().Value);
|
||||||
|
|
||||||
services.AddSingleton<IPrivySettings>(sp =>
|
|
||||||
sp.GetRequiredService<IOptions<PrivySettings>>().Value);
|
|
||||||
|
|
||||||
services.Configure<KaigenSettings>(configuration.GetSection("Kaigen"));
|
services.Configure<KaigenSettings>(configuration.GetSection("Kaigen"));
|
||||||
|
|
||||||
// Evm
|
// Evm
|
||||||
|
|||||||
@@ -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<HttpMessageHandler>();
|
|
||||||
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<Task<HttpResponseMessage>>(
|
|
||||||
"SendAsync",
|
|
||||||
ItExpr.IsAny<HttpRequestMessage>(),
|
|
||||||
ItExpr.IsAny<CancellationToken>()
|
|
||||||
)
|
|
||||||
.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<IPrivySettings>();
|
|
||||||
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<HttpRequestMessage>(req =>
|
|
||||||
req.Method == HttpMethod.Get &&
|
|
||||||
req.RequestUri.ToString().Contains($"/api/v1/users/{testDid}")),
|
|
||||||
ItExpr.IsAny<CancellationToken>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[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<string, string> { { "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<string, string>)expectedDict["headers"];
|
|
||||||
var actualHeaders = (Dictionary<string, string>)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<string, object>
|
|
||||||
{
|
|
||||||
["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<string, string> { { "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<string, object> ConvertAnonymousObjectToDictionary(object obj)
|
|
||||||
{
|
|
||||||
if (obj is Dictionary<string, object> dict)
|
|
||||||
return dict;
|
|
||||||
|
|
||||||
var result = new Dictionary<string, object>();
|
|
||||||
foreach (var prop in obj.GetType().GetProperties())
|
|
||||||
{
|
|
||||||
result[prop.Name] = prop.GetValue(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using Managing.Infrastructure.Evm.Models;
|
|
||||||
using Managing.Infrastructure.Evm.Models.Privy;
|
|
||||||
|
|
||||||
namespace Managing.Infrastructure.Evm.Abstractions;
|
|
||||||
|
|
||||||
public interface IPrivyService
|
|
||||||
{
|
|
||||||
Task<PrivyWallet> CreateWalletAsync(string chainType = "ethereum");
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Managing.Infrastructure.Evm.Abstractions;
|
|
||||||
|
|
||||||
public interface IPrivySettings
|
|
||||||
{
|
|
||||||
string AppId { get; set; }
|
|
||||||
string AppSecret { get; set; }
|
|
||||||
string AuthorizationKey { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user