diff --git a/src/DistributedNonce/Services/DistributedNonceService.cs b/src/DistributedNonce/Services/DistributedNonceService.cs index 6a669eb..d846eb3 100644 --- a/src/DistributedNonce/Services/DistributedNonceService.cs +++ b/src/DistributedNonce/Services/DistributedNonceService.cs @@ -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; @@ -22,7 +23,22 @@ 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; @@ -30,19 +46,26 @@ public async Task 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) diff --git a/tests/DistributedNonce.Tests/DistributedNonceIntegrationTests.cs b/tests/DistributedNonce.Tests/DistributedNonceIntegrationTests.cs index 77996fe..a03da29 100644 --- a/tests/DistributedNonce.Tests/DistributedNonceIntegrationTests.cs +++ b/tests/DistributedNonce.Tests/DistributedNonceIntegrationTests.cs @@ -59,12 +59,32 @@ public async Task OneTimeSetUp() _clientReturns5 = Substitute.For(); _clientReturns5 .SendRequestAsync(Arg.Any()) - .Returns(Task.FromResult(new HexBigInteger(new BigInteger(5)))); + .Returns(callInfo => + { + var request = callInfo.Arg(); + // 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))); + }); _clientReturns7 = Substitute.For(); _clientReturns7 .SendRequestAsync(Arg.Any()) - .Returns(Task.FromResult(new HexBigInteger(new BigInteger(7)))); + .Returns(callInfo => + { + var request = callInfo.Arg(); + // 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] @@ -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(); + + // Create a client that returns chain ID 1 (Ethereum mainnet) + var clientChain1 = Substitute.For(); + clientChain1 + .SendRequestAsync(Arg.Any()) + .Returns(callInfo => + { + var request = callInfo.Arg(); + if (request.Method == "eth_chainId") + { + return Task.FromResult(new HexBigInteger(new BigInteger(1))); + } + return Task.FromResult(new HexBigInteger(new BigInteger(10))); + }); + + // Create a client that returns chain ID 137 (Polygon mainnet) + var clientChain137 = Substitute.For(); + clientChain137 + .SendRequestAsync(Arg.Any()) + .Returns(callInfo => + { + var request = callInfo.Arg(); + if (request.Method == "eth_chainId") + { + return Task.FromResult(new HexBigInteger(new BigInteger(137))); + } + return Task.FromResult(new HexBigInteger(new BigInteger(20))); + }); + + 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"); + }); + } }