Nethereum.WalletConnect 5.8.0

Prefix Reserved
dotnet add package Nethereum.WalletConnect --version 5.8.0
                    
NuGet\Install-Package Nethereum.WalletConnect -Version 5.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nethereum.WalletConnect" Version="5.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nethereum.WalletConnect" Version="5.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Nethereum.WalletConnect" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nethereum.WalletConnect --version 5.8.0
                    
#r "nuget: Nethereum.WalletConnect, 5.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nethereum.WalletConnect@5.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nethereum.WalletConnect&version=5.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Nethereum.WalletConnect&version=5.8.0
                    
Install as a Cake Tool

Nethereum.WalletConnect

WalletConnect v2 protocol integration for connecting desktop and web applications to mobile wallet apps using QR code pairing.

Overview

Nethereum.WalletConnect provides integration with the WalletConnect v2 protocol, enabling dApps to connect to mobile wallet applications by scanning a QR code. Once connected, the wallet handles all transaction signing and message signing requests, while blockchain queries can optionally go through a custom RPC endpoint.

Key Features:

  • WalletConnect v2.3.0 protocol support (WalletConnectSharp.Sign)
  • QR code generation for mobile wallet pairing
  • NethereumWalletConnectService for connection management and wallet operations
  • NethereumWalletConnectHostProvider implementing IEthereumHostProvider
  • NethereumWalletConnectInterceptor for automatic request routing
  • Support for personal_sign, eth_signTypedData_v4, eth_sendTransaction
  • Chain switching (wallet_switchEthereumChain) and adding (wallet_addEthereumChain)
  • CAIP-25 session management with eip155 namespace
  • Event subscriptions for accountsChanged and chainChanged
  • Optional custom RPC endpoint for queries (hybrid mode)

Installation

dotnet add package Nethereum.WalletConnect

Or via Package Manager Console:

Install-Package Nethereum.WalletConnect

Dependencies

Package References:

  • Newtonsoft.Json 13.0.3
  • WalletConnect.Core 2.3.0
  • WalletConnect.Sign 2.3.0

Project References:

  • Nethereum.UI
  • Nethereum.Web3

Key Concepts

WalletConnect Protocol Flow

  1. Initialize Client: Create WalletConnectSignClient with ProjectId from WalletConnect Cloud
  2. Create Services: Instantiate NethereumWalletConnectService and NethereumWalletConnectHostProvider
  3. Generate QR Code: Call InitialiseConnectionAndGetQRUriAsync to get connection URI
  4. User Scans QR: User scans QR code with mobile wallet app
  5. Wait for Approval: Call WaitForConnectionApprovalAndGetSelectedAccountAsync
  6. Connected: Session established, can now send signing requests to wallet

CAIP-25 Namespaces

WalletConnect v2 uses CAIP-25 for multi-chain session management:

  • Required Namespace: Must include eip155:1 (Ethereum Mainnet) with eth_sendTransaction
  • Optional Namespace: Additional chains and methods (personal_sign, eth_signTypedData_v4, wallet operations)
  • Chain IDs: Format is eip155:{chainId} (e.g., eip155:1 for Mainnet, eip155:137 for Polygon)

Request Interception

NethereumWalletConnectInterceptor automatically routes specific methods through the connected wallet:

  • eth_sendTransaction
  • eth_sign
  • personal_sign
  • eth_signTypedData_v4
  • wallet_switchEthereumChain
  • wallet_addEthereumChain

All other methods (e.g., eth_call, eth_getBalance) go through the configured RPC endpoint.

Quick Start

1. Get WalletConnect Project ID

Sign up at https://cloud.walletconnect.com to get a free Project ID.

2. Initialize WalletConnect Client

using Nethereum.WalletConnect;
using WalletConnectSharp.Sign;
using WalletConnectSharp.Sign.Models;
using WalletConnectSharp.Core;
using WalletConnectSharp.Storage;

var options = new SignClientOptions()
{
    ProjectId = "YOUR_PROJECT_ID_HERE",
    Metadata = new Metadata()
    {
        Description = "My dApp description",
        Icons = new[] { "https://myapp.com/icon.png" },
        Name = "My dApp",
        Url = "https://myapp.com"
    },
    Storage = new InMemoryStorage()
};

var client = await WalletConnectSignClient.Init(options);

3. Create Services and Generate QR Code

using Nethereum.WalletConnect;
using QRCoder;

var walletConnectService = new NethereumWalletConnectService(client);
var walletConnectHostProvider = new NethereumWalletConnectHostProvider(walletConnectService);

