diff --git a/.changeset/hip-flowers-march.md b/.changeset/hip-flowers-march.md new file mode 100644 index 000000000..6cc944f51 --- /dev/null +++ b/.changeset/hip-flowers-march.md @@ -0,0 +1,13 @@ +--- +"@balancer/sdk": major +--- + +**WHAT**: Removed wrapped token functionality from the `Token` class and introduced a new `NativeToken` class to handle native tokens (like ETH) separately. + +**WHY**: This change improves type safety and separation of concerns by distinguishing between ERC-20 tokens and native tokens, making the API more explicit and preventing confusion about token types. + +**HOW**: Update your code by: +- Replace any usage of `Token` for native tokens with the new `NativeToken` class if using the wrapped functionality +- If not using any wrapped functionality, the `Token` can remain as is +- Import `NativeToken` from the SDK: `import { NativeToken } from '@balancer/sdk'` +- Use `NativeToken` for native token operations instead of `Token` with wrapped functionality diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts index 38a9b4f64..00775ced2 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts @@ -35,7 +35,7 @@ export const validateBuildCallInput = ( if (input.wethIsEth) { if ( !input.amountsIn.some((a) => - a.token.isUnderlyingEqual(NATIVE_ASSETS[chainId]), + NATIVE_ASSETS[chainId].isWrapped(a.token), ) ) { throw inputValidationError( diff --git a/src/entities/index.ts b/src/entities/index.ts index 3e696b4af..ddbbf64fb 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -30,6 +30,7 @@ export * from './removeLiquidityNested/removeLiquidityNestedV3'; export * from './removeLiquidityNested/removeLiquidityNestedV3/types'; export * from './slippage'; export * from './token'; +export * from './nativeToken'; export * from './tokenAmount'; export * from './types'; export * from './utils'; diff --git a/src/entities/nativeToken.ts b/src/entities/nativeToken.ts new file mode 100644 index 000000000..2d8a7d12f --- /dev/null +++ b/src/entities/nativeToken.ts @@ -0,0 +1,33 @@ +import { Address } from 'viem'; +import { Token } from './token'; + +/** + * NativeToken extends BaseToken and adds mandatory wrapped token functionality + * This class is specifically designed for native tokens that always have a wrapped version + */ +export class NativeToken extends Token { + public readonly wrapped: Address; + + public constructor( + chainId: number, + address: Address, + decimals: number, + wrapped: Address, + symbol?: string, + name?: string, + ) { + // Call parent constructor with core properties + super(chainId, address, decimals, symbol, name); + + // Set wrapped address (always mandatory for native tokens) + this.wrapped = wrapped.toLowerCase() as Address; + } + + public isWrappedEqual(token: NativeToken) { + return this.chainId === token.chainId && this.wrapped === token.wrapped; + } + + public isWrapped(token: Token) { + return this.chainId === token.chainId && this.wrapped === token.address; + } +} diff --git a/src/entities/swap/swaps/v2/auraBalSwaps/joinPool.ts b/src/entities/swap/swaps/v2/auraBalSwaps/joinPool.ts index 6f79f3c34..28d07b009 100644 --- a/src/entities/swap/swaps/v2/auraBalSwaps/joinPool.ts +++ b/src/entities/swap/swaps/v2/auraBalSwaps/joinPool.ts @@ -31,7 +31,7 @@ export function encodeJoinData( ); const useNativeAsset = - wethIsEth && token.isUnderlyingEqual(NATIVE_ASSETS[ChainId.MAINNET]); + wethIsEth && NATIVE_ASSETS[ChainId.MAINNET].isWrapped(token); const maxAmountsIn = Array(balWethAssets.length).fill(0n); maxAmountsIn[tokenInIndex] = inputAmount; diff --git a/src/entities/token.ts b/src/entities/token.ts index d306238cb..9f3c39a63 100644 --- a/src/entities/token.ts +++ b/src/entities/token.ts @@ -1,12 +1,12 @@ import { Address } from 'viem'; import { InputToken } from '../types'; + export class Token { public readonly chainId: number; public readonly address: Address; public readonly decimals: number; public readonly symbol?: string; public readonly name?: string; - public readonly wrapped: Address; public constructor( chainId: number, @@ -14,7 +14,6 @@ export class Token { decimals: number, symbol?: string, name?: string, - wrapped?: Address, ) { this.chainId = chainId; // Addresses are always lowercased for speed @@ -22,19 +21,12 @@ export class Token { this.decimals = decimals; this.symbol = symbol; this.name = name; - this.wrapped = ( - wrapped ? wrapped.toLowerCase() : address.toLowerCase() - ) as Address; } public isEqual(token: Token) { return this.chainId === token.chainId && this.address === token.address; } - public isUnderlyingEqual(token: Token) { - return this.chainId === token.chainId && this.wrapped === token.wrapped; - } - public isSameAddress(address: Address) { return this.address === address.toLowerCase(); } diff --git a/src/entities/utils/getValue.ts b/src/entities/utils/getValue.ts index 5c3ea63bc..1e8dac831 100644 --- a/src/entities/utils/getValue.ts +++ b/src/entities/utils/getValue.ts @@ -9,7 +9,7 @@ export const getValue = ( if (wethIsEth) { value = amountsIn.find((a) => - a.token.isUnderlyingEqual(NATIVE_ASSETS[a.token.chainId]), + NATIVE_ASSETS[a.token.chainId].isWrapped(a.token), )?.amount ?? 0n; } return value; diff --git a/src/entities/utils/replaceWrapped.ts b/src/entities/utils/replaceWrapped.ts index ada8cc136..38433db58 100644 --- a/src/entities/utils/replaceWrapped.ts +++ b/src/entities/utils/replaceWrapped.ts @@ -3,7 +3,7 @@ import { NATIVE_ASSETS, ZERO_ADDRESS } from '../../utils'; export function replaceWrapped(tokens: Token[], chainId: number): Token[] { return tokens.map((token) => { - if (token.isUnderlyingEqual(NATIVE_ASSETS[chainId])) { + if (NATIVE_ASSETS[chainId].isWrapped(token)) { return new Token(chainId, ZERO_ADDRESS, 18); } return token; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a26f0b4ba..303ff8c08 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,5 +1,5 @@ import { Address, Chain } from 'viem'; -import { Token } from '../entities/token'; +import { NativeToken } from '../entities/nativeToken'; import { arbitrum, avalanche, @@ -144,133 +144,133 @@ export const PERMIT2: Record = { }; export const NATIVE_ASSETS = { - [ChainId.ARBITRUM_ONE]: new Token( + [ChainId.ARBITRUM_ONE]: new NativeToken( ChainId.ARBITRUM_ONE, NATIVE_ADDRESS, 18, + '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 'ETH', 'Ether', - '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', ), - [ChainId.BASE]: new Token( + [ChainId.BASE]: new NativeToken( ChainId.BASE, NATIVE_ADDRESS, 18, + '0x4200000000000000000000000000000000000006', 'ETH', 'Ether', - '0x4200000000000000000000000000000000000006', ), - [ChainId.FANTOM]: new Token( + [ChainId.FANTOM]: new NativeToken( ChainId.FANTOM, NATIVE_ADDRESS, 18, + '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83', 'FANTOM', 'Fantom', - '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83', ), - [ChainId.FRAXTAL]: new Token( + [ChainId.FRAXTAL]: new NativeToken( ChainId.FRAXTAL, NATIVE_ADDRESS, 18, + '0xfc00000000000000000000000000000000000006', 'FRAXTAL', 'Fraxtal', - '0xfc00000000000000000000000000000000000006', ), - [ChainId.GNOSIS_CHAIN]: new Token( + [ChainId.GNOSIS_CHAIN]: new NativeToken( ChainId.GNOSIS_CHAIN, NATIVE_ADDRESS, 18, + '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d', 'xDAI', 'xDAI', - '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d', ), - [ChainId.GOERLI]: new Token( + [ChainId.GOERLI]: new NativeToken( ChainId.GOERLI, NATIVE_ADDRESS, 18, + '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', 'ETH', 'Ether', - '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', ), - [ChainId.MAINNET]: new Token( + [ChainId.MAINNET]: new NativeToken( ChainId.MAINNET, NATIVE_ADDRESS, 18, + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'ETH', 'Ether', - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', ), - [ChainId.MODE]: new Token( + [ChainId.MODE]: new NativeToken( ChainId.MODE, NATIVE_ADDRESS, 18, + '0x4200000000000000000000000000000000000006', 'ETH', 'Ether', - '0x4200000000000000000000000000000000000006', ), - [ChainId.OPTIMISM]: new Token( + [ChainId.OPTIMISM]: new NativeToken( ChainId.OPTIMISM, NATIVE_ADDRESS, 18, + '0x4200000000000000000000000000000000000006', 'ETH', 'Ether', - '0x4200000000000000000000000000000000000006', ), - [ChainId.POLYGON]: new Token( + [ChainId.POLYGON]: new NativeToken( ChainId.POLYGON, NATIVE_ADDRESS, 18, + '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 'MATIC', 'Matic', - '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', ), - [ChainId.SEPOLIA]: new Token( + [ChainId.SEPOLIA]: new NativeToken( ChainId.SEPOLIA, NATIVE_ADDRESS, 18, + '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', 'ETH', 'Ether', - '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', ), - [ChainId.AVALANCHE]: new Token( + [ChainId.AVALANCHE]: new NativeToken( ChainId.AVALANCHE, NATIVE_ADDRESS, 18, + '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', 'AVAX', 'Avax', - '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', ), - [ChainId.ZKEVM]: new Token( + [ChainId.ZKEVM]: new NativeToken( ChainId.ZKEVM, NATIVE_ADDRESS, 18, + '0xa2036f0538221a77a3937f1379699f44945018d0', 'MATIC', 'Matic', - '0xa2036f0538221a77a3937f1379699f44945018d0', ), - [ChainId.SONIC]: new Token( + [ChainId.SONIC]: new NativeToken( ChainId.SONIC, NATIVE_ADDRESS, 18, + '0x039e2fb66102314ce7b64ce5ce3e5183bc94ad38', 'S', 'Sonic', - '0x039e2fb66102314ce7b64ce5ce3e5183bc94ad38', ), - [ChainId.HYPEREVM]: new Token( + [ChainId.HYPEREVM]: new NativeToken( ChainId.HYPEREVM, HYPEREVM_NATIVE_ADDRESS, 18, + '0x5555555555555555555555555555555555555555', 'HYPE', 'Hype', - '0x5555555555555555555555555555555555555555', ), - [ChainId.PLASMA]: new Token( + [ChainId.PLASMA]: new NativeToken( ChainId.PLASMA, NATIVE_ADDRESS, 18, + '0x6100E367285b01F48D07953803A2d8dCA5D19873', 'XPL', 'Xpl', - '0x6100E367285b01F48D07953803A2d8dCA5D19873', ), }; diff --git a/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts b/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts index e769c2c4c..02d405042 100644 --- a/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts +++ b/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts @@ -142,17 +142,14 @@ async function testAuraBalSwap( expect(transactionReceipt.status).to.equal('success'); - if ( - wethIsEth && - tokenIn.isUnderlyingEqual(NATIVE_ASSETS[ChainId.MAINNET]) - ) { + if (wethIsEth && NATIVE_ASSETS[ChainId.MAINNET].isWrapped(tokenIn)) { expect(queryOutput.inputAmount.amount).to.equal(balanceDeltas[2]); expect(queryOutput.expectedAmountOut.amount).to.equal(balanceDeltas[1]); expect(call.value).to.eq(queryOutput.inputAmount.amount); expect(balanceDeltas[0]).to.eq(0n); } else if ( wethIsEth && - tokenOut.isUnderlyingEqual(NATIVE_ASSETS[ChainId.MAINNET]) + NATIVE_ASSETS[ChainId.MAINNET].isWrapped(tokenOut) ) { expect(queryOutput.inputAmount.amount).to.equal(balanceDeltas[0]); expect(queryOutput.expectedAmountOut.amount).to.equal(balanceDeltas[2]); diff --git a/test/lib/utils/addLiquidityHelper.ts b/test/lib/utils/addLiquidityHelper.ts index 8b1c2fe3e..100fb7db4 100644 --- a/test/lib/utils/addLiquidityHelper.ts +++ b/test/lib/utils/addLiquidityHelper.ts @@ -15,7 +15,6 @@ import { NATIVE_ASSETS, PoolState, Slippage, - Token, TokenAmount, VAULT_V2, Permit2Helper, @@ -23,6 +22,7 @@ import { isSameAddress, PublicWalletClient, missingParameterError, + Token, } from 'src'; import { getTokensForBalanceCheck } from './getTokensForBalanceCheck'; import { TxOutput, sendTransactionGetBalances } from './helper'; diff --git a/test/v2/removeLiquidity/composableStable.integration.test.ts b/test/v2/removeLiquidity/composableStable.integration.test.ts index a18ca8b65..885862074 100644 --- a/test/v2/removeLiquidity/composableStable.integration.test.ts +++ b/test/v2/removeLiquidity/composableStable.integration.test.ts @@ -16,7 +16,6 @@ import { RemoveLiquidityUnbalancedInput, RemoveLiquidityKind, Slippage, - Token, PoolState, RemoveLiquidity, Address, @@ -28,6 +27,7 @@ import { InputAmount, PoolType, RemoveLiquiditySingleTokenExactOutInput, + Token, } from '../../../src'; import { forkSetup } from '../../lib/utils/helper'; import { RemoveLiquidityTxInput } from '../../lib/utils/types';