diff --git a/packages/demo/backend/src/config/verbs.ts b/packages/demo/backend/src/config/verbs.ts index 5d48ac76..77fcb204 100644 --- a/packages/demo/backend/src/config/verbs.ts +++ b/packages/demo/backend/src/config/verbs.ts @@ -3,7 +3,7 @@ import { type VerbsConfig, type VerbsInterface, } from '@eth-optimism/verbs-sdk' -import { unichain } from 'viem/chains' +import { baseSepolia } from 'viem/chains' import { env } from './env.js' @@ -20,9 +20,13 @@ export function createVerbsConfig(): VerbsConfig { type: 'morpho', }, chains: [ + // { + // chainId: unichain.id, + // rpcUrl: unichain.rpcUrls.default.http[0], + // }, { - chainId: unichain.id, - rpcUrl: env.RPC_URL, + chainId: baseSepolia.id, + rpcUrl: baseSepolia.rpcUrls.default.http[0], }, ], } diff --git a/packages/demo/backend/src/controllers/lend.ts b/packages/demo/backend/src/controllers/lend.ts index 19cf9903..6a1bcc3e 100644 --- a/packages/demo/backend/src/controllers/lend.ts +++ b/packages/demo/backend/src/controllers/lend.ts @@ -42,6 +42,7 @@ export class LendController { ) return c.json({ vaults: formattedVaults }) } catch (error) { + console.error('[LendController.getVaults] Error:', error) return c.json( { error: 'Failed to get vaults', @@ -67,6 +68,7 @@ export class LendController { const formattedVault = await lendService.formatVaultResponse(vaultInfo) return c.json({ vault: formattedVault }) } catch (error) { + console.error('[LendController.getVault] Error:', error) return c.json( { error: 'Failed to get vault info', @@ -96,6 +98,7 @@ export class LendController { await lendService.formatVaultBalanceResponse(balance) return c.json(formattedBalance) } catch (error) { + console.error('[LendController.getVaultBalance] Error:', error) return c.json( { error: 'Failed to get vault balance', @@ -136,6 +139,7 @@ export class LendController { }, }) } catch (error) { + console.error('[LendController.deposit] Error:', error) return c.json( { error: 'Failed to deposit', diff --git a/packages/demo/backend/src/controllers/wallet.ts b/packages/demo/backend/src/controllers/wallet.ts index 33cdd6c9..bb6f3c1f 100644 --- a/packages/demo/backend/src/controllers/wallet.ts +++ b/packages/demo/backend/src/controllers/wallet.ts @@ -65,6 +65,7 @@ export class WalletController { userId, } satisfies CreateWalletResponse) } catch (error) { + console.error('[WalletController.createWallet] Error:', error) return c.json( { error: 'Failed to create wallet', @@ -103,6 +104,7 @@ export class WalletController { userId, } satisfies GetWalletResponse) } catch (error) { + console.error('[WalletController.getWallet] Error:', error) return c.json( { error: 'Failed to get wallet', @@ -134,6 +136,7 @@ export class WalletController { count: wallets.length, } satisfies GetAllWalletsResponse) } catch (error) { + console.error('[WalletController.getAllWallets] Error:', error) return c.json( { error: 'Failed to get wallets', @@ -159,6 +162,7 @@ export class WalletController { return c.json({ balance: serializeBigInt(balance) }) } catch (error) { + console.error('[WalletController.getBalance] Error:', error) return c.json( { error: 'Failed to get balance', @@ -186,6 +190,7 @@ export class WalletController { return c.json(result) } catch (error) { + console.error('[WalletController.fundWallet] Error:', error) return c.json( { error: 'Failed to fund wallet', @@ -222,6 +227,7 @@ export class WalletController { }, }) } catch (error) { + console.error('[WalletController.sendTokens] Error:', error) return c.json( { error: 'Failed to send tokens', diff --git a/packages/demo/backend/src/services/lend.ts b/packages/demo/backend/src/services/lend.ts index ef6d3788..7ab8cabd 100644 --- a/packages/demo/backend/src/services/lend.ts +++ b/packages/demo/backend/src/services/lend.ts @@ -112,7 +112,7 @@ export async function executeLendTransaction( throw new Error('No transaction data available for execution') } - const publicClient = verbs.chainManager.getPublicClient(130) + const publicClient = verbs.chainManager.getPublicClient(84532) // Base Sepolia const ethBalance = await publicClient.getBalance({ address: wallet.address }) const gasEstimate = await estimateGasCost( @@ -121,24 +121,25 @@ export async function executeLendTransaction( lendTransaction, ) + // Skip gas check when using gas sponsorship + // TODO: Add proper gas sponsorship detection if (ethBalance < gasEstimate) { - throw new Error('Insufficient ETH for gas fees') + // Proceed with gas sponsorship - Privy will handle gas fees + // throw new Error('Insufficient ETH for gas fees') } let depositHash: Address = '0x0' if (lendTransaction.transactionData.approval) { - const approvalSignedTx = await wallet.sign( + const approvalHash = await wallet.signAndSend( lendTransaction.transactionData.approval, ) - const approvalHash = await wallet.send(approvalSignedTx, publicClient) await publicClient.waitForTransactionReceipt({ hash: approvalHash }) } - const depositSignedTx = await wallet.sign( + depositHash = await wallet.signAndSend( lendTransaction.transactionData.deposit, ) - depositHash = await wallet.send(depositSignedTx, publicClient) await publicClient.waitForTransactionReceipt({ hash: depositHash }) return { ...lendTransaction, hash: depositHash } diff --git a/packages/demo/backend/src/services/wallet.ts b/packages/demo/backend/src/services/wallet.ts index 57bdf3a1..a5ab2b16 100644 --- a/packages/demo/backend/src/services/wallet.ts +++ b/packages/demo/backend/src/services/wallet.ts @@ -82,7 +82,7 @@ export async function getBalance(userId: string): Promise { totalFormattedBalance: formattedBalance, chainBalances: [ { - chainId: 130 as const, // Unichain + chainId: 84532 as const, // Base Sepolia balance: vaultBalance.balance, formattedBalance: formattedBalance, }, diff --git a/packages/demo/frontend/src/components/Terminal.tsx b/packages/demo/frontend/src/components/Terminal.tsx index 4f15c690..9a1bdca2 100644 --- a/packages/demo/frontend/src/components/Terminal.tsx +++ b/packages/demo/frontend/src/components/Terminal.tsx @@ -754,7 +754,7 @@ How much would you like to lend?` Vault: ${promptData.selectedVault.name} Amount: ${amount} USDC -Tx: https://uniscan.xyz/tx/${result.transaction.hash || 'pending'}`, +Tx: https://base-sepolia.blockscout.com/tx/${result.transaction.hash || 'pending'}`, timestamp: new Date(), } setLines((prev) => [...prev.slice(0, -1), successLine]) diff --git a/packages/sdk/src/constants/supportedChains.ts b/packages/sdk/src/constants/supportedChains.ts index a0ba0e22..8ed1bb20 100644 --- a/packages/sdk/src/constants/supportedChains.ts +++ b/packages/sdk/src/constants/supportedChains.ts @@ -1,5 +1,10 @@ -import { base, mainnet, unichain } from 'viem/chains' +import { base, baseSepolia, mainnet, unichain } from 'viem/chains' -export const SUPPORTED_CHAIN_IDS = [mainnet.id, unichain.id, base.id] as const +export const SUPPORTED_CHAIN_IDS = [ + mainnet.id, + unichain.id, + base.id, + baseSepolia.id, +] as const export type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number] diff --git a/packages/sdk/src/lend/providers/morpho/index.test.ts b/packages/sdk/src/lend/providers/morpho/index.test.ts index ebaf79c4..8b10bc56 100644 --- a/packages/sdk/src/lend/providers/morpho/index.test.ts +++ b/packages/sdk/src/lend/providers/morpho/index.test.ts @@ -58,9 +58,14 @@ describe('LendProviderMorpho', () => { transport: http(), }) + const mockChainManager = { + getPublicClient: () => mockPublicClient, + } as any + provider = new LendProviderMorpho( mockConfig, mockPublicClient as unknown as PublicClient, + mockChainManager, ) }) @@ -74,9 +79,14 @@ describe('LendProviderMorpho', () => { ...mockConfig, defaultSlippage: undefined, } + const mockChainManager = { + getPublicClient: () => mockPublicClient, + } as any + const providerWithDefaults = new LendProviderMorpho( configWithoutSlippage, mockPublicClient as unknown as PublicClient, + mockChainManager, ) expect(providerWithDefaults).toBeInstanceOf(LendProviderMorpho) }) diff --git a/packages/sdk/src/lend/providers/morpho/index.ts b/packages/sdk/src/lend/providers/morpho/index.ts index c397431b..5db78083 100644 --- a/packages/sdk/src/lend/providers/morpho/index.ts +++ b/packages/sdk/src/lend/providers/morpho/index.ts @@ -2,6 +2,7 @@ import { MetaMorphoAction } from '@morpho-org/blue-sdk-viem' import type { Address, PublicClient } from 'viem' import { encodeFunctionData, erc20Abi, formatUnits } from 'viem' +import type { ChainManager } from '../../../services/ChainManager.js' import type { LendOptions, LendTransaction, @@ -24,6 +25,11 @@ export const SUPPORTED_NETWORKS = { name: 'Unichain', morphoAddress: '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb' as Address, }, + BASE_SEPOLIA: { + chainId: 84532, + name: 'Base Sepolia', + morphoAddress: '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb' as Address, // Using same address as Morpho typically uses same deployment address + }, } as const /** @@ -37,21 +43,28 @@ export class LendProviderMorpho extends LendProvider { private morphoAddress: Address private defaultSlippage: number private publicClient: PublicClient + private chainManager: ChainManager /** * Create a new Morpho lending provider * @param config - Morpho lending configuration * @param publicClient - Viem public client for blockchain interactions // TODO: remove this + * @param chainManager - Chain manager for multi-chain support */ - constructor(config: MorphoLendConfig, publicClient: PublicClient) { + constructor( + config: MorphoLendConfig, + publicClient: PublicClient, + chainManager: ChainManager, + ) { super() - // Use Unichain as the default network for now - const network = SUPPORTED_NETWORKS.UNICHAIN + // Use Base Sepolia as the default network for testing + const network = SUPPORTED_NETWORKS.BASE_SEPOLIA this.morphoAddress = network.morphoAddress this.defaultSlippage = config.defaultSlippage || 50 // 0.5% default this.publicClient = publicClient + this.chainManager = chainManager } /** @@ -172,7 +185,7 @@ export class LendProviderMorpho extends LendProvider { * @returns Promise resolving to vault information */ async getVault(vaultAddress: Address): Promise { - return getVaultInfoHelper(vaultAddress, this.publicClient) + return getVaultInfoHelper(vaultAddress, this.chainManager) } /** @@ -180,7 +193,7 @@ export class LendProviderMorpho extends LendProvider { * @returns Promise resolving to array of vault information */ async getVaults(): Promise { - return getVaultsHelper(this.publicClient) + return getVaultsHelper(this.chainManager) } /** diff --git a/packages/sdk/src/lend/providers/morpho/vaults.ts b/packages/sdk/src/lend/providers/morpho/vaults.ts index e97ea088..80472515 100644 --- a/packages/sdk/src/lend/providers/morpho/vaults.ts +++ b/packages/sdk/src/lend/providers/morpho/vaults.ts @@ -1,8 +1,10 @@ import type { AccrualPosition, IToken } from '@morpho-org/blue-sdk' import { fetchAccrualVault } from '@morpho-org/blue-sdk-viem' -import type { Address, PublicClient } from 'viem' +import type { Address } from 'viem' -import { getTokenAddress, SUPPORTED_TOKENS } from '../../../supported/tokens.js' +import type { SupportedChainId } from '../../../constants/supportedChains.js' +import type { ChainManager } from '../../../services/ChainManager.js' +import { SUPPORTED_TOKENS } from '../../../supported/tokens.js' import type { ApyBreakdown, LendVaultInfo } from '../../../types/lend.js' import { fetchRewards, type RewardsBreakdown } from './api.js' @@ -13,22 +15,35 @@ export interface VaultConfig { address: Address name: string asset: IToken & { address: Address } + chainId: SupportedChainId } /** - * Supported vaults on Unichain for Morpho lending + * Supported vaults for Morpho lending */ export const SUPPORTED_VAULTS: VaultConfig[] = [ + // { + // // Gauntlet USDC vault on Unichain + // address: '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' as Address, + // name: 'Gauntlet USDC (Unichain)', + // asset: { + // address: getTokenAddress('USDC', 130)!, // USDC on Unichain + // symbol: SUPPORTED_TOKENS.USDC.symbol, + // decimals: BigInt(SUPPORTED_TOKENS.USDC.decimals), + // name: SUPPORTED_TOKENS.USDC.name, + // }, + // }, { - // Gauntlet USDC vault - primary supported vault - address: '0x38f4f3B6533de0023b9DCd04b02F93d36ad1F9f9' as Address, - name: 'Gauntlet USDC', + // USDC vault on Base Sepolia + address: '0x99067e5D73b1d6F1b5856E59209e12F5a0f86DED' as Address, + name: 'USDC Vault (Base Sepolia)', asset: { - address: getTokenAddress('USDC', 130)!, // USDC on Unichain + address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as Address, // USDC on Base Sepolia symbol: SUPPORTED_TOKENS.USDC.symbol, decimals: BigInt(SUPPORTED_TOKENS.USDC.decimals), name: SUPPORTED_TOKENS.USDC.name, }, + chainId: 84532 as SupportedChainId, // Base Sepolia }, ] @@ -109,12 +124,12 @@ export function calculateBaseApy(vault: any): number { /** * Get detailed vault information with enhanced rewards data * @param vaultAddress - Vault address - * @param publicClient - Viem public client + * @param chainManager - Chain manager for multi-chain support * @returns Promise resolving to detailed vault information */ export async function getVaultInfo( vaultAddress: Address, - publicClient: PublicClient, + chainManager: ChainManager, ): Promise { try { // 1. Find vault configuration for validation @@ -125,10 +140,29 @@ export async function getVaultInfo( } // 2. Fetch live vault data from Morpho SDK - const vault = await fetchAccrualVault(vaultAddress, publicClient) + const vault = await fetchAccrualVault( + vaultAddress, + chainManager.getPublicClient(config.chainId), + ).catch((error) => { + console.error('Failed to fetch vault info:', error) + return { + totalAssets: 0n, + totalSupply: 0n, + owner: '0x' as Address, + curator: '0x' as Address, + } + }) // 3. Fetch rewards data from API - const rewardsBreakdown = await fetchAndCalculateRewards(vaultAddress) + const rewardsBreakdown = await fetchAndCalculateRewards(vaultAddress).catch( + (error) => { + console.error('Failed to fetch rewards data:', error) + return { + other: 0, + totalRewardsApr: 0, + } + }, + ) // 4. Calculate APY breakdown const apyBreakdown = calculateApyBreakdown(vault, rewardsBreakdown) @@ -160,15 +194,15 @@ export async function getVaultInfo( /** * Get list of available vaults - * @param publicClient - Viem public client + * @param chainManager - Chain manager for multi-chain support * @returns Promise resolving to array of vault information */ export async function getVaults( - publicClient: PublicClient, + chainManager: ChainManager, ): Promise { try { const vaultInfoPromises = SUPPORTED_VAULTS.map((config) => - getVaultInfo(config.address, publicClient), + getVaultInfo(config.address, chainManager), ) return await Promise.all(vaultInfoPromises) } catch (error) { diff --git a/packages/sdk/src/supported/tokens.ts b/packages/sdk/src/supported/tokens.ts index ffb0a0e3..59e6b29c 100644 --- a/packages/sdk/src/supported/tokens.ts +++ b/packages/sdk/src/supported/tokens.ts @@ -1,5 +1,5 @@ import type { Address } from 'viem' -import { base, mainnet, unichain } from 'viem/chains' +import { base, baseSepolia, mainnet, unichain } from 'viem/chains' import type { SupportedChainId } from '@/constants/supportedChains.js' @@ -19,6 +19,7 @@ export const SUPPORTED_TOKENS: Record = { [mainnet.id]: '0x0000000000000000000000000000000000000000', [unichain.id]: '0x0000000000000000000000000000000000000000', [base.id]: '0x0000000000000000000000000000000000000000', + [baseSepolia.id]: '0x0000000000000000000000000000000000000000', }, }, USDC: { @@ -28,6 +29,7 @@ export const SUPPORTED_TOKENS: Record = { addresses: { [mainnet.id]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', [unichain.id]: '0x078d782b760474a361dda0af3839290b0ef57ad6', + [baseSepolia.id]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', }, }, MORPHO: { diff --git a/packages/sdk/src/verbs.ts b/packages/sdk/src/verbs.ts index 368ceb3a..a48402f0 100644 --- a/packages/sdk/src/verbs.ts +++ b/packages/sdk/src/verbs.ts @@ -1,5 +1,5 @@ import { createPublicClient, http, type PublicClient } from 'viem' -import { mainnet, unichain } from 'viem/chains' +import { baseSepolia, mainnet, unichain } from 'viem/chains' import { LendProviderMorpho } from '@/lend/index.js' import { ChainManager } from '@/services/ChainManager.js' @@ -30,8 +30,8 @@ export class Verbs implements VerbsInterface { this._chainManager = new ChainManager( config.chains || [ { - chainId: unichain.id, - rpcUrl: unichain.rpcUrls.default.http[0], + chainId: baseSepolia.id, + rpcUrl: baseSepolia.rpcUrls.default.http[0], }, ], ) @@ -39,16 +39,21 @@ export class Verbs implements VerbsInterface { if (config.lend) { // TODO: delete this code and just have the lend use the ChainManager const configChain = config.chains?.[0] - const chainId = configChain?.chainId || 130 // Default to Unichain - const chain = chainId === 130 ? unichain : mainnet + const chainId = configChain?.chainId || 84532 // Default to Base Sepolia + const chain = + chainId === 130 ? unichain : chainId === 84532 ? baseSepolia : mainnet const publicClient = createPublicClient({ chain, transport: http( - configChain?.rpcUrl || unichain.rpcUrls.default.http[0], + configChain?.rpcUrl || baseSepolia.rpcUrls.default.http[0], ), }) as PublicClient if (config.lend.type === 'morpho') { - this.lendProvider = new LendProviderMorpho(config.lend, publicClient) + this.lendProvider = new LendProviderMorpho( + config.lend, + publicClient, + this._chainManager, + ) } else { throw new Error( `Unsupported lending provider type: ${config.lend.type}`, diff --git a/packages/sdk/src/wallet/index.ts b/packages/sdk/src/wallet/index.ts index 96e0a715..d4a7500b 100644 --- a/packages/sdk/src/wallet/index.ts +++ b/packages/sdk/src/wallet/index.ts @@ -1,5 +1,4 @@ import { type Address, encodeFunctionData, erc20Abi, type Hash } from 'viem' -import { unichain } from 'viem/chains' import { fetchERC20Balance, fetchETHBalance } from '@/services/tokenBalance.js' import { SUPPORTED_TOKENS } from '@/supported/tokens.js' @@ -90,11 +89,11 @@ export class Wallet implements WalletInterface { } // Parse human-readable inputs - // TODO: Get actual chain ID from wallet context, for now using Unichain + // TODO: Get actual chain ID from wallet context, for now using Base Sepolia const { amount: parsedAmount, asset: resolvedAsset } = parseLendParams( amount, asset, - unichain.id, + 84532, // Base Sepolia ) // Set receiver to wallet address if not specified @@ -125,11 +124,11 @@ export class Wallet implements WalletInterface { throw new Error('Wallet not initialized') } - if (!this.walletProvider || !this.walletProvider.sign) { - throw new Error('Wallet provider does not support transaction signing') + if (!this.walletProvider || !(this.walletProvider as any).signAndSend) { + throw new Error('Wallet provider does not support signAndSend') } - return this.walletProvider.sign(this.id, transactionData) + return (this.walletProvider as any).signAndSend(this.id, transactionData) } /** @@ -205,8 +204,8 @@ export class Wallet implements WalletInterface { throw new Error('Amount must be greater than 0') } - // TODO: Get actual chain ID from wallet context, for now using Unichain - const chainId = unichain.id + // TODO: Get actual chain ID from wallet context, for now using Base Sepolia + const chainId = 84532 // Base Sepolia // Handle ETH transfers if (asset.toLowerCase() === 'eth') { diff --git a/packages/sdk/src/wallet/providers/privy.ts b/packages/sdk/src/wallet/providers/privy.ts index 124699f3..2355f5f4 100644 --- a/packages/sdk/src/wallet/providers/privy.ts +++ b/packages/sdk/src/wallet/providers/privy.ts @@ -103,12 +103,12 @@ export class WalletProviderPrivy implements WalletProvider { try { const response = await this.privy.walletApi.ethereum.sendTransaction({ walletId, - caip2: 'eip155:130', // Unichain + caip2: 'eip155:84532', // Base Sepolia transaction: { to: transactionData.to, data: transactionData.data as `0x${string}`, value: Number(transactionData.value), - chainId: 130, // Unichain + chainId: 84532, // Base Sepolia }, }) @@ -122,6 +122,64 @@ export class WalletProviderPrivy implements WalletProvider { } } + /** + * Sign and send a transaction with gas sponsorship support + * @description Signs and sends a transaction using Privy's RPC API with gas sponsorship + * @param walletId - Wallet identifier + * @param transactionData - Transaction data to sign and send + * @returns Promise resolving to transaction hash + * @throws Error if transaction signing fails + */ + async signAndSend( + walletId: string, + transactionData: TransactionData, + ): Promise { + try { + // Try using the rpc method directly for gas sponsorship + const rpcPayload = { + method: 'eth_sendTransaction', + caip2: 'eip155:84532', + params: { + transaction: { + to: transactionData.to, + data: transactionData.data, + value: transactionData.value, + }, + }, + sponsor: true, + } + + // Check if rpc method exists on walletApi for gas sponsorship + if ('rpc' in this.privy.walletApi && typeof this.privy.walletApi.rpc === 'function') { + const response = await (this.privy.walletApi as any).rpc({ + walletId, + ...rpcPayload, + }) + return response.data.hash as Hash + } + + // Fallback to regular sendTransaction if rpc method not available + const response = await this.privy.walletApi.ethereum.sendTransaction({ + walletId, + caip2: 'eip155:84532', // Base Sepolia + transaction: { + to: transactionData.to, + data: transactionData.data as `0x${string}`, + value: Number(transactionData.value), + chainId: 84532, // Base Sepolia + }, + }) + + return response.hash as Hash + } catch (error) { + throw new Error( + `Failed to sign and send transaction for wallet ${walletId}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } + } + /** * Sign a transaction without sending it * @description Signs a transaction using Privy's wallet API but doesn't send it @@ -142,7 +200,7 @@ export class WalletProviderPrivy implements WalletProvider { } // Get public client for gas estimation - const publicClient = this.verbs.chainManager.getPublicClient(130) // Unichain + const publicClient = this.verbs.chainManager.getPublicClient(84532) // Base Sepolia // Estimate gas limit const gasLimit = await publicClient.estimateGas({ @@ -166,7 +224,7 @@ export class WalletProviderPrivy implements WalletProvider { to: transactionData.to, data: transactionData.data as `0x${string}`, value: transactionData.value as `0x${string}`, - chainId: 130, // Unichain + chainId: 84532, // Base Sepolia type: 2, // EIP-1559 gasLimit: `0x${gasLimit.toString(16)}`, maxFeePerGas: `0x${(feeData.maxFeePerGas || BigInt(1000000000)).toString(16)}`, // fallback to 1 gwei @@ -174,9 +232,6 @@ export class WalletProviderPrivy implements WalletProvider { nonce: `0x${nonce.toString(16)}`, // Explicitly provide the correct nonce } - console.log( - `[PRIVY_PROVIDER] Complete tx params - Type: ${txParams.type}, Nonce: ${nonce}, Limit: ${gasLimit}, MaxFee: ${feeData.maxFeePerGas || 'fallback'}, Priority: ${feeData.maxPriorityFeePerGas || 'fallback'}`, - ) const response = await this.privy.walletApi.ethereum.signTransaction({ walletId,