// Subscribe to events
walletConnectHostProvider.SelectedAccountChanged += async (address) =>
{
    Console.WriteLine($"Account changed: {address}");
};

walletConnectHostProvider.NetworkChanged += async (chainId) =>
{
    Console.WriteLine($"Network changed: {chainId}");
};

// Get connection URI and generate QR code
var connectionOptions = NethereumWalletConnectService.GetDefaultConnectOptions();
var connectionUri = await walletConnectService.InitialiseConnectionAndGetQRUriAsync(connectionOptions);

// Generate QR code for display
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(connectionUri, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode qRCode = new PngByteQRCode(qrCodeData);
byte[] qrCodeBytes = qRCode.GetGraphic(20);

// Display qrCodeBytes to user (convert to image)
Console.WriteLine($"Scan this QR code with your wallet: {connectionUri}");

// Wait for user to approve connection in wallet
var selectedAddress = await walletConnectService.WaitForConnectionApprovalAndGetSelectedAccountAsync();
Console.WriteLine($"Connected to: {selectedAddress}");

4. Use Web3 with WalletConnect

var web3 = await walletConnectHostProvider.GetWeb3Async();

// Send transaction (goes through wallet for signing)
var receipt = await web3.Eth.GetEtherTransferService()
    .TransferEtherAndWaitForReceiptAsync("0xRecipientAddress", 0.1m);

// Query balance (goes through RPC endpoint if configured, or throws if not)
var balance = await web3.Eth.GetBalance.SendRequestAsync("0xAddress");

Usage Examples

Example 1: Complete Blazor WalletConnect Integration

Based on the official Nethereum WalletConnect Blazor console test.

@page "/"
@using Nethereum.WalletConnect
@using QRCoder
@using WalletConnectSharp.Sign
@using WalletConnectSharp.Sign.Models
@using WalletConnectSharp.Core
@using WalletConnectSharp.Storage

@if (walletConnectConnectedSession == null)
{
    <button @onclick="InitAsync">Connect Wallet</button>
    @if (!string.IsNullOrEmpty(QRByte))
    {
        <img src="@QRByte" Width="400" />
    }
}
else
{
    <div>
        <p>Address: @Address</p>
        <p>ChainId: @ChainId</p>
        <button @onclick="PersonalSignAsync">Personal Sign</button>
        <button @onclick="SignTypedDataAsync">Sign Typed Data</button>
        <button @onclick="SwitchChainAsync">Switch Chain</button>
        <button @onclick="AddEthereumChainAsync">Add Chain</button>
        <p>Response: @Response</p>
        <p>Recovered Account: @RecoveredAccount</p>
    </div>
}

@code {
    WalletConnectSignClient client;
    public string QRByte = "";
    NethereumWalletConnectService walletConnectService;
    WalletConnectConnectedSession walletConnectConnectedSession;
    NethereumWalletConnectHostProvider walletConnectHostProvider;
    public string Response;
    public string Address;
    public string ChainId;
    public string RecoveredAccount;

    public async Task InitAsync()
    {
        try
        {
            var options = new SignClientOptions()
            {
                ProjectId = "97d8fb2db9753c13645fd37d6920b2cc",
                Metadata = new Metadata()
                {
                    Description = "An example project to showcase WalletConnectSharpv2",
                    Icons = new[] { "https://walletconnect.com/meta/favicon.ico" },
                    Name = "WC Example",
                    Url = "https://walletconnect.com"
                },
                Storage = new InMemoryStorage()
            };

            var connectionOptions = NethereumWalletConnectService.GetDefaultConnectOptions();

            if (client == null)
            {
                client = await WalletConnectSignClient.Init(options);
            }

            walletConnectService = new NethereumWalletConnectService(client);
            // Initialize host provider immediately to hook up events
            walletConnectHostProvider = new NethereumWalletConnectHostProvider(walletConnectService);

            walletConnectHostProvider.SelectedAccountChanged += async (address) =>
            {
                Address = address;
                await InvokeAsync(StateHasChanged);
            };

            walletConnectHostProvider.NetworkChanged += async (chainId) =>
            {
                ChainId = chainId.ToString();
                await InvokeAsync(StateHasChanged);
            };

            var connectionUri = await walletConnectService.InitialiseConnectionAndGetQRUriAsync(connectionOptions);

            if (!string.IsNullOrEmpty(connectionUri))
            {
                // Generate QR code
                using MemoryStream ms = new();
                QRCodeGenerator qrCodeGenerate = new();
                QRCodeData qrCodeData = qrCodeGenerate.CreateQrCode(connectionUri, QRCodeGenerator.ECCLevel.Q);
                PngByteQRCode qRCode = new PngByteQRCode(qrCodeData);
                byte[] qrCodeBytes = qRCode.GetGraphic(20);
                string base64 = Convert.ToBase64String(qrCodeBytes);
                QRByte = string.Format("data:image/png;base64,{0}", base64);
                await InvokeAsync(StateHasChanged);

                // Wait for connection approval
                var selectedAddress = await walletConnectService.WaitForConnectionApprovalAndGetSelectedAccountAsync();
                walletConnectConnectedSession = walletConnectService.GetWalletConnectConnectedSession();
            }
        }
        catch (Exception ex)
        {
            Response = ex.Message;
            Console.WriteLine(ex.ToString());
        }
    }

    public async Task PersonalSignAsync()
    {
        try
        {
            var web3 = await walletConnectHostProvider.GetWeb3Async();
            var response = await web3.Eth.AccountSigning.PersonalSign.SendRequestAsync(
                new HexUTF8String("Hello World"));
            Response = response;
        }
        catch (Exception ex)
        {
            Response = ex.Message;
        }
    }

    public async Task SignTypedDataAsync()
    {
        try
        {
            var web3 = await walletConnectHostProvider.GetWeb3Async();
            var typedData = GetMailTypedDefinition();

            var mail = new Mail
            {
                From = new Person
                {
                    Name = "Cow",
                    Wallets = new List<string> { "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" }
                },
                To = new List<Person> { new Person { Name = "Bob" } },
                Contents = "Hello, Bob!"
            };

            typedData.Domain.ChainId = 1;
            typedData.SetMessage(mail);

            Response = await web3.Eth.AccountSigning.SignTypedDataV4.SendRequestAsync(typedData.ToJson());
            RecoveredAccount = new Eip712TypedDataSigner().RecoverFromSignatureV4(typedData, Response);
        }
        catch (Exception ex)
        {
            Response = ex.Message();
        }
    }

    public async Task SwitchChainAsync()
    {
        try
        {
            var web3 = await walletConnectHostProvider.GetWeb3Async();
            var response = await web3.Eth.HostWallet.SwitchEthereumChain.SendRequestAsync(
                new SwitchEthereumChainParameter() { ChainId = 1.ToHexBigInteger() });
            Response = response;
        }
        catch (Exception ex)
        {
            Response = ex.Message;
        }
    }

    public async Task AddEthereumChainAsync()
    {
        try
        {
            var web3 = await walletConnectHostProvider.GetWeb3Async();
            var chainFeature = ChainDefaultFeaturesServicesRepository.GetDefaultChainFeature(
                Nethereum.Signer.Chain.Optimism);
            var addParameter = chainFeature.ToAddEthereumChainParameter();
            var response = await web3.Eth.HostWallet.AddEthereumChain.SendRequestAsync(addParameter);
            Response = response;
        }
        catch (Exception ex)
        {
            Response = ex.Message;
        }
    }

    public TypedData<Domain> GetMailTypedDefinition()
    {
        return new TypedData<Domain>
        {
            Domain = new Domain
            {
                Name = "Ether Mail",
                Version = "1",
                ChainId = 1,
                VerifyingContract = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
            },
            Types = MemberDescriptionFactory.GetTypesMemberDescription(
                typeof(Domain), typeof(Group), typeof(Mail), typeof(Person)),
            PrimaryType = nameof(Mail),
        };
    }
}

Example 2: Custom Chain Configuration

using Nethereum.WalletConnect;

// Connect to specific chains (Mainnet + Polygon)
var connectionOptions = NethereumWalletConnectService.GetDefaultConnectOptions(1, 137);

var connectionUri = await walletConnectService.InitialiseConnectionAndGetQRUriAsync(connectionOptions);

Example 3: Hybrid Mode with Custom RPC Endpoint

using Nethereum.WalletConnect;

// Create host provider with custom RPC endpoint for queries
var walletConnectHostProvider = new NethereumWalletConnectHostProvider(
    walletConnectService,
    "https://mainnet.infura.io/v3/YOUR-PROJECT-ID");

var web3 = await walletConnectHostProvider.GetWeb3Async();

// Queries go through Infura
var balance = await web3.Eth.GetBalance.SendRequestAsync("0xAddress");

// Signing goes through WalletConnect
var signature = await web3.Eth.AccountSigning.PersonalSign.SendRequestAsync(
    new HexUTF8String("Sign this message"));

Example 4: Direct Service Usage (Without Host Provider)

using Nethereum.WalletConnect;
using Nethereum.Web3;

var walletConnectService = new NethereumWalletConnectService(client);
var connectionUri = await walletConnectService.InitialiseConnectionAndGetQRUriAsync();
var address = await walletConnectService.WaitForConnectionApprovalAndGetSelectedAccountAsync();

// Use Web3 with interceptor
var web3 = new Web3();
web3.Client.OverridingRequestInterceptor = new NethereumWalletConnectInterceptor(walletConnectService);

// Send transaction
var txHash = await walletConnectService.SendTransactionAsync(new TransactionInput
{
    To = "0xRecipient",
    Value = new HexBigInteger(Web3.Web3.Convert.ToWei(0.1m))
});

Example 5: Handling Account and Network Changes

using Nethereum.WalletConnect;

var walletConnectHostProvider = new NethereumWalletConnectHostProvider(walletConnectService);

walletConnectHostProvider.SelectedAccountChanged += async (newAddress) =>
{
    Console.WriteLine($"User switched to account: {newAddress}");
    // Update UI, reload balances, etc.
};

walletConnectHostProvider.NetworkChanged += async (newChainId) =>
{
    Console.WriteLine($"User switched to chain ID: {newChainId}");
    // Update UI to reflect new network
};

Example 6: Converting Chain IDs to EIP155 Format

using Nethereum.WalletConnect;

// Convert long chain ID to eip155 format
string eip155ChainId = NethereumWalletConnectService.GetEIP155ChainId(1);
// Returns: "eip155:1"

// Convert eip155 format to long
long chainId = NethereumWalletConnectService.GetChainIdFromEip155("eip155:137");
// Returns: 137

// Convert multiple chain IDs
string[] eip155ChainIds = NethereumWalletConnectService.GetEIP155ChainIds(1, 137, 10);
// Returns: ["eip155:1", "eip155:137", "eip155:10"]

API Reference

NethereumWalletConnectService

Core service for WalletConnect session management and wallet operations.

public class NethereumWalletConnectService : INethereumWalletConnectService
{
    // Constructor
    public NethereumWalletConnectService(ISignClient walletConnectClient);

    // Properties
    public ISignClient WalletConnectClient { get; }
    public string SelectedChainId { get; protected set; }
    public string SelectedAccount { get; protected set; }

    // Connection Management
    public Task<string> InitialiseConnectionAndGetQRUriAsync(
        ConnectOptions connectionOptions = null);
    public Task<string> WaitForConnectionApprovalAndGetSelectedAccountAsync();
    public WalletConnectConnectedSession GetWalletConnectConnectedSession();

    // Signing Operations
    public Task<string> PersonalSignAsync(string hexUtf8);
    public Task<string> SignAsync(string hexUtf8);
    public Task<string> SignTypedDataAsync(string hexUtf8);
    public Task<string> SignTypedDataV4Async(string hexUtf8);

    // Transaction Operations
    public Task<string> SendTransactionAsync(TransactionInput transaction);
    public Task<string> SignTransactionAsync(TransactionInput transaction);

    // Wallet Operations
    public Task<string> SwitchEthereumChainAsync(SwitchEthereumChainParameter chainId);
    public Task<string> AddEthereumChainAsync(AddEthereumChainParameter addEthereumChainParameter);

    // Static Helpers
    public static ConnectOptions GetDefaultConnectOptions();
    public static ConnectOptions GetDefaultConnectOptions(params long[] optionalEIP155chainIds);
    public static ConnectOptions GetDefaultConnectOptions(params string[] optionalEIP155chainIds);
    public static string GetEIP155ChainId(long chainId);
    public static long GetChainIdFromEip155(string chainId);
    public static string[] GetEIP155ChainIds(params long[] chainIds);

    // Constants
    public const string MAINNET = "eip155:1";
    public static readonly string[] DEFAULT_CHAINS = { MAINNET };
}

NethereumWalletConnectHostProvider

Implementation of IEthereumHostProvider for WalletConnect.

public class NethereumWalletConnectHostProvider : IEthereumHostProvider
{
    // Constructors
    public NethereumWalletConnectHostProvider(
        NethereumWalletConnectService walletConnectService,
        IClient client = null);

    public NethereumWalletConnectHostProvider(
        NethereumWalletConnectService walletConnectService,
        string url,
        AuthenticationHeaderValue authHeaderValue = null,
        JsonSerializerSettings jsonSerializerSettings = null,
        HttpClientHandler httpClientHandler = null,
        ILogger log = null);

    // Properties
    public static NethereumWalletConnectHostProvider Current { get; }
    public string Name { get; } // "WalletConnect"
    public bool Available { get; }
    public string SelectedAccount { get; }
    public long SelectedNetworkChainId { get; }
    public bool Enabled { get; }
    public IClient Client { get; }

    // Events
    public event Func<string, Task> SelectedAccountChanged;
    public event Func<long, Task> NetworkChanged;
    public event Func<bool, Task> AvailabilityChanged;
    public event Func<bool, Task> EnabledChanged;

    // Methods
    public Task<bool> CheckProviderAvailabilityAsync();
    public Task<IWeb3> GetWeb3Async();
    public Task<string> EnableProviderAsync();
    public Task<string> GetProviderSelectedAccountAsync();
    public Task<string> SignMessageAsync(string message);
    public Task ChangeSelectedAccountAsync(string selectedAccount);
    public Task ChangeSelectedNetworkAsync(long chainId);
}

NethereumWalletConnectInterceptor

Request interceptor for routing methods through WalletConnect.

public class NethereumWalletConnectInterceptor : RequestInterceptor
{
    // Constructor
    public NethereumWalletConnectInterceptor(INethereumWalletConnectService walletConnectService);
    public NethereumWalletConnectInterceptor(WalletConnectSignClient walletConnectSignClient);

    // Properties
    public static List<string> SigningWalletTransactionsMethods { get; protected set; }
    public string SelectedAccount { get; internal set; }

    // Intercepted Methods (automatically routed through wallet)
    // - eth_sendTransaction
    // - eth_sign
    // - personal_sign
    // - eth_signTypedData_v4
    // - wallet_switchEthereumChain
    // - wallet_addEthereumChain
}

WalletConnectConnectedSession

Session information for active WalletConnect connection.

public class WalletConnectConnectedSession
{
    public SessionStruct Session { get; set; }
    public string Address { get; set; }
    public string ChainId { get; set; }
}

Important Notes

WalletConnect Cloud Project ID Required

You must sign up for a free Project ID at https://cloud.walletconnect.com. This is required for the WalletConnect relay network.

Event Subscription Timing

Create NethereumWalletConnectHostProvider IMMEDIATELY after creating the service to ensure event subscriptions are set up before connection approval:

walletConnectService = new NethereumWalletConnectService(client);
// Create provider immediately - DO NOT DELAY
walletConnectHostProvider = new NethereumWalletConnectHostProvider(walletConnectService);

Connection Flow is Async

The connection flow requires user interaction:

  1. Display QR code to user
  2. User scans with mobile wallet
  3. User approves connection in wallet
  4. WaitForConnectionApprovalAndGetSelectedAccountAsync completes

Do not block the UI thread during this process.

RPC Endpoint Optional

If you don't provide a custom RPC endpoint, only signing methods will work. Queries (eth_call, eth_getBalance, etc.) will fail. For a complete dApp experience, provide an RPC endpoint:

var provider = new NethereumWalletConnectHostProvider(
    walletConnectService,
    "https://mainnet.infura.io/v3/YOUR-KEY");

Chain ID Format

WalletConnect uses CAIP-25 format for chain IDs: eip155:{chainId}

Use the helper methods to convert between formats:

  • GetEIP155ChainId(1)"eip155:1"
  • GetChainIdFromEip155("eip155:137")137

Storage Considerations

The example uses InMemoryStorage() which loses session on app restart. For production, consider implementing persistent storage to restore sessions.

Protocol Compatibility

WalletConnect v2 protocol is supported. Any mobile wallet application that implements the WalletConnect v2 protocol specification can connect via QR code pairing.

Dependencies

  • Nethereum.UI - IEthereumHostProvider interface
  • Nethereum.Web3 - Web3 client and transaction management
  • WalletConnect.Core - WalletConnect v2 core functionality
  • WalletConnect.Sign - WalletConnect v2 sign protocol

Similar Packages

  • Nethereum.Metamask - Browser extension wallet integration
  • Nethereum.EIP6963WalletInterop - EIP-6963 multi-wallet discovery
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.8.0 119 1/6/2026
5.0.0 295 5/28/2025
4.29.0 229 2/10/2025
4.28.0 216 1/7/2025
4.27.1 219 12/24/2024
4.27.0 202 12/24/2024
4.26.0 209 10/1/2024
4.25.0 212 9/19/2024
4.21.4 286 8/9/2024
4.21.3 235 7/22/2024
4.21.2 372 6/26/2024
4.21.1 220 6/26/2024
4.21.0 423 6/18/2024
4.20.0 468 3/28/2024
4.19.0 238 2/16/2024
4.18.0 362 11/21/2023
4.17.1 247 9/28/2023
4.17.0 227 9/28/2023