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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ is ported to C#/.NET 10, test-first and verified to the digit against the upstre
files, contributor & porting guides, and tag-driven NuGet packaging (SourceLink + symbols + MinVer).

### Changed
- **Nullable-reference hardening of the public API (pre-1.0.0).** Option/params types now carry explicit null
contracts taken from the upstream TypeScript interfaces: upstream-required fields are `required` in C#,
upstream-optional fields (`x?: T`) are nullable. `CurrencyAmount.Wrapped()` and `.AsBaseCurrency()` are now
non-nullable (they provably never return null), strengthening the contract for callers. The solution builds
with **zero compiler warnings**. Consumers using object initializers may need to supply fields now marked
`required` — a deliberate source-level tightening done before the stable release.
- Test assertions migrated from FluentAssertions to **AwesomeAssertions** (Apache-2.0 community fork)
to avoid FluentAssertions v8's commercial license. Test-only; no effect on the shipped package.
- Runtime dependencies updated to `Nethereum` 6.1.0 and `ExtendedNumerics.BigRational` 3000.0.2.132,
Expand All @@ -46,6 +52,15 @@ is ported to C#/.NET 10, test-first and verified to the digit against the upstre
to provide Ed25519 for the tamperproof-transactions port.

### Fixed
- **V3 `SwapRouter` input-token-permit path was unusable.** `SwapOptions.InputTokenPermit` was typed against an
empty stub class, so the only assignable value failed `SelfPermit.EncodePermit`'s type tests and always threw
`"Invalid permit options"` — while the valid permit types could not be assigned to it at all. The upstream
`PermitOptions` union is now modelled as `SelfPermit.IPermitOptions`, implemented by both
`StandardPermitArguments` and `AllowedPermitArguments`. Every permit option (`InputTokenPermit`,
`OutputTokenPermit`, `Token0Permit`, `Token1Permit`) is typed against it instead of `object`, so misuse is now
a compile error. Pinned by a new regression test; upstream ships no test for this path.
- `PoolTests.BigNums_CorrectlyHandlesTwoBigIntegers` awaited its `GetInputAmount` call — previously the
unawaited `Task` swallowed exceptions, so the test could pass vacuously.
- Several latent correctness bugs found while porting and pinned with upstream vectors: `sdk-core`
`sqrt`, the FOT `Token` guard, zkSync address slicing, exact `Fraction` formatting (no float),
`CurrencyAmount.ToExact()` overflow/format handling, `EncodeRouteToPath`, `Multicall` encoding,
Expand Down
25 changes: 25 additions & 0 deletions docs/PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ None — all seven original `NotImplementedException` stubs are ported test-firs
3. Implement until green, matching numbers to the digit. 4. Update the table above. 5. PR into `main`.

## 6. Intentional divergences
- **Nullable-reference hardening (pre-1.0.0).** The public option/params types now carry explicit null
contracts derived from the upstream TypeScript interfaces: a required upstream field (`x: T`) is `required`
in C#, an optional one (`x?: T`) is nullable (`T?`). Applied across `NonfungiblePositionManager`,
`Staker`, `SelfPermit` and `Payments`. The library and test suite now build with **zero compiler warnings**.
Two root causes were fixed rather than suppressed:
- `CurrencyAmount.Wrapped()` / `.AsBaseCurrency()` were declared nullable but provably never return null
(every path constructs a value via the non-nullable `FromFractionalAmount`, and `BaseCurrency.Wrapped()`
returns a non-nullable `Token`). Tightening the return types to non-nullable removed all five downstream
flow warnings (`CS8601`/`CS8602`) at once and strengthens the contract for callers.
- `Route._midPrice` is a lazily-computed cache and is genuinely null until first access; it is now `Price<,>?`.
- **`PermitOptions` union modelled with a marker interface — fixes a broken API path.** Upstream declares
`PermitOptions = StandardPermitArguments | AllowedPermitArguments`. The port had typed
`Staker.SwapOptions.InputTokenPermit` as an **empty stub class** (`Staker.PermitOptions {}`) while
`SelfPermit.EncodePermit` took `object` and type-tested for the two argument interfaces. The result: the only
value assignable to `InputTokenPermit` satisfied neither test, so the V3 SwapRouter's input-token-permit path
**could only ever throw `"Invalid permit options"`** — and the valid argument types could not be assigned to it
at all. Upstream ships no test for this path, which is why the port inherited the gap. Fixed by introducing
`SelfPermit.IPermitOptions` (implemented by both argument interfaces), typing `EncodePermit` and every permit
option against it (`Staker.SwapOptions.InputTokenPermit`, `NonfungiblePositionManager.AddLiquidityOptions.
Token0Permit`/`Token1Permit`, `Router.SwapOptions.InputTokenPermit`/`SwapAndAddOptions.OutputTokenPermit` —
all previously `object?`), and deleting the empty stub. Pinned by `V3/SwapRouterPermitTests.cs`.
- **`PoolTests.BigNums_CorrectlyHandlesTwoBigIntegers` now awaits `GetInputAmount`.** Upstream calls
`pool.getInputAmount(outputAmount)` without awaiting ("if output is correct, function has succeeded"). In C#
an unawaited `Task` swallows exceptions, so the test would have passed even if `GetInputAmount` threw on the
very big numbers it is named for. Awaiting it makes the assertion real; it passes.
- `CurrencyAmount.ToExact` now computes the exact decimal with `BigInteger` (integer part + zero-padded,
trailing-trimmed fractional part), matching Decimal.js. The earlier `(decimal)` cast overflowed
`System.Decimal` (~7.9e28) for large amounts; hardened test-first (`CurrencyAmountTests.cs`, incl. a
Expand Down
17 changes: 6 additions & 11 deletions src/UniswapSharp/Core/Entities/Fractions/CurrencyAmount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,21 @@ public string ToExact(string format = "0.#############################")
return integerPart.ToString(CultureInfo.InvariantCulture) + "." + fractional;
}

