Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow overriding RPC #118

Merged
merged 3 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 84 additions & 14 deletions Thirdweb.Tests/Thirdweb.RPC/Thirdweb.RPC.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,90 @@ public class RpcTests : BaseTests
public RpcTests(ITestOutputHelper output)
: base(output) { }

[Fact]
public void RpcOverride_None()
{
var client = ThirdwebClient.Create(secretKey: this.SecretKey);
var thirdwebRpc = $"https://1.rpc.thirdweb.com/{client.ClientId}";
var rpc = ThirdwebRPC.GetRpcInstance(client, 1);
Assert.Equal(thirdwebRpc, rpc.RpcUrl.AbsoluteUri);
}

[Fact]
public void RpcOverride_Single()
{
var customRpc = "https://eth.llamarpc.com/";
var client = ThirdwebClient.Create(secretKey: this.SecretKey, rpcOverrides: new Dictionary<BigInteger, string> { { 1, customRpc } });
var rpc = ThirdwebRPC.GetRpcInstance(client, 1);
Assert.Equal(customRpc, client.RpcOverrides[1]);
Assert.Equal(customRpc, rpc.RpcUrl.AbsoluteUri);
}

[Fact]
public void RpcOverride_Multiple()
{
var customRpc1 = "https://eth.llamarpc.com/";
var customRpc42161 = "https://arbitrum.llamarpc.com/";
var client = ThirdwebClient.Create(secretKey: this.SecretKey, rpcOverrides: new Dictionary<BigInteger, string> { { 1, customRpc1 }, { 42161, customRpc42161 } });
var rpc1 = ThirdwebRPC.GetRpcInstance(client, 1);
var rpc42161 = ThirdwebRPC.GetRpcInstance(client, 42161);
Assert.Equal(customRpc1, client.RpcOverrides[1]);
Assert.Equal(customRpc1, rpc1.RpcUrl.AbsoluteUri);
Assert.Equal(customRpc42161, client.RpcOverrides[42161]);
Assert.Equal(customRpc42161, rpc42161.RpcUrl.AbsoluteUri);
}

[Fact]
public void RpcOverride_Single_Default()
{
var customRpc = "https://eth.llamarpc.com/";
var client = ThirdwebClient.Create(secretKey: this.SecretKey, rpcOverrides: new Dictionary<BigInteger, string> { { 1, customRpc } });

var thirdwebRpc = $"https://42161.rpc.thirdweb.com/{client.ClientId}";

var rpc1 = ThirdwebRPC.GetRpcInstance(client, 1);
Assert.Equal(customRpc, rpc1.RpcUrl.AbsoluteUri);

var rpc42161 = ThirdwebRPC.GetRpcInstance(client, 42161);
Assert.Equal(thirdwebRpc, rpc42161.RpcUrl.AbsoluteUri);
}

[Fact]
public void RpcOverride_Multiple_Default()
{
var customRpc1 = "https://eth.llamarpc.com/";
var customRpc42161 = "https://arbitrum.llamarpc.com/";
var client = ThirdwebClient.Create(secretKey: this.SecretKey, rpcOverrides: new Dictionary<BigInteger, string> { { 1, customRpc1 }, { 42161, customRpc42161 } });

var thirdwebRpc = $"https://421614.rpc.thirdweb.com/{client.ClientId}";

var rpc1 = ThirdwebRPC.GetRpcInstance(client, 1);
Assert.Equal(customRpc1, rpc1.RpcUrl.AbsoluteUri);

var rpc42161 = ThirdwebRPC.GetRpcInstance(client, 42161);
Assert.Equal(customRpc42161, rpc42161.RpcUrl.AbsoluteUri);

var rpc421614 = ThirdwebRPC.GetRpcInstance(client, 421614);
Assert.Equal(thirdwebRpc, rpc421614.RpcUrl.AbsoluteUri);
}

