Skip to content

Commit 6f90f40

Browse files
committed
Refactor Deribit client with improvements and new features
Updated several client methods for rate limiting, added necessary options, and streamlined service registration. Introduced source generator for RPC methods, and created new classes for handling client state and rate-limited throttling.
1 parent b783139 commit 6f90f40

File tree

22 files changed

+1104
-457
lines changed

22 files changed

+1104
-457
lines changed

Prodigy.Solutions.Deribit.Client.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{929BB242-8EF
77
EndProject
88
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prodigy.Solutions.Deribit.Client", "src\Prodigy.Solutions.Deribit.Client\Prodigy.Solutions.Deribit.Client.csproj", "{52968099-215B-45F3-A9F1-FF55BC72C97A}"
99
EndProject
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prodigy.Solutions.Deribit.Client.Generators", "src\Prodigy.Solutions.Deribit.Client.Generators\Prodigy.Solutions.Deribit.Client.Generators.csproj", "{F5D98B2A-E604-4966-A77A-2C88343F89AA}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -17,11 +19,16 @@ Global
1719
{52968099-215B-45F3-A9F1-FF55BC72C97A}.Debug|Any CPU.Build.0 = Debug|Any CPU
1820
{52968099-215B-45F3-A9F1-FF55BC72C97A}.Release|Any CPU.ActiveCfg = Release|Any CPU
1921
{52968099-215B-45F3-A9F1-FF55BC72C97A}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{F5D98B2A-E604-4966-A77A-2C88343F89AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{F5D98B2A-E604-4966-A77A-2C88343F89AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{F5D98B2A-E604-4966-A77A-2C88343F89AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{F5D98B2A-E604-4966-A77A-2C88343F89AA}.Release|Any CPU.Build.0 = Release|Any CPU
2026
EndGlobalSection
2127
GlobalSection(SolutionProperties) = preSolution
2228
HideSolutionNode = FALSE
2329
EndGlobalSection
2430
GlobalSection(NestedProjects) = preSolution
2531
{52968099-215B-45F3-A9F1-FF55BC72C97A} = {929BB242-8EF2-4C56-AFFF-33AD2542F9B7}
32+
{F5D98B2A-E604-4966-A77A-2C88343F89AA} = {929BB242-8EF2-4C56-AFFF-33AD2542F9B7}
2633
EndGlobalSection
2734
EndGlobal
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<IsRoslynComponent>true</IsRoslynComponent>
7+
<LangVersion>latest</LangVersion>
8+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
9+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
10+
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<None Remove="bin\Debug\netstandard2.0\\Prodigy.Solutions.Deribit.Client.Generators.dll" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Text;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace Prodigy.Solutions.Deribit.Client.Generators
11+
{
12+
[Generator]
13+
public class RpcClientGenerator : ISourceGenerator
14+
{
15+
public void Initialize(GeneratorInitializationContext context)
16+
{
17+
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
18+
}
19+
20+
private class NameSymbolEqualityComparer : IEqualityComparer<INamedTypeSymbol>
21+
{
22+
public bool Equals(INamedTypeSymbol x, INamedTypeSymbol y)
23+
{
24+
if (ReferenceEquals(x, y)) return true;
25+
if (ReferenceEquals(x, null)) return false;
26+
if (ReferenceEquals(y, null)) return false;
27+
if (x.GetType() != y.GetType()) return false;
28+
return x.ToDisplayString() == y.ToDisplayString();
29+
}
30+
31+
public int GetHashCode(INamedTypeSymbol obj)
32+
{
33+
return obj.ToDisplayString().GetHashCode();
34+
}
35+
}
36+
37+
public void Execute(GeneratorExecutionContext context)
38+
{
39+
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
40+
{
41+
return;
42+
}
43+
44+
var methodsWithSymbol = receiver.CandidateMethods.Select(method =>
45+
{
46+
var model = context.Compilation.GetSemanticModel(method.SyntaxTree);
47+
if (model.GetDeclaredSymbol(method) is not IMethodSymbol symbol) return (null, null);
48+
return (Model: model, Symbol: symbol);
49+
})
50+
.Where(m => m is { Model: not null, Symbol: not null })
51+
.Select(m => (Model: m.Model!, Symbol: m.Symbol!))
52+
.GroupBy(m => m.Symbol.ContainingType, comparer: new NameSymbolEqualityComparer());
53+
54+
foreach (var methodGroup in methodsWithSymbol)
55+
{
56+
if (!methodGroup.Any())
57+
continue;
58+
59+
Console.WriteLine($"Found {methodGroup.Key.ToDisplayString()}");
60+
61+
var containingType = methodGroup.Key;
62+
var ns = containingType.ContainingNamespace;
63+
StringBuilder sb = new StringBuilder();
64+
sb.Append($@"
65+
#nullable enable
66+
67+
namespace {ns.ToDisplayString()}
68+
{{
69+
partial class {containingType.Name}
70+
{{
71+
");
72+
foreach (var method in methodGroup)
73+
{
74+
var symbol = method.Symbol!;
75+
76+
var attribute = symbol.GetAttributes()
77+
.FirstOrDefault(ad =>
78+
{
79+
var displayString = ad.AttributeClass?.ToDisplayString();
80+
return displayString is "Prodigy.Solutions.Deribit.Client.RpcCallAttribute" or "RpcCall";
81+
});
82+
83+
if (attribute == null) continue;
84+
85+
var returnTypeSymbol = symbol.ReturnType as INamedTypeSymbol;
86+
if (returnTypeSymbol == null) continue;
87+
var returnType = returnTypeSymbol.ToDisplayString();
88+
var returnTypeInvoke = returnTypeSymbol.TypeArguments[0].ToDisplayString();
89+
var endpoint = attribute.ConstructorArguments.FirstOrDefault(a => a.Type?.Name == "String").Value?.ToString();
90+
var tokens = attribute.ConstructorArguments.FirstOrDefault(a => a.Type?.Name == "Int32").Value?.ToString();
91+
var methodParameters = string.Join(", ", symbol.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}"));
92+
var isPrivate = (endpoint?.StartsWith("private/")).GetValueOrDefault();
93+
var privateCheckSource = isPrivate ? "Utilities.EnsureAuthenticated(_authenticationSession);" : "";
94+
95+
if (symbol.Parameters.Length == 0)
96+
{
97+
var methodSource = $@"
98+
public partial {returnType} {symbol.Name}({methodParameters})
99+
{{
100+
{privateCheckSource}
101+
return _deribitClient.InvokeAsync<{returnTypeInvoke}>(""{endpoint}"", {tokens});
102+
}}
103+
104+
";
105+
sb.Append(methodSource);
106+
}
107+
else
108+
{
109+
var parameters = string.Join(", ", symbol.Parameters.Select(p => $"(\"{ToLowerSnakeCase(p.Name)}\", {p.Name})"));;
110+
var methodSource = $@"
111+
public partial {returnType} {symbol.Name}({methodParameters})
112+
{{
113+
{privateCheckSource}
114+
var request = Prodigy.Solutions.Deribit.Client.ExpandoHelper.CreateExpando(new (string Key, object? Value)[] {{ {parameters} }});
115+
return _deribitClient.InvokeAsync<{returnTypeInvoke}>(""{endpoint}"", request, {tokens});
116+
}}
117+
118+
";
119+
sb.Append(methodSource);
120+
}
121+
}
122+
sb.Append($@"
123+
}}
124+
}}
125+
");
126+
127+
context.AddSource($"{containingType.Name}_RpcCall.g", SourceText.From(sb.ToString(), Encoding.UTF8));
128+
}
129+
}
130+
131+
private class SyntaxReceiver : ISyntaxReceiver
132+
{
133+
public List<MethodDeclarationSyntax> CandidateMethods { get; } = new();
134+
135+
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
136+
{
137+
if (!(syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax) ||
138+
methodDeclarationSyntax.AttributeLists.Count <= 0) return;
139+
140+
if (methodDeclarationSyntax.AttributeLists.Any(l =>
141+
l.Attributes.Any(a => a.Name is IdentifierNameSyntax { Identifier.Text: "RpcCall" })))
142+
{
143+
CandidateMethods.Add(methodDeclarationSyntax);
144+
}
145+
}
146+
}
147+
148+
public static string ToLowerSnakeCase(string str)
149+
{
150+
if (string.IsNullOrEmpty(str)) return str;
151+
152+
var sb = new StringBuilder();
153+
for (var i = 0; i < str.Length; i++)
154+
if (char.IsUpper(str[i]))
155+
{
156+
if (i != 0 && !char.IsUpper(str[i - 1])) sb.Append("_");
157+
sb.Append(char.ToLowerInvariant(str[i]));
158+
}
159+
else
160+
{
161+
sb.Append(str[i]);
162+
}
163+
164+
return sb.ToString();
165+
}
166+
}
167+
}

src/Prodigy.Solutions.Deribit.Client/Authentication/AuthRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ public class AuthRequest
1111
public string? State { get; init; }
1212

1313
public string? Scope { get; init; }
14-
}
14+
}

src/Prodigy.Solutions.Deribit.Client/Authentication/DeribitAuthenticationClient.cs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ public class DeribitAuthenticationClient
99
private readonly IOptions<DeribitClientOptions> _options;
1010
private readonly RequestSignatureGenerator _signatureGenerator;
1111

12-
private bool _isAuthenticated;
13-
private AuthRequest? _lastRequest;
14-
1512
public DeribitAuthenticationClient(DeribitJsonRpcClient deribitJsonRpcClient,
1613
RequestSignatureGenerator signatureGenerator, DeribitAuthenticationSession authSession,
1714
IOptions<DeribitClientOptions> options)
@@ -20,17 +17,9 @@ public DeribitAuthenticationClient(DeribitJsonRpcClient deribitJsonRpcClient,
2017
_signatureGenerator = signatureGenerator;
2118
_authSession = authSession;
2219
_options = options;
23-
authSession.Disconnected += AuthSessionOnDisconnected;
24-
}
25-
26-
private async void AuthSessionOnDisconnected(object? sender, EventArgs? e)
27-
{
28-
if (!_isAuthenticated || _lastRequest == null) return;
29-
30-
await AuthenticateAsync(_lastRequest);
3120
}
32-
33-
public async Task<AuthResponse?> AuthenticateAsync(AuthRequest? request = null)
21+
22+
internal async Task<AuthResponse?> AuthenticateAsync(AuthRequest? request = null)
3423
{
3524
request ??= new AuthRequest
3625
{
@@ -77,18 +66,16 @@ private async void AuthSessionOnDisconnected(object? sender, EventArgs? e)
7766
var response = await _deribitJsonRpcClient.InvokeAsync<AuthResponse>("public/auth", requestObject);
7867
if (response != null)
7968
{
80-
_isAuthenticated = true;
81-
_lastRequest = request;
8269
_authSession.SetAuthenticated(response);
8370
}
8471

8572
return response;
8673
}
8774

88-
public async Task LogoutAsync()
75+
internal async Task LogoutAsync()
8976
{
9077
Utilities.EnsureAuthenticated(_authSession);
9178
await _deribitJsonRpcClient.NotifyAsync("private/logout");
9279
_authSession.SetLoggedOut();
9380
}
94-
}
81+
}

src/Prodigy.Solutions.Deribit.Client/DeribitClientOptions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ namespace Prodigy.Solutions.Deribit.Client;
44

55
public class DeribitClientOptions
66
{
7-
[Required] public required Uri Uri { get; init; }
7+
[Required]
8+
public required Uri Uri { get; init; }
89

910
public string? ClientId { get; init; }
1011

1112
public string? ClientSecret { get; init; }
1213

1314
public TimeSpan WebsocketResponseTimeout { get; init; } = TimeSpan.FromSeconds(30);
14-
}
15+
16+
public int NonMatchingTokens = 500;
17+
18+
public int MatchingTokens = 2500;
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Prodigy.Solutions.Deribit.Client;
2+
3+
public enum DeribitClientState
4+
{
5+
Disconnected,
6+
Connecting,
7+
Connected,
8+
Authenticating,
9+
Authenticated,
10+
Failure
11+
}
12+
13+
public static class DeribitClientStateTrigger
14+
{
15+
public static string Connect = nameof(Connect);
16+
internal static string Connected = nameof(Connected);
17+
public static string Authenticate = nameof(Authenticate);
18+
internal static string Authenticated = nameof(Authenticated);
19+
internal static string AuthenticationFailed = nameof(AuthenticationFailed);
20+
public static string Disconnect = nameof(Disconnect);
21+
public static string Failure = nameof(Failure);
22+
}

0 commit comments

Comments
 (0)