public CurrencyAmount<BaseCurrency>? AsBaseCurrency() => new(this.Currency, Numerator, Denominator)
{

};
public CurrencyAmount<BaseCurrency> AsBaseCurrency() => new(this.Currency, Numerator, Denominator);


public CurrencyAmount<Token>? Wrapped()
/// <summary>
/// The amount re-expressed against the currency's wrapped <see cref="Token"/>.
/// Never null: every path either reuses this instance or constructs a new amount.
/// </summary>
public CurrencyAmount<Token> Wrapped()
{

if (Currency is Token)
{
var x = this as CurrencyAmount<Token>;

return x ?? FromFractionalAmount(Currency.Wrapped(), Numerator, Denominator);
}


return FromFractionalAmount(Currency.Wrapped(), Numerator, Denominator);


}

public bool Equals(CurrencyAmount<T>? other)
Expand Down
6 changes: 4 additions & 2 deletions src/UniswapSharp/Router/SwapRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ public class SwapOptions
/// <summary>Either a deadline (epoch seconds, as BigInteger/int/string) or a previous block hash (0x… string).</summary>
public object? DeadlineOrPreviousBlockhash { get; init; }

public object? InputTokenPermit { get; init; }
/// <summary>Accepts SelfPermit.StandardPermitArguments or SelfPermit.AllowedPermitArguments.</summary>
public V3.SelfPermit.IPermitOptions? InputTokenPermit { get; init; }
public V3.Payments.IFeeOptions? Fee { get; init; }
}

