diff --git a/.changeset/currency-amount-comparators.md b/.changeset/currency-amount-comparators.md new file mode 100644 index 000000000..a86d7ea49 --- /dev/null +++ b/.changeset/currency-amount-comparators.md @@ -0,0 +1,7 @@ +--- +"@uniswap/sdk-core": minor +"@uniswap/v2-sdk": patch +"@uniswap/router-sdk": patch +--- + +Add `lessThan`, `equalTo`, and `greaterThan` overrides on `CurrencyAmount` that enforce the same currency invariant used by `add`/`subtract`. The operand type is narrowed to `CurrencyAmount | 0`: comparing two `CurrencyAmount`s of different currencies now throws `CURRENCY`, and the only non-`CurrencyAmount` operand accepted is the literal `0` sentinel (so comparisons against bare `BigintIsh` values that ignore `decimalScale` are no longer allowed). Internal callers in `v2-sdk` and `router-sdk` that previously passed `JSBI.BigInt(0)` have been updated to pass `0`. diff --git a/sdks/router-sdk/src/entities/mixedRoute/trade.ts b/sdks/router-sdk/src/entities/mixedRoute/trade.ts index 634437a0f..066ac6b68 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/trade.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/trade.ts @@ -465,7 +465,7 @@ export class MixedRouteTrade diff --git a/sdks/router-sdk/src/entities/trade.ts b/sdks/router-sdk/src/entities/trade.ts index c71e98510..fefc6babe 100644 --- a/sdks/router-sdk/src/entities/trade.ts +++ b/sdks/router-sdk/src/entities/trade.ts @@ -315,7 +315,7 @@ export class Trade { }) }) + describe('comparators', () => { + const tokenA = new Token(1, ADDRESS_ONE, 18) + const tokenB = new Token(1, '0x0000000000000000000000000000000000000002', 18) + + it('lessThan returns true when first amount is smaller', () => { + const a = CurrencyAmount.fromRawAmount(tokenA, 1) + const b = CurrencyAmount.fromRawAmount(tokenA, 2) + expect(a.lessThan(b)).toBe(true) + expect(b.lessThan(a)).toBe(false) + }) + + it('equalTo returns true for equal amounts of the same currency', () => { + const a = CurrencyAmount.fromRawAmount(tokenA, 100) + const b = CurrencyAmount.fromRawAmount(tokenA, 100) + expect(a.equalTo(b)).toBe(true) + }) + + it('greaterThan returns true when first amount is larger', () => { + const a = CurrencyAmount.fromRawAmount(tokenA, 5) + const b = CurrencyAmount.fromRawAmount(tokenA, 1) + expect(a.greaterThan(b)).toBe(true) + }) + + it('comparison against the literal 0 sentinel works without currency check', () => { + const zero = CurrencyAmount.fromRawAmount(tokenA, 0) + const nonZero = CurrencyAmount.fromRawAmount(tokenA, 1) + expect(zero.equalTo(0)).toBe(true) + expect(nonZero.greaterThan(0)).toBe(true) + expect(zero.lessThan(0)).toBe(false) + }) + + it('only the literal 0 sentinel is accepted as a non-CurrencyAmount operand', () => { + const amount = CurrencyAmount.fromRawAmount(tokenA, 1) + // @ts-expect-error — non-zero BigintIsh is rejected by the narrowed type + expect(() => amount.greaterThan(JSBI.BigInt(5))).toThrow() + // @ts-expect-error — strings are rejected by the narrowed type + expect(() => amount.equalTo('1')).toThrow() + }) + + it('throws CURRENCY when comparing different currencies', () => { + const a = CurrencyAmount.fromRawAmount(tokenA, 1) + const b = CurrencyAmount.fromRawAmount(tokenB, 1) + expect(() => a.lessThan(b)).toThrow('CURRENCY') + expect(() => a.equalTo(b)).toThrow('CURRENCY') + expect(() => a.greaterThan(b)).toThrow('CURRENCY') + }) + }) + describe('#toExact', () => { it('does not throw for sig figs > currency.decimals', () => { const token = new Token(1, ADDRESS_ONE, 0) diff --git a/sdks/sdk-core/src/entities/fractions/currencyAmount.ts b/sdks/sdk-core/src/entities/fractions/currencyAmount.ts index dd96dfe60..f7cde1f2a 100644 --- a/sdks/sdk-core/src/entities/fractions/currencyAmount.ts +++ b/sdks/sdk-core/src/entities/fractions/currencyAmount.ts @@ -66,6 +66,21 @@ export class CurrencyAmount extends Fraction { return CurrencyAmount.fromFractionalAmount(this.currency, divided.numerator, divided.denominator) } + public lessThan(other: CurrencyAmount | 0): boolean { + invariant(other === 0 || this.currency.equals(other.currency), 'CURRENCY') + return super.lessThan(other) + } + + public equalTo(other: CurrencyAmount | 0): boolean { + invariant(other === 0 || this.currency.equals(other.currency), 'CURRENCY') + return super.equalTo(other) + } + + public greaterThan(other: CurrencyAmount | 0): boolean { + invariant(other === 0 || this.currency.equals(other.currency), 'CURRENCY') + return super.greaterThan(other) + } + public toSignificant( significantDigits: number = 6, format?: object, diff --git a/sdks/v2-sdk/src/entities/trade.ts b/sdks/v2-sdk/src/entities/trade.ts index 8a2e8d68d..1867debef 100644 --- a/sdks/v2-sdk/src/entities/trade.ts +++ b/sdks/v2-sdk/src/entities/trade.ts @@ -246,7 +246,7 @@ export class Trade try { @@ -340,7 +340,7 @@ export class Trade try {