From db2cfb61c6638ff377a56266601fc580c39bc4fe Mon Sep 17 00:00:00 2001 From: Jintu Das Date: Wed, 4 Mar 2026 12:20:31 +0530 Subject: [PATCH] feat: extend stargate chain support --- .../src/adapters/stargate/stargate.ts | 55 +++-- .../rebalance/src/adapters/stargate/types.ts | 53 +++++ .../test/adapters/stargate/stargate.spec.ts | 203 +++++++++++++++++- 3 files changed, 289 insertions(+), 22 deletions(-) diff --git a/packages/adapters/rebalance/src/adapters/stargate/stargate.ts b/packages/adapters/rebalance/src/adapters/stargate/stargate.ts index 5f34e356..ec3ffadb 100644 --- a/packages/adapters/rebalance/src/adapters/stargate/stargate.ts +++ b/packages/adapters/rebalance/src/adapters/stargate/stargate.ts @@ -14,9 +14,9 @@ import { jsonifyError, Logger } from '@mark/logger'; import { BridgeAdapter, MemoizedTransactionRequest, RebalanceTransactionMemo } from '../../types'; import { STARGATE_OFT_ABI } from './abi'; import { - STARGATE_USDT_POOL_ETH, USDT_ETH, - LZ_ENDPOINT_ID_TON, + STARGATE_POOL_ADDRESSES, + CHAIN_ID_TO_LZ_ENDPOINT, StargateSendParam, StargateMessagingFee, LzMessageStatus, @@ -27,6 +27,7 @@ import { tonAddressToBytes32, USDT_TON_STARGATE, } from './types'; +import { getDestinationAssetAddress } from '../../shared/asset'; // LayerZero Scan API base URL const LZ_SCAN_API_URL = 'https://scan.layerzero-api.com'; @@ -56,6 +57,26 @@ export class StargateBridgeAdapter implements BridgeAdapter { return SupportedBridge.Stargate; } + /** + * Resolve the destination token address for a route + * For TON, uses the Stargate-specific hex address; for EVM chains, looks up via chain config + */ + private resolveDstToken(route: RebalanceRoute): string | null { + if (route.destination === 30826) return USDT_TON_STARGATE; + return ( + getDestinationAssetAddress(route.asset, route.origin, route.destination, this.chains, this.logger) ?? null + ); + } + + /** + * Get the LayerZero endpoint ID for a chain + */ + protected getLzEndpointId(chainId: number): number { + const eid = CHAIN_ID_TO_LZ_ENDPOINT[chainId]; + if (eid === undefined) throw new Error(`No LayerZero endpoint ID configured for chain ${chainId}`); + return eid; + } + /** * Get the expected amount received after bridging via Stargate * @@ -99,8 +120,11 @@ export class StargateBridgeAdapter implements BridgeAdapter { return null; } - // For TON destination, use the Stargate-specific token address format - const dstToken = route.destination === 30826 ? USDT_TON_STARGATE : route.asset; + const dstToken = this.resolveDstToken(route); + if (!dstToken) { + this.logger.warn('Could not resolve destination token', { route }); + return null; + } // Use a placeholder address for quote - actual address will be used in send() const placeholderAddress = '0x1234567890abcdef1234567890abcdef12345678'; @@ -162,7 +186,7 @@ export class StargateBridgeAdapter implements BridgeAdapter { // Prepare send parameters for quote const sendParam: StargateSendParam = { - dstEid: LZ_ENDPOINT_ID_TON, + dstEid: this.getLzEndpointId(route.destination), to: pad('0x0000000000000000000000000000000000000000' as `0x${string}`, { size: 32 }), amountLD: BigInt(amount), minAmountLD: BigInt(0), // Will be calculated after quote @@ -304,8 +328,11 @@ export class StargateBridgeAdapter implements BridgeAdapter { return null; } - // For TON destination, use the Stargate-specific token address format - const dstToken = route.destination === 30826 ? USDT_TON_STARGATE : route.asset; + const dstToken = this.resolveDstToken(route); + if (!dstToken) { + this.logger.warn('Could not resolve destination token', { route }); + return null; + } // Calculate minimum amount with slippage (0.5%) const slippageBps = 50n; @@ -471,7 +498,7 @@ export class StargateBridgeAdapter implements BridgeAdapter { // Prepare send parameters const sendParam: StargateSendParam = { - dstEid: LZ_ENDPOINT_ID_TON, + dstEid: this.getLzEndpointId(route.destination), to: recipientBytes32, amountLD: BigInt(amount), minAmountLD: minAmount, @@ -745,13 +772,11 @@ export class StargateBridgeAdapter implements BridgeAdapter { * Get the Stargate pool address for an asset */ protected getPoolAddress(asset: string, chainId: number): `0x${string}` { - // For USDT on Ethereum mainnet - if (asset.toLowerCase() === USDT_ETH.toLowerCase() && chainId === 1) { - return STARGATE_USDT_POOL_ETH; - } - - // Add more pool addresses as needed - throw new Error(`No Stargate pool found for asset ${asset} on chain ${chainId}`); + const chainPools = STARGATE_POOL_ADDRESSES[chainId]; + if (!chainPools) throw new Error(`No Stargate pools configured for chain ${chainId}`); + const pool = chainPools[asset.toLowerCase()]; + if (!pool) throw new Error(`No Stargate pool found for asset ${asset} on chain ${chainId}`); + return pool; } /** diff --git a/packages/adapters/rebalance/src/adapters/stargate/types.ts b/packages/adapters/rebalance/src/adapters/stargate/types.ts index 28ba058b..b13e36bf 100644 --- a/packages/adapters/rebalance/src/adapters/stargate/types.ts +++ b/packages/adapters/rebalance/src/adapters/stargate/types.ts @@ -18,14 +18,47 @@ export const STARGATE_USDT_POOL_ETH = '0x933597a323Eb81cAe705C5bC29985172fd5A397 // USDT token on Ethereum mainnet export const USDT_ETH = '0xdAC17F958D2ee523a2206206994597C13D831ec7' as `0x${string}`; +// Token addresses - Base +export const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as `0x${string}`; + +// Token addresses - Arbitrum +export const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as `0x${string}`; +export const USDT_ARB = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' as `0x${string}`; + +// Token addresses - Mantle +export const USDC_MANTLE = '0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9' as `0x${string}`; +export const USDT_MANTLE = '0x201EBa5CC46D216Ce6DC03F6a759e8E766e956aE' as `0x${string}`; + +// Stargate Pool addresses - Base +export const STARGATE_USDC_POOL_BASE = '0x27a16dc786820B16E5c9028b75B99F6f604b5d26' as `0x${string}`; + +// Stargate Pool addresses - Arbitrum +export const STARGATE_USDC_POOL_ARB = '0xe8CDF27AcD73a434D661C84887215F7598e7d0d3' as `0x${string}`; +export const STARGATE_USDT_POOL_ARB = '0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0' as `0x${string}`; + +// Stargate Pool addresses - Mantle +export const STARGATE_USDC_POOL_MANTLE = '0xAc290Ad4e0c891FDc295ca4F0a6214cf6dC6acDC' as `0x${string}`; +export const STARGATE_USDT_POOL_MANTLE = '0xB715B85682B731dB9D5063187C450095c91C57FC' as `0x${string}`; + // ============================================================================ // LayerZero V2 Endpoint IDs // Reference: https://docs.layerzero.network/v2/deployments/chains // ============================================================================ export const LZ_ENDPOINT_ID_ETH = 30101; // Ethereum mainnet +export const LZ_ENDPOINT_ID_BASE = 30184; // Base mainnet +export const LZ_ENDPOINT_ID_ARB = 30110; // Arbitrum mainnet +export const LZ_ENDPOINT_ID_MANTLE = 30181; // Mantle mainnet export const LZ_ENDPOINT_ID_TON = 30826; // TON mainnet +export const CHAIN_ID_TO_LZ_ENDPOINT: Record = { + 1: LZ_ENDPOINT_ID_ETH, + 8453: LZ_ENDPOINT_ID_BASE, + 42161: LZ_ENDPOINT_ID_ARB, + 5000: LZ_ENDPOINT_ID_MANTLE, + 30826: LZ_ENDPOINT_ID_TON, +}; + // ============================================================================ // Chain IDs // ============================================================================ @@ -36,6 +69,23 @@ export const TAC_CHAIN_ID = 239; // TON does not have an EVM chain ID, we use LayerZero endpoint ID export const TON_CHAIN_ID = 30826; +// ============================================================================ +// Stargate Pool Addresses +// ============================================================================ + +export const STARGATE_POOL_ADDRESSES: Record> = { + 1: { [USDT_ETH.toLowerCase()]: STARGATE_USDT_POOL_ETH }, + 8453: { [USDC_BASE.toLowerCase()]: STARGATE_USDC_POOL_BASE }, + 42161: { + [USDC_ARB.toLowerCase()]: STARGATE_USDC_POOL_ARB, + [USDT_ARB.toLowerCase()]: STARGATE_USDT_POOL_ARB, + }, + 5000: { + [USDC_MANTLE.toLowerCase()]: STARGATE_USDC_POOL_MANTLE, + [USDT_MANTLE.toLowerCase()]: STARGATE_USDT_POOL_MANTLE, + }, +}; + // ============================================================================ // Stargate API Configuration // Reference: https://stargate.finance/api/v1/quotes @@ -122,6 +172,9 @@ export const USDT_TON_STARGATE = '0xb113a994b5024a16719f69139328eb759596c38a25f5 */ export const STARGATE_CHAIN_NAMES: Record = { 1: 'ethereum', + 8453: 'base', + 42161: 'arbitrum', + 5000: 'mantle', 30826: 'ton', 239: 'tac', }; diff --git a/packages/adapters/rebalance/test/adapters/stargate/stargate.spec.ts b/packages/adapters/rebalance/test/adapters/stargate/stargate.spec.ts index e6a30af2..f69e28ab 100644 --- a/packages/adapters/rebalance/test/adapters/stargate/stargate.spec.ts +++ b/packages/adapters/rebalance/test/adapters/stargate/stargate.spec.ts @@ -4,10 +4,23 @@ import { ChainConfiguration, SupportedBridge, RebalanceRoute, axiosGet, cleanupH import { jsonifyError, Logger } from '@mark/logger'; import { TransactionReceipt } from 'viem'; import { StargateBridgeAdapter } from '../../../src/adapters/stargate/stargate'; -import { - STARGATE_USDT_POOL_ETH, - USDT_ETH, - LZ_ENDPOINT_ID_TON, +import { + STARGATE_USDT_POOL_ETH, + USDT_ETH, + USDC_BASE, + STARGATE_USDC_POOL_BASE, + USDC_ARB, + USDT_ARB, + STARGATE_USDC_POOL_ARB, + STARGATE_USDT_POOL_ARB, + USDC_MANTLE, + USDT_MANTLE, + STARGATE_USDC_POOL_MANTLE, + STARGATE_USDT_POOL_MANTLE, + LZ_ENDPOINT_ID_TON, + LZ_ENDPOINT_ID_BASE, + LZ_ENDPOINT_ID_ARB, + LZ_ENDPOINT_ID_MANTLE, LzMessageStatus, STARGATE_CHAIN_NAMES, USDT_TON_STARGATE, @@ -80,6 +93,10 @@ class TestStargateBridgeAdapter extends StargateBridgeAdapter { public callGetPublicClient(chainId: number) { return this.getPublicClient(chainId); } + + public callGetLzEndpointId(chainId: number) { + return this.getLzEndpointId(chainId); + } } // Mock the Logger @@ -91,6 +108,9 @@ const mockLogger = { } as unknown as jest.Mocked; // Mock chain configurations (no real credentials) +const USDC_TICKER_HASH = '0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa'; +const USDT_TICKER_HASH = '0x8b1a1d9c2b109e527c9134b25b1a1833b16b6594f92daa9f6d9b7a6024bce9d0'; + const mockChains: Record = { '1': { assets: [ @@ -98,7 +118,7 @@ const mockChains: Record = { address: USDT_ETH, symbol: 'USDT', decimals: 6, - tickerHash: '0x8b1a1d9c2b109e527c9134b25b1a1833b16b6594f92daa9f6d9b7a6024bce9d0', + tickerHash: USDT_TICKER_HASH, isNative: false, balanceThreshold: '0', }, @@ -112,6 +132,82 @@ const mockChains: Record = { multicall3: '0xcA11bde05977b3631167028862bE2a173976CA11', }, }, + '8453': { + assets: [ + { + address: USDC_BASE, + symbol: 'USDC', + decimals: 6, + tickerHash: USDC_TICKER_HASH, + isNative: false, + balanceThreshold: '0', + }, + ], + providers: ['https://mock-base-rpc.example.com'], + invoiceAge: 3600, + gasThreshold: '5000000000000000', + deployments: { + everclear: '0xMockEverclearAddress', + permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3', + multicall3: '0xcA11bde05977b3631167028862bE2a173976CA11', + }, + }, + '42161': { + assets: [ + { + address: USDC_ARB, + symbol: 'USDC', + decimals: 6, + tickerHash: USDC_TICKER_HASH, + isNative: false, + balanceThreshold: '0', + }, + { + address: USDT_ARB, + symbol: 'USDT', + decimals: 6, + tickerHash: USDT_TICKER_HASH, + isNative: false, + balanceThreshold: '0', + }, + ], + providers: ['https://mock-arb-rpc.example.com'], + invoiceAge: 3600, + gasThreshold: '5000000000000000', + deployments: { + everclear: '0xMockEverclearAddress', + permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3', + multicall3: '0xcA11bde05977b3631167028862bE2a173976CA11', + }, + }, + '5000': { + assets: [ + { + address: USDC_MANTLE, + symbol: 'USDC', + decimals: 6, + tickerHash: USDC_TICKER_HASH, + isNative: false, + balanceThreshold: '0', + }, + { + address: USDT_MANTLE, + symbol: 'USDT', + decimals: 6, + tickerHash: USDT_TICKER_HASH, + isNative: false, + balanceThreshold: '0', + }, + ], + providers: ['https://mock-mantle-rpc.example.com'], + invoiceAge: 3600, + gasThreshold: '5000000000000000', + deployments: { + everclear: '0xMockEverclearAddress', + permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3', + multicall3: '0xcA11bde05977b3631167028862bE2a173976CA11', + }, + }, }; describe('StargateBridgeAdapter', () => { @@ -279,7 +375,32 @@ describe('StargateBridgeAdapter', () => { expect(result).toBe(STARGATE_USDT_POOL_ETH); }); - it('should throw error for unsupported asset', () => { + it('should return USDC pool address for Base', () => { + const result = adapter.callGetPoolAddress(USDC_BASE, 8453); + expect(result).toBe(STARGATE_USDC_POOL_BASE); + }); + + it('should return USDC pool address for Arbitrum', () => { + const result = adapter.callGetPoolAddress(USDC_ARB, 42161); + expect(result).toBe(STARGATE_USDC_POOL_ARB); + }); + + it('should return USDT pool address for Arbitrum', () => { + const result = adapter.callGetPoolAddress(USDT_ARB, 42161); + expect(result).toBe(STARGATE_USDT_POOL_ARB); + }); + + it('should return USDC pool address for Mantle', () => { + const result = adapter.callGetPoolAddress(USDC_MANTLE, 5000); + expect(result).toBe(STARGATE_USDC_POOL_MANTLE); + }); + + it('should return USDT pool address for Mantle', () => { + const result = adapter.callGetPoolAddress(USDT_MANTLE, 5000); + expect(result).toBe(STARGATE_USDT_POOL_MANTLE); + }); + + it('should throw error for unsupported asset on known chain', () => { expect(() => adapter.callGetPoolAddress('0xUnknownAsset', 1)).toThrow( 'No Stargate pool found for asset 0xUnknownAsset on chain 1' ); @@ -287,7 +408,7 @@ describe('StargateBridgeAdapter', () => { it('should throw error for unsupported chain', () => { expect(() => adapter.callGetPoolAddress(USDT_ETH, 999)).toThrow( - /No Stargate pool found/ + 'No Stargate pools configured for chain 999' ); }); }); @@ -841,9 +962,77 @@ describe('StargateBridgeAdapter', () => { expect(STARGATE_CHAIN_NAMES[1]).toBe('ethereum'); }); + it('should have mapping for base', () => { + expect(STARGATE_CHAIN_NAMES[8453]).toBe('base'); + }); + + it('should have mapping for arbitrum', () => { + expect(STARGATE_CHAIN_NAMES[42161]).toBe('arbitrum'); + }); + + it('should have mapping for mantle', () => { + expect(STARGATE_CHAIN_NAMES[5000]).toBe('mantle'); + }); + it('should have mapping for TON', () => { expect(STARGATE_CHAIN_NAMES[30826]).toBe('ton'); }); }); + + describe('getLzEndpointId', () => { + it('should return correct endpoint ID for Ethereum', () => { + expect(adapter.callGetLzEndpointId(1)).toBe(30101); + }); + + it('should return correct endpoint ID for Base', () => { + expect(adapter.callGetLzEndpointId(8453)).toBe(LZ_ENDPOINT_ID_BASE); + }); + + it('should return correct endpoint ID for Arbitrum', () => { + expect(adapter.callGetLzEndpointId(42161)).toBe(LZ_ENDPOINT_ID_ARB); + }); + + it('should return correct endpoint ID for Mantle', () => { + expect(adapter.callGetLzEndpointId(5000)).toBe(LZ_ENDPOINT_ID_MANTLE); + }); + + it('should return correct endpoint ID for TON', () => { + expect(adapter.callGetLzEndpointId(30826)).toBe(LZ_ENDPOINT_ID_TON); + }); + + it('should throw for unknown chain', () => { + expect(() => adapter.callGetLzEndpointId(99999)).toThrow( + 'No LayerZero endpoint ID configured for chain 99999' + ); + }); + }); + + describe('EVM to EVM route (Arb USDC → Mantle)', () => { + it('should get API quote with resolved destination token', async () => { + const route: RebalanceRoute = { + origin: 42161, + destination: 5000, + asset: USDC_ARB, // USDC on Arb + }; + + const mockApiResponse = { + quotes: [{ + route: { bridgeName: 'stargate' }, + dstAmount: '995000', + }], + }; + (axiosGet as jest.Mock).mockResolvedValue({ data: mockApiResponse } as never); + + const result = await adapter.callGetApiQuote('1000000', route); + expect(result).toBe('995000'); + + // Verify the API was called with the Mantle USDC address as dstToken + const callUrl = (axiosGet as jest.Mock).mock.calls[0][0] as string; + expect(callUrl).toContain('srcChainKey=arbitrum'); + expect(callUrl).toContain('dstChainKey=mantle'); + // dstToken should be the Mantle USDC address (resolved via chain config) + expect(callUrl.toLowerCase()).toContain(`dsttoken=${USDC_MANTLE.toLowerCase()}`); + }); + }); });