public class SwapAndAddOptions : SwapOptions
{
public object? OutputTokenPermit { get; init; }
/// <summary>Accepts SelfPermit.StandardPermitArguments or SelfPermit.AllowedPermitArguments.</summary>
public V3.SelfPermit.IPermitOptions? OutputTokenPermit { get; init; }
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/UniswapSharp/V3/Entities/Route.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public class Route<TInput, TOutput> where TInput : BaseCurrency where TOutput :
public TInput Input { get; }
public TOutput Output { get; }

private Price<TInput, TOutput> _midPrice = null;
// Lazily computed and cached on first access — genuinely null until then.
private Price<TInput, TOutput>? _midPrice;

public Route(List<Pool> pools, TInput input, TOutput output)
{
Expand Down
56 changes: 29 additions & 27 deletions src/UniswapSharp/V3/NonfungiblePositionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,29 @@ public class NonfungiblePositionManager
{
public class MethodParameters
{
public string Calldata { get; set; }
public string Value { get; set; }
public required string Calldata { get; set; }
public required string Value { get; set; }
}

// Common options for adding liquidity (mint or increase).
public abstract class AddLiquidityOptions
{
public Percent SlippageTolerance { get; set; }
public required Percent SlippageTolerance { get; set; }
public BigInteger Deadline { get; set; }

// Whether to spend ether. If set, one of the pool tokens must be WETH.
public NativeCurrency? UseNative { get; set; }

// Optional permit parameters for spending token0 / token1 (SelfPermit options).
public object? Token0Permit { get; set; }
public object? Token1Permit { get; set; }
// Optional permit parameters for spending token0 / token1.
// Accepts SelfPermit.StandardPermitArguments or SelfPermit.AllowedPermitArguments.
public SelfPermit.IPermitOptions? Token0Permit { get; set; }
public SelfPermit.IPermitOptions? Token1Permit { get; set; }
}

public class MintOptions : AddLiquidityOptions
{
// The account that should receive the minted NFT.
public string Recipient { get; set; }
public required string Recipient { get; set; }

// Creates the pool if not initialized before mint.
public bool CreatePool { get; set; }
Expand Down Expand Up @@ -162,29 +163,29 @@ public static MethodParameters AddCallParameters(Position position, AddLiquidity
public class CollectOptions
{
public BigInteger TokenId { get; set; }
public CurrencyAmount<BaseCurrency> ExpectedCurrencyOwed0 { get; set; }
public CurrencyAmount<BaseCurrency> ExpectedCurrencyOwed1 { get; set; }
public string Recipient { get; set; }
public required CurrencyAmount<BaseCurrency> ExpectedCurrencyOwed0 { get; set; }
public required CurrencyAmount<BaseCurrency> ExpectedCurrencyOwed1 { get; set; }
public required string Recipient { get; set; }
}

public class NFTPermitOptions
{
public byte V { get; set; }
public string R { get; set; }
public string S { get; set; }
public required string R { get; set; }
public required string S { get; set; }
public BigInteger Deadline { get; set; }
public string Spender { get; set; }
public required string Spender { get; set; }
}

public class RemoveLiquidityOptions
{
public BigInteger TokenId { get; set; }
public Percent LiquidityPercentage { get; set; }
public Percent SlippageTolerance { get; set; }
public required Percent LiquidityPercentage { get; set; }
public required Percent SlippageTolerance { get; set; }
public BigInteger Deadline { get; set; }
public bool BurnToken { get; set; }
public NFTPermitOptions? Permit { get; set; }
public CollectOptions CollectOptions { get; set; }
public required CollectOptions CollectOptions { get; set; }
}

private static List<string> EncodeCollect(CollectOptions options)
Expand Down Expand Up @@ -305,9 +306,10 @@ public static MethodParameters RemoveCallParameters(Position position, RemoveLiq

public class SafeTransferOptions
{
public string Sender { get; set; }
public string Recipient { get; set; }
public required string Sender { get; set; }
public required string Recipient { get; set; }
public BigInteger TokenId { get; set; }
// upstream `data?: string` — optional.
public string? Data { get; set; }
}

Expand Down Expand Up @@ -342,31 +344,31 @@ public static MethodParameters SafeTransferFromParameters(SafeTransferOptions op

public class TypedDataField
{
public string Name { get; set; }
public string Type { get; set; }
public required string Name { get; set; }
public required string Type { get; set; }
}

public class TypedDataDomain
{
public string Name { get; set; }
public required string Name { get; set; }
public int ChainId { get; set; }
public string Version { get; set; }
public string VerifyingContract { get; set; }
public required string Version { get; set; }
public required string VerifyingContract { get; set; }
}

public class NFTPermitValues
{
public string Spender { get; set; }
public required string Spender { get; set; }
public BigInteger TokenId { get; set; }
public BigInteger Deadline { get; set; }
public BigInteger Nonce { get; set; }
}

public class NFTPermitData
{
public TypedDataDomain Domain { get; set; }
public Dictionary<string, List<TypedDataField>> Types { get; set; }
public NFTPermitValues Values { get; set; }
public required TypedDataDomain Domain { get; set; }
public required Dictionary<string, List<TypedDataField>> Types { get; set; }
public required NFTPermitValues Values { get; set; }
}

private static Dictionary<string, List<TypedDataField>> NftPermitTypes() => new()
Expand Down
4 changes: 2 additions & 2 deletions src/UniswapSharp/V3/Payments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public interface IFeeOptions

public class FeeOptions : IFeeOptions
{
public Percent Fee { get; set; }
public string Recipient { get; set; }
public required Percent Fee { get; set; }
public required string Recipient { get; set; }
}
}
19 changes: 12 additions & 7 deletions src/UniswapSharp/V3/SelfPermit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ namespace UniswapSharp.V3;

public static class SelfPermit
{
public static string EncodePermit(Token token, object options)
/// <summary>
/// Marker for the upstream union <c>PermitOptions = StandardPermitArguments | AllowedPermitArguments</c>.
/// </summary>
public interface IPermitOptions;

public static string EncodePermit(Token token, IPermitOptions options)
{
if (options is IAllowedPermitArguments allowedOptions)
{
Expand Down Expand Up @@ -36,7 +41,7 @@ public static string EncodePermit(Token token, object options)
}
}

public interface IAllowedPermitArguments
public interface IAllowedPermitArguments : IPermitOptions
{
byte V { get; }
string R { get; }
Expand All @@ -45,7 +50,7 @@ public interface IAllowedPermitArguments
BigInteger Expiry { get; }
}

public interface IStandardPermitArguments
public interface IStandardPermitArguments : IPermitOptions
{
byte V { get; }
string R { get; }
Expand All @@ -57,17 +62,17 @@ public interface IStandardPermitArguments
public class AllowedPermitArguments : IAllowedPermitArguments
{
public byte V { get; set; }
public string R { get; set; }
public string S { get; set; }
public required string R { get; set; }
public required string S { get; set; }
public BigInteger Nonce { get; set; }
public BigInteger Expiry { get; set; }
}

public class StandardPermitArguments : IStandardPermitArguments
{
public byte V { get; set; }
public string R { get; set; }
public string S { get; set; }
public required string R { get; set; }
public required string S { get; set; }
public BigInteger Amount { get; set; }
public BigInteger Deadline { get; set; }
}
Expand Down
32 changes: 15 additions & 17 deletions src/UniswapSharp/V3/Staker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,20 @@ public static string EncodeDeposit(IncentiveKey[] incentiveKeys)
public class FullWithdrawOptions : IClaimOptions, IWithdrawOptions
{
public BigInteger TokenId { get; set; }
public string Recipient { get; set; }
public required string Recipient { get; set; }
public BigInteger? Amount { get; set; }
public string Owner { get; set; }
public string Data { get; set; }
public required string Owner { get; set; }
// upstream `data?: string` — optional.
public string? Data { get; set; }
}

public class IncentiveKey
{
public Token RewardToken { get; set; }
public Pool Pool { get; set; }
public required Token RewardToken { get; set; }
public required Pool Pool { get; set; }
public BigInteger StartTime { get; set; }
public BigInteger EndTime { get; set; }
public string Refundee { get; set; }
public required string Refundee { get; set; }
}

public interface IClaimOptions
Expand All @@ -156,28 +157,25 @@ public interface IClaimOptions
public class ClaimOptions : IClaimOptions
{
public BigInteger TokenId { get; set; }
public string Recipient { get; set; }
public required string Recipient { get; set; }
public BigInteger? Amount { get; set; }
}

public interface IWithdrawOptions
{
public string Owner { get; set; }
public string Data { get; set; }
}

public class PermitOptions
{
// Implement PermitOptions
public string? Data { get; set; }
}

public class SwapOptions
{
public Percent SlippageTolerance { get; set; }
public string Recipient { get; set; }
public required Percent SlippageTolerance { get; set; }
public required string Recipient { get; set; }
public BigInteger Deadline { get; set; }
public PermitOptions InputTokenPermit { get; set; }
// upstream `inputTokenPermit?: PermitOptions` / `fee?: FeeOptions` — optional.
// Accepts SelfPermit.StandardPermitArguments or SelfPermit.AllowedPermitArguments.
public SelfPermit.IPermitOptions? InputTokenPermit { get; set; }
public BigInteger? SqrtPriceLimitX96 { get; set; }
public Payments.FeeOptions Fee { get; set; }
public Payments.FeeOptions? Fee { get; set; }
}
}
5 changes: 3 additions & 2 deletions test/UniswapSharp.Testing/V3/ConstantsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ public void InitCodeHash_MatchesComputedBytecodeHash()
var computedInitCodeHash = Sha3Keccack.Current.CalculateHash(bytecodeBytes);
var computedInitCodeHashHex = "0x" + BitConverter.ToString(computedInitCodeHash).Replace("-", "").ToLowerInvariant();

// Assert that the computed hash matches the constant
Assert.Equal(Constants.POOL_INIT_CODE_HASH, computedInitCodeHashHex);
// Assert that the computed hash matches the supported API (which shares the same
// source-of-truth constant as the obsolete POOL_INIT_CODE_HASH).
Assert.Equal(Constants.PoolInitCodeHash(), computedInitCodeHashHex);
}


Expand Down
Loading
Loading