Skip to content
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
31 changes: 27 additions & 4 deletions src/DistributedNonce/Services/DistributedNonceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DistributedLockManager.Interfaces;
using Nethereum.Hex.HexTypes;
using Nethereum.JsonRpc.Client;
using Nethereum.RPC.Eth;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.RPC.Eth.Transactions;
using Nethereum.RPC.NonceServices;
Expand All @@ -22,27 +23,49 @@ private sealed class CreateDistributedNonceServiceInstance(string accountAddress
{
private readonly IDistributedLockService _distributedLockService = distributedLockService;
private readonly string _address = accountAddress;
public IClient Client { get; set; } = client;
private IClient _client = client;
private BigInteger? _chainId = null;
public IClient Client
{
get => _client;
set
{
ArgumentNullException.ThrowIfNull(value);

if (!ReferenceEquals(_client, value))
{
_client = value;
_chainId = null;
}
}
}
public BigInteger CurrentNonce { get; set; } = -1;
public bool UseLatestTransactionsOnly { get; set; } = useLatestTransactionsOnly;

public async Task<HexBigInteger> GetNextNonceAsync()
{
HexBigInteger nextNonce = new(BigInteger.Zero);

EthGetTransactionCount ethGetTransactionCount = new(Client);

await _distributedLockService.RunWithLockAsync(func: async () =>
{
try
{
if (!_chainId.HasValue)
{
var ethChainId = new EthChainId(Client);
var chainIdHex = await ethChainId.SendRequestAsync().ConfigureAwait(continueOnCapturedContext: false);
_chainId = chainIdHex.Value;
}

EthGetTransactionCount ethGetTransactionCount = new(Client);

BlockParameter block = BlockParameter.CreatePending();
if (UseLatestTransactionsOnly)
{
block = BlockParameter.CreateLatest();
}

HexBigInteger hexBigInteger =
HexBigInteger hexBigInteger =
await ethGetTransactionCount.SendRequestAsync(_address, block).
ConfigureAwait(continueOnCapturedContext: false);
if (hexBigInteger.Value <= CurrentNonce)
Expand Down
78 changes: 76 additions & 2 deletions tests/DistributedNonce.Tests/DistributedNonceIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,32 @@ public async Task OneTimeSetUp()
_clientReturns5 = Substitute.For<IClient>();
_clientReturns5
.SendRequestAsync<HexBigInteger>(Arg.Any<RpcRequest>())
.Returns(Task.FromResult(new HexBigInteger(new BigInteger(5))));
.Returns(callInfo =>
{
var request = callInfo.Arg<RpcRequest>();
// Mock eth_chainId to return chain ID 1 (Ethereum mainnet)
if (request.Method == "eth_chainId")
{
return Task.FromResult(new HexBigInteger(new BigInteger(1)));
}
// Mock eth_getTransactionCount to return 5
return Task.FromResult(new HexBigInteger(new BigInteger(5)));
});
Comment thread
Phelecks marked this conversation as resolved.
Resolved

_clientReturns7 = Substitute.For<IClient>();
_clientReturns7
.SendRequestAsync<HexBigInteger>(Arg.Any<RpcRequest>())
.Returns(Task.FromResult(new HexBigInteger(new BigInteger(7))));
.Returns(callInfo =>
{
var request = callInfo.Arg<RpcRequest>();
// Mock eth_chainId to return chain ID 1 (Ethereum mainnet)
if (request.Method == "eth_chainId")
{
return Task.FromResult(new HexBigInteger(new BigInteger(1)));
}
// Mock eth_getTransactionCount to return 7
return Task.FromResult(new HexBigInteger(new BigInteger(7)));
});
}

[OneTimeTearDown]
Expand Down Expand Up @@ -170,4 +190,58 @@ public async Task GetNextNonceAsync_Concurrent_Calls_Produce_Unique_Sequential_V
var expected = Enumerable.Range(7, count).Select(i => new BigInteger(i));
values.ShouldBe(expected, ignoreOrder: true);
}

[Test, Order(6)]
public async Task GetNextNonceAsync_DifferentChains_ProduceIndependentNonces()
{
if (scope is null) Assert.Fail("Service scope not initialized");
var sp = scope!.ServiceProvider;
var service = sp.GetRequiredService<DistributedNonceService>();

// Create a client that returns chain ID 1 (Ethereum mainnet)
var clientChain1 = Substitute.For<IClient>();
clientChain1
.SendRequestAsync<HexBigInteger>(Arg.Any<RpcRequest>())
.Returns(callInfo =>
{
var request = callInfo.Arg<RpcRequest>();
if (request.Method == "eth_chainId")
{
return Task.FromResult(new HexBigInteger(new BigInteger(1)));
}
return Task.FromResult(new HexBigInteger(new BigInteger(10)));
});
Comment thread
Phelecks marked this conversation as resolved.
Resolved

// Create a client that returns chain ID 137 (Polygon mainnet)
var clientChain137 = Substitute.For<IClient>();
clientChain137
.SendRequestAsync<HexBigInteger>(Arg.Any<RpcRequest>())
.Returns(callInfo =>
{
var request = callInfo.Arg<RpcRequest>();
if (request.Method == "eth_chainId")
{
return Task.FromResult(new HexBigInteger(new BigInteger(137)));
}
return Task.FromResult(new HexBigInteger(new BigInteger(20)));
});
Comment thread
Phelecks marked this conversation as resolved.
Resolved

var address = "0x0000000000000000000000000000000000000004";

// Each chain uses a separate service instance, so nonce counters are independent
var nonceServiceChain1 = service.GetInstance(address, clientChain1);
var nonceServiceChain137 = service.GetInstance(address, clientChain137);

// Get nonces from both chains
var nonceChain1 = await nonceServiceChain1.GetNextNonceAsync();
var nonceChain137 = await nonceServiceChain137.GetNextNonceAsync();

// They should return different nonces because they have independent nonce counters
// Chain 1 returns 10, Chain 137 returns 20
Assert.Multiple(() =>
{
Assert.That(nonceChain1.Value, Is.EqualTo(new BigInteger(10)), "Chain 1 should return nonce 10");
Assert.That(nonceChain137.Value, Is.EqualTo(new BigInteger(20)), "Chain 137 should return nonce 20");
});
}
}
Loading