Prepare production deploy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user