[Fact(Timeout = 120000)]
public async Task Request_WithRpcOverride()
{
var customRpc = "https://eth.llamarpc.com/";
var client = ThirdwebClient.Create(secretKey: this.SecretKey, rpcOverrides: new Dictionary<BigInteger, string> { { 1, customRpc } });

var rpc = ThirdwebRPC.GetRpcInstance(client, 1);
var blockNumber = await rpc.SendRequestAsync<string>("eth_blockNumber");
Assert.NotNull(blockNumber);
Assert.StartsWith("0x", blockNumber);

var rpc2 = ThirdwebRPC.GetRpcInstance(client, 42161);
var blockNumber2 = await rpc2.SendRequestAsync<string>("eth_blockNumber");
Assert.NotNull(blockNumber2);
Assert.StartsWith("0x", blockNumber2);
}

[Fact(Timeout = 120000)]
public async Task GetBlockNumber()
{
Expand Down Expand Up @@ -69,18 +153,4 @@ public async Task TestRpcError()
var exception = await Assert.ThrowsAsync<Exception>(async () => await rpc.SendRequestAsync<string>("eth_invalidMethod"));
Assert.Contains("RPC Error for request", exception.Message);
}

// [Fact(Timeout = 120000)]
// public async Task TestCache()
// {
// var client = ThirdwebClient.Create(secretKey: this.SecretKey);
// var rpc = ThirdwebRPC.GetRpcInstance(client, 421614);
// var blockNumber1 = await rpc.SendRequestAsync<string>("eth_blockNumber");
// await ThirdwebTask.Delay(1);
// var blockNumber2 = await rpc.SendRequestAsync<string>("eth_blockNumber");
// Assert.Equal(blockNumber1, blockNumber2);
// await ThirdwebTask.Delay(1000);
// var blockNumber3 = await rpc.SendRequestAsync<string>("eth_blockNumber");
// Assert.NotEqual(blockNumber1, blockNumber3);
// }
}
2 changes: 2 additions & 0 deletions Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ public async Task GetAllActiveSigners()
reqValidityEndTimestamp: Utils.GetUnixTimeStampIn10Years().ToString()
);

await ThirdwebTask.Delay(1000);

signers = await account.GetAllActiveSigners();

Assert.Equal(count + 1, signers.Count);
Expand Down
16 changes: 12 additions & 4 deletions Thirdweb/Thirdweb.Client/ThirdwebClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Thirdweb.Tests")]
using System.Numerics;

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Thirdweb.Tests")]

namespace Thirdweb;

Expand All @@ -20,6 +22,7 @@ public class ThirdwebClient
internal string SecretKey { get; }
internal string BundleId { get; }
internal ITimeoutOptions FetchTimeoutOptions { get; }
internal Dictionary<BigInteger, string> RpcOverrides { get; }

