diff --git a/packages/internal/dex/sdk/src/config/config.test.ts b/packages/internal/dex/sdk/src/config/config.test.ts index f430382484..11c4a90dd4 100644 --- a/packages/internal/dex/sdk/src/config/config.test.ts +++ b/packages/internal/dex/sdk/src/config/config.test.ts @@ -6,6 +6,26 @@ import { ExchangeConfiguration, ExchangeContracts } from './index'; import { IMMUTABLE_TESTNET_CHAIN_ID } from '../constants/chains'; describe('ExchangeConfiguration', () => { + const chainId = 999999999; + // This list can be updated with any Tokens that are deployed to the chain being configured + // These tokens will be used to find available pools for a swap + const commonRoutingTokensSingle: TokenInfo[] = [ + { + chainId, + address: '0x12958b06abdf2701ace6ceb3ce0b8b1ce11e0851', + decimals: 18, + symbol: 'FUN', + name: 'The Fungibles Token', + }, + ]; + + const contractOverrides: ExchangeContracts = { + multicall: test.TEST_MULTICALL_ADDRESS, + coreFactory: test.TEST_V3_CORE_FACTORY_ADDRESS, + quoterV2: test.TEST_QUOTER_ADDRESS, + peripheryRouter: test.TEST_PERIPHERY_ROUTER_ADDRESS, + }; + describe('when given sandbox environment with supported chain id', () => { it('should create successfully', () => { const baseConfig = new ImmutableConfiguration({ @@ -42,19 +62,12 @@ describe('ExchangeConfiguration', () => { describe('when given overrides', () => { it('should override any configuration with given values', () => { - const chainId = 999999999; + const dummyFeeRecipient = '0xb18c44b211065E69844FbA9AE146DA362104AfBf'; const immutableConfig = new ImmutableConfiguration({ environment: Environment.SANDBOX, }); // environment isn't used because we override all of the config values - const contractOverrides: ExchangeContracts = { - multicall: test.TEST_MULTICALL_ADDRESS, - coreFactory: test.TEST_V3_CORE_FACTORY_ADDRESS, - quoterV2: test.TEST_QUOTER_ADDRESS, - peripheryRouter: test.TEST_PERIPHERY_ROUTER_ADDRESS, - }; - // This list can be updated with any Tokens that are deployed to the chain being configured // These tokens will be used to find available pools for a swap const commonRoutingTokens: TokenInfo[] = [ @@ -81,12 +94,20 @@ describe('ExchangeConfiguration', () => { }, ]; + const secondaryFees = [ + { + feeRecipient: dummyFeeRecipient, + feeBasisPoints: 100, + }, + ]; + const rpcURL = 'https://anrpcurl.net'; const overrides: ExchangeOverrides = { rpcURL, exchangeContracts: contractOverrides, commonRoutingTokens, nativeToken: test.IMX_TEST_TOKEN, + secondaryFees, }; const config = new ExchangeConfiguration({ @@ -112,29 +133,76 @@ describe('ExchangeConfiguration', () => { expect(config.chain.commonRoutingTokens[2].address.toLowerCase()) .toEqual(commonRoutingTokens[2].address.toLowerCase()); + + expect(config.secondaryFees).toBeDefined(); + if (!config.secondaryFees) { + // This should never happen + throw new Error('Secondary fees should be defined'); + } + expect(config.secondaryFees[0].feeRecipient.toLowerCase()) + .toEqual(dummyFeeRecipient.toLowerCase()); + expect(config.secondaryFees[0].feeBasisPoints.toString()) + .toEqual(secondaryFees[0].feeBasisPoints.toString()); }); it('should throw when missing configuration', () => { - const chainId = 999999999; - const immutableConfig = new ImmutableConfiguration({ environment: Environment.SANDBOX, }); // environment isn't used because we override all of the config values - const contractOverrides: ExchangeContracts = { + const invalidContractOverrides: ExchangeContracts = { multicall: '', coreFactory: test.TEST_V3_CORE_FACTORY_ADDRESS, quoterV2: test.TEST_QUOTER_ADDRESS, peripheryRouter: test.TEST_PERIPHERY_ROUTER_ADDRESS, }; - const commonRoutingTokens: TokenInfo[] = [ + const rpcURL = 'https://anrpcurl.net'; + const overrides: ExchangeOverrides = { + rpcURL, + exchangeContracts: invalidContractOverrides, + commonRoutingTokens: commonRoutingTokensSingle, + nativeToken: test.IMX_TEST_TOKEN, + }; + + expect(() => new ExchangeConfiguration({ + chainId, + baseConfig: immutableConfig, + overrides, + })).toThrow(new InvalidConfigurationError('Invalid exchange contract address for multicall')); + }); + + it('show throw when given an invalid RPC URL', () => { + const immutableConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + }); // environment isn't used because we override all of the config values + + const rpcURL = ''; + const overrides: ExchangeOverrides = { + rpcURL, + exchangeContracts: contractOverrides, + commonRoutingTokens: commonRoutingTokensSingle, + nativeToken: test.IMX_TEST_TOKEN, + }; + + expect(() => new ExchangeConfiguration({ + chainId, + baseConfig: immutableConfig, + overrides, + })).toThrow(new InvalidConfigurationError('Missing override: rpcURL')); + }); + + it('should throw when given an invalid secondary fee recipient address', () => { + const invalidFeeRecipient = '0x18c44b211065E69844FbA9AE146DA362104AfBf'; // too short + + const immutableConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + }); // environment isn't used because we override all of the config values + + const secondaryFees = [ { - chainId, - address: '0x12958b06abdf2701ace6ceb3ce0b8b1ce11e0851', - decimals: 18, - symbol: 'FUN', - name: 'The Fungibles Token', + feeRecipient: invalidFeeRecipient, + feeBasisPoints: 100, }, ]; @@ -142,54 +210,72 @@ describe('ExchangeConfiguration', () => { const overrides: ExchangeOverrides = { rpcURL, exchangeContracts: contractOverrides, - commonRoutingTokens, + commonRoutingTokens: commonRoutingTokensSingle, nativeToken: test.IMX_TEST_TOKEN, + secondaryFees, }; expect(() => new ExchangeConfiguration({ chainId, baseConfig: immutableConfig, overrides, - })).toThrow(new InvalidConfigurationError('Invalid exchange contract address for multicall')); + })).toThrow(new InvalidConfigurationError( + `Invalid secondary fee recipient address: ${secondaryFees[0].feeRecipient}`, + )); }); - it('show throw when given an invalid RPC URL', () => { - const chainId = 999999999; + it('should throw when given invalid secondary fee basis points', () => { + const dummyFeeRecipient = '0xb18c44b211065E69844FbA9AE146DA362104AfBf'; const immutableConfig = new ImmutableConfiguration({ environment: Environment.SANDBOX, }); // environment isn't used because we override all of the config values - const contractOverrides: ExchangeContracts = { - multicall: '', - coreFactory: test.TEST_V3_CORE_FACTORY_ADDRESS, - quoterV2: test.TEST_QUOTER_ADDRESS, - peripheryRouter: test.TEST_PERIPHERY_ROUTER_ADDRESS, - }; - - const commonRoutingTokens: TokenInfo[] = [ + const secondaryFees = [ { - chainId, - address: '0x12958b06abdf2701ace6ceb3ce0b8b1ce11e0851', - decimals: 18, - symbol: 'FUN', - name: 'The Fungibles Token', + feeRecipient: dummyFeeRecipient, + feeBasisPoints: 10001, }, ]; - const rpcURL = ''; + const rpcURL = 'https://anrpcurl.net'; const overrides: ExchangeOverrides = { rpcURL, exchangeContracts: contractOverrides, - commonRoutingTokens, + commonRoutingTokens: commonRoutingTokensSingle, nativeToken: test.IMX_TEST_TOKEN, + secondaryFees, }; expect(() => new ExchangeConfiguration({ chainId, baseConfig: immutableConfig, overrides, - })).toThrow(new InvalidConfigurationError('Missing override: rpcURL')); + })).toThrow(new InvalidConfigurationError( + `Invalid secondary fee percentage: ${secondaryFees[0].feeBasisPoints}`, + )); + }); + + it('should not set secondary fees when not given', () => { + const immutableConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + }); // environment isn't used because we override all of the config values + + const rpcURL = 'https://anrpcurl.net'; + const overrides: ExchangeOverrides = { + rpcURL, + exchangeContracts: contractOverrides, + commonRoutingTokens: commonRoutingTokensSingle, + nativeToken: test.IMX_TEST_TOKEN, + }; + + const config = new ExchangeConfiguration({ + chainId, + baseConfig: immutableConfig, + overrides, + }); + + expect(config.secondaryFees).toEqual([]); }); }); }); diff --git a/packages/internal/dex/sdk/src/config/index.ts b/packages/internal/dex/sdk/src/config/index.ts index 35b62dcee0..83ef12c27e 100644 --- a/packages/internal/dex/sdk/src/config/index.ts +++ b/packages/internal/dex/sdk/src/config/index.ts @@ -2,7 +2,7 @@ import { Environment, ImmutableConfiguration } from '@imtbl/config'; import { IMMUTABLE_TESTNET_COMMON_ROUTING_TOKENS, TIMX_IMMUTABLE_TESTNET } from 'constants/tokens'; import { ChainNotSupportedError, InvalidConfigurationError } from 'errors'; import { IMMUTABLE_TESTNET_CHAIN_ID, IMMUTABLE_TESTNET_RPC_URL } from 'constants/chains'; -import { isValidNonZeroAddress } from 'lib'; +import { SecondaryFee, isValidNonZeroAddress } from 'lib'; import { Chain, ExchangeModuleConfiguration, ExchangeOverrides } from '../types'; export type ExchangeContracts = { @@ -39,17 +39,31 @@ export const SUPPORTED_CHAIN_IDS_FOR_ENVIRONMENT: Record { - if (!value) { + const keysToCheck = ['rpcURL', 'exchangeContracts', 'commonRoutingTokens', 'nativeToken'] as const; + for (const key of keysToCheck) { + if (!overrides[key]) { throw new InvalidConfigurationError(`Missing override: ${key}`); } - }); + } Object.entries(overrides.exchangeContracts).forEach(([key, contract]) => { if (!isValidNonZeroAddress(contract)) { throw new InvalidConfigurationError(`Invalid exchange contract address for ${key}`); } }); + + if (!overrides.secondaryFees) { + return; + } + + for (const secondaryFee of overrides.secondaryFees) { + if (!isValidNonZeroAddress(secondaryFee.feeRecipient)) { + throw new InvalidConfigurationError(`Invalid secondary fee recipient address: ${secondaryFee.feeRecipient}`); + } + if (secondaryFee.feeBasisPoints < 0 || secondaryFee.feeBasisPoints > 10000) { + throw new InvalidConfigurationError(`Invalid secondary fee percentage: ${secondaryFee.feeBasisPoints}`); + } + } } /** @@ -62,6 +76,8 @@ export class ExchangeConfiguration { public chain: Chain; + public secondaryFees: SecondaryFee[]; + constructor({ chainId, baseConfig, overrides }: ExchangeModuleConfiguration) { this.baseConfig = baseConfig; @@ -75,8 +91,11 @@ export class ExchangeConfiguration { nativeToken: overrides.nativeToken, }; + this.secondaryFees = overrides.secondaryFees ? overrides.secondaryFees : []; + return; } + this.secondaryFees = []; const chain = SUPPORTED_CHAIN_IDS_FOR_ENVIRONMENT[baseConfig.environment][chainId]; if (!chain) { diff --git a/packages/internal/dex/sdk/src/exchange.ts b/packages/internal/dex/sdk/src/exchange.ts index 289aa96f9a..c69ab2202c 100644 --- a/packages/internal/dex/sdk/src/exchange.ts +++ b/packages/internal/dex/sdk/src/exchange.ts @@ -13,7 +13,9 @@ import { import { Router } from './lib/router'; import { getERC20Decimals, isValidNonZeroAddress } from './lib/utils'; -import { ExchangeModuleConfiguration, TokenInfo, TransactionResponse } from './types'; +import { + ExchangeModuleConfiguration, SecondaryFee, TokenInfo, TransactionResponse, +} from './types'; import { getSwap } from './lib/transactionUtils/swap'; import { ExchangeConfiguration } from './config'; @@ -26,6 +28,8 @@ export class Exchange { private nativeToken: TokenInfo; + private secondaryFees: SecondaryFee[]; + constructor(configuration: ExchangeModuleConfiguration) { const config = new ExchangeConfiguration(configuration); @@ -36,6 +40,8 @@ export class Exchange { config.chain.rpcUrl, ); + this.secondaryFees = config.secondaryFees; + this.router = new Router( this.provider, config.chain.commonRoutingTokens, @@ -212,4 +218,8 @@ export class Exchange { TradeType.EXACT_OUTPUT, ); } + + private thereAreSecondaryFees(): boolean { + return this.secondaryFees.length > 0; + } } diff --git a/packages/internal/dex/sdk/src/types/index.ts b/packages/internal/dex/sdk/src/types/index.ts index 7bede060c9..4c7cf8304c 100644 --- a/packages/internal/dex/sdk/src/types/index.ts +++ b/packages/internal/dex/sdk/src/types/index.ts @@ -18,6 +18,17 @@ export type Chain = { nativeToken: TokenInfo; }; +/** + * Interface representing the secondary fees for a swap + * @property {string} feeRecipient - The fee recipient address + * @property {number} feeBasisPoints - The fee percentage in basis points + * @example 100 basis points = 1% + */ +export type SecondaryFee = { + feeRecipient: string; + feeBasisPoints: number; +}; + /** * Interface representing an amount with the token information * @property {TokenInfo} token - The token information @@ -83,6 +94,7 @@ export interface ExchangeOverrides { exchangeContracts: ExchangeContracts; commonRoutingTokens: TokenInfo[]; nativeToken: TokenInfo; + secondaryFees?: SecondaryFee[]; } export interface ExchangeModuleConfiguration