diff --git a/packages/mcps/vesu/__tests__/e2e/borrow.e2e.test.ts b/packages/mcps/vesu/__tests__/e2e/borrow.e2e.test.ts new file mode 100644 index 00000000..1b1b988a --- /dev/null +++ b/packages/mcps/vesu/__tests__/e2e/borrow.e2e.test.ts @@ -0,0 +1,445 @@ +import { describe, beforeAll, it, expect, afterEach } from '@jest/globals'; +import { + getOnchainRead, + getOnchainWrite, + getDataAsRecord, + getERC20Balance, +} from '@kasarlabs/ask-starknet-core'; +import { depositBorrowPosition } from '../../src/tools/write/deposit_borrow.js'; +import { repayBorrowPosition } from '../../src/tools/write/repay_borrow.js'; +import { getPools } from '../../src/tools/read/getPools.js'; +import { getPositions } from '../../src/tools/read/getPositions.js'; +import { getTokens } from '../../src/tools/read/getTokens.js'; +import { GENESIS_POOLID } from '../../src/lib/constants/index.js'; +import { normalizeAddress, expectLTVWithinTolerance } from '../helpers.js'; + +let accountAddress: string; + +const STRK_ADDRESS = + '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'; + +// Borrow test constants +const BORROW_DEPOSIT_AMOUNT = '400'; +const BORROW_TARGET_LTV = '66'; + +function getDataAsArray( + data: Record | Array | undefined +): Array { + if (!data || !Array.isArray(data)) { + throw new Error('Expected data to be an Array'); + } + return data; +} + +describe('Vesu Borrow E2E Tests', () => { + beforeAll(async () => { + accountAddress = process.env.STARKNET_ACCOUNT_ADDRESS || '0x0'; + + if (accountAddress === '0x0') { + throw new Error( + 'STARKNET_ACCOUNT_ADDRESS must be set in environment variables' + ); + } + + if (!process.env.STARKNET_RPC_URL) { + throw new Error('STARKNET_RPC_URL must be set in environment variables'); + } + }); + + describe('Read Operations', () => { + it('should get pools and verify v2 pool structure', async () => { + const onchainRead = getOnchainRead(); + const result = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const pools = getDataAsArray(result.data); + expect(pools.length).toBe(1); + const pool = pools[0]; + expect(pool.protocolVersion).toBe('v2'); + expect(pool.assets).toBeDefined(); + expect(Array.isArray(pool.assets)).toBe(true); + + // Verify STRK is in the pool + const strkAsset = pool.assets.find( + (asset: any) => asset.symbol.toUpperCase() === 'STRK' + ); + expect(strkAsset).toBeDefined(); + } + }); + + it('should get tokens and verify STRK token details', async () => { + const onchainRead = getOnchainRead(); + const result = await getTokens(onchainRead, { + symbol: 'STRK', + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const tokens = getDataAsArray(result.data); + expect(tokens.length).toBeGreaterThan(0); + const strkToken = tokens.find( + (token: any) => token.symbol.toUpperCase() === 'STRK' + ); + expect(strkToken).toBeDefined(); + expect(normalizeAddress(strkToken.address)).toBe( + normalizeAddress(STRK_ADDRESS) + ); + expect(strkToken.decimals).toBe(18); + } + }); + + it('should get positions filtered by borrow type', async () => { + const onchainRead = getOnchainRead(); + const result = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['borrow'], + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const positions = getDataAsArray(result.data); + // Positions might be empty, that's okay + expect(Array.isArray(positions)).toBe(true); + // All positions should be borrow type if any exist + positions.forEach((pos: any) => { + expect(pos.type).toBe('borrow'); + }); + } + }); + + it('should get all positions without type filter', async () => { + const onchainRead = getOnchainRead(); + const result = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const positions = getDataAsArray(result.data); + expect(Array.isArray(positions)).toBe(true); + } + }); + }); + + describe('Borrow Operations', () => { + it('should deposit STRK as collateral and borrow, then repay all', async () => { + const onchainWrite = getOnchainWrite(); + const provider = onchainWrite.provider; + const accountAddress = onchainWrite.account.address; + + // First, get available tokens in the pool to find a debt token + const onchainRead = getOnchainRead(); + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(poolsResult.status).toBe('success'); + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + const strkAsset = pool.assets.find( + (asset: any) => asset.symbol.toUpperCase() === 'STRK' + ); + + if (!strkAsset) { + throw new Error('STRK not found in pool'); + } + + // Find a different token to borrow (not STRK) + const debtAsset = pool.assets.find( + (asset: any) => asset.symbol.toUpperCase() == 'USDC' + ); + + if (!debtAsset) { + throw new Error('No debt asset found in pool'); + } + + const depositResult = await depositBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + depositAmount: BORROW_DEPOSIT_AMOUNT, + targetLTV: BORROW_TARGET_LTV, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.collateralSymbol).toBe('STRK'); + expect(data.debtSymbol).toBe(debtAsset.symbol); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Verify position exists + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['borrow'], + }); + + expect(positionsResult.status).toBe('success'); + if (positionsResult.status === 'success' && positionsResult.data) { + const positions = getDataAsArray(positionsResult.data); + const borrowPosition = positions.find( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'STRK' && + pos.debt?.symbol.toUpperCase() === debtAsset.symbol.toUpperCase() && + pos.type === 'borrow' + ); + expect(borrowPosition).toBeDefined(); + + // Verify LTV is within ±1% tolerance + if (borrowPosition?.ltv?.current?.value) { + // LTV from API is in format with decimals (e.g., 610000000000000000 = 61%) + // Convert to percentage: divide by 10^16 + const ltvValue = BigInt(borrowPosition.ltv.current.value); + const ltvDecimals = borrowPosition.ltv.current.decimals || 18; + const ltvPercentage = Number(ltvValue) / 10 ** ltvDecimals; + expectLTVWithinTolerance(ltvPercentage, parseInt(BORROW_TARGET_LTV)); + } + } + + // Repay all debt + const repayResult = await repayBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + poolId: GENESIS_POOLID, + }); + + expect(repayResult.status).toBe('success'); + if (repayResult.status === 'success' && repayResult.data) { + const data = getDataAsRecord(repayResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.collateralSymbol).toBe('STRK'); + expect(data.debtSymbol).toBe(debtAsset.symbol); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, 180000); + + it('should deposit STRK, borrow, repay partial, then repay remaining', async () => { + const onchainWrite = getOnchainWrite(); + const accountAddress = onchainWrite.account.address; + + // Get pool to find debt token + const onchainRead = getOnchainRead(); + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(poolsResult.status).toBe('success'); + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + const debtAsset = pool.assets.find( + (asset: any) => asset.symbol.toUpperCase() == 'USDC' + ); + + if (!debtAsset) { + throw new Error('No debt asset found in pool'); + } + + // Deposit and borrow + const depositResult = await depositBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + depositAmount: BORROW_DEPOSIT_AMOUNT, + targetLTV: BORROW_TARGET_LTV, + poolId: GENESIS_POOLID, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Get position to see debt amount + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['borrow'], + }); + + let totalDebt: string | undefined; + if ( + positionsResult.status === 'success' && + positionsResult.data && + Array.isArray(positionsResult.data) + ) { + const positions = positionsResult.data; + const borrowPosition = positions.find( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'STRK' && + pos.debt?.symbol.toUpperCase() === debtAsset.symbol.toUpperCase() && + pos.type === 'borrow' + ); + + // Verify LTV is within ±1% tolerance + if (borrowPosition?.ltv?.current?.value) { + const ltvValue = BigInt(borrowPosition.ltv.current.value); + const ltvDecimals = borrowPosition.ltv.current.decimals || 18; + const ltvPercentage = Number(ltvValue) / 10 ** ltvDecimals; + expectLTVWithinTolerance(ltvPercentage, parseInt(BORROW_TARGET_LTV)); + } + + if (borrowPosition?.debt?.value) { + // Convert debt value to human readable format + const debtValue = BigInt(borrowPosition.debt.value); + const decimals = borrowPosition.debt.decimals || 18; + const divisor = 10n ** BigInt(decimals); + const wholePart = debtValue / divisor; + const fractionalPart = debtValue % divisor; + const fractionalStr = fractionalPart + .toString() + .padStart(decimals, '0') + .replace(/0+$/, ''); + totalDebt = fractionalStr + ? `${wholePart}.${fractionalStr}` + : wholePart.toString(); + } + } + + if (totalDebt) { + // Repay partial (50% of debt) + const partialRepayAmount = (parseFloat(totalDebt) * 0.5).toFixed(6); + + const partialRepayResult = await repayBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + repayAmount: partialRepayAmount, + poolId: GENESIS_POOLID, + }); + + expect(partialRepayResult.status).toBe('success'); + if ( + partialRepayResult.status === 'success' && + partialRepayResult.data + ) { + const data = getDataAsRecord(partialRepayResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + // Repay remaining (all) + const repayAllResult = await repayBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + // repayAmount not provided = repay all + poolId: GENESIS_POOLID, + }); + + expect(repayAllResult.status).toBe('success'); + if (repayAllResult.status === 'success' && repayAllResult.data) { + const data = getDataAsRecord(repayAllResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, 180000); + + it('should handle borrow with insufficient balance gracefully', async () => { + const onchainWrite = getOnchainWrite(); + const onchainRead = getOnchainRead(); + + // Get pool to find debt token + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + const debtAsset = pool.assets.find( + (asset: any) => asset.symbol.toUpperCase() == 'USDC' + ); + + if (!debtAsset) { + throw new Error('No debt asset found in pool'); + } + + const result = await depositBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: debtAsset.symbol, + depositAmount: '1000000000', + targetLTV: '75', + poolId: GENESIS_POOLID, + }); + + expect(result.status).toBe('failure'); + expect(result.error).toBeDefined(); + }, 120000); + }); + + afterEach(async () => { + // Cleanup: repay all remaining borrow positions + try { + const onchainWrite = getOnchainWrite(); + const onchainRead = getOnchainRead(); + const accountAddress = onchainWrite.account.address; + + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['borrow'], + }); + + if ( + positionsResult.status === 'success' && + positionsResult.data && + Array.isArray(positionsResult.data) + ) { + const positions = positionsResult.data; + const strkBorrowPositions = positions.filter( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'STRK' && + pos.type === 'borrow' + ); + + for (const position of strkBorrowPositions) { + try { + await repayBorrowPosition(onchainWrite, { + collateralTokenSymbol: 'STRK', + debtTokenSymbol: position.debt?.symbol || '', + // repayAmount not provided = repay all + poolId: GENESIS_POOLID, + }); + await new Promise((resolve) => setTimeout(resolve, 3000)); + } catch (error) { + // Ignore errors during cleanup + console.warn('Cleanup repay error:', error); + } + } + } + } catch (error) { + // Ignore cleanup errors + console.warn('Cleanup error:', error); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }); +}); diff --git a/packages/mcps/vesu/__tests__/e2e/earn.e2e.test.ts b/packages/mcps/vesu/__tests__/e2e/earn.e2e.test.ts new file mode 100644 index 00000000..530588a4 --- /dev/null +++ b/packages/mcps/vesu/__tests__/e2e/earn.e2e.test.ts @@ -0,0 +1,321 @@ +import { describe, beforeAll, it, expect, afterEach } from '@jest/globals'; +import { + getOnchainRead, + getOnchainWrite, + getDataAsRecord, + getERC20Balance, +} from '@kasarlabs/ask-starknet-core'; +import { depositEarnPosition } from '../../src/tools/write/deposit_earn.js'; +import { withdrawEarnPosition } from '../../src/tools/write/withdraw_earn.js'; +import { getPools } from '../../src/tools/read/getPools.js'; +import { getPositions } from '../../src/tools/read/getPositions.js'; +import { getTokens } from '../../src/tools/read/getTokens.js'; +import { GENESIS_POOLID } from '../../src/lib/constants/index.js'; +import { normalizeAddress } from '../helpers.js'; + +let accountAddress: string; + +const STRK_ADDRESS = + '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'; + +function getDataAsArray( + data: Record | Array | undefined +): Array { + if (!data || !Array.isArray(data)) { + throw new Error('Expected data to be an Array'); + } + return data; +} + +describe('Vesu Deposit/Withdraw/Earn E2E Tests', () => { + beforeAll(async () => { + accountAddress = process.env.STARKNET_ACCOUNT_ADDRESS || '0x0'; + + if (accountAddress === '0x0') { + throw new Error( + 'STARKNET_ACCOUNT_ADDRESS must be set in environment variables' + ); + } + + if (!process.env.STARKNET_RPC_URL) { + throw new Error('STARKNET_RPC_URL must be set in environment variables'); + } + }); + + describe('Read Operations', () => { + it('should get pools using default pool (v2)', async () => { + const onchainRead = getOnchainRead(); + const result = await getPools(onchainRead, { + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const pools = getDataAsArray(result.data); + expect(pools.length).toBeGreaterThan(0); + + // Find the default pool (v2) + const defaultPool = pools.find( + (pool: any) => pool.id === GENESIS_POOLID + ); + expect(defaultPool).toBeDefined(); + expect(defaultPool.protocolVersion).toBe('v2'); + } + }); + + it('should get pools by poolId (default pool)', async () => { + const onchainRead = getOnchainRead(); + const result = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const pools = getDataAsArray(result.data); + expect(pools.length).toBe(1); + const pool = pools[0]; + expect(pool.id).toBe(GENESIS_POOLID); + expect(pool.protocolVersion).toBe('v2'); + expect(pool.assets).toBeDefined(); + expect(Array.isArray(pool.assets)).toBe(true); + } + }); + + it('should get tokens and find STRK', async () => { + const onchainRead = getOnchainRead(); + const result = await getTokens(onchainRead, { + symbol: 'STRK', + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const tokens = getDataAsArray(result.data); + expect(tokens.length).toBeGreaterThan(0); + const strkToken = tokens.find( + (token: any) => token.symbol.toUpperCase() === 'STRK' + ); + expect(strkToken).toBeDefined(); + expect(strkToken.address).toBeDefined(); + expect(strkToken.decimals).toBeDefined(); + } + }); + + it('should get tokens by address (STRK)', async () => { + const onchainRead = getOnchainRead(); + const result = await getTokens(onchainRead, { + address: STRK_ADDRESS, + }); + + expect(result.status).toBe('success'); + + if (result.status === 'success' && result.data) { + const tokens = getDataAsArray(result.data); + expect(tokens.length).toBeGreaterThan(0); + const normalizedStrkAddress = normalizeAddress(STRK_ADDRESS); + const strkToken = tokens.find( + (token: any) => + normalizeAddress(token.address) === normalizedStrkAddress + ); + expect(strkToken).toBeDefined(); + expect(strkToken.symbol).toBe('STRK'); + } + }); + + it('should get all tokens', async () => { + const onchainRead = getOnchainRead(); + const result = await getTokens(onchainRead, {}); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const tokens = getDataAsArray(result.data); + expect(tokens.length).toBeGreaterThan(0); + } + }); + + it('should get positions for account (earn type)', async () => { + const onchainRead = getOnchainRead(); + const result = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['earn'], + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const positions = getDataAsArray(result.data); + // Positions might be empty, that's okay + expect(Array.isArray(positions)).toBe(true); + } + }); + }); + + describe('Deposit/Withdraw Earn Operations', () => { + it('should deposit STRK to earn and then withdraw all', async () => { + const onchainWrite = getOnchainWrite(); + const provider = onchainWrite.provider; + const accountAddress = onchainWrite.account.address; + + const balanceBefore = await getERC20Balance( + provider, + STRK_ADDRESS, + accountAddress + ); + + // Deposit a small amount (1 STRK) + const depositAmount = '1'; + const depositResult = await depositEarnPosition(onchainWrite, { + depositTokenSymbol: 'STRK', + depositAmount, + poolId: GENESIS_POOLID, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.symbol).toBe('STRK'); + expect(data.amount).toBe(depositAmount); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Verify position exists + const onchainRead = getOnchainRead(); + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['earn'], + }); + + expect(positionsResult.status).toBe('success'); + if (positionsResult.status === 'success' && positionsResult.data) { + const positions = getDataAsArray(positionsResult.data); + const strkEarnPosition = positions.find( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'STRK' && + pos.type === 'earn' + ); + expect(strkEarnPosition).toBeDefined(); + } + + // Withdraw all + const withdrawResult = await withdrawEarnPosition(onchainWrite, { + withdrawTokenSymbol: 'STRK', + withdrawAmount: '0', // Withdraw all + poolId: GENESIS_POOLID, + }); + + expect(withdrawResult.status).toBe('success'); + if (withdrawResult.status === 'success' && withdrawResult.data) { + const data = getDataAsRecord(withdrawResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.symbol).toBe('STRK'); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, 180000); + + it('should deposit STRK with specific amount and withdraw specific amount', async () => { + const onchainWrite = getOnchainWrite(); + + // Deposit 1 STRK + const depositAmount = '1'; + const depositResult = await depositEarnPosition(onchainWrite, { + depositTokenSymbol: 'STRK', + depositAmount, + poolId: GENESIS_POOLID, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Withdraw a portion (0.3 STRK) + const withdrawAmount = '0.3'; + const withdrawResult = await withdrawEarnPosition(onchainWrite, { + withdrawTokenSymbol: 'STRK', + withdrawAmount, + poolId: GENESIS_POOLID, + }); + + expect(withdrawResult.status).toBe('success'); + if (withdrawResult.status === 'success' && withdrawResult.data) { + const data = getDataAsRecord(withdrawResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Withdraw remaining (all) + const withdrawAllResult = await withdrawEarnPosition(onchainWrite, { + withdrawTokenSymbol: 'STRK', + withdrawAmount: '0', // Withdraw all remaining + poolId: GENESIS_POOLID, + }); + + expect(withdrawAllResult.status).toBe('success'); + if (withdrawAllResult.status === 'success' && withdrawAllResult.data) { + const data = getDataAsRecord(withdrawAllResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, 180000); + + it('should handle deposit with insufficient balance gracefully', async () => { + const onchainWrite = getOnchainWrite(); + + const result = await depositEarnPosition(onchainWrite, { + depositTokenSymbol: 'STRK', + depositAmount: '1000000000', + poolId: GENESIS_POOLID, + }); + + expect(result.status).toBe('failure'); + expect(result.error).toBeDefined(); + }, 120000); + }); + + afterEach(async () => { + // Cleanup: withdraw all remaining positions + try { + const onchainWrite = getOnchainWrite(); + const onchainRead = getOnchainRead(); + const accountAddress = onchainWrite.account.address; + + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['earn'], + }); + + if ( + positionsResult.status === 'success' && + positionsResult.data && + Array.isArray(positionsResult.data) + ) { + try { + await withdrawEarnPosition(onchainWrite, { + withdrawTokenSymbol: 'STRK', + withdrawAmount: '0', // Withdraw all + }); + await new Promise((resolve) => setTimeout(resolve, 3000)); + } catch (error) { + // Ignore errors during cleanup + console.error('Cleanup withdraw error:', error); + } + } + } catch (error) { + // Ignore cleanup errors + console.error('Cleanup error:', error); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + }); +}); diff --git a/packages/mcps/vesu/__tests__/e2e/multiply.e2e.test.ts b/packages/mcps/vesu/__tests__/e2e/multiply.e2e.test.ts new file mode 100644 index 00000000..b64d27a7 --- /dev/null +++ b/packages/mcps/vesu/__tests__/e2e/multiply.e2e.test.ts @@ -0,0 +1,538 @@ +import { describe, beforeAll, it, expect, afterEach } from '@jest/globals'; +import { + getOnchainRead, + getOnchainWrite, + getDataAsRecord, +} from '@kasarlabs/ask-starknet-core'; +import { depositMultiplyPosition } from '../../src/tools/write/deposit_multiply.js'; +import { withdrawMultiplyPosition } from '../../src/tools/write/withdraw_multiply.js'; +import { updateMultiplyPosition } from '../../src/tools/write/update_multiply.js'; +import { getPools } from '../../src/tools/read/getPools.js'; +import { getPositions } from '../../src/tools/read/getPositions.js'; +import { getTokens } from '../../src/tools/read/getTokens.js'; +import { GENESIS_POOLID } from '../../src/lib/constants/index.js'; +import { normalizeAddress, expectLTVWithinTolerance } from '../helpers.js'; + +let accountAddress: string; + +const STRK_ADDRESS = + '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'; + +// Multiply test constants +const MULTIPLY_DEPOSIT_AMOUNT = '350'; // xSTRK amount +const MULTIPLY_TARGET_LTV = '65'; // 50% LTV +const MULTIPLY_UPDATE_LTV_LOW = '35'; // 25% LTV for update test +const MULTIPLY_UPDATE_LTV_HIGH = '80'; // 80% LTV for update test (with ±1% tolerance) + +function getDataAsArray( + data: Record | Array | undefined +): Array { + if (!data || !Array.isArray(data)) { + throw new Error('Expected data to be an Array'); + } + return data; +} + +describe('Vesu Multiply E2E Tests', () => { + beforeAll(async () => { + accountAddress = process.env.STARKNET_ACCOUNT_ADDRESS || '0x0'; + + if (accountAddress === '0x0') { + throw new Error( + 'STARKNET_ACCOUNT_ADDRESS must be set in environment variables' + ); + } + + if (!process.env.STARKNET_RPC_URL) { + throw new Error('STARKNET_RPC_URL must be set in environment variables'); + } + }); + + describe('Read Operations', () => { + it('should get pools and verify v2 pool for multiply operations', async () => { + const onchainRead = getOnchainRead(); + const result = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const pools = getDataAsArray(result.data); + expect(pools.length).toBe(1); + const pool = pools[0]; + expect(pool.protocolVersion).toBe('v2'); + expect(pool.assets).toBeDefined(); + expect(Array.isArray(pool.assets)).toBe(true); + expect(pool.assets.length).toBeGreaterThan(1); // Need at least 2 assets for multiply + } + }); + + it('should get tokens and verify STRK and other tokens', async () => { + const onchainRead = getOnchainRead(); + const result = await getTokens(onchainRead, {}); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const tokens = getDataAsArray(result.data); + expect(tokens.length).toBeGreaterThan(0); + + const strkToken = tokens.find( + (token: any) => token.symbol.toUpperCase() === 'STRK' + ); + expect(strkToken).toBeDefined(); + } + }); + + it('should get positions filtered by multiply type', async () => { + const onchainRead = getOnchainRead(); + const result = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['multiply'], + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const positions = getDataAsArray(result.data); + // Positions might be empty, that's okay + expect(Array.isArray(positions)).toBe(true); + // All positions should be multiply type if any exist + positions.forEach((pos: any) => { + expect(pos.type).toBe('multiply'); + }); + } + }); + + it('should get positions with multiple type filters', async () => { + const onchainRead = getOnchainRead(); + const result = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['multiply', 'borrow', 'earn'], + }); + + expect(result.status).toBe('success'); + if (result.status === 'success' && result.data) { + const positions = getDataAsArray(result.data); + expect(Array.isArray(positions)).toBe(true); + } + }); + }); + + describe('Multiply Operations', () => { + it('should deposit STRK as collateral, borrow, and close position', async () => { + const onchainWrite = getOnchainWrite(); + const accountAddress = onchainWrite.account.address; + + // Get pool to find debt token + const onchainRead = getOnchainRead(); + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(poolsResult.status).toBe('success'); + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + + // Use xSTRK as collateral and STRK as debt token + const depositResult = await depositMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + depositAmount: MULTIPLY_DEPOSIT_AMOUNT, + targetLTV: MULTIPLY_TARGET_LTV, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.collateralSymbol).toBe('xSTRK'); + expect(data.debtSymbol).toBe('STRK'); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Verify position exists + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['multiply'], + }); + + expect(positionsResult.status).toBe('success'); + if (positionsResult.status === 'success' && positionsResult.data) { + const positions = getDataAsArray(positionsResult.data); + const multiplyPosition = positions.find( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'XSTRK' && + pos.debt?.symbol.toUpperCase() === 'STRK' && + pos.type === 'multiply' + ); + expect(multiplyPosition).toBeDefined(); + + // Verify LTV is within ±1% tolerance + if (multiplyPosition?.ltv?.current?.value) { + const ltvValue = BigInt(multiplyPosition.ltv.current.value); + const ltvDecimals = multiplyPosition.ltv.current.decimals || 18; + const ltvPercentage = Number(ltvValue) / 10 ** ltvDecimals; + expectLTVWithinTolerance( + ltvPercentage, + parseInt(MULTIPLY_TARGET_LTV) + ); + } + } + + // Close position (withdraw all) + const withdrawResult = await withdrawMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + ekuboSlippage: 500, + }); + + expect(withdrawResult.status).toBe('success'); + if (withdrawResult.status === 'success' && withdrawResult.data) { + const data = getDataAsRecord(withdrawResult.data); + expect(data.transaction_hash).toBeDefined(); + expect(data.transaction_hash).toMatch(/^0x[0-9a-f]+$/i); + expect(data.collateralSymbol).toBe('xSTRK'); + expect(data.debtSymbol).toBe('STRK'); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }, 180000); + + it('should deposit STRK, update LTV, then close position', async () => { + const onchainWrite = getOnchainWrite(); + + // Get pool to find debt token + const onchainRead = getOnchainRead(); + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(poolsResult.status).toBe('success'); + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + + // Deposit and create multiply position with xSTRK as collateral and STRK as debt + const depositResult = await depositMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + depositAmount: MULTIPLY_DEPOSIT_AMOUNT, + targetLTV: MULTIPLY_TARGET_LTV, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Update LTV down to 25% + const updateDownResult = await updateMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + targetLTV: MULTIPLY_UPDATE_LTV_LOW, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(updateDownResult.status).toBe('success'); + if (updateDownResult.status === 'success' && updateDownResult.data) { + const data = getDataAsRecord(updateDownResult.data); + expect(data.transaction_hash).toBeDefined(); + // Verify LTV is within ±1% tolerance (24% to 26%) + const actualLTV = parseInt(data.targetLTV as string); + expectLTVWithinTolerance(actualLTV, parseInt(MULTIPLY_UPDATE_LTV_LOW)); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Update LTV back to 80% (with ±1% tolerance, so between 79% and 81%) + const updateUpResult = await updateMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + targetLTV: MULTIPLY_UPDATE_LTV_HIGH, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(updateUpResult.status).toBe('success'); + if (updateUpResult.status === 'success' && updateUpResult.data) { + const data = getDataAsRecord(updateUpResult.data); + expect(data.transaction_hash).toBeDefined(); + // Verify LTV is within ±1% tolerance (79% to 81%) + const actualLTV = parseInt(data.targetLTV as string); + expectLTVWithinTolerance(actualLTV, parseInt(MULTIPLY_UPDATE_LTV_HIGH)); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Close position + const withdrawResult = await withdrawMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + withdrawAmount: '0', // Close position + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(withdrawResult.status).toBe('success'); + if (withdrawResult.status === 'success' && withdrawResult.data) { + const data = getDataAsRecord(withdrawResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }, 180000); + + it('should deposit STRK, withdraw partial, then close position', async () => { + const onchainWrite = getOnchainWrite(); + const provider = onchainWrite.provider; + const accountAddress = onchainWrite.account.address; + + // Get pool to find debt token + const onchainRead = getOnchainRead(); + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + expect(poolsResult.status).toBe('success'); + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const pools = getDataAsArray(poolsResult.data); + const pool = pools[0]; + + // Deposit and create multiply position with xSTRK as collateral and STRK as debt + const depositResult = await depositMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + depositAmount: MULTIPLY_DEPOSIT_AMOUNT, + targetLTV: MULTIPLY_TARGET_LTV, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(depositResult.status).toBe('success'); + if (depositResult.status === 'success' && depositResult.data) { + const data = getDataAsRecord(depositResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Get position to see collateral amount + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['multiply'], + }); + + let collateralAmount: string | undefined; + if ( + positionsResult.status === 'success' && + positionsResult.data && + Array.isArray(positionsResult.data) + ) { + const positions = positionsResult.data; + const multiplyPosition = positions.find( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'XSTRK' && + pos.debt?.symbol.toUpperCase() === 'STRK' && + pos.type === 'multiply' + ); + + if (multiplyPosition?.collateral?.value) { + // Convert collateral value to human readable format + const collateralValue = BigInt(multiplyPosition.collateral.value); + const decimals = multiplyPosition.collateral.decimals || 18; + const divisor = 10n ** BigInt(decimals); + const wholePart = collateralValue / divisor; + const fractionalPart = collateralValue % divisor; + const fractionalStr = fractionalPart + .toString() + .padStart(decimals, '0') + .replace(/0+$/, ''); + collateralAmount = fractionalStr + ? `${wholePart}.${fractionalStr}` + : wholePart.toString(); + } + } + + if (collateralAmount) { + // Withdraw partial (35% of collateral) + const partialWithdrawAmount = ( + parseFloat(collateralAmount) * 0.35 + ).toFixed(6); + + const partialWithdrawResult = await withdrawMultiplyPosition( + onchainWrite, + { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + withdrawAmount: partialWithdrawAmount, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + } + ); + + expect(partialWithdrawResult.status).toBe('success'); + if ( + partialWithdrawResult.status === 'success' && + partialWithdrawResult.data + ) { + const data = getDataAsRecord(partialWithdrawResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + } + + // Close remaining position + const withdrawAllResult = await withdrawMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + ekuboSlippage: 500, + }); + + expect(withdrawAllResult.status).toBe('success'); + if (withdrawAllResult.status === 'success' && withdrawAllResult.data) { + const data = getDataAsRecord(withdrawAllResult.data); + expect(data.transaction_hash).toBeDefined(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }, 180000); + + it('should handle multiply with insufficient balance gracefully', async () => { + const onchainWrite = getOnchainWrite(); + const onchainRead = getOnchainRead(); + + // Get pool to find debt token + const poolsResult = await getPools(onchainRead, { + poolId: GENESIS_POOLID, + onlyVerified: true, + onlyEnabledAssets: true, + }); + + if (poolsResult.status !== 'success' || !poolsResult.data) { + throw new Error('Failed to get pool information'); + } + + const result = await depositMultiplyPosition(onchainWrite, { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + depositAmount: '1000000000', + targetLTV: MULTIPLY_TARGET_LTV, + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }); + + expect(result.status).toBe('failure'); + expect(result.error).toBeDefined(); + }, 120000); + }); + + afterEach(async () => { + // Cleanup: close all remaining multiply positions + try { + const onchainWrite = getOnchainWrite(); + const onchainRead = getOnchainRead(); + const accountAddress = onchainWrite.account.address; + + const positionsResult = await getPositions(onchainRead, { + walletAddress: accountAddress as `0x${string}`, + type: ['multiply'], + }); + + if ( + positionsResult.status === 'success' && + positionsResult.data && + Array.isArray(positionsResult.data) + ) { + const positions = positionsResult.data; + const xstrkMultiplyPositions = positions.filter( + (pos: any) => + pos.collateral?.symbol.toUpperCase() === 'XSTRK' && + pos.debt?.symbol.toUpperCase() === 'STRK' && + pos.type === 'multiply' + ); + + for (const position of xstrkMultiplyPositions) { + try { + // Check if position has debt to close + const debtAmount = position.nominalDebt?.value + ? BigInt(position.nominalDebt.value) + : position.debt?.value + ? BigInt(position.debt.value) + : 0n; + + let withdrawParams: any = { + collateralTokenSymbol: 'xSTRK', + debtTokenSymbol: 'STRK', + poolId: GENESIS_POOLID, + ekuboSlippage: 500, + }; + + if (debtAmount === 0n) { + // No debt to close, withdraw the net position amount (collateral amount) + if (position.collateral?.value) { + const collateralValue = BigInt(position.collateral.value); + const decimals = position.collateral.decimals || 18; + const divisor = 10n ** BigInt(decimals); + const wholePart = collateralValue / divisor; + const fractionalPart = collateralValue % divisor; + const fractionalStr = fractionalPart + .toString() + .padStart(decimals, '0') + .replace(/0+$/, ''); + const collateralAmount = fractionalStr + ? `${wholePart}.${fractionalStr}` + : wholePart.toString(); + withdrawParams.withdrawAmount = collateralAmount; + } else { + // Fallback: try to close anyway if collateral value is not available + withdrawParams.withdrawAmount = '0'; + } + } else { + // Has debt, close position + withdrawParams.withdrawAmount = '0'; + } + + await withdrawMultiplyPosition(onchainWrite, withdrawParams); + await new Promise((resolve) => setTimeout(resolve, 10000)); + } catch (error) { + // Ignore errors during cleanup + console.warn('Cleanup withdraw multiply error:', error); + } + } + } + } catch (error) { + // Ignore cleanup errors + console.warn('Cleanup error:', error); + } + + await new Promise((resolve) => setTimeout(resolve, 30000)); + }); +}); diff --git a/packages/mcps/vesu/__tests__/helpers.ts b/packages/mcps/vesu/__tests__/helpers.ts new file mode 100644 index 00000000..e9092960 --- /dev/null +++ b/packages/mcps/vesu/__tests__/helpers.ts @@ -0,0 +1,48 @@ +/** + * Normalizes an address by padding it to 64 hex characters (66 with 0x prefix) + * This ensures addresses are compared in a consistent format regardless of their original length + * @param addr - Address to normalize + * @returns Normalized address in lowercase + */ +export function normalizeAddress(addr: string): string { + const withoutPrefix = addr.startsWith('0x') ? addr.slice(2) : addr; + const padded = withoutPrefix.padStart(64, '0'); + return `0x${padded}`.toLowerCase(); +} + +/** + * Verifies that an LTV value is within ±1% tolerance of the expected value + * Handles both decimal format (0-1) and percentage format (0-100) + * @param actualLTV - The actual LTV value (as string or number, can be decimal 0-1 or percentage 0-100) + * @param expectedLTV - The expected LTV value in percentage format (as string or number, 0-100) + * @returns true if the LTV is within tolerance, throws error otherwise + */ +export function expectLTVWithinTolerance( + actualLTV: string | number, + expectedLTV: string | number +): void { + let actual: number; + if (typeof actualLTV === 'string') { + actual = parseFloat(actualLTV); + } else { + actual = actualLTV; + } + + const expected = + typeof expectedLTV === 'string' ? parseInt(expectedLTV) : expectedLTV; + + // Convert actual LTV to percentage if it's in decimal format (0-1) + // If actual is between 0 and 1, it's in decimal format, convert to percentage + if (actual >= 0 && actual <= 1) { + actual = actual * 100; + } + + const minLTV = expected - 1; + const maxLTV = expected + 1; + + if (actual < minLTV || actual > maxLTV) { + throw new Error( + `LTV ${actual}% is not within ±1% tolerance of expected ${expected}% (expected range: ${minLTV}%-${maxLTV}%)` + ); + } +} diff --git a/packages/mcps/vesu/__tests__/setup.ts b/packages/mcps/vesu/__tests__/setup.ts new file mode 100644 index 00000000..b99f862e --- /dev/null +++ b/packages/mcps/vesu/__tests__/setup.ts @@ -0,0 +1,7 @@ +import { config } from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +config({ path: resolve(__dirname, '../../../../.env') }); diff --git a/packages/mcps/vesu/jest.config.js b/packages/mcps/vesu/jest.config.js new file mode 100644 index 00000000..12d5ffa8 --- /dev/null +++ b/packages/mcps/vesu/jest.config.js @@ -0,0 +1,26 @@ +export default { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + '^@kasarlabs/ask-starknet-core$': '/../../core/src/index.ts', + }, + transformIgnorePatterns: ['node_modules/(?!(@kasarlabs/ask-starknet-core)/)'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + module: 'ESNext', + }, + }, + ], + }, + testMatch: ['**/__tests__/**/*.test.ts', '**/__tests__/**/*.e2e.test.ts'], + collectCoverageFrom: ['src/**/*.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + testTimeout: 180000, + setupFilesAfterEnv: ['/__tests__/setup.ts'], +}; diff --git a/packages/mcps/vesu/package.json b/packages/mcps/vesu/package.json index 10186aeb..fc1ffd7c 100644 --- a/packages/mcps/vesu/package.json +++ b/packages/mcps/vesu/package.json @@ -10,7 +10,12 @@ "build": "tsc && chmod 755 build/index.js", "clean": "rm -rf build", "clean:all": "rm -rf build node_modules", - "start": "node build/index.js" + "start": "node build/index.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/e2e", + "test:borrow": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/e2e/borrow.e2e.test.ts", + "test:earn": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/e2e/earn.e2e.test.ts", + "test:multiply": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/e2e/multiply.e2e.test.ts" }, "files": [ "build" diff --git a/packages/mcps/vesu/src/lib/utils/contracts.ts b/packages/mcps/vesu/src/lib/utils/contracts.ts index 809bf550..22413f8e 100644 --- a/packages/mcps/vesu/src/lib/utils/contracts.ts +++ b/packages/mcps/vesu/src/lib/utils/contracts.ts @@ -1,9 +1,9 @@ import { Contract, RpcProvider } from 'starknet'; import { Address } from '../../interfaces/index.js'; +import { erc20Abi } from '@kasarlabs/ask-starknet-core'; import { vTokenAbi } from '../abis/vTokenAbi.js'; import { singletonAbi } from '../abis/singletonAbi.js'; import { extensionAbi } from '../abis/extensionAbi.js'; -import { erc20Abi } from '@kasarlabs/ask-starknet-core'; import { multiplyAbi } from '../abis/multiplyAbi.js'; import { poolAbi } from '../abis/poolAbi.js'; diff --git a/packages/mcps/vesu/src/lib/utils/getEkuboQuote.ts b/packages/mcps/vesu/src/lib/utils/getEkuboQuote.ts index 0e2de445..61338fc1 100644 --- a/packages/mcps/vesu/src/lib/utils/getEkuboQuote.ts +++ b/packages/mcps/vesu/src/lib/utils/getEkuboQuote.ts @@ -24,7 +24,7 @@ export async function getEkuboQuoteFromAPI( ): Promise { // Use API to get quote with pool parameters const baseUrl = - ekuboQuoterUrl || 'https://starknet-mainnet-quoter-api.ekubo.org'; + ekuboQuoterUrl || 'https://prod-api-quoter.ekubo.org/23448594291968334'; const apiAmount = isExactIn ? amount : -amount; const url = `${baseUrl}/${apiAmount}/${tokenIn.address}/${tokenOut.address}`; diff --git a/packages/mcps/vesu/src/tools/write/deposit_multiply.ts b/packages/mcps/vesu/src/tools/write/deposit_multiply.ts index b6abd466..2413565f 100644 --- a/packages/mcps/vesu/src/tools/write/deposit_multiply.ts +++ b/packages/mcps/vesu/src/tools/write/deposit_multiply.ts @@ -41,11 +41,8 @@ export class DepositMultiplyService { env: onchainWrite ): Promise { try { - const account = new Account({ - provider: this.env.provider, - address: this.walletAddress, - signer: this.env.account.signer, - }); + // Use the account from env directly instead of creating a new one + const account = env.account; // For v2, poolId is the address of the pool contract const poolId = (params.poolId || GENESIS_POOLID) as Hex; const pool = await getPool(poolId); @@ -141,7 +138,6 @@ export class DepositMultiplyService { toBN(debtPrice.value); const provider = env.provider; - const wallet = env.account; // Use Ekubo API to get quote and automatically extract pool parameters // For deposit: swap debt -> collateral, so order is debtToken/collateralToken @@ -188,7 +184,7 @@ export class DepositMultiplyService { calldata: call.calldata, })); - const tx = await wallet.execute(calls); + const tx = await account.execute(calls); await provider.waitForTransaction(tx.transaction_hash); @@ -262,6 +258,7 @@ export const depositMultiplyPosition = async ( }, }; } else { + console.error('Deposit multiply error:', result.error); return { status: 'failure', error: result.error || 'Unknown error',