private ThirdwebClient(
string clientId = null,
Expand All @@ -30,7 +33,8 @@ private ThirdwebClient(
string sdkName = null,
string sdkOs = null,
string sdkPlatform = null,
string sdkVersion = null
string sdkVersion = null,
Dictionary<BigInteger, string> rpcOverrides = null
)
{
if (string.IsNullOrEmpty(clientId) && string.IsNullOrEmpty(secretKey))
Expand Down Expand Up @@ -71,6 +75,8 @@ private ThirdwebClient(

this.HttpClient = httpClient ?? new ThirdwebHttpClient();
this.HttpClient.SetHeaders(defaultHeaders);

this.RpcOverrides = rpcOverrides;
}

/// <summary>
Expand All @@ -85,6 +91,7 @@ private ThirdwebClient(
/// <param name="sdkOs">The SDK OS (optional).</param>
/// <param name="sdkPlatform">The SDK platform (optional).</param>
/// <param name="sdkVersion">The SDK version (optional).</param>
/// <param name="rpcOverrides">Mapping of chain id to your custom rpc for that chain id (optional, defaults to thirdweb RPC).</param>
/// <returns>A new instance of <see cref="ThirdwebClient"/>.</returns>
public static ThirdwebClient Create(
string clientId = null,
Expand All @@ -95,9 +102,10 @@ public static ThirdwebClient Create(
string sdkName = null,
string sdkOs = null,
string sdkPlatform = null,
string sdkVersion = null
string sdkVersion = null,
Dictionary<BigInteger, string> rpcOverrides = null
)
{
return new ThirdwebClient(clientId, secretKey, bundleId, fetchTimeoutOptions, httpClient, sdkName, sdkOs, sdkPlatform, sdkVersion);
return new ThirdwebClient(clientId, secretKey, bundleId, fetchTimeoutOptions, httpClient, sdkName, sdkOs, sdkPlatform, sdkVersion, rpcOverrides);
}
}
26 changes: 19 additions & 7 deletions Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
using System.Text;
using Newtonsoft.Json;

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Thirdweb.Tests")]

namespace Thirdweb;

/// <summary>
/// Represents the Thirdweb RPC client for sending requests and handling responses.
/// </summary>
public class ThirdwebRPC : IDisposable
{
internal Uri RpcUrl { get; }

private const int BatchSizeLimit = 100;
private readonly TimeSpan _batchInterval = TimeSpan.FromMilliseconds(50);

private readonly Uri _rpcUrl;
private readonly TimeSpan _rpcTimeout;
private readonly Dictionary<string, (object Response, DateTime Timestamp)> _cache = new();
private readonly TimeSpan _cacheDuration = TimeSpan.FromMilliseconds(25);
Expand Down Expand Up @@ -49,7 +52,15 @@ public static ThirdwebRPC GetRpcInstance(ThirdwebClient client, BigInteger chain
throw new ArgumentException("Invalid Chain ID");
}

var key = $"{client.ClientId}_{chainId}_{client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc)}";
string key;
if (client.RpcOverrides != null && client.RpcOverrides.TryGetValue(chainId, out var rpcOverride))
{
key = $"{client.ClientId}_{chainId}_{rpcOverride}_{client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc)}";
}
else
{
key = $"{client.ClientId}_{chainId}_{client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc)}";
}

if (!_rpcs.ContainsKey(key))
{
Expand Down Expand Up @@ -77,7 +88,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
{
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
var cacheKey = GetCacheKey(this.RpcUrl.ToString(), method, parameters);
if (this._cache.TryGetValue(cacheKey, out var cachedItem) && (DateTime.Now - cachedItem.Timestamp) < this._cacheDuration)
{
if (cachedItem.Response is TResponse cachedResponse)
Expand Down Expand Up @@ -121,7 +132,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
{
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
var cacheKey = GetCacheKey(this.RpcUrl.ToString(), method, parameters);
this._cache[cacheKey] = (response, DateTime.Now);
}
return response;
Expand All @@ -133,7 +144,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
var deserializedResponse = JsonConvert.DeserializeObject<TResponse>(JsonConvert.SerializeObject(result));
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
var cacheKey = GetCacheKey(this.RpcUrl.ToString(), method, parameters);
this._cache[cacheKey] = (deserializedResponse, DateTime.Now);
}
return deserializedResponse;
Expand All @@ -148,7 +159,8 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
private ThirdwebRPC(ThirdwebClient client, BigInteger chainId)
{
this._httpClient = client.HttpClient;
this._rpcUrl = new Uri($"https://{chainId}.rpc.thirdweb.com/{client.ClientId}");
var rpcOverride = client.RpcOverrides?.FirstOrDefault(r => r.Key == chainId);
this.RpcUrl = new Uri(rpcOverride?.Value ?? $"https://{chainId}.rpc.thirdweb.com/{client.ClientId}");
this._rpcTimeout = TimeSpan.FromMilliseconds(client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc));
_ = this.StartBackgroundFlushAsync();
}
Expand All @@ -161,7 +173,7 @@ private async Task SendBatchAsync(List<RpcRequest> batch)
try
{
using var cts = new CancellationTokenSource(this._rpcTimeout);
var response = await this._httpClient.PostAsync(this._rpcUrl.ToString(), content, cts.Token).ConfigureAwait(false);
var response = await this._httpClient.PostAsync(this.RpcUrl.ToString(), content, cts.Token).ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
Expand Down
Loading